Files
tradon/ir/translation.py
2025-12-26 13:11:43 +00:00

1727 lines
66 KiB
Python
Executable File

# 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 os
from collections import defaultdict
from difflib import SequenceMatcher
from io import BytesIO
import polib
from defusedxml.minidom import parseString
from genshi.filters.i18n import extract as genshi_extract
from lxml import etree
from relatorio.reporting import MIMETemplateLoader
from relatorio.templates.opendocument import get_zip_file
from sql import Column, Literal, Null
from sql.aggregate import Max
from sql.conditionals import Case
from sql.functions import Position, Substring
from trytond.cache import Cache
from trytond.config import config
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.model import Index, ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.pyson import Eval, PYSONEncoder
from trytond.tools import cursor_dict, file_open, grouped_slice
from trytond.tools.string_ import LazyString, StringPartitioned
from trytond.transaction import (
Transaction, inactive_records, without_check_access)
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
from .action import ACTION_SELECTION
from .lang import get_parent_language as get_parent
TRANSLATION_TYPE = [
('field', 'Field'),
('model', 'Model'),
('report', 'Report'),
('selection', 'Selection'),
('view', 'View'),
('wizard_button', 'Wizard Button'),
('help', 'Help'),
]
INTERNAL_LANG = 'en'
ACTION_MODELS = {'ir.action'} | dict(ACTION_SELECTION).keys()
class OverriddenError(UserError):
pass
class TrytonPOFile(polib.POFile):
def sort(self):
return super(TrytonPOFile, self).sort(
key=lambda x: (x.msgctxt, x.msgid))
class Translation(
fields.fmany2one(
'module_ref', 'module', 'ir.module,name', "Module",
readonly=True, ondelete='CASCADE'),
fields.fmany2one(
'overriding_module_ref', 'overriding_module', 'ir.module,name',
"Overriding Module", readonly=True),
ModelSQL, ModelView):
"Translation"
__name__ = "ir.translation"
name = fields.Char('Field Name', required=True)
res_id = fields.Integer('Resource ID', required=True)
lang = fields.Selection('get_language', string='Language')
type = fields.Selection(TRANSLATION_TYPE, string='Type',
required=True)
src = fields.Text('Source')
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', context=False)
_translation_report_cache = Cache(
'ir.translation.get_report', context=False)
_get_language_cache = Cache('ir.translation.get_language', context=False)
@classmethod
def __setup__(cls):
cls.name.search_unaccented = False
super().__setup__()
table = cls.__table__()
cls._sql_indexes.update({
Index(
table, (Index.Unaccent(table.src), Index.Similarity())),
Index(
table, (Index.Unaccent(table.value), Index.Similarity())),
Index(
table,
(table.type, Index.Equality()),
(table.name, Index.Equality()),
(table.lang, Index.Equality()),
(table.res_id, Index.Range()),
(table.fuzzy, Index.Equality()),
(Index.Unaccent(table.value), Index.Similarity())),
Index(
table,
(table.type, Index.Equality()),
(table.name, Index.Equality()),
(table.lang, Index.Equality()),
(table.fuzzy, Index.Equality())),
Index(
table,
(table.res_id, Index.Equality()),
(table.name, Index.Equality()),
(table.lang, Index.Equality()),
(table.type, Index.Equality())),
})
@classmethod
def __register__(cls, module_name):
table = cls.__table_handler__(module_name)
# Migration from 5.0: remove src_md5
if table.column_exist('src_md5'):
table.drop_constraint('translation_md5_uniq')
table.drop_column('src_md5')
super(Translation, cls).__register__(module_name)
@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()
if not src:
return
cursor.execute(*ir_translation.select(ir_translation.id,
where=(ir_translation.lang == INTERNAL_LANG)
& (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()
if trans_id is None:
cursor.execute(*ir_translation.insert(
[Column(ir_translation, c)
for c in ('name', 'lang', 'type', 'src',
'value', 'module', 'fuzzy', 'res_id')],
[[
name, INTERNAL_LANG, 'model', src,
'', module_name, False, -1]]))
else:
cursor.execute(*ir_translation.update(
[ir_translation.src],
[src],
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
translations = dict(
field=defaultdict(dict),
help=defaultdict(dict),
selection=defaultdict(dict))
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 == INTERNAL_LANG)
& ir_translation.type.in_(
('field', 'help', 'selection'))
& ir_translation.name.in_(names))))
for trans in cursor_dict(cursor):
sources = translations[trans['type']][trans['name']]
sources[trans['src']] = trans
columns = [ir_translation.name, ir_translation.lang,
ir_translation.type, ir_translation.src, ir_translation.value,
ir_translation.module, ir_translation.fuzzy, ir_translation.res_id]
def insert(field, type, name, string):
inserted = False
for val in string:
if not val or val in translations[type][name]:
continue
if isinstance(val, LazyString):
continue
assert type not in {'field', 'help'} or not inserted, (
"More than one resource "
f"for {type} of {name} in {module_name}")
cursor.execute(
*ir_translation.insert(columns,
[[
name, INTERNAL_LANG, type, val,
'', module_name, False, -1]]))
inserted = True
for field_name, field in model._fields.items():
name = model.__name__ + ',' + field_name
insert(field, 'field', name, field.string)
insert(field, 'help', name, field.help)
if (hasattr(field, 'selection')
and isinstance(field.selection, (tuple, list))
and getattr(field, 'translate_selection', True)):
selection = [s for _, s in field.selection]
insert(field, 'selection', name, selection)
@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 == INTERNAL_LANG)
& (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):
if not button.string:
return
trans_name = '%s,%s,%s' % (
wizard.__name__, state_name, button.state)
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.value, ir_translation.module,
ir_translation.fuzzy, ir_translation.res_id],
[[
trans_name, INTERNAL_LANG,
'wizard_button', button.string,
'', module_name,
False, -1]]))
elif trans_buttons[trans_name] != button.string:
cursor.execute(*ir_translation.update(
[ir_translation.src],
[button.string],
where=ir_translation.id
== trans_buttons[trans_name]['id']))
for state_name, state in wizard.states.items():
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):
language = Transaction().language
result = cls._get_language_cache.get(language)
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(language, result)
return result
@classmethod
def view_attributes(cls):
return [('/form//field[@name="value"]', 'spell', Eval('lang'))]
@classmethod
@without_check_access
def get_ids(cls, name, ttype, lang, ids, cached_after=None):
"Return translation for each id"
pool = Pool()
ModelFields = pool.get('ir.model.field')
Model = pool.get('ir.model')
context = Transaction().context
fuzzy_translation = context.get('fuzzy_translation', False)
translations, to_fetch = {}, []
name = str(name)
ttype = str(ttype)
lang = str(lang)
if name.split(',')[0] in ('ir.model.field', 'ir.model'):
field_name = name.split(',')[1]
if name.split(',')[0] == 'ir.model.field':
if field_name == 'field_description':
ttype = 'field'
else:
ttype = 'help'
records = ModelFields.browse(ids)
else:
ttype = 'model'
records = Model.browse(ids)
trans_args = []
for record in records:
if ttype in ('field', 'help'):
name = record.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 + ',' + record.name
else:
name = record.model + ',' + field_name
translations[record.id] = cls.get_source(name, ttype, lang)
if translations[record.id] is None:
with Transaction().set_context(language=lang):
if ttype in {'field', 'help'}:
try:
field = getattr(
pool.get(record.model), record.name)
except KeyError:
continue
translations[record.id] = ''
if ttype == 'field':
value = field.string
else:
value = field.help
else:
try:
model = pool.get(record.model)
except KeyError:
continue
if not model.__doc__:
continue
value = model._get_name()
if isinstance(value, StringPartitioned):
for source in value:
translations[record.id] += source
else:
translations[record.id] = value
return translations
# Don't use cache for fuzzy translation
if (not fuzzy_translation
and (not cached_after
or not cls._translation_cache.sync_since(cached_after))):
for obj_id in ids:
trans = cls._translation_cache.get((name, ttype, lang, obj_id),
-1)
if trans != -1:
translations[obj_id] = trans
else:
to_fetch.append(obj_id)
else:
to_fetch = ids
if to_fetch:
# Get parent translations
parent_lang = get_parent(lang)
if parent_lang:
translations.update(
cls.get_ids(name, ttype, parent_lang, to_fetch))
if fuzzy_translation:
fuzzy_clause = []
else:
fuzzy_clause = [('fuzzy', '=', False)]
for sub_to_fetch in grouped_slice(to_fetch):
for translation in cls.search([
('lang', '=', lang),
('type', '=', ttype),
('name', '=', name),
('value', '!=', ''),
('value', '!=', None),
('res_id', 'in', list(sub_to_fetch)),
] + fuzzy_clause):
translations[translation.res_id] = translation.value
# Don't store fuzzy translation in cache
if not fuzzy_translation:
for res_id in to_fetch:
value = translations.setdefault(res_id)
cls._translation_cache.set(
(name, ttype, lang, res_id), value)
return translations
@classmethod
@without_check_access
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=INTERNAL_LANG):
records = ModelFields.browse(ids)
else:
ttype = 'model'
with Transaction().set_context(language=INTERNAL_LANG):
records = Model.browse(ids)
def get_name(record):
if ttype in ('field', 'help'):
return record.model + ',' + record.name
else:
return record.model + ',' + field_name
name2translations = defaultdict(list)
for translation in cls.search([
('lang', '=', lang),
('type', '=', ttype),
('name', 'in', [get_name(r) for r in records]),
]):
name2translations[translation.name].append(translation)
to_save, to_delete = [], []
for record, value in zip(records, values):
translations = name2translations.get(get_name(record))
if lang == INTERNAL_LANG:
src = value
else:
src = getattr(record, field_name)
if not translations:
if not src and not value:
continue
translation = cls()
translation.name = name
translation.lang = lang
translation.type = ttype
translations.append(translation)
if not src and not value:
to_delete.extend(translations)
else:
for translation in translations:
translation.src = src
translation.value = value
translation.fuzzy = False
to_save.append(translation)
cls.save(to_save)
cls.delete(to_delete)
return
Model = pool.get(model_name)
with Transaction().set_context(language=Config.get_language()):
records = Model.browse(ids)
id2translations = defaultdict(list)
other_translations = defaultdict(list)
for translation in cls.search([
('lang', '=', lang),
('type', '=', ttype),
('name', '=', name),
('res_id', 'in', ids),
]):
id2translations[translation.res_id].append(translation)
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[translation.res_id].append(translation)
to_save, to_delete = [], []
for record, value in zip(records, values):
translations = id2translations[record.id]
if lang == Config.get_language():
src = value
else:
src = getattr(record, field_name)
if not translations:
if not src and not value:
continue
translation = cls()
translation.name = name
translation.lang = lang
translation.type = ttype
translation.res_id = record.id
translations.append(translation)
else:
other_langs = other_translations[record.id]
if not src and not value:
to_delete.extend(other_langs)
else:
for other_lang in other_langs:
other_lang.src = src
other_lang.fuzzy = True
to_save.append(other_lang)
if not src and not value:
to_delete.extend(translations)
else:
for translation in translations:
translation.value = value
translation.src = src
translation.fuzzy = False
to_save.append(translation)
cls.save(to_save)
cls.delete(to_delete)
@classmethod
@without_check_access
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)),
])
cls.delete(translations)
@classmethod
def get_source(cls, name, ttype, lang, source=None):
"Return translation for source"
args = (name, ttype, lang, source)
result = cls.get_sources([args])
return result[args]
@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 = {}
parent_args = []
parent_langs = []
clauses = []
transaction = Transaction()
if len(args) > transaction.database.IN_MAX:
for sub_args in grouped_slice(args):
res.update(cls.get_sources(list(sub_args)))
return res
to_cache = []
for name, ttype, lang, source in args:
name = str(name)
ttype = str(ttype)
lang = str(lang)
if source is not None:
source = str(source)
trans = cls._translation_cache.get((name, ttype, lang, source), -1)
if trans != -1:
res[(name, ttype, lang, source)] = trans
else:
to_cache.append((name, ttype, lang, source))
parent_lang = get_parent(lang)
if parent_lang:
parent_args.append((name, ttype, parent_lang, source))
parent_langs.append(lang)
res[(name, ttype, lang, source)] = None
clause = [
('lang', '=', lang),
('type', '=', ttype),
('name', '=', name),
('value', '!=', ''),
('value', '!=', None),
('fuzzy', '=', False),
('res_id', '=', -1),
]
if source is not None:
clause.append(('src', '=', source))
clauses.append(clause)
# Get parent transactions
if parent_args:
parent_src = cls.get_sources(parent_args)
for (name, ttype, parent_lang, source), lang in zip(
parent_args, parent_langs):
res[(name, ttype, lang, source)] = parent_src[
(name, ttype, parent_lang, source)]
in_max = transaction.database.IN_MAX // 7
for sub_clause in grouped_slice(clauses, in_max):
for translation in cls.search(['OR'] + list(sub_clause)):
key = (translation.name, translation.type,
translation.lang, translation.src)
if key not in args:
key = key[:-1] + (None,)
res[key] = translation.value
for key in to_cache:
cls._translation_cache.set(key, res[key])
return res
@classmethod
def get_report(cls, report_name, text):
language = Transaction().language
key = (report_name, language)
if cls._translation_report_cache.get(key) is None:
cache = {}
code = language
while code:
translations = cls.search([
('lang', '=', code),
('type', '=', 'report'),
('name', '=', report_name),
('value', '!=', ''),
('value', '!=', None),
('fuzzy', '=', False),
('res_id', '=', -1),
], order=[('module', 'DESC')])
for translation in translations:
cache.setdefault(translation.src, translation.value)
code = get_parent(code)
cls._translation_report_cache.set(key, cache)
return cls._translation_report_cache.get(key, {}).get(text, text)
@classmethod
def copy(cls, translations, default=None):
default = default.copy() if default is not None else {}
default.setdefault('module')
default.setdefault('overriding_module')
return super().copy(translations, default=default)
@classmethod
def delete(cls, translations):
cls.__clear_cache_for(translations)
return super().delete(translations)
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
for vals in vlist:
if not vals.get('module'):
if Transaction().context.get('module'):
vals['module'] = Transaction().context['module']
translations = super().create(vlist)
cls.__clear_cache_for(translations)
return translations
@classmethod
def write(cls, *args):
super().write(*args)
translations = sum(args[0:None:2], [])
cls.__clear_cache_for(translations)
@classmethod
def __clear_cache_for(cls, translations):
cls._translation_cache.clear()
cls._translation_report_cache.clear()
types = {t.type for t in translations}
models = {t.model for t in translations}
cls._clear_cache_for(types, models)
@classmethod
def _clear_cache_for(cls, types, models):
pool = Pool()
if 'field' in types and 'ir.message' in models:
pool.get('ir.message')._message_cache.clear()
if 'field' in types and 'ir.model' in models:
pool.get('ir.model')._get_names_cache.clear()
if 'field' in types and 'ir.model.field' in models:
pool.get('ir.model.field')._get_name_cache.clear()
if 'field' in types and ACTION_MODELS & models:
pool.get('ir.action.keyword')._get_keyword_cache.clear()
ModelView._view_toolbar_get_cache.clear()
if 'field' in types and {'ir.export', 'ir.email.template'} & models:
ModelView._view_toolbar_get_cache.clear()
if 'view' in types:
ModelView._fields_view_get_cache.clear()
@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 == 'model':
return (self.name, self.res_id, self.type)
return (self.name, self.res_id, self.type, self.src)
@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')
if isinstance(po_path, str):
po_path = [po_path]
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 = []
id2translation = {}
key2ids = {}
module_translations = cls.search([
('lang', '=', lang),
('module', '=', module),
], order=[])
for translation in module_translations:
# Migration from 5.0: ignore error type
if translation.type == 'error':
continue
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 {
'report', 'view', 'wizard_button', 'selection'}:
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
return translation
# 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 pofile in po_path:
for entry in polib.pofile(pofile):
if entry.obsolete:
continue
translation, res_id = cls.from_poentry(entry)
# Migration from 5.0: ignore error type
if translation.type == 'error':
continue
translation.lang = lang
translation.module = module
noupdate = False
if '.' in res_id:
to_save.append(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([_f for _f in to_save if _f])
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):
raise OverriddenError(
gettext('ir.msg_translation_overridden',
name=translation.name,
overriding_module=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)
if entry.msgid or entry.msgstr:
pofile.append(entry)
if pofile:
pofile.sort()
return str(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 extract_report_opendocument(self, content):
def extract(node):
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
if node.nodeValue:
txt = node.nodeValue.strip()
if txt:
yield txt
for child in [x for x in node.childNodes]:
for string in extract(child):
yield string
zip_ = get_zip_file(BytesIO(content))
for content_xml in [
zip_.read('content.xml'),
zip_.read('styles.xml'),
]:
document = parseString(content_xml)
for string in extract(document.documentElement):
yield string
extract_report_odt = extract_report_opendocument
extract_report_odp = extract_report_opendocument
extract_report_ods = extract_report_opendocument
extract_report_odg = extract_report_opendocument
def extract_report_genshi(template_class):
def method(self, content,
keywords=None, comment_tags=None, **options):
options['template_class'] = template_class
content = BytesIO(content)
if keywords is None:
keywords = []
if comment_tags is None:
comment_tags = []
for _, _, string, _ in genshi_extract(
content, keywords, comment_tags, options):
if string:
yield string
if not template_class:
raise ValueError('a template class is required')
return method
factories = MIMETemplateLoader().factories
extract_report_txt = extract_report_genshi(factories['text'])
extract_report_xml = extract_report_genshi(
factories.get('markup', factories.get('xml')))
extract_report_html = extract_report_genshi(
factories.get('markup', factories.get('xml')))
extract_report_xhtml = extract_report_genshi(
factories.get('markup', factories.get('xml')))
del factories
@inactive_records
def set_report(self):
pool = Pool()
Report = pool.get('ir.action.report')
Translation = pool.get('ir.translation')
if self.model == Report:
reports = self.records
elif not self.model or self.model.__name__ == 'ir.ui.menu':
reports = Report.search([('translatable', '=', True)])
else:
return
cursor = Transaction().connection.cursor()
translation = Translation.__table__()
report_strings = defaultdict(set)
for report in reports:
content = None
if report.report:
with file_open(report.report.replace('/', os.sep),
mode='rb') as fp:
content = fp.read()
for content, module in [
(report.report_content_custom, None),
(content, report.module)]:
if not content:
continue
cursor.execute(*translation.select(
translation.id, translation.name, translation.src,
where=(translation.lang == INTERNAL_LANG)
& (translation.type == 'report')
& (translation.name == report.report_name)
& (translation.module == module)))
trans_reports = {t['src']: t for t in cursor_dict(cursor)}
strings = report_strings[report.report_name, report.module]
func_name = 'extract_report_%s' % report.template_extension
strings.update(getattr(self, func_name)(content))
for string in strings:
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],
[string, True],
where=(
translation.name == report.report_name)
& (translation.type == 'report')
& (translation.src == string_trans)
& (translation.module == 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.res_id],
[[
report.report_name, INTERNAL_LANG,
'report', string,
'', module,
False, -1]]))
for (report_name, module), strings in report_strings.items():
query = translation.delete(
where=(translation.name == report_name)
& (translation.type == 'report')
& (translation.module == module))
if strings:
query.where &= ~translation.src.in_(list(strings))
cursor.execute(*query)
def _translate_view(self, element):
strings = []
for attr in ['string', '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
@inactive_records
def set_view(self):
pool = Pool()
View = pool.get('ir.ui.view')
Translation = pool.get('ir.translation')
if self.model == View:
views = self.records
elif not self.model or self.model.__name__ == 'ir.ui.menu':
views = View.search([])
else:
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 == INTERNAL_LANG)
& (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)
views2 = View.search([
('model', '=', view.model),
('id', '!=', view.id),
('module', '=', view.module),
])
for view2 in views2:
xml2 = view2.arch
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
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.fuzzy],
[string, 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.value, translation.module,
translation.fuzzy, translation.res_id],
[[
view.model, INTERNAL_LANG,
'view', string,
'', 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
field = Model._fields.get(field_name)
if not field:
return True
if translation.src not in list(field.string):
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
@inactive_records
def _clean_report(translation):
pool = Pool()
Report = pool.get('ir.action.report')
if not Report.search([
('report_name', '=', translation.name),
('translatable', '=', True),
]):
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
field = Model._fields.get(field_name)
if not field:
return True
if not field.help:
return True
if translation.src not in list(field.help):
return True
def transition_clean(self):
pool = Pool()
Translation = pool.get('ir.translation')
ModelData = pool.get('ir.model.data')
to_delete = []
records = defaultdict(lambda: defaultdict(set))
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)
if translation.type == 'model' and translation.res_id >= 0:
model_name, _ = translation.name.split(',', 1)
records[model_name][translation.res_id].add(translation.id)
with inactive_records():
for model_name, translations in records.items():
Model = pool.get(model_name)
for sub_ids in grouped_slice(translations.keys()):
sub_ids = list(sub_ids)
records = Model.search([('id', 'in', sub_ids)])
for record_id in set(sub_ids) - set(map(int, records)):
to_delete.extend(translations[record_id])
# 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')
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 = ['report', 'view', 'wizard_button', 'selection']
_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()
Config = pool.get('ir.configuration')
Translation = pool.get('ir.translation')
Report = pool.get('ir.action.report')
View = pool.get('ir.ui.view')
cursor = Transaction().connection.cursor()
cursor_update = Transaction().connection.cursor()
translation = Translation.__table__()
lang = self.start.language.code
parent_lang = get_parent(lang)
if self.model == Report:
reports = self.records
source_clause = ((translation.type == 'report')
& translation.name.in_([r.report_name for r in reports]))
elif self.model == View:
views = self.records
source_clause = ((translation.type == 'view')
& translation.name.in_([v.model for v in views]))
else:
source_clause = Literal(True)
default_lang = Config.get_language()
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 == default_lang)
& source_clause
& translation.type.in_(self._source_types))
- translation.select(*columns,
where=(translation.lang == lang)
& source_clause
& 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)
if parent_lang:
columns.append(translation.value)
cursor.execute(*(translation.select(*columns,
where=(translation.lang == parent_lang)
& source_clause
& translation.type.in_(self._source_types))
& translation.select(*columns,
where=(translation.lang == lang)
& source_clause
& translation.type.in_(self._source_types))))
for row in cursor_dict(cursor):
cursor_update.execute(*translation.update(
[translation.value],
[''],
where=(translation.name == row['name'])
& (translation.res_id == row['res_id'])
& (translation.type == row['type'])
& (translation.src == row['src'])
& (translation.module == row['module'])
& (translation.lang == lang)))
if self.model in {Report, View}:
return
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 == default_lang)
& 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)
if parent_lang:
columns.append(translation.value)
cursor.execute(*(translation.select(*columns,
where=(translation.lang == parent_lang)
& translation.type.in_(self._ressource_types))
& translation.select(*columns,
where=(translation.lang == lang)
& translation.type.in_(self._ressource_types))))
for row in cursor_dict(cursor):
cursor_update.execute(*translation.update(
[translation.value],
[''],
where=(translation.name == row['name'])
& (translation.res_id == row['res_id'])
& (translation.type == row['type'])
& (translation.module == row['module'])
& (translation.lang == lang)))
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 == default_lang)
& 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.src != '')
& (translation.src != Null)))
& (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', '!=', INTERNAL_LANG),
])
module = fields.Many2One('ir.module', 'Module', required=True,
domain=[
('state', 'in', ['activated', 'to upgrade', 'to remove']),
])
@classmethod
def default_language(cls):
Lang = Pool().get('ir.lang')
code = Transaction().context.get('language')
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'
language = fields.Many2One('ir.lang', 'Language', readonly=True)
module = fields.Many2One('ir.module', 'Module', readonly=True)
file = fields.Binary('File', readonly=True, filename='filename')
filename = fields.Char('Filename')
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
cast = self.result.__class__.file.cast
self.result.file = False # No need to store it in session
return {
'module': self.start.module.id,
'language': self.start.language.id,
'file': cast(file_) if file_ else None,
'filename': '%s.po' % self.start.language.code,
}
class TranslationReport(Wizard):
"Open translations of report"
__name__ = 'ir.translation.report'
start_state = 'open_'
open_ = StateAction('ir.act_translation_report')
def do_open_(self, action):
context = Transaction().context
reports = self.records
action['pyson_domain'] = PYSONEncoder().encode([
('type', '=', 'report'),
('name', 'in', [r.report_name for r in reports]),
])
# Behaves like a relate to have name suffix
action['keyword'] = 'form_relate'
return action, {
'model': context['active_model'],
'ids': context['active_ids'],
'id': context['active_id'],
}