[FIX] *: adapt code to jsonb translations

Website: the context install_filename='dummy' is used to prevent
arch_updated from becoming True while updating translations of
ir_ui_view.arch_db (if arch_update becomes True, test_inherit_specific
fails)

Fuzzy search for jsonb translated fields has been adapted in the case of
website.  It may require some refactoring later.
This commit is contained in:
Chong Wang (cwg)
2022-09-15 22:37:50 +02:00
parent 9e23f8ca7c
commit f8c2b02abe
38 changed files with 786 additions and 842 deletions
+18 -14
View File
@@ -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))
-1
View File
@@ -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
-59
View File
@@ -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
)
)
+10
View File
@@ -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
# ------------------------------------------------------------
+5 -26
View File
@@ -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
+7 -86
View File
@@ -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': '<p>SpanishBody for <t t-out="object.name" /></p>',
}]
test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}'
test_template.with_context(lang=lang_code).body_html = '<p>SpanishBody for <t t-out="object.name" /></p>'
# 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
+3 -16
View File
@@ -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',
+9 -42
View File
@@ -107,62 +107,29 @@ class TestMailTemplate(MailCommon):
'body_html': '<p>foo</p>',
})
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 = '<t t-esc="foo"/>'
employee_template.with_context(lang='fr_FR').body_html = '<t t-esc="foo"/>'
employee_translations_of_body.unlink() # delete old translation, to test the creation now
body_translation_vals['value'] = '<p t-esc="foo"/>'
# 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 = '<t t-esc="foo"/>'
# reset the body_html to static
employee_template.body_html = False
employee_template.body_html = '<p>foo</p>'
### 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) """
@@ -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(
@@ -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",
})
@@ -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()
@@ -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)
@@ -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 }}',
@@ -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({
+3 -14
View File
@@ -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)
+25 -8
View File
@@ -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):
"""
+18 -59
View File
@@ -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)
+4 -14
View File
@@ -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,
})
+4 -5
View File
@@ -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})
+6 -2
View File
@@ -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
@@ -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')
+3 -6
View File
@@ -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')
+10 -14
View File
@@ -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 = '<div>hello</div>'
self.base_view.update_field_translations('arch_db', {'fr_BE': {'hello': 'bonjour'}})
self.assertEqual(self.base_view.with_context(lang='fr_BE').arch, '<div>bonjour</div>')
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, '<div>hello</div>',
self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '<div>bonjour</div>',
"copy on write (COW) also copy existing translations")
translation.value = 'hi'
self.assertEqual(specific_view.with_context(lang='en_US').arch, '<div>hello</div>',
self.base_view.update_field_translations('arch_db', {'fr_BE': {'bonjour': 'salut'}})
self.assertEqual(self.base_view.with_context(lang='fr_BE').arch, '<div>salut</div>')
self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '<div>bonjour</div>',
"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, '<div>hi</div>',
self.assertEqual(specific_view.with_context(lang='fr_BE').arch, '<div>salut</div>',
"loading module translation copy translation from base to specific view")
def test_soc_complete_flow(self):
@@ -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']
@@ -7,7 +7,8 @@
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'view.xml'
'view.xml',
'data/test_translation_import_data.xml'
],
'installable': True,
'assets': {
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="test.translation.import.model1" id="test_translation_import_model1_record1">
<field name="name">Tableware</field>
<field name="xml" type="xml"><form string="Fork"><div>Knife</div><div>Spoon</div></form></field>
</record>
</odoo>
@@ -1,2 +1,5 @@
module,type,name,res_id,src,value,comments
test_translation_import,code,addons/test_translation_import/models.py,20,Accounting,samva,
test_translation_import,model,"test.translation.import.model1,name",test_translation_import.test_translation_import_model1_record1,Tableware,"Tableware, Dot"
test_translation_import,model_term,"test.translation.import.model1,html",test_translation_import.test_translation_import_model1_record1,Fork,"Fork, Dot"
test_translation_import,model_term,"test.translation.import.model1,html",test_translation_import.test_translation_import_model1_record1,Spoon,"Spoon, Dot"
test_translation_import,model_term,"test.translation.import.model1,html",test_translation_import.test_translation_import_model1_record1,Knife,"Kinfe, Dot"
1 module type name res_id src value comments
2 test_translation_import code model addons/test_translation_import/models.py test.translation.import.model1,name 20 test_translation_import.test_translation_import_model1_record1 Accounting Tableware samva Tableware, Dot
3 test_translation_import model_term test.translation.import.model1,html test_translation_import.test_translation_import_model1_record1 Fork Fork, Dot
4 test_translation_import model_term test.translation.import.model1,html test_translation_import.test_translation_import_model1_record1 Spoon Spoon, Dot
5 test_translation_import model_term test.translation.import.model1,html test_translation_import.test_translation_import_model1_record1 Knife Kinfe, Dot
+137 -48
View File
@@ -1,80 +1,169 @@
# This is a test PO file, not a true french translation.
# See the POT file for further information.
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * test_translation_import
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 6.1\n"
"Project-Id-Version: Odoo Server 15.5alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-10-17 12:36+0000\n"
"PO-Revision-Date: 2012-10-17 12:36+0000\n"
"Last-Translator: <>\n"
"POT-Creation-Date: 2022-07-01 12:46+0000\n"
"PO-Revision-Date: 2022-07-01 12:46+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"
# Note: there is normally an additional line:
# #: code:addons/test_translation_import/models.py:15
# This line is present in the POT and removed here to test the translation
# import behavior.
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__name
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB"
msgstr "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB in french"
msgid "Code Lazy, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__bar
msgid "Bar Import Type"
msgstr "Bar in french"
# Note: the line in pot is 21 and is 14 here
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:14
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Ijkl"
msgstr "Ijkl in french"
msgid "Code, %(num)s, %(symbol)s, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__foo
msgid "Foo Import Type"
msgstr "Foo in french"
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, %s, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name
msgid "Efgh"
msgstr "Efgh in french"
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, English"
msgstr "Code, Français"
#. module: test_translation_import
#: model:ir.model,name:test_translation_import.model_test_translation_import
msgid "Test: Translation Import"
msgstr "Test: Import de traductions"
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_uid
msgid "Created by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_date
msgid "Created on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__display_name
msgid "Display Name"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Fork"
msgstr "Fourchette"
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import_model1__name
msgid "Help, English"
msgstr "Aider, Français"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__id
msgid "ID"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Knife"
msgstr "Couteau"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1____last_update
msgid "Last Modified on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_uid
msgid "Last Updated by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_date
msgid "Last Updated on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__name
msgid "Name"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__selection
msgid "Selection"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__bar
msgid "Selection Bar"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__foo
msgid "Selection Foo"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Spoon"
msgstr "Cuillère"
#. module: test_translation_import
#: model:test.translation.import.model1,name:test_translation_import.test_translation_import_model1_record1
msgid "Tableware"
msgstr "Vaisselle"
#. module: test_translation_import
#: model:ir.actions.act_window,name:test_translation_import.action_test_translation_import_model1
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import_model1
msgid "Test translation import model1"
msgstr ""
#. module: test_translation_import
#: model:ir.actions.act_window,name:test_translation_import.action_test_translation_import
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import
msgid "Test translation import"
msgid "Test translation model1"
msgstr "Test translation import in french"
#. module: test_translation_import
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation
msgid "Test translation"
msgstr "Test translation in french"
#: model:ir.model,name:test_translation_import.model_test_translation_import_model1
msgid "Translation Test 1"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/arthur.py:42
#: code:addons/base/models/arthur.py:43
msgid "Test translation with two code lines"
msgstr "Test traduction avec deux lignes de code"
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__xml
msgid "XML"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/ford.py:43
msgid "Test translation with a code type but different line number in pot"
msgstr "Test traduction avec un type code mais différent numéro de ligne dans le pot"
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "do export"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__other_name
#: code:addons/base/models/marvin.py:42
#: code:addons/base/models/trillian.py:42
msgid "Test translation with two code type and model"
msgstr "Test traduction avec deux type code et un model"
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "slot"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "slot 2"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "text node"
msgstr ""
+153 -20
View File
@@ -1,12 +1,13 @@
# This is a test PO file, not a true french translation.
# See the POT file for further information.
# This is a test PO file, not a complete one. It is manually maintained
# to test the import translation behavior of Odoo.
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 6.1\n"
"Project-Id-Version: Odoo Server 15.5alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-10-17 12:36+0000\n"
"PO-Revision-Date: 2012-10-17 12:36+0000\n"
"Last-Translator: <>\n"
"POT-Creation-Date: 2022-07-01 12:46+0000\n"
"PO-Revision-Date: 2022-07-01 12:46+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -14,22 +15,154 @@ msgstr ""
"Plural-Forms: \n"
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:14
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Ijkl"
msgstr "Ijkl in belgian french"
#. module: test_translation_import
#: model:ir.model,name:test_translation_import.model_test_translation_import
msgid "Test: Translation Import"
msgstr "Test: Import de traductions in belgian french"
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name
msgid "Efgh"
msgid "Code Lazy, English"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/ford.py:43
msgid "Test translation with a code type but different line number in pot"
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, %(num)s, %(symbol)s, English"
msgstr "Code, %(num)s, %(symbol)s, Français, Belgium"
#. module: test_translation_import
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, %s, English"
msgstr "Code, %s, Français, Belgium"
#. module: test_translation_import
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, English"
msgstr "Code, Français, Belgium"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_uid
msgid "Created by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_date
msgid "Created on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__display_name
msgid "Display Name"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Fork"
msgstr "Fourchette, Belgium"
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import_model1__name
msgid "Help, English"
msgstr "Aider, Français, Belgium"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__id
msgid "ID"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Knife"
msgstr "Couteau, Belgium"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1____last_update
msgid "Last Modified on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_uid
msgid "Last Updated by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_date
msgid "Last Updated on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__name
msgid "Name"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__selection
msgid "Selection"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__bar
msgid "Selection Bar"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__foo
msgid "Selection Foo"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Spoon"
msgstr "Cuillère, Belgium"
#. module: test_translation_import
#: model:test.translation.import.model1,name:test_translation_import.test_translation_import_model1_record1
msgid "Tableware"
msgstr "Vaisselle, Belgium"
#. module: test_translation_import
#: model:ir.actions.act_window,name:test_translation_import.action_test_translation_import_model1
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import_model1
msgid "Test translation import model1"
msgstr ""
#. module: test_translation_import
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import
msgid "Test translation model1"
msgstr ""
#. module: test_translation_import
#: model:ir.model,name:test_translation_import.model_test_translation_import_model1
msgid "Translation Test 1"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__xml
msgid "XML"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "do export"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "slot"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "slot 2"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "text node"
msgstr ""
@@ -0,0 +1,50 @@
# This is a test PO file, not a complete one. It is manually maintained
# to test the import translation behavior of Odoo.
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.5alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-01 12:46+0000\n"
"PO-Revision-Date: 2022-07-01 12:46+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: test_translation_import
#: model:test.translation.import.model1,name:test_translation_import.test_translation_import_model1_record1
msgid "Tableware"
msgstr ""
#. module: test_translation_import
#: model_term:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Fork"
msgstr ""
#. module: test_translation_import
#: model_term:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Knife"
msgstr "Couteau, Canada"
#. module: test_translation_import
#: model_term:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Spoon"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import_model1__name
msgid "Help, English"
msgstr ""
#. module: test_translation_import
#: code:addons/test_translation_import/models/model.py:0
msgid "Code, English"
msgstr ""
#. module: test_translation_import
#: code:addons/test_translation_import/models/model.py:0
msgid "Code Lazy, English"
msgstr ""
@@ -1,21 +1,13 @@
# This is a test POT file, not a true template. It is manually maintained
# to test the import translation behavior of OpenERP.
#
# In particular, the
# `1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB` source is
# given with two targets (the #: comments): `code` and `field`. The code one is
# removed in the fr.po file. Still, the import should generate a database entry
# for the `code` one. I.e. the targets defined in the POT must be added to the
# targets defined in the PO file. This was done to fix a bug, as reported by
# lp:933496.
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 6.1\n"
"Project-Id-Version: Odoo Server 15.5alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-10-17 12:36+0000\n"
"PO-Revision-Date: 2012-10-17 12:36+0000\n"
"Last-Translator: <>\n"
"POT-Creation-Date: 2022-07-01 12:46+0000\n"
"PO-Revision-Date: 2022-07-01 12:46+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -23,113 +15,154 @@ msgstr ""
"Plural-Forms: \n"
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:15
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__name
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB"
msgid "Code Lazy, English"
msgstr ""
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:21
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Accounting"
msgid "Code, %(num)s, %(symbol)s, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__bar
msgid "Bar Import Type"
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, %s, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__create_uid
#: code:addons/test_translation_import/models/models.py:0
#, python-format
msgid "Code, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_uid
msgid "Created by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__create_date
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__create_date
msgid "Created on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__display_name
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__display_name
msgid "Display Name"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name
msgid "Efgh"
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Fork"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__foo
msgid "Foo Import Type"
#: model:ir.model.fields,help:test_translation_import.field_test_translation_import_model1__name
msgid "Help, English"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__id
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__id
msgid "ID"
msgstr ""
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:21
#, python-format
msgid "Ijkl"
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Knife"
msgstr ""
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:19
#, python-format
msgid "Klingon"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import____last_update
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1____last_update
msgid "Last Modified on"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__write_uid
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_uid
msgid "Last Updated by"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__write_date
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__write_date
msgid "Last Updated on"
msgstr ""
#. module: test_translation_import
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation
msgid "Test translation"
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__name
msgid "Name"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__selection
msgid "Selection"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__bar
msgid "Selection Bar"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import_model1__selection__foo
msgid "Selection Foo"
msgstr ""
#. module: test_translation_import
#: model_terms:test.translation.import.model1,xml:test_translation_import.test_translation_import_model1_record1
msgid "Spoon"
msgstr ""
#. module: test_translation_import
#: model:test.translation.import.model1,name:test_translation_import.test_translation_import_model1_record1
msgid "Tableware"
msgstr ""
#. module: test_translation_import
#: model:ir.actions.act_window,name:test_translation_import.action_test_translation_import_model1
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import_model1
msgid "Test translation import model1"
msgstr ""
#. module: test_translation_import
#: model:ir.actions.act_window,name:test_translation_import.action_test_translation_import
#: model:ir.ui.menu,name:test_translation_import.menu_test_translation_import
msgid "Test translation import"
msgid "Test translation model1"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/arthur.py:42
#: code:addons/base/models/arthur.py:43
#: model:ir.model,name:test_translation_import.model_test_translation_import_model1
msgid "Translation Test 1"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import_model1__xml
msgid "XML"
msgstr ""
#. module: test_translation_import
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "Test translation with two code lines"
msgid "do export"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/ford.py:42
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "Test translation with a code type but different line number in pot"
msgid "slot"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__other_name
#: code:addons/base/models/marvin.py:42
#: code:addons/base/models/trillian.py:42
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "Test translation with two code type and model"
msgid "slot 2"
msgstr ""
#. module: test_translation_import
#: model:ir.model,name:test_translation_import.model_test_translation_import
msgid "Test: Translation Import"
#. openerp-web
#: code:addons/test_translation_import/static/src/xml/js_templates.xml:0
#, python-format
msgid "text node"
msgstr ""
@@ -2,11 +2,11 @@
# to test the import translation behavior of Odoo.
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.4\n"
"Project-Id-Version: Odoo Server 15.5alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 13:20+0000\n"
"PO-Revision-Date: 2018-09-18 13:20+0000\n"
"Last-Translator: <>\n"
"POT-Creation-Date: 2022-07-01 12:46+0000\n"
"PO-Revision-Date: 2022-07-01 12:46+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -14,7 +14,16 @@ msgstr ""
"Plural-Forms: \n"
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:18
#, python-format
msgid "Klingon"
msgstr "tlhIngan"
#: model:test.translation.import.model1,name:test_translation_import.test_translation_import_model1_record1
msgid "Tableware"
msgstr "Tableware, Klingon"
#. module: test_translation_import
#: code:addons/test_translation_import/models/model.py:0
msgid "Code, English"
msgstr "Code, Klingon"
#. module: test_translation_import
#: code:addons/test_translation_import/models/model.py:0
msgid "Code Lazy, English"
msgstr "Code Lazy, Klingon"
@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, _
class m(models.TransientModel):
""" A model to provide source strings. """
_name = 'test.translation.import'
_description = 'Test: Translation Import'
name = fields.Char('1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB',
size=32, help='Efgh')
other_name = fields.Char('Test translation with two code type and model')
import_type = fields.Selection([
('foo', 'Foo Import Type'),
('bar', 'Bar Import Type'),
])
_('Ijkl')
# With the name label above, this source string should be generated twice.
_('1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB')
_('Klingon')
_('Accounting')
@@ -0,0 +1 @@
from . import models
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, _, _lt
from odoo.tools.translate import xml_translate
class TestTranslationImportModel1(models.Model):
_name = 'test.translation.import.model1'
_description = 'Translation Test 1'
name = fields.Char('Name', translate=True, help='Help, English')
selection = fields.Selection([
('foo', 'Selection Foo'),
('bar', 'Selection Bar'),
])
xml = fields.Text('XML', translate=xml_translate)
def get_code_translation(self):
return _('Code, English')
def get_code_lazy_translation(self):
return _lt('Code Lazy, English')
def get_code_placeholder_translation(self, *args, **kwargs):
return _('Code, %s, English', *args, **kwargs)
def get_code_named_placeholder_translation(self, *args, **kwargs):
return _('Code, %(num)s, %(symbol)s, English', *args, **kwargs)
@@ -1,2 +1,2 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_test_translation_import","access.test.translation.import","model_test_translation_import",,0,0,0,0
"access_test_translation_import_model1","access.test.translation.import.model1","model_test_translation_import_model1",,0,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_translation_import access_test_translation_import_model1 access.test.translation.import access.test.translation.import.model1 model_test_translation_import model_test_translation_import_model1 0 0 0 0
@@ -9,153 +9,96 @@ from odoo.tests import common, tagged
from odoo.tools.misc import file_open, mute_logger
from odoo.tools.translate import _, _lt, TranslationFileReader, TranslationModuleReader
from odoo import Command
from odoo.addons.base.models.ir_fields import BOOLEAN_TRANSLATIONS
TRANSLATED_TERM = _lt("Klingon")
class TestImport(common.TransactionCase):
class TestTermCount(common.TransactionCase):
def test_import_code_translation(self):
self.env['res.lang']._activate_lang('fr_FR')
def test_count_term(self):
"""
Just make sure we have as many translation entries as we wanted.
"""
# Tip: code translations don't need to be imported explicitly
# odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
model = self.env['test.translation.import.model1']
self.assertEqual(
model.with_context(lang='fr_FR').get_code_translation(),
'Code, Français'
)
def test_import_model_translation(self):
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB'),
], order='type')
self.assertEqual(len(translations), 2)
self.assertEqual(translations[0].type, 'code')
self.assertEqual(translations[0].module, 'test_translation_import')
self.assertEqual(translations[0].name, 'addons/test_translation_import/models.py')
self.assertEqual(translations[0].comments, '')
self.assertEqual(translations[0].res_id, 15)
self.assertEqual(translations[1].type, 'model')
self.assertEqual(translations[1].module, 'test_translation_import')
self.assertEqual(translations[1].name, 'ir.model.fields,field_description')
self.assertEqual(translations[1].comments, '')
field = self.env['ir.model.fields'].search([('model', '=', 'test.translation.import'), ('name', '=', 'name')])
self.assertEqual(translations[1].res_id, field.id)
def test_count_term_module(self):
"""
Just make sure we have as many translation entries as we wanted and module deducted from file content
"""
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
self.assertEqual(
record.with_context(lang='fr_FR').name,
'Vaisselle'
)
def test_import_model_term_translation(self):
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', 'Ijkl'),
('module', '=', 'test_translation_import'),
])
self.assertEqual(len(translations), 1)
self.assertEqual(translations.res_id, 21)
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
self.assertEqual(
record.with_context(lang='fr_FR').xml,
'<form string="Fourchette"><div>Couteau</div><div>Cuillère</div></form>'
)
def test_noupdate(self):
"""
Make sure no update do not overwrite translations
"""
menu = self.env.ref('test_translation_import.menu_test_translation_import')
menu.name = "New Name"
self.assertEqual(menu.name, 'Test translation model1')
# install french and change translation content
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
self.assertEqual(menu.with_context(lang='fr_FR').name, "Test translation import in french")
menu.with_context(lang='fr_FR').name = "Nouveau nom"
# reload with overwrite
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False, overwrite=True)
# trans_load invalidates ormcache but not record cache
self.env.invalidate_all()
self.assertEqual(menu.name, "New Name")
self.assertEqual(menu.name, "Test translation model1")
self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom")
def test_lang_with_base(self):
self.env['res.lang']._activate_lang('fr_BE')
self.env['res.lang']._activate_lang('fr_CA')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_BE', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr_BE.po', 'fr_BE', verbose=False, overwrite=True)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_CA', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr_CA.po', 'fr_CA', verbose=False, overwrite=True)
# language override base language
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_BE'),
('value', '=like', '% belgian french'),
])
self.assertEqual(len(translations), 2)
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
self.assertEqual(
record.with_context(lang='fr_BE').get_code_translation(),
'Code, Français, Belgium'
)
self.assertEqual(
record.with_context(lang='fr_BE').name,
'Vaisselle, Belgium'
)
self.assertEqual(
record.with_context(lang='fr_BE').xml,
'<form string="Fourchette, Belgium"><div>Couteau, Belgium</div><div>Cuillère, Belgium</div></form>'
)
# not specified localized language fallback on base language
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_BE'),
('src', '=', 'Efgh'),
('value', '=', 'Efgh in french'),
])
self.assertEqual(len(translations), 1)
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_BE'),
('src', '=', 'Test translation with a code type but different line number in pot'),
('value', '=', 'Test traduction avec un type code mais différent numéro de ligne dans le pot'),
])
self.assertEqual(len(translations), 1)
def test_no_duplicate(self):
"""
Just make sure we do not create duplicated translation with 'code' type
"""
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
ids = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', 'Test translation with two code lines'),
])
self.assertEqual(len(ids), 1)
ids = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', 'Test translation with a code type but different line number in pot'),
])
self.assertEqual(len(ids), 1)
ids = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', 'Test translation with two code type and model'),
])
self.assertEqual(len(ids), 2)
self.assertEqual(len(ids.filtered(lambda t: t.type == 'code')), 1)
def test_export_empty_string(self):
"""When the string and the translation is equal the translation is empty"""
# Export the translations
def update_translations(create_empty_translation=False):
self.env['res.lang']._activate_lang('fr_FR')
with closing(io.BytesIO()) as bufferobj:
odoo.tools.trans_export('fr_FR', ['test_translation_import'], bufferobj, 'po', self.cr)
bufferobj.name = 'test_translation_import/i18n/fr.po'
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR',
verbose=False,
create_empty_translation=create_empty_translation,
overwrite=True)
# Check that the not translated key is not created
update_translations()
translation = self.env['ir.translation'].search_count([('src', '=', 'Efgh'), ('value', '=', '')])
self.assertFalse(translation, 'An empty translation is not imported')
# Check that "Generate Missing Terms" create empty string for not translated key
update_translations(create_empty_translation=True)
translation = self.env['ir.translation'].search_count([('src', '=', 'Efgh'), ('value', '=', '')])
self.assertTrue(translation, 'The translation of "Efgh" should be empty')
# Modify the value translated for the equal value of the key
menu = self.env.ref('test_translation_import.menu_test_translation_import')
menu.name = "New Name"
menu.with_context(lang='fr_FR').name = "New Name"
update_translations()
self.assertEqual(menu.with_context(lang='fr_FR').name, "New Name", 'The translation of "New Name" should be "New Name"')
# Modify the value translated for another different value
menu.name = "New Name"
menu.with_context(lang='fr_FR').name = "Nouveau nom"
update_translations()
self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom", 'The translation of "New Name" should be "Nouveau nom"')
self.assertEqual(
record.with_context(lang='fr_CA').get_code_translation(),
'Code, Français'
)
self.assertEqual(
record.with_context(lang='fr_CA').name,
'Vaisselle'
)
self.assertEqual(
record.with_context(lang='fr_CA').xml,
'<form string="Fourchette"><div>Couteau, Canada</div><div>Cuillère</div></form>'
)
def test_import_from_po_file(self):
"""Test the import from a single po file works"""
@@ -174,11 +117,15 @@ class TestTermCount(common.TransactionCase):
tlh_lang = self.env['res.lang']._lang_get('tlh')
self.assertTrue(tlh_lang, "The imported language was not creates")
trans_count = self.env['ir.translation'].search_count([('lang', '=', 'tlh')])
self.assertEqual(trans_count, 1, "The imported translations were not created")
self.env = self.env(context=dict(self.env.context, lang="tlh"))
self.assertEqual(_("Klingon"), "tlhIngan", "The code translation was not applied")
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
self.assertEqual(
record.with_context(lang='tlh').get_code_translation(),
'Code, Klingon'
)
self.assertEqual(
record.with_context(lang='tlh').name,
'Tableware, Klingon'
)
def test_lazy_translation(self):
"""Test the import from a single po file works"""
@@ -194,21 +141,34 @@ class TestTermCount(common.TransactionCase):
with mute_logger('odoo.addons.base.models.res_lang'):
import_tlh.import_lang()
context = {'lang': "tlh"}
self.assertEqual(_("Klingon"), "tlhIngan", "The direct code translation was not applied")
model = self.env['test.translation.import.model1']
TRANSLATED_TERM = model.get_code_lazy_translation()
self.assertEqual(
model.with_context(lang='tlh').get_code_translation(),
"Code, Klingon",
"The direct code translation was not applied"
)
context = None
# Comparison of lazy strings must be explicitely casted to string
with self.assertRaises(NotImplementedError):
TRANSLATED_TERM == "Klingon"
self.assertEqual(str(TRANSLATED_TERM), "Klingon", "The translation should not be applied yet")
TRANSLATED_TERM == "Code, English"
self.assertEqual(str(TRANSLATED_TERM), "Code Lazy, English", "The translation should not be applied yet")
context = {'lang': "tlh"}
self.assertEqual(str(TRANSLATED_TERM), "tlhIngan", "The lazy code translation was not applied")
self.assertEqual(str(TRANSLATED_TERM), "Code Lazy, Klingon", "The lazy code translation was not applied")
self.assertEqual("Do you speak " + TRANSLATED_TERM, "Do you speak tlhIngan", "str + _lt concatenation failed")
self.assertEqual(TRANSLATED_TERM + ", I speak it", "tlhIngan, I speak it", "_lt + str concatenation failed")
self.assertEqual(TRANSLATED_TERM + TRANSLATED_TERM, "tlhIngantlhIngan", "_lt + _lt concatenation failed")
self.assertEqual("Do you speak " + TRANSLATED_TERM, "Do you speak Code Lazy, Klingon", "str + _lt concatenation failed")
self.assertEqual(TRANSLATED_TERM + ", I speak it", "Code Lazy, Klingon, I speak it", "_lt + str concatenation failed")
self.assertEqual(TRANSLATED_TERM + TRANSLATED_TERM, "Code Lazy, KlingonCode Lazy, Klingon", "_lt + _lt concatenation failed")
# test lazy translation in another module
self.env['res.lang']._activate_lang('fr_FR')
context = {'lang': 'en_US'}
self.assertEqual(str(BOOLEAN_TRANSLATIONS[0]), 'yes')
context = {'lang': 'fr_FR'}
self.assertEqual(str(BOOLEAN_TRANSLATIONS[0]), 'oui')
def test_import_from_csv_file(self):
"""Test the import from a single CSV file works"""
@@ -227,118 +187,44 @@ class TestTermCount(common.TransactionCase):
dot_lang = self.env['res.lang']._lang_get('dot')
self.assertTrue(dot_lang, "The imported language was not creates")
trans_count = self.env['ir.translation'].search_count([('lang', '=', 'dot')])
self.assertEqual(trans_count, 1, "The imported translations were not created")
self.env = self.env(context=dict(self.env.context, lang="dot"))
self.assertEqual(_("Accounting"), "samva", "The code translation was not applied")
def test_export_pollution(self):
""" Test that exporting the translation only exports the translations of the module """
with file_open('test_translation_import/i18n/dot.csv', 'rb') as f:
csv_file = base64.b64encode(f.read())
# dot.csv only contains one term
import_tlh = self.env["base.language.import"].create({
'name': 'Dothraki',
'code': 'dot',
'data': csv_file,
'filename': 'dot.csv',
})
with mute_logger('odoo.addons.base.models.res_lang'):
import_tlh.import_lang()
# create a translation that has the same src as an existing field but no module
# information and a different res_id that the real field
# this translation should not be included in the export
self.env['ir.translation'].create({
'src': '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB',
'value': '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB in Dothraki',
'type': 'model',
'name': 'ir.model.fields,field_description',
'res_id': -1,
'lang': 'dot',
})
module = self.env.ref('base.module_test_translation_import')
export = self.env["base.language.export"].create({
'lang': 'dot',
'format': 'po',
'modules': [Command.set([module.id])]
})
export.act_getfile()
po_file = export.data
reader = TranslationFileReader(base64.b64decode(po_file).decode(), fileformat='po')
for row in reader:
if row['value']:
# should contains only one row from the csv, not the manual one
self.assertEqual(row['src'], "Accounting")
self.assertEqual(row['value'], "samva")
# code translation cannot be changed or imported, it only depends on the po file in the module directory
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
self.assertEqual(
record.with_context(lang='dot').get_code_translation(),
'Code, English'
)
self.assertEqual(
record.with_context(lang='dot').name,
'Tableware, Dot'
)
def test_translation_placeholder(self):
"""Verify placeholder use in _()"""
context = {'lang': "fr_BE"}
self.env.ref("base.lang_fr_BE").active = True
self.env['res.lang']._activate_lang('fr_BE')
# translation with positional placeholders
translation = self.env['ir.translation'].create({
'src': 'Text with %s placeholder',
'value': 'Text avec %s marqueur',
'type': 'code',
'name': 'addons/test_translation_import/tests/test_count_term.py',
'res_id': 0,
'lang': 'fr_BE',
})
model_fr_BE = self.env['test.translation.import.model1'].with_context(lang='fr_BE')
# correctly translate
self.assertEqual(
_("Text with %s placeholder", 1),
"Text avec 1 marqueur",
model_fr_BE.get_code_placeholder_translation(1),
"Code, 1, Français, Belgium",
"Translation placeholders were not applied"
)
# source error: wrong arguments
with self.assertRaises(TypeError), self.cr.savepoint():
_("Text with %s placeholder", 1, "🧀")
# translation error: log error and fallback on source
translation.value = "Text avec s% marqueur"
with self.assertLogs('odoo.tools.translate', 'ERROR'):
self.assertEqual(
_("Text with %s placeholder", 1),
"Text with 1 placeholder",
"Fallback to source was not used for bad translation"
)
# translation with named placeholders
translation = self.env['ir.translation'].create({
'src': 'Text with %(num)s placeholders %(symbol)s',
'value': 'Text avec %(num)s marqueurs %(symbol)s',
'type': 'code',
'name': 'addons/test_translation_import/tests/test_count_term.py',
'res_id': 0,
'lang': 'fr_BE',
})
with self.assertRaises(TypeError):
model_fr_BE.get_code_placeholder_translation(1, "🧀")
# correctly translate
self.assertEqual(
_("Text with %(num)s placeholders %(symbol)s", num=2, symbol="🧀"),
"Text avec 2 marqueurs 🧀",
model_fr_BE.get_code_named_placeholder_translation(num=2, symbol="🧀"),
"Code, 2, 🧀, Français, Belgium",
"Translation placeholders were not applied"
)
# source error: wrong arguments
with self.assertRaises(KeyError), self.cr.savepoint():
_("Text with %(num)s placeholders %(symbol)s", symbol="🧀")
# translation error: log error and fallback on source
translation.value = "Text avec %(num)s marqueurs %(symbole)s"
with self.assertLogs('odoo.tools.translate', 'ERROR'):
self.assertEqual(
_("Text with %(num)s placeholders %(symbol)s", num=2, symbol="🧀"),
"Text with 2 placeholders 🧀",
"Fallback to source was not used for bad translation"
)
with self.assertRaises(KeyError):
model_fr_BE.get_code_named_placeholder_translation(symbol="🧀"),
@tagged('post_install', '-at_install')
@@ -347,23 +233,10 @@ class TestTranslationFlow(common.TransactionCase):
def test_export_import(self):
""" Ensure export+import gives the same result as loading a language """
# load language and generate missing terms to create missing empty terms
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.update.translations"].create({'lang': 'fr_FR'}).act_update()
translations = self.env["ir.translation"].search([
('lang', '=', 'fr_FR'),
('module', '=', 'test_translation_import'),
('value', '!=', ''),
])
# minus 3 as the original fr.po contains 3 fake code translations (cf
# test_no_duplicate test) which are not found by babel_extract_terms
init_translation_count = len(translations) - 3
self.env["base.language.install"].create({
'overwrite': True,
'lang_ids': [(6, 0, [self.env.ref('base.lang_fr').id])],
}).lang_install()
module = self.env.ref('base.module_test_translation_import')
export = self.env["base.language.export"].create({
@@ -375,7 +248,30 @@ class TestTranslationFlow(common.TransactionCase):
po_file = export.data
self.assertIsNotNone(po_file)
translations.unlink()
record = self.env.ref('test_translation_import.test_translation_import_model1_record1')
record.invalidate_recordset()
self.assertEqual(
record.with_context(lang='fr_FR').name,
'Vaisselle'
)
self.assertEqual(
record.with_context(lang='fr_FR').xml,
'<form string="Fourchette"><div>Couteau</div><div>Cuillère</div></form>'
)
# remove All translations
record.name = False
record.name = 'Tableware'
record.xml = False
record.xml = '<form string="Fork"><div>Knife</div><div>Spoon</div></form>'
self.assertEqual(
record.with_context(lang='fr_FR').name,
'Tableware'
)
self.assertEqual(
record.with_context(lang='fr_FR').xml,
'<form string="Fork"><div>Knife</div><div>Spoon</div></form>'
)
import_fr = self.env["base.language.import"].create({
'name': 'French',
@@ -387,13 +283,14 @@ class TestTranslationFlow(common.TransactionCase):
with mute_logger('odoo.addons.base.models.res_lang'):
import_fr.import_lang()
import_translation = self.env["ir.translation"].search([
('lang', '=', 'fr_FR'),
('module', '=', 'test_translation_import'),
('value', '!=', ''),
])
self.assertEqual(init_translation_count, len(import_translation))
self.assertEqual(
record.with_context(lang='fr_FR').name,
'Vaisselle'
)
self.assertEqual(
record.with_context(lang='fr_FR').xml,
'<form string="Fourchette"><div>Couteau</div><div>Cuillère</div></form>'
)
def test_export_import_csv(self):
""" Ensure can reimport exported csv """
@@ -409,11 +306,6 @@ class TestTranslationFlow(common.TransactionCase):
po_file = export.data
self.assertIsNotNone(po_file)
self.env["ir.translation"].search([
('lang', '=', 'fr_FR'),
('module', '=', 'test_translation_import')
]).unlink()
import_fr = self.env["base.language.import"].create({
'name': 'French',
'code': 'fr_FR',
@@ -422,7 +314,7 @@ class TestTranslationFlow(common.TransactionCase):
'overwrite': False,
})
with mute_logger('odoo.addons.base.models.res_lang'):
import_fr.with_context(create_empty_translation=True).import_lang()
import_fr.with_context().import_lang()
def test_export_static_templates(self):
trans_static = []
+8 -8
View File
@@ -2,19 +2,19 @@
<odoo>
<data noupdate="1">
<record id="action_test_translation_import" model="ir.actions.act_window">
<field name="name">Test translation import</field>
<record id="action_test_translation_import_model1" model="ir.actions.act_window">
<field name="name">Test translation import model1</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">test.translation.import</field>
<field name="res_model">test.translation.import.model1</field>
<field name="view_mode">tree,form</field>
<field name="target">current</field>
</record>
<menuitem id="menu_test_translation" name="Test translation" sequence="650"/>
<menuitem id="menu_test_translation_import" name="Test translation model1" sequence="650"/>
<menuitem id="menu_test_translation_import"
name="Test translation import"
action="action_test_translation_import"
parent="menu_test_translation"/>
<menuitem id="menu_test_translation_import_model1"
name="Test translation import model1"
action="action_test_translation_import_model1"
parent="menu_test_translation_import"/>
</data>
</odoo>