From 0bc3ae0ed58c030d32d094a3549b2c56fb1ddb6b Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Tue, 27 Dec 2022 09:02:04 +0000 Subject: [PATCH] [FIX] base: ignore spaces around text content when matching translation When the same text appears several times inside a translated field but nested in different HTMLs, the matching for each one is done independently if various spacing appear in the HTML. This commit strips the spaces around the matched texts so that texts that are synchronized on purpose do not become desynchronized. Doing this leads to collisions on the keys of `text2term`, it therefore also has to replace it with a dictionary of text to list of terms. opw-3098819 closes odoo/odoo#109798 X-original-commit: 6cec590a2dad43063d3bb747838393a8df13aa04 Signed-off-by: Benoit Socias (bso) --- odoo/addons/base/tests/test_translate.py | 51 ++++++++++++++++++++++++ odoo/fields.py | 9 +++-- odoo/tools/translate.py | 3 +- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index 6a652d17d70..05a4723bb90 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -874,6 +874,57 @@ class TestXMLTranslation(TransactionCase): self.assertEqual(view.with_env(env_fr).arch_db, archf % (terms_en[0], terms_fr[1])) self.assertEqual(view.with_env(env_nl).arch_db, archf % (terms_en[0], terms_nl[1])) + def test_sync_xml_collision(self): + """ Check translations of 'arch' after xml tags changes in source terms + when the same term appears in different elements with different + styles. + """ + archf = '''
+ %s +
+ +
+
+
+

%s

+
+
+
''' + terms_en = ('Bread and cheese', 'Knive and Fork', 'Knive and Fork') + terms_fr = ('Pain et fromage', 'Couteau et Fourchette', 'Couteau et Fourchette') + terms_nl = ('Brood and kaas', 'Mes en Vork', 'Mes en Vork') + view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl) + + env_nolang = self.env(context={}) + env_en = self.env(context={'lang': 'en_US'}) + env_fr = self.env(context={'lang': 'fr_FR'}) + env_nl = self.env(context={'lang': 'nl_NL'}) + + self.assertEqual(view.with_env(env_nolang).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) + + # modify source term in view (small change) + terms_en = ('Bread and cheese', 'Knife and Fork', 'Knife and Fork') + view.with_env(env_en).write({'arch_db': archf % terms_en}) + + # check whether translations have been kept + self.assertEqual(view.with_env(env_nolang).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) + + # modify source term in view (actual text change) + terms_en = ('Bread and cheese', 'Fork and Knife', 'Fork and Knife') + view.with_env(env_en).write({'arch_db': archf % terms_en}) + + # check whether translations have been reset + self.assertEqual(view.with_env(env_nolang).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch_db, archf % (terms_fr[0], terms_en[1], terms_en[2])) + self.assertEqual(view.with_env(env_nl).arch_db, archf % (terms_nl[0], terms_en[1], terms_en[2])) + def test_cache_consistency(self): view = self.env["ir.ui.view"].create({ "name": "test_translate_xml_cache_invalidation", diff --git a/odoo/fields.py b/odoo/fields.py index cf522bdae31..2e03ac87312 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1801,14 +1801,17 @@ class _String(Field): continue from_lang_value = old_translations.get(lang, old_translations.get('en_US')) translation_dictionary = self.get_translation_dictionary(from_lang_value, old_translations) - text2term = {self.get_text_content(term): term for term in new_terms} + text2terms = defaultdict(list) + for term in new_terms: + text2terms[self.get_text_content(term)].append(term) for old_term in list(translation_dictionary.keys()): if old_term not in new_terms: old_term_text = self.get_text_content(old_term) - matches = get_close_matches(old_term_text, text2term, 1, 0.9) + matches = get_close_matches(old_term_text, text2terms, 1, 0.9) if matches: - translation_dictionary[text2term[matches[0]]] = translation_dictionary.pop(old_term) + closest_term = get_close_matches(old_term, text2terms[matches[0]], 1, 0)[0] + translation_dictionary[closest_term] = translation_dictionary.pop(old_term) # pylint: disable=not-callable new_translations = { l: self.translate(lambda term: translation_dictionary.get(term, {l: None})[l], cache_value) diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index afcad285d07..2fee48869d8 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -358,7 +358,8 @@ def html_term_converter(value): def get_text_content(term): """ Return the textual content of the given term. """ - return html.fromstring(term).text_content() + content = html.fromstring(term).text_content() + return " ".join(content.split()) xml_translate.get_text_content = get_text_content html_translate.get_text_content = get_text_content