diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index a5265ba694c..d390d235f92 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -44,6 +44,7 @@ The kernel of OpenERP, needed for all installation. 'data/res.country.state.csv', 'ir/wizard/wizard_menu_view.xml', 'ir/ir.xml', + 'ir/ir_translation_view.xml', 'ir/ir_filters.xml', 'ir/ir_config_parameter_view.xml', 'ir/workflow/workflow_view.xml', diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 3189870aa68..ea549c39993 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1147,75 +1147,6 @@ - - - - Translations - ir.translation - - - - - - - - - - - - - Translations - ir.translation - -
-
- -
- - - - - - - - - - - - - - - - - - -
-
-
- - Translations - ir.translation - - - - - - - - - - - - - Translated Terms - ir.translation - form - - - - + + Translations + ir.translation + + + + + + + + + + + + + + Translations + ir.translation + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Translations + ir.translation + + + + + + + + + + + + + Translated Terms + ir.translation + form + + + + + + + \ No newline at end of file diff --git a/openerp/addons/base/ir/wizard/__init__.py b/openerp/addons/base/ir/wizard/__init__.py index 3f5495524f5..23cc77e8c15 100644 --- a/openerp/addons/base/ir/wizard/__init__.py +++ b/openerp/addons/base/ir/wizard/__init__.py @@ -19,6 +19,5 @@ # ############################################################################## import wizard_menu -import wizard_screen # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/wizard/wizard_menu.py b/openerp/addons/base/ir/wizard/wizard_menu.py index 3b4dc797c99..17f0fc13f1d 100644 --- a/openerp/addons/base/ir/wizard/wizard_menu.py +++ b/openerp/addons/base/ir/wizard/wizard_menu.py @@ -47,25 +47,6 @@ class wizard_model_menu(osv.osv_memory): 'icon': 'STOCK_INDENT' }, context) return {'type':'ir.actions.act_window_close'} -wizard_model_menu() - -class wizard_model_menu_line(osv.osv_memory): - _name = 'wizard.ir.model.menu.create.line' - _columns = { - 'wizard_id': fields.many2one('wizard.ir.model.menu.create','Wizard'), - 'sequence': fields.integer('Sequence'), - 'view_type': fields.selection([ - ('tree','Tree'), - ('form','Form'), - ('graph','Graph'), - ('calendar','Calendar'), - ('gantt','Gantt')],'View Type',required=True), - 'view_id': fields.many2one('ir.ui.view', 'View'), - } - _defaults = { - 'view_type': lambda self,cr,uid,ctx: 'tree' - } -wizard_model_menu_line() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/wizard/wizard_screen.py b/openerp/addons/base/ir/wizard/wizard_screen.py deleted file mode 100644 index d413ce0474e..00000000000 --- a/openerp/addons/base/ir/wizard/wizard_screen.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Copyright (C) 2010 OpenERP s.a. (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import base64 -import os -import random - -import tools -from osv import fields,osv - -# Simple base class for wizards that wish to use random images on the left -# side of the form. -class wizard_screen(osv.osv_memory): - _name = 'ir.wizard.screen' - - def _get_image(self, cr, uid, context=None): - path = os.path.join('base','res','config_pixmaps','%d.png'%random.randrange(1,4)) - image_file = file_data = tools.file_open(path,'rb') - try: - file_data = image_file.read() - return base64.encodestring(file_data) - finally: - image_file.close() - - def _get_image_fn(self, cr, uid, ids, name, args, context=None): - image = self._get_image(cr, uid, context) - return dict.fromkeys(ids, image) # ok to use .fromkeys() as the image is same for all - - _columns = { - 'config_logo': fields.function(_get_image_fn, string='Image', type='binary'), - } - - _defaults = { - 'config_logo': _get_image - } -wizard_screen() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 7621d5907ba..e86bcc15d74 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -627,45 +627,14 @@ class module(osv.osv): self.write(cr, uid, [mod_browse.id], {'category_id': p_id}) def update_translations(self, cr, uid, ids, filter_lang=None, context=None): - if context is None: - context = {} if not filter_lang: - pool = pooler.get_pool(cr.dbname) - lang_obj = pool.get('res.lang') - lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)]) - filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)] + res_lang = self.pool.get('res.lang') + lang_ids = res_lang.search(cr, uid, [('translatable', '=', True)]) + filter_lang = [lang.code for lang in res_lang.browse(cr, uid, lang_ids)] elif not isinstance(filter_lang, (list, tuple)): filter_lang = [filter_lang] - - for mod in self.browse(cr, uid, ids): - if mod.state != 'installed': - continue - modpath = modules.get_module_path(mod.name) - if not modpath: - # unable to find the module. we skip - continue - for lang in filter_lang: - iso_lang = tools.get_iso_codes(lang) - f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po') - context2 = context and context.copy() or {} - if f and '_' in iso_lang: - iso_lang2 = iso_lang.split('_')[0] - f2 = modules.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po') - if f2: - _logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang) - tools.trans_load(cr, f2, lang, verbose=False, context=context) - context2['overwrite'] = True - # Implementation notice: we must first search for the full name of - # the language derivative, like "en_UK", and then the generic, - # like "en". - if (not f) and '_' in iso_lang: - iso_lang = iso_lang.split('_')[0] - f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po') - if f: - _logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang) - tools.trans_load(cr, f, lang, verbose=False, context=context2) - elif iso_lang != 'en': - _logger.warning('module %s: no translation for language %s', mod.name, iso_lang) + modules = [m.name for m in self.browse(cr, uid, ids) if m.state == 'installed'] + self.pool.get('ir.translation').load(cr, modules, filter_lang, context=context) def check(self, cr, uid, ids, context=None): for mod in self.browse(cr, uid, ids, context=context): diff --git a/openerp/addons/base/module/wizard/base_export_language.py b/openerp/addons/base/module/wizard/base_export_language.py index 5ff99501943..95f1d85bf6c 100644 --- a/openerp/addons/base/module/wizard/base_export_language.py +++ b/openerp/addons/base/module/wizard/base_export_language.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# OpenERP, Open Source Business Applications +# Copyright (c) 2004-2012 OpenERP S.A. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -22,68 +22,58 @@ import tools import base64 import cStringIO -import pooler from osv import fields,osv from tools.translate import _ from tools.misc import get_iso_codes +NEW_LANG_KEY = '__new__' + class base_language_export(osv.osv_memory): + _name = "base.language.export" def _get_languages(self, cr, uid, context): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) - langs=lang_obj.browse(cr, uid, ids) - return [(lang.code, lang.name) for lang in langs] - - def act_cancel(self, cr, uid, ids, context=None): - #self.unlink(cr, uid, ids, context) - return {'type':'ir.actions.act_window_close' } - - def act_destroy(self, *args): - return {'type':'ir.actions.act_window_close' } + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, [('translatable', '=', True)]) + langs = lang_obj.browse(cr, uid, ids) + return [(NEW_LANG_KEY, _('New Language (Empty translation template)'))] + [(lang.code, lang.name) for lang in langs] + + _columns = { + 'name': fields.char('File Name', readonly=True), + 'lang': fields.selection(_get_languages, 'Language', required=True), + 'format': fields.selection([('csv','CSV File'), + ('po','PO File'), + ('tgz', 'TGZ Archive')], 'File Format', required=True), + 'modules': fields.many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id', 'Modules To Export', domain=[('state','=','installed')]), + 'data': fields.binary('File', readonly=True), + 'state': fields.selection([('choose', 'choose'), # choose language + ('get', 'get')]) # get the file + } + _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] + 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(this.lang, mods, buf, this.format, cr) - if this.format == 'csv': - this.advice = _("Save this document to a .CSV file and open it with your favourite spreadsheet software. The file encoding is UTF-8. You have to translate the latest column before reimporting it.") - elif this.format == 'po': - if not this.lang: - this.format = 'pot' - this.advice = _("Save this document to a %s file and edit it with a specific software or a text editor. The file encoding is UTF-8.") % ('.'+this.format,) - elif this.format == 'tgz': - ext = this.lang and '.po' or '.pot' - this.advice = _('Save this document to a .tgz file. This archive containt UTF-8 %s files and may be uploaded to launchpad.') % (ext,) - filename = _('new') - if not this.lang and len(mods) == 1: + buf = cStringIO.StringIO() + tools.trans_export(lang, mods, buf, this.format, cr) + filename = 'new' + if lang: + filename = get_iso_codes(lang) + elif len(mods) == 1: filename = mods[0] - if this.lang: - filename = get_iso_codes(this.lang) this.name = "%s.%s" % (filename, this.format) - out=base64.encodestring(buf.getvalue()) + out = base64.encodestring(buf.getvalue()) buf.close() - return self.write(cr, uid, ids, {'state':'get', 'data':out, 'advice':this.advice, 'name':this.name}, context=context) + self.write(cr, uid, ids, {'state': 'get', + 'data': out, + 'name':this.name}, context=context) + return True - _name = "base.language.export" - _inherit = "ir.wizard.screen" - _columns = { - 'name': fields.char('File Name', 16, readonly=True), - 'lang': fields.selection(_get_languages, 'Language', help='To export a new language, do not select a language.'), # not required: unset = new language - 'format': fields.selection( ( ('csv','CSV File'), ('po','PO File'), ('tgz', 'TGZ Archive')), 'File Format', required=True), - 'modules': fields.many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id', 'Modules', domain=[('state','=','installed')]), - 'data': fields.binary('File', readonly=True), - 'advice': fields.text('Advice', readonly=True), - 'state': fields.selection( ( ('choose','choose'), # choose language - ('get','get'), # get the file - ) ), - } - _defaults = { - 'state': lambda *a: 'choose', - 'name': lambda *a: 'lang.tar.gz' - } -base_language_export() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_export_language_view.xml b/openerp/addons/base/module/wizard/base_export_language_view.xml index 88e411845c3..cbf7104f957 100644 --- a/openerp/addons/base/module/wizard/base_export_language_view.xml +++ b/openerp/addons/base/module/wizard/base_export_language_view.xml @@ -1,31 +1,43 @@ - Export Translations base.language.export
- - + + + - - - - - - - - - + + +
+

Export Complete

+

Here is the exported translation file:

+

This file was generated using the universal Unicode/UTF-8 file encoding, please be sure to view and edit + using the same encoding.

+

The next step depends on the file format: +

    +
  • CSV format: you may edit it directly with your favorite spreadsheet software, + the rightmost column (value) contains the translations
  • +
  • PO(T) format: you should edit it with a PO editor such as + POEdit, or your preferred text editor
  • +
  • TGZ format: this is a compressed archive containing a PO file, directly suitable + for uploading to OpenERP's translation platform, + Launchpad
  • +
+

+

For more details about translating OpenERP in your language, please refer to the + documentation.

+
-
-
diff --git a/openerp/addons/base/module/wizard/base_import_language.py b/openerp/addons/base/module/wizard/base_import_language.py index 94b9ae4db59..9c36c9d7c54 100644 --- a/openerp/addons/base/module/wizard/base_import_language.py +++ b/openerp/addons/base/module/wizard/base_import_language.py @@ -29,11 +29,9 @@ class base_language_import(osv.osv_memory): _name = "base.language.import" _description = "Language Import" - _inherit = "ir.wizard.screen" - _columns = { - 'name': fields.char('Language Name',size=64 , required=True), - 'code': fields.char('Code (eg:en__US)',size=5 , required=True), + 'name': fields.char('Language Name', size=64 , required=True), + 'code': fields.char('ISO Code', size=5, help="ISO Language and Country code, e.g. en_US", required=True), 'data': fields.binary('File', required=True), 'overwrite': fields.boolean('Overwrite Existing Terms', help="If you enable this option, existing translations (including custom ones) " @@ -41,31 +39,25 @@ class base_language_import(osv.osv_memory): } def import_lang(self, cr, uid, ids, context=None): - """ - Import Language - @param cr: the current row, from the database cursor. - @param uid: the current user’s ID for security checks. - @param ids: the ID or list of IDs - @param context: A standard dictionary - """ if context is None: context = {} - import_data = self.browse(cr, uid, ids)[0] - if import_data.overwrite: + this = self.browse(cr, uid, ids[0]) + if this.overwrite: context.update(overwrite=True) fileobj = TemporaryFile('w+') - fileobj.write(base64.decodestring(import_data.data)) + try: + fileobj.write(base64.decodestring(this.data)) + + # now we determine the file format + fileobj.seek(0) + first_line = fileobj.readline().strip().replace('"', '').replace(' ', '') + fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po' + fileobj.seek(0) + + tools.trans_load_data(cr, fileobj, fileformat, this.code, lang_name=this.name, context=context) + finally: + fileobj.close() + return True - # now we determine the file format - fileobj.seek(0) - first_line = fileobj.readline().strip().replace('"', '').replace(' ', '') - fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po' - fileobj.seek(0) - - tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name, context=context) - fileobj.close() - return {} - -base_language_import() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_import_language_view.xml b/openerp/addons/base/module/wizard/base_import_language_view.xml index 727de4e381b..5e2aada7766 100644 --- a/openerp/addons/base/module/wizard/base_import_language_view.xml +++ b/openerp/addons/base/module/wizard/base_import_language_view.xml @@ -8,7 +8,7 @@
- + diff --git a/openerp/addons/base/module/wizard/base_language_install.py b/openerp/addons/base/module/wizard/base_language_install.py index 1e40f84cc60..ab65159f0d4 100644 --- a/openerp/addons/base/module/wizard/base_language_install.py +++ b/openerp/addons/base/module/wizard/base_language_install.py @@ -27,9 +27,7 @@ class base_language_install(osv.osv_memory): """ Install Language""" _name = "base.language.install" - _inherit = "ir.wizard.screen" _description = "Install Language" - _columns = { 'lang': fields.selection(tools.scan_languages(),'Language', required=True), 'overwrite': fields.boolean('Overwrite Existing Terms', help="If you check this box, your customized translations will be overwritten and replaced by the official ones."), @@ -63,6 +61,5 @@ class base_language_install(osv.osv_memory): 'target': 'new', 'res_id': ids and ids[0] or False, } -base_language_install() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_module_import.py b/openerp/addons/base/module/wizard/base_module_import.py index 3ef9132af0e..34f22bf82f0 100644 --- a/openerp/addons/base/module/wizard/base_module_import.py +++ b/openerp/addons/base/module/wizard/base_module_import.py @@ -34,9 +34,7 @@ class base_module_import(osv.osv_memory): """ Import Module """ _name = "base.module.import" - _inherit = "ir.wizard.screen" _description = "Import Module" - _columns = { 'module_file': fields.binary('Module .ZIP file', required=True), 'state':fields.selection([('init','init'),('done','done')], @@ -88,7 +86,5 @@ class base_module_import(osv.osv_memory): 'type': 'ir.actions.act_window', } -base_module_import() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_module_update.py b/openerp/addons/base/module/wizard/base_module_update.py index a0d21840d90..0893761cd28 100644 --- a/openerp/addons/base/module/wizard/base_module_update.py +++ b/openerp/addons/base/module/wizard/base_module_update.py @@ -25,7 +25,6 @@ class base_module_update(osv.osv_memory): _name = "base.module.update" _description = "Update Module" - _inherit = "ir.wizard.screen" _columns = { 'update': fields.integer('Number of modules updated', readonly=True), @@ -55,7 +54,4 @@ class base_module_update(osv.osv_memory): } return res - -base_module_update() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/openerp/addons/base/module/wizard/base_update_translations.py b/openerp/addons/base/module/wizard/base_update_translations.py index d24851b1cd5..abfbc445316 100644 --- a/openerp/addons/base/module/wizard/base_update_translations.py +++ b/openerp/addons/base/module/wizard/base_update_translations.py @@ -21,31 +21,28 @@ from osv import osv, fields import tools -import pooler import cStringIO from tools.translate import _ class base_update_translations(osv.osv_memory): def _get_languages(self, cr, uid, context): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) - langs=lang_obj.browse(cr, uid, ids) + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) + langs = lang_obj.browse(cr, uid, ids) return [(lang.code, lang.name) for lang in langs] def _get_lang_name(self, cr, uid, lang_code): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, [('code', '=', lang_code)]) + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, [('code', '=', lang_code)]) if not ids: raise osv.except_osv(_('Error!'), _('No language with code "%s" exists') % lang_code) lang = lang_obj.browse(cr, uid, ids[0]) return lang.name - def act_cancel(self, cr, uid, ids, context=None): - return {'type': 'ir.actions.act_window_close'} def act_update(self, cr, uid, ids, context=None): this = self.browse(cr, uid, ids)[0] lang_name = self._get_lang_name(cr, uid, this.lang) - buf=cStringIO.StringIO() + buf = cStringIO.StringIO() tools.trans_export(this.lang, ['all'], buf, 'csv', cr) tools.trans_load_data(cr, buf, 'csv', this.lang, lang_name=lang_name) buf.close() @@ -66,11 +63,8 @@ class base_update_translations(osv.osv_memory): return res _name = 'base.update.translations' - _inherit = "ir.wizard.screen" _columns = { 'lang': fields.selection(_get_languages, 'Language', required=True), } -base_update_translations() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_update_translations_view.xml b/openerp/addons/base/module/wizard/base_update_translations_view.xml index 4a20ef30bee..18fd30532bd 100644 --- a/openerp/addons/base/module/wizard/base_update_translations_view.xml +++ b/openerp/addons/base/module/wizard/base_update_translations_view.xml @@ -12,7 +12,7 @@ diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index d6b42e970bf..c24fc11a377 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -37,7 +37,6 @@ class res_config_configurable(osv.osv_memory): their view inherit from the related res_config_view_base view. ''' _name = 'res.config' - _inherit = 'ir.wizard.screen' def _next_action(self, cr, uid, context=None): Todos = self.pool['ir.actions.todo'] diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 5239b37bd2d..e7476827093 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -33,6 +33,7 @@ import logging import tarfile import tempfile import threading +from babel.messages import extract from os.path import join from datetime import datetime @@ -47,6 +48,9 @@ from openerp import SUPERUSER_ID _logger = logging.getLogger(__name__) +# used to notify web client that these translations should be loaded in the UI +WEB_TRANSLATION_COMMENT = "openerp-web" + _LOCALE2WIN32 = { 'af_ZA': 'Afrikaans_South Africa', 'sq_AL': 'Albanian_Albania', @@ -262,7 +266,7 @@ class TinyPoFile(object): self.lines_count = len(self.lines); self.first = True - self.tnrs= [] + self.extra_lines= [] return self def _get_lines(self): @@ -278,14 +282,14 @@ class TinyPoFile(object): return (self.lines_count - len(self.lines)) def next(self): - type = name = res_id = source = trad = None - - if self.tnrs: - type, name, res_id, source, trad = self.tnrs.pop(0) + trans_type = name = res_id = source = trad = None + if self.extra_lines: + trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0) if not res_id: res_id = '0' else: - tmp_tnrs = [] + comments = [] + targets = [] line = None fuzzy = False while (not line): @@ -295,15 +299,20 @@ class TinyPoFile(object): while line.startswith('#'): if line.startswith('#~ '): break - if line.startswith('#:'): + if line.startswith('#.'): + line = line[2:].strip() + if not line.startswith('module:'): + comments.append(line) + elif line.startswith('#:'): for lpart in line[2:].strip().split(' '): trans_info = lpart.strip().split(':',2) if trans_info and len(trans_info) == 2: - # looks like the translation type is missing, which is not + # looks like the translation trans_type is missing, which is not # unexpected because it is not a GetText standard. Default: 'code' trans_info[:0] = ['code'] if trans_info and len(trans_info) == 3: - tmp_tnrs.append(trans_info) + # this is a ref line holding the destination info (model, field, record) + targets.append(trans_info) elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'): fuzzy = True line = self.lines.pop(0).strip() @@ -326,7 +335,7 @@ class TinyPoFile(object): # if the source is "" and it's the first msgid, it's the special # msgstr with the informations about the traduction and the # traductor; we skip it - self.tnrs = [] + self.extra_lines = [] while line: line = self.lines.pop(0).strip() return self.next() @@ -343,10 +352,11 @@ class TinyPoFile(object): trad += unquote(line) line = self.lines.pop(0).strip() - if tmp_tnrs and not fuzzy: - type, name, res_id = tmp_tnrs.pop(0) - for t, n, r in tmp_tnrs: - self.tnrs.append((t, n, r, source, trad)) + if targets and not fuzzy: + trans_type, name, res_id = targets.pop(0) + for t, n, r in targets: + if t == trans_type == 'code': continue + self.extra_lines.append((t, n, r, source, trad, comments)) self.first = False @@ -355,7 +365,7 @@ class TinyPoFile(object): self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', self.cur_line(), source[:30]) return self.next() - return type, name, res_id, source, trad + return trans_type, name, res_id, source, trad, '\n'.join(comments) def write_infos(self, modules): import openerp.release as release @@ -384,11 +394,13 @@ class TinyPoFile(object): } ) - def write(self, modules, tnrs, source, trad): + def write(self, modules, tnrs, source, trad, comments=None): plurial = len(modules) > 1 and 's' or '' self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules))) + if comments: + self.buffer.write(''.join(('#. %s\n' % c for c in comments))) code = False for typy, name, res_id in tnrs: @@ -415,44 +427,43 @@ class TinyPoFile(object): def trans_export(lang, modules, buffer, format, cr): - def _process(format, modules, rows, buffer, lang, newlang): + def _process(format, modules, rows, buffer, lang): if format == 'csv': - writer=csv.writer(buffer, 'UNIX') - for row in rows: - writer.writerow(row) + writer = csv.writer(buffer, 'UNIX') + # write header first + writer.writerow(("module","type","name","res_id","src","value")) + for module, type, name, res_id, src, trad, comments in rows: + # Comments are ignored by the CSV writer + writer.writerow((module, type, name, res_id, src, trad)) elif format == 'po': - rows.pop(0) writer = TinyPoFile(buffer) writer.write_infos(modules) # we now group the translations by source. That means one translation per source. grouped_rows = {} - for module, type, name, res_id, src, trad in rows: + for module, type, name, res_id, src, trad, comments in rows: row = grouped_rows.setdefault(src, {}) row.setdefault('modules', set()).add(module) if ('translation' not in row) or (not row['translation']): row['translation'] = trad row.setdefault('tnrs', []).append((type, name, res_id)) + row.setdefault('comments', set()).update(comments) for src, row in grouped_rows.items(): - writer.write(row['modules'], row['tnrs'], src, row['translation']) + writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments']) elif format == 'tgz': - rows.pop(0) rows_by_module = {} for row in rows: module = row[0] - # first row is the "header", as in csv, it will be popped - rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],]) - rows_by_module[module].append(row) - + rows_by_module.setdefault(module, []).append(row) tmpdir = tempfile.mkdtemp() for mod, modrows in rows_by_module.items(): tmpmoddir = join(tmpdir, mod, 'i18n') os.makedirs(tmpmoddir) - pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '') + pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '') buf = file(join(tmpmoddir, pofilename), 'w') - _process('po', [mod], modrows, buf, lang, newlang) + _process('po', [mod], modrows, buf, lang) buf.close() tar = tarfile.open(fileobj=buffer, mode='w|gz') @@ -463,16 +474,15 @@ def trans_export(lang, modules, buffer, format, cr): raise Exception(_('Unrecognized extension: must be one of ' '.csv, .po, or .tgz (received .%s).' % format)) - newlang = not bool(lang) - if newlang: - lang = 'en_US' - trans = trans_generate(lang, modules, cr) - if newlang and format!='csv': - for trx in trans: - trx[-1] = '' - modules = set([t[0] for t in trans[1:]]) - _process(format, modules, trans, buffer, lang, newlang) - del trans + 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:]]) + _process(format, modules, translations, buffer, lang) + del translations def trans_parse_xsl(de): res = [] @@ -535,6 +545,46 @@ def in_modules(object_name, modules): module = module_dict.get(module, module) return module in modules + +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 + :param comment_tags: a list of translator tags to search for and + include in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` + tuples + :rtype: ``iterator`` + """ + result = [] + def handle_text(text, lineno): + text = (text or "").strip() + if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc. + result.append((lineno, None, text, [])) + + # not using elementTree.iterparse because we need to skip sub-trees in case + # the ancestor element had a reason to be skipped + def iter_elements(current_element): + for el in current_element: + if isinstance(el, SKIPPED_ELEMENT_TYPES): continue + if "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"): + handle_text(el.text, el.sourceline) + for att in ('title', 'alt', 'label', 'placeholder'): + if att in el.attrib: + handle_text(el.attrib[att], el.sourceline) + iter_elements(el) + handle_text(el.tail, el.sourceline) + + tree = etree.parse(fileobj) + iter_elements(tree.getroot()) + + return result + + def trans_generate(lang, modules, cr): dbname = cr.dbname @@ -566,8 +616,8 @@ def trans_generate(lang, modules, cr): cr.execute(query, query_param) _to_translate = [] - def push_translation(module, type, name, id, source): - tuple = (module, source, name, id, type) + def push_translation(module, type, name, id, source, comments=None): + tuple = (module, source, name, id, type, comments or []) if source and tuple not in _to_translate: _to_translate.append(tuple) @@ -717,7 +767,7 @@ def trans_generate(lang, modules, cr): if not hasattr(msg, '__call__'): push_translation(module, term_type, model, 0, encode(msg)) - for (model_id, model, module) in cr.fetchall(): + for (_, model, module) in cr.fetchall(): module = encode(module) model = encode(model) @@ -733,7 +783,6 @@ def trans_generate(lang, modules, cr): for constraint in getattr(model_obj, '_sql_constraints', []): push_constraint_msg(module, 'sql_constraint', model, constraint[2]) - # parse source code for _() calls def get_module_from_path(path, mod_paths=None): if not mod_paths: # First, construct a list of possible paths @@ -771,92 +820,71 @@ def trans_generate(lang, modules, cr): _logger.debug("Scanning modules at paths: ", path_list) mod_paths = [] - join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL) - join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL) - re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL) - re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL) - def export_code_terms_from_file(fname, path, root, terms_type): + def verified_module_filepaths(fname, path, root): fabsolutepath = join(root, fname) frelativepath = fabsolutepath[len(path):] + display_path = "addons%s" % frelativepath module = get_module_from_path(fabsolutepath, mod_paths=mod_paths) - is_mod_installed = module in installed_modules - if (('all' in modules) or (module in modules)) and is_mod_installed: - _logger.debug("Scanning code of %s at module: %s", frelativepath, module) - src_file = misc.file_open(fabsolutepath, subdir='') + if (('all' in modules) or (module in modules)) and module in installed_modules: + return module, fabsolutepath, frelativepath, display_path + return None, None, None, None + + def babel_extract_terms(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) + extra_comments = extra_comments or [] + if module: + src_file = open(fabsolutepath, 'r') try: - code_string = src_file.read() + for lineno, message, comments in extract.extract(extract_method, src_file, + keywords=extract_keywords): + push_translation(module, trans_type, display_path, lineno, + encode(message), comments + extra_comments) finally: src_file.close() - if module in installed_modules: - frelativepath = str("addons" + frelativepath) - ite = re_dquotes.finditer(code_string) - code_offset = 0 - code_line = 1 - for i in ite: - src = i.group(1) - if src.startswith('""'): - assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30]) - src = src[2:-2] - else: - src = join_dquotes.sub(r'\1', src) - # try to count the lines from the last pos to our place: - code_line += code_string[code_offset:i.start(1)].count('\n') - # now, since we did a binary read of a python source file, we - # have to expand pythonic escapes like the interpreter does. - src = src.decode('string_escape') - push_translation(module, terms_type, frelativepath, code_line, encode(src)) - code_line += i.group(1).count('\n') - code_offset = i.end() # we have counted newlines up to the match end - - ite = re_quotes.finditer(code_string) - code_offset = 0 #reset counters - code_line = 1 - for i in ite: - src = i.group(1) - if src.startswith("''"): - assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30]) - src = src[2:-2] - else: - src = join_quotes.sub(r'\1', src) - code_line += code_string[code_offset:i.start(1)].count('\n') - src = src.decode('string_escape') - push_translation(module, terms_type, frelativepath, code_line, encode(src)) - code_line += i.group(1).count('\n') - code_offset = i.end() # we have counted newlines up to the match end for path in path_list: _logger.debug("Scanning files of modules at %s", path) for root, dummy, files in osutil.walksymlinks(path): - for fname in itertools.chain(fnmatch.filter(files, '*.py')): - export_code_terms_from_file(fname, path, root, 'code') - for fname in itertools.chain(fnmatch.filter(files, '*.mako')): - export_code_terms_from_file(fname, path, root, 'report') + for fname in fnmatch.filter(files, '*.py'): + babel_extract_terms(fname, path, root) + for fname in fnmatch.filter(files, '*.mako'): + babel_extract_terms(fname, path, root, trans_type='report') + # 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}) + # QWeb template files + if fnmatch.fnmatch(root, '*/static/src/xml*'): + for fname in fnmatch.filter(files, '*.xml'): + babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb', + extra_comments=[WEB_TRANSLATION_COMMENT]) - - out = [["module","type","name","res_id","src","value"]] # header + out = [] _to_translate.sort() # translate strings marked as to be translated - for module, source, name, id, type in _to_translate: - trans = trans_obj._get_source(cr, uid, name, type, lang, source) - out.append([module, type, name, id, source, encode(trans) or '']) - + for module, source, name, id, type, comments in _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]) return out -def trans_load(cr, filename, lang, verbose=True, context=None): +def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None): try: fileobj = misc.file_open(filename) _logger.info("loading %s", filename) fileformat = os.path.splitext(filename)[-1][1:].lower() - r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context) + result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context) fileobj.close() - return r + return result 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, context=None): +def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None): """Populates the ir_translation table.""" if verbose: _logger.info('loading translation file for language %s', lang) @@ -868,8 +896,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, trans_obj = pool.get('ir.translation') iso_lang = misc.get_iso_codes(lang) try: - uid = 1 - ids = lang_obj.search(cr, uid, [('code','=', lang)]) + ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)]) if not ids: # lets create the language with locale information @@ -886,14 +913,14 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, break elif fileformat == 'po': reader = TinyPoFile(fileobj) - f = ['type', 'name', 'res_id', 'src', 'value'] + f = ['type', 'name', 'res_id', 'src', 'value', 'comments'] else: _logger.error('Bad file format: %s', fileformat) raise Exception(_('Bad file format')) # read the rest of the file line = 1 - irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context) + irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context) for row in reader: line += 1 @@ -903,39 +930,32 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # dictionary which holds values for this line of the csv file # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ..., - # 'src': ..., 'value': ...} - dic = {'lang': lang} - dic_module = False - for i in range(len(f)): - if f[i] in ('module',): - continue - dic[f[i]] = row[i] + # 'src': ..., 'value': ..., 'module':...} + dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments')) + dic['lang'] = lang + for i, field in enumerate(f): + dic[field] = row[i] # This would skip terms that fail to specify a res_id - if not dic.get('res_id', False): + if not dic.get('res_id'): continue 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 else: - try: - tmodel = dic['name'].split(',')[0] - if '.' in res_id: - tmodule, tname = res_id.split('.', 1) - else: - tmodule = dic_module - tname = res_id - dic['imd_model'] = tmodel - dic['imd_module'] = tmodule - dic['imd_name'] = tname - - dic['res_id'] = None - except Exception: - _logger.warning("Could not decode resource for %s, please fix the po file.", - dic['res_id'], exc_info=True) - dic['res_id'] = None + 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 + dic['res_id'] = None irt_cursor.push(dic)