Skip to content

Commit

Permalink
[ADD] prefer-env-translation: Add new check for odoo v18.0 (#516)
Browse files Browse the repository at this point in the history
Related to odoo/odoo#174844

The new way to translate is using `self.env._()` instead of `_` method

But if there is not environment available in the code you can use LazyTranslate

```python
from odoo. tools import LazyTranslate
_lt = LazyTranslate(__name__)
```

Co-authored-by: trisdoan <[email protected]>
  • Loading branch information
moylop260 and trisdoan authored Jan 14, 2025
1 parent a6db757 commit 29c136b
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 68 deletions.
118 changes: 67 additions & 51 deletions README.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/pylint_odoo/checkers/custom_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from astroid import builder, exceptions as astroid_exceptions, nodes
from pylint.checkers import logging

from .. import misc
from .odoo_addons import OdooAddons
from .odoo_base_checker import OdooBaseChecker

Expand Down Expand Up @@ -67,9 +68,9 @@ def add_message(self, msgid, *args, **kwargs):
return super().add_message(msgid, *args, **kwargs)

def visit_call(self, node):
if not isinstance(node.func, nodes.Name):
name = OdooAddons.get_func_name(node.func)
if name not in misc.TRANSLATION_METHODS:
return
name = node.func.name
with config_logging_modules(self.linter, ("odoo",)):
self._check_log_method(node, name)

Expand Down
17 changes: 14 additions & 3 deletions src/pylint_odoo/checkers/odoo_addons.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@
"deprecated-odoo-model-method",
CHECK_DESCRIPTION,
),
"W8161": (
"Better using self.env._ More info at https://github.com/odoo/odoo/pull/174844",
"prefer-env-translation",
CHECK_DESCRIPTION,
),
}

DFTL_MANIFEST_REQUIRED_KEYS = ["license"]
Expand Down Expand Up @@ -569,6 +574,7 @@ class OdooAddons(OdooBaseChecker, BaseChecker):
"odoo_maxversion": "13.0",
},
"no-raise-unlink": {"odoo_minversion": "15.0"},
"prefer-env-translation": {"odoo_minversion": "18.0"},
}

def __init__(self, linter: PyLinter):
Expand Down Expand Up @@ -789,6 +795,7 @@ def _get_assignation_nodes(self, node):
"method-inverse",
"method-search",
"no-write-in-compute",
"prefer-env-translation",
"print-used",
"renamed-field-parameter",
"sql-injection",
Expand Down Expand Up @@ -870,8 +877,7 @@ def visit_call(self, node):
self.odoo_computes.add(method_name)
if (
isinstance(argument_aux, nodes.Call)
and isinstance(argument_aux.func, nodes.Name)
and argument_aux.func.name == "_"
and self.get_func_name(argument_aux.func) in misc.TRANSLATION_METHODS
):
self.add_message("translation-field", node=argument_aux)
index += 1
Expand Down Expand Up @@ -942,7 +948,12 @@ def visit_call(self, node):
self.add_message("translation-required", node=node, args=("message_post", keyword, as_string))

# Call _(...) with variables into the term to be translated
if isinstance(node.func, nodes.Name) and node.func.name == "_" and node.args:
if self.get_func_name(node.func) in misc.TRANSLATION_METHODS and node.args:
# "_" -> isinstance(node.func, nodes.Name)
# "self.env._" -> isinstance(node.func, nodes.Attribute)
if isinstance(node.func, nodes.Name) and node.func.as_string() in misc.TRANSLATION_METHODS:
self.add_message("prefer-env-translation", node=node)

wrong = ""
right = ""
arg = node.args[0]
Expand Down
1 change: 1 addition & 0 deletions src/pylint_odoo/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"18.0",
]
DFTL_MANIFEST_VERSION_FORMAT = r"({valid_odoo_versions})\.\d+\.\d+\.\d+$"
TRANSLATION_METHODS = ("_", "_lt")


class StringParseError(TypeError):
Expand Down
195 changes: 195 additions & 0 deletions testing/resources/test_repo/broken_module/models/broken_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
from odoo import fields, models, _
from odoo.exceptions import UserError
from odoo import exceptions
from odoo.tools.translate import LazyTranslate


# Relatives import for odoo addons
from odoo.addons.broken_module import broken_model as broken_model1
Expand All @@ -61,6 +63,7 @@
import itertools
from itertools import groupby

_lt = LazyTranslate(__name__)
other_field = fields.Char()


Expand Down Expand Up @@ -402,6 +405,134 @@ def my_method1(self, variable1):
self.message_post(_('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method11(self, variable1):
# Shouldn't show error of field-argument-translate
self.my_method2(self.env._('hello world'))

# Message post with new translation function
self.message_post(subject=self.env._('Subject translatable'),
body=self.env._('Body translatable'))
self.message_post(self.env._('Body translatable'),
self.env._('Subject translatable'))
self.message_post(self.env._('Body translatable'),
subject=self.env._('Subject translatable'))
self.message_post(self.env._('A CDR has been recovered for %s') % (variable1,))
self.message_post(self.env._('A CDR has been recovered for %s') % variable1)
self.message_post(self.env._('Var {a}').format(a=variable1))
self.message_post(self.env._('Var %(variable)s') % {'variable': variable1})
self.message_post(subject=self.env._('Subject translatable'),
body=self.env._('Body translatable %s') % variable1)
self.message_post(subject=self.env._('Subject translatable %(variable)s') %
{'variable': variable1},
message_type='notification')
self.message_post(self.env._('Body translatable'),
self.env._('Subject translatable {a}').format(a=variable1))
self.message_post(self.env._('Body translatable %s') % variable1,
self.env._('Subject translatable %(variable)s') %
{'variable': variable1})
self.message_post('<p>%s</p>' % self.env._('Body translatable'))
self.message_post(body='<p>%s</p>' % self.env._('Body translatable'))

# translation new function with variables in the term
variable2 = variable1
self.message_post(self.env._('Variable not translatable: %s' % variable1))
self.message_post(self.env._('Variables not translatable: %s, %s' % (
variable1, variable2)))
self.message_post(body=self.env._('Variable not translatable: %s' % variable1))
self.message_post(body=self.env._('Variables not translatable: %s %s' % (
variable1, variable2)))
error_msg = self.env._('Variable not translatable: %s' % variable1)
error_msg = self.env._('Variables not translatable: %s, %s' % (
variable1, variable2))
error_msg = self.env._('Variable not translatable: {}'.format(variable1))
error_msg = self.env._('Variables not translatable: {}, {variable2}'.format(
variable1, variable2=variable2))

# string with parameters without name
# so you can't change the order in the translation
self.env._('%s %d') % ('hello', 3)
self.env._('%s %s') % ('hello', 'world')
self.env._('{} {}').format('hello', 3)
self.env._('{} {}').format('hello', 'world')

# Valid cases
self.env._('%(strname)s') % {'strname': 'hello'}
self.env._('%(strname)s %(intname)d') % {'strname': 'hello', 'intname': 3}
self.env._('%s') % 'hello'
self.env._('%d') % 3
self.env._('{}').format('hello')
self.env._('{}').format(3)

# It raised exception but it was already fixed
msg = "Invalid not _ method %s".lstrip() % "value"
# It should emit message but binop.left is showing "lstrip" only instead of "_"
self.message_post(self.env._('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method111(self, variable1):
# Shouldn't show error of field-argument-translate
self.my_method2(_lt('hello world'))

# Message post with new translation function
self.message_post(subject=_lt('Subject translatable'),
body=_lt('Body translatable'))
self.message_post(_lt('Body translatable'),
_lt('Subject translatable'))
self.message_post(_lt('Body translatable'),
subject=_lt('Subject translatable'))
self.message_post(_lt('A CDR has been recovered for %s') % (variable1,))
self.message_post(_lt('A CDR has been recovered for %s') % variable1)
self.message_post(_lt('Var {a}').format(a=variable1))
self.message_post(_lt('Var %(variable)s') % {'variable': variable1})
self.message_post(subject=_lt('Subject translatable'),
body=_lt('Body translatable %s') % variable1)
self.message_post(subject=_lt('Subject translatable %(variable)s') %
{'variable': variable1},
message_type='notification')
self.message_post(_lt('Body translatable'),
_lt('Subject translatable {a}').format(a=variable1))
self.message_post(_lt('Body translatable %s') % variable1,
_lt('Subject translatable %(variable)s') %
{'variable': variable1})
self.message_post('<p>%s</p>' % _lt('Body translatable'))
self.message_post(body='<p>%s</p>' % _lt('Body translatable'))

# translation new function with variables in the term
variable2 = variable1
self.message_post(_lt('Variable not translatable: %s' % variable1))
self.message_post(_lt('Variables not translatable: %s, %s' % (
variable1, variable2)))
self.message_post(body=_lt('Variable not translatable: %s' % variable1))
self.message_post(body=_lt('Variables not translatable: %s %s' % (
variable1, variable2)))
error_msg = _lt('Variable not translatable: %s' % variable1)
error_msg = _lt('Variables not translatable: %s, %s' % (
variable1, variable2))
error_msg = _lt('Variable not translatable: {}'.format(variable1))
error_msg = _lt('Variables not translatable: {}, {variable2}'.format(
variable1, variable2=variable2))

# string with parameters without name
# so you can't change the order in the translation
_lt('%s %d') % ('hello', 3)
_lt('%s %s') % ('hello', 'world')
_lt('{} {}').format('hello', 3)
_lt('{} {}').format('hello', 'world')

# Valid cases
_lt('%(strname)s') % {'strname': 'hello'}
_lt('%(strname)s %(intname)d') % {'strname': 'hello', 'intname': 3}
_lt('%s') % 'hello'
_lt('%d') % 3
_lt('{}').format('hello')
_lt('{}').format(3)

# It raised exception but it was already fixed
msg = "Invalid not _ method %s".lstrip() % "value"
# It should emit message but binop.left is showing "lstrip" only instead of "_"
self.message_post(_lt('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method2(self, variable2):
return variable2

Expand Down Expand Up @@ -432,6 +563,18 @@ def my_method7(self):
# Method with translation
raise UserError(_('String with translation'))

def my_method71(self):
user_id = 1
if user_id != 99:
# Method with translation
raise UserError(self.env._('String with translation'))

def my_method72(self):
user_id = 1
if user_id != 99:
# Method with translation
raise UserError(_lt('String with translation'))

def my_method8(self):
user_id = 1
if user_id != 99:
Expand Down Expand Up @@ -476,6 +619,28 @@ def my_method13(self):
raise exceptions.Warning(_(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method131(self):
# Shouldn't show error
raise exceptions.Warning(self.env._(
'String with params format {p1}').format(p1='v1'))
raise exceptions.Warning(self.env._(
'String with params format {p1}'.format(p1='v1')))
raise exceptions.Warning(self.env._(
'String with params format %(p1)s') % {'p1': 'v1'})
raise exceptions.Warning(self.env._(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method132(self):
# Shouldn't show error
raise exceptions.Warning(_lt(
'String with params format {p1}').format(p1='v1'))
raise exceptions.Warning(_lt(
'String with params format {p1}'.format(p1='v1')))
raise exceptions.Warning(_lt(
'String with params format %(p1)s') % {'p1': 'v1'})
raise exceptions.Warning(_lt(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method14(self):
_("String with missing args %s %s", "param1")
_("String with missing kwargs %(param1)s", param2="hola")
Expand All @@ -491,6 +656,36 @@ def my_method14(self):
_("String with correct args %s", "param1")
_("String with correct kwargs %(param1)s", param1="hola")

def my_method141(self):
self.env._("String with missing args %s %s", "param1")
self.env._("String with missing kwargs %(param1)s", param2="hola")
self.env._(f"String with f-interpolation {self.param1}")
self.env._("String unsupported character %y", "param1")
self.env._("format truncated %s%", 'param1')
self.env._("too many args %s", 'param1', 'param2')

self.env._("multi-positional args without placeholders %s %s", 'param1', 'param2')

self.env._("multi-positional args without placeholders {} {}".format('param1', 'param2'))

self.env._("String with correct args %s", "param1")
self.env._("String with correct kwargs %(param1)s", param1="hola")

def my_method142(self):
_lt("String with missing args %s %s", "param1")
_lt("String with missing kwargs %(param1)s", param2="hola")
_lt(f"String with f-interpolation {self.param1}")
_lt("String unsupported character %y", "param1")
_lt("format truncated %s%", 'param1')
_lt("too many args %s", 'param1', 'param2')

_lt("multi-positional args without placeholders %s %s", 'param1', 'param2')

_lt("multi-positional args without placeholders {} {}".format('param1', 'param2'))

_lt("String with correct args %s", "param1")
_lt("String with correct kwargs %(param1)s", param1="hola")

def old_api_method_alias(self, cursor, user, ids, context=None): # old api
pass

Expand Down
Loading

0 comments on commit 29c136b

Please sign in to comment.