[MERGE] base: remove field source of ir.translation
Instead, make sure the field src is always up to date Change a bit the behaviour of `_write` to always try to update potential translations when in monolanguage (was only done in multi-language if different than en_US) and always create translations when writing on a translatable field when in multi-language (was skipped in en_US) Add tests Task-id: 2031752 Pad: https://pad.odoo.com/p/r.dbf835d0aae979d858f73446dcbfb1df closes odoo/odoo#34598 Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
@@ -63,7 +63,7 @@ class IrTranslation(models.Model):
|
||||
project_modules[module] = tx_project
|
||||
|
||||
for translation in self:
|
||||
if not translation.module or not translation.source or translation.lang == 'en_US':
|
||||
if not translation.module or not translation.src or translation.lang == 'en_US':
|
||||
# custom or source term
|
||||
translation.transifex_url = False
|
||||
continue
|
||||
@@ -85,6 +85,6 @@ class IrTranslation(models.Model):
|
||||
'lang': lang_code,
|
||||
'module': translation.module,
|
||||
'src': "text:'" + werkzeug.url_quote_plus(
|
||||
translation.source[:50].replace("\n", "").replace("'", "")
|
||||
translation.src[:50].replace("\n", "").replace("'", "")
|
||||
) + "'",
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class WebsiteGengo(http.Controller):
|
||||
result = {"done": 0}
|
||||
gengo_translation_ids = request.env['ir.translation'].search([('id', 'in', translated_ids), ('gengo_translation', '!=', False)])
|
||||
for trans in gengo_translation_ids:
|
||||
result['done'] += len(trans.source.split())
|
||||
result['done'] += len(trans.src.split())
|
||||
return result
|
||||
|
||||
@http.route('/website/check_gengo_set', type='json', auth='user', website=True)
|
||||
|
||||
@@ -171,8 +171,6 @@ class IrTranslation(models.Model):
|
||||
lang = fields.Selection(selection='_get_languages', string='Language', validate=False)
|
||||
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
|
||||
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
|
||||
source = fields.Text(string='Source term', compute='_compute_source',
|
||||
inverse='_inverse_source', search='_search_source')
|
||||
value = fields.Text(string='Translation Value')
|
||||
module = fields.Char(index=True, help="Module this term belongs to")
|
||||
|
||||
@@ -196,55 +194,6 @@ class IrTranslation(models.Model):
|
||||
langs = self.env['res.lang'].search([])
|
||||
return [(lang.code, lang.name) for lang in langs]
|
||||
|
||||
@api.depends('type', 'name', 'res_id')
|
||||
def _compute_source(self):
|
||||
''' Get source name for the translation. If object type is model, return
|
||||
the value stored in db. Otherwise, return value store in src field.
|
||||
'''
|
||||
for record in self:
|
||||
record.source = record.src
|
||||
if record.type != 'model':
|
||||
continue
|
||||
model_name, field_name = record.name.split(',')
|
||||
if model_name not in self.env:
|
||||
continue
|
||||
model = self.env[model_name]
|
||||
field = model._fields.get(field_name)
|
||||
if field is None:
|
||||
continue
|
||||
if not callable(field.translate):
|
||||
# Pass context without lang, need to read real stored field, not translation
|
||||
try:
|
||||
result = model.browse(record.res_id).with_context(lang=None).read([field_name])
|
||||
except AccessError:
|
||||
# because we can read self but not the record,
|
||||
# that means we would get an access error when accessing the translations
|
||||
# so instead we defer the access right to the "check" method
|
||||
result = [{field_name: _("Cannot be translated; record not accessible.")}]
|
||||
record.source = result[0][field_name] if result else False
|
||||
|
||||
def _inverse_source(self):
|
||||
''' When changing source term of a translation, change its value in db
|
||||
for the associated object, and the src field.
|
||||
'''
|
||||
self.ensure_one()
|
||||
if self.type == 'model':
|
||||
model_name, field_name = self.name.split(',')
|
||||
model = self.env[model_name]
|
||||
field = model._fields[field_name]
|
||||
if not callable(field.translate):
|
||||
# Make a context without language information, because we want
|
||||
# to write on the value stored in db and not on the one
|
||||
# associated with the current language. Also not removing lang
|
||||
# from context trigger an error when lang is different.
|
||||
model.browse(self.res_id).with_context(lang=None).write({field_name: self.source})
|
||||
if self.src != self.source:
|
||||
self.write({'src': self.source})
|
||||
|
||||
def _search_source(self, operator, value):
|
||||
''' the source term is stored on 'src' field '''
|
||||
return [('src', operator, value)]
|
||||
|
||||
def _auto_init(self):
|
||||
res = super(IrTranslation, self)._auto_init()
|
||||
# Add separate md5 index on src (no size limit on values, and good performance).
|
||||
@@ -329,6 +278,22 @@ class IrTranslation(models.Model):
|
||||
])
|
||||
return len(ids)
|
||||
|
||||
def _set_source(self, name, ids, src):
|
||||
""" Update the translation source of records.
|
||||
|
||||
:param name: a string defined as "<model_name>,<field_name>"
|
||||
:param ids: the ids of the given records
|
||||
:param src: the source of the translation
|
||||
"""
|
||||
self._cr.execute("""UPDATE ir_translation
|
||||
SET src=%s
|
||||
WHERE type=%s AND name=%s AND res_id IN %s
|
||||
RETURNING id""",
|
||||
(src, 'model', name, tuple(ids)))
|
||||
existing_ids = [row[0] for row in self._cr.fetchall()]
|
||||
# invalidate src for updated translations
|
||||
self.invalidate_cache(fnames=['src'], ids=existing_ids)
|
||||
|
||||
@api.model
|
||||
def _get_source_query(self, name, types, lang, source, res_id):
|
||||
if source:
|
||||
@@ -645,6 +610,8 @@ class IrTranslation(models.Model):
|
||||
This method is used for creations of translations where the given
|
||||
``vals_list`` is trusted to be the right values and potential
|
||||
conflicts should be updated to the new given value.
|
||||
Mandatory values: name, lang, res_id, src, type
|
||||
The other keys are ignored during update if not present
|
||||
"""
|
||||
rows_by_type = defaultdict(list)
|
||||
for vals in vals_list:
|
||||
@@ -661,7 +628,10 @@ class IrTranslation(models.Model):
|
||||
ON CONFLICT (type, lang, name, res_id) WHERE type='model'
|
||||
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) =
|
||||
(EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type,
|
||||
EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
|
||||
EXCLUDED.value,
|
||||
COALESCE(EXCLUDED.module, ir_translation.module),
|
||||
COALESCE(EXCLUDED.state, ir_translation.state),
|
||||
COALESCE(EXCLUDED.comments, ir_translation.comments))
|
||||
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
|
||||
""".format(", ".join(["%s"] * len(rows_by_type['model'])))
|
||||
self.env.cr.execute(query, rows_by_type['model'])
|
||||
@@ -679,6 +649,30 @@ class IrTranslation(models.Model):
|
||||
""".format(", ".join(["%s"] * len(rows_by_type['model_terms'])))
|
||||
self.env.cr.execute(query, rows_by_type['model_terms'])
|
||||
|
||||
def _update_translations(self, vals_list):
|
||||
""" Update translations of type 'model' or 'model_terms'.
|
||||
|
||||
This method is used for update of translations where the given
|
||||
``vals_list`` is trusted to be the right values
|
||||
No new translation will be created
|
||||
"""
|
||||
grouped_rows = {}
|
||||
for vals in vals_list:
|
||||
key = (vals['lang'], vals['type'], vals['name'])
|
||||
grouped_rows.setdefault(key, [vals['value'], vals['src'], vals['state'], []])
|
||||
grouped_rows[key][3].append(vals['res_id'])
|
||||
|
||||
for where, values in grouped_rows.items():
|
||||
self._cr.execute(
|
||||
""" UPDATE ir_translation
|
||||
SET value=%s,
|
||||
src=%s,
|
||||
state=%s
|
||||
WHERE lang=%s AND type=%s AND name=%s AND res_id in %s
|
||||
""",
|
||||
(values[0], values[1], values[2], where[0], where[1], where[2], tuple(values[3]))
|
||||
)
|
||||
|
||||
@api.model
|
||||
def translate_fields(self, model, id, field=None):
|
||||
""" Open a view for translating the field(s) of the record (model, id). """
|
||||
|
||||
@@ -275,6 +275,13 @@ class TestTranslation(TransactionCase):
|
||||
category_fr = category.with_context({'lang': 'fr_FR'})
|
||||
self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value")
|
||||
|
||||
translation_fr = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', category.id),
|
||||
('lang', '=', 'fr_FR'),
|
||||
])
|
||||
self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source")
|
||||
|
||||
def test_103_duplicate_record_fr(self):
|
||||
category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'})
|
||||
|
||||
@@ -284,6 +291,13 @@ class TestTranslation(TransactionCase):
|
||||
category_fr = category.with_context({'lang': 'fr_FR'})
|
||||
self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value")
|
||||
|
||||
translation_fr = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', category.id),
|
||||
('lang', '=', 'fr_FR'),
|
||||
])
|
||||
self.assertEqual(translation_fr.src, 'Clients (copie)', "Did not set new name as source")
|
||||
|
||||
def test_104_orderby_translated_field(self):
|
||||
""" Test search ordered by a translated field. """
|
||||
# create a category with a French translation
|
||||
@@ -328,6 +342,143 @@ class TestTranslation(TransactionCase):
|
||||
self.assertEqual(len(translations), 1, "Translations were not duplicated after `translate_fields` call")
|
||||
self.assertEqual(translations.value, "Apprenti", "The first translation must stay")
|
||||
|
||||
def test_106_en_us_translation(self):
|
||||
""" Test synchronizing translations with duplicated source """
|
||||
# create a category with a French translation
|
||||
cheese = self.env['res.partner.category'].create({'name': 'Cheese'})
|
||||
|
||||
self.env['ir.translation'].translate_fields('res.partner.category', cheese.id, 'name')
|
||||
|
||||
translations = self.env['ir.translation'].search([('name', '=', 'res.partner.category,name'), ('res_id', '=', cheese.id)], order='lang')
|
||||
self.assertEqual(len(translations), 2)
|
||||
self.assertRecordValues(translations,
|
||||
[{'lang': 'en_US', 'src': 'Cheese', 'value': 'Cheese'},
|
||||
{'lang': 'fr_FR', 'src': 'Cheese', 'value': 'Cheese'}])
|
||||
|
||||
# Translate in both language
|
||||
translations[0].value = 'The Cheese'
|
||||
translations[1].value = 'Fromage'
|
||||
|
||||
# lang=None bypass translation system
|
||||
self.assertEqual(cheese.with_context(lang=None).name, 'Cheese')
|
||||
self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage')
|
||||
self.assertEqual(cheese.with_context(lang='en_US').name, 'The Cheese')
|
||||
|
||||
# set a new master value
|
||||
cheese.with_context(lang='en_US').write({'name': 'Delicious Cheese'})
|
||||
|
||||
# every src must be updated
|
||||
self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese')
|
||||
self.assertRecordValues(translations,
|
||||
[{'lang': 'en_US', 'src': 'Delicious Cheese', 'value': 'Delicious Cheese'},
|
||||
{'lang': 'fr_FR', 'src': 'Delicious Cheese', 'value': 'Fromage'}])
|
||||
|
||||
self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese')
|
||||
self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage')
|
||||
self.assertEqual(cheese.with_context(lang='en_US').name, 'Delicious Cheese')
|
||||
|
||||
def test_107_duplicate_record_en(self):
|
||||
category = self.customers.with_context({'lang': 'en_US'}).copy()
|
||||
|
||||
category_no = category.with_context({})
|
||||
self.assertEqual(category_no.name, 'Customers', "Duplication did not set untranslated value")
|
||||
|
||||
category_fr = category.with_context({'lang': 'fr_FR'})
|
||||
self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value")
|
||||
|
||||
translation_fr = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', category.id),
|
||||
('lang', '=', 'fr_FR'),
|
||||
])
|
||||
self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source")
|
||||
|
||||
class TestTranslationWrite(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = self.env['res.partner.category'].create({'name': 'Reblochon'})
|
||||
|
||||
def test_01_en(self):
|
||||
langs = self.env['res.lang'].get_installed()
|
||||
self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages")
|
||||
|
||||
self.category.with_context(lang='en_US').write({'name': 'English Name'})
|
||||
name = self.category.with_context(lang=None).read(['name'])
|
||||
self.assertEqual(name[0]['name'], "English Name", "Reference field not updated")
|
||||
translations = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', self.category.id),
|
||||
('lang', '=', 'en_US'),
|
||||
])
|
||||
self.assertEqual(len(translations), 0, "No English translation should be created when writing in English")
|
||||
|
||||
def test_02_en_translated(self):
|
||||
langs = self.env['res.lang'].get_installed()
|
||||
self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages")
|
||||
translation = self.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'res.partner.category,name',
|
||||
'lang': 'en_US',
|
||||
'res_id': self.category.id,
|
||||
'src': 'Reblochon',
|
||||
'value': 'Translated Name',
|
||||
'state': 'translated',
|
||||
})
|
||||
|
||||
self.category.with_context(lang='en_US').write({'name': 'English Name'})
|
||||
translation_value = translation.read(['value'])
|
||||
self.assertEqual(translation_value[0]['value'], "English Name", "Existing translation was not updated")
|
||||
|
||||
source_name = self.category.with_context(lang=None).read(['name'])
|
||||
self.assertEqual(source_name[0]['name'], "English Name", "Reference field not updated")
|
||||
|
||||
def test_03_fr_single(self):
|
||||
self.env['res.lang'].load_lang('fr_FR')
|
||||
self.env['res.users'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
|
||||
self.env.ref('base.lang_en').active = False
|
||||
|
||||
langs = self.env['res.lang'].get_installed()
|
||||
self.assertEqual([('fr_FR', 'French / Français')], langs, "Test did not started with expected languages")
|
||||
|
||||
self.category.with_context(lang='fr_FR').write({'name': 'French Name'})
|
||||
source_name = self.category.with_context(lang=None).read(['name'])
|
||||
self.assertEqual(source_name[0]['name'], "French Name", "Reference field not updated")
|
||||
translations = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', self.category.id),
|
||||
('lang', '=', 'fr_FR'),
|
||||
])
|
||||
self.assertEqual(len(translations), 0, "No French translation should be created when writing in French")
|
||||
|
||||
def test_04_fr_multi(self):
|
||||
self.env['res.lang'].load_lang('fr_FR')
|
||||
|
||||
langs = self.env['res.lang'].get_installed()
|
||||
self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs,
|
||||
"Test did not started with expected languages")
|
||||
self.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'res.partner.category,name',
|
||||
'lang': 'en_US',
|
||||
'res_id': self.category.id,
|
||||
'src': 'Reblochon',
|
||||
'value': 'Translated Name',
|
||||
'state': 'translated',
|
||||
})
|
||||
|
||||
self.category.with_context(lang='fr_FR').write({'name': 'French Name'})
|
||||
self.category.with_context(lang='en_US').write({'name': 'English Name'})
|
||||
translations = self.env['ir.translation'].search([
|
||||
('name', '=', 'res.partner.category,name'),
|
||||
('res_id', '=', self.category.id),
|
||||
], order='lang')
|
||||
self.assertRecordValues(translations, [
|
||||
{'src': 'English Name', 'value': 'English Name', 'lang': 'en_US'},
|
||||
{'src': 'English Name', 'value': 'French Name', 'lang': 'fr_FR'}
|
||||
])
|
||||
|
||||
|
||||
|
||||
class TestXMLTranslation(TransactionCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
string="Web-only translations"
|
||||
domain="[('comments', 'like', 'openerp-web')]"/>
|
||||
<field name="lang"/>
|
||||
<field name="source"/>
|
||||
<field name="src"/>
|
||||
<field name="value"/>
|
||||
<field name="name" filter_domain="('|', ('name', '=', self), ('name', 'ilike', self + ','))" string="Model"/>
|
||||
<field name="res_id" filter_domain="('&', ('name', '=', 'ir.ui.view,arch_db'), ('res_id', '=', self))" string="View ID"/>
|
||||
@@ -38,7 +38,7 @@
|
||||
<field name="res_id"/>
|
||||
</group>
|
||||
<group string="Source Term">
|
||||
<field name="source" nolabel="1" height="400"/>
|
||||
<field name="src" nolabel="1" height="400"/>
|
||||
</group>
|
||||
<group string="Translation">
|
||||
<field name="value" nolabel="1" height="400"/>
|
||||
@@ -55,7 +55,7 @@
|
||||
<field name="model">ir.translation</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Translations" editable="top">
|
||||
<field name="source"/>
|
||||
<field name="src"/>
|
||||
<field name="value"/>
|
||||
<field name="name"/>
|
||||
<field name="lang"/>
|
||||
@@ -71,7 +71,7 @@
|
||||
<tree string="Translations" editable="top" create="false">
|
||||
<field name="lang" readonly="1"/>
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="source"/>
|
||||
<field name="src"/>
|
||||
<field name="value"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
|
||||
@@ -1230,8 +1230,7 @@ class TestFields(common.TransactionCase):
|
||||
email = self.env.ref('test_new_api.emailmessage_0_0')
|
||||
self.assertEqual(email.message, message)
|
||||
|
||||
french = self.env['res.lang']._lang_get('fr_FR')
|
||||
french.active = True
|
||||
self.env['res.lang'].load_lang('fr_FR')
|
||||
|
||||
def count(msg):
|
||||
# return the number of translations of msg.label
|
||||
|
||||
+26
-6
@@ -3397,6 +3397,12 @@ Fields:
|
||||
other_fields = [] # list of non-column fields
|
||||
single_lang = len(self.env['res.lang'].get_installed()) <= 1
|
||||
has_translation = self.env.lang and self.env.lang != 'en_US'
|
||||
# when there is only one language, update existing translations but
|
||||
# do not create new ones
|
||||
if single_lang:
|
||||
process_translations = self.env['ir.translation']._update_translations
|
||||
else:
|
||||
process_translations = self.env['ir.translation']._upsert_translations
|
||||
|
||||
for name, val in vals.items():
|
||||
field = self._fields[name]
|
||||
@@ -3410,6 +3416,9 @@ Fields:
|
||||
# val is not a translation: update the table
|
||||
val = field.convert_to_column(val, self, vals)
|
||||
columns.append((name, field.column_format, val))
|
||||
tname = "%s,%s" % (self._name, name)
|
||||
if field.translate is True and self.env.lang:
|
||||
self.env['ir.translation']._set_source(tname, self.ids, val)
|
||||
updated.append(name)
|
||||
else:
|
||||
other_fields.append(field)
|
||||
@@ -3440,6 +3449,7 @@ Fields:
|
||||
+ '\n\n({} {}, {} {})'.format(_('Records:'), sub_ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
|
||||
translation_values = []
|
||||
for name in updated:
|
||||
field = self._fields[name]
|
||||
if callable(field.translate):
|
||||
@@ -3447,17 +3457,26 @@ Fields:
|
||||
# synchronize translated terms when possible.
|
||||
self.env['ir.translation']._sync_terms_translations(field, self)
|
||||
|
||||
elif has_translation and field.translate:
|
||||
elif self.env.lang and field.translate:
|
||||
# The translated value of a field has been modified.
|
||||
src_trans = self.read([name])[0][name]
|
||||
src_trans = self.with_context(lang=None).read([name])[0][name]
|
||||
if not src_trans:
|
||||
# Insert value to DB
|
||||
src_trans = vals[name]
|
||||
self.with_context(lang=None).write({name: src_trans})
|
||||
tname = "%s,%s" % (self._name, name)
|
||||
val = field.convert_to_column(vals[name], self, vals)
|
||||
self.env['ir.translation']._set_ids(
|
||||
tname, 'model', self.env.lang, self.ids, val, src_trans)
|
||||
translation_values += [dict(
|
||||
src=src_trans,
|
||||
value=val,
|
||||
name=tname,
|
||||
lang=self.env.lang,
|
||||
type='model',
|
||||
state='translated',
|
||||
res_id=res_id) for res_id in self.ids]
|
||||
|
||||
if translation_values:
|
||||
process_translations(translation_values)
|
||||
|
||||
# mark fields to recompute; do this before setting other fields, because
|
||||
# the latter can require the value of computed fields, e.g., a one2many
|
||||
@@ -4283,11 +4302,12 @@ Fields:
|
||||
vals_list = []
|
||||
for vals in Translation.search_read(domain):
|
||||
del vals['id']
|
||||
del vals['source'] # remove source to avoid triggering _set_src
|
||||
del vals['module'] # duplicated vals is not linked to any module
|
||||
vals['res_id'] = target_id
|
||||
if not callable(field.translate):
|
||||
vals['src'] = new_wo_lang[name]
|
||||
if vals['lang'] == old.env.lang and field.translate is True:
|
||||
# force a source if the new_val was not changed by copy override
|
||||
# update master record if the new_val was not changed by copy override
|
||||
if new_val == old[name]:
|
||||
new_wo_lang[name] = old_wo_lang[name]
|
||||
vals['src'] = old_wo_lang[name]
|
||||
|
||||
Reference in New Issue
Block a user