[IMP] core: support delay translation

Jsonb data structure of model_terms translated fields' columns
'{
	"en_US": "<div>Apple</div>"
	"fr_FR": "<div>Pomme</div><div>Banane</div>"
	"_fr_FR": "<div>Pomme</div>"
}'::jsonb

"column" IS NULL OR "column"->>'en_US' IS NOT NULL

"column"->>'lang'
1. stores last confirmed value
2. logically fallbacks to
    COALESCE(
        "column"->>'lang',
        "column"->>'en_US'
    )

"column"->>'_lang'
1. stores translations and the last written html/xml structure
2. shares the same html/xml structure with other "column"->>'_lang'
3. logically fallbacks to
    COALESCE(
        "column"->>'_lang',
        "column"->>'lang',
        "column"->>'_en_US',
        "column"->>'en_US'
    )

Context:
1. `delay_translations`(new) only write values to _langs while keeping
translations for langs when the written value has at least one translatable term
2. `check_translations`(new) read _langs values to create translation mapping
for the translation dialog
3. `edit_translations` read values whose translatable terms are wrapped by
`<span></span>` with term information for the TRANSLATE mode of website

Potential issue
since the record value of a model terms translated field is logical content
dependent (check_translations, edit_translations), all computed field computed
from any model terms translated field should also be marked.
@api.depends_context('lang', 'edit_translations', 'check_translations')

Data flow for column value, cache value and record value
(make sure the window is wide enough to see the graph)

                             +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+
                             ' Record(str):                                          '
                             '                                                       '
                             ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
                             ' | "<div><span data-oe-model='model'                 | '               {            read fr_FR             {
                             ' | data-oe-id='id' ...>French</span></div>"          | ' <------------ } context.get('edit_translations')  } <+
                             ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+  |
                             ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+  |
                             ' | "<div>French</div>"                               | '               {            read fr_FR             {  |
                             ' |                                                   | ' <------------ } context.get('check_translations') } <+
                             ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+  |
+~~~~~~~~~~~~+               ' +---------------------------------------------------+ '                                                      |
{ read fr_FR { ------------> ' | "<a>French</a>"                                   | '                                                      |
+~~~~~~~~~~~~+               ' +---------------------------------------------------+ '                                                      |
  ^                          '                                                       '                                                      |
  |                          +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+                                                      |
  |                                                                                                                                         |
  |                                                                                                                                         |
  |                                                                                                                                         |
  |                          +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+                                                      |
  |                          ' Cache(dict):                                          '                                                      |
  |                          '                                                       '                                                      |
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' |                                                   | ' -----------------------------------------------------+
  |                          ' | "_fr_FR": "<div>French</div>"                     | '
  |                          ' |                                                   | ' <----------------------------------------------------+
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
  +------------------------- ' |                                                   | '               {              fetch fr_FR               {
                             ' | "fr_FR": "<a>French</a>"                          | '               }    context.get('edit_translations')    }
  +------------------------> ' |                                                   | '               {  or context.get('check_translations')  {
  |                          ' +---------------------------------------------------+ '               +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
+~~~~~~~~~~~~~~~~~+          '                                                       '                                                      ^
{   fetch fr_FR   {          +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+                              COALESCE(               |
+~~~~~~~~~~~~~~~~~+                                                                                                     "c"->>'_fr_FR',     |
  ^                                                                                                                     "c"->>'fr_FR',      |
  | COALESCE(                                                                                                           "c"->>'_en_US',     |
  |     "c"->>'fr_FR',       +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+                                  "c"->>'en_US'       |
  |     "c"->>'en_US'        ' Database(jsonb):                                      '                              )                       |
  | )                        '                                                       '                                                      |
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' | "_fr_FR": "<div>French</div>"                     | ' -----------------------------------------------------+
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' +---------------------------------------------------+ '                                                      |
  +------------------------- ' | "fr_FR": "<a>French</a>"                          | ' -----------------------------------------------------+
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' | "_en_US": "<div>English</div>"                    | ' -----------------------------------------------------+
  |                          ' +---------------------------------------------------+ '                                                      |
  |                          ' +---------------------------------------------------+ '                                                      |
  +------------------------- ' | "en_US": "<div>English1</div><div>English2</div>" | ' -----------------------------------------------------+
                             ' +---------------------------------------------------+ '
                             '                                                       '
                             +- - - - - - - - - - - - - - - - - - - - - - - - - - - -+

Task: 3043340
Part-of: odoo/odoo#139819
This commit is contained in:
Chong Wang (cwg)
2023-10-27 11:35:03 +00:00
parent 43c6b5e7a8
commit 2d08f97c07
15 changed files with 342 additions and 86 deletions
@@ -2544,13 +2544,19 @@ export class Wysiwyg extends Component {
// iframe or not. // iframe or not.
// If the element is given, then search within not from the document. // If the element is given, then search within not from the document.
const $ = element ? getJqueryFromDocument(element) : getJqueryFromDocument(this.$editable[0].ownerDocument); const $ = element ? getJqueryFromDocument(element) : getJqueryFromDocument(this.$editable[0].ownerDocument);
const $allBlocks = $((this.options || {}).savableSelector).filter('.o_dirty'); const $allBlocks = $((this.options || {}).savableSelector).filter(
this.options.enableTranslation
? '.o_dirty, .o_delay_translation'
: '.o_dirty');
const $dirty = $('.o_dirty'); const $dirty = $('.o_dirty');
$dirty $dirty
.removeAttr('contentEditable') .removeAttr('contentEditable')
.removeClass('o_dirty oe_carlos_danger o_is_inline_editable'); .removeClass('o_dirty oe_carlos_danger o_is_inline_editable');
const $delay_translation = $('.o_delay_translation');
$delay_translation.removeClass('o_delay_translation');
$('.o_editable') $('.o_editable')
.removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed'); .removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed');
@@ -2678,7 +2684,10 @@ export class Wysiwyg extends Component {
escapedHtml, escapedHtml,
!$el.data('oe-expression') && $el.data('oe-xpath') || null !$el.data('oe-expression') && $el.data('oe-xpath') || null
], { ], {
context, context: {
...context,
delay_translations: true,
},
}); });
return result; return result;
} }
+17 -12
View File
@@ -199,21 +199,26 @@ class IrModuleModule(models.Model):
if dst_mname != new_rec._name: if dst_mname != new_rec._name:
continue continue
old_field = old_rec._fields[src_fname] old_field = old_rec._fields[src_fname]
old_translations = { old_stored_translations = old_field._get_stored_translations(old_rec)
lang: value if not old_stored_translations:
for lang, value in old_field._get_stored_translations(old_rec).items()
if lang in valid_langs
}
if not old_translations:
continue continue
if not callable(old_field.translate): if old_field.translate is True:
if old_rec[src_fname] == new_rec[dst_fname]: if old_rec[src_fname] != new_rec[dst_fname]:
new_rec.update_field_translations(dst_fname, old_translations) continue
new_rec.update_field_translations(dst_fname, {
k: v for k, v in old_stored_translations.items() if k in valid_langs and k != cur_lang
})
else: else:
old_translation_lang = old_translations.get(cur_lang) or old_translations.get('en_US') old_translations = {
k: old_stored_translations.get(f'_{k}', v)
for k, v in old_stored_translations.items()
if k in valid_langs
}
# {from_lang_term: {lang: to_lang_term} # {from_lang_term: {lang: to_lang_term}
translation_dictionary = old_field.get_translation_dictionary(old_translation_lang, { translation_dictionary = old_field.get_translation_dictionary(
lang: value for lang, value in old_translations.items() if lang != cur_lang}) old_translations.pop(cur_lang, old_translations['en_US']),
old_translations
)
# {lang: {old_term: new_term} # {lang: {old_term: new_term}
translations = defaultdict(dict) translations = defaultdict(dict)
for from_lang_term, to_lang_terms in translation_dictionary.items(): for from_lang_term, to_lang_terms in translation_dictionary.items():
@@ -995,7 +995,7 @@ export class WysiwygAdapterComponent extends Wysiwyg {
}); });
}; };
} }
if (this._isDirty()) { if (this._isDirty() || this.options.enableTranslation) {
return this.save().then(callback, event.data.onFailure); return this.save().then(callback, event.data.onFailure);
} else { } else {
return callback(); return callback();
+1 -1
View File
@@ -214,7 +214,7 @@ actual arch.
model_id = fields.Many2one("ir.model", string="Model of the view", compute='_compute_model_id', inverse='_inverse_compute_model_id') model_id = fields.Many2one("ir.model", string="Model of the view", compute='_compute_model_id', inverse='_inverse_compute_model_id')
@api.depends('arch_db', 'arch_fs', 'arch_updated') @api.depends('arch_db', 'arch_fs', 'arch_updated')
@api.depends_context('read_arch_from_file', 'lang', 'edit_translations') @api.depends_context('read_arch_from_file', 'lang', 'edit_translations', 'check_translations')
def _compute_arch(self): def _compute_arch(self):
def resolve_external_ids(arch_fs, view_xml_id): def resolve_external_ids(arch_fs, view_xml_id):
def replacer(m): def replacer(m):
+6 -6
View File
@@ -1280,8 +1280,8 @@ class TestQueries(TransactionCase):
with self.assertQueries([''' with self.assertQueries(['''
SELECT "res_partner_title"."id" SELECT "res_partner_title"."id"
FROM "res_partner_title" FROM "res_partner_title"
WHERE (COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>'en_US') like %s) WHERE (COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>%s) like %s)
ORDER BY COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>'en_US') ORDER BY COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>%s)
''']): ''']):
Model.search([('name', 'like', 'foo')]) Model.search([('name', 'like', 'foo')])
@@ -1325,10 +1325,10 @@ class TestQueries(TransactionCase):
self.assertEqual(Model._rec_names_search, ['name', 'model']) self.assertEqual(Model._rec_names_search, ['name', 'model'])
with self.assertQueries([''' with self.assertQueries(['''
SELECT "ir_model"."id", "ir_model"."name"->>'en_US' SELECT "ir_model"."id", "ir_model"."name"->>%s
FROM "ir_model" FROM "ir_model"
WHERE ( WHERE (
("ir_model"."name"->>'en_US' ILIKE %s) ("ir_model"."name"->>%s ILIKE %s)
OR ("ir_model"."model"::text ILIKE %s) OR ("ir_model"."model"::text ILIKE %s)
) )
ORDER BY "ir_model"."model" ORDER BY "ir_model"."model"
@@ -1337,10 +1337,10 @@ class TestQueries(TransactionCase):
Model.name_search('foo') Model.name_search('foo')
with self.assertQueries([''' with self.assertQueries(['''
SELECT "ir_model"."id", "ir_model"."name"->>'en_US' SELECT "ir_model"."id", "ir_model"."name"->>%s
FROM "ir_model" FROM "ir_model"
WHERE ( WHERE (
("ir_model"."name" is NULL OR "ir_model"."name"->>'en_US' not ilike %s) ("ir_model"."name" is NULL OR "ir_model"."name"->>%s not ilike %s)
AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL) AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL)
) )
ORDER BY "ir_model"."model" ORDER BY "ir_model"."model"
+134
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from hashlib import sha256
from unittest.mock import patch from unittest.mock import patch
import logging import logging
import time import time
@@ -1158,6 +1159,117 @@ class TestXMLTranslation(TransactionCase):
self.assertEqual(view.with_context(lang='en_US').arch_db, '<form string="X">Bread and cheese<div>Fork3</div></form>') self.assertEqual(view.with_context(lang='en_US').arch_db, '<form string="X">Bread and cheese<div>Fork3</div></form>')
self.assertEqual(view.with_context(lang='es_ES').arch_db, '<form string="X">Bread and cheese<div>Tenedor3</div></form>') self.assertEqual(view.with_context(lang='es_ES').arch_db, '<form string="X">Bread and cheese<div>Tenedor3</div></form>')
def test_delay_translations(self):
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = ('Couteau', 'Fourchette', 'Cuiller')
view0 = self.create_view(archf, terms_en, fr_FR=terms_fr)
archf2 = '<form string="%s"><p>%s</p><div>%s</div></form>'
terms_en2 = ('new Knife', 'Fork', 'Spoon')
# write en_US with delay_translations
view0.with_context(lang='en_US', delay_translations=True).arch_db = archf2 % terms_en2
view0.invalidate_recordset()
self.assertEqual(
view0.with_context(lang='en_US').arch_db,
archf2 % terms_en2,
'en_US value should be the latest one since it is updated directly'
)
self.assertEqual(view0.with_context(lang='en_US', check_translations=True).arch_db, archf2 % terms_en2)
self.assertEqual(
view0.with_context(lang='fr_FR').arch_db,
archf % terms_fr,
"fr_FR value should keep the same since its translations hasn't been confirmed"
)
self.assertEqual(
view0.with_context(lang='fr_FR', edit_translations=True).arch_db,
'<form string="'
'&lt;span '
'class=&quot;o_delay_translation&quot; '
'data-oe-model=&quot;ir.ui.view&quot; '
f'data-oe-id=&quot;{view0.id}&quot; '
'data-oe-field=&quot;arch_db&quot; '
'data-oe-translation-state=&quot;to_translate&quot; '
f'data-oe-translation-initial-sha=&quot;{sha256(terms_en2[0].encode()).hexdigest()}&quot;'
'&gt;'
f'{terms_en2[0]}'
'&lt;/span&gt;"'
'>'
'<p>'
'<span '
'class="o_delay_translation" '
'data-oe-model="ir.ui.view" '
f'data-oe-id="{view0.id}" '
'data-oe-field="arch_db" '
'data-oe-translation-state="translated" '
f'data-oe-translation-initial-sha="{sha256(terms_fr[1].encode()).hexdigest()}"'
'>'
f'{terms_fr[1]}'
'</span>'
'</p>'
'<div>'
'<span '
'class="o_delay_translation" '
'data-oe-model="ir.ui.view" '
f'data-oe-id="{view0.id}" '
'data-oe-field="arch_db" '
'data-oe-translation-state="translated" '
f'data-oe-translation-initial-sha="{sha256(terms_fr[2].encode()).hexdigest()}"'
'>'
f'{terms_fr[2]}'
'</span>'
'</div>'
'</form>'
)
self.assertEqual(
view0.with_context(lang='fr_FR', check_translations=True).arch_db,
archf2 % (terms_en2[0], terms_fr[1], terms_fr[2])
)
self.assertEqual(
view0.with_context(lang='nl_NL').arch_db,
archf2 % terms_en2,
"nl_NL value should fallback to en_US value"
)
self.assertEqual(
view0.with_context(lang='nl_NL', check_translations=True).arch_db,
archf2 % terms_en2
)
# update and confirm translations
view0.update_field_translations('arch_db', {'fr_FR': {}})
self.assertEqual(
view0.with_context(lang='fr_FR').arch_db,
archf2 % (terms_en2[0], terms_fr[1], terms_fr[2])
)
self.assertEqual(
view0.with_context(lang='fr_FR', check_translations=True).arch_db,
archf2 % (terms_en2[0], terms_fr[1], terms_fr[2])
)
def test_delay_translations_no_term(self):
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = ('Couteau', 'Fourchette', 'Cuiller')
view0 = self.create_view(archf, terms_en, fr_FR=terms_fr)
archf2 = '<form/>'
# delay_translations only works when the written value has at least one translatable term
view0.with_context(lang='en_US', delay_translations=True).arch_db = archf2
for lang in ('en_US', 'fr_FR', 'nl_NL'):
self.assertEqual(
view0.with_context(lang=lang).arch_db,
archf2,
f'arch_db for {lang} should be {archf2}'
)
self.assertEqual(
view0.with_context(lang=lang, check_translations=True).arch_db,
archf2,
f'arch_db for {lang} should be {archf2} when check_translations'
)
class TestHTMLTranslation(TransactionCase): class TestHTMLTranslation(TransactionCase):
def test_write_non_existing(self): def test_write_non_existing(self):
@@ -1172,6 +1284,28 @@ class TestHTMLTranslation(TransactionCase):
# same behavior is expected for translated fields # same behavior is expected for translated fields
company.flush_recordset() company.flush_recordset()
def test_delay_translations_no_term(self):
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.lang']._activate_lang('nl_NL')
Company = self.env['res.company']
company0 = Company.create({'name': 'company_1', 'report_footer': '<h1>Knife</h1>'})
company0.update_field_translations('report_footer', {'fr_FR': {'Knife': 'Couteau'}})
for html in ('<h1></h1>', '', False):
# delay_translations only works when the written value has at least one translatable term
company0.with_context(lang='en_US', delay_translations=True).report_footer = html
for lang in ('en_US', 'fr_FR', 'nl_NL'):
self.assertEqual(
company0.with_context(lang=lang).report_footer,
html,
f'report_footer for {lang} should be {html}'
)
self.assertEqual(
company0.with_context(lang=lang, check_translations=True).report_footer,
html,
f'report_footer for {lang} should be {html} when check_translations'
)
@tagged('post_install', '-at_install') @tagged('post_install', '-at_install')
class TestLanguageInstallPerformance(TransactionCase): class TestLanguageInstallPerformance(TransactionCase):
@@ -46,19 +46,19 @@ class TestIndexedTranslation(odoo.tests.TransactionCase):
SELECT "test_new_api_indexed_translation"."id" SELECT "test_new_api_indexed_translation"."id"
FROM "test_new_api_indexed_translation" FROM "test_new_api_indexed_translation"
WHERE (jsonb_path_query_array("test_new_api_indexed_translation"."name", '$.*')::text ILIKE %s WHERE (jsonb_path_query_array("test_new_api_indexed_translation"."name", '$.*')::text ILIKE %s
AND "test_new_api_indexed_translation"."name"->>'en_US' ILIKE %s) AND "test_new_api_indexed_translation"."name"->>%s ILIKE %s)
ORDER BY "test_new_api_indexed_translation"."id" ORDER BY "test_new_api_indexed_translation"."id"
""", """ """, """
SELECT "test_new_api_indexed_translation"."id" SELECT "test_new_api_indexed_translation"."id"
FROM "test_new_api_indexed_translation" FROM "test_new_api_indexed_translation"
WHERE (jsonb_path_query_array("test_new_api_indexed_translation"."name", '$.*')::text ILIKE %s WHERE (jsonb_path_query_array("test_new_api_indexed_translation"."name", '$.*')::text ILIKE %s
AND COALESCE("test_new_api_indexed_translation"."name"->>%s, "test_new_api_indexed_translation"."name"->>'en_US') ILIKE %s) AND COALESCE("test_new_api_indexed_translation"."name"->>%s, "test_new_api_indexed_translation"."name"->>%s) ILIKE %s)
ORDER BY "test_new_api_indexed_translation"."id" ORDER BY "test_new_api_indexed_translation"."id"
""", """ """, """
SELECT "test_new_api_indexed_translation"."id" SELECT "test_new_api_indexed_translation"."id"
FROM "test_new_api_indexed_translation" FROM "test_new_api_indexed_translation"
WHERE ("test_new_api_indexed_translation"."name" IS NULL WHERE ("test_new_api_indexed_translation"."name" IS NULL
OR COALESCE("test_new_api_indexed_translation"."name"->>%s, "test_new_api_indexed_translation"."name"->>'en_US') ILIKE %s) OR COALESCE("test_new_api_indexed_translation"."name"->>%s, "test_new_api_indexed_translation"."name"->>%s) ILIKE %s)
ORDER BY "test_new_api_indexed_translation"."id" ORDER BY "test_new_api_indexed_translation"."id"
"""]): """]):
record_en.search([('name', 'ilike', 'foo')]) record_en.search([('name', 'ilike', 'foo')])
@@ -2805,9 +2805,9 @@ class TestFields(TransactionCaseWithUserDemo):
with self.assertQueries([""" with self.assertQueries(["""
SELECT "test_new_api_prefetch"."id", SELECT "test_new_api_prefetch"."id",
"test_new_api_prefetch"."name"->>'en_US', "test_new_api_prefetch"."name"->>%s,
"test_new_api_prefetch"."description"->>'en_US', "test_new_api_prefetch"."description"->>%s,
"test_new_api_prefetch"."html_description"->>'en_US', "test_new_api_prefetch"."html_description"->>%s,
"test_new_api_prefetch"."create_uid", "test_new_api_prefetch"."create_uid",
"test_new_api_prefetch"."create_date", "test_new_api_prefetch"."create_date",
"test_new_api_prefetch"."write_uid", "test_new_api_prefetch"."write_uid",
@@ -278,7 +278,7 @@ class PropertiesCase(TestPropertiesMixin):
"test_new_api_message"."author", "test_new_api_message"."author",
"test_new_api_message"."name", "test_new_api_message"."name",
"test_new_api_message"."important", "test_new_api_message"."important",
"test_new_api_message"."label"->>'en_US', "test_new_api_message"."label"->>%s,
"test_new_api_message"."priority", "test_new_api_message"."priority",
"test_new_api_message"."active", "test_new_api_message"."active",
"test_new_api_message"."create_uid", "test_new_api_message"."create_uid",
@@ -139,6 +139,31 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
translation_importer.save(overwrite=True) translation_importer.save(overwrite=True)
self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau') self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau')
def test_write_from_ori_term(self):
self.test1.with_context(lang='fr_FR').html = '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>'
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
def test_delay_write_from_ori_term(self):
self.test1.with_context(lang='fr_FR', delay_translations=True).html = '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>'
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test1.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
def test_translate_from_ori_term(self): def test_translate_from_ori_term(self):
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>') self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>') self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
@@ -158,6 +183,31 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>') self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>') self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
def test_write_from_related_term(self):
self.test3.with_context(lang='fr_FR').html = '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>'
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
def test_delay_write_from_related_term(self):
self.test3.with_context(lang='fr_FR', delay_translations=True).html = '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>'
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test1.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US', check_translations=True).html, '<p>Nouveau couteau</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR', check_translations=True).html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
def test_translate_from_related_term(self): def test_translate_from_related_term(self):
self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>') self.assertEqual(self.test1.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>') self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
+15 -7
View File
@@ -944,7 +944,7 @@ class Cache(object):
cache_value = field_cache.get(record.id, EMPTY_DICT) cache_value = field_cache.get(record.id, EMPTY_DICT)
if cache_value is None: if cache_value is None:
return True return True
lang = record.env.lang or 'en_US' lang = field._lang(record.env)
return lang in cache_value return lang in cache_value
return record.id in field_cache return record.id in field_cache
@@ -965,7 +965,7 @@ class Cache(object):
field_cache = self._get_field_cache(record, field) field_cache = self._get_field_cache(record, field)
cache_value = field_cache[record._ids[0]] cache_value = field_cache[record._ids[0]]
if field.translate and cache_value is not None: if field.translate and cache_value is not None:
lang = record.env.lang or 'en_US' lang = field._lang(record.env)
return cache_value[lang] return cache_value[lang]
return cache_value return cache_value
except KeyError: except KeyError:
@@ -986,6 +986,7 @@ class Cache(object):
""" """
field_cache = self._set_field_cache(record, field) field_cache = self._set_field_cache(record, field)
if field.translate and value is not None: if field.translate and value is not None:
# only for model translated fields
lang = record.env.lang or 'en_US' lang = record.env.lang or 'en_US'
cache_value = field_cache.get(record._ids[0]) or {} cache_value = field_cache.get(record._ids[0]) or {}
cache_value[lang] = value cache_value[lang] = value
@@ -1019,6 +1020,7 @@ class Cache(object):
dirty must raise an exception dirty must raise an exception
""" """
if field.translate: if field.translate:
# only for model translated fields
lang = records.env.lang or 'en_US' lang = records.env.lang or 'en_US'
field_cache = self._get_field_cache(records, field) field_cache = self._get_field_cache(records, field)
cache_values = [] cache_values = []
@@ -1065,14 +1067,20 @@ class Cache(object):
if field.translate: if field.translate:
if records.env.context.get('prefetch_langs'): if records.env.context.get('prefetch_langs'):
langs = {lang for lang, _ in records.env['res.lang'].get_installed()} | {'en_US'} langs = {lang for lang, _ in records.env['res.lang'].get_installed()} | {'en_US'}
_langs = {f'_{l}' for l in langs} if field._lang(records.env).startswith('_') else set()
for id_, val in zip(records._ids, values): for id_, val in zip(records._ids, values):
if val is None: if val is None:
field_cache.setdefault(id_, None) field_cache.setdefault(id_, None)
else: else:
val_all_en = dict.fromkeys(langs, val['en_US']) if _langs: # fallback missing _lang to lang if exists
field_cache[id_] = {**val_all_en, **val} val.update({f'_{k}': v for k, v in val.items() if k in langs and f'_{k}' not in val})
field_cache[id_] = {
**dict.fromkeys(langs, val['en_US']), # fallback missing lang to en_US
**dict.fromkeys(_langs, val.get('_en_US')), # fallback missing _lang to _en_US
**val
}
else: else:
lang = records.env.lang or 'en_US' lang = field._lang(records.env)
for id_, val in zip(records._ids, values): for id_, val in zip(records._ids, values):
if val is None: if val is None:
field_cache.setdefault(id_, None) field_cache.setdefault(id_, None)
@@ -1106,7 +1114,7 @@ class Cache(object):
""" Return the cached values of ``field`` for ``records`` until a value is not found. """ """ Return the cached values of ``field`` for ``records`` until a value is not found. """
field_cache = self._get_field_cache(records, field) field_cache = self._get_field_cache(records, field)
if field.translate: if field.translate:
lang = records.env.lang or 'en_US' lang = field._lang(records.env)
def get_value(id_): def get_value(id_):
cache_value = field_cache[id_] cache_value = field_cache[id_]
@@ -1160,7 +1168,7 @@ class Cache(object):
""" Return the ids of ``records`` that have no value for ``field``. """ """ Return the ids of ``records`` that have no value for ``field``. """
field_cache = self._get_field_cache(records, field) field_cache = self._get_field_cache(records, field)
if field.translate: if field.translate:
lang = records.env.lang or 'en_US' lang = field._lang(records.env)
for record_id in records._ids: for record_id in records._ids:
cache_value = field_cache.get(record_id, False) cache_value = field_cache.get(record_id, False)
if cache_value is False or not (cache_value is None or lang in cache_value): if cache_value is False or not (cache_value is None or lang in cache_value):
+47 -12
View File
@@ -1715,20 +1715,23 @@ class _String(Field):
if value is None: if value is None:
return False return False
if callable(self.translate) and record.env.context.get('edit_translations'): if callable(self.translate) and record.env.context.get('edit_translations'):
terms = self.get_trans_terms(value) if not (terms := self.get_trans_terms(value)):
return value
base_lang = record._get_base_lang() base_lang = record._get_base_lang()
if base_lang != (record.env.lang or 'en_US'): if base_lang != (record.env.lang or 'en_US'):
base_value = record.with_context(edit_translations=None, lang=base_lang)[self.name] base_value = record.with_context(edit_translations=None, check_translations=True, lang=base_lang)[self.name]
base_terms = self.get_trans_terms(base_value) base_terms = self.get_trans_terms(base_value)
term_to_state = {term: "translated" if base_term != term else "to_translate" for term, base_term in zip(terms, base_terms)} term_to_state = {term: "translated" if base_term != term else "to_translate" for term, base_term in zip(terms, base_terms)}
else: else:
term_to_state = defaultdict(lambda: 'translated') term_to_state = defaultdict(lambda: 'translated')
lang = record.env.lang or 'en_US'
delay_translation = value != record.with_context(edit_translations=None, check_translations=None, lang=lang)[self.name]
# use a wrapper to let the frontend js code identify each term and its metadata in the 'edit_translations' context # use a wrapper to let the frontend js code identify each term and its metadata in the 'edit_translations' context
def translate_func(term):
return f'''<span {'class="o_delay_translation" ' if delay_translation else ''}data-oe-model="{record._name}" data-oe-id="{record.id}" data-oe-field="{self.name}" data-oe-translation-state="{term_to_state[term]}" data-oe-translation-initial-sha="{sha256(term.encode()).hexdigest()}">{term}</span>'''
# pylint: disable=not-callable # pylint: disable=not-callable
value = self.translate( value = self.translate(translate_func, value)
lambda term: f'''<span data-oe-model="{record._name}" data-oe-id="{record.id}" data-oe-field="{self.name}" data-oe-translation-state="{term_to_state[term]}" data-oe-translation-initial-sha="{sha256(term.encode()).hexdigest()}">{term}</span>''',
value
)
return value return value
def convert_to_write(self, value, record): def convert_to_write(self, value, record):
@@ -1773,6 +1776,23 @@ class _String(Field):
res = cr.fetchone() res = cr.fetchone()
return res[0] if res else None return res[0] if res else None
def get_translation_fallback_langs(self, env):
lang = self._lang(env)
if lang == '_en_US':
return '_en_US', 'en_US'
if lang == 'en_US':
return ('en_US',)
if lang.startswith('_'):
return lang, lang[1:], '_en_US', 'en_US'
return lang, 'en_US'
def _lang(self, env):
context = env.context
lang = env.lang or 'en_US'
if callable(self.translate) and (context.get('edit_translations') or context.get('check_translations')):
lang = '_' + lang
return lang
def write(self, records, value): def write(self, records, value):
if not self.translate or value is False or value is None: if not self.translate or value is False or value is None:
super().write(records, value) super().write(records, value)
@@ -1793,6 +1813,7 @@ class _String(Field):
# not dirty fields # not dirty fields
if not dirty: if not dirty:
lang = self._lang(records.env)
cache.update_raw(records, self, [{lang: cache_value} for _id in records._ids], dirty=False) cache.update_raw(records, self, [{lang: cache_value} for _id in records._ids], dirty=False)
return return
@@ -1812,6 +1833,7 @@ class _String(Field):
# pylint: disable=not-callable # pylint: disable=not-callable
cache_value = self.translate(lambda t: None, cache_value) cache_value = self.translate(lambda t: None, cache_value)
new_terms = set(self.get_trans_terms(cache_value)) new_terms = set(self.get_trans_terms(cache_value))
delay_translations = records.env.context.get('delay_translations')
for record in records: for record in records:
# shortcut when no term needs to be translated # shortcut when no term needs to be translated
if not new_terms: if not new_terms:
@@ -1819,11 +1841,16 @@ class _String(Field):
continue continue
# _get_stored_translations can be refactored and prefetches translations for multi records, # _get_stored_translations can be refactored and prefetches translations for multi records,
# but it is really rare to write the same non-False/None/no-term value to multi records # but it is really rare to write the same non-False/None/no-term value to multi records
old_translations = self._get_stored_translations(record) stored_translations = self._get_stored_translations(record)
if not old_translations: if not stored_translations:
new_translations_list.append({'en_US': cache_value, lang: cache_value}) new_translations_list.append({'en_US': cache_value, lang: cache_value})
continue continue
from_lang_value = old_translations.get(lang, old_translations['en_US']) old_translations = {
k: stored_translations.get(f'_{k}', v)
for k, v in stored_translations.items()
if not k.startswith('_')
}
from_lang_value = old_translations.pop(lang, old_translations['en_US'])
translation_dictionary = self.get_translation_dictionary(from_lang_value, old_translations) translation_dictionary = self.get_translation_dictionary(from_lang_value, old_translations)
text2terms = defaultdict(list) text2terms = defaultdict(list)
for term in new_terms: for term in new_terms:
@@ -1844,10 +1871,18 @@ class _String(Field):
l: self.translate(lambda term: translation_dictionary.get(term, {l: None})[l], cache_value) l: self.translate(lambda term: translation_dictionary.get(term, {l: None})[l], cache_value)
for l in old_translations.keys() for l in old_translations.keys()
} }
new_translations[lang] = cache_value if delay_translations:
new_store_translations = stored_translations
new_store_translations.update({f'_{k}': v for k, v in new_translations.items()})
new_store_translations.pop(f'_{lang}', None)
else:
new_store_translations = new_translations
new_store_translations[lang] = cache_value
if not records.env['res.lang']._lang_get_id('en_US'): if not records.env['res.lang']._lang_get_id('en_US'):
new_translations['en_US'] = cache_value new_store_translations['en_US'] = cache_value
new_translations_list.append(new_translations) new_store_translations.pop('_en_US', None)
new_translations_list.append(new_store_translations)
# Maybe we can use Cache.update(records.with_context(cache_update_raw=True), self, new_translations_list, dirty=True) # Maybe we can use Cache.update(records.with_context(cache_update_raw=True), self, new_translations_list, dirty=True)
cache.update_raw(records, self, new_translations_list, dirty=True) cache.update_raw(records, self, new_translations_list, dirty=True)
+36 -28
View File
@@ -2819,10 +2819,11 @@ class BaseModel(metaclass=MetaModel):
elif field.translate and not self.env.context.get('prefetch_langs'): elif field.translate and not self.env.context.get('prefetch_langs'):
sql_field = SQL.identifier(alias, fname) sql_field = SQL.identifier(alias, fname)
lang = self.env.lang or 'en_US' langs = field.get_translation_fallback_langs(self.env)
if lang == 'en_US': sql_field_langs = [SQL("%s->>%s", sql_field, lang) for lang in langs]
return SQL("%s->>'en_US'", sql_field) if len(sql_field_langs) == 1:
return SQL("COALESCE(%s->>%s, %s->>'en_US')", sql_field, lang, sql_field) return sql_field_langs[0]
return SQL("COALESCE(%s)", SQL(", ").join(sql_field_langs))
elif field.type == 'properties' and property_name: elif field.type == 'properties' and property_name:
return self._field_properties_to_sql(alias, fname, property_name, query) return self._field_properties_to_sql(alias, fname, property_name, query)
@@ -3618,13 +3619,16 @@ class BaseModel(metaclass=MetaModel):
# assert record_fr.with_context(lang='fr_FR') == '<div>English 1</div><div>French 2<div/>' # assert record_fr.with_context(lang='fr_FR') == '<div>English 1</div><div>French 2<div/>'
# assert record_nl.with_context(lang='nl_NL') == '<div>English 3</div><div>English 2<div/>' # assert record_nl.with_context(lang='nl_NL') == '<div>English 3</div><div>English 2<div/>'
old_translations = field._get_stored_translations(self) stored_translations = field._get_stored_translations(self)
if not old_translations: if not stored_translations:
return False return False
new_translations = old_translations old_translations = {
old_value_en = old_translations.get('en_US') k: stored_translations.get(f'_{k}', v)
for k, v in stored_translations.items()
if not k.startswith('_')
}
for lang, translation in translations.items(): for lang, translation in translations.items():
old_value = new_translations.get(lang, old_value_en) old_value = old_translations.get(lang) or old_translations.get('en_US')
if digest: if digest:
old_terms = field.get_trans_terms(old_value) old_terms = field.get_trans_terms(old_value)
old_terms_digested2value = {digest(old_term): old_term for old_term in old_terms} old_terms_digested2value = {digest(old_term): old_term for old_term in old_terms}
@@ -3633,8 +3637,9 @@ class BaseModel(metaclass=MetaModel):
for key, value in translation.items() for key, value in translation.items()
if key in old_terms_digested2value if key in old_terms_digested2value
} }
new_translations[lang] = field.translate(translation.get, old_value) stored_translations[lang] = field.translate(translation.get, old_value)
self.env.cache.update_raw(self, field, [new_translations], dirty=True) stored_translations.pop(f'_{lang}', None)
self.env.cache.update_raw(self, field, [stored_translations], dirty=True)
# the following write is incharge of # the following write is incharge of
# 1. mark field as modified # 1. mark field as modified
@@ -3657,16 +3662,17 @@ class BaseModel(metaclass=MetaModel):
field = self._fields[field_name] field = self._fields[field_name]
# We don't forbid reading inactive/non-existing languages, # We don't forbid reading inactive/non-existing languages,
langs = set(langs or [l[0] for l in self.env['res.lang'].get_installed()]) langs = set(langs or [l[0] for l in self.env['res.lang'].get_installed()])
val_en = self.with_context(lang='en_US')[field_name] self_lang = self.with_context(check_translations=True, prefetch_langs=True)
val_en = self_lang.with_context(lang='en_US')[field_name]
if not callable(field.translate): if not callable(field.translate):
translations = [{ translations = [{
'lang': lang, 'lang': lang,
'source': val_en, 'source': val_en,
'value': self.with_context(lang=lang)[field_name] 'value': self_lang.with_context(lang=lang)[field_name]
} for lang in langs] } for lang in langs]
else: else:
translation_dictionary = field.get_translation_dictionary( translation_dictionary = field.get_translation_dictionary(
val_en, {lang: self.with_context(lang=lang)[field_name] for lang in langs} val_en, {lang: self_lang.with_context(lang=lang)[field_name] for lang in langs}
) )
translations = [{ translations = [{
'lang': lang, 'lang': lang,
@@ -5474,25 +5480,27 @@ class BaseModel(metaclass=MetaModel):
elif field.translate and field.store and name not in excluded and old[name]: elif field.translate and field.store and name not in excluded and old[name]:
# for translatable fields we copy their translations # for translatable fields we copy their translations
old_translations = field._get_stored_translations(old) old_stored_translations = field._get_stored_translations(old)
if not old_translations: if not old_stored_translations:
continue continue
lang = self.env.lang or 'en_US' lang = self.env.lang or 'en_US'
old_value_lang = old_translations.pop(lang, old_translations['en_US']) if field.translate is True:
old_translations = { new.update_field_translations(name, {
lang: value k: v for k, v in old_stored_translations.items() if k in valid_langs and k != lang
for lang, value in old_translations.items() })
if lang in valid_langs
}
if not old_translations:
continue
if not callable(field.translate):
new.update_field_translations(name, old_translations)
else: else:
old_translations = {
k: old_stored_translations.get(f'_{k}', v)
for k, v in old_stored_translations.items()
if k in valid_langs
}
# {from_lang_term: {lang: to_lang_term}
translation_dictionary = field.get_translation_dictionary(
old_translations.pop(lang, old_translations['en_US']),
old_translations
)
# {lang: {old_term: new_term}} # {lang: {old_term: new_term}}
translations = defaultdict(dict) translations = defaultdict(dict)
# {from_lang_term: {lang: to_lang_term}
translation_dictionary = field.get_translation_dictionary(old_value_lang, old_translations)
for from_lang_term, to_lang_terms in translation_dictionary.items(): for from_lang_term, to_lang_terms in translation_dictionary.items():
for lang, to_lang_term in to_lang_terms.items(): for lang, to_lang_term in to_lang_terms.items():
translations[lang][from_lang_term] = to_lang_term translations[lang][from_lang_term] = to_lang_term
+5 -4
View File
@@ -1444,11 +1444,12 @@ class expression(object):
check_null = len(params) < len(right) check_null = len(params) < len(right)
if params: if params:
params = [field.convert_to_column(p, model, validate=False).adapted['en_US'] for p in params] params = [field.convert_to_column(p, model, validate=False).adapted['en_US'] for p in params]
lang = model.env.lang or 'en_US' langs = field.get_translation_fallback_langs(model.env)
if lang == 'en_US': sql_left_langs = [SQL("%s->>%s", sql_field, lang) for lang in langs]
sql_left = SQL("%s->>'en_US'", sql_field) if len(sql_left_langs) == 1:
sql_left = sql_left_langs[0]
else: else:
sql_left = SQL("COALESCE(%s->>%s, %s->>'en_US')", sql_field, lang, sql_field) sql_left = SQL('COALESCE(%s)', SQL(', ').join(sql_left_langs))
sql = SQL("%s %s %s", sql_left, sql_operator, tuple(params)) sql = SQL("%s %s %s", sql_left, sql_operator, tuple(params))
else: else:
# The case for (left, 'in', []) or (left, 'not in', []). # The case for (left, 'in', []) or (left, 'not in', []).
+12 -6
View File
@@ -1017,7 +1017,7 @@ class TranslationReader:
if not records: if not records:
return return
for record in records: for record in records.with_context(check_translations=True):
module = imd_per_id[record.id].module module = imd_per_id[record.id].module
xml_name = "%s.%s" % (module, imd_per_id[record.id].name) xml_name = "%s.%s" % (module, imd_per_id[record.id].name)
for field_name, field in record._fields.items(): for field_name, field in record._fields.items():
@@ -1406,16 +1406,20 @@ class TranslationImporter:
for id_, xmlid, values, noupdate in cr.fetchall(): for id_, xmlid, values, noupdate in cr.fetchall():
if not values: if not values:
continue continue
value_en = values.get('en_US') _value_en = values.get('_en_US', values['en_US'])
if not value_en: if not _value_en:
continue continue
# {src: {lang: value}} # {src: {lang: value}}
record_dictionary = field_dictionary[xmlid] record_dictionary = field_dictionary[xmlid]
langs = {lang for translations in record_dictionary.values() for lang in translations.keys()} langs = {lang for translations in record_dictionary.values() for lang in translations.keys()}
translation_dictionary = field.get_translation_dictionary( translation_dictionary = field.get_translation_dictionary(
value_en, _value_en,
{k: v for k, v in values.items() if k in langs} {
k: values.get(f'_{k}', v)
for k, v in values.items()
if k in langs
}
) )
if force_overwrite or (not noupdate and overwrite): if force_overwrite or (not noupdate and overwrite):
@@ -1429,7 +1433,9 @@ class TranslationImporter:
translation_dictionary[term_en] = translations translation_dictionary[term_en] = translations
for lang in langs: for lang in langs:
values[lang] = field.translate(lambda term: translation_dictionary.get(term, {}).get(lang), value_en) # translate and confirm model_terms translations
values[lang] = field.translate(lambda term: translation_dictionary.get(term, {}).get(lang), _value_en)
values.pop(f'_{lang}', None)
params.extend((id_, Json(values))) params.extend((id_, Json(values)))
if params: if params:
env.cr.execute(f""" env.cr.execute(f"""