diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 39b5a93a8e6..9c670161a28 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -727,8 +727,15 @@ class Import(models.TransientModel): if '/' not in header: # Then, try exact match - IrTranslation = self.env['ir.translation'] - translated_header = IrTranslation._get_source('ir.model.fields,field_description', 'model', self.env.lang, header).lower() + if header: + field_rec = ( + self.env['ir.model.fields'].sudo().with_context(lang='en_US') + .search([('field_description', '=', header)], limit=1) + .with_env(self.env) + ) + translated_header = (field_rec.sudo().field_description or header).lower() + else: + translated_header = "" for field in fields_tree: # exact match found based on the field technical name if header.casefold() == field['name'].casefold(): diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index daf467eddd8..da7e57f20e3 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -193,7 +193,9 @@ class HrEmployeePrivate(models.Model): public.read(fields) for fname in fields: values = self.env.cache.get_values(public, public._fields[fname]) - self.env.cache.update(self, self._fields[fname], values) + if self._fields[fname].translate: + values = [(value.copy() if value else None) for value in values] + self.env.cache.update_raw(self, self._fields[fname], values) @api.model def _cron_check_work_permit_validity(self): diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 6b76757fadf..3579a10f93b 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -297,7 +297,7 @@ class IrHttp(models.AbstractModel): modules = IrHttpModel.get_translation_frontend_modules() user_context = request.session.context if request.session.uid else {} lang = user_context.get('lang') - translation_hash = request.env['ir.translation'].get_web_translations_hash(modules, lang) + translation_hash = request.env['ir.http'].get_web_translations_hash(modules, lang) session_info.update({ 'translationURL': '/website/translations', diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index bdc7e3979b7..50bd4700272 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -153,14 +153,19 @@ class AccountFrFec(models.TransientModel): unaffected_earnings_results = self._do_query_unaffected_earnings() unaffected_earnings_line = False - sql_query = ''' + if self.pool['account.account'].name.translate: + lang = self.env.user.lang or get_lang(self.env).code + aa_name = f"COALESCE(aa.name->>'{lang}', aa.name->>'en_US')" + else: + aa_name = "aa.name" + sql_query = f''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, - replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, + replace(replace(MIN({aa_name}), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, @@ -236,14 +241,14 @@ class AccountFrFec(models.TransientModel): rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable - sql_query = ''' + sql_query = f''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, - replace(MIN(aa.name), '|', '/') AS CompteLib, + replace(MIN({aa_name}), '|', '/') AS CompteLib, CASE WHEN MIN(aa.account_type) IN ('asset_receivable', 'liability_payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' @@ -298,14 +303,19 @@ class AccountFrFec(models.TransientModel): rows_to_write.append(listrow) # LINES - sql_query = ''' + if self.pool['account.journal'].name.translate: + lang = self.env.user.lang or get_lang(self.env).code + aj_name = f"COALESCE(aj.name->>'{lang}', aj.name->>'en_US')" + else: + aj_name = "aj.name" + sql_query = f''' SELECT REGEXP_REPLACE(replace(aj.code, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalCode, - REGEXP_REPLACE(replace(COALESCE(aj__name.value, aj.name), '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalLib, + REGEXP_REPLACE(replace({aj_name}, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalLib, REGEXP_REPLACE(replace(am.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS EcritureNum, TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate, aa.code AS CompteNum, - REGEXP_REPLACE(replace(aa.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS CompteLib, + REGEXP_REPLACE(replace({aa_name}, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS CompteLib, CASE WHEN aa.account_type IN ('asset_receivable', 'liability_payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' @@ -343,11 +353,6 @@ class AccountFrFec(models.TransientModel): LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_journal aj ON aj.id = am.journal_id - LEFT JOIN ir_translation aj__name ON aj__name.res_id = aj.id - AND aj__name.type = 'model' - AND aj__name.name = 'account.journal,name' - AND aj__name.lang = %s - AND aj__name.value != '' JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id @@ -369,9 +374,8 @@ class AccountFrFec(models.TransientModel): am.name, aml.id ''' - lang = self.env.user.lang or get_lang(self.env).code self._cr.execute( - sql_query, (lang, self.date_from, self.date_to, company.id)) + sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): rows_to_write.append(list(row)) diff --git a/addons/l10n_multilang/models/account.py b/addons/l10n_multilang/models/account.py index ef2027cffa6..c538986edff 100644 --- a/addons/l10n_multilang/models/account.py +++ b/addons/l10n_multilang/models/account.py @@ -82,9 +82,3 @@ class AccountAnalyticAccount(models.Model): _inherit = 'account.analytic.account' name = fields.Char(translate=True) - - -class ResCountryState(models.Model): - _inherit = 'res.country.state' - - name = fields.Char(translate=True) diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py index 8b5cc601e48..dc133f5a509 100644 --- a/addons/l10n_multilang/models/l10n_multilang.py +++ b/addons/l10n_multilang/models/l10n_multilang.py @@ -38,26 +38,9 @@ class AccountChartTemplate(models.Model): :return: True """ - xlat_obj = self.env['ir.translation'] - #find the source from Account Template for lang in langs: - #find the value from Translation - value = xlat_obj._get_ids(in_ids._name + ',' + in_field, 'model', lang, in_ids.ids) - counter = 0 - for element in in_ids.with_context(lang=None): - if value[element.id]: - #copy Translation from Source to Destination object - xlat_obj._set_ids( - out_ids._name + ',' + in_field, - 'model', - lang, - out_ids[counter].ids, - value[element.id], - element[in_field] - ) - else: - _logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element[in_field])) - counter += 1 + for in_id, out_id in zip(in_ids.with_context(lang=lang), out_ids.with_context(lang=lang)): + out_id[in_field] = in_id[in_field] return True def process_coa_translations(self): diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index a7eaa787c3b..03bcd6eb3c1 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -52,7 +52,6 @@ from . import ir_http from . import ir_mail_server from . import ir_model from . import ir_model_fields -from . import ir_translation from . import ir_ui_view from . import ir_qweb from . import ir_websocket diff --git a/addons/mail/models/ir_http.py b/addons/mail/models/ir_http.py index 2c99c97c2b3..ff89ab3a5fb 100644 --- a/addons/mail/models/ir_http.py +++ b/addons/mail/models/ir_http.py @@ -18,7 +18,7 @@ class IrHttp(models.AbstractModel): user_context = {'lang': guest.lang} mods = odoo.conf.server_wide_modules or [] lang = user_context.get("lang") - translation_hash = request.env['ir.translation'].sudo().get_web_translations_hash(mods, lang) + translation_hash = request.env['ir.http'].sudo().get_web_translations_hash(mods, lang) result['cache_hashes']['translations'] = translation_hash result.update({ 'name': guest.name, diff --git a/addons/mail/models/ir_translation.py b/addons/mail/models/ir_translation.py deleted file mode 100644 index a572447c11a..00000000000 --- a/addons/mail/models/ir_translation.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, _ -from odoo.exceptions import AccessError - - -class IrTranslation(models.Model): - _inherit = 'ir.translation' - - def create(self, vals_list): - translations = super().create(vals_list) - translations._check_is_dynamic() - return translations - - def write(self, vals): - res = super().write(vals) - self._check_is_dynamic() - return res - - def _check_is_dynamic(self): - # if we don't modify translation of at least a model that inherits from mail.render.mixin, we ignore it - # translation.name can be a path, and so not in the pool, so type(None) will exclude these translations. - translations_for_mail_render_mixin = self.filtered( - lambda translation: issubclass(type(self.env.get(translation.name.split(',')[0])), self.pool['mail.render.mixin']) - ) - if not translations_for_mail_render_mixin: - return - - # if we are admin, or that we can update mail.template we ignore - if self.env.is_admin() or self.env.user.has_group('mail.group_mail_template_editor'): - return - - # Check that we don't add qweb code in translation when you don't have the rights - - # prefill cache - ids_by_model_by_lang = {} - tuple_lang_model_id = translations_for_mail_render_mixin.mapped( - lambda translation: (translation.lang, translation.name.split(',')[0], translation.res_id) - ) - for lang, model, _id in tuple_lang_model_id: - ids_by_model_by_lang.setdefault(lang, {}).setdefault(model, set()).add(_id) - for lang in ids_by_model_by_lang: - for res_model, res_ids in ids_by_model_by_lang[lang].items(): - self.env[res_model].with_context(lang=lang).browse(res_ids) - - for trans in translations_for_mail_render_mixin: - res_model, res_id = trans.name.split(',')[0], trans.res_id - rec = self.env[res_model].with_context(lang=trans.lang).browse(res_id) - - if rec._is_dynamic(): - group = self.env.ref('mail.group_mail_template_editor') - more_info = len(self) > 1 and ' [%s]' % rec or '' - raise AccessError( - _('Only users belonging to the "%(group)s" group can modify translation related to dynamic templates.%(xtra)s', - group=group.name, - xtra=more_info - ) - ) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 9e8d2cff87d..39874b7b25c 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -104,6 +104,16 @@ class MailRenderMixin(models.AbstractModel): self._check_access_right_dynamic_template() return True + def _update_field_translations(self, fname, translations, digest=None): + res = super()._update_field_translations(fname, translations, digest) + # TBD the below check is only for model_term translations. + # Because for model translations, super().update_field_translations will call write to check + if self._unrestricted_rendering: + # If the rendering is unrestricted (e.g. mail.template), + # check the user is part of the mail editor group to modify a template if the template is dynamic + self._check_access_right_dynamic_template() + return res + # ------------------------------------------------------------ # TOOLS # ------------------------------------------------------------ diff --git a/addons/mail/models/template_reset_mixin.py b/addons/mail/models/template_reset_mixin.py index b03aa5362d5..9b7b59fce3d 100644 --- a/addons/mail/models/template_reset_mixin.py +++ b/addons/mail/models/template_reset_mixin.py @@ -11,7 +11,7 @@ from odoo.modules import get_module_resource from odoo.modules.module import get_resource_from_path, get_resource_path from odoo.tools.convert import xml_import from odoo.tools.misc import file_open -from odoo.tools.translate import TranslationFileReader +from odoo.tools.translate import TranslationFileReader, _trans_load_data class TemplateResetMixin(models.AbstractModel): @@ -62,10 +62,8 @@ class TemplateResetMixin(models.AbstractModel): def _override_translation_term(self, module_name, xml_ids): processed_base_langs = [] - trans_to_remove = [] for code, _ in self.env['res.lang'].get_installed(): lang_code = tools.get_iso_codes(code) - translation_processed = False # In case of sub languages (e.g fr_BE), load the base language first, (e.g fr.po) and # then load the main translation file (e.g fr_BE.po) @@ -76,28 +74,17 @@ class TemplateResetMixin(models.AbstractModel): if base_trans_file: if base_lang_code not in processed_base_langs: processed_base_langs.append(base_lang_code) - translation_processed = self._process_translation_data(base_trans_file, code, xml_ids) + self._process_translation_data(base_trans_file, code, xml_ids) # Step 2: reset translation file with main language file (can possibly override the # terms coming from the base language) trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po') if trans_file: - translation_processed = self._process_translation_data(trans_file, code, xml_ids) + self._process_translation_data(trans_file, code, xml_ids) - # If no translation data available to update, unlink custom translated terms linked to template - if not translation_processed: - trans_to_remove.append(code) - - if trans_to_remove: - self.env['ir.translation'].search([ - ('name', 'like', f'{self._name},%'), - ('res_id', '=', self.id), - ('lang', 'in', trans_to_remove), - ('module', 'in', [module_name, False]), - ]).unlink() def _process_translation_data(self, trans_file, lang, xml_ids): - """Populates the ir_translation table. + """Load translations. :param trans_file: path to a translation file to open, e.g. {addon_path}/mail/i18n/es.po :param lang: language code of the translations contained in `trans_file` @@ -106,17 +93,9 @@ class TemplateResetMixin(models.AbstractModel): :return Boolean: True if translation data is available and processed, False otherwise """ with file_open(trans_file, mode='rb', filter_ext=(".po",)) as fileobj: - translation_processed = False reader = TranslationFileReader(fileobj) # Process a single PO entry - for row in reader: - if row.get('imd_name') in xml_ids: - translation_processed = True - # copy Translation from Source to Destination object - self.env['ir.translation']._set_ids( - row['name'], 'model', lang, self._ids, row.get('value', ''), row['src'], - ) - return translation_processed + _trans_load_data(self.env.cr, reader, lang, overwrite=True, force_overwrite=True, xml_ids=xml_ids) def reset_template(self): """Resets the Template with values given in source file. We ignore the case of diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 26fbd22c4d3..d8061f99946 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -1117,84 +1117,11 @@ class MailCommon(common.TransactionCase, MailCase): # Make sure Spanish translations have not been altered if test_record: - description_translations = cls.env['ir.translation'].search([ - ('module', '=', 'test_mail'), - ('src', '=', test_record._description), - ('lang', '=', lang_code) - ]) - if description_translations: - description_translations.update({'value': 'Spanish description'}) - else: - description_translations.create({ - 'lang': lang_code, - 'module': 'test_mail', - 'name': 'ir.model,name', - 'res_id': cls.env['ir.model']._get_id(test_record._name), - 'src': test_record._description, - 'state': 'translated', - 'type': 'model', - 'value': 'Spanish description', - }) - - translations_tocreate = [] - # Have a TestStuff always available - test_stuff_translations = cls.env['ir.translation'].search([ - ('module', '=', 'test_mail'), - ('src', '=', 'TestStuff'), - ('lang', '=', lang_code) - ]) - if test_stuff_translations: - test_stuff_translations.update({'value': 'TestSpanishStuff'}) - else: - translations_tocreate.append({ - 'lang': lang_code, - 'name': 'idontknow', - 'module': 'test_mail', - 'res_id': False, - 'src': 'TestStuff', - 'state': 'translated', - 'type': 'code', - 'value': 'TestSpanishStuff', - }) - - view_translations = cls.env['ir.translation'].search([ - ('module', '=', 'mail'), - ('src', '=', 'View %s'), - ('lang', '=', lang_code) - ]) - if view_translations: - view_translations.update({'value': 'SpanishView'}) - else: - translations_tocreate.append({ - 'lang': lang_code, - 'name': 'idontknow', - 'module': 'mail', - 'res_id': False, - 'src': 'View %s', - 'state': 'translated', - 'type': 'code', - 'value': 'SpanishView %s', - }) + cls.env['ir.model']._get(test_record._name).with_context(lang=lang_code).name = 'Spanish description' # Prepare some translated value for template if given - if test_template: - translations_tocreate += [{ - 'lang': lang_code, - 'module': 'mail', - 'name': 'mail.template,subject', - 'res_id': test_template.id, - 'state': 'translated', - 'type': 'model', - 'value': 'SpanishSubject for {{ object.name }}', - }, { - 'lang': lang_code, - 'module': 'mail', - 'name': 'mail.template,body_html', - 'res_id': test_template.id, - 'state': 'translated', - 'type': 'model', - 'value': '

SpanishBody for

', - }] + test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}' + test_template.with_context(lang=lang_code).body_html = '

SpanishBody for

' # create a custom layout for email notification if not layout_arch_db: @@ -1233,17 +1160,11 @@ class MailCommon(common.TransactionCase, MailCase): 'name': 'test_layout', 'res_id': view.id }) - translations_tocreate.append({ - 'lang': lang_code, - 'module': 'mail', - 'name': 'ir.ui.view,arch_db', - 'res_id': view.id, - 'src': 'English Layout for', - 'state': 'translated', - 'type': 'model_terms', - 'value': 'Spanish Layout para', + view.update_field_translations('arch_db', { + lang_code: { + 'English Layout for': 'Spanish Layout para' + } }) - cls.env['ir.translation'].create(translations_tocreate) def _generate_attachments_data(self, count, res_model=None, res_id=None, attach_values=None): # attachment visibility depends on what they are attached to diff --git a/addons/mail/tests/test_mail_render.py b/addons/mail/tests/test_mail_render.py index 1c3c8202653..7a601f87a35 100644 --- a/addons/mail/tests/test_mail_render.py +++ b/addons/mail/tests/test_mail_render.py @@ -112,22 +112,9 @@ class TestMailRender(common.MailCommon): }) # some translations - cls.env['ir.translation'].create({ - 'type': 'model', - 'name': 'mail.template,subject', - 'lang': 'fr_FR', - 'res_id': cls.test_template.id, - 'src': cls.test_template.subject, - 'value': cls.base_qweb_bits_fr[0], - }) - cls.env['ir.translation'].create({ - 'type': 'model', - 'name': 'mail.template,body_html', - 'lang': 'fr_FR', - 'res_id': cls.test_template.id, - 'src': cls.test_template.body_html, - 'value': cls.base_qweb_bits_fr[1], - }) + cls.test_template.with_context(lang='fr_FR').subject = cls.base_qweb_bits_fr[0] + cls.test_template.with_context(lang='fr_FR').body_html = cls.base_qweb_bits_fr[1] + cls.env['ir.model.data'].create({ 'name': 'test_template_xmlid', 'module': 'mail', diff --git a/addons/mail/tests/test_mail_template.py b/addons/mail/tests/test_mail_template.py index 6d7d50df4fe..75ab522fad7 100644 --- a/addons/mail/tests/test_mail_template.py +++ b/addons/mail/tests/test_mail_template.py @@ -107,62 +107,29 @@ class TestMailTemplate(MailCommon): 'body_html': '

foo

', }) - Translation = self.env['ir.translation'] - ### check qweb dynamic - Translation.insert_missing(employee_template._fields['body_html'], employee_template) - employee_translations_of_body = Translation.with_user(self.user_employee).search( - [('res_id', '=', employee_template.id), ('name', '=', 'mail.template,body_html'), ('lang', '=', 'fr_FR')], - limit=1 - ) - # keep a copy to create new translation later - body_translation_vals = employee_translations_of_body.read([])[0] - # write on translation for template without dynamic code is allowed - employee_translations_of_body.value = 'non-qweb' + employee_template.with_context(lang='fr_FR').body_html = 'non-qweb' # cannot write dynamic code on mail_template translation for employee without the group mail_template_editor. with self.assertRaises(AccessError): - employee_translations_of_body.value = '' + employee_template.with_context(lang='fr_FR').body_html = '' - employee_translations_of_body.unlink() # delete old translation, to test the creation now - body_translation_vals['value'] = '

' - - # admin can create - new = Translation.create(body_translation_vals) - new.unlink() - - # Employee without mail_template_editor group cannot create dynamic translation for mail.render.mixin - with self.assertRaises(AccessError): - Translation.with_user(self.user_employee).create(body_translation_vals) + employee_template.with_context(lang='fr_FR').sudo().body_html = '' + # reset the body_html to static + employee_template.body_html = False + employee_template.body_html = '

foo

' ### check qweb inline dynamic - Translation.insert_missing(employee_template._fields['subject'], employee_template) - employee_translations_of_subject = Translation.with_user(self.user_employee).search( - [('res_id', '=', employee_template.id), ('name', '=', 'mail.template,subject'), ('lang', '=', 'fr_FR')], - limit=1 - ) - # keep a copy to create new translation later - subject_translation_vals = employee_translations_of_subject.read([])[0] - # write on translation for template without dynamic code is allowed - employee_translations_of_subject.value = 'non-qweb' + employee_template.with_context(lang='fr_FR').subject = 'non-qweb' # cannot write dynamic code on mail_template translation for employee without the group mail_template_editor. with self.assertRaises(AccessError): - employee_translations_of_subject.value = '{{ object.foo }}' + employee_template.with_context(lang='fr_FR').subject = '{{ object.foo }}' - employee_translations_of_subject.unlink() # delete old translation, to test the creation now - subject_translation_vals['value'] = '{{ object.foo }}' - - # admin can create - new = Translation.create(subject_translation_vals) - new.unlink() - - # Employee without mail_template_editor group cannot create dynamic translation for mail.render.mixin - with self.assertRaises(AccessError): - Translation.with_user(self.user_employee).create(subject_translation_vals) + employee_template.with_context(lang='fr_FR').sudo().subject = '{{ object.foo }}' def test_server_archived_usage_protection(self): """ Test the protection against using archived server (servers used cannot be archived) """ diff --git a/addons/mail_plugin/controllers/mail_plugin.py b/addons/mail_plugin/controllers/mail_plugin.py index 06ce25c6dfb..c2ed36c748a 100644 --- a/addons/mail_plugin/controllers/mail_plugin.py +++ b/addons/mail_plugin/controllers/mail_plugin.py @@ -434,7 +434,7 @@ class MailPluginController(http.Controller): def _prepare_translations(self): lang = request.env['res.users'].browse(request.uid).lang - translations_per_module = request.env["ir.translation"].get_translations_for_webclient( + translations_per_module = request.env["ir.http"].get_translations_for_webclient( self._translation_modules_whitelist(), lang)[0] translations_dict = {} for module in self._translation_modules_whitelist(): diff --git a/addons/mrp_subcontracting/controllers/portal.py b/addons/mrp_subcontracting/controllers/portal.py index 6343fdadfdb..a76948e31ea 100644 --- a/addons/mrp_subcontracting/controllers/portal.py +++ b/addons/mrp_subcontracting/controllers/portal.py @@ -91,7 +91,7 @@ class CustomerPortal(portal.CustomerPortal): user_context = dict(request.env.context) if request.session.uid else {} mods = conf.server_wide_modules or [] lang = user_context.get("lang") - translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang) + translation_hash = request.env['ir.http'].get_web_translations_hash(mods, lang) cache_hashes = { "translations": translation_hash, } diff --git a/addons/privacy_lookup/wizard/privacy_lookup_wizard.py b/addons/privacy_lookup/wizard/privacy_lookup_wizard.py index 1be671a4b25..4d362285ee5 100644 --- a/addons/privacy_lookup/wizard/privacy_lookup_wizard.py +++ b/addons/privacy_lookup/wizard/privacy_lookup_wizard.py @@ -131,6 +131,8 @@ class PrivacyLookupWizard(models.TransientModel): rec_name = model._rec_name or 'name' is_normalized = field_name == 'email_normalized' or (model_name == 'mailing.trace' and field_name == 'email') if rec_name in model and model._fields[model._rec_name].type == 'char': + if model._fields[model._rec_name].translate: + rec_name = f"jsonb_path_query_array({rec_name}, '$.*')::text" additional_query += """ {field_name} {search_type} %s OR {rec_name} ilike %s """.format( diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 105fbe6cb1d..f2d7351f9e4 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -139,7 +139,7 @@ class ProjectCustomerPortal(CustomerPortal): # Update Cache user_context['lang'] = lang lang = user_context.get("lang") - translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang) + translation_hash = request.env['ir.http'].get_web_translations_hash(mods, lang) cache_hashes = { "translations": translation_hash, } diff --git a/addons/purchase_stock/tests/test_reordering_rule.py b/addons/purchase_stock/tests/test_reordering_rule.py index 9e934fb1c3b..028a0ca1fec 100644 --- a/addons/purchase_stock/tests/test_reordering_rule.py +++ b/addons/purchase_stock/tests/test_reordering_rule.py @@ -576,8 +576,8 @@ class TestReorderingRule(TransactionCase): ])], }) self.env['res.lang']._activate_lang('fr_FR') - self.env['ir.translation']._set_ids('product.template,name', 'model', 'fr_FR', product.product_tmpl_id.ids, 'produit en français') - self.env['ir.translation']._set_ids('product.product,name', 'model', 'fr_FR', product.ids, 'produit en français') + product.product_tmpl_id.with_context(lang='fr_FR').name = 'produit en français' + product.with_context(lang='fr_FR').name = 'produit en français' default_vendor = self.env["res.partner"].create({ "name": "Supplier A", }) diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py index 34369d1271a..648880d8d16 100644 --- a/addons/sale_management/models/sale_order_template.py +++ b/addons/sale_management/models/sale_order_template.py @@ -94,33 +94,7 @@ class SaleOrderTemplate(models.Model): for lang in languages: for line in self.sale_order_template_line_ids: if line.name == line.product_id.get_product_multiline_description_sale(): - self.create_or_update_translations( - model_name='sale.order.template.line,name', lang_code=lang.code, - res_id=line.id, src=line.name, - value=line.product_id.with_context(lang=lang.code).get_product_multiline_description_sale()) + line.with_context(lang=lang.code).name = line.product_id.with_context(lang=lang.code).get_product_multiline_description_sale() for option in self.sale_order_template_option_ids: if option.name == option.product_id.get_product_multiline_description_sale(): - self.create_or_update_translations( - model_name='sale.order.template.option,name', lang_code=lang.code, - res_id=option.id, src=option.name, - value=option.product_id.with_context(lang=lang.code).get_product_multiline_description_sale()) - - def create_or_update_translations(self, model_name, lang_code, res_id, src, value): - data = { - 'type': 'model', - 'name': model_name, - 'lang': lang_code, - 'res_id': res_id, - 'src': src, - 'value': value, - 'state': 'inprogress', - } - existing_trans = self.env['ir.translation'].search([ - ('name', '=', model_name), - ('res_id', '=', res_id), - ('lang', '=', lang_code) - ]) - if not existing_trans: - self.env['ir.translation'].create(data) - else: - existing_trans.write(data) + option.with_context(lang=lang.code).name = option.product_id.with_context(lang=lang.code).get_product_multiline_description_sale() diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 7341c9fd44a..56ddcbf4a2b 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -19,6 +19,7 @@ from odoo.exceptions import AccessError from odoo.tests import tagged from odoo.tools import mute_logger, formataddr from odoo.tests.common import users +from odoo.tools.translate import code_translations class TestMessagePostCommon(TestMailCommon, TestRecipients): @@ -1240,6 +1241,12 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_layout_email_lang_context(self): + # hack into code translations: + code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'} + self.addCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES')) + code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'} + self.addCleanup(code_translations.python_translations.pop, ('mail', 'es_ES')) + test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES') test_records[1].message_subscribe(self.partner_2.ids) diff --git a/addons/test_mail_sms/tests/test_sms_composer.py b/addons/test_mail_sms/tests/test_sms_composer.py index 3c54a009c32..03ed57fccaf 100644 --- a/addons/test_mail_sms/tests/test_sms_composer.py +++ b/addons/test_mail_sms/tests/test_sms_composer.py @@ -415,14 +415,7 @@ class TestSMSComposerMass(TestSMSCommon): def test_composer_mass_active_ids_w_template_and_lang(self): self.env['res.lang']._activate_lang('fr_FR') - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'sms.template,body', - 'lang': 'fr_FR', - 'res_id': self.sms_template.id, - 'src': self.sms_template.body, - 'value': 'Cher·e· {{ object.display_name }} ceci est un SMS.', - }) + self.sms_template.with_context(lang='fr_FR').body = 'Cher·e· {{ object.display_name }} ceci est un SMS.' # set template to try to use customer lang self.sms_template.write({ 'lang': '{{ object.customer_id.lang }}', @@ -481,14 +474,7 @@ class TestSMSComposerMass(TestSMSCommon): and the usage with the sms composer """ # Create the lang info self.env['res.lang']._activate_lang('fr_FR') - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'sms.template,body', - 'lang': 'fr_FR', - 'res_id': self.sms_template.id, - 'src': self.sms_template.body, - 'value': "Hello {{ object.display_name }} ceci est en français.", - }) + self.sms_template.with_context(lang='fr_FR').body = 'Hello {{ object.display_name }} ceci est en français.' # set template to try to use customer lang self.sms_template.write({ 'lang': '{{ object.customer_id.lang }}', diff --git a/addons/test_mail_sms/tests/test_sms_template.py b/addons/test_mail_sms/tests/test_sms_template.py index beebc78124a..225a95fdda3 100644 --- a/addons/test_mail_sms/tests/test_sms_template.py +++ b/addons/test_mail_sms/tests/test_sms_template.py @@ -29,13 +29,8 @@ class TestSmsTemplate(TestSMSCommon, TestSMSRecipients): def test_sms_template_lang(self): self.env['res.lang']._activate_lang('fr_FR') self.user_admin.write({'lang': 'en_US'}) - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'sms.template,body', - 'lang': 'fr_FR', - 'res_id': self.sms_template.id, - 'src': self.sms_template.body, - 'value': self.body_fr, + self.sms_template.update_field_translations('body', { + 'fr_FR': self.body_fr }) # set template to try to use customer lang self.sms_template.write({ diff --git a/addons/test_website/tests/test_is_multilang.py b/addons/test_website/tests/test_is_multilang.py index 167256c15bf..22167234d01 100644 --- a/addons/test_website/tests/test_is_multilang.py +++ b/addons/test_website/tests/test_is_multilang.py @@ -38,21 +38,10 @@ class TestIsMultiLang(odoo.tests.HttpCase): website.domain = self.base_url() # for _is_canonical_url website.default_lang_id = en website.language_ids = en + it + be - params = { - 'src': country1.name, - 'value': country1.name + ' Italia', - 'type': 'model', - 'name': 'res.country,name', - 'res_id': country1.id, - 'lang': it.code, - 'state': 'translated', - } - self.env['ir.translation'].create(params) - params.update({ - 'value': country1.name + ' Belgium', - 'lang': be.code, + country1.update_field_translations('name', { + it.code: country1.name + ' Italia', + be.code: country1.name + ' Belgium' }) - self.env['ir.translation'].create(params) r = self.url_open(f'/test_lang_url/{country1.id}') self.assertEqual(r.status_code, 200) diff --git a/addons/transifex/__init__.py b/addons/transifex/__init__.py deleted file mode 100644 index dc5e6b693d1..00000000000 --- a/addons/transifex/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import models diff --git a/addons/transifex/__manifest__.py b/addons/transifex/__manifest__.py deleted file mode 100644 index af9073d7559..00000000000 --- a/addons/transifex/__manifest__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -{ - 'name': 'Transifex integration', - 'version': '1.0', - 'summary': 'Add a link to edit a translation in Transifex', - 'category': 'Hidden/Tools', - 'description': - """ -Transifex integration -===================== -This module will add a link to the Transifex project in the translation view. -The purpose of this module is to speed up translations of the main modules. - -To work, Odoo uses Transifex configuration files `.tx/config` to detec the -project source. Custom modules will not be translated (as not published on -the main Transifex project). - -The language the user tries to translate must be activated on the Transifex -project. - """, - 'data': [ - 'data/transifex_data.xml', - 'data/ir_translation_view.xml', - ], - 'depends': ['base'], - 'license': 'LGPL-3', -} diff --git a/addons/transifex/data/ir_translation_view.xml b/addons/transifex/data/ir_translation_view.xml deleted file mode 100644 index 05062442c37..00000000000 --- a/addons/transifex/data/ir_translation_view.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - ir.translation.transifex - ir.translation - - - - - - - - - - ir.translation.transifex - ir.translation - - - - - - - - - - ir.translation.src.value.transifex - ir.translation - - - - - - - - - diff --git a/addons/transifex/data/transifex_data.xml b/addons/transifex/data/transifex_data.xml deleted file mode 100644 index a8faefd1661..00000000000 --- a/addons/transifex/data/transifex_data.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - transifex.project_url - https://www.transifex.com/odoo - - - diff --git a/addons/transifex/i18n/ar.po b/addons/transifex/i18n/ar.po deleted file mode 100644 index 180604872c1..00000000000 --- a/addons/transifex/i18n/ar.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Shaima Safar , 2018 -# Mustafa Rawi , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Mustafa Rawi , 2019\n" -"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ar\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "رابط Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "الترجمة" diff --git a/addons/transifex/i18n/az.po b/addons/transifex/i18n/az.po deleted file mode 100644 index bb7509e66f9..00000000000 --- a/addons/transifex/i18n/az.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Language-Team: Azerbaijani (https://www.transifex.com/odoo/teams/41243/az/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: az\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/bs.po b/addons/transifex/i18n/bs.po deleted file mode 100644 index fa13a398615..00000000000 --- a/addons/transifex/i18n/bs.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Bosnian (https://www.transifex.com/odoo/teams/41243/bs/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: bs\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Prevod" diff --git a/addons/transifex/i18n/ca.po b/addons/transifex/i18n/ca.po deleted file mode 100644 index 17c2d8f63a4..00000000000 --- a/addons/transifex/i18n/ca.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -# Manel Fernandez , 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Manel Fernandez , 2018\n" -"Language-Team: Catalan (https://www.transifex.com/odoo/teams/41243/ca/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ca\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Traducció" diff --git a/addons/transifex/i18n/cs.po b/addons/transifex/i18n/cs.po deleted file mode 100644 index c05a5ec419b..00000000000 --- a/addons/transifex/i18n/cs.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# trendspotter , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Last-Translator: trendspotter , 2019\n" -"Language-Team: Czech (https://www.transifex.com/odoo/teams/41243/cs/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: cs\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Překlad" diff --git a/addons/transifex/i18n/da.po b/addons/transifex/i18n/da.po deleted file mode 100644 index b1e05663b61..00000000000 --- a/addons/transifex/i18n/da.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -# lhmflexerp , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: lhmflexerp , 2018\n" -"Language-Team: Danish (https://www.transifex.com/odoo/teams/41243/da/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: da\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Oversættelse" diff --git a/addons/transifex/i18n/de.po b/addons/transifex/i18n/de.po deleted file mode 100644 index abafa95b507..00000000000 --- a/addons/transifex/i18n/de.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# AppleMentalist , 2018 -# darenkster , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: darenkster , 2018\n" -"Language-Team: German (https://www.transifex.com/odoo/teams/41243/de/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Übersetzung" diff --git a/addons/transifex/i18n/el.po b/addons/transifex/i18n/el.po deleted file mode 100644 index 8a61c681502..00000000000 --- a/addons/transifex/i18n/el.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: el\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Μετάφραση" diff --git a/addons/transifex/i18n/es.po b/addons/transifex/i18n/es.po deleted file mode 100644 index d0bd94b343e..00000000000 --- a/addons/transifex/i18n/es.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# e2f , 2018 -# Jesús Alan Ramos Rodríguez , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Jesús Alan Ramos Rodríguez , 2018\n" -"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "URL de Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Traducción" diff --git a/addons/transifex/i18n/et.po b/addons/transifex/i18n/et.po deleted file mode 100644 index b8e7ccf6cc2..00000000000 --- a/addons/transifex/i18n/et.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Rivo Zängov , 2018 -# Martin Trigaux, 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Estonian (https://www.transifex.com/odoo/teams/41243/et/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: et\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifexi URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Tõlge" diff --git a/addons/transifex/i18n/fa.po b/addons/transifex/i18n/fa.po deleted file mode 100644 index 64ae0f9f63d..00000000000 --- a/addons/transifex/i18n/fa.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Persian (https://www.transifex.com/odoo/teams/41243/fa/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: fa\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "ترجمه" diff --git a/addons/transifex/i18n/fi.po b/addons/transifex/i18n/fi.po deleted file mode 100644 index f2556edd58a..00000000000 --- a/addons/transifex/i18n/fi.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Tuomo Aura , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Tuomo Aura , 2018\n" -"Language-Team: Finnish (https://www.transifex.com/odoo/teams/41243/fi/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: fi\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Käännös" diff --git a/addons/transifex/i18n/fr.po b/addons/transifex/i18n/fr.po deleted file mode 100644 index ac2e65dfdb8..00000000000 --- a/addons/transifex/i18n/fr.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# e2f , 2018 -# Lucas Deliege , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Lucas Deliege , 2018\n" -"Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "URL Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Traduction" diff --git a/addons/transifex/i18n/gu.po b/addons/transifex/i18n/gu.po deleted file mode 100644 index 247c0a8f609..00000000000 --- a/addons/transifex/i18n/gu.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Language-Team: Gujarati (https://www.transifex.com/odoo/teams/41243/gu/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: gu\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/he.po b/addons/transifex/i18n/he.po deleted file mode 100644 index d529e60bb2d..00000000000 --- a/addons/transifex/i18n/he.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Hebrew (https://www.transifex.com/odoo/teams/41243/he/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: he\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "תרגום" diff --git a/addons/transifex/i18n/hr.po b/addons/transifex/i18n/hr.po deleted file mode 100644 index 0d9da2db851..00000000000 --- a/addons/transifex/i18n/hr.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Karolina Tonković , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Last-Translator: Karolina Tonković , 2019\n" -"Language-Team: Croatian (https://www.transifex.com/odoo/teams/41243/hr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: hr\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Prijevod" diff --git a/addons/transifex/i18n/hu.po b/addons/transifex/i18n/hu.po deleted file mode 100644 index 8df3ceef301..00000000000 --- a/addons/transifex/i18n/hu.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# krnkris, 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: krnkris, 2018\n" -"Language-Team: Hungarian (https://www.transifex.com/odoo/teams/41243/hu/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: hu\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Fordítás" diff --git a/addons/transifex/i18n/id.po b/addons/transifex/i18n/id.po deleted file mode 100644 index 425cf8f8ee0..00000000000 --- a/addons/transifex/i18n/id.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Rachmat Aditiya , 2017 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-02 11:26+0000\n" -"PO-Revision-Date: 2017-10-02 11:26+0000\n" -"Last-Translator: Rachmat Aditiya , 2017\n" -"Language-Team: Indonesian (https://www.transifex.com/odoo/teams/41243/id/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: id\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation_transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "ir.translation" -msgstr "ir.translation" diff --git a/addons/transifex/i18n/is.po b/addons/transifex/i18n/is.po deleted file mode 100644 index 0849d2d2947..00000000000 --- a/addons/transifex/i18n/is.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Bjorn Ingvarsson , 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Bjorn Ingvarsson , 2018\n" -"Language-Team: Icelandic (https://www.transifex.com/odoo/teams/41243/is/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: is\n" -"Plural-Forms: nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Translation" diff --git a/addons/transifex/i18n/it.po b/addons/transifex/i18n/it.po deleted file mode 100644 index c06120b95e3..00000000000 --- a/addons/transifex/i18n/it.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Sergio Zanchetta , 2018 -# Paolo Valier, 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Paolo Valier, 2018\n" -"Language-Team: Italian (https://www.transifex.com/odoo/teams/41243/it/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "URL di Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Traduzione" diff --git a/addons/transifex/i18n/ja.po b/addons/transifex/i18n/ja.po deleted file mode 100644 index 417bf89af8e..00000000000 --- a/addons/transifex/i18n/ja.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Japanese (https://www.transifex.com/odoo/teams/41243/ja/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ja\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "翻訳" diff --git a/addons/transifex/i18n/km.po b/addons/transifex/i18n/km.po deleted file mode 100644 index 94345be7797..00000000000 --- a/addons/transifex/i18n/km.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Language-Team: Khmer (https://www.transifex.com/odoo/teams/41243/km/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: km\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/ko.po b/addons/transifex/i18n/ko.po deleted file mode 100644 index 3aba58c1096..00000000000 --- a/addons/transifex/i18n/ko.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Link Up링크업 , 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Link Up링크업 , 2018\n" -"Language-Team: Korean (https://www.transifex.com/odoo/teams/41243/ko/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ko\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/lb.po b/addons/transifex/i18n/lb.po deleted file mode 100644 index 8b4ab9cc190..00000000000 --- a/addons/transifex/i18n/lb.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Language-Team: Luxembourgish (https://www.transifex.com/odoo/teams/41243/lb/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: lb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/lt.po b/addons/transifex/i18n/lt.po deleted file mode 100644 index a374b06a59d..00000000000 --- a/addons/transifex/i18n/lt.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2019 -# Silvija Butko , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Last-Translator: Silvija Butko , 2019\n" -"Language-Team: Lithuanian (https://www.transifex.com/odoo/teams/41243/lt/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: lt\n" -"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Vertimas" diff --git a/addons/transifex/i18n/lv.po b/addons/transifex/i18n/lv.po deleted file mode 100644 index 8d0aba9fc27..00000000000 --- a/addons/transifex/i18n/lv.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-02 11:26+0000\n" -"PO-Revision-Date: 2017-10-02 11:26+0000\n" -"Language-Team: Latvian (https://www.transifex.com/odoo/teams/41243/lv/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: lv\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation_transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "ir.translation" -msgstr "" diff --git a/addons/transifex/i18n/mn.po b/addons/transifex/i18n/mn.po deleted file mode 100644 index 71508554683..00000000000 --- a/addons/transifex/i18n/mn.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2019 -# nurbakhit nurka , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Last-Translator: nurbakhit nurka , 2019\n" -"Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: mn\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Трансфикс" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Орчуулга" diff --git a/addons/transifex/i18n/nb.po b/addons/transifex/i18n/nb.po deleted file mode 100644 index f8b30537f8b..00000000000 --- a/addons/transifex/i18n/nb.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Jan Pedro Tumusok , 2018 -# Jorunn D. Newth, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Jorunn D. Newth, 2018\n" -"Language-Team: Norwegian Bokmål (https://www.transifex.com/odoo/teams/41243/nb/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: nb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Oversettelse" diff --git a/addons/transifex/i18n/nl.po b/addons/transifex/i18n/nl.po deleted file mode 100644 index cd050f7ff88..00000000000 --- a/addons/transifex/i18n/nl.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Yenthe Van Ginneken , 2018 -# Pol Van Dingenen , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Pol Van Dingenen , 2018\n" -"Language-Team: Dutch (https://www.transifex.com/odoo/teams/41243/nl/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: nl\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Vertaling" diff --git a/addons/transifex/i18n/pl.po b/addons/transifex/i18n/pl.po deleted file mode 100644 index f014e3b61ab..00000000000 --- a/addons/transifex/i18n/pl.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Piotr Szlązak , 2018 -# Tadeusz Karpiński , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Tadeusz Karpiński , 2018\n" -"Language-Team: Polish (https://www.transifex.com/odoo/teams/41243/pl/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pl\n" -"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Strona Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Tłumaczenie" diff --git a/addons/transifex/i18n/pt.po b/addons/transifex/i18n/pt.po deleted file mode 100644 index 68bd3a5ba55..00000000000 --- a/addons/transifex/i18n/pt.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Language-Team: Portuguese (https://www.transifex.com/odoo/teams/41243/pt/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pt\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/pt_BR.po b/addons/transifex/i18n/pt_BR.po deleted file mode 100644 index 4f8659cf90b..00000000000 --- a/addons/transifex/i18n/pt_BR.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Mateus Lopes , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Mateus Lopes , 2018\n" -"Language-Team: Portuguese (Brazil) (https://www.transifex.com/odoo/teams/41243/pt_BR/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Tradução" diff --git a/addons/transifex/i18n/ro.po b/addons/transifex/i18n/ro.po deleted file mode 100644 index f92d4e44af8..00000000000 --- a/addons/transifex/i18n/ro.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Language-Team: Romanian (https://www.transifex.com/odoo/teams/41243/ro/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ro\n" -"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/ru.po b/addons/transifex/i18n/ru.po deleted file mode 100644 index d02a3badec3..00000000000 --- a/addons/transifex/i18n/ru.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Denis Trepalin , 2019 -# Vasiliy Korobatov , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2019-08-26 09:14+0000\n" -"Last-Translator: Vasiliy Korobatov , 2019\n" -"Language-Team: Russian (https://www.transifex.com/odoo/teams/41243/ru/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "URL-адрес Transifex" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Перевод" diff --git a/addons/transifex/i18n/sk.po b/addons/transifex/i18n/sk.po deleted file mode 100644 index 252b1124e75..00000000000 --- a/addons/transifex/i18n/sk.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Jaroslav Bosansky , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Jaroslav Bosansky , 2018\n" -"Language-Team: Slovak (https://www.transifex.com/odoo/teams/41243/sk/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sk\n" -"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Preklad" diff --git a/addons/transifex/i18n/sl.po b/addons/transifex/i18n/sl.po deleted file mode 100644 index 26eadf0bb3b..00000000000 --- a/addons/transifex/i18n/sl.po +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-02 11:26+0000\n" -"PO-Revision-Date: 2017-10-02 11:26+0000\n" -"Language-Team: Slovenian (https://www.transifex.com/odoo/teams/41243/sl/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sl\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation_transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "ir.translation" -msgstr "" diff --git a/addons/transifex/i18n/sr.po b/addons/transifex/i18n/sr.po deleted file mode 100644 index 370d20caf52..00000000000 --- a/addons/transifex/i18n/sr.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Serbian (https://www.transifex.com/odoo/teams/41243/sr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sr\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Prevod" diff --git a/addons/transifex/i18n/sr@latin.po b/addons/transifex/i18n/sr@latin.po deleted file mode 100644 index 1ac0a3a66f0..00000000000 --- a/addons/transifex/i18n/sr@latin.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux , 2017 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.saas~18\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-02 11:26+0000\n" -"PO-Revision-Date: 2017-10-02 11:26+0000\n" -"Last-Translator: Martin Trigaux , 2017\n" -"Language-Team: Serbian (Latin) (https://www.transifex.com/odoo/teams/41243/sr%40latin/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sr@latin\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation_transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "ir.translation" -msgstr "ir.translation" diff --git a/addons/transifex/i18n/sv.po b/addons/transifex/i18n/sv.po deleted file mode 100644 index 44632619d57..00000000000 --- a/addons/transifex/i18n/sv.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Martin Trigaux, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-22 14:10+0000\n" -"PO-Revision-Date: 2018-03-22 14:10+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" -"Language-Team: Swedish (https://www.transifex.com/odoo/teams/41243/sv/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: sv\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "ir.translation" -msgstr "ir.translation" diff --git a/addons/transifex/i18n/th.po b/addons/transifex/i18n/th.po deleted file mode 100644 index 8dc72557b7a..00000000000 --- a/addons/transifex/i18n/th.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Khwunchai Jaengsawang , 2018 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-08-24 09:33+0000\n" -"Last-Translator: Khwunchai Jaengsawang , 2018\n" -"Language-Team: Thai (https://www.transifex.com/odoo/teams/41243/th/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: th\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "การแปล" diff --git a/addons/transifex/i18n/tr.po b/addons/transifex/i18n/tr.po deleted file mode 100644 index 1f73b689f7b..00000000000 --- a/addons/transifex/i18n/tr.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Murat Kaplan , 2018 -# Ediz Duman , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Ediz Duman , 2018\n" -"Language-Team: Turkish (https://www.transifex.com/odoo/teams/41243/tr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: tr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Çeviri" diff --git a/addons/transifex/i18n/transifex.pot b/addons/transifex/i18n/transifex.pot deleted file mode 100644 index 5ea595ad9f8..00000000000 --- a/addons/transifex/i18n/transifex.pot +++ /dev/null @@ -1,45 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~14.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-09-14 10:29+0000\n" -"PO-Revision-Date: 2021-09-14 10:29+0000\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_src_value_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Contribute" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,help:transifex.field_ir_translation__transifex_url -msgid "Propose a modification in the official version of Odoo" -msgstr "" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_src_value_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "" diff --git a/addons/transifex/i18n/uk.po b/addons/transifex/i18n/uk.po deleted file mode 100644 index efc2d5bee97..00000000000 --- a/addons/transifex/i18n/uk.po +++ /dev/null @@ -1,36 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# Alina Lisnenko , 2018 -# Bohdan Lisnenko, 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Bohdan Lisnenko, 2018\n" -"Language-Team: Ukrainian (https://www.transifex.com/odoo/teams/41243/uk/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: uk\n" -"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex URL" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Переклад" diff --git a/addons/transifex/i18n/vi.po b/addons/transifex/i18n/vi.po deleted file mode 100644 index 319c225be0d..00000000000 --- a/addons/transifex/i18n/vi.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# thanh nguyen , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: thanh nguyen , 2018\n" -"Language-Team: Vietnamese (https://www.transifex.com/odoo/teams/41243/vi/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: vi\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "Bản dịch" diff --git a/addons/transifex/i18n/zh_CN.po b/addons/transifex/i18n/zh_CN.po deleted file mode 100644 index 2be5dece2e4..00000000000 --- a/addons/transifex/i18n/zh_CN.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# e2f , 2018 -# e2f_cn c7 , 2018 -# Jeffery CHEN Fan , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Jeffery CHEN Fan , 2018\n" -"Language-Team: Chinese (China) (https://www.transifex.com/odoo/teams/41243/zh_CN/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: zh_CN\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex 网址" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "翻译" diff --git a/addons/transifex/i18n/zh_TW.po b/addons/transifex/i18n/zh_TW.po deleted file mode 100644 index 8091f164c81..00000000000 --- a/addons/transifex/i18n/zh_TW.po +++ /dev/null @@ -1,35 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * transifex -# -# Translators: -# 敬雲 林 , 2018 -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: 敬雲 林 , 2018\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/odoo/teams/41243/zh_TW/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: zh_TW\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_dialog_view_tree_transifex -#: model_terms:ir.ui.view,arch_db:transifex.ir_translation_view_tree_transifex -msgid "Transifex" -msgstr "Transifex" - -#. module: transifex -#: model:ir.model.fields,field_description:transifex.field_ir_translation__transifex_url -msgid "Transifex URL" -msgstr "Transifex 網址" - -#. module: transifex -#: model:ir.model,name:transifex.model_ir_translation -msgid "Translation" -msgstr "翻譯" diff --git a/addons/transifex/models/__init__.py b/addons/transifex/models/__init__.py deleted file mode 100644 index 76fec4841b3..00000000000 --- a/addons/transifex/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import ir_translation diff --git a/addons/transifex/models/ir_translation.py b/addons/transifex/models/ir_translation.py deleted file mode 100644 index d6acdb5f7cb..00000000000 --- a/addons/transifex/models/ir_translation.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from configparser import ConfigParser -from os.path import join as opj -import os -import werkzeug.urls - -import odoo -from odoo import models, fields - - -class IrTranslation(models.Model): - - _inherit = 'ir.translation' - - transifex_url = fields.Char("Transifex URL", compute='_get_transifex_url', help="Propose a modification in the official version of Odoo") - - def _get_transifex_url(self): - """ Construct transifex URL based on the module on configuration """ - # e.g. 'https://www.transifex.com/odoo/' - base_url = self.env['ir.config_parameter'].sudo().get_param('transifex.project_url') - - tx_config_file = ConfigParser() - tx_sections = [] - for addon_path in odoo.addons.__path__: - tx_path = opj(addon_path, '.tx', 'config') - if os.path.isfile(tx_path): - tx_config_file.read(tx_path) - # first section is [main], after [odoo-11.sale] - tx_sections.extend(tx_config_file.sections()[1:]) - - # parent directory ad .tx/config is root directory in odoo/odoo - tx_path = opj(addon_path, os.pardir, '.tx', 'config') - if os.path.isfile(tx_path): - tx_config_file.read(tx_path) - tx_sections.extend(tx_config_file.sections()[1:]) - - if not base_url or not tx_sections: - self.update({'transifex_url': False}) - else: - base_url = base_url.rstrip('/') - - # will probably be the same for all terms, avoid multiple searches - translation_languages = list(set(self.mapped('lang'))) - languages = self.env['res.lang'].with_context(active_test=False).search( - [('code', 'in', translation_languages)]) - - language_codes = dict((l.code, l.iso_code) for l in languages) - - # .tx/config files contains the project reference - # using ini files like '[odoo-master.website_sale]' - translation_modules = set(self.mapped('module')) - project_modules = {} - for module in translation_modules: - for section in tx_sections: - tx_project, tx_mod = section.split('.') - if tx_mod == module: - project_modules[module] = tx_project - - for translation in self: - if not translation.module or not translation.src or translation.lang == 'en_US': - # custom or source term - translation.transifex_url = False - continue - - lang_code = language_codes.get(translation.lang) - if not lang_code: - translation.transifex_url = False - continue - - project = project_modules.get(translation.module) - if not project: - translation.transifex_url = False - continue - - # e.g. https://www.transifex.com/odoo/odoo-10/translate/#fr/sale/42?q=text:'Sale+Order' - src = werkzeug.urls.url_quote_plus(translation.src[:50].replace("\n", "").replace("'", "\\'")) - src = f"'{src}'" if "+" in src else src - translation.transifex_url = "%(url)s/%(project)s/translate/#%(lang)s/%(module)s/42?q=%(src)s" % { - 'url': base_url, - 'project': project, - 'lang': lang_code, - 'module': translation.module, - 'src': f"text%3A{src}", - } diff --git a/addons/transifex/static/description/icon.png b/addons/transifex/static/description/icon.png deleted file mode 100644 index 9e6c9556004..00000000000 Binary files a/addons/transifex/static/description/icon.png and /dev/null differ diff --git a/addons/transifex/static/description/icon.svg b/addons/transifex/static/description/icon.svg deleted file mode 100644 index a1c957930b5..00000000000 --- a/addons/transifex/static/description/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/addons/web/controllers/webclient.py b/addons/web/controllers/webclient.py index d5e1b7e76ac..7b5fdc4ce90 100644 --- a/addons/web/controllers/webclient.py +++ b/addons/web/controllers/webclient.py @@ -96,7 +96,7 @@ class WebClient(http.Controller): elif mods is None: mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or []) - translations_per_module, lang_params = request.env["ir.translation"].get_translations_for_webclient(mods, lang) + translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang) body = json.dumps({ 'lang': lang, diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index 1be2dfa9f20..8ebc6dc1f8a 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -104,7 +104,7 @@ class Http(models.AbstractModel): "max_file_upload_size": max_file_upload_size, "home_action_id": user.action_id.id, "cache_hashes": { - "translations": request.env['ir.translation'].sudo().get_web_translations_hash( + "translations": request.env['ir.http'].sudo().get_web_translations_hash( mods, request.session.context['lang'] ) if session_uid else None, }, diff --git a/addons/web/static/src/legacy/js/views/basic/basic_controller.js b/addons/web/static/src/legacy/js/views/basic/basic_controller.js index bd48e2ab3bf..8fc49d99031 100644 --- a/addons/web/static/src/legacy/js/views/basic/basic_controller.js +++ b/addons/web/static/src/legacy/js/views/basic/basic_controller.js @@ -944,25 +944,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { } var record = this.model.get(ev.data.id, { raw: true }); var res_id = record.res_id || record.res_ids[0]; - var result = await this._rpc({ - route: '/web/dataset/call_button', - params: { - model: 'ir.translation', - method: 'translate_fields', - args: [record.model, res_id, ev.data.fieldName], - kwargs: { context: record.getContext() }, - } - }); this.translationDialog = new TranslationDialog(this, { - domain: result.domain, - searchName: result.context.search_default_name, fieldName: ev.data.fieldName, + resId: res_id, userLanguageValue: ev.target.value || '', + dataPointModel: record.model, dataPointID: record.id, isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert, - isText: result.context.translation_type === 'text', - showSrc: result.context.translation_show_src, + context: record.getContext(), }); return this.translationDialog.open(); }, diff --git a/addons/web/static/src/legacy/js/widgets/translation_dialog.js b/addons/web/static/src/legacy/js/widgets/translation_dialog.js index 4e145bff33e..2655304a3d0 100644 --- a/addons/web/static/src/legacy/js/widgets/translation_dialog.js +++ b/addons/web/static/src/legacy/js/widgets/translation_dialog.js @@ -14,29 +14,24 @@ odoo.define('web.TranslationDialog', function (require) { * @constructor * @param {Widget} parent * @param {Object} [options] - * @param {string} [options.domain] the domain needed to get the translation terms * @param {string} [options.fieldName] the name of the field currently translated (from the model of the form view) - * @param {string} [options.searchName] the name of the actual field that is the reference for translation (in the form of model,field) + * @param {integer} [options.resId] the ID of record currently translated * @param {string} [options.userLanguageValue] the value of the translation in the language of the user, as seen in the from view (might be empty) * @param {string} [options.dataPointID] the data point id of the record for which we do the translations * @param {boolean} [options.isComingFromTranslationAlert] the initiator of the dialog, might be a link on a field or the translation alert on top of the form - * @param {boolean} [options.isText] is the field a text field (multiline) or char (single line) - * @param {boolean} [options.showSrc] is the source of the translation should be rendered (for partial translations, i.e. XML content) * */ init: function (parent, options) { options = options || {}; this.fieldName = options.fieldName; - this.domain = options.domain; - this.searchName = options.searchName; + this.resId = options.resId; this.userLanguageValue = options.userLanguageValue; - this.domain.push(['name', "=", `${this.searchName}`]); + this.dataPointModel = options.dataPointModel; this.dataPointID = options.dataPointID; this.isComingFromTranslationAlert = options.isComingFromTranslationAlert; this.currentInterfaceLanguage = session.user_context.lang; - this.isText = options.isText; - this.showSrc = options.showSrc; + this.context = options.context; this._super(parent, _.extend({ size: 'large', @@ -56,31 +51,35 @@ odoo.define('web.TranslationDialog', function (require) { this._loadLanguages().then((l) => { this.languages = l; return this._loadTranslations().then((t) => { - this.translations = t; + [this.translations, this.context] = t; + let id = 1; + this.translations.forEach((t) => t['id'] = id++); + this.isText = this.context.translation_type === 'text'; + this.showSource = this.context.translation_show_source; }); }), ]).then(() => { this.data = this.translations.map((term) => { let relatedLanguage = this.languages.find((language) => language[0] === term.lang); - if (!term.value && !this.showSrc) { - term.value = term.src; + if (!term.value && !this.showSource) { + term.value = term.source; } return { id: term.id, lang: term.lang, langName: relatedLanguage[1], - source: term.src, + source: term.source, // we set the translation value coming from the database, except for the language // the user is currently utilizing. Then we set the translation value coming // from the value of the field in the form value: (term.lang === this.currentInterfaceLanguage && - !this.showSrc && + !this.showSource && !this.isComingFromTranslationAlert) ? this.userLanguageValue : term.value || '' }; }); this.data.sort((left, right) => - (left.langName < right.langName || (left.langName === right.langName && left.source < right.source)) ? -1 : 1); + left.langName.localeCompare(right.langName)); }); }, @@ -92,12 +91,11 @@ odoo.define('web.TranslationDialog', function (require) { * @private */ _loadTranslations: function () { - const domain = [...this.domain, ['lang', 'in', this.languages.map(l => l[0])]]; return this._rpc({ - model: 'ir.translation', - method: 'search_read', - fields: ['lang', 'src', 'value'], - domain: domain, + model: this.dataPointModel, + method: 'get_field_translations', + args: [[this.resId], this.fieldName], + context: this.context, }); }, /** @@ -137,16 +135,16 @@ odoo.define('web.TranslationDialog', function (require) { this.el.querySelectorAll('input[type=text],textarea').forEach((t) => { var initialValue = this.data.find((d) => d.id == t.dataset.id); if (initialValue.value !== t.value) { - updatedTerm[t.dataset.id] = t.value; + updatedTerm[t.dataset.id] = {lang: initialValue.lang, source: initialValue.source, value: t.value}; - if (initialValue.lang === this.currentInterfaceLanguage && !this.showSrc) { + if (initialValue.lang === this.currentInterfaceLanguage && !this.showSource) { // when the user has changed the term for the language he is // using in the interface, this change should be reflected // in the form view // partial translations being handled server side are // also ignored var changes = {}; - changes[this.fieldName] = updatedTerm[initialValue.id]; + changes[this.fieldName] = t.value; updateFormViewField = { dataPointID: this.dataPointID, changes: changes, @@ -158,16 +156,23 @@ odoo.define('web.TranslationDialog', function (require) { // updatedTerm only contains the id and values of the terms that // have been updated by the user - var saveUpdatedTermsProms = Object.keys(updatedTerm).map((id) => { - var writeTranslation = { - model: 'ir.translation', - method: 'write', - context: this.context, - args: [[parseInt(id, 10)], { value: updatedTerm[id] }] - }; - return this._rpc(writeTranslation); - }); - return Promise.all(saveUpdatedTermsProms).then(() => { + const translations = {}; + if (this.showSource) { // model terms translation + Object.entries(updatedTerm).forEach(([id, term]) => { + if (!translations[term.lang]) { + translations[term.lang] = {}; + } + translations[term.lang][term.source] = term.value; + }); + } + else { // model translation + Object.entries(updatedTerm).forEach(([id, term]) => translations[term.lang] = term.value); + } + return this._rpc({ + model: this.dataPointModel, + method: 'update_field_translations', + args: [[this.resId], this.fieldName, translations], + }).then(() => { // we might have to update the value of the field on the form // view that opened the translation dialog if (updateFormViewField) { diff --git a/addons/web/static/src/legacy/scss/form_view.scss b/addons/web/static/src/legacy/scss/form_view.scss index 691d2a8a06b..9fd8d25a8c0 100644 --- a/addons/web/static/src/legacy/scss/form_view.scss +++ b/addons/web/static/src/legacy/scss/form_view.scss @@ -713,6 +713,20 @@ $o-form-label-margin-right: 0px; padding-right: 25px; } } + .oe_no_translation_content { + &:not(.btn) { + display: none; + } + &.btn { + width: 100% !important; + margin-left: 0px; + visibility: hidden; + } + &.btn:after { + content: 'EN'; + visibility: visible; + } + } // Text field with oe_inline class .o_field_text.oe_inline { diff --git a/addons/web/static/src/legacy/xml/translation_dialog.xml b/addons/web/static/src/legacy/xml/translation_dialog.xml index 2ccfc981ec1..37bc49a1a61 100644 --- a/addons/web/static/src/legacy/xml/translation_dialog.xml +++ b/addons/web/static/src/legacy/xml/translation_dialog.xml @@ -2,13 +2,13 @@
-
+
-
+
-
+
diff --git a/addons/web/static/src/views/fields/translation_button.js b/addons/web/static/src/views/fields/translation_button.js index 769ecffaea0..a92a79ce66e 100644 --- a/addons/web/static/src/views/fields/translation_button.js +++ b/addons/web/static/src/views/fields/translation_button.js @@ -17,7 +17,6 @@ const { Component, useEnv } = owl; */ export function useTranslationDialog() { const dialog = useService("dialog"); - const rpc = useService("rpc"); const env = useEnv(); async function openTranslationDialog({ record, fieldName, updateField }) { @@ -43,22 +42,14 @@ export function useTranslationDialog() { return; } } - const { resModel, resId, context } = record; - const result = await rpc("/web/dataset/call_button", { - model: "ir.translation", - method: "translate_fields", - args: [resModel, resId, fieldName], - kwargs: { context }, - }); + const { resModel, resId } = record; dialog.add(TranslationDialog, { - domain: result.domain, - searchName: result.context.search_default_name, fieldName: fieldName, + resId: resId, + resModel: resModel, userLanguageValue: record.data[fieldName] || "", isComingFromTranslationAlert: false, - isText: result.context.translation_type === "text", - showSource: result.context.translation_show_src, updateField, }); } diff --git a/addons/web/static/src/views/fields/translation_dialog.js b/addons/web/static/src/views/fields/translation_dialog.js index 0cf8348e013..5e227d8d517 100644 --- a/addons/web/static/src/views/fields/translation_dialog.js +++ b/addons/web/static/src/views/fields/translation_dialog.js @@ -20,18 +20,22 @@ export class TranslationDialog extends Component { onWillStart(async () => { const languages = await loadLanguages(this.orm); - const translations = await this.loadTranslations(languages); + const [translations, context] = await this.loadTranslations(languages); + let id = 1; + translations.forEach((t) => (t.id = id++)); + this.props.isText = context.translation_type === "text"; + this.props.showSource = context.translation_show_source; this.terms = translations.map((term) => { const relatedLanguage = languages.find((l) => l[0] === term.lang); if (!term.value && !this.props.showSource) { - term.value = term.src; + term.value = term.source; } return { id: term.id, lang: term.lang, langName: relatedLanguage[1], - source: term.src, + source: term.source, // we set the translation value coming from the database, except for the language // the user is currently utilizing. Then we set the translation value coming // from the value of the field in the form @@ -43,11 +47,7 @@ export class TranslationDialog extends Component { : term.value || "", }; }); - this.terms.sort((a, b) => - a.langName < b.langName || (a.langName === b.langName && a.source < b.source) - ? -1 - : 1 - ); + this.terms.sort((a, b) => a.langName.localeCompare(b.langName)); }); } @@ -63,23 +63,37 @@ export class TranslationDialog extends Component { * Load the translation terms for the installed language, for the current model and res_id */ async loadTranslations(languages) { - const domain = [...this.domain, ["lang", "in", languages.map((l) => l[0])]]; - return this.orm.searchRead("ir.translation", domain, ["lang", "src", "value"]); + return this.orm.call(this.props.resModel, "get_field_translations", [ + [this.props.resId], + this.props.fieldName, + ]); } /** * Save all the terms that have been updated */ async onSave() { - await Promise.all( - this.terms.map(async (term) => { - if (term.id in this.updatedTerms && term.value !== this.updatedTerms[term.id]) { - await this.orm.write("ir.translation", [term.id], { - value: this.updatedTerms[term.id], - }); + const translations = {}; + + this.terms.map((term) => { + const updatedTermValue = this.updatedTerms[term.id]; + if (term.id in this.updatedTerms && term.value !== updatedTermValue) { + if (this.props.showSource) { + if (!translations[term.lang]) { + translations[term.lang] = {}; + } + translations[term.lang][term.source] = updatedTermValue; + } else { + translations[term.lang] = updatedTermValue; } - }) - ); + } + }); + + await this.orm.call(this.props.resModel, "update_field_translations", [ + [this.props.resId], + this.props.fieldName, + translations, + ]); // we might have to update the value of the field on the form // view that opened the translation dialog diff --git a/addons/web/static/src/views/form/form_controller.scss b/addons/web/static/src/views/form/form_controller.scss index c269c4d179b..09af88c8b16 100644 --- a/addons/web/static/src/views/form/form_controller.scss +++ b/addons/web/static/src/views/form/form_controller.scss @@ -436,6 +436,20 @@ $o-form-label-margin-right: 0px; padding-right: 25px; } } + .oe_no_translation_content { + :not(.btn) { + display: none; + } + .btn { + width: 100% !important; + margin-left: 0px; + visibility: hidden; + } + .btn:after { + content: 'EN'; + visibility: visible; + } + } iframe.wysiwyg_iframe + .o_field_translate { right: 30px !important; top: 7px !important; diff --git a/addons/web/static/src/webclient/actions/debug_items.js b/addons/web/static/src/webclient/actions/debug_items.js index 87ab7ec8b7b..ec4bf238512 100644 --- a/addons/web/static/src/webclient/actions/debug_items.js +++ b/addons/web/static/src/webclient/actions/debug_items.js @@ -101,25 +101,6 @@ function manageFilters({ action, env }) { }; } -function technicalTranslation({ action, env }) { - if (!action.res_model) { - return null; - } - return { - type: "item", - description: env._t("Technical Translation"), - callback: async () => { - const result = await env.services.orm.call( - "ir.translation", - "get_technical_translations", - [action.res_model] - ); - env.services.action.doAction(result); - }, - sequence: 140, - }; -} - function viewAccessRights({ accessRights, action, env }) { if (!action.res_model || !accessRights.canSeeModelAccess) { return null; @@ -190,7 +171,6 @@ debugRegistry .add("editAction", editAction) .add("viewFields", viewFields) .add("manageFilters", manageFilters) - .add("technicalTranslation", technicalTranslation) .add("accessSeparator", accessSeparator) .add("viewAccessRights", viewAccessRights) .add("viewRecordRules", viewRecordRules); diff --git a/addons/web/static/tests/legacy/fields/basic_fields_tests.js b/addons/web/static/tests/legacy/fields/basic_fields_tests.js index 17b41385726..4f2152ea32c 100644 --- a/addons/web/static/tests/legacy/fields/basic_fields_tests.js +++ b/addons/web/static/tests/legacy/fields/basic_fields_tests.js @@ -160,19 +160,6 @@ QUnit.module('Legacy basic_fields', { position: "after", }] }, - "ir.translation": { - fields: { - lang: {type: "char"}, - value: {type: "char"}, - res_id: {type: "integer"} - }, - records: [{ - id: 99, - res_id: 37, - value: '', - lang: 'en_US' - }] - }, }; target = getFixture(); @@ -1653,25 +1640,18 @@ QUnit.module('Legacy basic_fields', { user_context: {lang: 'en_US'}, }, mockRPC: function (route, args) { - if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { - assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); - return Promise.resolve({ - domain: [], - context: {search_default_name: 'partnes,foo'}, - }); + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record"); + return Promise.resolve([ + [{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}], + {translation_type: "char", translation_show_source: false}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); } - if (args.method === "search_read" && args.model == "ir.translation") { - return Promise.resolve([ - {lang: 'en_US', src: 'yop', value: 'yop', id: 42}, - {lang: 'fr_BE', src: 'yop', value: 'valeur français', id: 43} - ]); - } - if (args.method === "write" && args.model == "ir.translation") { - assert.deepEqual(args.args[1], {value: "english value"}, - "the new translation value should be written"); + if (route === "/web/dataset/call_kw/partner/update_field_translations") { + assert.deepEqual(args.args, [[1], "foo", {"en_US": "english value"}], "the new translation value should be written"); return Promise.resolve(); } return this._super.apply(this, arguments); @@ -1741,31 +1721,21 @@ QUnit.module('Legacy basic_fields', { user_context: {lang: 'en_US'}, }, mockRPC: function (route, args) { - if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { - assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); - return Promise.resolve({ - domain: [], - context: { - search_default_name: 'partner,foo', - translation_type: 'char', - translation_show_src: true, - }, - }); + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record"); + return Promise.resolve([ + [{lang: "en_US", source: "first paragraph", value: "first paragraph"}, + {lang: "en_US", source: "second paragraph", value: "second paragraph"}, + {lang: "fr_BE", source: "first paragraph", value: "premier paragraphe"}, + {lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}], + {translation_type: "char", translation_show_source: true}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); } - if (args.method === "search_read" && args.model == "ir.translation") { - return Promise.resolve([ - {lang: 'en_US', src: 'first paragraph', value: 'first paragraph', id: 42}, - {lang: 'en_US', src: 'second paragraph', value: 'second paragraph', id: 43}, - {lang: 'fr_BE', src: 'first paragraph', value: 'premier paragraphe', id: 44}, - {lang: 'fr_BE', src: 'second paragraph', value: 'deuxième paragraphe', id: 45}, - ]); - } - if (args.method === "write" && args.model == "ir.translation") { - assert.deepEqual(args.args[1], {value: "first paragraph modified"}, - "Wrong update on translation"); + if (route === "/web/dataset/call_kw/partner/update_field_translations") { + assert.deepEqual(args.args, [[1], "foo", {"en_US": {"first paragraph": "first paragraph modified"}}], "the new translation value should be written"); return Promise.resolve(); } return this._super.apply(this, arguments); @@ -2694,15 +2664,15 @@ QUnit.module('Legacy basic_fields', { '', res_id: 1, mockRPC: function (route, args) { - if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { - assert.deepEqual(args.args, ["partner",1,"txt"], 'should call "call_button" route'); - return Promise.resolve({ - domain: [], - context: {search_default_name: 'partnes,foo'}, - }); + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + assert.deepEqual(args.args, [[1],"txt"], "should translate the txt field of the record"); + return Promise.resolve([ + [{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}], + {translation_type: "text", translation_show_source: false}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return Promise.resolve([["en_US"], ["fr_BE"]]); + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); } return this._super.apply(this, arguments); }, diff --git a/addons/web/static/tests/legacy/views/form_tests.js b/addons/web/static/tests/legacy/views/form_tests.js index 3d075d30439..96e348b6b7b 100644 --- a/addons/web/static/tests/legacy/views/form_tests.js +++ b/addons/web/static/tests/legacy/views/form_tests.js @@ -140,19 +140,6 @@ QUnit.module('LegacyViews', { {id: 14, display_name: "silver", color: 5}, ] }, - "ir.translation": { - fields: { - lang_code: {type: "char"}, - value: {type: "char"}, - res_id: {type: "integer"} - }, - records: [{ - id: 99, - res_id: 12, - value: '', - lang_code: 'en_US' - }] - }, user: { fields: { name: {string: "Name", type: "char"}, @@ -7382,16 +7369,7 @@ QUnit.module('LegacyViews', { }); QUnit.test('translation alerts preserved on reverse breadcrumb', async function (assert) { - assert.expect(2); - - serverData.models['ir.translation'] = { - fields: { - name: { string: "name", type: "char" }, - source: {string: "Source", type: "char"}, - value: {string: "Value", type: "char"}, - }, - records: [], - }; + assert.expect(1); serverData.models.partner.fields.foo.translate = true; @@ -7402,12 +7380,6 @@ QUnit.module('LegacyViews', { '' + '', 'partner,false,search': '', - 'ir.translation,false,list': '' + - '' + - '' + - '' + - '', - 'ir.translation,false,search': '', }; serverData.actions = { @@ -7418,14 +7390,6 @@ QUnit.module('LegacyViews', { type: 'ir.actions.act_window', views: [[false, 'form']], }, - 2: { - id: 2, - name: 'Translate', - res_model: 'ir.translation', - type: 'ir.actions.act_window', - views: [[false, 'list']], - target: 'current', - } }; const webClient = await createWebClient({ serverData }); @@ -7440,13 +7404,6 @@ QUnit.module('LegacyViews', { assert.containsOnce(target, '.o_legacy_form_view .alert > div', "should have a translation alert"); - - await doAction(webClient, 2); - - await testUtils.dom.click($('.o_control_panel .breadcrumb a:first')); - await legacyExtraNextTick(); - assert.containsOnce(target, '.o_legacy_form_view .alert > div', - "should have a translation alert"); }); QUnit.test('translate event correctly handled with multiple controllers', async function (assert) { @@ -7485,16 +7442,16 @@ QUnit.module('LegacyViews', { if (route === '/web/dataset/call_kw/product/get_formview_id') { return Promise.resolve(false); } - if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { - assert.deepEqual(args.args, ["product",37,"name"], 'should call "call_button" route'); + if (route === "/web/dataset/call_kw/product/get_field_translations") { + assert.deepEqual(args.args, [[37],"name"], "should translate the name field of the record"); nbTranslateCalls++; - return Promise.resolve({ - domain: [], - context: {search_default_name: 'partnes,foo'}, - }); + return Promise.resolve([ + [{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}], + {translation_type: "char", translation_show_source: false}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return Promise.resolve([["en_US"], ["fr_BE"]]); + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); } return this._super.apply(this, arguments); }, diff --git a/addons/web/static/tests/legacy/views/list_tests.js b/addons/web/static/tests/legacy/views/list_tests.js index 40bf500d8e5..3e6190f3334 100644 --- a/addons/web/static/tests/legacy/views/list_tests.js +++ b/addons/web/static/tests/legacy/views/list_tests.js @@ -123,31 +123,6 @@ QUnit.module('LegacyViews', { {id: "2-20170808020000", name: "virtual"}, ] }, - "ir.translation": { - fields: { - lang_code: {type: "char"}, - src: {type: "char"}, - value: {type: "char"}, - res_id: {type: "integer"}, - name: {type: "char"}, - lang: {type: "char"}, - }, - records: [{ - id: 99, - res_id: 1, - value: '', - lang_code: 'en_US', - lang: 'en_US', - name: 'foo,foo' - },{ - id: 100, - res_id: 1, - value: '', - lang_code: 'fr_BE', - lang: 'fr_BE', - name: 'foo,foo' - }] - }, }; serverData = { models: this.data }; @@ -3620,11 +3595,11 @@ QUnit.module('LegacyViews', { model: 'foo', data: this.data, mockRPC: function (route, args) { - if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { - return Promise.resolve({ - domain: [], - context: {search_default_name: 'foo,foo'}, - }); + if (route === "/web/dataset/call_kw/foo/get_field_translations") { + return Promise.resolve([ + [{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}], + {translation_type: "char", translation_show_source: false}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([["en_US","English"], ["fr_BE", "Frenglish"]]); diff --git a/addons/web/static/tests/views/fields/char_field_tests.js b/addons/web/static/tests/views/fields/char_field_tests.js index 213c0473996..05bd6634daf 100644 --- a/addons/web/static/tests/views/fields/char_field_tests.js +++ b/addons/web/static/tests/views/fields/char_field_tests.js @@ -123,21 +123,6 @@ QUnit.module("Fields", (hooks) => { }, ], }, - "ir.translation": { - fields: { - lang: { type: "char" }, - value: { type: "char" }, - res_id: { type: "integer" }, - }, - records: [ - { - id: 99, - res_id: 37, - value: "", - lang: "en_US", - }, - ], - }, }, }; @@ -301,7 +286,7 @@ QUnit.module("Fields", (hooks) => { }); QUnit.test("char field translatable", async function (assert) { - assert.expect(13); + assert.expect(11); serverData.models.partner.fields.foo.translate = true; serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), { @@ -325,33 +310,25 @@ QUnit.module("Fields", (hooks) => { `, mockRPC(route, { args, method, model }) { - if (route === "/web/dataset/call_button" && method === "translate_fields") { - assert.deepEqual( - args, - ["partner", 1, "foo"], - 'should call "call_button" route' - ); - return Promise.resolve({ - domain: [], - context: { search_default_name: "partnes,foo" }, - }); - } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([ ["en_US", "English"], ["fr_BE", "French (Belgium)"], ]); } - if (method === "search_read" && model === "ir.translation") { + if (route === "/web/dataset/call_kw/partner/get_field_translations") { return Promise.resolve([ - { lang: "en_US", src: "yop", value: "yop", id: 42 }, - { lang: "fr_BE", src: "yop", value: "valeur français", id: 43 }, + [ + { lang: "en_US", source: "yop", value: "yop" }, + { lang: "fr_BE", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, ]); } - if (method === "write" && model === "ir.translation") { + if (route === "/web/dataset/call_kw/partner/update_field_translations") { assert.deepEqual( - args[1], - { value: "english value" }, + args[2], + { en_US: "english value" }, "the new translation value should be written" ); return Promise.resolve(null); @@ -417,7 +394,7 @@ QUnit.module("Fields", (hooks) => { }); QUnit.test("html field translatable", async function (assert) { - assert.expect(6); + assert.expect(5); serverData.models.partner.fields.foo.translate = true; serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), { @@ -441,55 +418,48 @@ QUnit.module("Fields", (hooks) => { `, mockRPC(route, { args, method, model }) { - if (route === "/web/dataset/call_button" && method === "translate_fields") { - assert.deepEqual( - args, - ["partner", 1, "foo"], - `should call "call_button" route` - ); - return Promise.resolve({ - domain: [], - context: { - search_default_name: "partner,foo", - translation_type: "char", - translation_show_src: true, - }, - }); - } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([ ["en_US", "English"], ["fr_BE", "French (Belgium)"], ]); } - if (method === "search_read" && model === "ir.translation") { + if (route === "/web/dataset/call_kw/partner/get_field_translations") { return Promise.resolve([ - { lang: "en_US", src: "first paragraph", value: "first paragraph", id: 42 }, + [ + { + lang: "en_US", + source: "first paragraph", + value: "first paragraph", + }, + { + lang: "en_US", + source: "second paragraph", + value: "second paragraph", + }, + { + lang: "fr_BE", + source: "first paragraph", + value: "premier paragraphe", + }, + { + lang: "fr_BE", + source: "second paragraph", + value: "deuxième paragraphe", + }, + ], { - lang: "en_US", - src: "second paragraph", - value: "second paragraph", - id: 43, - }, - { - lang: "fr_BE", - src: "first paragraph", - value: "premier paragraphe", - id: 44, - }, - { - lang: "fr_BE", - src: "second paragraph", - value: "deuxième paragraphe", - id: 45, + translation_type: "char", + translation_show_source: true, }, ]); } - if (method === "write" && model === "ir.translation") { + + if (route === "/web/dataset/call_kw/partner/update_field_translations") { assert.deepEqual( - args[1], - { value: "first paragraph modified" }, - "Wrong update on translation" + args[2], + { en_US: { "first paragraph": "first paragraph modified" } }, + "the new translation value should be written" ); return Promise.resolve(null); } diff --git a/addons/web/static/tests/views/fields/text_field_tests.js b/addons/web/static/tests/views/fields/text_field_tests.js index 3c878d98426..e216d15286c 100644 --- a/addons/web/static/tests/views/fields/text_field_tests.js +++ b/addons/web/static/tests/views/fields/text_field_tests.js @@ -49,21 +49,6 @@ QUnit.module("Fields", (hooks) => { }, ], }, - "ir.translation": { - fields: { - lang: { type: "char" }, - value: { type: "char" }, - res_id: { type: "integer" }, - }, - records: [ - { - id: 99, - res_id: 37, - value: "", - lang: "en_US", - }, - ], - }, }, }; @@ -370,7 +355,7 @@ QUnit.module("Fields", (hooks) => { }); QUnit.test("text field translatable", async function (assert) { - assert.expect(4); + assert.expect(3); serverData.models.partner.fields.txt.translate = true; serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), { @@ -391,19 +376,20 @@ QUnit.module("Fields", (hooks) => { `, mockRPC(route, { args, method }) { - if (route === "/web/dataset/call_button" && method === "translate_fields") { - assert.deepEqual( - args, - ["partner", 1, "txt"], - `should call "call_button" route` - ); - return Promise.resolve({ - domain: [], - context: { search_default_name: "partnes,foo" }, - }); - } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return Promise.resolve([["en_US"], ["fr_BE"]]); + return Promise.resolve([ + ["en_US", "English"], + ["fr_BE", "French (Belgium)"], + ]); + } + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + return Promise.resolve([ + [ + { lang: "en_US", source: "yop", value: "yop" }, + { lang: "fr_BE", source: "yop", value: "valeur français" }, + ], + { translation_type: "text", translation_show_source: false }, + ]); } }, }); diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 8cf882e1ae7..270d27a7209 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -180,21 +180,6 @@ QUnit.module("Views", (hooks) => { { id: 14, display_name: "silver", color: 5 }, ], }, - "ir.translation": { - fields: { - lang_code: { type: "char" }, - value: { type: "char" }, - res_id: { type: "integer" }, - }, - records: [ - { - id: 99, - res_id: 12, - value: "", - lang_code: "en_US", - }, - ], - }, user: { fields: { name: { string: "Name", type: "char" }, @@ -8466,20 +8451,16 @@ QUnit.module("Views", (hooks) => { ["CUST2", "second custom"], ]; } - if (args.method === "translate_fields") { + if (route === "/web/dataset/call_kw/partner/get_field_translations") { assert.step(`translate args ${JSON.stringify(args.args)}`); assert.step(`translate context ${JSON.stringify(args.kwargs.context)}`); - return { - domain: [["res_id", "=", args.args[1]]], - context: { search_default_name: "partner_type,foo" }, - }; - } - if (args.method === "search_read" && args.model === "ir.translation") { - assert.step( - `search_read translations args: ${JSON.stringify( - args.args - )} ; kwargs: ${JSON.stringify(args.kwargs)}` - ); + return Promise.resolve([ + [ + { lang: "CUST", source: "yop", value: "yop" }, + { lang: "CUST2", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, + ]); } }, }); @@ -8487,9 +8468,8 @@ QUnit.module("Views", (hooks) => { await click(target, ".o_field_translate.btn-link"); assert.verifySteps([ - `translate args ["partner",1,"foo"]`, + `translate args [[1],"foo"]`, `translate context {"lang":"en","uid":7,"tz":"taht"}`, - `search_read translations args: [] ; kwargs: {"domain":[["res_id","=",1],["name","=","partner_type,foo"],["lang","in",["CUST","CUST2"]]],"fields":["lang","src","value"],"context":{"lang":"en","uid":7,"tz":"taht"}}`, ]); assert.containsOnce(target, ".modal"); @@ -8522,6 +8502,15 @@ QUnit.module("Views", (hooks) => { ["CUST2", "second custom"], ]; } + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + return Promise.resolve([ + [ + { lang: "CUST", source: "yop", value: "yop" }, + { lang: "CUST2", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, + ]); + } }, }); @@ -8536,7 +8525,7 @@ QUnit.module("Views", (hooks) => { await click(target, ".o_field_translate.btn-link"); await click(target.querySelectorAll(".modal-footer button")[0]); // save - assert.verifySteps(["create", "read", "translate_fields", "get_installed", "search_read"]); + assert.verifySteps(["create", "read", "get_installed", "get_field_translations"]); assert.containsOnce(target, ".modal"); assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo"); }); @@ -8583,12 +8572,21 @@ QUnit.module("Views", (hooks) => { ["CUST2", "second custom"], ]; } + if (route === "/web/dataset/call_kw/partner/get_field_translations") { + return Promise.resolve([ + [ + { lang: "CUST", source: "yop", value: "yop" }, + { lang: "CUST2", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, + ]); + } }, }); assert.verifySteps(["get_views", "read"]); await clickEdit(target); await click(target, ".o_field_legacy_char .o_field_translate.btn-link"); - assert.verifySteps(["translate_fields", "get_installed", "search_read"]); + assert.verifySteps(["get_installed", "get_field_translations"]); assert.containsOnce(target, ".modal"); assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo"); }); @@ -8644,22 +8642,11 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("translation alerts preserved on reverse breadcrumb", async function (assert) { - serverData.models["ir.translation"] = { - fields: { - name: { string: "name", type: "char" }, - source: { string: "Source", type: "char" }, - value: { string: "Value", type: "char" }, - }, - records: [], - }; - serverData.models.partner.fields.foo.translate = true; serverData.views = { "partner,false,form": `
`, "partner,false,search": "", - "ir.translation,false,list": ``, - "ir.translation,false,search": "", }; serverData.actions = { @@ -8670,15 +8657,6 @@ QUnit.module("Views", (hooks) => { type: "ir.actions.act_window", views: [[false, "form"]], }, - 2: { - id: 2, - name: "Translate", - res_model: "ir.translation", - type: "ir.actions.act_window", - views: [[false, "list"]], - target: "current", - flags: { search_view: true, action_buttons: true }, - }, }; const webClient = await createWebClient({ serverData }); @@ -8692,17 +8670,12 @@ QUnit.module("Views", (hooks) => { await clickSave(target); assert.containsOnce(target, ".alert .o_field_translate", "should have a translation alert"); - - await doAction(webClient, 2); - - await click(target.querySelector(".o_control_panel .breadcrumb a:nth-child(1)")); - assert.containsOnce(target, ".alert .o_field_translate", "should have a translation alert"); }); QUnit.test( "translate event correctly handled with multiple controllers", async function (assert) { - assert.expect(3); + assert.expect(2); serverData.models.product.fields.name.translate = true; serverData.models.partner.records[0].product_id = 37; @@ -8742,23 +8715,21 @@ QUnit.module("Views", (hooks) => { if (route === "/web/dataset/call_kw/product/get_formview_id") { return false; } - if ( - route === "/web/dataset/call_button" && - args.method === "translate_fields" - ) { - assert.deepEqual( - args.args, - ["product", 37, "name"], - 'should call "call_button" route' - ); - nbTranslateCalls++; - return { - domain: [], - context: { search_default_name: "partnes,foo" }, - }; - } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return [["en_US"], ["fr_BE"]]; + return [ + ["en_US", "English"], + ["fr_BE", "French (Belgium)"], + ]; + } + if (route === "/web/dataset/call_kw/product/get_field_translations") { + nbTranslateCalls++; + return Promise.resolve([ + [ + { lang: "en_US", source: "yop", value: "yop" }, + { lang: "fr_BE", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, + ]); } }, }); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 8af79664a08..89193d519b9 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -197,34 +197,6 @@ QUnit.module("Views", (hooks) => { }, records: [{ id: "2-20170808020000", name: "virtual" }], }, - "ir.translation": { - fields: { - lang_code: { type: "char" }, - src: { type: "char" }, - value: { type: "char" }, - res_id: { type: "integer" }, - name: { type: "char" }, - lang: { type: "char" }, - }, - records: [ - { - id: 99, - res_id: 1, - value: "", - lang_code: "en_US", - lang: "en_US", - name: "foo,foo", - }, - { - id: 100, - res_id: 1, - value: "", - lang_code: "fr_BE", - lang: "fr_BE", - name: "foo,foo", - }, - ], - }, }, }; setupViewRegistries(); @@ -4096,18 +4068,21 @@ QUnit.module("Views", (hooks) => { resModel: "foo", serverData, mockRPC(route, args) { - if (route === "/web/dataset/call_button" && args.method === "translate_fields") { - return Promise.resolve({ - domain: [], - context: { search_default_name: "foo,foo" }, - }); - } if (route === "/web/dataset/call_kw/res.lang/get_installed") { return Promise.resolve([ ["en_US", "English"], ["fr_BE", "Frenglish"], ]); } + if (route === "/web/dataset/call_kw/foo/get_field_translations") { + return Promise.resolve([ + [ + { lang: "en_US", source: "yop", value: "yop" }, + { lang: "fr_BE", source: "yop", value: "valeur français" }, + ], + { translation_type: "char", translation_show_source: false }, + ]); + } }, arch: '' + '' + "", }); diff --git a/addons/web_editor/models/__init__.py b/addons/web_editor/models/__init__.py index f24f2756867..393b0721b56 100644 --- a/addons/web_editor/models/__init__.py +++ b/addons/web_editor/models/__init__.py @@ -5,7 +5,6 @@ from . import ir_attachment from . import ir_qweb_fields from . import ir_ui_view from . import ir_http -from . import ir_translation from . import ir_websocket from . import models diff --git a/addons/web_editor/models/ir_qweb_fields.py b/addons/web_editor/models/ir_qweb_fields.py index e2ea35cddbd..d3272ccb715 100644 --- a/addons/web_editor/models/ir_qweb_fields.py +++ b/addons/web_editor/models/ir_qweb_fields.py @@ -155,10 +155,13 @@ class Field(models.AbstractModel): attrs['placeholder'] = placeholder if options['translate'] and field.type in ('char', 'text'): - name = "%s,%s" % (record._name, field_name) - domain = [('name', '=', name), ('res_id', '=', record.id), ('type', '=', 'model'), ('lang', '=', options.get('lang'))] - translation = record.env['ir.translation'].search(domain, limit=1) - attrs['data-oe-translation-state'] = translation and translation.state or 'to_translate' + lang = record.env.lang or 'en_US' + if lang == 'en_US': + attrs['data-oe-translation-state'] = 'translated' + else: + value_en = record.with_context(lang='en_US')[field_name] + value_lang = record.with_context(lang=lang)[field_name] + attrs['data-oe-translation-state'] = 'translated' if value_en != value_lang else 'to_translate' return attrs diff --git a/addons/web_editor/models/ir_translation.py b/addons/web_editor/models/ir_translation.py deleted file mode 100644 index 0a0e34dafde..00000000000 --- a/addons/web_editor/models/ir_translation.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from lxml import etree - -from odoo import models, api -from odoo.tools.translate import encode, xml_translate, html_translate - - -def edit_translation_mapping(data): - data = dict(data, model=data['name'].partition(',')[0], value=data['value'] or data['src']) - return '%(value)s' % data - - -class IrTranslation(models.Model): - _inherit = 'ir.translation' - - @api.model - def _get_terms_mapping(self, field, records): - if self._context.get('edit_translations'): - self.insert_missing(field, records) - return edit_translation_mapping - return super(IrTranslation, self)._get_terms_mapping(field, records) - - def save_html(self, value): - """ Convert the HTML fragment ``value`` to XML if necessary, and write - it as the value of translation ``self``. - """ - assert len(self) == 1 and self.type == 'model_terms' - mname, fname = self.name.split(',') - field = self.env[mname]._fields[fname] - if field.translate == xml_translate: - # wrap value inside a div and parse it as HTML - div = "
%s
" % encode(value) - root = etree.fromstring(div, etree.HTMLParser(encoding='utf-8')) - # root is html > body > div - # serialize div as XML and discard surrounding tags - value = etree.tostring(root[0][0], encoding='utf-8')[5:-6] - elif field.translate == html_translate: - # wrap value inside a div and parse it as HTML - div = "
%s
" % encode(value) - root = etree.fromstring(div, etree.HTMLParser(encoding='utf-8')) - # root is html > body > div - # serialize div as HTML and discard surrounding tags - value = etree.tostring(root[0][0], encoding='utf-8', method='html')[5:-6] - return self.write({'value': value}) diff --git a/addons/web_editor/models/models.py b/addons/web_editor/models/models.py index 059e35f5651..effd7ffda3a 100644 --- a/addons/web_editor/models/models.py +++ b/addons/web_editor/models/models.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from hashlib import sha256 + from odoo import api, models @@ -12,3 +14,10 @@ class Base(models.AbstractModel): keys.append('sanitize') keys.append('sanitize_tags') return keys + + +class BaseModel(models.AbstractModel): + _inherit = 'base' + + def update_field_translations_sha(self, fname, translations): + return self._update_field_translations(fname, translations, lambda old_term: sha256(old_term.encode()).hexdigest()) diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 97aa04f1424..d78b0b5ff0b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -3818,8 +3818,8 @@ var SnippetsMenu = Widget.extend({ } if (!range || !$currentSelectionTarget.parents('#wrapwrap, .iframe-editor-wrapper .o_editable').length || - $(selection.anchorNode).parent('[data-oe-model]:not([data-oe-type="html"]):not([data-oe-field="arch"]):not([data-oe-translation-id])').length || - $(selection.focusNode).parent('[data-oe-model]:not([data-oe-type="html"]):not([data-oe-field="arch"]):not([data-oe-translation-id])').length || + $(selection.anchorNode).parent('[data-oe-model]:not([data-oe-type="html"]):not([data-oe-field="arch"]):not([data-oe-translation-initial-sha])').length || + $(selection.focusNode).parent('[data-oe-model]:not([data-oe-type="html"]):not([data-oe-field="arch"]):not([data-oe-translation-initial-sha])').length || (e && $(e.target).closest('.fa, img').length || this.options.wysiwyg.lastMediaClicked && $(this.options.wysiwyg.lastMediaClicked).is('.fa, img')) || (this.options.wysiwyg.lastElement && !this.options.wysiwyg.lastElement.isContentEditable) diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index b3feab5dd75..74dfd68141a 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -1016,7 +1016,7 @@ const Wysiwyg = Widget.extend({ observerData.observer.observe(observerData.field, observerOptions); } } else { - const odooFieldSelector = '[data-oe-model], [data-oe-translation-id]'; + const odooFieldSelector = '[data-oe-model], [data-oe-translation-initial-sha]'; const $odooFields = this.$editable.find(odooFieldSelector); this.odooFieldObservers = []; @@ -1032,8 +1032,8 @@ const Wysiwyg = Widget.extend({ .filter('[data-oe-field="' + $node.data('oe-field') + '"]'); } - if ($node.data('oe-translation-id')) { - $nodes = $nodes.filter('[data-oe-translation-id="' + $node.data('oe-translation-id') + '"]'); + if ($node.data('oe-translation-initial-sha')) { + $nodes = $nodes.filter('[data-oe-translation-initial-sha="' + $node.data('oe-translation-initial-sha') + '"]'); } if ($node.data('oe-type')) { $nodes = $nodes.filter('[data-oe-type="' + $node.data('oe-type') + '"]'); @@ -1922,13 +1922,21 @@ const Wysiwyg = Widget.extend({ * @override */ _saveTranslationElement: function ($el, context, withLang = true) { - if ($el.data('oe-translation-id')) { + if ($el.data('oe-translation-initial-sha')) { + const $els = $el; + const translations = {}; + translations[context.lang] = Object.assign({}, ...$els.toArray().map( + (x) => ({ + [$(x).data('oe-translation-initial-sha')]: this._getEscapedElement($(x)).html() + }) + )); return this._rpc({ - model: 'ir.translation', - method: 'save_html', + model: $els.data('oe-model'), + method: 'update_field_translations_sha', args: [ - [+$el.data('oe-translation-id')], - this._getEscapedElement($el).html() + [+$els.data('oe-id')], + $els.data('oe-field'), + translations, ], context: context, }); @@ -2151,10 +2159,13 @@ const Wysiwyg = Widget.extend({ $('.o_editable') .removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed'); - const defs = _.map($allBlocks, (el) => { - const $el = $(el); + const defs = _.map(_.groupBy($allBlocks.toArray(), + function($obj) { + return $obj.dataset['oe-model'] + $obj.dataset['oe-field'] + $obj.dataset['oe-id'] + }), (els) => { + const $els = $(els); - $el.find('[class]').filter(function () { + $els.find('[class]').filter(function () { if (!this.getAttribute('class').match(/\S/)) { this.removeAttribute('class'); } @@ -2166,16 +2177,16 @@ const Wysiwyg = Widget.extend({ if (this.options.enableTranslation) { saveElement = '_saveTranslationElement'; } - return this[saveElement]($el, context || this.options.context) + return this[saveElement]($els, context || this.options.context) .then(function () { - $el.removeClass('o_dirty'); + $els.removeClass('o_dirty'); }).guardedCatch(function (response) { // because ckeditor regenerates all the dom, we can't just // setup the popover here as everything will be destroyed by // the DOM regeneration. Add markings instead, and returns a // new rejection with all relevant info var id = _.uniqueId('carlos_danger_'); - $el.addClass('o_dirty o_editable oe_carlos_danger ' + id); + $els.addClass('o_dirty o_editable oe_carlos_danger ' + id); $('.o_editable.' + id) .removeClass(id) .popover({ diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js index b41db58e906..4afbc222408 100644 --- a/addons/web_editor/static/tests/field_html_tests.js +++ b/addons/web_editor/static/tests/field_html_tests.js @@ -130,19 +130,6 @@ QUnit.module('web_editor', {}, function () { body_arch: "
yep
", }], }, - "ir.translation": { - fields: { - lang_code: {type: "char"}, - value: {type: "char"}, - res_id: {type: "integer"} - }, - records: [{ - id: 99, - res_id: 12, - value: '', - lang_code: 'en_US' - }] - }, }); testUtils.mock.patch(ajax, { @@ -880,15 +867,18 @@ QUnit.module('web_editor', {}, function () { '', res_id: 1, mockRPC: function (route, args) { - if (route === '/web/dataset/call_button' && args.method === 'translate_fields') { - assert.deepEqual(args.args, ['note.note', 1, 'body'], "should call 'call_button' route"); - return Promise.resolve({ - domain: [], - context: {search_default_name: 'partnes,foo'}, - }); + if (route === "/web/dataset/call_kw/note.note/get_field_translations") { + assert.deepEqual(args.args, [[1],"body"], "should translate the body field of the record"); + return Promise.resolve([ + [{lang: "en_US", source: "first paragraph", value: "first paragraph"}, + {lang: "en_US", source: "second paragraph", value: "second paragraph"}, + {lang: "fr_BE", source: "first paragraph", value: "premier paragraphe"}, + {lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}], + {translation_type: "text", translation_show_source: true}, + ]); } if (route === "/web/dataset/call_kw/res.lang/get_installed") { - return Promise.resolve([["en_US"], ["fr_BE"]]); + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); } return this._super.apply(this, arguments); }, diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index adb5c079f54..40ee6ae2a3b 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -18,7 +18,6 @@ from . import website_menu from . import website_page from . import website_rewrite from . import ir_rule -from . import ir_translation from . import ir_ui_view from . import res_company from . import res_partner diff --git a/addons/website/models/ir_module_module.py b/addons/website/models/ir_module_module.py index d68d32fb9c5..69988752211 100644 --- a/addons/website/models/ir_module_module.py +++ b/addons/website/models/ir_module_module.py @@ -3,7 +3,7 @@ import logging import os -from collections import OrderedDict +from collections import defaultdict, OrderedDict from odoo import api, fields, models from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG @@ -188,14 +188,31 @@ class IrModuleModule(models.Model): def _post_copy(self, old_rec, new_rec): self.ensure_one() translated_fields = self._theme_translated_fields.get(old_rec._name, []) + cur_lang = self.env.lang or 'en_US' + old_rec.flush_recordset() for (src_field, dst_field) in translated_fields: - self._cr.execute("""INSERT INTO ir_translation (lang, src, name, res_id, state, value, type, module) - SELECT t.lang, t.src, %s, %s, t.state, t.value, t.type, t.module - FROM ir_translation t - WHERE name = %s - AND res_id = %s - ON CONFLICT DO NOTHING""", - (dst_field, new_rec.id, src_field, old_rec.id)) + __, src_fname = src_field.split(',') + dst_mname, dst_fname = dst_field.split(',') + if dst_mname != new_rec._name: + continue + old_field = old_rec._fields[src_fname] + old_translations = old_field._get_stored_translations(old_rec) + if not old_translations: + continue + if not callable(old_field.translate): + if old_rec[src_fname] == new_rec[dst_fname]: + new_rec.update_field_translations(dst_fname, old_translations) + else: + old_translation_lang = old_translations.get(cur_lang) or old_translations.get('en_US') + # {from_lang_term: {lang: to_lang_term} + translation_dictionary = old_field.get_translation_dictionary(old_translation_lang, { + lang: value for lang, value in old_translations.items() if lang != cur_lang}) + # {lang: {old_term: new_term} + translations = defaultdict(dict) + for from_lang_term, to_lang_terms in translation_dictionary.items(): + for lang, to_lang_term in to_lang_terms.items(): + translations[lang][from_lang_term] = to_lang_term + new_rec.with_context(install_filename='dummy').update_field_translations(dst_fname, translations) def _theme_load(self, website): """ @@ -456,3 +473,70 @@ class IrModuleModule(models.Model): cow_view = View.browse(view_replay[0]) View._load_records_write_on_cow(cow_view, view_replay[1], view_replay[2]) self.pool.website_views_to_adapt.clear() + + @api.model + def _load_module_terms(self, modules, langs, overwrite=False): + """ Add missing website specific translation """ + res = super()._load_module_terms(modules, langs, overwrite=overwrite) + + if not langs or langs == ['en_US'] or not modules: + return res + + # Add specific view translations + + # use the translation dic of the generic to translate the specific + self.env.cr.flush() + cache = self.env.cache + View = self.env['ir.ui.view'] + field = self.env['ir.ui.view']._fields['arch_db'] + # assume there are not too many records + self.env.cr.execute(""" SELECT generic.arch_db, specific.arch_db, specific.id + FROM ir_ui_view generic + INNER JOIN ir_ui_view specific + ON generic.key = specific.key + WHERE generic.website_id IS NULL AND generic.type = 'qweb' + AND specific.website_id IS NOT NULL + """) + for generic_arch_db, specific_arch_db, specific_id in self.env.cr.fetchall(): + if not generic_arch_db: + continue + langs_update = (langs & generic_arch_db.keys()) - {'en_US'} + generic_arch_db_en = generic_arch_db.pop('en_US') + specific_arch_db_en = specific_arch_db.pop('en_US') + generic_arch_db = {k: generic_arch_db[k] for k in langs_update} + specific_arch_db = {k: specific_arch_db.get(k, specific_arch_db_en) for k in langs_update} + generic_translation_dictionary = field.get_translation_dictionary(generic_arch_db_en, generic_arch_db) + specific_translation_dictionary = field.get_translation_dictionary(specific_arch_db_en, specific_arch_db) + # update specific_translation_dictionary + for term_en, specific_term_langs in specific_translation_dictionary.items(): + if term_en not in generic_translation_dictionary: + continue + for lang, generic_term_lang in generic_translation_dictionary[term_en].items(): + if overwrite or term_en == specific_term_langs[lang]: + specific_term_langs[lang] = generic_term_lang + for lang in langs_update: + specific_arch_db[lang] = field.translate( + lambda term: specific_translation_dictionary.get(term, {lang: None})[lang], specific_arch_db_en) + specific_arch_db['en_US'] = specific_arch_db_en + cache.update_raw(View.browse(specific_id), field, [specific_arch_db], dirty=True) + + default_menu = self.env.ref('website.main_menu', raise_if_not_found=False) + if not default_menu: + return res + + o_menu_name = [f"'{lang}', o_menu.name->>'{lang}'" for lang in langs if lang != 'en_US'] + o_menu_name = 'jsonb_build_object(' + ', '.join(o_menu_name) + ')' + self.env.cr.execute(f""" + UPDATE website_menu menu + SET name = {'menu.name || ' + o_menu_name if overwrite else o_menu_name + ' || menu.name'} + FROM website_menu o_menu + INNER JOIN website_menu s_menu + ON o_menu.name->>'en_US' = s_menu.name->>'en_US' AND o_menu.url = s_menu.url + INNER JOIN website_menu root_menu + ON s_menu.parent_id = root_menu.id AND root_menu.parent_id IS NULL + WHERE o_menu.website_id IS NULL AND o_menu.parent_id = %s + AND s_menu.website_id IS NOT NULL + AND menu.id = s_menu.id + """, (default_menu.id,)) + + return res diff --git a/addons/website/models/ir_translation.py b/addons/website/models/ir_translation.py deleted file mode 100644 index a1703c55ab5..00000000000 --- a/addons/website/models/ir_translation.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - -class IrTranslation(models.Model): - _inherit = "ir.translation" - - def _load_module_terms(self, modules, langs, overwrite=False): - """ Add missing website specific translation """ - res = super()._load_module_terms(modules, langs, overwrite=overwrite) - - if not langs or not modules: - return res - - if overwrite: - conflict_clause = """ - ON CONFLICT {} - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = - (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, - EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '' - """ - else: - conflict_clause = " ON CONFLICT DO NOTHING" - - # Add specific view translations - self.env.cr.execute(""" - INSERT INTO ir_translation(name, lang, res_id, src, type, value, module, state, comments) - SELECT DISTINCT ON (specific.id, t.lang, md5(src)) t.name, t.lang, specific.id, t.src, t.type, t.value, t.module, t.state, t.comments - FROM ir_translation t - INNER JOIN ir_ui_view generic - ON t.type = 'model_terms' AND t.name = 'ir.ui.view,arch_db' AND t.res_id = generic.id - INNER JOIN ir_ui_view specific - ON generic.key = specific.key - WHERE t.lang IN %s and t.module IN %s - AND generic.website_id IS NULL AND generic.type = 'qweb' - AND specific.website_id IS NOT NULL""" + conflict_clause.format( - "(type, name, lang, res_id, md5(src))" - ), (tuple(langs), tuple(modules))) - - default_menu = self.env.ref('website.main_menu', raise_if_not_found=False) - if not default_menu: - return res - - # Add specific menu translations - self.env.cr.execute(""" - INSERT INTO ir_translation(name, lang, res_id, src, type, value, module, state, comments) - SELECT DISTINCT ON (s_menu.id, t.lang) t.name, t.lang, s_menu.id, t.src, t.type, t.value, t.module, t.state, t.comments - FROM ir_translation t - INNER JOIN website_menu o_menu - ON t.type = 'model' AND t.name = 'website.menu,name' AND t.res_id = o_menu.id - INNER JOIN website_menu s_menu - ON o_menu.name = s_menu.name AND o_menu.url = s_menu.url - INNER JOIN website_menu root_menu - ON s_menu.parent_id = root_menu.id AND root_menu.parent_id IS NULL - WHERE t.lang IN %s and t.module IN %s - AND o_menu.website_id IS NULL AND o_menu.parent_id = %s - AND s_menu.website_id IS NOT NULL - """ + conflict_clause.format( - "(type, lang, name, res_id) WHERE type = 'model'" - ), (tuple(langs), tuple(modules), default_menu.id)) - - return res diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 3114f59fe12..61ef236875a 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1379,12 +1379,13 @@ class Website(models.Model): def _get_cached(self, field): return self._get_cached_values()[field] - def _get_html_fields(self): - html_fields = [('ir_ui_view', 'arch_db')] + def _get_html_fields_attributes(self): + html_fields = [('ir_ui_view', 'arch_db', True)] cr = self.env.cr cr.execute(r""" SELECT f.model, - f.name + f.name, + f.translate FROM ir_model_fields f JOIN ir_model m ON m.id = f.model_id @@ -1394,10 +1395,10 @@ class Website(models.Model): AND f.model NOT LIKE 'ir.actions%' AND f.model != 'mail.message' """) - for model, name in cr.fetchall(): + for model, name, translate in cr.fetchall(): table = self.env[model]._table if tools.table_exists(cr, table) and tools.column_exists(cr, table, name): - html_fields.append((table, name)) + html_fields.append((table, name, translate)) return html_fields def _get_snippets_assets(self): @@ -1428,7 +1429,7 @@ class Website(models.Model): """) return self.env.cr.fetchall() - def _is_snippet_used(self, snippet_module, snippet_id, asset_version, asset_type, html_fields): + def _is_snippet_used(self, snippet_module, snippet_id, asset_version, asset_type, html_fields_attributes): snippet_occurences = [] # Check snippet template definition to avoid disabling its related assets. # This special case is needed because snippet template definitions do not @@ -1440,11 +1441,12 @@ class Website(models.Model): # As well as every snippet dropped in html fields self.env.cr.execute(sql.SQL(" UNION ").join( - sql.SQL("SELECT regexp_matches({}, {}, 'g') FROM {}").format( + sql.SQL("SELECT regexp_matches({}{}, {}, 'g') FROM {}").format( sql.Identifier(column), + sql.SQL("->>'en_US'" if translate else ''), sql.Placeholder('snippet_regex'), sql.Identifier(table) - ) for table, column in html_fields + ) for table, column, translate in html_fields_attributes ), {'snippet_regex': f'<([^>]*data-snippet="{snippet_id}"[^>]*)>'}) results = self.env.cr.fetchall() for r in results: @@ -1461,10 +1463,10 @@ class Website(models.Model): def _disable_unused_snippets_assets(self): snippets_assets = self._get_snippets_assets() - html_fields = self._get_html_fields() + html_fields_attributes = self._get_html_fields_attributes() for snippet_module, snippet_id, asset_version, asset_type, _asset_id in snippets_assets: - is_snippet_used = self._is_snippet_used(snippet_module, snippet_id, asset_version, asset_type, html_fields) + is_snippet_used = self._is_snippet_used(snippet_module, snippet_id, asset_version, asset_type, html_fields_attributes) # The regex catches XXX.scss, XXX.js and XXX_variables.scss assets_regex = f'{snippet_id}/{asset_version}.+{asset_type}' @@ -1652,6 +1654,7 @@ class Website(models.Model): """ match_pattern = r'[\w-]{%s,}' % min(4, len(search) - 3) similarity_threshold = 0.3 + lang = self.env.lang or 'en_US' for search_detail in search_details: model_name, fields = search_detail['model'], search_detail['search_fields'] model = self.env[model_name] @@ -1668,6 +1671,11 @@ class Website(models.Model): field=unaccent(sql.SQL("{table}.{field}").format( table=sql.Identifier((self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._table), field=sql.Identifier(field) + )) if not (self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._fields[field].translate else + unaccent(sql.SQL("COALESCE({table}.{field}->>{lang}, {table}.{field}->>'en_US')").format( + table=sql.Identifier((self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._table), + field=sql.Identifier(field), + lang=sql.Literal(lang) )) ) for field in fields] best_similarity = sql.SQL('GREATEST({similarities})').format( @@ -1698,55 +1706,6 @@ class Website(models.Model): ) self.env.cr.execute(query, {'search': search}) ids = {row[0] for row in self.env.cr.fetchall() if row[1] and row[1] >= similarity_threshold} - if self.env.lang: - # Specific handling for website.page that inherits its arch_db and name fields - # TODO make more generic - if 'arch_db' in fields: - # Look for partial translations - similarity = sql.SQL("word_similarity({search}, {field})").format( - search=unaccent(sql.Placeholder('search')), - field=unaccent(sql.SQL('t.value')) - ) - names = ['%s,%s' % (self.env['ir.ui.view']._name, field) for field in fields] - query = sql.SQL(""" - SELECT {table}.id, {similarity} AS _similarity - FROM {table} - LEFT JOIN ir_ui_view v ON {table}.view_id = v.id - LEFT JOIN ir_translation t ON v.id = t.res_id - WHERE t.lang = {lang} - AND t.name = ANY({names}) - AND t.type = 'model_terms' - AND t.value IS NOT NULL - ORDER BY _similarity desc - LIMIT 1000 - """).format( - table=sql.Identifier(model._table), - similarity=similarity, - lang=sql.Placeholder('lang'), - names=sql.Placeholder('names'), - ) - else: - similarity = sql.SQL("word_similarity({search}, {field})").format( - search=unaccent(sql.Placeholder('search')), - field=unaccent(sql.SQL('value')) - ) - names = ['%s,%s' % (model._name, field) for field in fields] - query = sql.SQL(""" - SELECT res_id, {similarity} AS _similarity - FROM ir_translation - WHERE lang = {lang} - AND name = ANY({names}) - AND type = 'model' - AND value IS NOT NULL - ORDER BY _similarity desc - LIMIT 1000 - """).format( - similarity=similarity, - lang=sql.Placeholder('lang'), - names=sql.Placeholder('names'), - ) - self.env.cr.execute(query, {'lang': self.env.lang, 'names': names, 'search': search}) - ids.update(row[0] for row in self.env.cr.fetchall() if row[1] and row[1] >= similarity_threshold) domain.append([('id', 'in', list(ids))]) domain = AND(domain) records = model.search_read(domain, fields, limit=limit) diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 2a0945456d4..eeb9139dc9d 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -231,28 +231,18 @@ class Page(models.Model): # Perform search in translations # TODO Remove when domains will support xml_translate fields query = sql.SQL(""" - SELECT {table}.{id} + SELECT {table}.id FROM {table} - LEFT JOIN ir_ui_view v ON {table}.{view_id} = v.{id} - LEFT JOIN ir_translation t ON v.{id} = t.{res_id} - WHERE t.lang = {lang} - AND t.name = ANY({names}) - AND t.type = 'model_terms' - AND t.value ilike {search} + LEFT JOIN ir_ui_view v ON {table}.view_id = v.id + WHERE v.name ILIKE {search} + OR jsonb_path_query_array(v.arch_db,'$.*')::text ILIKE {search} LIMIT {limit} """).format( table=sql.Identifier(self._table), - id=sql.Identifier('id'), - view_id=sql.Identifier('view_id'), - res_id=sql.Identifier('res_id'), - lang=sql.Placeholder('lang'), - names=sql.Placeholder('names'), search=sql.Placeholder('search'), limit=sql.Placeholder('limit'), ) self.env.cr.execute(query, { - 'lang': self.env.lang, - 'names': ['ir.ui.view,arch_db', 'ir.ui.view,name'], 'search': '%%%s%%' % escape_psql(search), 'limit': limit, }) diff --git a/addons/website/static/src/components/translator/translator.js b/addons/website/static/src/components/translator/translator.js index 828a66f67d6..63d5b0002ca 100644 --- a/addons/website/static/src/components/translator/translator.js +++ b/addons/website/static/src/components/translator/translator.js @@ -53,13 +53,13 @@ export class TranslatorInfoDialog extends Component { TranslatorInfoDialog.components = { WebsiteDialog }; TranslatorInfoDialog.template = 'website.TranslatorInfoDialog'; -const savableSelector = '[data-oe-translation-id], ' + +const savableSelector = '[data-oe-translation-initial-sha], ' + '[data-oe-model][data-oe-id][data-oe-field], ' + - '[placeholder*="data-oe-translation-id="], ' + - '[title*="data-oe-translation-id="], ' + - '[value*="data-oe-translation-id="], ' + - 'textarea:contains(data-oe-translation-id), ' + - '[alt*="data-oe-translation-id="]'; + '[placeholder*="data-oe-translation-initial-sha="], ' + + '[title*="data-oe-translation-initial-sha="], ' + + '[value*="data-oe-translation-initial-sha="], ' + + 'textarea:contains(data-oe-translation-initial-sha), ' + + '[alt*="data-oe-translation-initial-sha="]'; export class WebsiteTranslator extends WebsiteEditorComponent { setup() { @@ -110,11 +110,8 @@ export class WebsiteTranslator extends WebsiteEditorComponent { } getTranslationObject(nodeEl) { - let { oeTranslationId: id } = nodeEl.dataset; - if (!id) { - const { oeModel, oeId, oeField } = nodeEl.dataset; - id = [oeModel, oeId, oeField].join(','); - } + const { oeModel, oeId, oeField, oeTranslationInitialMd5 } = nodeEl.dataset; + const id = [oeModel, oeId, oeField, oeTranslationInitialMd5].join(','); let translation = this.translations.filter(t => t.id === id)[0]; if (!translation) { translation = { id }; @@ -128,10 +125,10 @@ export class WebsiteTranslator extends WebsiteEditorComponent { const self = this; var attrs = ['placeholder', 'title', 'alt', 'value']; const $editable = this.getEditableArea(); - const translationRegex = /]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/; + const translationRegex = /]*data-oe-translation-initial-sha="([^"]+)"[^>]*>(.*)<\/span>/; let $edited = $(); _.each(attrs, function (attr) { - const attrEdit = $editable.filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img'); + const attrEdit = $editable.filter('[' + attr + '*="data-oe-translation-initial-sha="]').filter(':empty, input, select, textarea, img'); attrEdit.each(function () { var $node = $(this); var translation = $node.data('translation') || {}; @@ -147,7 +144,7 @@ export class WebsiteTranslator extends WebsiteEditorComponent { }); $edited = $edited.add(attrEdit); }); - const textEdit = $editable.filter('textarea:contains(data-oe-translation-id)'); + const textEdit = $editable.filter('textarea:contains(data-oe-translation-initial-sha)'); textEdit.each(function () { var $node = $(this); var translation = $node.data('translation') || {}; diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js index c516e790078..a7f6fff87e2 100644 --- a/addons/website/static/tests/tours/rte.js +++ b/addons/website/static/tests/tours/rte.js @@ -89,7 +89,7 @@ wTourUtils.registerWebsitePreviewTour('rte_translator', { trigger: '.modal-footer .btn-secondary', }, { content: "check if translation is activate", - trigger: 'iframe [data-oe-translation-id]', + trigger: 'iframe [data-oe-translation-initial-sha]', }, { content: "translate text", extra_trigger: '#oe_snippets.o_loaded', diff --git a/addons/website/tests/test_configurator.py b/addons/website/tests/test_configurator.py index 641c7406b0d..5cf817e609a 100644 --- a/addons/website/tests/test_configurator.py +++ b/addons/website/tests/test_configurator.py @@ -56,11 +56,10 @@ class TestConfiguratorCommon(odoo.tests.HttpCase): class TestConfiguratorTranslation(TestConfiguratorCommon): def test_01_configurator_translation(self): - with mute_logger('odoo.addons.base.models.ir_translation'): - self.env["base.language.install"].create({ - 'overwrite': True, - 'lang_ids': [(6, 0, [self.env.ref('base.lang_fr').id])], - }).lang_install() + self.env["base.language.install"].create({ + 'overwrite': True, + 'lang_ids': [(6, 0, [self.env.ref('base.lang_fr').id])], + }).lang_install() feature = self.env['website.configurator.feature'].search([('name', '=', 'Privacy Policy')]) feature.with_context(lang='fr_FR').write({'name': 'Politique de confidentialité'}) self.env.ref('base.user_admin').write({'lang': self.env.ref('base.lang_fr').code}) diff --git a/addons/website/tests/test_controllers.py b/addons/website/tests/test_controllers.py index 2b9acdf929a..ae7a843c291 100644 --- a/addons/website/tests/test_controllers.py +++ b/addons/website/tests/test_controllers.py @@ -20,6 +20,7 @@ class TestControllers(tests.HttpCase): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') suggested_links_url = base_url + '/website/get_suggested_links' + old_pages = Page for i in range(0, 10): new_page = Page.create({ 'name': 'Generic', @@ -32,11 +33,14 @@ class TestControllers(tests.HttpCase): 'is_published': True, }) if i % 2 == 0: - # mark as old - new_page._write({'write_date': '2020-01-01'}) + old_pages += new_page else: last_5_url_edited.append(new_page.url) + self.opener.post(url=suggested_links_url, json={'params': {'needle': '/'}}) + # mark as old + old_pages._write({'write_date': '2020-01-01'}) + res = self.opener.post(url=suggested_links_url, json={'params': {'needle': '/'}}) resp = json.loads(res.content) assert 'result' in resp diff --git a/addons/website/tests/test_disable_unused_snippets_assets.py b/addons/website/tests/test_disable_unused_snippets_assets.py index 4be813cb03d..87a9d0f2f1f 100644 --- a/addons/website/tests/test_disable_unused_snippets_assets.py +++ b/addons/website/tests/test_disable_unused_snippets_assets.py @@ -63,9 +63,11 @@ class TestDisableSnippetsAssets(TransactionCase): self.homepage.write({ 'arch_db': HOMEPAGE_UP_TO_DATE, }) + self.homepage.flush_recordset() self.mega_menu.write({ 'mega_menu_content': MEGA_MENU_OUTDATED, }) + self.mega_menu.flush_recordset() self.Website._disable_unused_snippets_assets() s_website_form_000_scss = self._get_snippet_asset('s_website_form', '000', 'scss') diff --git a/addons/website/tests/test_menu.py b/addons/website/tests/test_menu.py index eb0e721292b..b391304be45 100644 --- a/addons/website/tests/test_menu.py +++ b/addons/website/tests/test_menu.py @@ -69,7 +69,7 @@ class TestMenu(common.TransactionCase): self.assertEqual(total_menus + 4, Menu.search_count([]), "New website's bootstraping should have duplicate default menu tree (Top/Home/Contactus/Sub Default Menu)") def test_04_specific_menu_translation(self): - Translation = self.env['ir.translation'] + IrModuleModule = self.env['ir.module.module'] Menu = self.env['website.menu'] existing_menus = Menu.search([]) @@ -85,9 +85,6 @@ class TestMenu(common.TransactionCase): # create fr_FR translation for template menu self.env.ref('base.lang_fr').active = True template_menu.with_context(lang='fr_FR').name = 'Menu en français' - Translation.search([ - ('name', '=', 'website.menu,name'), ('res_id', '=', template_menu.id), - ]).module = 'website' self.assertEqual(specific1.name, 'Menu in english', 'Translating template menu does not translate specific menu') @@ -95,7 +92,7 @@ class TestMenu(common.TransactionCase): specific1.name = 'Menu in french' # loading translation add missing specific translation - Translation._load_module_terms(['website'], ['fr_FR']) + IrModuleModule._load_module_terms(['website'], ['fr_FR']) Menu.invalidate_model(['name']) self.assertEqual(specific1.name, 'Menu in french', 'Load translation without overwriting keep existing translation') @@ -103,7 +100,7 @@ class TestMenu(common.TransactionCase): 'Load translation add missing translation from template menu') # loading translation with overwrite sync all translations from menu template - Translation._load_module_terms(['website'], ['fr_FR'], overwrite=True) + IrModuleModule._load_module_terms(['website'], ['fr_FR'], overwrite=True) Menu.invalidate_model(['name']) self.assertEqual(specific1.name, 'Menu en français', 'Load translation with overwriting update existing menu from template') diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py index 6b999761459..3e2d7c918e1 100644 --- a/addons/website/tests/test_ui.py +++ b/addons/website/tests/test_ui.py @@ -91,6 +91,7 @@ class TestUiHtmlEditor(odoo.tests.HttpCase): ''' generic_page.arch = oe_structure_layout + oe_structure_layout = generic_page.arch self.start_tour(self.env['website'].get_client_action_url('/generic'), 'html_editor_multiple_templates', login='admin') self.assertEqual(View.search_count([('key', '=', 'test.generic_view')]), 2, "homepage view should have been COW'd") self.assertTrue(generic_page.arch == oe_structure_layout, "Generic homepage view should be untouched") diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 1dcdeb807f2..a039f42b219 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -995,29 +995,25 @@ class TestCowViewSaving(TestViewSavingCommon): View.pool._init = original_pool_init def test_specific_view_translation(self): - Translation = self.env['ir.translation'] - - Translation.insert_missing(self.base_view._fields['arch_db'], self.base_view) - translation = Translation.search([ - ('res_id', '=', self.base_view.id), ('name', '=', 'ir.ui.view,arch_db') - ]) - translation.value = 'hello' - translation.module = 'website' - + self.env['res.lang']._activate_lang('fr_BE') + self.base_view.with_context(lang='en_US').arch_db = '
hello
' + self.base_view.update_field_translations('arch_db', {'fr_BE': {'hello': 'bonjour'}}) + self.assertEqual(self.base_view.with_context(lang='fr_BE').arch, '
bonjour
') self.base_view.with_context(website_id=1).write({'active': True}) specific_view = self.base_view._get_specific_views() - self.base_view - self.assertEqual(specific_view.with_context(lang='en_US').arch, '
hello
', + self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '
bonjour
', "copy on write (COW) also copy existing translations") - translation.value = 'hi' - self.assertEqual(specific_view.with_context(lang='en_US').arch, '
hello
', + self.base_view.update_field_translations('arch_db', {'fr_BE': {'bonjour': 'salut'}}) + self.assertEqual(self.base_view.with_context(lang='fr_BE').arch, '
salut
') + self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '
bonjour
', "updating translation of base view doesn't update specific view") - Translation._load_module_terms(['website'], ['en_US'], overwrite=True) + self.env['ir.module.module']._load_module_terms(['website'], ['en_US', 'fr_BE'], overwrite=True) specific_view.invalidate_model(['arch_db', 'arch']) - self.assertEqual(specific_view.with_context(lang='en_US').arch, '
hi
', + self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '
salut
', "loading module translation copy translation from base to specific view") def test_soc_complete_flow(self): diff --git a/addons/website_blog/tests/test_performance.py b/addons/website_blog/tests/test_performance.py index 164656e44c2..b05c8b8a4f1 100644 --- a/addons/website_blog/tests/test_performance.py +++ b/addons/website_blog/tests/test_performance.py @@ -14,7 +14,7 @@ class TestBlogPerformance(UtilPerf): def test_10_perf_sql_blog_standard_data(self): self.assertEqual(self._get_url_hot_query('/blog'), 11) - self.assertEqual(self._get_url_hot_query('/blog', cache=False), 27) + self.assertEqual(self._get_url_hot_query('/blog', cache=False), 24) def test_20_perf_sql_blog_bigger_data_scaling(self): BlogPost = self.env['blog.post'] @@ -27,9 +27,9 @@ class TestBlogPerformance(UtilPerf): blog_post.tag_ids += blog_tags blog_tags = blog_tags[:-1] self.assertEqual(self._get_url_hot_query('/blog'), 11) - self.assertEqual(self._get_url_hot_query('/blog', cache=False), 34) - self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 19) - self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 22) + self.assertEqual(self._get_url_hot_query('/blog', cache=False), 31) + self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 16) + self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 18) def test_30_perf_sql_blog_bigger_data_scaling(self): BlogPost = self.env['blog.post'] diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index 4357a31fd24..24df0e7f803 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -39,7 +39,6 @@ The kernel of Odoo, needed for all installation. 'views/ir_attachment_views.xml', 'views/ir_rule_views.xml', 'views/ir_sequence_views.xml', - 'views/ir_translation_views.xml', 'views/ir_ui_menu_views.xml', 'views/ir_ui_view_views.xml', 'views/ir_default_views.xml', @@ -59,7 +58,6 @@ The kernel of Odoo, needed for all installation. 'wizard/base_module_upgrade_views.xml', 'wizard/base_module_uninstall_views.xml', 'wizard/base_export_language_views.xml', - 'wizard/base_update_translations_views.xml', 'wizard/base_partner_merge_views.xml', 'data/ir_actions_data.xml', 'data/ir_demo_failure_data.xml', diff --git a/odoo/addons/base/data/base_data.sql b/odoo/addons/base/data/base_data.sql index 6cd12e6cd25..419e3891093 100644 --- a/odoo/addons/base/data/base_data.sql +++ b/odoo/addons/base/data/base_data.sql @@ -27,7 +27,7 @@ CREATE TABLE res_users ( CREATE TABLE res_groups ( id serial NOT NULL, - name varchar NOT NULL, + name jsonb NOT NULL, primary key(id) ); @@ -38,7 +38,7 @@ CREATE TABLE ir_module_category ( write_date timestamp without time zone, write_uid integer, -- references res_users on delete set null, parent_id integer REFERENCES ir_module_category ON DELETE SET NULL, - name character varying NOT NULL, + name jsonb NOT NULL, primary key(id) ); @@ -49,15 +49,15 @@ CREATE TABLE ir_module_module ( write_date timestamp without time zone, write_uid integer, -- references res_users on delete set null, website character varying, - summary character varying, + summary jsonb, name character varying NOT NULL, author character varying, icon varchar, state character varying(16), latest_version character varying, - shortdesc character varying, + shortdesc jsonb, category_id integer REFERENCES ir_module_category ON DELETE SET NULL, - description text, + description jsonb, application boolean default False, demo boolean default False, web boolean DEFAULT FALSE, @@ -135,6 +135,6 @@ insert into res_users (id, login, password, active, partner_id, company_id, crea insert into ir_model_data (name, module, model, noupdate, res_id) VALUES ('user_root', 'base', 'res.users', true, 1); select setval('res_users_id_seq', 1); -insert into res_groups (id, name) VALUES (1, 'Employee'); +insert into res_groups (id, name) VALUES (1, '{"en_US": "Employee"}'); insert into ir_model_data (name, module, model, noupdate, res_id) VALUES ('group_user', 'base', 'res.groups', true, 1); select setval('res_groups_id_seq', 1); diff --git a/odoo/addons/base/i18n/base.pot b/odoo/addons/base/i18n/base.pot index 2b80fe9cb81..5b6c68804bf 100644 --- a/odoo/addons/base/i18n/base.pot +++ b/odoo/addons/base/i18n/base.pot @@ -29684,3 +29684,27 @@ msgstr "" #: model:res.country,name:base.ax msgid "Åland Islands" msgstr "" + +#. module: base +#: code:addons/base/models/ir_fields.py:0 +#, python-format +msgid "yes" +msgstr "" + +#. module: base +#: code:addons/base/models/ir_fields.py:0 +#, python-format +msgid "no" +msgstr "" + +#. module: base +#: code:addons/base/models/ir_fields.py:0 +#, python-format +msgid "true" +msgstr "" + +#. module: base +#: code:addons/base/models/ir_fields.py:0 +#, python-format +msgid "false" +msgstr "" diff --git a/odoo/addons/base/models/__init__.py b/odoo/addons/base/models/__init__.py index 5ad7f201450..86fe94b5ba0 100644 --- a/odoo/addons/base/models/__init__.py +++ b/odoo/addons/base/models/__init__.py @@ -14,7 +14,6 @@ from . import ir_binary from . import ir_cron from . import ir_filters from . import ir_default -from . import ir_translation from . import ir_exports from . import ir_rule from . import ir_config_parameter diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 9a90d60f737..817daa0071c 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -25,7 +25,7 @@ class IrActions(models.Model): _table = 'ir_actions' _order = 'name' - name = fields.Char(required=True) + name = fields.Char(string='Action Name', required=True, translate=True) type = fields.Char(string='Action Type', required=True) xml_id = fields.Char(compute='_compute_xml_id', string="External ID") help = fields.Html(string='Action Description', @@ -240,7 +240,6 @@ class IrActionsActWindow(models.Model): fvg = self.env[act.res_model].get_view(act.search_view_id.id, 'search') act.search_view = str(fvg) - name = fields.Char(string='Action Name', translate=True) type = fields.Char(default="ir.actions.act_window") view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null') domain = fields.Char(string='Domain Value', @@ -372,7 +371,6 @@ class IrActionsActUrl(models.Model): _inherit = 'ir.actions.actions' _order = 'name' - name = fields.Char(string='Action Name', translate=True) type = fields.Char(default='ir.actions.act_url') url = fields.Text(string='Action URL', required=True) target = fields.Selection([('new', 'New Window'), ('self', 'This Window')], @@ -421,7 +419,6 @@ class IrActionsServer(models.Model): # - Command: x2Many commands namespace # To return an action, assign: action = {...}\n\n\n\n""" - name = fields.Char(string='Action Name', translate=True) type = fields.Char(default='ir.actions.server') usage = fields.Selection([ ('ir_actions_server', 'Server Action'), @@ -842,7 +839,6 @@ class IrActionsActClient(models.Model): _table = 'ir_act_client' _order = 'name' - name = fields.Char(string='Action Name', translate=True) type = fields.Char(default='ir.actions.client') tag = fields.Char(string='Client action tag', required=True, diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 141c274dbda..e805dce13dd 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -91,7 +91,6 @@ class IrActionsReport(models.Model): _table = 'ir_act_report_xml' _order = 'name' - name = fields.Char(translate=True) type = fields.Char(default='ir.actions.report') binding_type = fields.Selection(default='report') model = fields.Char(required=True, string='Model Name') diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py index 960327bcad5..5d514a3f76d 100644 --- a/odoo/addons/base/models/ir_cron.py +++ b/odoo/addons/base/models/ir_cron.py @@ -179,7 +179,7 @@ class ir_cron(models.Model): def _get_all_ready_jobs(cls, cr): """ Return a list of all jobs that are ready to be executed """ cr.execute(""" - SELECT * + SELECT *, cron_name->>'en_US' as cron_name FROM ir_cron WHERE active = true AND numbercall != 0 @@ -241,7 +241,7 @@ class ir_cron(models.Model): # Learn more: https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS query = """ - SELECT * + SELECT *, cron_name->>'en_US' as cron_name FROM ir_cron WHERE active = true AND numbercall != 0 diff --git a/odoo/addons/base/models/ir_fields.py b/odoo/addons/base/models/ir_fields.py index 362ec117252..619d66ab4e1 100644 --- a/odoo/addons/base/models/ir_fields.py +++ b/odoo/addons/base/models/ir_fields.py @@ -8,7 +8,8 @@ import psycopg2 import pytz from odoo import api, Command, fields, models, _ -from odoo.tools import ustr +from odoo.tools import ustr, OrderedSet +from odoo.tools.translate import code_translations, _lt REFERENCING_FIELDS = {None, 'id', '.id'} def only_ref_fields(record): @@ -16,6 +17,13 @@ def only_ref_fields(record): def exclude_ref_fields(record): return {k: v for k, v in record.items() if k not in REFERENCING_FIELDS} +# these lazy translations promise translations for ['yes', 'no', 'true', 'false'] +BOOLEAN_TRANSLATIONS = ( + _lt('yes'), + _lt('no'), + _lt('true'), + _lt('false') +) class ImportWarning(Warning): """ Used to send warnings upwards the stack during the import process """ @@ -190,8 +198,8 @@ class IrFieldsConverter(models.AbstractModel): # potentially broken casefolding? What about locales? trues = set(word.lower() for word in itertools.chain( [u'1', u"true", u"yes"], # don't use potentially translated values - self._get_translations(['code'], u"true"), - self._get_translations(['code'], u"yes"), + self._get_boolean_translations(u"true"), + self._get_boolean_translations(u"yes"), )) if value.lower() in trues: return True, [] @@ -199,8 +207,8 @@ class IrFieldsConverter(models.AbstractModel): # potentially broken casefolding? What about locales? falses = set(word.lower() for word in itertools.chain( [u'', u"0", u"false", u"no"], - self._get_translations(['code'], u"false"), - self._get_translations(['code'], u"no"), + self._get_boolean_translations(u"false"), + self._get_boolean_translations(u"no"), )) if value.lower() in falses: return False, [] @@ -296,17 +304,46 @@ class IrFieldsConverter(models.AbstractModel): return fields.Datetime.to_string(dt.astimezone(pytz.UTC)), [] @api.model - def _get_translations(self, types, src): - types = tuple(types) + def _get_boolean_translations(self, src): # Cache translations so they don't have to be reloaded from scratch on # every row of the file tnx_cache = self._cr.cache.setdefault(self._name, {}) - if tnx_cache.setdefault(types, {}) and src in tnx_cache[types]: - return tnx_cache[types][src] + if src in tnx_cache: + return tnx_cache[src] - Translations = self.env['ir.translation'] - tnx = Translations.search([('type', 'in', types), ('src', '=', src)]) - result = tnx_cache[types][src] = [t.value for t in tnx if t.value is not False] + values = OrderedSet() + for lang, __ in self.env['res.lang'].get_installed(): + translations = code_translations.get_python_translations('base', lang) + if src in translations: + values.add(translations[src]) + + result = tnx_cache[src] = list(values) + return result + + @api.model + def _get_selection_translations(self, field, src): + if not src: + return [] + # Cache translations so they don't have to be reloaded from scratch on + # every row of the file + tnx_cache = self._cr.cache.setdefault(self._name, {}) + if src in tnx_cache: + return tnx_cache[src] + + values = OrderedSet() + self.env['ir.model.fields.selection'].flush_model() + query = """ + SELECT s.name + FROM ir_model_fields_selection s + JOIN ir_model_fields f ON s.field_id = f.id + WHERE f.model = %s AND f.name = %s AND s.name->>'en_US' = %s + """ + self.env.cr.execute(query, [field.model_name, field.name, src]) + for (name,) in self.env.cr.fetchall(): + name.pop('en_US') + values.update(name.values()) + + result = tnx_cache[src] = list(values) return result @api.model @@ -317,7 +354,14 @@ class IrFieldsConverter(models.AbstractModel): for item, label in selection: label = ustr(label) - labels = [label] + self._get_translations(('selection', 'model', 'code'), label) + if callable(field.selection): + labels = [label] + for item2, label2 in field._description_selection(self.env): + if item2 == item: + labels.append(label2) + break + else: + labels = [label] + self._get_selection_translations(field, label) # case insensitive comparaison of string to allow to set the value even if the given 'value' param is not # exactly (case sensitive) the same as one of the selection item. if value.lower() == str(item).lower() or any(value.lower() == label.lower() for label in labels): diff --git a/odoo/addons/base/models/ir_filters.py b/odoo/addons/base/models/ir_filters.py index 3a3938b3caa..0acab9baf64 100644 --- a/odoo/addons/base/models/ir_filters.py +++ b/odoo/addons/base/models/ir_filters.py @@ -11,7 +11,7 @@ class IrFilters(models.Model): _description = 'Filters' _order = 'model_id, name, id desc' - name = fields.Char(string='Filter Name', translate=True, required=True) + name = fields.Char(string='Filter Name', required=True) user_id = fields.Many2one('res.users', string='User', ondelete='cascade', help="The user this filter is private to. When left empty the filter is public " "and available to all users.") @@ -28,7 +28,11 @@ class IrFilters(models.Model): @api.model def _list_all_models(self): - self._cr.execute("SELECT model, name FROM ir_model ORDER BY name") + lang = self.env.lang or 'en_US' + self._cr.execute( + "SELECT model, COALESCE(name->>%s, name->>'en_US') FROM ir_model ORDER BY 2", + [lang], + ) return self._cr.fetchall() def copy(self, default=None): diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index da8c869823b..d1ce7eb26d7 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -4,6 +4,7 @@ #---------------------------------------------------------- import base64 import hashlib +import json import logging import mimetypes import os @@ -22,6 +23,7 @@ from odoo.exceptions import AccessDenied, AccessError, MissingError from odoo.http import request, Response, ROUTING_KEYS, Stream from odoo.service import security from odoo.tools import consteq, submap +from odoo.tools.translate import code_translations from odoo.modules.module import get_resource_path, get_module_path _logger = logging.getLogger(__name__) @@ -198,3 +200,45 @@ class IrHttp(models.AbstractModel): @api.autovacuum def _gc_sessions(self): http.root.session_store.vacuum() + + @api.model + def get_translations_for_webclient(self, modules, lang): + if not modules: + modules = self.pool._init_modules + if not lang: + lang = self._context.get("lang") + langs = self.env['res.lang']._lang_get(lang) + lang_params = None + if langs: + lang_params = { + "name": langs.name, + "direction": langs.direction, + "date_format": langs.date_format, + "time_format": langs.time_format, + "grouping": langs.grouping, + "decimal_point": langs.decimal_point, + "thousands_sep": langs.thousands_sep, + "week_start": langs.week_start, + } + lang_params['week_start'] = int(lang_params['week_start']) + lang_params['code'] = lang + + # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is + # done server-side when the language is loaded, so we only need to load the user's lang. + translations_per_module = {} + for module in modules: + translations_per_module[module] = code_translations.get_web_translations(module, lang) + + return translations_per_module, lang_params + + @api.model + @tools.ormcache('frozenset(modules)', 'lang') + def get_web_translations_hash(self, modules, lang): + translations, lang_params = self.get_translations_for_webclient(modules, lang) + translation_cache = { + 'lang_parameters': lang_params, + 'modules': translations, + 'lang': lang, + 'multi_lang': len(self.env['res.lang'].sudo().get_installed()) > 1, + } + return hashlib.sha1(json.dumps(translation_cache, sort_keys=True).encode()).hexdigest() diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 213c08223b5..8e84e64a605 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -11,6 +11,7 @@ from collections.abc import Mapping from operator import itemgetter from psycopg2 import sql +from psycopg2.extras import Json from odoo import api, fields, models, tools, _, _lt, Command from odoo.exceptions import AccessError, UserError, ValidationError @@ -101,29 +102,55 @@ def query_update(cr, table, values, selectors): return [row[0] for row in cr.fetchall()] -def upsert(cr, table, cols, rows, conflict): +def select_en(model, fnames, where, params): + """ Select the given columns from the given model's table, with the given WHERE clause. + Translated fields are returned in 'en_US'. + """ + table = quote(model._table) + cols = ", ".join( + f"{quote(fname)}->>'en_US'" if model._fields[fname].translate else quote(fname) + for fname in fnames + ) + query = f"SELECT {cols} FROM {table} WHERE {where}" + model.env.cr.execute(query, params) + return model.env.cr.fetchall() + + +def upsert_en(model, fnames, rows, conflict): """ Insert or update the table with the given rows. - :param cr: database cursor - :param table: table name - :param cols: list of column names + :param model: recordset of the model to query + :param fnames: list of column names :param rows: list of tuples, where each tuple value corresponds to a column name :param conflict: list of column names to put into the ON CONFLICT clause :return: the ids of the inserted or updated rows """ - query = """ - INSERT INTO {table} ({cols}) VALUES {rows} - ON CONFLICT ({conflict}) DO UPDATE SET ({cols}) = ({excluded}) + table = quote(model._table) + cols = ", ".join(quote(fname) for fname in fnames) + values = ", ".join("%s" for row in rows) + conf = ", ".join(conflict) + excluded = ", ".join(f"EXCLUDED.{quote(fname)}" for fname in fnames) + query = f""" + INSERT INTO {table} ({cols}) VALUES {values} + ON CONFLICT ({conf}) DO UPDATE SET ({cols}) = ({excluded}) RETURNING id - """.format( - table=quote(table), - cols=", ".join(quote(col) for col in cols), - rows=", ".join("%s" for row in rows), - conflict=", ".join(conflict), - excluded=", ".join("EXCLUDED." + quote(col) for col in cols), - ) - cr.execute(query, rows) - return [row[0] for row in cr.fetchall()] + """ + + # for translated fields, we can actually erase the json value, as + # translations will be reloaded after this + def identity(val): + return val + + def jsonify(val): + return Json({'en_US': val}) if val is not None else val + + wrappers = [(jsonify if model._fields[fname].translate else identity) for fname in fnames] + params = [ + tuple(func(val) for func, val in zip(wrappers, row)) + for row in rows + ] + model.env.cr.execute(query, params) + return [row[0] for row in model.env.cr.fetchall()] # @@ -260,11 +287,6 @@ class IrModel(models.Model): self._cr.execute(sql.SQL('DROP VIEW {}').format(sql.Identifier(table))) elif kind == 'r': self._cr.execute(sql.SQL('DROP TABLE {} CASCADE').format(sql.Identifier(table))) - # discard all translations for this model - self._cr.execute(""" - DELETE FROM ir_translation - WHERE type IN ('model', 'model_terms') AND name LIKE %s - """, [model.model + ',%']) else: _logger.runbot('The model %s could not be dropped because it did not exist in the registry.', model.model) return True @@ -357,21 +379,16 @@ class IrModel(models.Model): cols = list(unique(['model'] + list(rows[0]))) expected = [tuple(row[col] for col in cols) for row in rows] - cr = self.env.cr - query = "SELECT {}, id FROM ir_model WHERE model IN %s".format( - ", ".join(quote(col) for col in cols) - ) - cr.execute(query, [tuple(model_names)]) model_ids = {} existing = {} - for row in cr.fetchall(): - model_ids[row[0]] = row[-1] - existing[row[0]] = row[:-1] + for row in select_en(self, ['id'] + cols, "model IN %s", [tuple(model_names)]): + model_ids[row[1]] = row[0] + existing[row[1]] = row[1:] # create or update rows rows = [row for row in expected if existing.get(row[0]) != row] if rows: - ids = upsert(self.env.cr, self._table, cols, rows, ['model']) + ids = upsert_en(self, cols, rows, ['model']) for row, id_ in zip(rows, ids): model_ids[row[0]] = id_ self.pool.post_init(mark_modified, self.browse(ids), cols[1:]) @@ -417,7 +434,8 @@ class IrModel(models.Model): Parent._inherit_children.discard(name) # add manual models cr = self.env.cr - cr.execute('SELECT * FROM ir_model WHERE state=%s', ['manual']) + # we cannot use self._fields to determine translated fields, as it has not been set up yet + cr.execute("SELECT *, name->>'en_US' AS name FROM ir_model WHERE state = 'manual'") for model_data in cr.dictfetchall(): model_class = self._instanciate(model_data) Model = model_class._build_model(self.pool, cr) @@ -725,12 +743,6 @@ class IrModelFields(models.Model): tables_to_drop.add(rel_name) if field.state == 'manual' and is_model: model._pop_field(field.name) - if field.translate: - # discard all translations for this field - self._cr.execute(""" - DELETE FROM ir_translation - WHERE type IN ('model', 'model_terms') AND name=%s - """, ['%s,%s' % (field.model, field.name)]) if tables_to_drop: # drop the relation tables that are not used by other fields @@ -911,9 +923,12 @@ class IrModelFields(models.Model): # names of the models to patch patched_models = set() + # write callable(self._fields[fname].translate) means changing content + translate_only = self.env.lang not in (None, 'en_US') and all(self._fields[fname].translate is True for fname in vals) + if vals and self: for item in self: - if item.state != 'manual': + if item.state != 'manual' and not translate_only: raise UserError(_('Properties of base fields cannot be altered in this manner! ' 'Please modify them through Python code, ' 'preferably through a custom addon!')) @@ -1041,20 +1056,16 @@ class IrModelFields(models.Model): cols = list(unique(['model', 'name'] + list(rows[0]))) expected = [tuple(row[col] for col in cols) for row in rows] - query = "SELECT {}, id FROM ir_model_fields WHERE model IN %s".format( - ", ".join(quote(col) for col in cols), - ) - cr.execute(query, [tuple(model_names)]) field_ids = {} existing = {} - for row in cr.fetchall(): - field_ids[row[:2]] = row[-1] - existing[row[:2]] = row[:-1] + for row in select_en(self, ['id'] + cols, "model IN %s", [tuple(model_names)]): + field_ids[row[1:3]] = row[0] + existing[row[1:3]] = row[1:] # create or update rows rows = [row for row in expected if existing.get(row[:2]) != row] if rows: - ids = upsert(cr, self._table, cols, rows, ['model', 'name']) + ids = upsert_en(self, cols, rows, ['model', 'name']) for row, id_ in zip(rows, ids): field_ids[row[:2]] = id_ self.pool.post_init(mark_modified, self.browse(ids), cols[2:]) @@ -1086,7 +1097,12 @@ class IrModelFields(models.Model): @tools.ormcache() def _all_manual_field_data(self): cr = self._cr - cr.execute("SELECT * FROM ir_model_fields WHERE state='manual'") + # we cannot use self._fields to determine translated fields, as it has not been set up yet + cr.execute(""" + SELECT *, field_description->>'en_US' AS field_description, help->>'en_US' AS help + FROM ir_model_fields + WHERE state = 'manual' + """) result = defaultdict(dict) for row in cr.dictfetchall(): result[row['model']][row['name']] = row @@ -1168,6 +1184,42 @@ class IrModelFields(models.Model): except Exception: _logger.exception("Failed to load field %s.%s: skipped", model._name, field_data['name']) + @api.model + @tools.ormcache_context('model_name', keys=('lang',)) + def get_field_string(self, model_name): + """ Return the translation of fields strings in the context's language. + Note that the result contains the available translations only. + + :param model_name: the name of a model + :return: the model's fields' strings as a dictionary `{field_name: field_string}` + """ + fields = self.sudo().search([('model', '=', model_name)]) + return {field.name: field.field_description for field in fields} + + @api.model + @tools.ormcache_context('model_name', keys=('lang',)) + def get_field_help(self, model_name): + """ Return the translation of fields help in the context's language. + Note that the result contains the available translations only. + + :param model_name: the name of a model + :return: the model's fields' help as a dictionary `{field_name: field_help}` + """ + fields = self.sudo().search([('model', '=', model_name)]) + return {field.name: field.help for field in fields} + + @api.model + @tools.ormcache_context('model_name', 'field_name', keys=('lang',)) + def get_field_selection(self, model_name, field_name): + """ Return the translation of a field's selection in the context's language. + Note that the result contains the available translations only. + + :param model_name: the name of the field's model + :param field_name: the name of the field + :return: the fields' selection as a list + """ + field = self._get(model_name, field_name) + return [(sel.value, sel.name) for sel in field.selection_ids] class IrModelSelection(models.Model): _name = 'ir.model.fields.selection' @@ -1192,8 +1244,9 @@ class IrModelSelection(models.Model): return self._get_selection_data(field_id) def _get_selection_data(self, field_id): + # return selection as expected on registry (no translations) self._cr.execute(""" - SELECT value, name + SELECT value, name->>'en_US' FROM ir_model_fields_selection WHERE field_id=%s ORDER BY sequence, id @@ -1223,7 +1276,7 @@ class IrModelSelection(models.Model): cr = self.env.cr query = """ - SELECT s.field_id, s.value, s.name, s.sequence + SELECT s.field_id, s.value, s.name->>'en_US', s.sequence FROM ir_model_fields_selection s, ir_model_fields f WHERE s.field_id = f.id AND f.model IN %s """ @@ -1234,7 +1287,7 @@ class IrModelSelection(models.Model): cols = ['field_id', 'value', 'name', 'sequence'] rows = [key + val for key, val in expected.items() if existing.get(key) != val] if rows: - ids = upsert(cr, self._table, cols, rows, ['field_id', 'value']) + ids = upsert_en(self, cols, rows, ['field_id', 'value']) self.pool.post_init(mark_modified, self.browse(ids), cols[2:]) # update their XML ids @@ -1285,8 +1338,10 @@ class IrModelSelection(models.Model): cur_row['value'], model_name, field_name) rows_to_remove.append(cur_row['id']) elif cur_row is None: + new_row['name'] = Json({'en_US': new_row['name']}) rows_to_insert.append(dict(new_row, field_id=field_id)) elif any(new_row[key] != cur_row[key] for key in new_row): + new_row['name'] = Json({'en_US': new_row['name']}) rows_to_update.append(dict(new_row, id=cur_row['id'])) if rows_to_insert: @@ -1308,7 +1363,7 @@ class IrModelSelection(models.Model): a dict {field_name: {value: row_values}}. """ query = """ - SELECT s.* + SELECT s.*, s.name->>'en_US' AS name FROM ir_model_fields_selection s JOIN ir_model_fields f ON s.field_id=f.id WHERE f.model=%s and f.name=%s @@ -1556,7 +1611,7 @@ class IrModelConstraint(models.Model): return assert type in ('f', 'u') cr = self._cr - query = """ SELECT c.id, type, definition, message + query = """ SELECT c.id, type, definition, message->>'en_US' as message FROM ir_model_constraint c, ir_module_module m WHERE c.module=m.id AND c.name=%s AND m.name=%s """ cr.execute(query, (conname, module)) @@ -1572,8 +1627,7 @@ class IrModelConstraint(models.Model): (SELECT id FROM ir_model WHERE model=%s), %s, %s, %s) RETURNING id""" - cr.execute(query, - (conname, self.env.uid, self.env.uid, module, model._name, type, definition, message)) + cr.execute(query, (conname, self.env.uid, self.env.uid, module, model._name, type, definition, Json({'en_US': message}))) return self.browse(cr.fetchone()[0]) cons_id = cons.pop('id') @@ -1582,7 +1636,7 @@ class IrModelConstraint(models.Model): SET write_date=now() AT TIME ZONE 'UTC', write_uid=%s, type=%s, definition=%s, message=%s WHERE id=%s""" - cr.execute(query, (self.env.uid, type, definition, message, cons_id)) + cr.execute(query, (self.env.uid, type, definition, Json({'en_US': message}), cons_id)) return self.browse(cons_id) def _reflect_constraints(self, model_names): @@ -1735,17 +1789,18 @@ class IrModelAccess(models.Model): :rtype: list """ assert access_mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode' - self._cr.execute(""" - SELECT c.name, g.name + lang = self.env.lang or 'en_US' + self._cr.execute(f""" + SELECT COALESCE(c.name->>%s, c.name->>'en_US'), COALESCE(g.name->>%s, g.name->>'en_US') FROM ir_model_access a JOIN ir_model m ON (a.model_id = m.id) JOIN res_groups g ON (a.group_id = g.id) LEFT JOIN ir_module_category c ON (c.id = g.category_id) - WHERE m.model = %%s + WHERE m.model = %s AND a.active = TRUE - AND a.perm_%s = TRUE + AND a.perm_{access_mode} = TRUE ORDER BY c.name, g.name NULLS LAST - """ % access_mode, [model_name]) + """, [lang, lang, model_name]) return [('%s/%s' % x) if x[0] else x[1] for x in self._cr.fetchall()] # The context parameter is useful when the method translates error messages. diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index cbcc8352486..a8ce7c0947a 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -32,6 +32,7 @@ from odoo.osv import expression from odoo.tools.parse_version import parse_version from odoo.tools.misc import topological_sort from odoo.http import request +from odoo.modules import get_module_path, get_module_resource _logger = logging.getLogger(__name__) @@ -947,7 +948,7 @@ class Module(models.Model): for mod in update_mods } mod_names = topological_sort(mod_dict) - self.env['ir.translation']._load_module_terms(mod_names, filter_lang, overwrite) + self.env['ir.module.module']._load_module_terms(mod_names, filter_lang, overwrite) def _check(self): for module in self: @@ -1024,6 +1025,50 @@ class Module(models.Model): return super(Module, self).search_panel_select_range(field_name, **kwargs) + @api.model + def _load_module_terms(self, modules, langs, overwrite=False): + """ Load PO files of the given modules for the given languages. """ + # load i18n files + for module_name in modules: + modpath = get_module_path(module_name) + if not modpath: + continue + for lang in langs: + lang_code = tools.get_iso_codes(lang) + lang_overwrite = overwrite + base_lang_code = None + if '_' in lang_code: + base_lang_code = lang_code.split('_')[0] + + # Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po) + if base_lang_code: + base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po') + if base_trans_file: + _logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang) + tools.trans_load(self._cr, base_trans_file, lang, verbose=False, overwrite=lang_overwrite) + lang_overwrite = True # make sure the requested translation will override the base terms later + + # i18n_extra folder is for additional translations handle manually (eg: for l10n_be) + base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po') + if base_trans_extra_file: + _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang) + tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, overwrite=lang_overwrite) + lang_overwrite = True # make sure the requested translation will override the base terms later + + # Step 2: then load the main translation file, possibly overriding the terms coming from the base language + trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po') + if trans_file: + _logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang) + tools.trans_load(self._cr, trans_file, lang, verbose=False, overwrite=lang_overwrite) + elif lang_code != 'en_US': + _logger.info('module %s: no translation for language %s', module_name, lang_code) + + trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po') + if trans_extra_file: + _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang) + tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, overwrite=lang_overwrite) + return True + DEP_STATES = STATES + [('unknown', 'Unknown')] diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py deleted file mode 100644 index cb74e377eae..00000000000 --- a/odoo/addons/base/models/ir_translation.py +++ /dev/null @@ -1,927 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import hashlib -import itertools -import json -import logging -import operator -from collections import defaultdict -from difflib import get_close_matches - -from odoo import api, fields, models, tools, SUPERUSER_ID, _ -from odoo.exceptions import AccessError, UserError, ValidationError -from odoo.modules import get_module_path, get_module_resource - -_logger = logging.getLogger(__name__) - -TRANSLATION_TYPE = [ - ('model', 'Model Field'), - ('model_terms', 'Structured Model Field'), - ('code', 'Code'), -] - - -class IrTranslationImport(object): - """ Temporary cursor for optimizing mass insert into model 'ir.translation'. - - Open it (attached to a sql cursor), feed it with translation data and - finish() it in order to insert multiple translations in a batch. - """ - _table = 'tmp_ir_translation_import' - - def __init__(self, cr, overwrite=False): - """ Store some values, and also create a temporary SQL table to accept - the data. - - :param model: the model to insert the data into (as a recordset) - """ - self._cr = cr - self._model_table = "ir_translation" - self._overwrite = overwrite - self._debug = False - self._rows = [] - - # Note that Postgres will NOT inherit the constraints or indexes - # of ir_translation, so this copy will be much faster. - query = """ CREATE TEMP TABLE %s ( - imd_model VARCHAR(64), - imd_name VARCHAR(128), - noupdate BOOLEAN - ) INHERITS (%s) """ % (self._table, self._model_table) - self._cr.execute(query) - - def push(self, trans_dict): - """ Feed a translation, as a dictionary, into the cursor """ - params = dict(trans_dict, state="translated") - - self._rows.append((params['name'], params['lang'], params['res_id'], - params['src'], params['type'], params['imd_model'], - params['module'], params['imd_name'], params['value'], - params['state'], params['comments'])) - - def finish(self): - """ Transfer the data from the temp table to ir.translation """ - cr = self._cr - - # Step 0: insert rows in batch - query = """ INSERT INTO %s (name, lang, res_id, src, type, imd_model, - module, imd_name, value, state, comments) - VALUES """ % self._table - for rows in cr.split_for_in_conditions(self._rows): - cr.execute(query + ", ".join(["%s"] * len(rows)), rows) - - _logger.debug("ir.translation.cursor: We have %d entries to process", len(self._rows)) - - # Step 1: resolve ir.model.data references to res_ids - cr.execute(""" UPDATE %s AS ti - SET res_id = imd.res_id, - noupdate = imd.noupdate - FROM ir_model_data AS imd - WHERE ti.res_id IS NULL - AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL - AND ti.module = imd.module AND ti.imd_name = imd.name - AND ti.imd_model = imd.model; """ % self._table) - - if self._debug: - cr.execute(""" SELECT module, imd_name, imd_model FROM %s - WHERE res_id IS NULL AND module IS NOT NULL """ % self._table) - for row in cr.fetchall(): - _logger.info("ir.translation.cursor: missing res_id for %s.%s <%s> ", *row) - - # Records w/o res_id must _not_ be inserted into our db, because they are - # referencing non-existent data. - cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % self._table) - - count = 0 - # Step 2: insert new or upsert non-noupdate translations - if self._overwrite: - cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) - SELECT name, lang, res_id, src, type, value, module, state, comments - FROM %s - WHERE type = 'code' - AND noupdate IS NOT TRUE - ON CONFLICT (type, lang, md5(src)) WHERE type = 'code' - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, - CASE WHEN %s.comments = 'openerp-web' THEN 'openerp-web' ELSE EXCLUDED.comments END - ) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """ % (self._model_table, self._table, self._model_table)) - count += cr.rowcount - cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) - SELECT name, lang, res_id, src, type, value, module, state, comments - FROM %s - WHERE type = 'model' - AND noupdate IS NOT TRUE - ON CONFLICT (type, lang, name, res_id) WHERE type = 'model' - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """ % (self._model_table, self._table)) - count += cr.rowcount - - cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) - SELECT name, lang, res_id, src, type, value, module, state, comments - FROM %s - WHERE type = 'model_terms' - AND noupdate IS NOT TRUE - ON CONFLICT (type, name, lang, res_id, md5(src)) - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """ % (self._model_table, self._table)) - count += cr.rowcount - cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) - SELECT name, lang, res_id, src, type, value, module, state, comments - FROM %s - WHERE %%s OR noupdate is true - ON CONFLICT DO NOTHING; - """ % (self._model_table, self._table), [not self._overwrite]) - count += cr.rowcount - - if self._debug: - cr.execute("SELECT COUNT(*) FROM ONLY %s" % self._model_table) - total = cr.fetchone()[0] - _logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", total, count) - - # Step 3: cleanup - cr.execute("DROP TABLE %s" % self._table) - self._rows.clear() - return True - - -class IrTranslation(models.Model): - _name = "ir.translation" - _description = 'Translation' - _log_access = False - - name = fields.Char(string='Translated field', required=True) - res_id = fields.Integer(string='Record ID', index=True) - lang = fields.Selection(selection='_get_languages', string='Language', validate=False) - type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True) - src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility - value = fields.Text(string='Translation Value') - module = fields.Char(index=True, help="Module this term belongs to") - - state = fields.Selection([('to_translate', 'To Translate'), - ('inprogress', 'Translation in Progress'), - ('translated', 'Translated')], - string="Status", default='to_translate', - help="Automatically set to let administators find new terms that might need to be translated") - - # aka gettext extracted-comments - we use them to flag openerp-web translation - # cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html - comments = fields.Text(string='Translation comments', index=True) - - _sql_constraints = [ - ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)', - 'Language code of translation item must be among known languages'), - ] - - @api.model - def _get_languages(self): - return self.env['res.lang'].get_installed() - - def _auto_init(self): - res = super(IrTranslation, self)._auto_init() - # Add separate md5 index on src (no size limit on values, and good performance). - tools.create_index(self._cr, 'ir_translation_src_md5', self._table, ['md5(src)']) - # Cover 'model_terms' type - tools.create_unique_index(self._cr, 'ir_translation_unique', self._table, - ['type', 'name', 'lang', 'res_id', 'md5(src)']) - if not tools.index_exists(self._cr, 'ir_translation_code_unique'): - self._cr.execute("CREATE UNIQUE INDEX ir_translation_code_unique ON ir_translation (type, lang, md5(src)) WHERE type = 'code'") - if not tools.index_exists(self._cr, 'ir_translation_model_unique'): - self._cr.execute("CREATE UNIQUE INDEX ir_translation_model_unique ON ir_translation (type, lang, name, res_id) WHERE type = 'model'") - - return res - - @api.model - def _get_ids(self, name, tt, lang, ids): - """ Return the translations of records. - - :param name: a string defined as "," - :param tt: the type of translation (should always be "model") - :param lang: the language code - :param ids: the ids of the given records - """ - translations = dict.fromkeys(ids, False) - if ids: - self._cr.execute("""SELECT res_id, value FROM ir_translation - WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s""", - (lang, tt, name, tuple(ids))) - for res_id, value in self._cr.fetchall(): - translations[res_id] = value - return translations - - CACHED_MODELS = {'ir.model.fields', 'ir.ui.view'} - - def _modified_model(self, model_name): - """ Invalidate the ormcache if necessary, depending on ``model_name``. - This should be called when modifying translations of type 'model'. - """ - if model_name in self.CACHED_MODELS: - self.clear_caches() - - def _modified(self): - """ Invalidate the ormcache if necessary, depending on the translations ``self``. """ - # DLE P63: test_views.py - for trans in self: - if trans.type == 'model_terms' and trans.res_id: - model, field = trans.name.split(',') - if model in self.env: - model = self.env[model] - if field in model._fields: - field = model._fields[field] - record = model.browse(trans.res_id) - record.modified([field.name]) - for trans in self: - if (trans.type != 'model' or - (trans.name.split(',')[0] in self.CACHED_MODELS) or - (trans.comments and 'openerp-web' in trans.comments)): # clear get_web_trans_hash - self.clear_caches() - break - - @api.model - def _set_ids(self, name, tt, lang, ids, value, src=None): - """ Update the translations of records. - - :param name: a string defined as "," - :param tt: the type of translation (should always be "model") - :param lang: the language code - :param ids: the ids of the given records - :param value: the value of the translation - :param src: the source of the translation - """ - self._modified_model(name.split(',')[0]) - self.flush_model() - - # update existing translations - self._cr.execute("""UPDATE ir_translation - SET value=%s, src=%s, state=%s - WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s - RETURNING res_id""", - (value, src, 'translated', lang, tt, name, tuple(ids))) - existing_ids = [row[0] for row in self._cr.fetchall()] - self.invalidate_model(['value', 'src', 'state']) - - # create missing translations - self.sudo().create([{ - 'lang': lang, - 'type': tt, - 'name': name, - 'res_id': res_id, - 'value': value, - 'src': src, - 'state': 'translated', - } - for res_id in set(ids) - set(existing_ids) - ]) - return len(ids) - - def _set_source(self, name, ids, src): - """ Update the translation source of records. - - :param name: a string defined as "," - :param ids: the ids of the given records - :param src: the source of the translation - """ - self.flush_model() - self._cr.execute("""UPDATE ir_translation - SET src=%s - WHERE type=%s AND name=%s AND res_id IN %s - RETURNING id""", - (src, 'model', name, tuple(ids))) - existing_ids = [row[0] for row in self._cr.fetchall()] - # invalidate src for updated translations - self.browse(existing_ids).invalidate_recordset(['src']) - - @api.model - def _get_source_query(self, name, types, lang, source, res_id): - self.flush_model() - if source: - # Note: the extra test on md5(src) is a hint for postgres to use the - # index ir_translation_src_md5 - query = """SELECT value FROM ir_translation - WHERE lang=%s AND type in %s AND src=%s AND md5(src)=md5(%s)""" - source = tools.ustr(source) - params = (lang or '', types, source, source) - if res_id: - query += " AND res_id in %s" - params += (res_id,) - if name: - query += " AND name=%s" - params += (tools.ustr(name),) - else: - query = """ SELECT value FROM ir_translation - WHERE lang=%s AND type in %s AND name=%s """ - params = (lang or '', types, tools.ustr(name)) - - return (query, params) - - @tools.ormcache('name', 'types', 'lang', 'source', 'res_id') - def __get_source(self, name, types, lang, source, res_id): - # res_id is a tuple or None, otherwise ormcache cannot cache it! - query, params = self._get_source_query(name, types, lang, source, res_id) - self._cr.execute(query, params) - res = self._cr.fetchone() - trad = res and res[0] or u'' - if source and not trad: - return tools.ustr(source) - return trad - - @api.model - def _get_source(self, name, types, lang, source=None, res_id=None): - """ Return the translation for the given combination of ``name``, - ``type``, ``language`` and ``source``. All values passed to this method - should be unicode (not byte strings), especially ``source``. - - :param name: identification of the term to translate, such as field name (optional if source is passed) - :param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings) - :param lang: language code of the desired translation - :param source: optional source term to translate (should be unicode) - :param res_id: optional resource id or a list of ids to translate (if used, ``source`` should be set) - :rtype: unicode - :return: the request translation, or an empty unicode string if no translation was - found and `source` was not passed - """ - # FIXME: should assert that `source` is unicode and fix all callers to - # always pass unicode so we can remove the string encoding/decoding. - if not lang: - return tools.ustr(source or '') - if isinstance(types, str): - types = (types,) - if res_id: - if isinstance(res_id, int): - res_id = (res_id,) - else: - res_id = tuple(res_id) - return self.__get_source(name, types, lang, source, res_id) - - @api.model - def _get_terms_query(self, field, records): - """ Utility function that makes the query for field terms. """ - self.flush_model() - query = """ SELECT * FROM ir_translation - WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s """ - name = "%s,%s" % (field.model_name, field.name) - params = (records.env.lang, 'model_terms', name, tuple(records.ids)) - return query, params - - @api.model - def _get_terms_mapping(self, field, records): - """ Return a function mapping a ir_translation row (dict) to a value. - This method is called before querying the database for translations. - """ - return lambda data: data['value'] - - @api.model - def _get_terms_translations(self, field, records): - """ Return the terms and translations of a given `field` on `records`. - - :return: {record_id: {source: value}} - """ - result = {rid: {} for rid in records.ids} - if records: - map_trans = self._get_terms_mapping(field, records) - query, params = self._get_terms_query(field, records) - self._cr.execute(query, params) - for data in self._cr.dictfetchall(): - result[data['res_id']][data['src']] = map_trans(data) - return result - - @api.model - def _sync_terms_translations(self, field, records): - """ Synchronize the translations to the terms to translate, after the - English value of a field is modified. The algorithm tries to match - existing translations to the terms to translate, provided the distance - between modified strings is not too large. It allows to not retranslate - data where a typo has been fixed in the English value. - """ - if not callable(field.translate): - return - - Translation = self.env['ir.translation'] - outdated = Translation - discarded = Translation - - for record in records: - # get field value and terms to translate - value = record[field.name] - terms = set(field.get_trans_terms(value)) - translations = Translation.search([ - ('type', '=', 'model_terms'), - ('name', '=', "%s,%s" % (field.model_name, field.name)), - ('res_id', '=', record.id), - ]) - - if not terms: - # discard all translations for that field - discarded += translations - continue - - # remap existing translations on terms when possible; each term - # should be translated at most once per language - done = set() # {(src, lang), ...} - translations_to_match = [] - - for translation in translations: - if not translation.value: - discarded += translation - # consider it done to avoid being matched against another term - done.add((translation.src, translation.lang)) - elif translation.src in terms: - done.add((translation.src, translation.lang)) - else: - translations_to_match.append(translation) - - if translations_to_match: - text2term = {field.get_text_content(term): term for term in terms} - for translation in translations_to_match: - # match the terms without formatting elements - src_text = field.get_text_content(translation.src) - matches = get_close_matches(src_text, text2term, 1, 0.9) - src = text2term[matches[0]] if matches else None - if not src: - outdated += translation - elif (src, translation.lang) in done: - discarded += translation - else: - vals = {'src': src, 'state': translation.state} - if translation.lang == records.env.lang: - vals['value'] = src - translation.write(vals) - done.add((src, translation.lang)) - - # process outdated and discarded translations - outdated.write({'state': 'to_translate'}) - - if discarded: - # delete in SQL to avoid invalidating the whole cache - discarded._modified() - discarded.modified(self._fields) - discarded.flush_recordset() - discarded.invalidate_recordset() - self.env.cr.execute("DELETE FROM ir_translation WHERE id IN %s", [discarded._ids]) - - @api.model - @tools.ormcache_context('model_name', keys=('lang',)) - def get_field_string(self, model_name): - """ Return the translation of fields strings in the context's language. - Note that the result contains the available translations only. - - :param model_name: the name of a model - :return: the model's fields' strings as a dictionary `{field_name: field_string}` - """ - fields = self.env['ir.model.fields'].sudo().search([('model', '=', model_name)]) - return {field.name: field.field_description for field in fields} - - @api.model - @tools.ormcache_context('model_name', keys=('lang',)) - def get_field_help(self, model_name): - """ Return the translation of fields help in the context's language. - Note that the result contains the available translations only. - - :param model_name: the name of a model - :return: the model's fields' help as a dictionary `{field_name: field_help}` - """ - fields = self.env['ir.model.fields'].sudo().search([('model', '=', model_name)]) - return {field.name: field.help for field in fields} - - @api.model - @tools.ormcache_context('model_name', 'field_name', keys=('lang',)) - def get_field_selection(self, model_name, field_name): - """ Return the translation of a field's selection in the context's language. - Note that the result contains the available translations only. - - :param model_name: the name of the field's model - :param field_name: the name of the field - :return: the fields' selection as a list - """ - field = self.env['ir.model.fields']._get(model_name, field_name) - return [(sel.value, sel.name) for sel in field.selection_ids] - - def check(self, mode): - """ Check access rights of operation ``mode`` on ``self`` for the - current user. Raise an AccessError in case conditions are not met. - """ - if self.env.is_superuser(): - return - - # collect translated field records (model_ids) and other translations - trans_ids = [] - model_ids = defaultdict(set) - model_fields = defaultdict(set) - for trans in self: - if trans.type in ('model', 'model_terms'): - mname, fname = trans.name.split(',') - model_ids[mname].add(trans.res_id) - model_fields[mname].add(fname) - else: - trans_ids.append(trans.id) - - # check for regular access rights on other translations - if trans_ids: - records = self.browse(trans_ids) - records.check_access_rights(mode) - records.check_access_rule(mode) - - # check for read/write access on translated field records - fmode = 'read' if mode == 'read' else 'write' - for mname, ids in model_ids.items(): - records = self.env[mname].browse(ids).exists() - records.check_access_rights(fmode) - records.check_field_access_rights(fmode, model_fields[mname]) - if mode == 'create' and set(records._ids) != ids: - raise ValidationError(_("Creating translation on non existing records")) - if not records: - continue - records.check_access_rule(fmode) - - @api.constrains('type', 'name', 'value') - def _check_value(self): - for trans in self.with_context(lang=None): - if trans.type == 'model' and trans.value: - mname, fname = trans.name.split(',') - record = trans.env[mname].browse(trans.res_id) - field = record._fields[fname] - if callable(field.translate): - src = trans.src - val = trans.value.strip() - # check whether applying (src -> val) then (val -> src) - # gives the original value back - value0 = field.translate(lambda term: None, record[fname]) - value1 = field.translate({src: val}.get, value0) - # don't check the reverse if no translation happened - if value0 == value1: - continue - value2 = field.translate({val: src}.get, value1) - if value2 != value0: - raise ValidationError(_("Translation is not valid:\n%s", val)) - - @api.model_create_multi - def create(self, vals_list): - records = super(IrTranslation, self.sudo()).create(vals_list).with_env(self.env) - records.check('create') - records._modified() - # DLE P62: `test_translate.py`, `test_sync` - self.env.flush_all() - return records - - def write(self, vals): - if vals.get('value'): - vals.setdefault('state', 'translated') - elif vals.get('src') or not vals.get('value', True): - vals.setdefault('state', 'to_translate') - self.check('write') - result = super(IrTranslation, self.sudo()).write(vals) - self.check('write') - self._modified() - # DLE P62: `test_translate.py`, `test_sync` - # when calling `flush` with a field list, if there is no value for one of these fields, - # the flush to database is not done. - # this causes issues when changing the src/value of a translation, as when we read, we ask the flush, - # but its not really the field which is in the towrite values, but its translation - self.env.flush_all() - return result - - def unlink(self): - self.check('unlink') - self._modified() - return super(IrTranslation, self.sudo()).unlink() - - @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - # DLE P67, `test_new_fields.py`, `test_80_copy` - # When assigning a translation to a field - # e.g. email.with_context(lang='fr_FR').label = "bonjour" - # and then search on translations for this translation, must flush as the translation has not yet been written in database - if any(field.translate for field in self.env.cache.get_dirty_fields()): - self.env.flush_all() - return super(IrTranslation, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) - - @api.model - def insert_missing(self, field, records): - """ Insert missing translations for `field` on `records`. """ - self.flush_model() - records = records.with_context(lang=None) - external_ids = records.get_external_id() # if no xml_id, empty string - if callable(field.translate): - # insert missing translations for each term in src - query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module, state) - SELECT l.code, 'model_terms', %(name)s, %(res_id)s, %(src)s, '', %(module)s, 'to_translate' - FROM res_lang l - WHERE l.active AND NOT EXISTS ( - SELECT 1 FROM ir_translation - WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s - ) - ON CONFLICT DO NOTHING; - """ - for record in records: - module = external_ids[record.id].split('.')[0] - src = record[field.name] or None - for term in set(field.get_trans_terms(src)): - self._cr.execute(query, { - 'name': "%s,%s" % (field.model_name, field.name), - 'res_id': record.id, - 'src': term, - 'module': module - }) - else: - # insert missing translations for src - query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module, state) - SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, '', %(module)s, 'to_translate' - FROM res_lang l - WHERE l.active AND NOT EXISTS ( - SELECT 1 FROM ir_translation - WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s - ); - - DELETE FROM ir_translation dup - WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s - AND dup.id NOT IN (SELECT MAX(t.id) - FROM ir_translation t - WHERE t.lang=dup.lang AND type='model' AND name=%(name)s AND res_id=%(res_id)s - ); - - UPDATE ir_translation SET src=%(src)s - WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s; - """ - for record in records: - module = external_ids[record.id].split('.')[0] - self._cr.execute(query, { - 'name': "%s,%s" % (field.model_name, field.name), - 'res_id': record.id, - 'src': record[field.name] or None, - 'module': module - }) - self._modified_model(field.model_name) - - @api.model - def _upsert_translations(self, vals_list): - """ Insert or update translations of type 'model' or 'model_terms'. - - This method is used for creations of translations where the given - ``vals_list`` is trusted to be the right values and potential - conflicts should be updated to the new given value. - Mandatory values: name, lang, res_id, src, type - The other keys are ignored during update if not present - """ - self.flush_model() - rows_by_type = defaultdict(list) - for vals in vals_list: - rows_by_type[vals['type']].append(( - vals['name'], vals['lang'], vals['res_id'], vals['src'] or '', vals['type'], - vals.get('module'), vals['value'] or '', vals.get('state'), vals.get('comments'), - )) - - if rows_by_type['model']: - query = """ - INSERT INTO ir_translation (name, lang, res_id, src, type, - module, value, state, comments) - VALUES {} - ON CONFLICT (type, lang, name, res_id) WHERE type='model' - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = - (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, - EXCLUDED.value, - COALESCE(EXCLUDED.module, ir_translation.module), - COALESCE(EXCLUDED.state, ir_translation.state), - COALESCE(EXCLUDED.comments, ir_translation.comments)) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """.format(", ".join(["%s"] * len(rows_by_type['model']))) - self.env.cr.execute(query, rows_by_type['model']) - - if rows_by_type['model_terms']: - query = """ - INSERT INTO ir_translation (name, lang, res_id, src, type, - module, value, state, comments) - VALUES {} - ON CONFLICT (type, name, lang, res_id, md5(src)) - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = - (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, - EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """.format(", ".join(["%s"] * len(rows_by_type['model_terms']))) - self.env.cr.execute(query, rows_by_type['model_terms']) - - def _update_translations(self, vals_list): - """ Update translations of type 'model' or 'model_terms'. - - This method is used for update of translations where the given - ``vals_list`` is trusted to be the right values - No new translation will be created - """ - self.flush_model() - grouped_rows = {} - for vals in vals_list: - key = (vals['lang'], vals['type'], vals['name']) - grouped_rows.setdefault(key, [vals['value'], vals['src'], vals['state'], []]) - grouped_rows[key][3].append(vals['res_id']) - - for where, values in grouped_rows.items(): - self._cr.execute( - """ UPDATE ir_translation - SET value=%s, - src=%s, - state=%s - WHERE lang=%s AND type=%s AND name=%s AND res_id in %s - """, - (values[0], values[1], values[2], where[0], where[1], where[2], tuple(values[3])) - ) - self.invalidate_model(['value', 'src', 'state']) - - @api.model - def translate_fields(self, model, id, field=None): - """ Open a view for translating the field(s) of the record (model, id). """ - main_lang = 'en_US' - if not self.env['res.lang'].search_count([('code', '!=', main_lang)]): - raise UserError(_("Translation features are unavailable until you install an extra translation.")) - - # determine domain for selecting translations - record = self.env[model].with_context(lang=main_lang).browse(id) - domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')] - - def make_domain(fld, rec): - name = "%s,%s" % (fld.model_name, fld.name) - return ['&', ('res_id', '=', rec.id), ('name', '=', name)] - - # insert missing translations, and extend domain for related fields - for name, fld in record._fields.items(): - if not fld.translate: - continue - - rec = record - if fld.related: - try: - # traverse related fields up to their data source - while fld.related: - rec, fld = fld.traverse_related(rec) - if rec: - domain = ['|'] + domain + make_domain(fld, rec) - except AccessError: - continue - - assert fld.translate and rec._name == fld.model_name - self.insert_missing(fld, rec) - - action = { - 'name': _('Translate'), - 'res_model': 'ir.translation', - 'type': 'ir.actions.act_window', - 'view_mode': 'tree', - 'view_id': self.env.ref('base.view_translation_dialog_tree').id, - 'target': 'current', - 'domain': domain, - 'context': {}, - } - if field: - fld = record._fields[field] - if not fld.related: - action['context'] = { - 'search_default_name': "%s,%s" % (fld.model_name, fld.name), - } - else: - rec = record - try: - while fld.related: - rec, fld = fld.traverse_related(rec) - if rec: - action['context'] = {'search_default_name': "%s,%s" % (fld.model_name, fld.name),} - except AccessError: - pass - - action['target'] = 'new' - action['context']['translation_type'] = 'text' if fld.type in ['text', 'html'] else 'char' - action['context']['translation_show_src'] = False - if callable(fld.translate): - action['view_id'] = self.env.ref('base.view_translation_lang_src_value_tree').id, - action['context']['translation_show_src'] = True - else: - action['view_id'] = self.env.ref('base.view_translation_lang_value_tree').id, - - return action - - def _get_import_cursor(self, overwrite): - """ Return a cursor-like object for fast inserting translations """ - return IrTranslationImport(self._cr, overwrite) - - def _load_module_terms(self, modules, langs, overwrite=False): - """ Load PO files of the given modules for the given languages. """ - # load i18n files - for module_name in modules: - modpath = get_module_path(module_name) - if not modpath: - continue - for lang in langs: - lang_code = tools.get_iso_codes(lang) - lang_overwrite = overwrite - base_lang_code = None - if '_' in lang_code: - base_lang_code = lang_code.split('_')[0] - - # Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po) - if base_lang_code: - base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po') - if base_trans_file: - _logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang) - tools.trans_load(self._cr, base_trans_file, lang, verbose=False, overwrite=lang_overwrite) - lang_overwrite = True # make sure the requested translation will override the base terms later - - # i18n_extra folder is for additional translations handle manually (eg: for l10n_be) - base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po') - if base_trans_extra_file: - _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang) - tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, overwrite=lang_overwrite) - lang_overwrite = True # make sure the requested translation will override the base terms later - - # Step 2: then load the main translation file, possibly overriding the terms coming from the base language - trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po') - if trans_file: - _logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang) - tools.trans_load(self._cr, trans_file, lang, verbose=False, overwrite=lang_overwrite) - elif lang_code != 'en_US': - _logger.info('module %s: no translation for language %s', module_name, lang_code) - - trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po') - if trans_extra_file: - _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang) - tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, overwrite=lang_overwrite) - return True - - @api.model - def get_technical_translations(self, model_name): - """ Find the translations for the fields of `model_name` - - Find the technical translations for the fields of the model, including - string, tooltip and available selections. - - :return: action definition to open the list of available translations - """ - fields = self.env['ir.model.fields'].search([('model', '=', model_name)]) - selection_ids = tools.flatten([field.selection_ids.ids for field in fields if field.ttype == 'selection']) - view = self.env.ref("base.view_translation_tree", False) or self.env['ir.ui.view'] - return { - 'name': _("Technical Translations"), - 'view_mode': 'tree', - 'views': [(view.id, "list")], - 'res_model': 'ir.translation', - 'type': 'ir.actions.act_window', - 'domain': [ - '&', - ('type', '=', 'model'), - '|', - '&', ('res_id', 'in', fields.ids), - ('name', 'like', 'ir.model.fields,'), - '&', ('res_id', 'in', selection_ids), - ('name', 'like', 'ir.model.fields.selection,') - ], - } - - @api.model - def get_translations_for_webclient(self, mods, lang): - if not mods: - mods = [x['name'] for x in self.env['ir.module.module'].sudo().search_read( - [('state', '=', 'installed')], ['name'])] - if not lang: - lang = self._context.get("lang") - langs = self.env['res.lang']._lang_get(lang) - lang_params = None - if langs: - lang_params = { - "name": langs.name, - "direction": langs.direction, - "date_format": langs.date_format, - "time_format": langs.time_format, - "grouping": langs.grouping, - "decimal_point": langs.decimal_point, - "thousands_sep": langs.thousands_sep, - "week_start": langs.week_start, - } - lang_params['week_start'] = int(lang_params['week_start']) - lang_params['code'] = lang - - # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is - # done server-side when the language is loaded, so we only need to load the user's lang. - translations_per_module = {} - messages = self.env['ir.translation'].sudo().search_read([ - ('module', 'in', mods), ('lang', '=', lang), - ('comments', 'like', 'openerp-web'), ('value', '!=', False), - ('value', '!=', '')], - ['module', 'src', 'value', 'lang'], order='module') - for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')): - translations_per_module.setdefault(mod, {'messages': []}) - translations_per_module[mod]['messages'].extend({ - 'id': m['src'], - 'string': m['value']} - for m in msg_group) - - return translations_per_module, lang_params - - @api.model - @tools.ormcache('frozenset(mods)', 'lang') - def get_web_translations_hash(self, mods, lang): - translations, lang_params = self.get_translations_for_webclient(mods, lang) - translation_cache = { - 'lang_parameters': lang_params, - 'modules': translations, - 'lang': lang, - 'multi_lang': len(self.env['res.lang'].sudo().get_installed()) > 1, - } - return hashlib.sha1(json.dumps(translation_cache, sort_keys=True).encode()).hexdigest() diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 63786b76fcf..22ae309fac8 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -320,6 +320,9 @@ actual arch. data['arch_fs'] = '/'.join(path_info[0:2]) data['arch_updated'] = False view.write(data) + # the xml_translate will clean the arch_db when write (e.g. ('
') -> ('
')) + # view.arch should be reassigned here + view.arch = view.arch_db # the field 'arch' depends on the context and has been implicitly # modified in all languages; the invalidation below ensures that the # field does not keep an old value in another environment @@ -566,6 +569,12 @@ actual arch. self.inherit_children_ids.unlink() return super(View, self).unlink() + def _update_field_translations(self, fname, translations, digest=None): + res = super()._update_field_translations(fname, translations, digest) + if fname == 'arch_db' and 'install_filename' not in self._context: + self.write({'arch_updated': True}) + return res + @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() @@ -2028,10 +2037,6 @@ actual arch. # Misc #------------------------------------------------------ - def open_translations(self): - """ Open a view for editing the translations of field 'arch_db'. """ - return self.env['ir.translation'].translate_fields('ir.ui.view', self.id, 'arch_db') - @api.model def _validate_custom_views(self, model): """Validate architecture of custom views (= without xml id) for a given model. diff --git a/odoo/addons/base/models/res_lang.py b/odoo/addons/base/models/res_lang.py index 3fa70be9e50..16e6850e3ef 100644 --- a/odoo/addons/base/models/res_lang.py +++ b/odoo/addons/base/models/res_lang.py @@ -305,8 +305,6 @@ class Lang(models.Model): raise UserError(_("You cannot delete the language which is Active!\nPlease de-activate the language first.")) def unlink(self): - for language in self: - self.env['ir.translation'].search([('lang', '=', language.code)]).unlink() self.clear_caches() return super(Lang, self).unlink() diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv index 1ef2990e269..83006e29d53 100644 --- a/odoo/addons/base/security/ir.model.access.csv +++ b/odoo/addons/base/security/ir.model.access.csv @@ -28,8 +28,6 @@ "access_ir_sequence_group_system","ir_sequence group_system","model_ir_sequence","group_system",1,1,1,1 "access_ir_sequence_date_range_group_user","ir_sequence_date_range group_user","model_ir_sequence_date_range","group_user",1,0,0,0 "access_ir_sequence_date_range_group_system","ir_sequence_date_range group_system","model_ir_sequence_date_range","group_system",1,1,1,1 -"access_ir_translation_all","ir_translation all","model_ir_translation","group_user",1,1,1,1 -"access_ir_translation_group_system","ir_translation group_system","model_ir_translation","group_system",1,1,1,1 "access_ir_ui_menu_group_user","ir_ui_menu group_user","model_ir_ui_menu",base.group_user,1,0,0,0 "access_ir_ui_menu_group_system","ir_ui_menu group_system","model_ir_ui_menu","group_system",1,1,1,1 "access_ir_ui_view_group_user","ir_ui_view group_user","model_ir_ui_view",,0,0,0,0 @@ -121,7 +119,6 @@ "access_base_module_upgrade","access.base.module.upgrade","model_base_module_upgrade","base.group_system",1,1,1,0 "access_base_module_uninstall","access.base.module.uninstall","model_base_module_uninstall","base.group_system",1,1,1,0 "access_base_language_export","access.base.language.export","model_base_language_export","base.group_user",1,1,1,0 -"access_base_update_translations","access.base.update.translations","model_base_update_translations","base.group_system",1,1,1,0 "access_base_partner_merge_line","access.base.partner.merge.line","model_base_partner_merge_line","base.group_partner_manager",1,1,1,1 "access_base_partner_merge_automatic_wizard","access.base.partner.merge.automatic.wizard","model_base_partner_merge_automatic_wizard","base.group_partner_manager",1,1,1,0 "access_ir_profile","ir_profile","model_ir_profile","group_system",1,1,1,1 diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index 4db957b9399..df162c976cb 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -214,6 +214,17 @@ class TestAPI(SavepointCaseWithUserDemo): with self.assertRaises(AccessError): demo_partner.company_id.name + @mute_logger('odoo.models') + def test_55_environment_lang(self): + """ Check the record env.lang behavior """ + partner = self.partner_demo + self.env['res.lang']._activate_lang('fr_FR') + self.assertEqual(partner.with_context(lang=None).env.lang, None, 'None lang context should have None env.lang') + self.assertEqual(partner.with_context(lang='en_US').env.lang, 'en_US', 'en_US active lang context should have en_US env.lang') + self.assertEqual(partner.with_context(lang='fr_FR').env.lang, 'fr_FR', 'fr_FR active lang context should have fr_FR env.lang') + self.assertEqual(partner.with_context(lang='nl_NL').env.lang, None, 'Inactive lang context lang should have None env.lang') + self.assertEqual(partner.with_context(lang='Dummy').env.lang, None, 'Ilegal lang context should have None env.lang') + @mute_logger('odoo.models') def test_60_cache(self): """ Check the record cache behavior """ diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 26fe1116140..abb3c68311a 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -1171,14 +1171,8 @@ class TestQueries(TransactionCase): with self.assertQueries([''' SELECT "res_partner_title".id FROM "res_partner_title" - LEFT JOIN "ir_translation" AS "res_partner_title__name" ON - ("res_partner_title"."id" = "res_partner_title__name"."res_id" - AND "res_partner_title__name"."type" = 'model' - AND "res_partner_title__name"."name" = %s - AND "res_partner_title__name"."lang" = %s - AND "res_partner_title__name"."value" != %s) - WHERE COALESCE("res_partner_title__name"."value", "res_partner_title"."name") LIKE %s - ORDER BY COALESCE("res_partner_title__name"."value", "res_partner_title"."name") + WHERE jsonb_path_query_array("res_partner_title"."name", '$.*')::text like %s + ORDER BY COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US') ''']): Model.search([('name', 'like', 'foo')]) @@ -1226,7 +1220,7 @@ class TestQueries(TransactionCase): with self.assertQueries([''' SELECT "ir_model".id FROM "ir_model" - WHERE ("ir_model"."name" ILIKE %s OR ("ir_model"."model"::text ILIKE %s)) + WHERE (jsonb_path_query_array("ir_model"."name",'$.*')::text ILIKE %s OR ("ir_model"."model"::text ILIKE %s)) ORDER BY "ir_model"."model" LIMIT 100 ''']): @@ -1237,7 +1231,7 @@ class TestQueries(TransactionCase): SELECT "ir_model".id FROM "ir_model" WHERE ( - "ir_model"."name" NOT ILIKE %s + jsonb_path_query_array("ir_model"."name",'$.*')::text NOT ILIKE %s AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL) ) ORDER BY "ir_model"."model" diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index 96151c05580..0a9fdc4bee8 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -5,9 +5,11 @@ import logging import time from psycopg2 import IntegrityError +from psycopg2.extras import Json +import io from odoo.exceptions import AccessError, ValidationError -from odoo.tools import mute_logger +from odoo.tools import trans_load_data from odoo.tools.translate import quote, unquote, xml_translate, html_translate from odoo.tests.common import TransactionCase, BaseCase, new_test_user, tagged @@ -264,7 +266,7 @@ class TestLanguageInstall(TransactionCase): def _load_module_terms(self, modules, langs, overwrite=False): loaded.append((modules, langs, overwrite)) - with patch('odoo.addons.base.models.ir_translation.IrTranslation._load_module_terms', _load_module_terms): + 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 @@ -280,15 +282,17 @@ class TestTranslation(TransactionCase): 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.env['ir.translation'].create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'module': 'base', - 'lang': 'fr_FR', - 'res_id': cls.customers.id, - 'value': 'Clients', - 'state': 'translated', - }) + + 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' + trans_load_data(cls.env.cr, f, 'po', 'fr_FR', verbose=True, overwrite=True) def test_101_create_translated_record(self): category = self.customers.with_context({}) @@ -301,18 +305,11 @@ class TestTranslation(TransactionCase): category = self.customers.with_context({'lang': 'fr_FR'}).copy() category_no = category.with_context({}) - self.assertEqual(category_no.name, 'Customers', "Duplication did not set untranslated value") + 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") - translation_fr = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', category.id), - ('lang', '=', 'fr_FR'), - ]) - self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source") - def test_103_duplicate_record_fr(self): category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'}) @@ -322,13 +319,6 @@ class TestTranslation(TransactionCase): category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value") - translation_fr = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', category.id), - ('lang', '=', 'fr_FR'), - ]) - self.assertEqual(translation_fr.src, 'Clients (copie)', "Did not set new name as source") - def test_104_orderby_translated_field(self): """ Test search ordered by a translated field. """ # create a category with a French translation @@ -340,75 +330,6 @@ class TestTranslation(TransactionCase): self.assertEqual(categories.ids, [padawans.id, self.customers.id], "Search ordered by translated name should return Padawans (Apprentis) before Customers (Clients)") - def test_105_duplicated_translation(self): - """ Test synchronizing translations with duplicated source """ - # create a category with a French translation - padawans = self.env['res.partner.category'].create({'name': 'Padawan'}) - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'module':'base', - 'lang': 'fr_FR', - 'res_id': padawans.id, - 'value': 'Apprenti', - 'state': 'translated', - }) - # change name and insert a duplicate manually - padawans.write({'name': 'Padawans'}) - with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): - with self.env.cr.savepoint(): - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'module':'base', - 'lang': 'fr_FR', - 'res_id': padawans.id, - 'value': 'Apprentis', - 'state': 'translated', - }) - self.env['ir.translation'].translate_fields('res.partner.category', padawans.id, 'name') - translations = self.env['ir.translation'].search([ - ('res_id', '=', padawans.id), ('name', '=', 'res.partner.category,name'), ('lang', '=', 'fr_FR'), - ]) - self.assertEqual(len(translations), 1, "Translations were not duplicated after `translate_fields` call") - self.assertEqual(translations.value, "Apprenti", "The first translation must stay") - - def test_106_en_us_translation(self): - """ Test synchronizing translations with duplicated source """ - # create a category with a French translation - cheese = self.env['res.partner.category'].create({'name': 'Cheese'}) - - self.env['ir.translation'].translate_fields('res.partner.category', cheese.id, 'name') - - translations = self.env['ir.translation'].search([('name', '=', 'res.partner.category,name'), ('res_id', '=', cheese.id)], order='lang') - self.assertEqual(len(translations), 2) - self.assertRecordValues(translations, - [{'lang': 'en_US', 'src': 'Cheese', 'value': ''}, - {'lang': 'fr_FR', 'src': 'Cheese', 'value': ''}]) - - # Translate in both language - translations[0].value = 'The Cheese' - translations[1].value = 'Fromage' - - # lang=None bypass translation system - self.assertEqual(cheese.with_context(lang=None).name, 'Cheese') - self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage') - self.assertEqual(cheese.with_context(lang='en_US').name, 'The Cheese') - self.env.flush_all() - - # set a new master value - cheese.with_context(lang='en_US').write({'name': 'Delicious Cheese'}) - - # every src must be updated - self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese') - self.assertRecordValues(translations, - [{'lang': 'en_US', 'src': 'Delicious Cheese', 'value': 'Delicious Cheese'}, - {'lang': 'fr_FR', 'src': 'Delicious Cheese', 'value': 'Fromage'}]) - - self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese') - self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage') - self.assertEqual(cheese.with_context(lang='en_US').name, 'Delicious Cheese') - def test_107_duplicate_record_en(self): category = self.customers.with_context({'lang': 'en_US'}).copy() @@ -418,52 +339,93 @@ class TestTranslation(TransactionCase): category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value") - translation_fr = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', category.id), - ('lang', '=', 'fr_FR'), - ]) - self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source") + 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") + + # 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') - def test_01_en(self): langs = self.env['res.lang'].get_installed() - self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages") + self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, + "Test did not started with expected languages") - self.category.with_context(lang='en_US').write({'name': 'English Name'}) - name = self.category.with_context(lang=None).read(['name']) - self.assertEqual(name[0]['name'], "English Name", "Reference field not updated") - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ('lang', '=', 'en_US'), - ]) - self.assertEqual(len(translations), 0, "No English translation should be created when writing in English") + 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') - def test_02_en_translated(self): - langs = self.env['res.lang'].get_installed() - self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages") - translation = self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'lang': 'en_US', - 'res_id': self.category.id, - 'src': 'Reblochon', - 'value': 'Translated Name', - 'state': 'translated', - }) + category.with_context(lang='en_US').name = 'English 2' + self.assertEqual(category.with_context(lang='fr_FR').name, 'English 2') - self.category.with_context(lang='en_US').write({'name': 'English Name'}) - translation_value = translation.read(['value']) - self.assertEqual(translation_value[0]['value'], "English Name", "Existing translation was not updated") + 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') - source_name = self.category.with_context(lang=None).read(['name']) - self.assertEqual(source_name[0]['name'], "English Name", "Reference field not updated") + 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') @@ -474,14 +436,16 @@ class TestTranslationWrite(TransactionCase): 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'}) - source_name = self.category.with_context(lang=None).read(['name']) - self.assertEqual(source_name[0]['name'], "French Name", "Reference field not updated") - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ('lang', '=', 'fr_FR'), - ]) - self.assertEqual(len(translations), 0, "No French translation should be created when writing in French") + + 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') @@ -490,58 +454,31 @@ class TestTranslationWrite(TransactionCase): self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs, "Test did not started with expected languages") - category_en = self.category.with_context(lang='en_US') - category_fr = self.category.with_context(lang='fr_FR') + 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' + trans_load_data(self.env.cr, f, 'po', 'en_US', verbose=True, overwrite=True) - # no translation at first - self.assertEqual(category_fr.name, 'Reblochon') - self.assertFalse(self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang')) + self.category.with_context(lang='fr_FR').write({'name': 'French Name'}) + self.category.with_context(lang='en_US').write({'name': 'English Name'}) - # change source - self.category.write({'name': 'Blorb'}) - self.assertEqual(category_fr.name, 'Blorb') - self.assertFalse(self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang')) + # 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") - # change source - category_en.write({'name': 'Cheese'}) - self.assertEqual(category_fr.name, 'Cheese') - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang') - self.assertRecordValues(translations, [ - {'src': 'Cheese', 'value': 'Cheese', 'lang': 'en_US'}, - ]) + # force save to database and clear the cache: force a clean state + self.category.invalidate_recordset() - # add a translation - category_fr.write({'name': 'French Name'}) - self.assertEqual(category_en.name, 'Cheese') - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang') - self.assertRecordValues(translations, [ - {'src': 'Cheese', 'value': 'Cheese', 'lang': 'en_US'}, - {'src': 'Cheese', 'value': 'French Name', 'lang': 'fr_FR'}, - ]) - - # change source - category_en.write({'name': 'English Name'}) - self.assertEqual(category_fr.name, 'French Name') - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang') - self.assertRecordValues(translations, [ - {'src': 'English Name', 'value': 'English Name', 'lang': 'en_US'}, - {'src': 'English Name', 'value': 'French Name', 'lang': 'fr_FR'} - ]) + # 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') @@ -557,17 +494,18 @@ class TestTranslationWrite(TransactionCase): self.category.with_context(lang='es_ES').write({'name': 'Spanish Name'}) self.category.with_context(lang=None).write({'name': 'None Name'}) - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.partner.category,name'), - ('res_id', '=', self.category.id), - ], order='lang') - self.assertRecordValues(translations, [ - {'src': 'None Name', 'value': 'Spanish Name', 'lang': 'es_ES'}, - {'src': 'None Name', 'value': 'French Name', 'lang': 'fr_FR'}, - ]) + # 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") - def test_05_remove_multi_empty_string(self): - self._test_05_remove_multi("") + # 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) @@ -584,142 +522,100 @@ class TestTranslationWrite(TransactionCase): belgium.with_context(lang='en_US').write({'vat_label': 'VAT'}) belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA'}) - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.country,vat_label'), - ('res_id', '=', belgium.id), - ]) - self.assertEqual(len(translations), 2, "Translations are not created") - # remove the value belgium.with_context(lang='fr_FR').write({'vat_label': empty_value}) # should recover the initial value from db - self.assertFalse( + self.assertEqual( + empty_value, belgium.with_context(lang='fr_FR').vat_label, - "Value was not reset" + "Value should be the empty_value" ) - self.assertFalse( + self.assertEqual( + empty_value, belgium.with_context(lang='en_US').vat_label, - "Value was not reset in other languages" + "Value should be the empty_value" ) - self.assertFalse( + self.assertEqual( + empty_value, belgium.with_context(lang=None).vat_label, - "Value was not reset on the field model" + "Value should be the empty_value" ) - translations = self.env['ir.translation'].search([ - ('name', '=', 'res.country,vat_label'), - ('res_id', '=', belgium.id), - ]) - self.assertEqual(len(translations), 0, "Translations were not removed") - - # simulate remove the English translation in the interface - belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA'}) belgium.with_context(lang='en_US').write({'vat_label': 'VAT'}) - self.env['ir.translation'].translate_fields('res.country', belgium.id, 'vat_label') - en_translation = self.env['ir.translation'].search([ - ('name', '=', 'res.country,vat_label'), - ('res_id', '=', belgium.id), - ('lang', '=', 'en_US'), - ]) - en_translation.write({'value': ''}) + belgium.with_context(lang='fr_FR').write({'vat_label': 'TVA'}) - # should recover the initial value from db + # remove the value + belgium.with_context(lang='en_US').write({'vat_label': empty_value}) self.assertEqual( - "TVA", belgium.with_context(lang='fr_FR').vat_label, - "French translation was not kept" + empty_value, + belgium.with_context(lang='fr_FR').vat_label, + "Value should be the empty_value" ) self.assertEqual( - "VAT", belgium.with_context(lang='en_US').vat_label, - "Did not fallback to source when reset" + 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_orphan(self): - """ What happens with orphan translations. """ + def test_write_empty_and_value(self): self.env['res.lang']._activate_lang('fr_FR') + self.env['res.lang']._activate_lang('nl_NL') - # create a user with access rights on partner categories - user = new_test_user(self.env, 'deleter') - group = self.env.ref('base.group_partner_manager') - user.groups_id = [(4, group.id)] + 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") - # this access rule triggers a MissingError - self.env['ir.rule'].create({ - 'model_id': self.env['ir.model']._get_id('res.partner.category'), - 'groups': [(4, group.id)], - 'domain_force': "[('name', 'ilike', 'e')]", - }) + 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'}) - # create a translation, and delete the record from the database - translation = self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'lang': 'fr_FR', - 'res_id': self.category.id, - 'src': 'Reblochon', - 'value': 'Parfum Exquis', - 'state': 'translated', - }) - self.env.flush_all() - self.env.invalidate_all() - self.cr.execute("DELETE FROM res_partner_category WHERE id=%s", [self.category.id]) + belgium.invalidate_recordset() - # deleting the translation should be possible, provided the user has - # access rights on the translation's model - user0 = new_test_user(self.env, 'cannot modify category') - with self.assertRaises(AccessError): - translation.with_user(user0).unlink() + 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') - translation.with_user(user).unlink() + 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, '') - # however, creating orphan translations should not be possible - with self.assertRaises(ValidationError): - translation.with_user(user).create({ - 'type': 'model', - 'name': 'res.partner.category,name', - 'lang': 'fr_FR', - 'res_id': self.category.id, - 'src': 'Reblochon', - 'value': 'Parfum Exquis', - 'state': 'translated', - }) + def test_cresate_emtpy_false(self): + self._test_create_empty(False) - def test_write(self): - """ What happens with orphan translations. """ + # 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") - # create a user with access rights on partner categories - user = new_test_user(self.env, 'updater') - group = self.env.ref('base.group_system') - user.groups_id = [(4, group.id)] - action = user.env["ir.actions.act_window"].create({ - "name": "Dummy Action", - "res_model": "res.users", - "help": "

Cheese

", - }) + 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) - # create a translation, and delete the record from the database - translation = user.env['ir.translation'].create({ - 'type': 'model_terms', - 'name': 'ir.actions.act_window,help', - 'lang': 'fr_FR', - 'res_id': action.id, - 'src': 'Cheese', - 'value': 'Fromage', - 'state': 'translated', - }) - self.env.flush_all() - self.env.invalidate_all() + 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') - # deleting the translation should be possible, provided the user has - # access rights on the translation's model - user0 = new_test_user(self.env, 'cannot modify an action') - with self.assertRaises(AccessError): - translation.with_user(user0).unlink() - - translation.with_user(user).unlink() + 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_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']) @@ -742,14 +638,17 @@ class TestTranslationWrite(TransactionCase): # 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" - self.env['ir.translation'].create({ - 'type': 'model', - 'name': 'ir.model.fields,field_description', - 'lang': 'fr_FR', - 'res_id': ir_model_field.id, - 'src': 'Name', - 'value': LABEL, - }) + + 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' + trans_load_data(self.env.cr, f, 'po', 'fr_FR', verbose=True, overwrite=True) # check that fields_get() returns the expected label model = self.env['ir.model'].with_context(lang='fr_FR') @@ -775,23 +674,15 @@ class TestXMLTranslation(TransactionCase): 'model': 'res.partner', 'arch': archf % terms, }) - # DLE P70: `_sync_terms_translations`, which delete translations for which there is no value, is called sooner than before - # because it's called in `_write`, which is called by `flush`, which is called by the `search`. - # `arch_db` is in `_write` instead of `create` because `arch_db` is the inverse of `arch`. - # We need to flush `arch_db` before creating the translations otherwise the translation for which there is no value will be deleted, - # while the `test_sync_update` specifically needs empty translations - self.env.flush_all() + view.invalidate_recordset() + + val = {'en_US': archf % terms} for lang, trans_terms in kwargs.items(): - for src, val in zip(terms, trans_terms): - self.env['ir.translation'].create({ - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': lang, - 'res_id': view.id, - 'src': src, - 'value': val, - 'state': 'translated', - }) + 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): @@ -801,7 +692,7 @@ class TestXMLTranslation(TransactionCase): terms_fr = ('Couteau', 'Fourchette', 'Cuiller') view0 = self.create_view(archf, terms_en, fr_FR=terms_fr) - env_en = self.env(context={}) + env_en = self.env(context={'lang': 'en_US'}) env_fr = self.env(context={'lang': 'fr_FR'}) # check translated field @@ -865,17 +756,16 @@ class TestXMLTranslation(TransactionCase): view.with_env(env_fr).write({'arch_db': archf % new_terms_fr}) # check whether translations have been synchronized - self.assertEqual(view.with_env(env_nolang).arch_db, archf % new_terms_fr) 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 = '
%s
' - terms_en = ('Bread and cheese',) - terms_fr = ('Pain et fromage',) - terms_nl = ('Brood and kaas',) + archf = '
%s
%s
' + 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={}) @@ -883,57 +773,30 @@ class TestXMLTranslation(TransactionCase): env_fr = self.env(context={'lang': 'fr_FR'}) env_nl = self.env(context={'lang': 'nl_NL'}) - self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en) - self.assertEqual(view.with_env(env_en).arch, archf % terms_en) - self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr) - self.assertEqual(view.with_env(env_nl).arch, archf % terms_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',) - view.with_env(env_en).write({'arch': archf % terms_en}) + 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, archf % terms_en) - self.assertEqual(view.with_env(env_en).arch, archf % terms_en) - self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr) - self.assertEqual(view.with_env(env_nl).arch, archf % terms_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 (actual text change) - terms_en = ('Bread and butter',) - view.with_env(env_en).write({'arch': archf % terms_en}) + 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, archf % terms_en) - self.assertEqual(view.with_env(env_en).arch, archf % terms_en) - self.assertEqual(view.with_env(env_fr).arch, archf % terms_en) - self.assertEqual(view.with_env(env_nl).arch, archf % terms_en) - - def test_sync_update(self): - """ Check translations after major changes in source terms. """ - archf = '
%s
%s
' - terms_src = ('Subtotal', 'Subtotal:') - terms_en = ('', 'Sub total:') - view = self.create_view(archf, terms_src, en_US=terms_en) - - translations = self.env['ir.translation'].search([ - ('type', '=', 'model_terms'), - ('name', '=', "ir.ui.view,arch_db"), - ('res_id', '=', view.id), - ]) - self.assertEqual(len(translations), 2) - - # modifying the arch should sync existing translations without errors - new_arch = archf % ('Subtotal', 'Subtotal :
') - view.write({"arch_db": new_arch}) - - translations = self.env['ir.translation'].search([ - ('type', '=', 'model_terms'), - ('name', '=', "ir.ui.view,arch_db"), - ('res_id', '=', view.id), - ]) - # 'Subtotal' being src==value, it will be discared - # 'Subtotal:' will be discarded as it match 'Subtotal' instead of 'Subtotal :
' - self.assertEqual(len(translations), 0) + 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_cache_consistency(self): view = self.env["ir.ui.view"].create({ @@ -950,6 +813,35 @@ class TestXMLTranslation(TransactionCase): self.assertIn("", view.arch_db) self.assertIn("", view_fr.arch_db) + # TODO 1. add method to translate html/xml field. 2. add tests new translation method + def test_update_field_translations(self): + archf = '
%s
%s
' + 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, '
Bread and cheese
Fork2
') + self.assertEqual(view.with_context(lang='en_US').arch_db, '
Bread and cheese
Fork2
') + self.assertEqual(view.with_context(lang='fr_FR').arch_db, '
Pain et fromage
Fourchette2
') + self.assertEqual(view.with_context(lang='nl_NL').arch_db, view_nl) + + view.invalidate_recordset() + self.assertEqual(view.arch_db, '
Bread and cheese
Fork2
') + self.assertEqual(view.with_context(lang='en_US').arch_db, '
Bread and cheese
Fork2
') + self.assertEqual(view.with_context(lang='fr_FR').arch_db, '
Pain et fromage
Fourchette2
') + self.assertEqual(view.with_context(lang='nl_NL').arch_db, view_nl) @tagged('post_install', '-at_install') class TestLanguageInstallPerformance(TransactionCase): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index bd59399e898..5e07bd81754 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -9,6 +9,7 @@ from functools import partial from lxml import etree from lxml.builder import E from psycopg2 import IntegrityError +from psycopg2.extras import Json from odoo.exceptions import AccessError, ValidationError from odoo.tests import common @@ -295,26 +296,12 @@ class TestViewInheritance(ViewCase): self.env['res.lang']._activate_lang('fr_FR') v = self.makeView("T", arch='
Bar
') - self.env['ir.translation']._upsert_translations([{ - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': 'fr_FR', - 'res_id': v.id, - 'src': 'Foo', - 'value': 'Fou', - }, { - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': 'fr_FR', - 'res_id': v.id, - 'src': 'Bar', - 'value': 'Barre', - }]) + v.update_field_translations('arch_db', {'fr_FR': {'Foo': 'Fou', 'Bar': 'Barre'}}) self.assertEqual(v.arch, '
Bar
') # modify v to discard translations; this should not invalidate 'arch'! - v.arch = '
' - self.assertEqual(v.arch, '
') + v.arch = '
' + self.assertEqual(v.arch, '') def test_get_combined_arch_query_count(self): # If the query count increases, you probably made the view combination @@ -744,14 +731,7 @@ class TestNoModel(ViewCase): 'inherit_id': False, 'type': 'qweb', }) - self.env['ir.translation'].create({ - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'res_id': view.id, - 'lang': 'fr_FR', - 'src': TEXT_EN, - 'value': TEXT_FR, - }) + view.update_field_translations('arch_db', {'fr_FR': {TEXT_EN: TEXT_FR}}) view = view.with_context(lang='fr_FR') self.assertEqual(view.arch, ARCH % TEXT_FR) @@ -1516,6 +1496,9 @@ class TestViews(ViewCase): kw.pop('id', None) kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary') kw.setdefault('active', True) + if 'arch_db' in kw: + arch_db = kw['arch_db'] + kw['arch_db'] = Json({'en_US': arch_db}) if self.env.lang == 'en_US' else Json({'en_US': arch_db, self.env.lang: arch_db}) keys = sorted(kw) fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys) @@ -3216,7 +3199,7 @@ class TestViewTranslations(common.TransactionCase): super().setUpClass() cls.env['res.lang']._activate_lang('fr_FR') cls.env['res.lang']._activate_lang('nl_NL') - cls.env['ir.translation']._load_module_terms(['base'], ['fr_FR', 'nl_NL']) + cls.env['ir.module.module']._load_module_terms(['base'], ['fr_FR', 'nl_NL']) def create_view(self, archf, terms, **kwargs): view = self.env['ir.ui.view'].create({ @@ -3230,19 +3213,12 @@ class TestViewTranslations(common.TransactionCase): # We need to flush `arch_db` before creating the translations otherwise the translation for which there is no value will be deleted, # while the `test_sync_update` specifically needs empty translations self.env.flush_all() - self.env['ir.translation'].create([ - { - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': lang, - 'res_id': view.id, - 'src': src, - 'value': val, - 'state': 'translated', - } - for lang, trans_terms in kwargs.items() - for src, val in zip(terms, trans_terms) - ]) + 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]) + self.env.invalidate_all() return view def test_sync(self): @@ -3279,7 +3255,7 @@ class TestViewTranslations(common.TransactionCase): view.with_env(env_fr).write({'arch': archf % new_terms_fr}) # check whether translations have been synchronized - self.assertEqual(view.with_env(env_nolang).arch, archf % new_terms_fr) + self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en) self.assertEqual(view.with_env(env_en).arch, archf % terms_en) self.assertEqual(view.with_env(env_fr).arch, archf % new_terms_fr) self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl) @@ -3329,27 +3305,11 @@ class TestViewTranslations(common.TransactionCase): terms_en = ('', 'Sub total:') view = self.create_view(archf, terms_src, en_US=terms_en) - translations = self.env['ir.translation'].search([ - ('type', '=', 'model_terms'), - ('name', '=', "ir.ui.view,arch_db"), - ('res_id', '=', view.id), - ]) - self.assertEqual(len(translations), 2) - # modifying the arch should sync existing translations without errors new_arch = archf % ('Subtotal', 'Subtotal :
') view.write({"arch": new_arch}) self.assertEqual(view.arch, new_arch) - translations = self.env['ir.translation'].search([ - ('type', '=', 'model_terms'), - ('name', '=', "ir.ui.view,arch_db"), - ('res_id', '=', view.id), - ]) - # 'Subtotal' being src==value, it will be discared - # 'Subtotal:' will be discarded as it match 'Subtotal' instead of 'Subtotal :
' - self.assertEqual(len(translations), 0) - def test_cache_consistency(self): view = self.env["ir.ui.view"].create({ "name": "test_translate_xml_cache_invalidation", diff --git a/odoo/addons/base/views/ir_translation_views.xml b/odoo/addons/base/views/ir_translation_views.xml deleted file mode 100644 index 657b1247313..00000000000 --- a/odoo/addons/base/views/ir_translation_views.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - ir.translation - - - - - - - - - - - - - - - - ir.translation - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - ir.translation - - - - - - - - - - - - - - - ir.translation - - - - - - - - - - - - - ir.translation - - - - - - - - - - ir.translation - - - - - - - - - - - Translated Terms - ir.translation - - - - - -
diff --git a/odoo/addons/base/views/ir_ui_view_views.xml b/odoo/addons/base/views/ir_ui_view_views.xml index aa0a2003e7e..c989e819c07 100644 --- a/odoo/addons/base/views/ir_ui_view_views.xml +++ b/odoo/addons/base/views/ir_ui_view_views.xml @@ -28,8 +28,7 @@
-