[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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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="'
|
||||||
|
'<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="to_translate" '
|
||||||
|
f'data-oe-translation-initial-sha="{sha256(terms_en2[0].encode()).hexdigest()}"'
|
||||||
|
'>'
|
||||||
|
f'{terms_en2[0]}'
|
||||||
|
'</span>"'
|
||||||
|
'>'
|
||||||
|
'<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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user