[IMP] *: translation methods cleaning

Extract some cleaning wanted in #30759 but not related to language packs

- `amount_to_text` should work only on activated languages
- deprecate `load_lang` and rely on `_create_lang` or `_activate_lang`
- `trans_load_data` and `_load_module_terms` no longer silently activate languages
- pass explicitly parameters instead of relying on context content
- replace some `IrTranslation._load_module_terms(['base'], ['fr_FR'])` by `BaseModule._update_translations(['fr_FR'])` for higher level methods
- create `TranslationExporter` class to clarify the `trans_export` method (and clean dead code)

closes odoo/odoo#38859

Task-id: 2088290
Pad: https://pad.odoo.com/p/r.f6f789f8711d21314bd902972153ea9d
Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
Odoo's Mergebot
2019-11-19 14:58:29 +01:00
committed by GitHub
26 changed files with 322 additions and 239 deletions
@@ -368,7 +368,7 @@ class TestSMSComposerMass(test_mail_full_common.BaseFunctionalTest, sms_common.M
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
def test_composer_mass_active_ids_w_template_and_lang(self):
self.env.ref('base.lang_fr').write({'active': True})
self.env['res.lang']._activate_lang('fr_FR')
self.env['ir.translation'].create({
'type': 'model',
'name': 'sms.template,body',
@@ -425,7 +425,7 @@ class TestSMSComposerMass(test_mail_full_common.BaseFunctionalTest, sms_common.M
""" Test the context action from a SMS template (Add context action button)
and the usage with the sms composer """
# Create the lang info
self.env.ref('base.lang_fr').write({'active': True})
self.env['res.lang']._activate_lang('fr_FR')
self.env['ir.translation'].create({
'type': 'model',
'name': 'sms.template,body',
@@ -25,7 +25,7 @@ class TestSmsTemplate(test_mail_full_common.BaseFunctionalTest, sms_common.MockS
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
def test_sms_template_lang(self):
self.env.ref('base.lang_fr').write({'active': True})
self.env['res.lang']._activate_lang('fr_FR')
self.user_admin.write({'lang': 'en_US'})
self.env['ir.translation'].create({
'type': 'model',
+1 -2
View File
@@ -15,8 +15,7 @@ class TestUrlCommon(odoo.tests.HttpCase):
'domain': self.domain,
})
lang_fr = self.env.ref('base.lang_fr')
lang_fr.write({'active': True})
lang_fr = self.env['res.lang']._activate_lang('fr_FR')
self.website.language_ids = self.env.ref('base.lang_en') + lang_fr
self.website.default_lang_id = self.env.ref('base.lang_en')
+2 -2
View File
@@ -13,8 +13,8 @@ class TestLangUrl(HttpCase):
# Simulate multi lang without loading translations
self.website = self.env.ref('website.default_website')
lang_fr = self.env.ref('base.lang_fr')
lang_fr.write({'active': True, 'url_code': 'fr'})
lang_fr = self.env['res.lang']._activate_lang('fr_FR')
lang_fr.write({'url_code': 'fr'})
self.website.language_ids = self.env.ref('base.lang_en') + lang_fr
self.website.default_lang_id = self.env.ref('base.lang_en')
+1 -1
View File
@@ -73,7 +73,7 @@ class TestQwebProcessAtt(TransactionCase):
def setUp(self):
super(TestQwebProcessAtt, self).setUp()
self.website = self.env.ref('website.default_website')
self.env.ref('base.lang_fr').active = True
self.env['res.lang']._activate_lang('fr_FR')
self.website.language_ids = self.env.ref('base.lang_en') + self.env.ref('base.lang_fr')
self.website.default_lang_id = self.env.ref('base.lang_en')
self.website.cdn_activated = True
+2 -2
View File
@@ -873,7 +873,7 @@ class Module(models.Model):
cat_id = modules.db.create_categories(self._cr, categs)
self.write({'category_id': cat_id})
def _update_translations(self, filter_lang=None):
def _update_translations(self, filter_lang=None, overwrite=False):
if not filter_lang:
langs = self.env['res.lang'].get_installed()
filter_lang = [code for code, _ in langs]
@@ -886,7 +886,7 @@ class Module(models.Model):
for mod in update_mods
}
mod_names = topological_sort(mod_dict)
self.env['ir.translation']._load_module_terms(mod_names, filter_lang)
self.env['ir.translation']._load_module_terms(mod_names, filter_lang, overwrite)
def _check(self):
for module in self:
+13 -19
View File
@@ -27,15 +27,15 @@ class IrTranslationImport(object):
"""
_table = 'tmp_ir_translation_import'
def __init__(self, model):
def __init__(self, cr, overwrite=False):
""" Store some values, and also create a temporary SQL table to accept
the data.
:param model: the model to insert the data into (as a recordset)
"""
self._cr = model._cr
self._model_table = model._table
self._overwrite = model._context.get('overwrite', False)
self._cr = cr
self._model_table = "ir_translation"
self._overwrite = overwrite
self._debug = False
self._rows = []
@@ -774,24 +774,18 @@ class IrTranslation(models.Model):
return action
@api.model
def _get_import_cursor(self):
def _get_import_cursor(self, overwrite):
""" Return a cursor-like object for fast inserting translations """
return IrTranslationImport(self)
return IrTranslationImport(self._cr, overwrite)
def _load_module_terms(self, modules, langs):
def _load_module_terms(self, modules, langs, overwrite=False):
""" Load PO files of the given modules for the given languages. """
# make sure the given languages are active
res_lang = self.env['res.lang'].sudo()
for lang in langs:
res_lang.load_lang(lang)
# load i18n files
for module_name in modules:
modpath = get_module_path(module_name)
if not modpath:
continue
for lang in langs:
context = dict(self._context)
lang_code = tools.get_iso_codes(lang)
base_lang_code = None
if '_' in lang_code:
@@ -802,28 +796,28 @@ class IrTranslation(models.Model):
base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po')
if base_trans_file:
_logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(self._cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
tools.trans_load(self._cr, base_trans_file, lang, verbose=False, overwrite=overwrite)
overwrite = True # make sure the requested translation will override the base terms later
# i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
if base_trans_extra_file:
_logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, overwrite=overwrite)
overwrite = True # make sure the requested translation will override the base terms later
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language
trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file:
_logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(self._cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, trans_file, lang, verbose=False, overwrite=overwrite)
elif lang_code != 'en_US':
_logger.info('module %s: no translation for language %s', module_name, lang_code)
trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
if trans_extra_file:
_logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, overwrite=overwrite)
return True
@api.model
+1 -2
View File
@@ -110,8 +110,7 @@ class Currency(models.Model):
integer_value = int(parts[0])
fractional_value = int(parts[2] or 0)
lang_code = self.env.context.get('lang') or self.env.user.lang
lang = self.env['res.lang'].with_context(active_test=False).search([('code', '=', lang_code)])
lang = tools.get_lang(self.env)
amount_words = tools.ustr('{amt_value} {amt_word}').format(
amt_value=_num2words(integer_value, lang=lang.iso_code),
amt_word=self.currency_unit_label,
+21 -14
View File
@@ -88,15 +88,24 @@ class Lang(models.Model):
if not self.search_count([]):
_logger.error("No language is active.")
@api.model
# TODO remove me after v14
def load_lang(self, lang, lang_name=None):
""" Create the given language if necessary, and make it active. """
# if the language exists, simply make it active
language = self.with_context(active_test=False).search([('code', '=', lang)], limit=1)
if language:
language.write({'active': True})
return language.id
_logger.warning("Call to deprecated method load_lang, use _create_lang or _activate_lang instead")
language = self._activate_lang(lang) or self._create_lang(lang, lang_name)
return language.id
def _activate_lang(self, code):
""" Activate languages
:param code: code of the language to activate
:return: the language matching 'code' activated
"""
lang = self.with_context(active_test=False).search([('code', '=', code)])
if lang and not lang.active:
lang.active = True
return lang
def _create_lang(self, lang, lang_name=None):
""" Create the given language and make it active. """
# create the language with locale information
fail = True
iso_lang = tools.get_iso_codes(lang)
@@ -149,7 +158,7 @@ class Lang(models.Model):
'grouping' : str(conv.get('grouping', [])),
}
try:
return self.create(lang_info).id
return self.create(lang_info)
finally:
tools.resetlocale()
@@ -157,18 +166,16 @@ class Lang(models.Model):
def install_lang(self):
"""
This method is called from odoo/addons/base/base_data.xml to load
This method is called from odoo/addons/data/res_lang_data.xml to load
some language and set it as the default for every partners. The
language is set via tools.config by the RPC 'create' method on the
language is set via tools.config by the '_initialize_db' method on the
'db' object. This is a fragile solution and something else should be
found.
"""
# config['load_language'] is a comma-separated list or None
lang_code = (tools.config.get('load_language') or 'en_US').split(',')[0]
lang = self._lang_get(lang_code)
if not lang:
self.load_lang(lang_code)
lang = self._activate_lang(lang_code) or self._create_lang(lang_code)
IrDefault = self.env['ir.default']
default_value = IrDefault.get('res.partner', 'lang')
if default_value is None:
@@ -219,7 +226,7 @@ class Lang(models.Model):
active_lang = [lang.code for lang in self.filtered(lambda l: l.active)]
if active_lang:
mods = self.env['ir.module.module'].search([('state', '=', 'installed')])
mods._update_translations(filter_lang=active_lang)
mods._update_translations(active_lang)
@api.model_create_multi
def create(self, vals_list):
+3 -1
View File
@@ -672,11 +672,13 @@ class TestExpression(SavepointCaseWithUserDemo):
def test_lp1071710(self):
""" Check that we can exclude translated fields (bug lp:1071710) """
# first install french language
self.env['ir.translation']._load_module_terms(['base'], ['fr_FR'])
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.partner'].search([('name', '=', 'Pepper Street')]).country_id = self.env.ref('base.be')
# actual test
Country = self.env['res.country'].with_context(lang='fr_FR')
be = self.env.ref('base.be')
be.with_context(lang='fr_FR').name = "Belgique"
self.assertNotEqual(be.name, "Belgique", "Setting a translation should not impact other languages")
not_be = self._search(Country, [('name', '!=', 'Belgique')])
self.assertNotIn(be, not_be)
+2 -1
View File
@@ -220,7 +220,8 @@ class TestFormatLangDate(TransactionCase):
lang = self.env['res.lang']
# Activate French and Simplified Chinese (test with non-ASCII characters)
lang.search([('active', '=', False), ('code', 'in', ['fr_FR', 'zh_CN'])]).write({'active': True})
lang._activate_lang('fr_FR')
lang._activate_lang('zh_CN')
# -- test `date`
# Change a single parameter
+2 -4
View File
@@ -522,10 +522,8 @@ class TestQWebNS(TransactionCase):
current_lang = 'en_US'
other_lang = 'fr_FR'
self.env['res.lang'].load_lang(lang=other_lang)
self.env['res.lang']._lang_get(other_lang).write({
'active': True,
lang = self.env['res.lang']._activate_lang(other_lang)
lang.write({
'decimal_point': '*',
'thousands_sep': '/'
})
+10 -7
View File
@@ -247,7 +247,8 @@ class TestTranslation(TransactionCase):
def setUp(self):
super(TestTranslation, self).setUp()
self.env['ir.translation']._load_module_terms(['base'], ['fr_FR'])
lang = self.env['res.lang']._activate_lang('fr_FR')
self.env.ref('base.module_base')._update_translations(['fr_FR'])
self.customers = self.env['res.partner.category'].create({'name': 'Customers'})
self.env['ir.translation'].create({
'type': 'model',
@@ -436,7 +437,7 @@ class TestTranslationWrite(TransactionCase):
self.assertEqual(source_name[0]['name'], "English Name", "Reference field not updated")
def test_03_fr_single(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.users'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
self.env.ref('base.lang_en').active = False
@@ -454,7 +455,7 @@ class TestTranslationWrite(TransactionCase):
self.assertEqual(len(translations), 0, "No French translation should be created when writing in French")
def test_04_fr_multi(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
langs = self.env['res.lang'].get_installed()
self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs,
@@ -481,9 +482,9 @@ class TestTranslationWrite(TransactionCase):
])
def test_04_fr_multi_no_en(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.lang']._activate_lang('es_ES')
self.env['res.users'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
self.env['res.lang'].load_lang('es_ES')
self.env.ref('base.lang_en').active = False
langs = self.env['res.lang'].get_installed()
@@ -504,7 +505,7 @@ class TestTranslationWrite(TransactionCase):
])
def test_05_remove_multi(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
langs = self.env['res.lang'].get_installed()
self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs,
@@ -576,7 +577,9 @@ class TestTranslationWrite(TransactionCase):
class TestXMLTranslation(TransactionCase):
def setUp(self):
super(TestXMLTranslation, self).setUp()
self.env['ir.translation']._load_module_terms(['base'], ['fr_FR', 'nl_NL'])
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.lang']._activate_lang('nl_NL')
self.env.ref('base.module_base')._update_translations(['fr_FR', 'nl_NL'])
def create_view(self, archf, terms, **kwargs):
view = self.env['ir.ui.view'].create({
+1 -1
View File
@@ -606,7 +606,7 @@ class TestNoModel(ViewCase):
"""
Test if translations work correctly without a model
"""
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
ARCH = '<template name="foo">%s</template>'
TEXT_EN = "Copyright copyrighter"
TEXT_FR = u"Copyrighter, tous droits réservés"
@@ -28,7 +28,6 @@ class BaseLanguageImport(models.TransientModel):
def import_lang(self):
this = self[0]
this = this.with_context(overwrite=this.overwrite)
with TemporaryFile('wb+') as buf:
try:
buf.write(base64.decodebytes(this.data))
@@ -37,8 +36,14 @@ class BaseLanguageImport(models.TransientModel):
buf.seek(0)
fileformat = os.path.splitext(this.filename)[-1][1:].lower()
tools.trans_load_data(this._cr, buf, fileformat, this.code,
lang_name=this.name, context=this._context)
Lang = self.env["res.lang"]
lang = Lang._activate_lang(self.code) or Lang._create_lang(
self.code, lang_name=self.name
)
tools.trans_load_data(
this._cr, buf, fileformat, this.code, overwrite=self.overwrite
)
except Exception as e:
_logger.exception('File unsuccessfully imported, due to format mismatch.')
raise UserError(
@@ -33,7 +33,8 @@ class BaseLanguageInstall(models.TransientModel):
def lang_install(self):
self.ensure_one()
mods = self.env['ir.module.module'].search([('state', '=', 'installed')])
mods.with_context(overwrite=self.overwrite)._update_translations(self.lang)
self.env['res.lang']._activate_lang(self.lang)
mods._update_translations(self.lang, self.overwrite)
self.state = 'done'
self.env.cr.execute('ANALYZE ir_translation')
@@ -26,10 +26,7 @@ class BaseUpdateTranslations(models.TransientModel):
return lang.name
def act_update(self):
this = self[0]
lang_name = self._get_lang_name(this.lang)
with tempfile.NamedTemporaryFile() as buf:
tools.trans_export(this.lang, ['all'], buf, 'po', self._cr)
context = {'create_empty_translation': True}
tools.trans_load_data(self._cr, buf, 'po', this.lang, lang_name=lang_name, context=context)
tools.trans_export(self.lang, ['all'], buf, 'po', self._cr)
tools.trans_load_data(self._cr, buf, 'po', self.lang, create_empty_translation=True)
return {'type': 'ir.actions.act_window_close'}
@@ -33,7 +33,7 @@ class TestJavascriptAssetsBundle(FileTouchable):
super(TestJavascriptAssetsBundle, self).setUp()
self.jsbundle_xmlid = 'test_assetsbundle.bundle1'
self.cssbundle_xmlid = 'test_assetsbundle.bundle2'
self.env['res.lang'].load_lang('ar_SY')
self.env['res.lang']._activate_lang('ar_SY')
def _get_asset(self, xmlid, env=None):
@@ -291,7 +291,7 @@ class TestDurationExport(TestBasicExport):
def setUp(self):
super(TestDurationExport, self).setUp()
# needs to have lang installed otherwise falls back on en_US
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
def test_default_unit(self):
converter = self.get_converter('float', 'duration')
@@ -332,7 +332,7 @@ class TestRelativeDatetime(TestBasicExport):
def setUp(self):
super(TestRelativeDatetime, self).setUp()
# needs to have lang installed otherwise falls back on en_US
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
def test_basic(self):
converter = self.get_converter('datetime', 'relative')
+1 -1
View File
@@ -285,7 +285,7 @@ class test_selection(CreatorCase):
[[u"Bar"]])
def test_localized_export(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
ir_field = self.env['ir.model.fields']._get('export.selection', 'value')
selection = ir_field.selection_ids
translations = dict(self.translations_fr)
+1 -1
View File
@@ -73,7 +73,7 @@ class ImporterCase(common.TransactionCase):
return '__test__.' + name
def add_translations(self, name, type, code, *tnx):
self.env['res.lang'].load_lang(code)
self.env['res.lang']._activate_lang(code)
Translations = self.env['ir.translation']
for source, value in tnx:
Translations.create({
@@ -1412,7 +1412,7 @@ class TestFields(TransactionCaseWithUserDemo):
email = self.env.ref('test_new_api.emailmessage_0_0')
self.assertEqual(email.message, message)
self.env['res.lang'].load_lang('fr_FR')
self.env['res.lang']._activate_lang('fr_FR')
def count(msg):
# return the number of translations of msg.label
+2 -1
View File
@@ -27,7 +27,8 @@ class TestUiTranslation(odoo.tests.HttpCase):
@mute_logger('odoo.sql_db', 'odoo.http')
def test_01_sql_constraints(self):
# Raise an SQL constraint and test the message
self.env['ir.translation']._load_module_terms(['test_new_api'], ['fr_FR'])
self.env['res.lang']._activate_lang('fr_FR')
self.env.ref('base.module_test_new_api')._update_translations(['fr_FR'])
constraint = self.env.ref('test_new_api.constraint_test_new_api_category_positive_color')
message = constraint.with_context(lang='fr_FR').message
self.assertEqual(message, "La couleur doit être une valeur positive !")
@@ -18,7 +18,8 @@ class TestTermCount(common.TransactionCase):
"""
Just make sure we have as many translation entries as we wanted.
"""
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB'),
@@ -40,6 +41,7 @@ class TestTermCount(common.TransactionCase):
"""
Just make sure we have as many translation entries as we wanted and module deducted from file content
"""
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
translations = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
@@ -56,10 +58,11 @@ class TestTermCount(common.TransactionCase):
menu = self.env.ref('test_translation_import.menu_test_translation_import')
menu.name = "New Name"
# install french and change translation content
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
menu.with_context(lang='fr_FR').name = "Nouveau nom"
# reload with overwrite
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False, context={'overwrite': True})
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False, overwrite=True)
# trans_load invalidates ormcache but not record cache
menu.env.cache.invalidate()
@@ -67,8 +70,9 @@ class TestTermCount(common.TransactionCase):
self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom")
def test_lang_with_base(self):
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_BE', module_name='test_translation_import', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr_BE.po', 'fr_BE', module_name='test_translation_import', verbose=False, context={'overwrite': True})
self.env['res.lang']._activate_lang('fr_BE')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_BE', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr_BE.po', 'fr_BE', verbose=False, overwrite=True)
# language override base language
translations = self.env['ir.translation'].search([
@@ -95,7 +99,8 @@ class TestTermCount(common.TransactionCase):
"""
Just make sure we do not create duplicated translation with 'code' type
"""
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
self.env['res.lang']._activate_lang('fr_FR')
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
ids = self.env['ir.translation'].search([
('lang', '=', 'fr_FR'),
('src', '=', 'Test translation with two code lines'),
@@ -118,12 +123,15 @@ class TestTermCount(common.TransactionCase):
def test_export_empty_string(self):
"""When the string and the translation is equal the translation is empty"""
# Export the translations
def update_translations(context=None):
context = dict(context or {}, overwrite=True)
def update_translations(create_empty_translation=False):
self.env['res.lang']._activate_lang('fr_FR')
with closing(io.BytesIO()) as bufferobj:
odoo.tools.trans_export('fr_FR', ['test_translation_import'], bufferobj, 'po', self.cr)
bufferobj.name = 'test_translation_import/i18n/fr.po'
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR', verbose=False, context=context)
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR',
verbose=False,
create_empty_translation=create_empty_translation,
overwrite=True)
# Check that the not translated key is not created
update_translations()
@@ -131,7 +139,7 @@ class TestTermCount(common.TransactionCase):
self.assertFalse(translation, 'An empty translation is not imported')
# Check that "Generate Missing Terms" create empty string for not translated key
update_translations({'create_empty_translation': True})
update_translations(create_empty_translation=True)
translation = self.env['ir.translation'].search_count([('src', '=', 'Efgh'), ('value', '=', '')])
self.assertTrue(translation, 'The translation of "Efgh" should be empty')
@@ -232,7 +240,8 @@ class TestTranslationFlow(common.TransactionCase):
translations = self.env["ir.translation"].search([
('lang', '=', 'fr_FR'),
('module', '=', 'test_translation_import')
('module', '=', 'test_translation_import'),
('value', '!=', ''),
])
# minus 3 as the original fr.po contains 3 fake code translations (cf
@@ -259,10 +268,12 @@ class TestTranslationFlow(common.TransactionCase):
'overwrite': False,
})
with mute_logger('odoo.addons.base.models.res_lang'):
import_fr.with_context(create_empty_translation=True).import_lang()
import_fr.import_lang()
import_translation = self.env["ir.translation"].search([
('lang', '=', 'fr_FR'),
('module', '=', 'test_translation_import')
('module', '=', 'test_translation_import'),
('value', '!=', ''),
])
self.assertEqual(init_translation_count, len(import_translation))
+2 -2
View File
@@ -111,14 +111,14 @@ def export_translation():
def import_translation():
config = odoo.tools.config
context = {'overwrite': config["overwrite_existing_translations"]}
overwrite = config["overwrite_existing_translations"]
dbname = config['db_name']
registry = odoo.modules.registry.Registry.new(dbname)
with odoo.api.Environment.manage():
with registry.cursor() as cr:
odoo.tools.trans_load(
cr, config["translate_in"], config["language"], context=context,
cr, config["translate_in"], config["language"], overwrite=overwrite,
)
def main(args):
+214 -149
View File
@@ -13,7 +13,7 @@ import re
import tarfile
import tempfile
import threading
from collections import defaultdict
from collections import defaultdict, namedtuple
from datetime import datetime
from os.path import join
@@ -647,13 +647,13 @@ class PoFileReader:
continue
_logger.error("malformed po file: unknown occurrence: %s", occurrence)
def TranslationFileWriter(target, fileformat='po', lang=None, modules=None):
def TranslationFileWriter(target, fileformat='po', lang=None):
""" Iterate over translation file to return Odoo translation entries """
if fileformat == 'csv':
return CSVFileWriter(target)
if fileformat == 'po':
return PoFileWriter(target, modules=modules, lang=lang)
return PoFileWriter(target, lang=lang)
if fileformat == 'tgz':
return TarFileWriter(target, lang=lang)
@@ -677,13 +677,34 @@ class CSVFileWriter:
class PoFileWriter:
""" Iterate over po file to return Odoo translation entries """
def __init__(self, target, modules, lang):
import odoo.release as release
def __init__(self, target, lang):
self.buffer = target
self.lang = lang
self.po = polib.POFile()
def write_rows(self, rows):
# we now group the translations by source. That means one translation per source.
grouped_rows = {}
modules = set([])
for module, type, name, res_id, src, trad, comments in rows:
row = grouped_rows.setdefault(src, {})
row.setdefault('modules', set()).add(module)
if not row.get('translation') and trad != src:
row['translation'] = trad
row.setdefault('tnrs', []).append((type, name, res_id))
row.setdefault('comments', set()).update(comments)
modules.add(module)
for src, row in sorted(grouped_rows.items()):
if not self.lang:
# translation template, so no translation value
row['translation'] = ''
elif not row.get('translation'):
row['translation'] = ''
self.add_entry(row['modules'], sorted(row['tnrs']), src, row['translation'], row['comments'])
import odoo.release as release
self.po.header = "Translation of %s.\n" \
"This file contains the translation of the following modules:\n" \
"%s" % (release.description, ''.join("\t* %s\n" % m for m in modules))
@@ -701,25 +722,6 @@ class PoFileWriter:
'Plural-Forms': '',
}
def write_rows(self, rows):
# we now group the translations by source. That means one translation per source.
grouped_rows = {}
for module, type, name, res_id, src, trad, comments in rows:
row = grouped_rows.setdefault(src, {})
row.setdefault('modules', set()).add(module)
if not row.get('translation') and trad != src:
row['translation'] = trad
row.setdefault('tnrs', []).append((type, name, res_id))
row.setdefault('comments', set()).update(comments)
for src, row in sorted(grouped_rows.items()):
if not self.lang:
# translation template, so no translation value
row['translation'] = ''
elif not row.get('translation'):
row['translation'] = ''
self.add_entry(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
# buffer expects bytes
self.buffer.write(str(self.po).encode())
@@ -763,7 +765,7 @@ class TarFileWriter:
for mod, modrows in rows_by_module.items():
with io.BytesIO() as buf:
po = PoFileWriter(buf, modules=[mod], lang=self.lang)
po = PoFileWriter(buf, lang=self.lang)
po.write_rows(modrows)
buf.seek(0)
@@ -781,14 +783,10 @@ class TarFileWriter:
self.tar.close()
# Methods to export the translation file
def trans_export(lang, modules, buffer, format, cr):
translations = trans_generate(lang, modules, cr)
modules = set(t[0] for t in translations)
writer = TranslationFileWriter(buffer, fileformat=format, lang=lang, modules=modules)
writer.write_rows(translations)
del translations
reader = TranslationModuleReader(cr, modules=modules, lang=lang)
writer = TranslationFileWriter(buffer, fileformat=format, lang=lang)
writer.write_rows(reader)
def trans_parse_rml(de):
@@ -872,12 +870,44 @@ def babel_extract_qweb(fileobj, keywords, comment_tags, options):
_extract_translatable_qweb_terms(tree.getroot(), handle_text)
return result
ImdInfo = namedtuple('ExternalId', ['name', 'model', 'res_id', 'module'])
def trans_generate(lang, modules, cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
to_translate = set()
def push_translation(module, type, name, id, source, comments=None):
class TranslationModuleReader:
""" Retrieve translated records per module
:param cr: cursor to database to export
:param modules: list of modules to filter the exported terms, can be ['all']
records with no external id are always ignored
:param lang: language code to retrieve the translations
retrieve source terms only if not set
"""
def __init__(self, cr, modules=None, lang=None):
self._cr = cr
self._modules = modules or ['all']
self._lang = lang
self.env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
self._to_translate = []
self._path_list = [(path, True) for path in odoo.addons.__path__]
self._installed_modules = [
m['name']
for m in self.env['ir.module.module'].search_read([('state', '=', 'installed')], fields=['name'])
]
self._export_translatable_records()
self._export_translatable_resources()
def __iter__(self):
""" Export ir.translation values for all retrieved records """
IrTranslation = self.env['ir.translation']
for module, source, name, res_id, ttype, comments in self._to_translate:
trans = IrTranslation._get_source(name, ttype, self._lang, source) if self._lang else ""
yield (module, ttype, name, res_id, source, encode(trans) or '', comments)
def _push_translation(self, module, ttype, name, res_id, source, comments=None):
# empty and one-letter terms are ignored, they probably are not meant to be
# translated, and would be very hard to translate anyway.
sanitized_term = (source or '').strip()
@@ -886,84 +916,98 @@ def trans_generate(lang, modules, cr):
if not sanitized_term or len(sanitized_term) <= 1:
return
tnx = (module, source, name, id, type, tuple(comments or ()))
to_translate.add(tnx)
self._to_translate.append((module, source, name, res_id, ttype, tuple(comments or ())))
def translatable_model(record):
if not record._translate:
return False
def _get_translatable_records(self, records):
""" Filter the records that are translatable
if record._name == 'ir.model.fields.selection':
record = record.field_id
if record._name == 'ir.model.fields':
field_name = record.name
field_model = env.get(record.model)
if (field_model is None or not field_model._translate or
field_name not in field_model._fields):
return False
A record is considered as untranslatable if:
- it does not exist
- the model is flagged with _translate=False
- it is a field of a model flagged with _translate=False
- it is a selection of a field of a model flagged with _translate=False
return True
:param records: a list of namedtuple ImdInfo belonging to the same model
"""
model = next(iter(records)).model
if model not in self.env:
_logger.error("Unable to find object %r", model)
return self.browse()
query = 'SELECT min(name), model, res_id, module FROM ir_model_data'
if not self.env[model]._translate:
return self.env[model].browse()
if 'all_installed' in modules:
query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
res_ids = [r.res_id for r in records]
records = self.env[model].browse(res_ids).exists()
if len(records) < len(res_ids):
missing_ids = set(records.ids) - set(res_ids)
_logger.warning("Unable to find objects %r with id %d", model, ', '.join(missing_ids))
if not records:
return records
if 'all' not in modules:
query += ' WHERE module IN %s'
query_param = (tuple(modules),)
else:
query += ' WHERE module != %s'
query_param = ('__export__',)
if model == 'ir.model.fields.selection':
fields = defaultdict(list)
for selection in records:
fields[selection.field_id] = selection
for field, selection in fields.items():
field_name = field.name
field_model = self.env.get(field.model)
if (field_model is None or not field_model._translate or
field_name not in field_model._fields):
# the selection is linked to a model with _translate=False, remove it
records -= selection
elif model == 'ir.model.fields':
for field in records:
field_name = field.name
field_model = self.env.get(field.model)
if (field_model is None or not field_model._translate or
field_name not in field_model._fields):
# the field is linked to a model with _translate=False, remove it
records -= field
query += ' GROUP BY model, res_id, module ORDER BY module, model, min(name)'
return records
cr.execute(query, query_param)
for (xml_name, model, res_id, module) in cr.fetchall():
xml_name = "%s.%s" % (module, xml_name)
def _export_translatable_records(self):
""" Export translations of all translated records having an external id """
if model not in env:
_logger.error(u"Unable to find object %r", model)
continue
query = """SELECT min(name), model, res_id, module
FROM ir_model_data
WHERE module = ANY(%s)
GROUP BY model, res_id, module
ORDER BY module, model, min(name)"""
record = env[model].browse(res_id)
if not record.exists():
_logger.warning(u"Unable to find object %r with id %d", model, res_id)
continue
if 'all' not in self._modules:
query_param = list(self._modules)
else:
query_param = self._installed_modules
if not translatable_model(record):
continue
self._cr.execute(query, (query_param,))
for field_name, field in record._fields.items():
if field.translate:
name = model + "," + field_name
try:
value = record[field_name] or ''
except Exception:
continue
for term in set(field.get_trans_terms(value)):
trans_type = 'model_terms' if callable(field.translate) else 'model'
push_translation(module, trans_type, name, xml_name, term)
records_per_model = defaultdict(dict)
for (xml_name, model, res_id, module) in self._cr.fetchall():
records_per_model[model][res_id] = ImdInfo(xml_name, model, res_id, module)
# End of data for ir.model.data query results
for model, imd_per_id in records_per_model.items():
records = self._get_translatable_records(imd_per_id.values())
if not records:
continue
installed_modules = [
m['name']
for m in env['ir.module.module'].search_read([('state', '=', 'installed')], fields=['name'])
]
for record in records:
xml_name = "%s.%s" % (imd_per_id[record.id].module, imd_per_id[record.id].name)
for field_name, field in record._fields.items():
if field.translate:
name = model + "," + field_name
try:
value = record[field_name] or ''
except Exception:
continue
for term in set(field.get_trans_terms(value)):
trans_type = 'model_terms' if callable(field.translate) else 'model'
self._push_translation(module, trans_type, name, xml_name, term)
path_list = [(path, True) for path in odoo.addons.__path__]
# Also scan these non-addon paths
for bin_path in ['osv', 'report', 'modules', 'service', 'tools']:
path_list.append((os.path.join(config['root_path'], bin_path), True))
# non-recursive scan for individual files in root directory but without
# scanning subdirectories that may contain addons
path_list.append((config['root_path'], False))
_logger.debug("Scanning modules at paths: %s", path_list)
def get_module_from_path(path):
for (mp, rec) in path_list:
def _get_module_from_path(self, path):
for (mp, rec) in self._path_list:
mp = os.path.join(mp, '')
dirname = os.path.join(os.path.dirname(path), '')
if rec and path.startswith(mp) and dirname != mp:
@@ -971,22 +1015,24 @@ def trans_generate(lang, modules, cr):
return path.split(os.path.sep)[0]
return 'base' # files that are not in a module are considered as being in 'base' module
def verified_module_filepaths(fname, path, root):
def _verified_module_filepaths(self, fname, path, root):
fabsolutepath = join(root, fname)
frelativepath = fabsolutepath[len(path):]
display_path = "addons%s" % frelativepath
module = get_module_from_path(fabsolutepath)
if ('all' in modules or module in modules) and module in installed_modules:
module = self._get_module_from_path(fabsolutepath)
if ('all' in self._modules or module in self._modules) and module in self._installed_modules:
if os.path.sep != '/':
display_path = display_path.replace(os.path.sep, '/')
return module, fabsolutepath, frelativepath, display_path
return None, None, None, None
def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
def _babel_extract_terms(self, fname, path, root, extract_method="python", trans_type='code',
extra_comments=None, extract_keywords={'_': None}):
module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
module, fabsolutepath, _, display_path = self._verified_module_filepaths(fname, path, root)
if not module:
return
extra_comments = extra_comments or []
if not module: return
src_file = open(fabsolutepath, 'rb')
options = {}
if extract_method == 'python':
@@ -996,76 +1042,98 @@ def trans_generate(lang, modules, cr):
# Babel 0.9.6 yields lineno, message, comments
# Babel 1.3 yields lineno, message, comments, context
lineno, message, comments = extracted[:3]
push_translation(module, trans_type, display_path, lineno,
self._push_translation(module, trans_type, display_path, lineno,
encode(message), comments + extra_comments)
except Exception:
_logger.exception("Failed to extract terms from %s", fabsolutepath)
finally:
src_file.close()
for (path, recursive) in path_list:
_logger.debug("Scanning files of modules at %s", path)
for root, dummy, files in os.walk(path, followlinks=True):
for fname in fnmatch.filter(files, '*.py'):
babel_extract_terms(fname, path, root,
extract_keywords={'_': None, '_lt': None})
# Javascript source files in the static/src/js directory, rest is ignored (libs)
if fnmatch.fnmatch(root, '*/static/src/js*'):
for fname in fnmatch.filter(files, '*.js'):
babel_extract_terms(fname, path, root, 'javascript',
extra_comments=[WEB_TRANSLATION_COMMENT],
extract_keywords={'_t': None, '_lt': None})
# QWeb template files
if fnmatch.fnmatch(root, '*/static/src/xml*'):
for fname in fnmatch.filter(files, '*.xml'):
babel_extract_terms(fname, path, root, 'odoo.tools.translate:babel_extract_qweb',
extra_comments=[WEB_TRANSLATION_COMMENT])
if not recursive:
# due to topdown, first iteration is in first level
break
def _export_translatable_resources(self):
""" Export translations for static terms
This will include:
- the python strings marked with _() or _lt()
- the javascript strings marked with _t() or _lt() inside static/src/js/
- the strings inside Qweb files inside static/src/xml/
"""
out = []
# translate strings marked as to be translated
Translation = env['ir.translation']
for module, source, name, id, type, comments in sorted(to_translate):
trans = Translation._get_source(name, type, lang, source) if lang else ""
out.append((module, type, name, id, source, encode(trans) or '', comments))
return out
# Also scan these non-addon paths
for bin_path in ['osv', 'report', 'modules', 'service', 'tools']:
self._path_list.append((os.path.join(config['root_path'], bin_path), True))
# non-recursive scan for individual files in root directory but without
# scanning subdirectories that may contain addons
self._path_list.append((config['root_path'], False))
_logger.debug("Scanning modules at paths: %s", self._path_list)
for (path, recursive) in self._path_list:
_logger.debug("Scanning files of modules at %s", path)
for root, dummy, files in os.walk(path, followlinks=True):
for fname in fnmatch.filter(files, '*.py'):
self._babel_extract_terms(fname, path, root,
extract_keywords={'_': None, '_lt': None})
# Javascript source files in the static/src/js directory, rest is ignored (libs)
if fnmatch.fnmatch(root, '*/static/src/js*'):
for fname in fnmatch.filter(files, '*.js'):
self._babel_extract_terms(fname, path, root, 'javascript',
extra_comments=[WEB_TRANSLATION_COMMENT],
extract_keywords={'_t': None, '_lt': None})
# QWeb template files
if fnmatch.fnmatch(root, '*/static/src/xml*'):
for fname in fnmatch.filter(files, '*.xml'):
self._babel_extract_terms(fname, path, root, 'odoo.tools.translate:babel_extract_qweb',
extra_comments=[WEB_TRANSLATION_COMMENT])
if not recursive:
# due to topdown, first iteration is in first level
break
def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
def trans_load(cr, filename, lang, verbose=True, create_empty_translation=False, overwrite=False):
try:
with file_open(filename, mode='rb') as fileobj:
_logger.info("loading %s", filename)
fileformat = os.path.splitext(filename)[-1][1:].lower()
result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
return result
return trans_load_data(cr, fileobj, fileformat, lang,
verbose=verbose,
create_empty_translation=create_empty_translation,
overwrite=overwrite)
except IOError:
if verbose:
_logger.error("couldn't read translation file %s", filename)
return None
def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
"""Populates the ir_translation table."""
def trans_load_data(cr, fileobj, fileformat, lang,
verbose=True, create_empty_translation=False, overwrite=False):
"""Populates the ir_translation table.
:param fileobj: buffer open to a translation file
:param fileformat: format of the `fielobj` file, one of 'po' or 'csv'
:param lang: language code of the translations contained in `fileobj`
language must be present and activated in the database
:param verbose: increase log output
:param create_empty_translation: create an ir.translation record, even if no value
is provided in the translation entry
:param overwrite: if an ir.translation already exists for a term, replace it with
the one in `fileobj`
"""
if verbose:
_logger.info('loading translation file for language %s', lang)
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, context or {})
Lang = env['res.lang']
Translation = env['ir.translation']
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
try:
if not Lang.search_count([('code', '=', lang)]):
# lets create the language with locale information
Lang.load_lang(lang=lang, lang_name=lang_name)
if not env['res.lang']._lang_get(lang):
_logger.error("Couldn't read translation for lang '%s', language not found", lang)
return None
# now, the serious things: we read the language file
fileobj.seek(0)
reader = TranslationFileReader(fileobj, fileformat=fileformat)
# read the rest of the file
irt_cursor = Translation._get_import_cursor()
# read the rest of the file with a cursor-like object for fast inserting translations"
Translation = env['ir.translation']
irt_cursor = Translation._get_import_cursor(overwrite)
def process_row(row):
"""Process a single PO (or POT) entry."""
@@ -1078,12 +1146,9 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
dic.update(row)
# do not import empty values
if not env.context.get('create_empty_translation', False) and not dic['value']:
if not create_empty_translation and not dic['value']:
return
if dic['type'] == 'code' and module_name:
dic['module'] = module_name
irt_cursor.push(dic)
# First process the entries from the PO file (doing so also fills/removes