From f32141017bf0f8eb04d1dfe4c6ac1185ab88aeb5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 13:45:59 +0200 Subject: [PATCH 01/12] [IMP] base: language export wizard * move stuff around * call write() from browse, correctly pass context to browse * remove useless default to file name * use contextlib with stringio --- .../module/wizard/base_export_language.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/openerp/addons/base/module/wizard/base_export_language.py b/openerp/addons/base/module/wizard/base_export_language.py index eb9004b9abe..e76121672af 100644 --- a/openerp/addons/base/module/wizard/base_export_language.py +++ b/openerp/addons/base/module/wizard/base_export_language.py @@ -20,6 +20,7 @@ ############################################################################## import base64 +import contextlib import cStringIO from openerp import tools @@ -51,29 +52,26 @@ class base_language_export(osv.osv_memory): } _defaults = { 'state': 'choose', - 'name': 'lang.tar.gz', 'lang': NEW_LANG_KEY, 'format': 'csv', } def act_getfile(self, cr, uid, ids, context=None): - this = self.browse(cr, uid, ids)[0] + this = self.browse(cr, uid, ids, context=context)[0] lang = this.lang if this.lang != NEW_LANG_KEY else False - mods = map(lambda m: m.name, this.modules) or ['all'] - mods.sort() - buf = cStringIO.StringIO() - tools.trans_export(lang, mods, buf, this.format, cr) + mods = sorted(map(lambda m: m.name, this.modules)) or ['all'] + + with contextlib.closing(cStringIO.StringIO()) as buf: + tools.trans_export(lang, mods, buf, this.format, cr) + out = base64.encodestring(buf.getvalue()) + filename = 'new' if lang: filename = get_iso_codes(lang) elif len(mods) == 1: filename = mods[0] - this.name = "%s.%s" % (filename, this.format) - out = base64.encodestring(buf.getvalue()) - buf.close() - self.write(cr, uid, ids, {'state': 'get', - 'data': out, - 'name':this.name}, context=context) + name = "%s.%s" % (filename, this.format) + this.write({ 'state': 'get', 'data': out, 'name': name }) return { 'type': 'ir.actions.act_window', 'res_model': 'base.language.export', From f164c44ae2dcea8547f310406dba4169dd966016 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 13:50:59 +0200 Subject: [PATCH 02/12] [FIX] base: export PO template files as pot was already done when exporting to tgz, but not for po --- openerp/addons/base/module/wizard/base_export_language.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/module/wizard/base_export_language.py b/openerp/addons/base/module/wizard/base_export_language.py index e76121672af..e6df0536cb2 100644 --- a/openerp/addons/base/module/wizard/base_export_language.py +++ b/openerp/addons/base/module/wizard/base_export_language.py @@ -70,7 +70,10 @@ class base_language_export(osv.osv_memory): filename = get_iso_codes(lang) elif len(mods) == 1: filename = mods[0] - name = "%s.%s" % (filename, this.format) + extension = this.format + if not lang and extension == 'po': + extension = 'pot' + name = "%s.%s" % (filename, extension) this.write({ 'state': 'get', 'data': out, 'name': name }) return { 'type': 'ir.actions.act_window', From 4beba1dc31dc5cbfbd0ebe238188c00d10b841b5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 14:20:25 +0200 Subject: [PATCH 03/12] [IMP] translations: parse views iteratively instead of recursively also fix a pair of docstrings --- openerp/tools/translate.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 7bf147027ad..37e6492e362 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -566,18 +566,17 @@ def trans_parse_view(element, callback): :param callable callback: a callable in the form ``f(term, source_line)``, that will be called for each extracted term. """ - if (not isinstance(element, SKIPPED_ELEMENT_TYPES) - and element.tag.lower() not in SKIPPED_ELEMENTS - and element.text): - _push(callback, element.text, element.sourceline) - if element.tail: - _push(callback, element.tail, element.sourceline) - for attr in ('string', 'help', 'sum', 'confirm', 'placeholder'): - value = element.get(attr) - if value: - _push(callback, value, element.sourceline) - for n in element: - trans_parse_view(n, callback) + for el in element.iter(): + if (not isinstance(el, SKIPPED_ELEMENT_TYPES) + and el.tag.lower() not in SKIPPED_ELEMENTS + and el.text): + _push(callback, el.text, el.sourceline) + if el.tail: + _push(callback, el.tail, el.sourceline) + for attr in ('string', 'help', 'sum', 'confirm', 'placeholder'): + value = el.get(attr) + if value: + _push(callback, value, el.sourceline) # tests whether an object is in a list of modules def in_modules(object_name, modules): @@ -598,9 +597,9 @@ def _extract_translatable_qweb_terms(element, callback): a QWeb template, and call ``callback(term)`` for each translatable term that is found in the document. - :param ElementTree element: root of etree document to extract terms from - :param callable callback: a callable in the form ``f(term, source_line)``, - that will be called for each extracted term. + :param etree._Element element: root of etree document to extract terms from + :param Callable callback: a callable in the form ``f(term, source_line)``, + that will be called for each extracted term. """ # not using elementTree.iterparse because we need to skip sub-trees in case # the ancestor element had a reason to be skipped @@ -620,6 +619,7 @@ def _extract_translatable_qweb_terms(element, callback): def babel_extract_qweb(fileobj, keywords, comment_tags, options): """Babel message extractor for qweb template files. + :param fileobj: the file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions @@ -628,7 +628,7 @@ def babel_extract_qweb(fileobj, keywords, comment_tags, options): :param options: a dictionary of additional options (optional) :return: an iterator over ``(lineno, funcname, message, comments)`` tuples - :rtype: ``iterator`` + :rtype: Iterable """ result = [] def handle_text(text, lineno): From 9964aae7a3e5572e1e6f598408566ef8017a106c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 14:23:56 +0200 Subject: [PATCH 04/12] [IMP] translations: simplify condition in qweb terms extraction --- openerp/tools/translate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 37e6492e362..0528ba2a409 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -608,8 +608,7 @@ def _extract_translatable_qweb_terms(element, callback): if (el.tag.lower() not in SKIPPED_ELEMENTS and "t-js" not in el.attrib and not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) - and not ("t-translation" in el.attrib and - el.attrib["t-translation"].strip() == "off")): + and el.get("t-translation", '').strip() != "off"): _push(callback, el.text, el.sourceline) for att in ('title', 'alt', 'label', 'placeholder'): if att in el.attrib: From 8ee2a89731a257e5980f8265bd6b29e8d455aa00 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 15:12:47 +0200 Subject: [PATCH 05/12] [FIX] translations: don't skip first line of translations when extracting module names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used to be the first line was the CSV headers, the slice was left over after these were removed from the source data. It probably didn't hurt (only issue would be if the first module — alphabetically — has a single translatable term), but it's just as clean not to have that. Also removed now-unused variable (probably leftover of the CSV thing as well) --- openerp/tools/translate.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 0528ba2a409..4eb6b397a7e 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -505,13 +505,8 @@ def trans_export(lang, modules, buffer, format, cr): raise Exception(_('Unrecognized extension: must be one of ' '.csv, .po, or .tgz (received .%s).' % format)) - trans_lang = lang - if not trans_lang and format == 'csv': - # CSV files are meant for translators and they need a starting point, - # so we at least put the original term in the translation column - trans_lang = 'en_US' translations = trans_generate(lang, modules, cr) - modules = set([t[0] for t in translations[1:]]) + modules = set(t[0] for t in translations) _process(format, modules, translations, buffer, lang) del translations From 4d4d4f248f4896f69de9d7bef1b1c4f11385faac Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 17:27:32 +0200 Subject: [PATCH 06/12] [FIX] base: incorrect translation mark --- openerp/addons/base/res/res_config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index b5438e7c400..1b6b17bb9e1 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -466,10 +466,12 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin): dep_name = [x.shortdesc for x in module_pool.browse( cr, uid, dep_ids + module_ids, context=context)] message = '\n'.join(dep_name) - return {'warning': {'title': _('Warning!'), - 'message': - _('Disabling this option will also uninstall the following modules \n%s' % message) - }} + return { + 'warning': { + 'title': _('Warning!'), + 'message': _('Disabling this option will also uninstall the following modules \n%s') % message, + } + } return {} def _get_classified_fields(self, cr, uid, context=None): From 184f396ea0be93582153ff910b8d1359d8a110b7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 17:28:45 +0200 Subject: [PATCH 07/12] [FIX] web_diagram: can't mark empty strings for translations, gettext does not like empty msgids --- addons/web_diagram/static/src/js/diagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_diagram/static/src/js/diagram.js b/addons/web_diagram/static/src/js/diagram.js index 77876c9dc52..567cc1ae794 100644 --- a/addons/web_diagram/static/src/js/diagram.js +++ b/addons/web_diagram/static/src/js/diagram.js @@ -216,7 +216,7 @@ instance.web.DiagramView = instance.web.View.extend({ }; CuteEdge.creation_callback = function(node_start, node_end){ - return {label:_t("")}; + return {label: ''}; }; CuteEdge.new_edge_callback = function(cuteedge){ self.add_connector(cuteedge.get_start().id, From f1af60b3a64eb2f8b55a744be37b2141680851b5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 17:29:09 +0200 Subject: [PATCH 08/12] [ADD] doc: basic translations guide --- doc/guides/translations.rst | 97 ++++++++++++++++++++++++++ doc/guides/translations/po-export.png | Bin 0 -> 6069 bytes 2 files changed, 97 insertions(+) create mode 100644 doc/guides/translations.rst create mode 100644 doc/guides/translations/po-export.png diff --git a/doc/guides/translations.rst b/doc/guides/translations.rst new file mode 100644 index 00000000000..792caa85108 --- /dev/null +++ b/doc/guides/translations.rst @@ -0,0 +1,97 @@ +.. _guides/translations: + +=================== +Translating Modules +=================== + +Exporting translatable term +=========================== + +A number of terms in your modules are "implicitly translatable" as a result, +even if you haven't done any specific work towards translation you can export +your module's translatable terms and may find content to work with. + +.. todo:: needs technical features + +Translations export is done via the administration interface by logging into +the backend interface and opening :menuselection:`Settings --> Translations +--> Import / Export --> Export Translations` + +* leave the language to the default (new language/empty template) +* select the `PO File`_ format +* select your module +* click :guilabel:`Export` and download the file + +.. image:: translations/po-export.* + :align: center + :width: 75% + +This gives you a file called :file:`{yourmodule}.po` which should be renamed +to :file:`{yourmodule}.pot` and moved to the :file:`{yourmodule}/i18n/` +directory. The file is a *PO Template* which simply lists translatable strings +and from which actual translations (PO files) can be created. PO files can +be created using msginit_, with a dedicated translation tool like POEdit_ or +by simply copying the template to a new file called :file:`{language}.po`. +Translation files should be put in :file:`{yourmodule}/i18n/`, next to +:file:`{yourmodule}.pot`, and will be automatically loaded by Odoo when the +corresponding language is installed (via :menuselection:`Settings --> +Translations --> Load a Translation`) + +.. note:: translations for all loaded languages are also installed or updated + when installing or updating a module + +Implicit exports +================ + +Odoo automatically exports translatable strings from "data"-type content: + +* in non-QWeb views, all text nodes are exported as well as the content of + the ``string``, ``help``, ``sum``, ``confirm`` and ``placeholder`` + attributes +* QWeb templates (both server-side and client-side), all text nodes are + exported except inside ``t-translation="off"`` blocks, the content of the + ``title``, ``alt``, ``label`` and ``placeholder`` attributes are also + exported +* for :class:`~openerp.fields.Field`, unless their model is marked with + ``_translate = False``: + + * their ``string`` and ``help`` attributes are exported + * if ``selection`` is present and a list (or tuple), it's exported + * if their ``translate`` attribute is set to ``True``, all of their existing + values (across all records) are exported +* help/error messages of :attr:`~openerp.models.Model._constraints` and + :attr:`~openerp.models.Model._sql_constraints` are exported + +Explicit exports +================ + +When it comes to more "imperative" situations in Python code or Javascript +code, Odoo is not able to automatically export translatable terms and they +must be marked explicitly for export. This is done by wrapping a literal +string in a function call. + +In Python, the wrapping function is :func:`openerp.tools.translate._`:: + + title = _("Bank Accounts") + +In JavaScript, the wrapping function is generally :js:func:`openerp.web._t`: + +.. code-block:: javascript + + title = _t("Bank Accounts") + +.. warning:: + + only literal strings can be marked for exports, not expressions and not + variables. For situations where strings are formatted, this means the + format string must be marked not the formatted string:: + + # bad, the extract may work but it will not correctly translate the text + _("Scheduled meeting with %s" % invitee.name) + + # good + _("Scheduled meeting with %s") % invitee.name + +.. _PO File: http://en.wikipedia.org/wiki/Gettext#Translating +.. _msginit: http://www.gnu.org/software/gettext/manual/gettext.html#Creating +.. _POEdit: http://poedit.net/ diff --git a/doc/guides/translations/po-export.png b/doc/guides/translations/po-export.png new file mode 100644 index 0000000000000000000000000000000000000000..82f8d5ad0118578c2f7f111f73cddbf54d2e0c67 GIT binary patch literal 6069 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 literal 0 HcmV?d00001 From d7fb4d903df0335857570b647311a3a6b0c99982 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 10 Oct 2014 17:29:39 +0200 Subject: [PATCH 09/12] [IMP] various tentative improvements to translation code --- openerp/tools/translate.py | 72 ++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 4eb6b397a7e..6a8a6f890da 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -635,13 +635,11 @@ def trans_generate(lang, modules, cr): dbname = cr.dbname registry = openerp.registry(dbname) - trans_obj = registry.get('ir.translation') - model_data_obj = registry.get('ir.model.data') + trans_obj = registry['ir.translation'] + model_data_obj = registry['ir.model.data'] uid = 1 - l = registry.models.items() - l.sort() - query = 'SELECT name, model, res_id, module' \ + query = 'SELECT name, model, res_id, module' \ ' FROM ir_model_data' query_models = """SELECT m.id, m.model, imd.module @@ -661,15 +659,16 @@ def trans_generate(lang, modules, cr): cr.execute(query, query_param) - _to_translate = [] + _to_translate = set() def push_translation(module, type, name, id, source, comments=None): - tuple = (module, source, name, id, type, comments or []) # empty and one-letter terms are ignored, they probably are not meant to be # translated, and would be very hard to translate anyway. if not source or len(source.strip()) <= 1: return - if tuple not in _to_translate: - _to_translate.append(tuple) + + tnx = (module, source, name, id, type, tuple(comments or ())) + if tnx not in _to_translate: + _to_translate.add(tnx) def encode(s): if isinstance(s, unicode): @@ -698,15 +697,15 @@ def trans_generate(lang, modules, cr): _logger.error("Unable to find object %r", model) continue - if not registry[model]._translate: + Model = registry[model] + if not Model._translate: # explicitly disabled continue - exists = registry[model].exists(cr, uid, res_id) - if not exists: + obj = Model.browse(cr, uid, res_id) + if not obj.exists(): _logger.warning("Unable to find object %r with id %d", model, res_id) continue - obj = registry[model].browse(cr, uid, res_id) if model=='ir.ui.view': d = etree.XML(encode(obj.arch)) @@ -824,9 +823,9 @@ def trans_generate(lang, modules, cr): if model_obj._sql_constraints: push_local_constraints(module, model_obj, 'sql_constraints') - modobj = registry['ir.module.module'] - installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')]) - installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name'])) + installed_modules = map( + lambda m: m['name'], + registry['ir.module.module'].search_read(cr, uid, [('state', '=', 'installed')], fields=['name'])) path_list = list(openerp.modules.module.ad_paths) # Also scan these non-addon paths @@ -835,14 +834,12 @@ def trans_generate(lang, modules, cr): _logger.debug("Scanning modules at paths: %s", path_list) - mod_paths = list(path_list) - def get_module_from_path(path): - for mp in mod_paths: - if path.startswith(mp) and (os.path.dirname(path) != mp): + for mp in path_list: + if path.startswith(mp) and os.path.dirname(path) != mp: path = path[len(mp)+1:] return path.split(os.path.sep)[0] - return 'base' # files that are not in a module are considered as being in 'base' module + return 'base' # files that are not in a module are considered as being in 'base' module def verified_module_filepaths(fname, path, root): fabsolutepath = join(root, fname) @@ -857,20 +854,20 @@ def trans_generate(lang, modules, cr): extra_comments=None, extract_keywords={'_': None}): module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root) extra_comments = extra_comments or [] - if module: - src_file = open(fabsolutepath, 'r') - try: - for extracted in extract.extract(extract_method, src_file, - keywords=extract_keywords): - # 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, - encode(message), comments + extra_comments) - except Exception: - _logger.exception("Failed to extract terms from %s", fabsolutepath) - finally: - src_file.close() + if not module: return + src_file = open(fabsolutepath, 'r') + try: + for extracted in extract.extract(extract_method, src_file, + keywords=extract_keywords): + # 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, + encode(message), comments + extra_comments) + except Exception: + _logger.exception("Failed to extract terms from %s", fabsolutepath) + finally: + src_file.close() for path in path_list: _logger.debug("Scanning files of modules at %s", path) @@ -893,11 +890,10 @@ def trans_generate(lang, modules, cr): extra_comments=[WEB_TRANSLATION_COMMENT]) out = [] - _to_translate.sort() # translate strings marked as to be translated - for module, source, name, id, type, comments in _to_translate: + for module, source, name, id, type, comments in sorted(_to_translate): trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source) - out.append([module, type, name, id, source, encode(trans) or '', comments]) + out.append((module, type, name, id, source, encode(trans) or '', comments)) return out def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None): From 4854d5562fd7a895c636304a5eef43575d9f5f8f Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 26 Nov 2014 17:36:42 +0100 Subject: [PATCH 10/12] [IMP] translate: improve management of targets in POT file --- openerp/tools/translate.py | 92 +++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 6a8a6f890da..e02319f1204 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -32,10 +32,10 @@ import tarfile import tempfile import threading from babel.messages import extract -from os.path import join - +from collections import defaultdict from datetime import datetime from lxml import etree +from os.path import join import config import misc @@ -938,11 +938,11 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, reader = csv.reader(fileobj, quotechar='"', delimiter=',') # read the first line of the file (it contains columns titles) for row in reader: - f = row + fields = row break elif fileformat == 'po': reader = TinyPoFile(fileobj) - f = ['type', 'name', 'res_id', 'src', 'value', 'comments'] + fields = ['type', 'name', 'res_id', 'src', 'value', 'comments'] # Make a reader for the POT file and be somewhat defensive for the # stable branch. @@ -951,10 +951,10 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # Normally the path looks like /path/to/xxx/i18n/lang.po # and we try to find the corresponding # /path/to/xxx/i18n/xxx.pot file. - head, _ = os.path.split(fileobj.name) - head2, _ = os.path.split(head) - head3, tail3 = os.path.split(head2) - pot_handle = misc.file_open(os.path.join(head3, tail3, 'i18n', tail3 + '.pot')) + addons_module_i18n, _ = os.path.split(fileobj.name) + addons_module, _ = os.path.split(addons_module_i18n) + addons, module = os.path.split(addons_module) + pot_handle = misc.file_open(os.path.join(addons, module, 'i18n', module + '.pot')) pot_reader = TinyPoFile(pot_handle) except: pass @@ -963,59 +963,57 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, _logger.error('Bad file format: %s', fileformat) raise Exception(_('Bad file format')) - # Read the POT `reference` comments, and keep them indexed by source - # string. - pot_targets = {} + # Read the POT references, and keep them indexed by source string. + class Target(object): + def __init__(self): + self.value = None + self.targets = set() # set of (type, name, res_id) + self.comments = None + + pot_targets = defaultdict(Target) for type, name, res_id, src, _, comments in pot_reader: if type is not None: - pot_targets.setdefault(src, {'value': None, 'targets': []}) - pot_targets[src]['targets'].append((type, name, res_id)) + target = pot_targets[src] + target.targets.add((type, name, res_id)) + target.comments = comments # read the rest of the file irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context) def process_row(row): """Process a single PO (or POT) entry.""" - # skip empty rows and rows where the translation field (=last fiefd) is empty - #if (not row) or (not row[-1]): - # return - # dictionary which holds values for this line of the csv file # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ..., # 'src': ..., 'value': ..., 'module':...} - dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments')) + dic = dict.fromkeys(('type', 'name', 'res_id', 'src', 'value', + 'comments', 'imd_model', 'imd_name', 'module')) dic['lang'] = lang - for i, field in enumerate(f): - dic[field] = row[i] + dic.update(zip(fields, row)) - # Get the `reference` comments from the POT. - src = row[3] - if pot_reader and src in pot_targets: - pot_targets[src]['targets'] = filter(lambda x: x != row[:3], pot_targets[src]['targets']) - pot_targets[src]['value'] = row[4] - if not pot_targets[src]['targets']: - del pot_targets[src] + # discard the target from the POT targets. + src = dic['src'] + if src in pot_targets: + target = pot_targets[src] + target.value = dic['value'] + target.targets.discard((dic['type'], dic['name'], dic['res_id'])) # This would skip terms that fail to specify a res_id - if not dic.get('res_id'): + res_id = dic['res_id'] + if not res_id: return - res_id = dic.pop('res_id') - if res_id and isinstance(res_id, (int, long)) \ - or (isinstance(res_id, basestring) and res_id.isdigit()): - dic['res_id'] = int(res_id) - dic['module'] = module_name + if unicode(res_id).isdigit(): + # res_id is either an integer, or a string composed of digits only + dic['res_id'] = int(res_id) + dic['module'] = module_name else: - tmodel = dic['name'].split(',')[0] - if '.' in res_id: - tmodule, tname = res_id.split('.', 1) - else: - tmodule = False - tname = res_id - dic['imd_model'] = tmodel - dic['imd_name'] = tname - dic['module'] = tmodule + # res_id is an xml id dic['res_id'] = None + dic['imd_model'] = dic['name'].split(',')[0] + if '.' in res_id: + dic['module'], dic['imd_name'] = res_id.split('.', 1) + else: + dic['module'], dic['imd_name'] = False, res_id irt_cursor.push(dic) @@ -1027,10 +1025,11 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # Then process the entries implied by the POT file (which is more # correct w.r.t. the targets) if some of them remain. pot_rows = [] - for src in pot_targets: - value = pot_targets[src]['value'] - for type, name, res_id in pot_targets[src]['targets']: - pot_rows.append((type, name, res_id, src, value, comments)) + for src, target in pot_targets.iteritems(): + if target.value: + for type, name, res_id in target.targets: + pot_rows.append((type, name, res_id, src, target.value, target.comments)) + pot_targets.clear() for row in pot_rows: process_row(row) @@ -1038,6 +1037,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, trans_obj.clear_caches() if verbose: _logger.info("translation file loaded succesfully") + except IOError: filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat) _logger.exception("couldn't read translation file %s", filename) From 4d6fb49b8f9a18d01b6d17defa21ea1c97940a23 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 27 Nov 2014 10:35:07 +0100 Subject: [PATCH 11/12] [IMP] doc: move translations to reference documentation --- doc/reference.rst | 1 + doc/{guides => reference}/translations.rst | 27 +++++++++--------- .../translations/po-export.png | Bin 3 files changed, 14 insertions(+), 14 deletions(-) rename doc/{guides => reference}/translations.rst (75%) rename doc/{guides => reference}/translations/po-export.png (100%) diff --git a/doc/reference.rst b/doc/reference.rst index 3e1597db1ef..9c995ef9638 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -18,5 +18,6 @@ Reference reference/qweb reference/javascript + reference/translations reference/reports reference/workflows diff --git a/doc/guides/translations.rst b/doc/reference/translations.rst similarity index 75% rename from doc/guides/translations.rst rename to doc/reference/translations.rst index 792caa85108..45616189c0d 100644 --- a/doc/guides/translations.rst +++ b/doc/reference/translations.rst @@ -1,4 +1,4 @@ -.. _guides/translations: +.. _reference/translations: =================== Translating Modules @@ -26,16 +26,15 @@ the backend interface and opening :menuselection:`Settings --> Translations :align: center :width: 75% -This gives you a file called :file:`{yourmodule}.po` which should be renamed -to :file:`{yourmodule}.pot` and moved to the :file:`{yourmodule}/i18n/` -directory. The file is a *PO Template* which simply lists translatable strings -and from which actual translations (PO files) can be created. PO files can -be created using msginit_, with a dedicated translation tool like POEdit_ or -by simply copying the template to a new file called :file:`{language}.po`. -Translation files should be put in :file:`{yourmodule}/i18n/`, next to -:file:`{yourmodule}.pot`, and will be automatically loaded by Odoo when the -corresponding language is installed (via :menuselection:`Settings --> -Translations --> Load a Translation`) +This gives you a file called :file:`{yourmodule}.pot` which should be moved to +the :file:`{yourmodule}/i18n/` directory. The file is a *PO Template* which +simply lists translatable strings and from which actual translations (PO files) +can be created. PO files can be created using msginit_, with a dedicated +translation tool like POEdit_ or by simply copying the template to a new file +called :file:`{language}.po`. Translation files should be put in +:file:`{yourmodule}/i18n/`, next to :file:`{yourmodule}.pot`, and will be +automatically loaded by Odoo when the corresponding language is installed (via +:menuselection:`Settings --> Translations --> Load a Translation`) .. note:: translations for all loaded languages are also installed or updated when installing or updating a module @@ -70,7 +69,7 @@ code, Odoo is not able to automatically export translatable terms and they must be marked explicitly for export. This is done by wrapping a literal string in a function call. -In Python, the wrapping function is :func:`openerp.tools.translate._`:: +In Python, the wrapping function is :func:`openerp._`:: title = _("Bank Accounts") @@ -82,9 +81,9 @@ In JavaScript, the wrapping function is generally :js:func:`openerp.web._t`: .. warning:: - only literal strings can be marked for exports, not expressions and not + Only literal strings can be marked for exports, not expressions and not variables. For situations where strings are formatted, this means the - format string must be marked not the formatted string:: + format string must be marked, not the formatted string:: # bad, the extract may work but it will not correctly translate the text _("Scheduled meeting with %s" % invitee.name) diff --git a/doc/guides/translations/po-export.png b/doc/reference/translations/po-export.png similarity index 100% rename from doc/guides/translations/po-export.png rename to doc/reference/translations/po-export.png From be10d1e573c09806c105a04bc3ca956c90923423 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 27 Nov 2014 11:07:09 +0100 Subject: [PATCH 12/12] [IMP] translate: small, non-breaking code improvements --- openerp/tools/translate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index e02319f1204..32fd5bcf6fe 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -667,8 +667,7 @@ def trans_generate(lang, modules, cr): return tnx = (module, source, name, id, type, tuple(comments or ())) - if tnx not in _to_translate: - _to_translate.add(tnx) + _to_translate.add(tnx) def encode(s): if isinstance(s, unicode): @@ -1002,8 +1001,8 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, if not res_id: return - if unicode(res_id).isdigit(): - # res_id is either an integer, or a string composed of digits only + if isinstance(res_id, (int, long)) or \ + (isinstance(res_id, basestring) and res_id.isdigit()): dic['res_id'] = int(res_id) dic['module'] = module_name else: