[IMP] translations: synchronize terms when English value of field is modified

Also adapt the insertion of missing translations when clicking on the translation icon.
This commit is contained in:
Raphael Collet
2015-07-06 16:58:03 +02:00
parent 8ddfb1888b
commit 2ba2cbca5f
2 changed files with 172 additions and 75 deletions
+147 -54
View File
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from difflib import get_close_matches
import logging
from openerp import api, tools
import openerp.modules
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.exceptions import UserError
from openerp.exceptions import AccessError, UserError
_logger = logging.getLogger(__name__)
@@ -172,16 +173,18 @@ class ir_translation(osv.osv):
context = {}
res = dict.fromkeys(ids, False)
for record in self.browse(cr, uid, ids, context=context):
if record.type != 'model':
res[record.id] = record.src
else:
model_name, field = record.name.split(',')
res[record.id] = record.src
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
if model is not None:
if model is None:
continue
field = model._fields[field_name]
if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
context_no_lang = dict(context, lang=None)
result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang)
res[record.id] = result[0][field] if result else False
result = model.read(cr, uid, [record.res_id], [field_name], context=context_no_lang)
res[record.id] = result[0][field_name] if result else False
return res
def _set_src(self, cr, uid, id, name, value, args, context=None):
@@ -191,15 +194,18 @@ class ir_translation(osv.osv):
if context is None:
context = {}
record = self.browse(cr, uid, id, context=context)
if record.type == 'model':
model_name, field = record.name.split(',')
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
#We need to take the context without the language information, because we want to write on the
#value store in db and not on the one associate with current language.
#Also not removing lang from context trigger an error when lang is different
context_wo_lang = context.copy()
context_wo_lang.pop('lang', None)
model.write(cr, uid, [record.res_id], {field: value}, context=context_wo_lang)
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.
context_wo_lang = context.copy()
context_wo_lang.pop('lang', None)
model.write(cr, uid, [record.res_id], {field_name: value}, context=context_wo_lang)
return self.write(cr, uid, id, {'src': value}, context=context)
_columns = {
@@ -394,6 +400,49 @@ class ir_translation(osv.osv):
result[res_id][src] = map_trans(trans_id, src, value)
return result
@api.model
def _sync_terms_translations(self, field, records):
""" Synchronize the translations to the terms to translate, after the
English value of a field is modified. The algorithm tries to match
existing translations to the terms to translate, provided the distance
between modified strings is not too large. It allows to not retranslate
data where a typo has been fixed in the English value.
"""
if not callable(getattr(field, 'translate', None)):
return
trans = self.env['ir.translation']
outdated = trans
discarded = trans
for record in records:
# get field value and terms to translate
value = record[field.name]
terms = set(field.get_trans_terms(value))
record_trans = trans.search([
('type', '=', 'model'),
('name', '=', "%s,%s" % (field.model_name, field.name)),
('res_id', '=', record.id),
])
if not terms:
# discard all translations for that field
discarded += record_trans
continue
# remap existing translations on terms when possible
for trans in record_trans:
if trans.src not in terms:
matches = get_close_matches(trans.src, terms, 1, 0.9)
if matches:
trans.write({'src': matches[0], 'state': trans.state})
else:
outdated += trans
# process outdated and discarded translations
outdated.write({'state': 'to_translate'})
discarded.unlink()
@api.model
@tools.ormcache_context('model_name', keys=('lang',))
def get_field_string(self, model_name):
@@ -431,10 +480,10 @@ class ir_translation(osv.osv):
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
if vals.get('src') or ('value' in vals and not(vals.get('value'))):
vals.update({'state':'to_translate'})
if vals.get('value'):
vals.update({'state':'translated'})
vals.setdefault('state', 'translated')
elif vals.get('src') or not vals.get('value', True):
vals.setdefault('state', 'to_translate')
result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
self.clear_caches()
self.pool['ir.ui.view'].clear_cache()
@@ -450,38 +499,81 @@ class ir_translation(osv.osv):
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
return result
def translate_fields(self, cr, uid, model, id, field=None, context=None):
trans_model = self.pool[model]
domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
langs_ids = self.pool.get('res.lang').search(cr, uid, [('code', '!=', 'en_US')], context=context)
if not langs_ids:
raise UserError(_("Translation features are unavailable until you install an extra Odoo translation."))
langs = [lg.code for lg in self.pool.get('res.lang').browse(cr, uid, langs_ids, context=context)]
@api.model
def insert_missing(self, field, records):
""" Insert missing translations for `field` on `records`. """
records = records.with_context(lang=None)
if callable(field.translate):
# insert missing translations for each term in src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
FROM res_lang l
WHERE l.code != 'en_US' AND NOT EXISTS (
SELECT 1 FROM ir_translation
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s
);
"""
for record in records:
src = record[field.name] or None
for term in set(field.get_trans_terms(src)):
self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id,
'src': term,
})
else:
# insert missing translations for src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
FROM res_lang l
WHERE l.code != 'en_US' AND NOT EXISTS (
SELECT 1 FROM ir_translation
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s
);
UPDATE ir_translation SET src=%(src)s
WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s;
"""
for record in records:
self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id,
'src': record[field.name] or None,
})
self.clear_caches()
@api.model
def translate_fields(self, model, id, field=None):
""" Open a view for translating the field(s) of the record (model, id). """
main_lang = 'en_US'
translatable_fields = []
for k, f in trans_model._fields.items():
if getattr(f, 'translate', False):
if f.inherited:
parent_id = trans_model.read(cr, uid, [id], [f.related[0]], context=context)[0][f.related[0]][0]
translatable_fields.append({'name': k, 'id': parent_id, 'model': f.base_field.model_name})
domain.insert(0, '|')
domain.extend(['&', ('res_id', '=', parent_id), ('name', '=', "%s,%s" % (f.base_field.model_name, k))])
else:
translatable_fields.append({'name': k, 'id': id, 'model': model })
if len(langs):
fields = [f.get('name') for f in translatable_fields]
record = trans_model.read(cr, uid, [id], fields, context={ 'lang': main_lang })[0]
for lg in langs:
for f in translatable_fields:
# Check if record exists, else create it (at once)
sql = """INSERT INTO ir_translation (lang, src, name, type, res_id, value)
SELECT %s, %s, %s, 'model', %s, %s WHERE NOT EXISTS
(SELECT 1 FROM ir_translation WHERE lang=%s AND name=%s AND res_id=%s AND type='model');
UPDATE ir_translation SET src = %s WHERE lang=%s AND name=%s AND res_id=%s AND type='model';
"""
src = record[f['name']] or None
name = "%s,%s" % (f['model'], f['name'])
cr.execute(sql, (lg, src , name, f['id'], src, lg, name, f['id'], src, lg, name, id))
if not self.env['res.lang'].search_count([('code', '!=', main_lang)]):
raise UserError(_("Translation features are unavailable until you install an extra translation."))
# determine domain for selecting translations
record = self.env[model].with_context(lang=main_lang).browse(id)
domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
def make_domain(fld, rec):
name = "%s,%s" % (fld.model_name, fld.name)
return ['&', ('res_id', '=', rec.id), ('name', '=', name)]
# insert missing translations, and extend domain for related fields
for name, fld in record._fields.items():
if not getattr(fld, 'translate', False):
continue
rec = record
if fld.related:
try:
# traverse related fields up to their data source
while fld.related:
rec, fld = fld.traverse_related(rec)
if rec:
domain = ['|'] + domain + make_domain(fld, rec)
except AccessError:
continue
assert fld.translate and rec._name == fld.model_name
self.insert_missing(fld, rec)
action = {
'name': 'Translate',
@@ -492,10 +584,11 @@ class ir_translation(osv.osv):
'domain': domain,
}
if field:
f = trans_model._fields[field]
action['context'] = {
'search_default_name': "%s,%s" % (f.base_field.model_name, field)
}
fld = record._fields[field]
if not fld.related:
action['context'] = {
'search_default_name': "%s,%s" % (fld.model_name, fld.name),
}
return action
def _get_import_cursor(self, cr, uid, context=None):
+25 -21
View File
@@ -3816,7 +3816,12 @@ class BaseModel(object):
if hasattr(column, 'selection') and vals[field]:
self._check_selection_field_value(cr, user, field, vals[field], context=context)
if column._classic_write and not hasattr(column, '_fnct_inv'):
if not (totranslate and column.translate):
if totranslate and column.translate:
# vals[field] is a translation: do not update the table
if callable(column.translate):
flabel = ffield.get_description(recs.env)['string']
raise UserError(_("You cannot update the translated value of field '%s'") % flabel)
else:
updates.append((field, '%s', column._symbol_set[1](vals[field])))
direct.append(field)
else:
@@ -3841,26 +3846,25 @@ class BaseModel(object):
if cr.rowcount != len(sub_ids):
raise MissingError(_('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
if totranslate:
# TODO: optimize
for f in direct:
if callable(self._columns[f].translate):
# We do not handle the update of a field where translate
# is a callable. One should update the value without a
# language, or update translated terms separately.
raise ValidationError(_(
"The translation of the field '%s' cannot be "
"directly updated. You can either update its value "
"in English, or update translated terms separately."
) % self._fields[f].string)
elif self._columns[f].translate:
src_trans = self.pool[self._name].read(cr, user, ids, [f])[0][f]
if not src_trans:
src_trans = vals[f]
# Inserting value to DB
context_wo_lang = dict(context, lang=None)
self.write(cr, user, ids, {f: vals[f]}, context=context_wo_lang)
self.pool.get('ir.translation')._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
# TODO: optimize
for f in direct:
column = self._columns[f]
if callable(column.translate):
# The English value of a field has been modified,
# synchronize translated terms when possible.
assert not totranslate
self.pool['ir.translation']._sync_terms_translations(
cr, user, self._fields[f], recs, context=context)
elif column.translate and totranslate:
# The translated value of a field has been modified.
src_trans = self.pool[self._name].read(cr, user, ids, [f])[0][f]
if not src_trans:
# Insert value to DB
src_trans = vals[f]
context_wo_lang = dict(context, lang=None)
self.write(cr, user, ids, {f: vals[f]}, context=context_wo_lang)
self.pool['ir.translation']._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
# invalidate and mark new-style fields to recompute; do this before
# setting other fields, because it can require the value of computed