Skip to content

Commit 6f5e19f

Browse files
committed
[IMP] rewrite translatable strings to use named placeholders
Refactor translatable strings to use named placeholders instead of positional ones. This improves readability, reduces the risk of translation errors, and makes translations easier to maintain and reorder across languages.
1 parent 38e7a02 commit 6f5e19f

8 files changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
2+
import re
3+
from odoo_module_migrate.base_migration_script import BaseMigrationScript
4+
5+
6+
def multi_value_translation_replacement_function(match, single_quote=True):
7+
format_string = match.group(1)
8+
dictionary_entries = match.group(2)
9+
10+
formatted_entries = []
11+
for entry in dictionary_entries.split(","):
12+
if ":" in entry:
13+
[key, value] = entry.split(":")
14+
formatted_entries.append(
15+
"{}={}".format(key.strip().strip("'").strip('"'), value.strip())
16+
)
17+
18+
formatted_entries = ", ".join(formatted_entries)
19+
20+
if single_quote:
21+
return f"_('{format_string}', {formatted_entries})"
22+
return f'_("{format_string}", {formatted_entries})'
23+
24+
25+
def format_parenthesis(match):
26+
format_string = match.group(1)
27+
dictionary_entries = match.group(2)
28+
29+
if dictionary_entries.endswith(","):
30+
dictionary_entries = dictionary_entries[:-1]
31+
32+
return f"_({format_string}, {dictionary_entries})"
33+
34+
35+
def format_replacement_function(match, single_quote=True):
36+
format_string = re.sub(r"\{\d*\}", "%s", match.group(1))
37+
format_string = re.sub(r"{(\w+)}", r"%(\1)s", format_string)
38+
arguments = " ".join(match.group(2).split())
39+
40+
if arguments.endswith(","):
41+
arguments = arguments[:-1]
42+
43+
if single_quote:
44+
return f"_('{format_string}', {arguments})"
45+
return f'_("{format_string}", {arguments})'
46+
47+
48+
def replace_translation_function(
49+
logger, module_path, module_name, manifest_path, migration_steps, tools
50+
):
51+
files_to_process = tools.get_files(module_path, (".py",))
52+
53+
replaces = {
54+
r'_\(\s*"([^"]+)"\s*\)\s*%\s*\{([^}]+)\}': lambda match: multi_value_translation_replacement_function(
55+
match, single_quote=False
56+
),
57+
r"_\(\s*'([^']+)'\s*\)\s*%\s*\{([^}]+)\}": lambda match: multi_value_translation_replacement_function(
58+
match, single_quote=True
59+
),
60+
r'_\(\s*(["\'].*?%[ds].*?["\'])\s*\)\s*%\s*\(\s*(.+)\s*\)': format_parenthesis,
61+
r'_\(\s*(["\'].*?%[ds].*?["\'])\s*\)\s*?%\s*?([^\s]+)': r"_(\1, \2)",
62+
r'_\(\s*"([^"]*)"\s*\)\.format\(\s*(\s*[^)]+)\)': lambda match: format_replacement_function(
63+
match, single_quote=False
64+
),
65+
r"_\(\s*'([^']*)'\s*\)\.format\(\s*(\s*[^)]+)\)": lambda match: format_replacement_function(
66+
match, single_quote=True
67+
),
68+
}
69+
70+
for file in files_to_process:
71+
try:
72+
tools._replace_in_file(
73+
file,
74+
replaces,
75+
log_message=f"""Improve _() function: {file}""",
76+
)
77+
except Exception as e:
78+
logger.error(f"Error processing file {file}: {str(e)}")
79+
80+
81+
class MigrationScript(BaseMigrationScript):
82+
83+
_GLOBAL_FUNCTIONS = [replace_translation_function]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.py:
2+
_\(\s*["\'].*?%s.*?["\']: "[Warning] In some languages the order of the placeholders may have to be modified, which is impossible if they are unnamed. We can use named placedholders to avoid this"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import res_partner
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from odoo import _, models
4+
from odoo.exceptions import ValidationError
5+
6+
7+
class ResPartner(models.Model):
8+
_inherit = "res.partner"
9+
10+
def test_improve_translation(self):
11+
return _("It's %s", 2024)
12+
13+
def test_improve_transalation_with_line_break(self):
14+
raise ValidationError(
15+
_("The '%s' is empty or 0. It should have a non-null value.", "Price")
16+
)
17+
18+
def test_improve_translation_with_parenthesis(self):
19+
return _("It's %s", 2024)
20+
21+
def test_improve_translation_with_single_quote(self):
22+
return _('It is %s', 2024)
23+
24+
def test_improve_translation_with_parenthesis_and_line_break(self):
25+
raise ValidationError(
26+
_("%s are only valid until %s", "User", 2024)
27+
)
28+
29+
def test_improve_translation_with_brackets(self):
30+
return _("User %(name)s has %(items)s items", name="Dev", items=5)
31+
32+
def test_improve_translation_with_single_quote(self):
33+
return _('User %(name)s has %(items)s items', name='Dev', items=5)
34+
35+
def test_improve_translation_with_brackets_and_line_break(self):
36+
return _("User %(name)s has %(items)s items", name="Dev", items=5)
37+
38+
def test_improve_translation_with_format(self):
39+
return _("It's %s", 2024)
40+
41+
def test_improve_translation_with_format_and_single_quote(self):
42+
return _('It is %s', 2024)
43+
44+
def test_improve_translation_with_format_and_line_break(self):
45+
return _("It's %s", 2024)
46+
47+
def test_improve_translation_with_format_has_end_comma(self):
48+
return _("It's %s", 2024)
49+
50+
def test_improve_translation_with_format_multi_params(self):
51+
return _("User %(name)s has %(items)s items", name="Dev", items=5)
52+
53+
def test_improve_translation_with_format_multi_params_and_line_break(self):
54+
return _("User %(name)s has %(items)s items", name="Dev", items=5)
55+
56+
def test_improve_translation_with_format_multi_params_has_end_comma(self):
57+
return _('User %(name)s has "acb" %(items)s items', name="Dev", items=5)
58+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import res_partner
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from odoo import _, models
4+
from odoo.exceptions import ValidationError
5+
6+
7+
class ResPartner(models.Model):
8+
_inherit = "res.partner"
9+
10+
def test_improve_translation(self):
11+
return _("It's %s") % 2024
12+
13+
def test_improve_transalation_with_line_break(self):
14+
raise ValidationError(
15+
_("The '%s' is empty or 0. It should have a non-null value.")
16+
% "Price"
17+
)
18+
19+
def test_improve_translation_with_parenthesis(self):
20+
return _("It's %s") % (2024)
21+
22+
def test_improve_translation_with_single_quote(self):
23+
return _('It is %s') % (2024)
24+
25+
def test_improve_translation_with_parenthesis_and_line_break(self):
26+
raise ValidationError(
27+
_("%s are only valid until %s") % (
28+
"User", 2024,
29+
)
30+
)
31+
32+
def test_improve_translation_with_brackets(self):
33+
return _("User %(name)s has %(items)s items") % {"name":"Dev", "items": 5}
34+
35+
def test_improve_translation_with_single_quote(self):
36+
return _('User %(name)s has %(items)s items') % {'name':'Dev', 'items': 5}
37+
38+
def test_improve_translation_with_brackets_and_line_break(self):
39+
return _(
40+
"User %(name)s has %(items)s items"
41+
) % {
42+
"name": "Dev",
43+
"items": 5,
44+
}
45+
46+
def test_improve_translation_with_format(self):
47+
return _("It's {}").format(2024)
48+
49+
def test_improve_translation_with_format_and_single_quote(self):
50+
return _('It is {}').format(2024)
51+
52+
def test_improve_translation_with_format_and_line_break(self):
53+
return _(
54+
"It's {}"
55+
).format(
56+
2024
57+
)
58+
59+
def test_improve_translation_with_format_has_end_comma(self):
60+
return _("It's {}").format(2024,)
61+
62+
def test_improve_translation_with_format_multi_params(self):
63+
return _("User {name} has {items} items").format(name="Dev", items=5)
64+
65+
def test_improve_translation_with_format_multi_params_and_line_break(self):
66+
return _(
67+
"User {name} has {items} items"
68+
).format(
69+
name="Dev",
70+
items=5
71+
)
72+
73+
def test_improve_translation_with_format_multi_params_has_end_comma(self):
74+
return _('User {name} has "acb" {items} items').format(name="Dev", items=5,)
75+

0 commit comments

Comments
 (0)