[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:
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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/>'
|
||||
|
||||
Reference in New Issue
Block a user