[IMP] core: void model translation

Before this commit, there is no way to "void" model translations, i.e.,
discard a translation and let the value fall back on the 'en_US' value.
The API of methods write() and update_field_translations() can only
overwrite the translations for the specified languages.

After this commit, calling update_field_translations() with a falsy
value except the empty string discards the corresponding translation
value, and let the value of the field in the given language fall back on
the 'en_US' value of the field.

X-original-commit: 5434fb845c393327db377abf872c448f4860a7d3
Part-of: odoo/odoo#111869
This commit is contained in:
Chong Wang (cwg)
2023-02-03 16:32:34 +01:00
parent 9e997c7f07
commit 4e282f10fc
4 changed files with 83 additions and 32 deletions
@@ -74,10 +74,10 @@ export class TranslationDialog extends Component {
if (!translations[term.lang]) {
translations[term.lang] = {};
}
const source = term.value ? term.value : term.source;
translations[term.lang][source] = updatedTermValue;
const oldTermValue = term.value ? term.value : term.source;
translations[term.lang][oldTermValue] = updatedTermValue || term.source;
} else {
translations[term.lang] = updatedTermValue;
translations[term.lang] = updatedTermValue || false;
}
}
});
@@ -319,9 +319,9 @@ QUnit.module("Fields", (hooks) => {
if (call_get_field_translations === 1) {
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "english value" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
{ lang: "es_ES", source: "yop", value: "" },
{ lang: "en_US", source: "english value", value: "english value" },
{ lang: "fr_BE", source: "english value", value: "valeur français" },
{ lang: "es_ES", source: "english value", value: "english value" },
],
{ translation_type: "char", translation_show_source: false },
]);
@@ -330,8 +330,8 @@ QUnit.module("Fields", (hooks) => {
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(
args[2],
{ en_US: "english value", es_ES: "" },
"the new translation value should be written"
{ en_US: "english value", es_ES: false },
"the new translation value should be written and the value false voids the translation"
);
return Promise.resolve(null);
}
@@ -401,8 +401,8 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
translations[2].value,
"",
"Spanish translation should be an empty string"
"english value",
"Spanish translation should fallback to the English translation"
);
});
+38 -1
View File
@@ -8,7 +8,7 @@ from psycopg2 import IntegrityError
from psycopg2.extras import Json
import io
from odoo.exceptions import AccessError, ValidationError
from odoo.exceptions import UserError
from odoo.tools import sql
from odoo.tools.translate import quote, unquote, xml_translate, html_translate, TranslationImporter
from odoo.tests.common import TransactionCase, BaseCase, new_test_user, tagged
@@ -687,6 +687,43 @@ class TestTranslationWrite(TransactionCase):
self.assertEqual(group.with_context(lang='fr_FR').comment, 'French comment 2')
self.assertEqual(group.with_context(lang='en_US').comment, 'French comment')
def test_update_field_translations(self):
self.env['res.lang']._activate_lang('fr_FR')
categoryEN = self.category.with_context(lang='en_US')
categoryFR = self.category.with_context(lang='fr_FR')
self.category.update_field_translations('name', {'en_US': 'English Name', 'fr_FR': 'French Name'})
self.assertEqual(categoryEN.name, 'English Name')
self.assertEqual(categoryFR.name, 'French Name')
# void fr_FR translation and fallback to en_US
self.category.update_field_translations('name', {'fr_FR': False})
self.assertEqual(categoryEN.name, 'English Name')
self.assertEqual(categoryFR.name, 'English Name')
categoryEN.name = 'English Name 2'
self.assertEqual(categoryEN.name, 'English Name 2')
self.assertEqual(categoryFR.name, 'English Name 2')
# cannot void en_US
self.category.update_field_translations('name', {'en_US': 'English Name', 'fr_FR': 'French Name'})
self.category.update_field_translations('name', {'en_US': False})
self.assertEqual(categoryEN.name, 'English Name')
self.assertEqual(categoryFR.name, 'French Name')
# empty str is a valid translation
self.category.update_field_translations('name', {'fr_FR': ''})
self.assertEqual(categoryEN.name, 'English Name')
self.assertEqual(categoryFR.name, '')
self.category.update_field_translations('name', {'en_US': '', 'fr_FR': 'French Name'})
self.assertEqual(categoryEN.name, '')
self.assertEqual(categoryFR.name, 'French Name')
# raise error when the translations are in the form for model_terms translated fields
with self.assertRaises(UserError):
self.category.update_field_translations('name', {'fr_FR': {'English Name': 'French Name'}})
def test_field_selection(self):
""" Test translations of field selections. """
self.env['res.lang']._activate_lang('fr_FR')
+35 -21
View File
@@ -3001,10 +3001,13 @@ class BaseModel(metaclass=MetaModel):
The main difference comes from the extra function ``digest``, which may
be used to make identifiers for old terms.
:param dict translations: if the field has ``translate=True``, it should be a dictionary
like ``{lang: new_value}``; if ``translate`` is a callable, it should be like
``{lang: {old_term: new_term}}``, or ``{lang: {digest(old_term): new_term}}`` when
``digest`` is a callable
:param dict translations:
if the field has ``translate=True``, it should be a dictionary like ``{lang: new_value}``
new_value: str: the new translation for lang
new_value: False: void the current translation for lang and fallback to current en_US value
if ``translate`` is a callable, it should be like
``{lang: {old_term: new_term}}``, or ``{lang: {digest(old_term): new_term}}`` when ``digest`` is callable
new_value: str: the new translation of old_term for lang
:param digest: an optional digest function for the old_term
"""
self.ensure_one()
@@ -3018,25 +3021,36 @@ class BaseModel(metaclass=MetaModel):
# a non-related non-stored computed field cannot be translated, even if it has inverse function
return False
# Strictly speaking, a translated related/computed field cannot be stored
# because the compute function only support one language
# `not field.store` is a redundant logic.
# But some developers store translated related fields.
# In these cases, only all translations of the first stored translation field will be updated
# For other stored related translated field, the translation for the flush language will be updated
if field.related and not field.store:
related_path, field_name = field.related.rsplit(".", 1)
return self.mapped(related_path)._update_field_translations(field_name, translations, digest)
self.check_access_rights('write')
self.check_field_access_rights('write', [field_name])
self.check_access_rule('write')
if field.translate is True:
for lang, translation in translations.items():
if translation is not None:
self.with_context(lang=lang)[field_name] = translation
# falsy values (except emtpy str) are used to void the corresponding translation
if any(translation and not isinstance(translation, str) for translation in translations.values()):
raise UserError(_("Translations for model translated fields only accept falsy values and str"))
value_en = translations.get('en_US', True)
if not value_en and value_en != '':
translations.pop('en_US')
translations = {
lang: translation if isinstance(translation, str) else None
for lang, translation in translations.items()
}
self.invalidate_recordset([field_name])
self._cr.execute(f'''
UPDATE {self._table} SET {field_name} = jsonb_strip_nulls({field_name} || %s) WHERE id = %s
''', (Json(translations), self.id))
self.modified([field_name])
else:
# Strictly speaking, a translated related/computed field cannot be stored
# because the compute function only support one language
# `not field.store` is a redundant logic.
# But some developers store translated related fields.
# In these cases, only all translations of the first stored translation field will be updated
# For other stored related translated field, the translation for the flush language will be updated
if field.related and not field.store:
related_path, field_name = field.related.rsplit(".", 1)
return self.mapped(related_path)._update_field_translations(field_name, translations, digest)
self.check_access_rights('write')
self.check_field_access_rights('write', [field_name])
self.check_access_rule('write')
# Note:
# update terms in 'en_US' will not change its value other translated values
# record_en = Model_en.create({'html': '<div>English 1</div><div>English 2<div/>'