Files
tryton/trytond/ir/translation.py
T
Cédric Krier 39bcde5357 Use a separate cursor in loop to set translation
cursor_dict returns an iterator that consumes the cursor. This means the cursor
can not be used before the iteration is stopped. So we need to use another
cursor to make the update queries.
2016-03-17 17:14:53 +01:00

1626 lines
63 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import zipfile
import polib
import xml.dom.minidom
from difflib import SequenceMatcher
import os
from hashlib import md5
from lxml import etree
from itertools import izip
from io import BytesIO
from sql import Column, Null
from sql.functions import Substring, Position
from sql.conditionals import Case
from sql.operators import Or, And
from sql.aggregate import Max
from ..model import ModelView, ModelSQL, fields, Unique
from ..wizard import Wizard, StateView, StateTransition, StateAction, \
Button
from ..tools import file_open, reduce_ids, grouped_slice, cursor_dict
from .. import backend
from ..pyson import PYSONEncoder, Eval
from ..transaction import Transaction
from ..pool import Pool
from ..cache import Cache
from ..config import config
__all__ = ['Translation',
'TranslationSetStart', 'TranslationSetSucceed', 'TranslationSet',
'TranslationCleanStart', 'TranslationCleanSucceed', 'TranslationClean',
'TranslationUpdateStart', 'TranslationUpdate',
'TranslationExportStart', 'TranslationExportResult', 'TranslationExport',
]
TRANSLATION_TYPE = [
('field', 'Field'),
('model', 'Model'),
('odt', 'ODT'),
('selection', 'Selection'),
('view', 'View'),
('wizard_button', 'Wizard Button'),
('help', 'Help'),
('error', 'Error'),
]
class TrytonPOFile(polib.POFile):
def sort(self):
return super(TrytonPOFile, self).sort(
key=lambda x: (x.msgctxt, x.msgid))
class Translation(ModelSQL, ModelView):
"Translation"
__name__ = "ir.translation"
name = fields.Char('Field Name', required=True)
res_id = fields.Integer('Resource ID', select=True, required=True)
lang = fields.Selection('get_language', string='Language')
type = fields.Selection(TRANSLATION_TYPE, string='Type',
required=True)
src = fields.Text('Source')
src_md5 = fields.Char('Source MD5', size=32, required=True)
value = fields.Text('Translation Value')
module = fields.Char('Module', readonly=True)
fuzzy = fields.Boolean('Fuzzy')
model = fields.Function(fields.Char('Model'), 'get_model',
searcher='search_model')
overriding_module = fields.Char('Overriding Module', readonly=True)
_translation_cache = Cache('ir.translation', size_limit=10240,
context=False)
_get_language_cache = Cache('ir.translation')
@classmethod
def __setup__(cls):
super(Translation, cls).__setup__()
t = cls.__table__()
cls._sql_constraints += [
('translation_md5_uniq',
Unique(t,
t.name, t.res_id, t.lang, t.type, t.src_md5, t.module),
'Translation must be unique'),
]
cls._error_messages.update({
'translation_overridden': (
"You can not export translation %(name)s because it is an "
"overridden translation by module %(overriding_module)s"),
})
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
transaction = Transaction()
cursor = transaction.connection.cursor()
ir_translation = cls.__table__()
table = TableHandler(cls, module_name)
# Migration from 1.8: new field src_md5
src_md5_exist = table.column_exist('src_md5')
if not src_md5_exist:
table.add_raw_column('src_md5',
cls.src_md5.sql_type(),
cls.src_md5.sql_format, None,
cls.src_md5.size, string=cls.src_md5.string)
table.drop_constraint('translation_uniq')
table.index_action(['lang', 'type', 'name', 'src'], 'remove')
super(Translation, cls).__register__(module_name)
# Migration from 1.8: fill new field src_md5
if not src_md5_exist:
offset = 0
limit = transaction.database.IN_MAX
translations = True
while translations:
translations = cls.search([], offset=offset, limit=limit)
offset += limit
for translation in translations:
src_md5 = cls.get_src_md5(translation.src)
cls.write([translation], {
'src_md5': src_md5,
})
table = TableHandler(cls, module_name)
table.not_null_action('src_md5', action='add')
# Migration from 2.2 and 2.8
cursor.execute(*ir_translation.update([ir_translation.res_id],
[-1], where=(ir_translation.res_id == Null)
| (ir_translation.res_id == 0)))
table = TableHandler(cls, module_name)
table.index_action(['lang', 'type', 'name'], 'add')
@classmethod
def register_model(cls, model, module_name):
cursor = Transaction().connection.cursor()
ir_translation = cls.__table__()
if not model.__doc__:
return
name = model.__name__ + ',name'
src = model._get_name()
cursor.execute(*ir_translation.select(ir_translation.id,
where=(ir_translation.lang == 'en_US')
& (ir_translation.type == 'model')
& (ir_translation.name == name)
# Keep searching on all values for migration
& ((ir_translation.res_id == -1)
| (ir_translation.res_id == Null)
| (ir_translation.res_id == 0))))
trans_id = None
if cursor.rowcount == -1 or cursor.rowcount is None:
data = cursor.fetchone()
if data:
trans_id, = data
elif cursor.rowcount != 0:
trans_id, = cursor.fetchone()
src_md5 = Translation.get_src_md5(src)
if trans_id is None:
cursor.execute(*ir_translation.insert(
[Column(ir_translation, c)
for c in ('name', 'lang', 'type', 'src', 'src_md5',
'value', 'module', 'fuzzy', 'res_id')],
[[name, 'en_US', 'model', src, src_md5, '',
module_name, False, -1]]))
else:
cursor.execute(*ir_translation.update(
[ir_translation.src, ir_translation.src_md5],
[src, src_md5],
where=ir_translation.id == trans_id))
@classmethod
def register_fields(cls, model, module_name):
cursor = Transaction().connection.cursor()
ir_translation = cls.__table__()
# Prefetch field translations
trans_fields = {}
trans_help = {}
trans_selection = {}
if model._fields:
names = ['%s,%s' % (model.__name__, f) for f in model._fields]
cursor.execute(*ir_translation.select(ir_translation.id,
ir_translation.name, ir_translation.src,
ir_translation.type,
where=((ir_translation.lang == 'en_US')
& ir_translation.type.in_(
('field', 'help', 'selection'))
& ir_translation.name.in_(names))))
for trans in cursor_dict(cursor):
if trans['type'] == 'field':
trans_fields[trans['name']] = trans
elif trans['type'] == 'help':
trans_help[trans['name']] = trans
elif trans['type'] == 'selection':
trans_selection.setdefault(trans['name'], {})
trans_selection[trans['name']][trans['src']] = trans
def update_insert_field(field, trans_name):
string_md5 = cls.get_src_md5(field.string)
if trans_name not in trans_fields:
cursor.execute(*ir_translation.insert(
[ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src,
ir_translation.src_md5, ir_translation.value,
ir_translation.module, ir_translation.fuzzy,
ir_translation.res_id],
[[trans_name, 'en_US', 'field', field.string,
string_md5, '', module_name, False, -1]]))
elif trans_fields[trans_name]['src'] != field.string:
cursor.execute(*ir_translation.update(
[ir_translation.src, ir_translation.src_md5],
[field.string, string_md5],
where=ir_translation.id ==
trans_fields[trans_name]['id']))
def update_insert_help(field, trans_name):
help_md5 = cls.get_src_md5(field.help)
if trans_name not in trans_help:
if field.help:
cursor.execute(*ir_translation.insert(
[ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src,
ir_translation.src_md5, ir_translation.value,
ir_translation.module, ir_translation.fuzzy,
ir_translation.res_id],
[[trans_name, 'en_US', 'help', field.help,
help_md5, '', module_name, False, -1]]))
elif trans_help[trans_name]['src'] != field.help:
cursor.execute(*ir_translation.update(
[ir_translation.src, ir_translation.src_md5],
[field.help, help_md5],
where=ir_translation.id ==
trans_help[trans_name]['id']))
def insert_selection(field, trans_name):
for (_, val) in field.selection:
if (trans_name not in trans_selection
or val not in trans_selection[trans_name]):
val_md5 = cls.get_src_md5(val)
cursor.execute(*ir_translation.insert(
[ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src,
ir_translation.src_md5, ir_translation.value,
ir_translation.module, ir_translation.fuzzy,
ir_translation.res_id],
[[trans_name, 'en_US', 'selection', val, val_md5,
'', module_name, False, -1]]))
for field_name, field in model._fields.iteritems():
trans_name = model.__name__ + ',' + field_name
update_insert_field(field, trans_name)
update_insert_help(field, trans_name)
if (hasattr(field, 'selection')
and isinstance(field.selection, (tuple, list))
and getattr(field, 'translate_selection', True)):
insert_selection(field, trans_name)
@classmethod
def register_error_messages(cls, model, module_name):
cursor = Transaction().connection.cursor()
ir_translation = cls.__table__()
cursor.execute(*ir_translation.select(
ir_translation.id, ir_translation.src,
where=((ir_translation.lang == 'en_US')
& (ir_translation.type == 'error')
& (ir_translation.name == model.__name__))))
trans_error = {t['src']: t for t in cursor_dict(cursor)}
errors = model._get_error_messages()
for error in set(errors):
if error not in trans_error:
error_md5 = Translation.get_src_md5(error)
cursor.execute(*ir_translation.insert(
[ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src,
ir_translation.src_md5, ir_translation.value,
ir_translation.module, ir_translation.fuzzy,
ir_translation.res_id],
[[model.__name__, 'en_US', 'error', error, error_md5,
'', module_name, False, -1]]))
@classmethod
def register_wizard(cls, wizard, module_name):
cursor = Transaction().connection.cursor()
ir_translation = cls.__table__()
# Prefetch button translations
cursor.execute(*ir_translation.select(
ir_translation.id, ir_translation.name, ir_translation.src,
where=((ir_translation.lang == 'en_US')
& (ir_translation.type == 'wizard_button')
& (ir_translation.name.like(wizard.__name__ + ',%')))))
trans_buttons = {t['name']: t for t in cursor_dict(cursor)}
def update_insert_button(state_name, button):
trans_name = '%s,%s,%s' % (
wizard.__name__, state_name, button.state)
src_md5 = cls.get_src_md5(button.string)
if trans_name not in trans_buttons:
cursor.execute(*ir_translation.insert(
[ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src,
ir_translation.src_md5, ir_translation.value,
ir_translation.module, ir_translation.fuzzy,
ir_translation.res_id],
[[trans_name, 'en_US', 'wizard_button', button.string,
src_md5, '', module_name, False, -1]]))
elif trans_buttons[trans_name] != button.string:
cursor.execute(*ir_translation.update(
[ir_translation.src, ir_translation.src_md5],
[button.string, src_md5],
where=ir_translation.id ==
trans_buttons[trans_name]['id']))
for state_name, state in wizard.states.iteritems():
if not isinstance(state, StateView):
continue
for button in state.buttons:
update_insert_button(state_name, button)
@staticmethod
def default_fuzzy():
return False
@staticmethod
def default_res_id():
return -1
def get_model(self, name):
return self.name.split(',')[0]
@classmethod
def search_rec_name(cls, name, clause):
clause = tuple(clause)
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('src',) + clause[1:],
('value',) + clause[1:],
(cls._rec_name,) + clause[1:],
]
@classmethod
def search_model(cls, name, clause):
table = cls.__table__()
_, operator, value = clause
Operator = fields.SQL_OPERATORS[operator]
return [('id', 'in', table.select(table.id,
where=Operator(Substring(table.name, 1,
Case((Position(',', table.name) > 0,
Position(',', table.name) - 1),
else_=0)), value)))]
@classmethod
def get_language(cls):
result = cls._get_language_cache.get(None)
if result is not None:
return result
pool = Pool()
Lang = pool.get('ir.lang')
langs = Lang.search([])
result = [(lang.code, lang.name) for lang in langs]
cls._get_language_cache.set(None, result)
return result
@classmethod
def get_src_md5(cls, src):
return md5((src or '').encode('utf-8')).hexdigest()
@classmethod
def view_attributes(cls):
return [('/form//field[@name="value"]', 'spell', Eval('lang'))]
@classmethod
def get_ids(cls, name, ttype, lang, ids):
"Return translation for each id"
pool = Pool()
ModelFields = pool.get('ir.model.field')
Model = pool.get('ir.model')
translations, to_fetch = {}, []
name = unicode(name)
ttype = unicode(ttype)
lang = unicode(lang)
if name.split(',')[0] in ('ir.model.field', 'ir.model'):
field_name = name.split(',')[1]
with Transaction().set_context(_check_access=False):
if name.split(',')[0] == 'ir.model.field':
if field_name == 'field_description':
ttype = u'field'
else:
ttype = u'help'
records = ModelFields.browse(ids)
else:
ttype = u'model'
records = Model.browse(ids)
trans_args = []
for record in records:
if ttype in ('field', 'help'):
name = record.model.model + ',' + record.name
else:
name = record.model + ',' + field_name
trans_args.append((name, ttype, lang, None))
cls.get_sources(trans_args)
for record in records:
if ttype in ('field', 'help'):
name = record.model.model + ',' + record.name
else:
name = record.model + ',' + field_name
translations[record.id] = cls.get_source(name, ttype, lang)
return translations
# Don't use cache for fuzzy translation
if not Transaction().context.get(
'fuzzy_translation', False):
for obj_id in ids:
trans = cls._translation_cache.get((lang, ttype, name, obj_id),
-1)
if trans != -1:
translations[obj_id] = trans
else:
to_fetch.append(obj_id)
else:
to_fetch = ids
if to_fetch:
transaction = Transaction()
cursor = transaction.connection.cursor()
table = cls.__table__()
fuzzy_sql = table.fuzzy == False
if Transaction().context.get('fuzzy_translation', False):
fuzzy_sql = None
in_max = transaction.database.IN_MAX // 7
for sub_to_fetch in grouped_slice(to_fetch, in_max):
red_sql = reduce_ids(table.res_id, sub_to_fetch)
where = And(((table.lang == lang),
(table.type == ttype),
(table.name == name),
(table.value != ''),
(table.value != Null),
red_sql,
))
if fuzzy_sql:
where &= fuzzy_sql
cursor.execute(*table.select(table.res_id, table.value,
where=where))
for res_id, value in cursor.fetchall():
# Don't store fuzzy translation in cache
if not Transaction().context.get(
'fuzzy_translation', False):
cls._translation_cache.set((lang, ttype, name, res_id),
value)
translations[res_id] = value
for res_id in ids:
if res_id not in translations:
cls._translation_cache.set((lang, ttype, name, res_id), False)
translations[res_id] = False
return translations
@classmethod
def set_ids(cls, name, ttype, lang, ids, values):
"Set translation for each id"
pool = Pool()
ModelFields = pool.get('ir.model.field')
Model = pool.get('ir.model')
Config = pool.get('ir.configuration')
transaction = Transaction()
in_max = transaction.database.IN_MAX
if len(ids) > in_max:
for i in range(0, len(ids), in_max):
sub_ids = ids[i:i + in_max]
sub_values = values[i:i + in_max]
cls.set_ids(name, ttype, lang, sub_ids, sub_values)
return
model_name, field_name = name.split(',')
if model_name in ('ir.model.field', 'ir.model'):
if model_name == 'ir.model.field':
if field_name == 'field_description':
ttype = 'field'
else:
ttype = 'help'
with Transaction().set_context(language='en_US'):
records = ModelFields.browse(ids)
else:
ttype = 'model'
with Transaction().set_context(language='en_US'):
records = Model.browse(ids)
def get_name(record):
if ttype in ('field', 'help'):
return record.model.model + ',' + record.name
else:
return record.model + ',' + field_name
translations = {}
for translation in cls.search([
('lang', '=', lang),
('type', '=', ttype),
('name', 'in', [get_name(r) for r in records]),
]):
translations[translation.name] = translation
to_create = []
for record, value in izip(records, values):
translation = translations.get(get_name(record))
if not translation:
to_create.append({
'name': name,
'lang': lang,
'type': ttype,
'src': getattr(record, field_name),
'value': value,
'fuzzy': False,
})
else:
with Transaction().set_context(_check_access=False):
cls.write([translation], {
'src': getattr(record, field_name),
'value': value,
'fuzzy': False,
})
if to_create:
with Transaction().set_context(_check_access=False):
cls.create(to_create)
return
Model = pool.get(model_name)
with Transaction().set_context(language=Config.get_language()):
records = Model.browse(ids)
translations = {}
for translation in cls.search([
('lang', '=', lang),
('type', '=', ttype),
('name', '=', name),
('res_id', 'in', ids),
]):
translations[translation.res_id] = translation
other_translations = {}
if (lang == Config.get_language()
and Transaction().context.get('fuzzy_translation', True)):
for translation in cls.search([
('lang', '!=', lang),
('type', '=', ttype),
('name', '=', name),
('res_id', 'in', ids),
]):
other_translations.setdefault(translation.res_id, []
).append(translation)
to_create = []
for record, value in izip(records, values):
translation = translations.get(record.id)
if not translation:
to_create.append({
'name': name,
'lang': lang,
'type': ttype,
'res_id': record.id,
'value': value,
'src': getattr(record, field_name),
'fuzzy': False,
})
else:
with Transaction().set_context(_check_access=False):
cls.write([translation], {
'value': value,
'src': getattr(record, field_name),
'fuzzy': False,
})
other_langs = other_translations.get(record.id)
if other_langs:
cls.write(other_langs, {
'src': getattr(record, field_name),
'fuzzy': True,
})
if to_create:
with Transaction().set_context(_check_access=False):
cls.create(to_create)
@classmethod
def delete_ids(cls, model, ttype, ids):
"Delete translation for each id"
translations = []
for sub_ids in grouped_slice(ids):
translations += cls.search([
('type', '=', ttype),
('name', 'like', model + ',%'),
('res_id', 'in', list(sub_ids)),
])
with Transaction().set_context(_check_access=False):
cls.delete(translations)
@classmethod
def get_source(cls, name, ttype, lang, source=None):
"Return translation for source"
name = unicode(name)
ttype = unicode(ttype)
lang = unicode(lang)
if source is not None:
source = unicode(source)
trans = cls._translation_cache.get((lang, ttype, name, source), -1)
if trans != -1:
return trans
cursor = Transaction().connection.cursor()
table = cls.__table__()
where = ((table.lang == lang)
& (table.type == ttype)
& (table.name == name)
& (table.value != '')
& (table.value != Null)
& (table.fuzzy == False)
& (table.res_id == -1))
if source is not None:
where &= table.src == source
cursor.execute(*table.select(table.value, where=where))
res = cursor.fetchone()
if res:
cls._translation_cache.set((lang, ttype, name, source), res[0])
return res[0]
else:
cls._translation_cache.set((lang, ttype, name, source), False)
return None
@classmethod
def get_sources(cls, args):
'''
Take a list of (name, ttype, lang, source).
Add the translations to the cache.
Return a dict with the translations.
'''
res = {}
clause = []
transaction = Transaction()
cursor = transaction.connection.cursor()
table = cls.__table__()
if len(args) > transaction.database.IN_MAX:
for sub_args in grouped_slice(args):
res.update(cls.get_sources(list(sub_args)))
return res
for name, ttype, lang, source in args:
name = unicode(name)
ttype = unicode(ttype)
lang = unicode(lang)
if source is not None:
source = unicode(source)
trans = cls._translation_cache.get((lang, ttype, name, source), -1)
if trans != -1:
res[(name, ttype, lang, source)] = trans
else:
res[(name, ttype, lang, source)] = None
cls._translation_cache.set((lang, ttype, name, source), None)
where = And(((table.lang == lang),
(table.type == ttype),
(table.name == name),
(table.value != ''),
(table.value != Null),
(table.fuzzy == False),
(table.res_id == -1),
))
if source is not None:
where &= table.src == source
clause.append(where)
if clause:
in_max = transaction.database.IN_MAX // 7
for sub_clause in grouped_slice(clause, in_max):
cursor.execute(*table.select(
table.lang, table.type, table.name, table.src,
table.value,
where=Or(list(sub_clause))))
for lang, ttype, name, source, value in cursor.fetchall():
if (name, ttype, lang, source) not in args:
source = None
res[(name, ttype, lang, source)] = value
cls._translation_cache.set((lang, ttype, name, source),
value)
return res
@classmethod
def delete(cls, translations):
cls._translation_cache.clear()
ModelView._fields_view_get_cache.clear()
return super(Translation, cls).delete(translations)
@classmethod
def create(cls, vlist):
cls._translation_cache.clear()
ModelView._fields_view_get_cache.clear()
vlist = [x.copy() for x in vlist]
cursor = Transaction().connection.cursor()
table = cls.__table__()
for vals in vlist:
if not vals.get('module'):
if Transaction().context.get('module'):
vals['module'] = Transaction().context['module']
elif vals.get('type', '') in ('odt', 'view', 'wizard_button',
'selection', 'error'):
cursor.execute(*table.select(table.module,
where=(table.name == vals.get('name') or '')
& (table.res_id == vals.get('res_id') or -1)
& (table.lang == 'en_US')
& (table.type == vals.get('type') or '')
& (table.src == vals.get('src') or '')))
fetchone = cursor.fetchone()
if fetchone:
vals['module'] = fetchone[0]
else:
cursor.execute(*table.select(table.module, table.src,
where=(table.name == vals.get('name') or '')
& (table.res_id == vals.get('res_id') or -1)
& (table.lang == 'en_US')
& (table.type == vals.get('type') or '')))
fetchone = cursor.fetchone()
if fetchone:
vals['module'], vals['src'] = fetchone
vals['src_md5'] = cls.get_src_md5(vals.get('src'))
return super(Translation, cls).create(vlist)
@classmethod
def write(cls, translations, values, *args):
cls._translation_cache.clear()
ModelView._fields_view_get_cache.clear()
actions = iter((translations, values) + args)
args = []
for translations, values in zip(actions, actions):
if 'src' in values:
values = values.copy()
values['src_md5'] = cls.get_src_md5(values.get('src'))
args.extend((translations, values))
return super(Translation, cls).write(*args)
@classmethod
def extra_model_data(cls, model_data):
"Yield extra model linked to the model data"
if model_data.model in (
'ir.action.report',
'ir.action.act_window',
'ir.action.wizard',
'ir.action.url',
):
yield 'ir.action'
@property
def unique_key(self):
if self.type in ('odt', 'view', 'wizard_button', 'selection', 'error'):
return (self.name, self.res_id, self.type, self.src)
elif self.type in ('field', 'model', 'help'):
return (self.name, self.res_id, self.type)
@classmethod
def from_poentry(cls, entry):
'Returns a translation instance for a entry of pofile and its res_id'
ttype, name, res_id = entry.msgctxt.split(':')
src = entry.msgid
value = entry.msgstr
fuzzy = 'fuzzy' in entry.flags
translation = cls(name=name, type=ttype, src=src, fuzzy=fuzzy,
value=value)
return translation, res_id
@classmethod
def translation_import(cls, lang, module, po_path):
pool = Pool()
ModelData = pool.get('ir.model.data')
models_data = ModelData.search([
('module', '=', module),
])
fs_id2prop = {}
for model_data in models_data:
fs_id2prop.setdefault(model_data.model, {})
fs_id2prop[model_data.model][model_data.fs_id] = \
(model_data.db_id, model_data.noupdate)
for extra_model in cls.extra_model_data(model_data):
fs_id2prop.setdefault(extra_model, {})
fs_id2prop[extra_model][model_data.fs_id] = \
(model_data.db_id, model_data.noupdate)
translations = set()
to_save = []
pofile = polib.pofile(po_path)
id2translation = {}
key2ids = {}
module_translations = cls.search([
('lang', '=', lang),
('module', '=', module),
], order=[])
for translation in module_translations:
key = translation.unique_key
if not key:
raise ValueError('Unknow translation type: %s' %
translation.type)
key2ids.setdefault(key, []).append(translation.id)
if len(module_translations) <= config.getint('cache', 'record'):
id2translation[translation.id] = translation
def override_translation(ressource_id, new_translation):
res_id_module, res_id = ressource_id.split('.')
if res_id:
model_data, = ModelData.search([
('module', '=', res_id_module),
('fs_id', '=', res_id),
])
res_id = model_data.db_id
else:
res_id = -1
with Transaction().set_context(module=res_id_module):
domain = [
('name', '=', new_translation.name),
('res_id', '=', res_id),
('lang', '=', new_translation.lang),
('type', '=', new_translation.type),
('module', '=', res_id_module),
]
if new_translation.type in ('odt', 'view', 'wizard_button',
'selection', 'error'):
domain.append(('src', '=', new_translation.src))
translation, = cls.search(domain)
if translation.value != new_translation.value:
translation.value = new_translation.value
translation.overriding_module = module
translation.fuzzy = new_translation.fuzzy
translation.save()
# Make a first loop to retreive translation ids in the right order to
# get better read locality and a full usage of the cache.
translation_ids = []
if len(module_translations) <= config.getint('cache', 'record'):
processes = (True,)
else:
processes = (False, True)
for processing in processes:
if (processing
and len(module_translations) > config.getint('cache',
'record')):
id2translation = dict((t.id, t)
for t in cls.browse(translation_ids))
for entry in pofile:
if entry.obsolete:
continue
translation, res_id = cls.from_poentry(entry)
translation.lang = lang
translation.module = module
noupdate = False
if '.' in res_id:
override_translation(res_id, translation)
continue
model = translation.name.split(',')[0]
if (model in fs_id2prop
and res_id in fs_id2prop[model]):
res_id, noupdate = fs_id2prop[model][res_id]
if res_id:
try:
res_id = int(res_id)
except ValueError:
res_id = None
if not res_id:
res_id = -1
translation.res_id = res_id
key = translation.unique_key
if not key:
raise ValueError('Unknow translation type: %s' %
translation.type)
ids = key2ids.get(key, [])
if not processing:
translation_ids.extend(ids)
continue
if not ids:
to_save.append(translation)
else:
for translation_id in ids:
old_translation = id2translation[translation_id]
if not noupdate:
old_translation.value = translation.value
old_translation.fuzzy = translation.fuzzy
to_save.append(old_translation)
else:
translations.add(old_translation)
cls.save(to_save)
translations |= set(to_save)
if translations:
all_translations = set(cls.search([
('module', '=', module),
('lang', '=', lang),
]))
translations_to_delete = all_translations - translations
cls.delete(list(translations_to_delete))
return len(translations)
@classmethod
def translation_export(cls, lang, module):
pool = Pool()
ModelData = pool.get('ir.model.data')
Config = pool.get('ir.configuration')
models_data = ModelData.search([
('module', '=', module),
])
db_id2fs_id = {}
for model_data in models_data:
db_id2fs_id.setdefault(model_data.model, {})
db_id2fs_id[model_data.model][model_data.db_id] = model_data.fs_id
for extra_model in cls.extra_model_data(model_data):
db_id2fs_id.setdefault(extra_model, {})
db_id2fs_id[extra_model][model_data.db_id] = model_data.fs_id
pofile = TrytonPOFile(wrapwidth=78)
pofile.metadata = {
'Content-Type': 'text/plain; charset=utf-8',
}
with Transaction().set_context(language=Config.get_language()):
translations = cls.search([
('lang', '=', lang),
('module', '=', module),
], order=[])
for translation in translations:
if (translation.overriding_module
and translation.overriding_module != module):
cls.raise_user_error('translation_overridden', {
'name': translation.name,
'name': translation.overriding_module,
})
flags = [] if not translation.fuzzy else ['fuzzy']
trans_ctxt = '%(type)s:%(name)s:' % {
'type': translation.type,
'name': translation.name,
}
res_id = translation.res_id
if res_id >= 0:
model, _ = translation.name.split(',')
if model in db_id2fs_id:
res_id = db_id2fs_id[model].get(res_id)
else:
continue
trans_ctxt += '%s' % res_id
entry = polib.POEntry(msgid=(translation.src or ''),
msgstr=(translation.value or ''), msgctxt=trans_ctxt,
flags=flags)
pofile.append(entry)
if pofile:
pofile.sort()
return unicode(pofile).encode('utf-8')
else:
return
class TranslationSetStart(ModelView):
"Set Translation"
__name__ = 'ir.translation.set.start'
class TranslationSetSucceed(ModelView):
"Set Translation"
__name__ = 'ir.translation.set.succeed'
class TranslationSet(Wizard):
"Set Translation"
__name__ = "ir.translation.set"
start = StateView('ir.translation.set.start',
'ir.translation_set_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Set', 'set_', 'tryton-ok', default=True),
])
set_ = StateTransition()
succeed = StateView('ir.translation.set.succeed',
'ir.translation_set_succeed_view_form', [
Button('OK', 'end', 'tryton-ok', default=True),
])
def _translate_report(self, node):
strings = []
if node.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
if node.parentNode \
and node.parentNode.tagName in ('text:placeholder',
'text:page-number', 'text:page-count'):
return strings
if node.nodeValue:
txt = node.nodeValue.strip()
if txt:
strings.append(txt)
for child in [x for x in node.childNodes]:
strings.extend(self._translate_report(child))
return strings
def set_report(self):
pool = Pool()
Report = pool.get('ir.action.report')
Translation = pool.get('ir.translation')
with Transaction().set_context(active_test=False):
reports = Report.search([])
if not reports:
return
cursor = Transaction().connection.cursor()
translation = Translation.__table__()
for report in reports:
cursor.execute(*translation.select(
translation.id, translation.name, translation.src,
where=(translation.lang == 'en_US')
& (translation.type == 'odt')
& (translation.name == report.report_name)
& (translation.module == report.module or '')))
trans_reports = {t['src']: t for t in cursor_dict(cursor)}
strings = []
odt_content = ''
if report.report:
with file_open(report.report.replace('/', os.sep),
mode='rb') as fp:
odt_content = fp.read()
for content in (report.report_content_custom, odt_content):
if not content:
continue
content_io = BytesIO(content)
try:
content_z = zipfile.ZipFile(content_io, mode='r')
except zipfile.BadZipfile:
continue
content_xml = content_z.read('content.xml')
document = xml.dom.minidom.parseString(content_xml)
strings = self._translate_report(document.documentElement)
style_xml = content_z.read('styles.xml')
document = xml.dom.minidom.parseString(style_xml)
strings += self._translate_report(document.documentElement)
for string in {}.fromkeys(strings).keys():
src_md5 = Translation.get_src_md5(string)
done = False
if string in trans_reports:
del trans_reports[string]
continue
for string_trans in trans_reports:
if string_trans in strings:
continue
seqmatch = SequenceMatcher(lambda x: x == ' ',
string, string_trans)
if seqmatch.ratio() == 1.0:
del trans_reports[report.report_name][string_trans]
done = True
break
if seqmatch.ratio() > 0.6:
cursor.execute(*translation.update(
[translation.src, translation.fuzzy,
translation.src_md5],
[string, True, src_md5],
where=(translation.name == report.report_name)
& (translation.type == 'odt')
& (translation.src == string_trans)
& (translation.module == report.module)))
del trans_reports[string_trans]
done = True
break
if not done:
cursor.execute(*translation.insert(
[translation.name, translation.lang,
translation.type, translation.src,
translation.value, translation.module,
translation.fuzzy, translation.src_md5,
translation.res_id],
[[report.report_name, 'en_US', 'odt', string, '',
report.module, False, src_md5, -1]]))
if strings:
cursor.execute(*translation.delete(
where=(translation.name == report.report_name)
& (translation.type == 'odt')
& (translation.module == report.module)
& ~translation.src.in_(strings)))
def _translate_view(self, element):
strings = []
for attr in ('string', 'sum', 'confirm', 'help'):
if element.get(attr):
string = element.get(attr)
if string:
strings.append(string)
for child in element:
strings.extend(self._translate_view(child))
return strings
def set_view(self):
pool = Pool()
View = pool.get('ir.ui.view')
Translation = pool.get('ir.translation')
with Transaction().set_context(active_test=False):
views = View.search([])
if not views:
return
cursor = Transaction().connection.cursor()
translation = Translation.__table__()
for view in views:
cursor.execute(*translation.select(
translation.id, translation.name, translation.src,
where=(translation.lang == 'en_US')
& (translation.type == 'view')
& (translation.name == view.model)
& (translation.module == view.module)))
trans_views = {t['src']: t for t in cursor_dict(cursor)}
xml = (view.arch or '').strip()
if not xml:
continue
tree = etree.fromstring(xml)
root_element = tree.getroottree().getroot()
strings = self._translate_view(root_element)
with Transaction().set_context(active_test=False):
views2 = View.search([
('model', '=', view.model),
('id', '!=', view.id),
('module', '=', view.module),
])
for view2 in views2:
xml2 = view2.arch.strip()
if not xml2:
continue
tree2 = etree.fromstring(xml2)
root2_element = tree2.getroottree().getroot()
strings += self._translate_view(root2_element)
if not strings:
continue
for string in set(strings):
done = False
if string in trans_views:
del trans_views[string]
continue
string_md5 = Translation.get_src_md5(string)
for string_trans in trans_views:
if string_trans in strings:
continue
seqmatch = SequenceMatcher(lambda x: x == ' ',
string, string_trans)
if seqmatch.ratio() == 1.0:
del trans_views[string_trans]
done = True
break
if seqmatch.ratio() > 0.6:
cursor.execute(*translation.update(
[translation.src, translation.src_md5,
translation.fuzzy],
[string, string_md5, True],
where=(translation.id ==
trans_views[string_trans]['id'])))
del trans_views[string_trans]
done = True
break
if not done:
cursor.execute(*translation.insert(
[translation.name, translation.lang,
translation.type, translation.src,
translation.src_md5, translation.value,
translation.module, translation.fuzzy,
translation.res_id],
[[view.model, 'en_US', 'view', string, string_md5,
'', view.module, False, -1]]))
if strings:
cursor.execute(*translation.delete(
where=(translation.name == view.model)
& (translation.type == 'view')
& (translation.module == view.module)
& ~translation.src.in_(strings)))
def transition_set_(self):
self.set_report()
self.set_view()
return 'succeed'
class TranslationCleanStart(ModelView):
'Clean translation'
__name__ = 'ir.translation.clean.start'
class TranslationCleanSucceed(ModelView):
'Clean translation'
__name__ = 'ir.translation.clean.succeed'
class TranslationClean(Wizard):
"Clean translation"
__name__ = 'ir.translation.clean'
start = StateView('ir.translation.clean.start',
'ir.translation_clean_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Clean', 'clean', 'tryton-ok', default=True),
])
clean = StateTransition()
succeed = StateView('ir.translation.clean.succeed',
'ir.translation_clean_succeed_view_form', [
Button('OK', 'end', 'tryton-ok', default=True),
])
@staticmethod
def _clean_field(translation):
pool = Pool()
try:
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
try:
Model = pool.get(model_name)
except KeyError:
return True
if field_name not in Model._fields:
return True
@staticmethod
def _clean_model(translation):
pool = Pool()
try:
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
try:
Model = pool.get(model_name)
except KeyError:
return True
if translation.res_id >= 0:
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
if (not hasattr(field, 'translate')
or not field.translate):
return True
elif field_name not in ('name'):
return True
@staticmethod
def _clean_odt(translation):
pool = Pool()
Report = pool.get('ir.action.report')
with Transaction().set_context(active_test=False):
if not Report.search([
('report_name', '=', translation.name),
]):
return True
@staticmethod
def _clean_selection(translation):
pool = Pool()
try:
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
try:
Model = pool.get(model_name)
except KeyError:
return True
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
if (not hasattr(field, 'selection')
or not field.selection
or not getattr(field, 'translate_selection', True)):
return True
if (isinstance(field.selection, (tuple, list))
and translation.src not in dict(field.selection).values()):
return True
@staticmethod
def _clean_view(translation):
pool = Pool()
model_name = translation.name
try:
pool.get(model_name)
except KeyError:
return True
@staticmethod
def _clean_wizard_button(translation):
pool = Pool()
try:
wizard_name, state_name, button_name = \
translation.name.split(',', 2)
except ValueError:
return True
try:
Wizard = pool.get(wizard_name, type='wizard')
except KeyError:
return True
if not Wizard:
return True
state = Wizard.states.get(state_name)
if not state or not hasattr(state, 'buttons'):
return True
if button_name in [b.state for b in state.buttons]:
return False
return True
@staticmethod
def _clean_help(translation):
pool = Pool()
try:
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
try:
Model = pool.get(model_name)
except KeyError:
return True
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
return not field.help
@staticmethod
def _clean_error(translation):
pool = Pool()
model_name = translation.name
if model_name in (
'delete_xml_record',
'xml_record_desc',
'write_xml_record',
'relation_not_found',
'too_many_relations_found',
'xml_id_syntax_error',
'reference_syntax_error',
'domain_validation_record',
'required_validation_record',
'size_validation_record',
'digits_validation_record',
'selection_validation_record',
'time_format_validation_record',
'access_error',
'read_error',
'write_error',
'required_field',
'foreign_model_missing',
'foreign_model_exist',
'search_function_missing',
'selection_value_notfound',
'recursion_error',
):
return False
Model, Wizard = None, None
try:
Model = pool.get(model_name)
except KeyError:
try:
Wizard = pool.get(model_name, type='wizard')
except KeyError:
pass
if Model:
errors = Model._error_messages.values()
if issubclass(Model, ModelSQL):
errors += Model._sql_error_messages.values()
for _, _, error in Model._sql_constraints:
errors.append(error)
if translation.src not in errors:
return True
elif Wizard:
errors = Wizard._error_messages.values()
if translation.src not in errors:
return True
else:
return True
def transition_clean(self):
pool = Pool()
Translation = pool.get('ir.translation')
ModelData = pool.get('ir.model.data')
to_delete = []
keys = set()
translations = Translation.search([])
for translation in translations:
if getattr(self, '_clean_%s' % translation.type)(translation):
to_delete.append(translation.id)
elif translation.type in ('field', 'model', 'wizard_button',
'help'):
key = (translation.module, translation.lang, translation.type,
translation.name, translation.res_id)
if key in keys:
to_delete.append(translation.id)
else:
keys.add(key)
# skip translation handled in ir.model.data
models_data = ModelData.search([
('db_id', 'in', to_delete),
('model', '=', 'ir.translation'),
])
for mdata in models_data:
if mdata.db_id in to_delete:
to_delete.remove(mdata.db_id)
Translation.delete(Translation.browse(to_delete))
return 'succeed'
class TranslationUpdateStart(ModelView):
"Update translation"
__name__ = 'ir.translation.update.start'
language = fields.Many2One('ir.lang', 'Language', required=True,
domain=[('translatable', '=', True)])
@staticmethod
def default_language():
Lang = Pool().get('ir.lang')
code = Transaction().context.get('language', False)
try:
lang, = Lang.search([
('code', '=', code),
('translatable', '=', True),
], limit=1)
return lang.id
except ValueError:
return None
class TranslationUpdate(Wizard):
"Update translation"
__name__ = "ir.translation.update"
_source_types = ['odt', 'view', 'wizard_button', 'selection', 'error']
_ressource_types = ['field', 'model', 'help']
_updatable_types = ['field', 'model', 'selection', 'help']
start = StateView('ir.translation.update.start',
'ir.translation_update_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Update', 'update', 'tryton-ok', default=True),
])
update = StateAction('ir.act_translation_form')
@staticmethod
def transition_update():
return 'end'
def do_update(self, action):
pool = Pool()
Translation = pool.get('ir.translation')
cursor = Transaction().connection.cursor()
cursor_update = Transaction().connection.cursor()
translation = Translation.__table__()
lang = self.start.language.code
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.src.as_('src'), translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
& translation.type.in_(self._source_types))
- translation.select(*columns,
where=(translation.lang == lang)
& translation.type.in_(self._source_types))))
to_create = []
for row in cursor_dict(cursor):
to_create.append({
'name': row['name'],
'res_id': row['res_id'],
'lang': lang,
'type': row['type'],
'src': row['src'],
'module': row['module'],
})
if to_create:
Translation.create(to_create)
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
& translation.type.in_(self._ressource_types))
- translation.select(*columns,
where=(translation.lang == lang)
& translation.type.in_(self._ressource_types))))
to_create = []
for row in cursor_dict(cursor):
to_create.append({
'name': row['name'],
'res_id': row['res_id'],
'lang': lang,
'type': row['type'],
'module': row['module'],
})
if to_create:
Translation.create(to_create)
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.src.as_('src'), translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
& translation.type.in_(self._updatable_types))
- translation.select(*columns,
where=(translation.lang == lang)
& translation.type.in_(self._updatable_types))))
for row in cursor_dict(cursor):
cursor_update.execute(*translation.update(
[translation.fuzzy, translation.src],
[True, row['src']],
where=(translation.name == row['name'])
& (translation.type == row['type'])
& (translation.lang == lang)
& (translation.res_id == (row['res_id'] or -1))
& (translation.module == row['module'])))
cursor.execute(*translation.select(
translation.src.as_('src'),
Max(translation.value).as_('value'),
where=(translation.lang == lang)
& translation.src.in_(
translation.select(translation.src,
where=((translation.value == '')
| (translation.value == Null))
& (translation.lang == lang)))
& (translation.value != '')
& (translation.value != Null),
group_by=translation.src))
for row in cursor_dict(cursor):
cursor_update.execute(*translation.update(
[translation.fuzzy, translation.value],
[True, row['value']],
where=(translation.src == row['src'])
& ((translation.value == '') | (translation.value == Null))
& (translation.lang == lang)))
cursor_update.execute(*translation.update(
[translation.fuzzy],
[False],
where=((translation.value == '') | (translation.value == Null))
& (translation.lang == lang)))
action['pyson_domain'] = PYSONEncoder().encode([
('module', '!=', None),
('lang', '=', lang),
])
return action, {}
class TranslationExportStart(ModelView):
"Export translation"
__name__ = 'ir.translation.export.start'
language = fields.Many2One('ir.lang', 'Language', required=True,
domain=[
('translatable', '=', True),
('code', '!=', 'en_US'),
])
module = fields.Many2One('ir.module', 'Module', required=True,
domain=[
('state', 'in', ['installed', 'to upgrade', 'to remove']),
])
@classmethod
def default_language(cls):
Lang = Pool().get('ir.lang')
code = Transaction().context.get('language', False)
domain = [('code', '=', code)] + cls.language.domain
try:
lang, = Lang.search(domain, limit=1)
return lang.id
except ValueError:
return None
class TranslationExportResult(ModelView):
"Export translation"
__name__ = 'ir.translation.export.result'
file = fields.Binary('File', readonly=True)
class TranslationExport(Wizard):
"Export translation"
__name__ = "ir.translation.export"
start = StateView('ir.translation.export.start',
'ir.translation_export_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Export', 'export', 'tryton-ok', default=True),
])
export = StateTransition()
result = StateView('ir.translation.export.result',
'ir.translation_export_result_view_form', [
Button('Close', 'end', 'tryton-close'),
])
def transition_export(self):
pool = Pool()
Translation = pool.get('ir.translation')
self.result.file = Translation.translation_export(
self.start.language.code, self.start.module.name)
return 'result'
def default_result(self, fields):
file_ = self.result.file
self.result.file = False # No need to store it in session
return {
'file': file_,
}