# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from hashlib import sha256 from unittest.mock import patch import logging import time from psycopg2 import IntegrityError from psycopg2.extras import Json import io from odoo.exceptions import UserError from odoo.tools import sql from odoo.tools.translate import quote, unquote, xml_translate, html_translate, TranslationImporter, TranslationModuleReader from odoo.tests.common import TransactionCase, BaseCase, new_test_user, tagged _stats_logger = logging.getLogger('odoo.tests.stats') # a string with various unicode characters SPECIAL_CHARACTERS = " ¥®°²Æçéðπ⁉€∇⓵▲☑♂♥✓➔『にㄅ㊀中한︸🌈🌍👌😀" class TranslationToolsTestCase(BaseCase): def assertItemsEqual(self, a, b, msg=None): self.assertEqual(sorted(a), sorted(b), msg) def test_quote_unquote(self): def test_string(str): quoted = quote(str) #print "\n1:", repr(str) #print "2:", repr(quoted) unquoted = unquote("".join(quoted.split('"\n"'))) #print "3:", repr(unquoted) self.assertEqual(str, unquoted) test_string("""test \nall kinds\n \n o\r \\\\ nope\n\n" """) # The ones with 1+ backslashes directly followed by # a newline or literal N can fail... we would need a # state-machine parser to handle these, but this would # be much slower so it's better to avoid them at the moment self.assertRaises(AssertionError, quote, """test \nall kinds\n\no\r \\\\nope\n\n" """) def test_translate_xml_base(self): """ Test xml_translate() without formatting elements. """ terms = [] source = """
""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff', 'Blah blah blah', 'Put some more text here']) def test_translate_xml_text(self): """ Test xml_translate() on plain text. """ terms = [] source = "Blah blah blah" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, [source]) def test_translate_xml_unicode(self): """ Test xml_translate() on plain text with unicode characters. """ terms = [] source = u"Un heureux évènement" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, [source]) def test_translate_xml_text_entity(self): """ Test xml_translate() on plain text with HTML escaped entities. """ terms = [] source = "Blah blah blah" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, [source]) def test_translate_xml_inline1(self): """ Test xml_translate() with formatting elements. """ terms = [] source = """""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff', 'Blah blah blah', 'Put some more text here']) def test_translate_xml_inline2(self): """ Test xml_translate() with formatting elements embedding other elements. """ terms = [] source = """""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff', 'Blah blah blah', 'Put some more text here']) def test_translate_xml_inline3(self): """ Test xml_translate() with formatting elements without actual text. """ terms = [] source = """""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff', 'Blah blah blah']) def test_translate_xml_inline4(self): """ Test xml_translate() with inline elements with translated attrs only. """ terms = [] source = """""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff', '']) def test_translate_xml_inline5(self): """ Test xml_translate() with inline elements with empty translated attrs only. """ terms = [] source = """""" result = xml_translate(terms.append, source) self.assertEqual(result, source) self.assertItemsEqual(terms, ['Form stuff']) def test_translate_xml_t(self): """ Test xml_translate() with t-* attributes. """ terms = [] source = """A""" result = html_translate(lambda term: term, source) self.assertEqual(result, source) def test_translate_html_i(self): """ Test xml_translate() and html_translate() with elements. """ source = """B
C
A B
""" result = xml_translate(lambda term: term, source) self.assertEqual(result, """A B
""") result = html_translate(lambda term: term, source) self.assertEqual(result, source) class TestLanguageInstall(TransactionCase): def test_language_install(self): fr = self.env['res.lang'].with_context(active_test=False).search([('code', '=', 'fr_FR')]) self.assertTrue(fr) wizard = self.env['base.language.install'].create({'lang_ids': fr.ids}) self.env.flush_all() # running the wizard calls _load_module_terms() to load PO files loaded = [] def _load_module_terms(self, modules, langs, overwrite=False): loaded.append((modules, langs, overwrite)) with patch('odoo.addons.base.models.ir_module.Module._load_module_terms', _load_module_terms): wizard.lang_install() # _load_module_terms is called once with lang='fr_FR' and overwrite=True self.assertEqual(len(loaded), 1) self.assertEqual(loaded[0][1], ['fr_FR']) self.assertEqual(loaded[0][2], True) @tagged('post_install', '-at_install') class TestTranslationExport(TransactionCase): def test_export_translatable_resources(self): """Read files of installed modules and export translatable terms""" with self.assertNoLogs('odoo.tools.translate', "ERROR"): TranslationModuleReader(self.env.cr) class TestTranslation(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.env['res.lang']._activate_lang('fr_FR') cls.env.ref('base.module_base')._update_translations(['fr_FR']) cls.customers = cls.env['res.partner.category'].create({'name': 'Customers'}) cls.customers_xml_id = cls.customers.export_data(['id']).get('datas')[0][0] po_string = ''' #. module: __export__ #: model:res.partner.category,name:%s msgid "Customers" msgstr "Clients" ''' % cls.customers_xml_id with io.BytesIO(bytes(po_string, encoding='utf-8')) as f: f.name = 'dummy' translation_importer = TranslationImporter(cls.env.cr, verbose=True) translation_importer.load(f, 'po', 'fr_FR') translation_importer.save(overwrite=True) def test_101_create_translated_record(self): category = self.customers.with_context({}) self.assertEqual(category.name, 'Customers', "Error in basic name") category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients', "Translation not found") def test_102_duplicate_record(self): category = self.customers.with_context({'lang': 'fr_FR'}).copy() category_no = category.with_context({}) self.assertEqual(category_no.name, 'Customers', "Duplication should copy all translations") category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value") def test_103_duplicate_record_fr(self): category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'}) category_no = category.with_context({}) self.assertEqual(category_no.name, 'Clients (copie)', "Duplication should set untranslated value") category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value") def test_104_orderby_translated_field(self): """ Test search ordered by a translated field. """ # create a category with a French translation padawans = self.env['res.partner.category'].create({'name': 'Padawans'}) padawans_fr = padawans.with_context(lang='fr_FR') padawans_fr.write({'name': 'Apprentis'}) # search for categories, and sort them by (translated) name categories = padawans_fr.search([('id', 'in', [self.customers.id, padawans.id])], order='name') self.assertEqual(categories.ids, [padawans.id, self.customers.id], "Search ordered by translated name should return Padawans (Apprentis) before Customers (Clients)") def test_105_duplicate_record_multi_no_en(self): self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'}) self.env['res.lang']._activate_lang('nl_NL') self.customers.with_context(lang='nl_NL').name = 'Klanten' self.env['res.lang']._activate_lang('zh_CN') self.customers.with_context(lang='zh_CN').name = '客户' self.env.ref('base.lang_en').active = False self.env.ref('base.lang_zh_CN').active = False category = self.customers translations = category._fields['name']._get_stored_translations(category) self.assertDictEqual( translations, { 'en_US': 'Customers', 'fr_FR': 'Clients', 'nl_NL': 'Klanten', 'zh_CN': '客户', } ) category_copy = self.customers.with_context(lang='fr_FR').copy() translations = category_copy._fields['name']._get_stored_translations(category_copy) self.assertDictEqual( translations, { 'en_US': 'Customers', 'fr_FR': 'Clients', 'nl_NL': 'Klanten', }, 'English, French and Dutch translation should be copied, Chinese translation should be dropped' ) def test_107_duplicate_record_en(self): category = self.customers.with_context({'lang': 'en_US'}).copy() category_no = category.with_context({}) self.assertEqual(category_no.name, 'Customers', "Duplication did not set untranslated value") category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value") def test_108_search_en(self): CategoryEn = self.env['res.partner.category'].with_context(lang='en_US') category_equal = CategoryEn.search([('name', '=', 'Customers')]) self.assertEqual(category_equal.id, self.customers.id, "Search with '=' doesn't work for English") category_ilike = CategoryEn.search([('name', 'ilike', 'stoMer')]) self.assertIn(self.customers, category_ilike, "Search with 'ilike' doesn't work for English") category_eq_ilike = CategoryEn.search([('name', '=ilike', 'CustoMers')]) self.assertIn(self.customers, category_eq_ilike, "Search with '=ilike' doesn't work for English") category_in = CategoryEn.search([('name', 'in', ['Customers'])]) self.assertIn(self.customers, category_in, "Search with 'in' doesn't work for English") def test_109_search_fr(self): CategoryFr = self.env['res.partner.category'].with_context(lang='fr_FR') category_equal = CategoryFr.search([('name', '=', 'Clients')]) self.assertEqual(category_equal.id, self.customers.id, "Search with '=' doesn't work for non English") category_ilike = CategoryFr.search([('name', 'ilike', 'lIen')]) self.assertIn(self.customers, category_ilike, "Search with 'ilike' doesn't work for non English") category_eq_ilike = CategoryFr.search([('name', '=ilike', 'clieNts')]) self.assertIn(self.customers, category_eq_ilike, "Search with '=ilike' doesn't work for non English") category_in = CategoryFr.search([('name', 'in', ['Clients'])]) self.assertIn(self.customers, category_in, "Search with 'in' doesn't work for non English") def test_110_search_es(self): self.env['res.lang']._activate_lang('es_ES') langs = self.env['res.lang'].get_installed() self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français'), ('es_ES', 'Spanish / Español')], langs, "Test did not start with the expected languages") CategoryEs = self.env['res.partner.category'].with_context(lang='es_ES') category_equal = CategoryEs.search([('name', '=', 'Customers')]) self.assertEqual(category_equal.id, self.customers.id, "Search with '=' should use the English name if the current language translation is not available") category_ilike = CategoryEs.search([('name', 'ilike', 'usTom')]) self.assertIn(self.customers, category_ilike, "Search with 'ilike' should use the English name if the current language translation is not available") category_eq_ilike = CategoryEs.search([('name', '=ilike', 'CustoMers')]) self.assertIn(self.customers, category_eq_ilike, "Search with '=ilike' should use the English name if the current language translation is not available") category_in = CategoryEs.search([('name', 'in', ['Customers'])]) self.assertIn(self.customers, category_in, "Search with 'in' should use the English name if the current language translation is not available") def test_111_prefetch_langs(self): category_en = self.customers.with_context(lang='en_US') self.env.ref('base.lang_nl').active = True category_nl = category_en.with_context(lang='nl_NL') category_nl.name = 'Klanten' self.assertTrue(self.env.ref('base.lang_fr').active) category_fr = category_en.with_context(lang='fr_FR') self.assertFalse(self.env.ref('base.lang_zh_CN').active) category_zh = category_en.with_context(lang='zh_CN') self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'}) self.env.ref('base.lang_en').active = False category_fr.with_context(prefetch_langs=True).name category_nl.name category_en.name category_zh.name category_fr.invalidate_recordset() with self.assertQueryCount(1): self.assertEqual(category_fr.with_context(prefetch_langs=True).name, 'Clients') with self.assertQueryCount(0): self.assertEqual(category_nl.name, 'Klanten') self.assertEqual(category_en.name, 'Customers') self.assertEqual(category_zh.name, 'Customers') # TODO Currently, the unique constraint doesn't work for translatable field # def test_111_unique_en(self): # Country = self.env['res.country'] # country_1 = Country.create({'name': 'Odoo'}) # country_1.with_context(lang='fr_FR').name = 'Odoo_Fr' # country_1.flush_recordset() # # country_2 = Country.create({'name': 'Odoo2'}) # with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): # country_2.name = 'Odoo' # country_2.flush_recordset() # # with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): # country_3 = Country.create({'name': 'Odoo'}) class TestTranslationWrite(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.category = cls.env['res.partner.category'].create({'name': 'Reblochon'}) cls.category_xml_id = cls.category.export_data(['id']).get('datas')[0][0] def test_00(self): self.env['res.lang']._activate_lang('fr_FR') langs = self.env['res.lang'].get_installed() self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") category = self.env['res.partner.category'].with_context(lang='en_US').create({'name': 'English'}) self.assertEqual(category.with_context(lang='en_US').name, 'English') self.assertEqual(category.with_context(lang='fr_FR').name, 'English') category.with_context(lang='en_US').name = 'English 2' self.assertEqual(category.with_context(lang='fr_FR').name, 'English 2') category2 = self.env['res.partner.category'].with_context(lang='fr_FR').create({'name': 'French'}) self.assertEqual(category2.with_context(lang='en_US').name, 'French') self.assertEqual(category2.with_context(lang='fr_FR').name, 'French') category2.with_context(lang='en_US').name = 'English' self.assertEqual(category2.with_context(lang='fr_FR').name, 'French') category3 = self.env['res.partner.category'].with_context(lang='en_US').create({'name': 'English'}) self.assertEqual(category3.with_context(lang='en_US').name, 'English') self.assertEqual(category3.with_context(lang='fr_FR').name, 'English') category3.with_context(lang='fr_FR').name = 'French 2' category3.with_context(lang='en_US').name = 'English 2' self.assertEqual(category3.with_context(lang='fr_FR').name, 'French 2') def test_03_fr_single(self): self.env['res.lang']._activate_lang('fr_FR') self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'}) self.env.ref('base.lang_en').active = False langs = self.env['res.lang'].get_installed() self.assertEqual([('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") self.category.with_context(lang='fr_FR').write({'name': 'French Name'}) fr_name = self.category.with_context(lang='fr_FR').read(['name']) self.assertEqual(fr_name[0]['name'], "French Name", "Reference field not updated") # read from the cache self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") # read from database self.category.invalidate_recordset() self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") def test_04_fr_multi(self): self.env['res.lang']._activate_lang('fr_FR') langs = self.env['res.lang'].get_installed() self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") po_string = ''' #. module: __export__ #: model:res.partner.category,name:%s msgid "Reblochon" msgstr "Translated Name" ''' % self.category_xml_id with io.BytesIO(bytes(po_string, encoding='utf-8')) as f: f.name = 'dummy' translation_importer = TranslationImporter(self.env.cr, verbose=True) translation_importer.load(f, 'po', 'fr_FR') translation_importer.save(overwrite=True) self.category.with_context(lang='fr_FR').write({'name': 'French Name'}) self.category.with_context(lang='en_US').write({'name': 'English Name'}) # read from the cache first self.assertEqual(self.category.with_context(lang=None).name, "English Name") self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") self.assertEqual(self.category.with_context(lang='en_US').name, "English Name") # force save to database and clear the cache: force a clean state self.category.invalidate_recordset() # read from database self.assertEqual(self.category.with_context(lang=None).name, "English Name") self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") self.assertEqual(self.category.with_context(lang='en_US').name, "English Name") def test_04_fr_multi_no_en(self): self.env['res.lang']._activate_lang('fr_FR') self.env['res.lang']._activate_lang('es_ES') self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'}) self.env.ref('base.lang_en').active = False langs = self.env['res.lang'].get_installed() self.assertEqual([('fr_FR', 'French / Français'), ('es_ES', 'Spanish / Español')], langs, "Test did not start with the expected languages") self.category.with_context(lang='fr_FR').write({'name': 'French Name'}) self.category.with_context(lang='es_ES').write({'name': 'Spanish Name'}) self.category.with_context(lang=None).write({'name': 'None Name'}) # read from the cache first self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") self.assertEqual(self.category.with_context(lang='es_ES').name, "Spanish Name") self.assertEqual(self.category.with_context(lang=None).name, "None Name") # force save to database and clear the cache: force a clean state self.category.invalidate_recordset() # read from database self.assertEqual(self.category.with_context(lang='fr_FR').name, "French Name") self.assertEqual(self.category.with_context(lang='es_ES').name, "Spanish Name") self.assertEqual(self.category.with_context(lang=None).name, "None Name") def test_05_remove_multi_false(self): self._test_05_remove_multi(False) def _test_05_remove_multi(self, empty_value): self.env['res.lang']._activate_lang('fr_FR') langs = self.env['res.lang'].get_installed() self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") belgium = self.env.ref('base.be') # vat_label is translatable and not required belgium.with_context(lang='en_US').write({'vat_label': 'VAT'}) belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA'}) # remove the value belgium.with_context(lang='fr_FR').write({'vat_label': empty_value}) # should recover the initial value from db self.assertEqual( empty_value, belgium.with_context(lang='fr_FR').vat_label, "Value should be the empty_value" ) self.assertEqual( empty_value, belgium.with_context(lang='en_US').vat_label, "Value should be the empty_value" ) self.assertEqual( empty_value, belgium.with_context(lang=None).vat_label, "Value should be the empty_value" ) belgium.with_context(lang='en_US').write({'vat_label': 'VAT'}) belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA'}) # remove the value belgium.with_context(lang='en_US').write({'vat_label': empty_value}) self.assertEqual( empty_value, belgium.with_context(lang='fr_FR').vat_label, "Value should be the empty_value" ) self.assertEqual( empty_value, belgium.with_context(lang='en_US').vat_label, "Value should be the empty_value" ) self.assertEqual( empty_value, belgium.with_context(lang=None).vat_label, "Value should be the empty_value" ) def test_write_empty_and_value(self): self.env['res.lang']._activate_lang('fr_FR') self.env['res.lang']._activate_lang('nl_NL') langs = self.env['res.lang'].get_installed() self.assertEqual([('nl_NL', 'Dutch / Nederlands'), ('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") belgium = self.env.ref('base.be') # vat_label is translatable and not required belgium.with_context(lang='en_US').write({'vat_label': 'VAT_US'}) belgium.with_context(lang='fr_FR').write({'vat_label': 'VAT_FR'}) belgium.with_context(lang='nl_NL').write({'vat_label': 'VAT_NL'}) belgium.invalidate_recordset() belgium.with_context(lang='en_US').write({'vat_label': False}) belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA_FR2'}) self.assertEqual(belgium.with_context(lang='en_US').vat_label, 'TVA_FR2') self.assertEqual(belgium.with_context(lang='nl_NL').vat_label, 'TVA_FR2') belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA_FR3'}) belgium.with_context(lang='en_US').write({'vat_label': ''}) self.assertEqual(belgium.with_context(lang='en_US').vat_label, '') self.assertEqual(belgium.with_context(lang='nl_NL').vat_label, '') def test_create_empty_false(self): self._test_create_empty(False) # feature removed # def test_cresate_emtpy_empty_string(self): # self._test_create_empty('') def _test_create_empty(self, empty_value): self.env['res.lang']._activate_lang('fr_FR') langs = self.env['res.lang'].get_installed() self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") group = self.env['res.groups'].create({'name': 'test_group', 'comment': empty_value}) self.assertEqual(group.with_context(lang='en_US').comment, empty_value) self.assertEqual(group.with_context(lang='fr_FR').comment, empty_value) group.with_context(lang='fr_FR').comment = 'French comment' self.assertEqual(group.with_context(lang='fr_FR').comment, 'French comment') self.assertEqual(group.with_context(lang='en_US').comment, 'French comment') group.with_context(lang='fr_FR').comment = 'French comment 2' 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_update_field_translations_for_empty(self): self.env['res.lang']._activate_lang('nl_NL') self.env['res.lang']._activate_lang('fr_FR') group = self.env['res.groups'].create({'name': 'test_group', 'comment': False}) groupEN = group.with_context(lang='en_US') groupFR = group.with_context(lang='fr_FR') groupNL = group.with_context(lang='nl_NL') self.assertEqual(groupEN.comment, False) groupFR.update_field_translations('comment', {'nl_NL': 'Dutch Name', 'fr_FR': 'French Name'}) self.assertEqual(groupEN.comment, 'French Name', 'fr_FR value as the current env.lang is chosen as the default en_US value') self.assertEqual(groupFR.comment, 'French Name') self.assertEqual(groupNL.comment, 'Dutch Name') group.comment = False groupFR.update_field_translations('comment', {'nl_NL': False, 'fr_FR': False}) groupFR.flush_recordset() self.cr.execute("SELECT comment FROM res_groups WHERE id = %s", (group.id,)) (comment,) = self.cr.fetchone() self.assertEqual(comment, None) def test_field_selection(self): """ Test translations of field selections. """ self.env['res.lang']._activate_lang('fr_FR') field = self.env['ir.model']._fields['state'] self.assertEqual([key for key, _ in field.selection], ['manual', 'base']) ir_field = self.env['ir.model.fields']._get('ir.model', 'state') ir_field = ir_field.with_context(lang='fr_FR') ir_field.selection_ids[0].name = 'Custo' ir_field.selection_ids[1].name = 'Pas touche!' fg = self.env['ir.model'].fields_get(['state']) self.assertEqual(fg['state']['selection'], field.selection) fg = self.env['ir.model'].with_context(lang='fr_FR').fields_get(['state']) self.assertEqual(fg['state']['selection'], [('manual', 'Custo'), ('base', 'Pas touche!')]) def test_load_views(self): """ Test translations of field descriptions in get_view(). """ self.env['res.lang']._activate_lang('fr_FR') # add translation for the string of field ir.model.name ir_model_field = self.env['ir.model.fields']._get('ir.model', 'name') LABEL = "Description du Modèle" ir_model_field_xml_id = ir_model_field.export_data(['id']).get('datas')[0][0] po_string = ''' #. module: __export__ #: model:ir.model.fields,field_description:%s msgid "Model Description" msgstr "%s" ''' % (ir_model_field_xml_id, LABEL) with io.BytesIO(bytes(po_string, encoding='utf-8')) as f: f.name = 'dummy' translation_importer = TranslationImporter(self.env.cr, verbose=True) translation_importer.load(f, 'po', 'fr_FR') translation_importer.save(overwrite=True) # check that fields_get() returns the expected label model = self.env['ir.model'].with_context(lang='fr_FR') info = model.fields_get(['name']) self.assertEqual(info['name']['string'], LABEL) # check that get_views() also returns the expected label info = model.get_views([(False, 'form')]) self.assertEqual(info['models'][model._name]['name']['string'], LABEL) class TestXMLTranslation(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.env['res.lang']._activate_lang('fr_FR') cls.env['res.lang']._activate_lang('nl_NL') cls.env.ref('base.module_base')._update_translations(['fr_FR', 'nl_NL']) def create_view(self, archf, terms, **kwargs): view = self.env['ir.ui.view'].create({ 'name': 'test', 'model': 'res.partner', 'arch': archf % terms, }) view.invalidate_recordset() val = {'en_US': archf % terms} for lang, trans_terms in kwargs.items(): val[lang] = archf % trans_terms query = """UPDATE ir_ui_view SET arch_db = %s WHERE id = %s""" self.env.cr.execute(query, (Json(val), view.id)) return view def test_copy(self): """ Create a simple view, fill in translations, and copy it. """ archf = '' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = ('Couteau', 'Fourchette', 'Cuiller') view0 = self.create_view(archf, terms_en, fr_FR=terms_fr) env_en = self.env(context={'lang': 'en_US'}) env_fr = self.env(context={'lang': 'fr_FR'}) # check translated field self.assertEqual(view0.with_env(env_en).arch_db, archf % terms_en) self.assertEqual(view0.with_env(env_fr).arch_db, archf % terms_fr) # copy without lang view1 = view0.with_env(env_en).copy({}) self.assertEqual(view1.with_env(env_en).arch_db, archf % terms_en) self.assertEqual(view1.with_env(env_fr).arch_db, archf % terms_fr) # copy with lang='fr_FR' view2 = view0.with_env(env_fr).copy({}) self.assertEqual(view2.with_env(env_en).arch_db, archf % terms_en) self.assertEqual(view2.with_env(env_fr).arch_db, archf % terms_fr) # copy with lang='fr_FR' and translate=html_translate self.patch(type(self.env['ir.ui.view']).arch_db, 'translate', html_translate) view3 = view0.with_env(env_fr).copy({}) self.assertEqual(view3.with_env(env_en).arch_db, archf % terms_en) self.assertEqual(view3.with_env(env_fr).arch_db, archf % terms_fr) def test_spaces(self): """ Create translations where value has surrounding spaces. """ archf = '' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = (' Couteau', 'Fourchette ', ' Cuiller ') self.create_view(archf, terms_en, fr_FR=terms_fr) def test_sync(self): """ Check translations after minor change in source terms. """ archf = '' terms_en = ('Bread and cheeze',) terms_fr = ('Pain et fromage',) terms_nl = ('Brood and kaas',) 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 (fixed type in 'cheeze') terms_en = ('Bread and cheese',) view.with_env(env_en).write({'arch_db': archf % terms_en}) # check whether translations have been synchronized 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) view = self.create_view(archf, terms_fr, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl) # modify source term in view in another language with close term new_terms_fr = ('Pains et fromage',) view.with_env(env_fr).write({'arch_db': archf % new_terms_fr}) # check whether translations have been synchronized self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) self.assertEqual(view.with_env(env_fr).arch_db, archf % new_terms_fr) self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) def test_sync_xml(self): """ Check translations of 'arch' after xml tags changes in source terms. """ archf = '' terms_en = ('Bread and cheese', 'Fork') terms_fr = ('Pain et fromage', 'Fourchette') terms_nl = ('Brood and kaas', '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 (add css style) terms_en = ('Bread and cheese', '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 butter', 'Fork') 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_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_no_en(self): self.env['res.lang']._activate_lang('fr_FR') self.env['res.lang']._activate_lang('nl_NL') archf = '' terms_en = ('Bread and cheese', 'Fork') terms_fr = ('Pain et fromage', 'Fourchetta') terms_nl = ('Brood and kaas', 'Vork') view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl) self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'}) self.env.ref('base.lang_en').active = False env_nolang = self.env(context={}) env_fr = self.env(context={'lang': 'fr_FR'}) env_nl = self.env(context={'lang': 'nl_NL'}) # style change and typo change terms_fr = ('Pain et fromage', 'Fourchette') view.with_env(env_fr).write({'arch_db': archf % terms_fr}) self.assertEqual(view.with_env(env_nolang).arch_db, archf % terms_fr) self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr) self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) def test_sync_text_to_xml(self): """ Check translations of 'arch' after xml tags changes in source terms. """ archf = '' terms_en = ('Hi',) terms_fr = ('Salut',) terms_nl = ('Hallo',) 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'}) # modify the arch view, keep the same text content: 'Hi' terms_en = 'Hi' archf = '' view.with_env(env_en).write({'arch_db': archf % terms_en}) self.assertEqual(view.with_env(env_nolang).arch_db, archf % terms_en) self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) # check that we didn't set string="<span>Salut</span>" self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_en) 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 = '''''' 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_sync_xml_inline_modifiers(self): """ Check translations of 'arch' after xml tags changes in source terms when source term had updated modifiers attributes """ archf = '''''' 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={"install_mode": True}) env_en = self.env(context={'lang': 'en_US', "install_mode": True}) env_fr = self.env(context={'lang': 'fr_FR', "install_mode": True}) env_nl = self.env(context={'lang': 'nl_NL', "install_mode": True}) 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 attributes in source term terms_en = ('Bread and cheese', 'Knife and Fork', 'Knife and Fork') view.with_env(env_en).write({'arch_db': archf % terms_en}) terms_fr = ('Pain et fromage', 'Couteau et Fourchette', 'Couteau et Fourchette') # 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 attributes in source term terms_en = ('Bread and cheese', 'Knife and Fork', 'Knife and Fork') view.with_env(env_en).write({'arch_db': archf % terms_en}) terms_fr = ('Pain et fromage', 'Couteau et Fourchette', 'Couteau et Fourchette') # 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) def test_sync_xml_close_terms(self): """ Check translations of 'arch' after xml tags changes in source terms. """ archf = '' terms_en = ('RandomRandom1', 'RandomRandom2', 'RandomRandom3') terms_fr = ('RandomRandom1', 'AléatoireAléatoire2', 'AléatoireAléatoire3') view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr) env_nolang = self.env(context={}) env_en = self.env(context={'lang': 'en_US'}) env_fr = self.env(context={'lang': 'fr_FR'}) 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) # modify source term in view terms_en = ('RandomRandom1', 'SomethingElse', 'RandomRandom3') view.with_env(env_en).write({'arch_db': archf % terms_en}) # check whether close terms have correct translations 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 % ('RandomRandom1', 'SomethingElse', 'AléatoireAléatoire3')) def test_cache_consistency(self): view = self.env["ir.ui.view"].create({ "name": "test_translate_xml_cache_invalidation", "model": "res.partner", "arch": "", }) view_fr = view.with_context({"lang": "fr_FR"}) self.assertIn("", view.arch_db) self.assertIn("", view_fr.arch_db) # write with no lang, and check consistency in other languages view.write({"arch_db": ""}) self.assertIn("", view.arch_db) self.assertIn("", view_fr.arch_db) def test_update_field_translations(self): archf = '' terms_en = ('Bread and cheese', 'Fork') terms_fr = ('Pain et fromage', 'Fourchette') terms_nl = ('Brood and kaas', 'Vork') view = self.create_view(archf, terms_en, fr_FR=terms_fr, nl_NL=terms_nl) # cache arch_db view.arch_db view.with_context(lang='en_US').arch_db view.with_context(lang='fr_FR').arch_db view_nl = view.with_context(lang='nl_NL').arch_db view.update_field_translations('arch_db', { 'en_US': {'Fork': 'Fork2'}, 'fr_FR': {'Fourchette': 'Fourchette2'} }) self.assertEqual(view.arch_db, '') self.assertEqual(view.with_context(lang='en_US').arch_db, '') self.assertEqual(view.with_context(lang='fr_FR').arch_db, '') self.assertEqual(view.with_context(lang='nl_NL').arch_db, view_nl) view.invalidate_recordset() self.assertEqual(view.arch_db, '') self.assertEqual(view.with_context(lang='en_US').arch_db, '') self.assertEqual(view.with_context(lang='fr_FR').arch_db, '') self.assertEqual(view.with_context(lang='nl_NL').arch_db, view_nl) # update translations for fallback values and en_US self.env['res.lang']._activate_lang('es_ES') self.assertEqual(view.with_context(lang='es_ES').arch_db, '') view.update_field_translations('arch_db', { 'en_US': {'Fork2': 'Fork3'}, 'es_ES': {'Fork2': 'Tenedor3'} }) self.assertEqual(view.with_context(lang='en_US').arch_db, '') self.assertEqual(view.with_context(lang='es_ES').arch_db, '') def test_delay_translations(self): archf = '' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = ('Couteau', 'Fourchette', 'Cuiller') view0 = self.create_view(archf, terms_en, fr_FR=terms_fr) archf2 = '' terms_en2 = ('new Knife', 'Fork', 'Spoon') # write en_US with delay_translations view0.with_context(lang='en_US', delay_translations=True).arch_db = archf2 % terms_en2 view0.invalidate_recordset() self.assertEqual( view0.with_context(lang='en_US').arch_db, archf2 % terms_en2, 'en_US value should be the latest one since it is updated directly' ) self.assertEqual(view0.with_context(lang='en_US', check_translations=True).arch_db, archf2 % terms_en2) self.assertEqual( view0.with_context(lang='fr_FR').arch_db, archf % terms_fr, "fr_FR value should keep the same since its translations hasn't been confirmed" ) self.assertEqual( view0.with_context(lang='fr_FR', edit_translations=True).arch_db, '' ) self.assertEqual( view0.with_context(lang='fr_FR', check_translations=True).arch_db, archf2 % (terms_en2[0], terms_fr[1], terms_fr[2]) ) self.assertEqual( view0.with_context(lang='nl_NL').arch_db, archf2 % terms_en2, "nl_NL value should fallback to en_US value" ) self.assertEqual( view0.with_context(lang='nl_NL', check_translations=True).arch_db, archf2 % terms_en2 ) # update and confirm translations view0.update_field_translations('arch_db', {'fr_FR': {}}) self.assertEqual( view0.with_context(lang='fr_FR').arch_db, archf2 % (terms_en2[0], terms_fr[1], terms_fr[2]) ) self.assertEqual( view0.with_context(lang='fr_FR', check_translations=True).arch_db, archf2 % (terms_en2[0], terms_fr[1], terms_fr[2]) ) def test_delay_translations_no_term(self): archf = '' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = ('Couteau', 'Fourchette', 'Cuiller') view0 = self.create_view(archf, terms_en, fr_FR=terms_fr) archf2 = '' # delay_translations only works when the written value has at least one translatable term view0.with_context(lang='en_US', delay_translations=True).arch_db = archf2 for lang in ('en_US', 'fr_FR', 'nl_NL'): self.assertEqual( view0.with_context(lang=lang).arch_db, archf2, f'arch_db for {lang} should be {archf2}' ) self.assertEqual( view0.with_context(lang=lang, check_translations=True).arch_db, archf2, f'arch_db for {lang} should be {archf2} when check_translations' ) class TestHTMLTranslation(TransactionCase): def test_write_non_existing(self): html = '''My first paragraph.
''' company = self.env['res.company'].browse(9999) company.report_footer = html self.assertHTMLEqual(company.report_footer, html) # flushing on non-existing records does not break for scalar fields; the # same behavior is expected for translated fields company.flush_recordset() def test_delay_translations_no_term(self): self.env['res.lang']._activate_lang('fr_FR') self.env['res.lang']._activate_lang('nl_NL') Company = self.env['res.company'] company0 = Company.create({'name': 'company_1', 'report_footer': '