summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@openerp.com>2015-06-24 10:27:31 +0200
committerRaphael Collet <rco@openerp.com>2015-07-06 16:58:03 +0200
commit2ba2cbca5f539219dc0e97f2ee71bcb6a0fe1952 (patch)
treefa52d4c87553b18ceb31a135b9ef160177d59184
parent8ddfb1888b9f5ef20f00f2a4c9d5fe3a16c319cf (diff)
[IMP] translations: synchronize terms when English value of field is modified
Also adapt the insertion of missing translations when clicking on the translation icon.
-rw-r--r--openerp/addons/base/ir/ir_translation.py201
-rw-r--r--openerp/models.py46
2 files changed, 172 insertions, 75 deletions
diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py
index 46502a2f358..ea18178d57a 100644
--- a/openerp/addons/base/ir/ir_translation.py
+++ b/openerp/addons/base/ir/ir_translation.py
@@ -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 = {
@@ -395,6 +401,49 @@ class ir_translation(osv.osv):
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):
""" Return the translation of fields strings in the context's language.
@@ -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):
diff --git a/openerp/models.py b/openerp/models.py
index 4a6f380feba..6955ff46207 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -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