1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
28 import openerp.sql_db as sql_db
34 from babel.messages import extract
35 from os.path import join
37 from datetime import datetime
38 from lxml import etree
42 from misc import SKIPPED_ELEMENT_TYPES
45 from openerp import SUPERUSER_ID
47 _logger = logging.getLogger(__name__)
49 # used to notify web client that these translations should be loaded in the UI
50 WEB_TRANSLATION_COMMENT = "openerp-web"
52 SKIPPED_ELEMENTS = ('script', 'style')
55 'af_ZA': 'Afrikaans_South Africa',
56 'sq_AL': 'Albanian_Albania',
57 'ar_SA': 'Arabic_Saudi Arabia',
58 'eu_ES': 'Basque_Spain',
59 'be_BY': 'Belarusian_Belarus',
60 'bs_BA': 'Bosnian_Bosnia and Herzegovina',
61 'bg_BG': 'Bulgarian_Bulgaria',
62 'ca_ES': 'Catalan_Spain',
63 'hr_HR': 'Croatian_Croatia',
64 'zh_CN': 'Chinese_China',
65 'zh_TW': 'Chinese_Taiwan',
66 'cs_CZ': 'Czech_Czech Republic',
67 'da_DK': 'Danish_Denmark',
68 'nl_NL': 'Dutch_Netherlands',
69 'et_EE': 'Estonian_Estonia',
70 'fa_IR': 'Farsi_Iran',
71 'ph_PH': 'Filipino_Philippines',
72 'fi_FI': 'Finnish_Finland',
73 'fr_FR': 'French_France',
74 'fr_BE': 'French_France',
75 'fr_CH': 'French_France',
76 'fr_CA': 'French_France',
77 'ga': 'Scottish Gaelic',
78 'gl_ES': 'Galician_Spain',
79 'ka_GE': 'Georgian_Georgia',
80 'de_DE': 'German_Germany',
81 'el_GR': 'Greek_Greece',
82 'gu': 'Gujarati_India',
83 'he_IL': 'Hebrew_Israel',
85 'hu': 'Hungarian_Hungary',
86 'is_IS': 'Icelandic_Iceland',
87 'id_ID': 'Indonesian_indonesia',
88 'it_IT': 'Italian_Italy',
89 'ja_JP': 'Japanese_Japan',
92 'ko_KR': 'Korean_Korea',
94 'lt_LT': 'Lithuanian_Lithuania',
95 'lat': 'Latvian_Latvia',
96 'ml_IN': 'Malayalam_India',
98 'mn': 'Cyrillic_Mongolian',
99 'no_NO': 'Norwegian_Norway',
100 'nn_NO': 'Norwegian-Nynorsk_Norway',
101 'pl': 'Polish_Poland',
102 'pt_PT': 'Portuguese_Portugal',
103 'pt_BR': 'Portuguese_Brazil',
104 'ro_RO': 'Romanian_Romania',
105 'ru_RU': 'Russian_Russia',
106 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
107 'sk_SK': 'Slovak_Slovakia',
108 'sl_SI': 'Slovenian_Slovenia',
109 #should find more specific locales for spanish countries,
110 #but better than nothing
111 'es_AR': 'Spanish_Spain',
112 'es_BO': 'Spanish_Spain',
113 'es_CL': 'Spanish_Spain',
114 'es_CO': 'Spanish_Spain',
115 'es_CR': 'Spanish_Spain',
116 'es_DO': 'Spanish_Spain',
117 'es_EC': 'Spanish_Spain',
118 'es_ES': 'Spanish_Spain',
119 'es_GT': 'Spanish_Spain',
120 'es_HN': 'Spanish_Spain',
121 'es_MX': 'Spanish_Spain',
122 'es_NI': 'Spanish_Spain',
123 'es_PA': 'Spanish_Spain',
124 'es_PE': 'Spanish_Spain',
125 'es_PR': 'Spanish_Spain',
126 'es_PY': 'Spanish_Spain',
127 'es_SV': 'Spanish_Spain',
128 'es_UY': 'Spanish_Spain',
129 'es_VE': 'Spanish_Spain',
130 'sv_SE': 'Swedish_Sweden',
131 'ta_IN': 'English_Australia',
132 'th_TH': 'Thai_Thailand',
133 'tr_TR': 'Turkish_Turkey',
134 'uk_UA': 'Ukrainian_Ukraine',
135 'vi_VN': 'Vietnamese_Viet Nam',
136 'tlh_TLH': 'Klingon',
141 class UNIX_LINE_TERMINATOR(csv.excel):
142 lineterminator = '\n'
144 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
147 # Warning: better use self.pool.get('ir.translation')._get_source if you can
149 def translate(cr, name, source_type, lang, source=None):
151 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s and src=%s', (lang, source_type, str(name), source))
153 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
155 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
156 res_trans = cr.fetchone()
157 res = res_trans and res_trans[0] or False
160 class GettextAlias(object):
163 # find current DB based on thread/worker db name (see netsvc)
164 db_name = getattr(threading.currentThread(), 'dbname', None)
166 return sql_db.db_connect(db_name)
168 def _get_cr(self, frame, allow_create=True):
169 # try, in order: cr, cursor, self.env.cr, self.cr
170 if 'cr' in frame.f_locals:
171 return frame.f_locals['cr'], False
172 if 'cursor' in frame.f_locals:
173 return frame.f_locals['cursor'], False
174 s = frame.f_locals.get('self')
175 if hasattr(s, 'env'):
176 return s.env.cr, False
180 # create a new cursor
183 return db.cursor(), True
186 def _get_uid(self, frame):
187 # try, in order: uid, user, self.env.uid
188 if 'uid' in frame.f_locals:
189 return frame.f_locals['uid']
190 if 'user' in frame.f_locals:
191 return int(frame.f_locals['user']) # user may be a record
192 s = frame.f_locals.get('self')
195 def _get_lang(self, frame):
196 # try, in order: context.get('lang'), kwargs['context'].get('lang'),
197 # self.env.lang, self.localcontext.get('lang')
199 if frame.f_locals.get('context'):
200 lang = frame.f_locals['context'].get('lang')
202 kwargs = frame.f_locals.get('kwargs', {})
203 if kwargs.get('context'):
204 lang = kwargs['context'].get('lang')
206 s = frame.f_locals.get('self')
207 if hasattr(s, 'env'):
210 if hasattr(s, 'localcontext'):
211 lang = s.localcontext.get('lang')
213 # Last resort: attempt to guess the language of the user
214 # Pitfall: some operations are performed in sudo mode, and we
215 # don't know the originial uid, so the language may
216 # be wrong when the admin language differs.
217 pool = getattr(s, 'pool', None)
218 (cr, dummy) = self._get_cr(frame, allow_create=False)
219 uid = self._get_uid(frame)
220 if pool and cr and uid:
221 lang = pool['res.users'].context_get(cr, uid)['lang']
224 def __call__(self, source):
229 frame = inspect.currentframe()
235 lang = self._get_lang(frame)
237 cr, is_new_cr = self._get_cr(frame)
239 # Try to use ir.translation to benefit from global cache if possible
240 registry = openerp.registry(cr.dbname)
241 res = registry['ir.translation']._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
243 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
245 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
247 _logger.debug('translation went wrong for "%r", skipped', source)
248 # if so, double-check the root/base translations filenames
258 """Returns quoted PO term string, with special PO characters escaped"""
259 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
260 return '"%s"' % s.replace('\\','\\\\') \
261 .replace('"','\\"') \
262 .replace('\n', '\\n"\n"')
264 re_escaped_char = re.compile(r"(\\.)")
265 re_escaped_replacements = {'n': '\n', }
267 def _sub_replacement(match_obj):
268 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
271 """Returns unquoted PO term string, with special PO characters unescaped"""
272 return re_escaped_char.sub(_sub_replacement, str[1:-1])
274 # class to handle po files
275 class TinyPoFile(object):
276 def __init__(self, buffer):
279 def warn(self, msg, *args):
280 _logger.warning(msg, *args)
284 self.lines = self._get_lines()
285 self.lines_count = len(self.lines)
291 def _get_lines(self):
292 lines = self.buffer.readlines()
293 # remove the BOM (Byte Order Mark):
295 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
297 lines.append('') # ensure that the file ends with at least an empty line
301 return self.lines_count - len(self.lines)
304 trans_type = name = res_id = source = trad = None
306 trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0)
315 if 0 == len(self.lines):
316 raise StopIteration()
317 line = self.lines.pop(0).strip()
318 while line.startswith('#'):
319 if line.startswith('#~ '):
321 if line.startswith('#.'):
322 line = line[2:].strip()
323 if not line.startswith('module:'):
324 comments.append(line)
325 elif line.startswith('#:'):
326 # Process the `reference` comments. Each line can specify
327 # multiple targets (e.g. model, view, code, selection,
328 # ...). For each target, we will return an additional
330 for lpart in line[2:].strip().split(' '):
331 trans_info = lpart.strip().split(':',2)
332 if trans_info and len(trans_info) == 2:
333 # looks like the translation trans_type is missing, which is not
334 # unexpected because it is not a GetText standard. Default: 'code'
335 trans_info[:0] = ['code']
336 if trans_info and len(trans_info) == 3:
337 # this is a ref line holding the destination info (model, field, record)
338 targets.append(trans_info)
339 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
341 line = self.lines.pop(0).strip()
343 # allow empty lines between comments and msgid
344 line = self.lines.pop(0).strip()
345 if line.startswith('#~ '):
346 while line.startswith('#~ ') or not line.strip():
347 if 0 == len(self.lines):
348 raise StopIteration()
349 line = self.lines.pop(0)
350 # This has been a deprecated entry, don't return anything
353 if not line.startswith('msgid'):
354 raise Exception("malformed file: bad line: %s" % line)
355 source = unquote(line[6:])
356 line = self.lines.pop(0).strip()
357 if not source and self.first:
358 # if the source is "" and it's the first msgid, it's the special
359 # msgstr with the informations about the traduction and the
360 # traductor; we skip it
361 self.extra_lines = []
363 line = self.lines.pop(0).strip()
366 while not line.startswith('msgstr'):
368 raise Exception('malformed file at %d'% self.cur_line())
369 source += unquote(line)
370 line = self.lines.pop(0).strip()
372 trad = unquote(line[7:])
373 line = self.lines.pop(0).strip()
375 trad += unquote(line)
376 line = self.lines.pop(0).strip()
378 if targets and not fuzzy:
379 # Use the first target for the current entry (returned at the
380 # end of this next() call), and keep the others to generate
381 # additional entries (returned the next next() calls).
382 trans_type, name, res_id = targets.pop(0)
383 for t, n, r in targets:
384 if t == trans_type == 'code': continue
385 self.extra_lines.append((t, n, r, source, trad, comments))
391 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
392 self.cur_line(), source[:30])
394 return trans_type, name, res_id, source, trad, '\n'.join(comments)
396 def write_infos(self, modules):
397 import openerp.release as release
398 self.buffer.write("# Translation of %(project)s.\n" \
399 "# This file contains the translation of the following modules:\n" \
404 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
405 '''"Report-Msgid-Bugs-To: \\n"\n''' \
406 '''"POT-Creation-Date: %(now)s\\n"\n''' \
407 '''"PO-Revision-Date: %(now)s\\n"\n''' \
408 '''"Last-Translator: <>\\n"\n''' \
409 '''"Language-Team: \\n"\n''' \
410 '''"MIME-Version: 1.0\\n"\n''' \
411 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
412 '''"Content-Transfer-Encoding: \\n"\n''' \
413 '''"Plural-Forms: \\n"\n''' \
416 % { 'project': release.description,
417 'version': release.version,
418 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
419 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
423 def write(self, modules, tnrs, source, trad, comments=None):
425 plurial = len(modules) > 1 and 's' or ''
426 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
429 self.buffer.write(''.join(('#. %s\n' % c for c in comments)))
432 for typy, name, res_id in tnrs:
433 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
438 # only strings in python code are python formated
439 self.buffer.write("#, python-format\n")
441 if not isinstance(trad, unicode):
442 trad = unicode(trad, 'utf8')
443 if not isinstance(source, unicode):
444 source = unicode(source, 'utf8')
448 % (quote(source), quote(trad))
449 self.buffer.write(msg.encode('utf8'))
452 # Methods to export the translation file
454 def trans_export(lang, modules, buffer, format, cr):
456 def _process(format, modules, rows, buffer, lang):
458 writer = csv.writer(buffer, 'UNIX')
460 writer.writerow(("module","type","name","res_id","src","value"))
461 for module, type, name, res_id, src, trad, comments in rows:
462 # Comments are ignored by the CSV writer
463 writer.writerow((module, type, name, res_id, src, trad))
465 writer = TinyPoFile(buffer)
466 writer.write_infos(modules)
468 # we now group the translations by source. That means one translation per source.
470 for module, type, name, res_id, src, trad, comments in rows:
471 row = grouped_rows.setdefault(src, {})
472 row.setdefault('modules', set()).add(module)
473 if not row.get('translation') and trad != src:
474 row['translation'] = trad
475 row.setdefault('tnrs', []).append((type, name, res_id))
476 row.setdefault('comments', set()).update(comments)
478 for src, row in sorted(grouped_rows.items()):
480 # translation template, so no translation value
481 row['translation'] = ''
482 elif not row.get('translation'):
483 row['translation'] = src
484 writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
486 elif format == 'tgz':
490 rows_by_module.setdefault(module, []).append(row)
491 tmpdir = tempfile.mkdtemp()
492 for mod, modrows in rows_by_module.items():
493 tmpmoddir = join(tmpdir, mod, 'i18n')
494 os.makedirs(tmpmoddir)
495 pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
496 buf = file(join(tmpmoddir, pofilename), 'w')
497 _process('po', [mod], modrows, buf, lang)
500 tar = tarfile.open(fileobj=buffer, mode='w|gz')
505 raise Exception(_('Unrecognized extension: must be one of '
506 '.csv, .po, or .tgz (received .%s).' % format))
509 if not trans_lang and format == 'csv':
510 # CSV files are meant for translators and they need a starting point,
511 # so we at least put the original term in the translation column
513 translations = trans_generate(lang, modules, cr)
514 modules = set([t[0] for t in translations[1:]])
515 _process(format, modules, translations, buffer, lang)
518 def trans_parse_xsl(de):
519 return list(set(trans_parse_xsl_aux(de, False)))
521 def trans_parse_xsl_aux(de, t):
527 if isinstance(n, SKIPPED_ELEMENT_TYPES) or n.tag.startswith('{http://www.w3.org/1999/XSL/Transform}'):
530 l = n.text.strip().replace('\n',' ')
532 res.append(l.encode("utf8"))
534 l = n.tail.strip().replace('\n',' ')
536 res.append(l.encode("utf8"))
537 res.extend(trans_parse_xsl_aux(n, t))
540 def trans_parse_rml(de):
544 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
546 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
547 for s in string_list:
549 res.append(s.encode("utf8"))
550 res.extend(trans_parse_rml(n))
553 def _push(callback, term, source_line):
554 """ Sanity check before pushing translation terms """
555 term = (term or "").strip().encode('utf8')
556 # Avoid non-char tokens like ':' '...' '.00' etc.
557 if len(term) > 8 or any(x.isalpha() for x in term):
558 callback(term, source_line)
560 def trans_parse_view(element, callback):
561 """ Helper method to recursively walk an etree document representing a
562 regular view and call ``callback(term)`` for each translatable term
563 that is found in the document.
565 :param ElementTree element: root of etree document to extract terms from
566 :param callable callback: a callable in the form ``f(term, source_line)``,
567 that will be called for each extracted term.
569 if (not isinstance(element, SKIPPED_ELEMENT_TYPES)
570 and element.tag.lower() not in SKIPPED_ELEMENTS
572 _push(callback, element.text, element.sourceline)
574 _push(callback, element.tail, element.sourceline)
575 for attr in ('string', 'help', 'sum', 'confirm', 'placeholder'):
576 value = element.get(attr)
578 _push(callback, value, element.sourceline)
580 trans_parse_view(n, callback)
582 # tests whether an object is in a list of modules
583 def in_modules(object_name, modules):
592 module = object_name.split('.')[0]
593 module = module_dict.get(module, module)
594 return module in modules
596 def _extract_translatable_qweb_terms(element, callback):
597 """ Helper method to walk an etree document representing
598 a QWeb template, and call ``callback(term)`` for each
599 translatable term that is found in the document.
601 :param ElementTree element: root of etree document to extract terms from
602 :param callable callback: a callable in the form ``f(term, source_line)``,
603 that will be called for each extracted term.
605 # not using elementTree.iterparse because we need to skip sub-trees in case
606 # the ancestor element had a reason to be skipped
608 if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
609 if (el.tag.lower() not in SKIPPED_ELEMENTS
610 and "t-js" not in el.attrib
611 and not ("t-jquery" in el.attrib and "t-operation" not in el.attrib)
612 and not ("t-translation" in el.attrib and
613 el.attrib["t-translation"].strip() == "off")):
614 _push(callback, el.text, el.sourceline)
615 for att in ('title', 'alt', 'label', 'placeholder'):
617 _push(callback, el.attrib[att], el.sourceline)
618 _extract_translatable_qweb_terms(el, callback)
619 _push(callback, el.tail, el.sourceline)
621 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
622 """Babel message extractor for qweb template files.
623 :param fileobj: the file-like object the messages should be extracted from
624 :param keywords: a list of keywords (i.e. function names) that should
625 be recognized as translation functions
626 :param comment_tags: a list of translator tags to search for and
627 include in the results
628 :param options: a dictionary of additional options (optional)
629 :return: an iterator over ``(lineno, funcname, message, comments)``
634 def handle_text(text, lineno):
635 result.append((lineno, None, text, []))
636 tree = etree.parse(fileobj)
637 _extract_translatable_qweb_terms(tree.getroot(), handle_text)
640 def trans_generate(lang, modules, cr):
643 registry = openerp.registry(dbname)
644 trans_obj = registry.get('ir.translation')
645 model_data_obj = registry.get('ir.model.data')
647 l = registry.models.items()
650 query = 'SELECT name, model, res_id, module' \
651 ' FROM ir_model_data'
653 query_models = """SELECT m.id, m.model, imd.module
654 FROM ir_model AS m, ir_model_data AS imd
655 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
657 if 'all_installed' in modules:
658 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
659 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
661 if 'all' not in modules:
662 query += ' WHERE module IN %s'
663 query_models += ' AND imd.module in %s'
664 query_param = (tuple(modules),)
665 query += ' ORDER BY module, model, name'
666 query_models += ' ORDER BY module, model'
668 cr.execute(query, query_param)
671 def push_translation(module, type, name, id, source, comments=None):
672 tuple = (module, source, name, id, type, comments or [])
673 # empty and one-letter terms are ignored, they probably are not meant to be
674 # translated, and would be very hard to translate anyway.
675 if not source or len(source.strip()) <= 1:
677 if tuple not in _to_translate:
678 _to_translate.append(tuple)
681 if isinstance(s, unicode):
682 return s.encode('utf8')
685 def push(mod, type, name, res_id, term):
686 term = (term or '').strip()
688 push_translation(mod, type, name, res_id, term)
690 def get_root_view(xml_id):
691 view = model_data_obj.xmlid_to_object(cr, uid, xml_id)
693 while view.mode != 'primary':
694 view = view.inherit_id
695 xml_id = view.get_external_id(cr, uid).get(view.id, xml_id)
698 for (xml_name,model,res_id,module) in cr.fetchall():
699 module = encode(module)
700 model = encode(model)
701 xml_name = "%s.%s" % (module, encode(xml_name))
703 if model not in registry:
704 _logger.error("Unable to find object %r", model)
707 if not registry[model]._translate:
708 # explicitly disabled
711 exists = registry[model].exists(cr, uid, res_id)
713 _logger.warning("Unable to find object %r with id %d", model, res_id)
715 obj = registry[model].browse(cr, uid, res_id)
717 if model=='ir.ui.view':
718 d = etree.XML(encode(obj.arch))
719 if obj.type == 'qweb':
720 view_id = get_root_view(xml_name)
721 push_qweb = lambda t,l: push(module, 'view', 'website', view_id, t)
722 _extract_translatable_qweb_terms(d, push_qweb)
724 push_view = lambda t,l: push(module, 'view', obj.model, xml_name, t)
725 trans_parse_view(d, push_view)
726 elif model=='ir.actions.wizard':
727 pass # TODO Can model really be 'ir.actions.wizard' ?
729 elif model=='ir.model.fields':
731 field_name = encode(obj.name)
732 except AttributeError, exc:
733 _logger.error("name error in %s: %s", xml_name, str(exc))
735 objmodel = registry.get(obj.model)
736 if (objmodel is None or field_name not in objmodel._columns
737 or not objmodel._translate):
739 field_def = objmodel._columns[field_name]
741 name = "%s,%s" % (encode(obj.model), field_name)
742 push_translation(module, 'field', name, 0, encode(field_def.string))
745 push_translation(module, 'help', name, 0, encode(field_def.help))
747 if field_def.translate:
748 ids = objmodel.search(cr, uid, [])
749 obj_values = objmodel.read(cr, uid, ids, [field_name])
750 for obj_value in obj_values:
751 res_id = obj_value['id']
752 if obj.name in ('ir.model', 'ir.ui.menu'):
754 model_data_ids = model_data_obj.search(cr, uid, [
755 ('model', '=', model),
756 ('res_id', '=', res_id),
758 if not model_data_ids:
759 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
761 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
762 for dummy, val in field_def.selection:
763 push_translation(module, 'selection', name, 0, encode(val))
765 elif model=='ir.actions.report.xml':
766 name = encode(obj.report_name)
769 fname = obj.report_rml
770 parse_func = trans_parse_rml
771 report_type = "report"
773 fname = obj.report_xsl
774 parse_func = trans_parse_xsl
776 if fname and obj.report_type in ('pdf', 'xsl'):
778 report_file = misc.file_open(fname)
780 d = etree.parse(report_file)
781 for t in parse_func(d.iter()):
782 push_translation(module, report_type, name, 0, t)
785 except (IOError, etree.XMLSyntaxError):
786 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
788 for field_name, field_def in obj._columns.items():
789 if model == 'ir.model' and field_name == 'name' and obj.name == obj.model:
790 # ignore model name if it is the technical one, nothing to translate
792 if field_def.translate:
793 name = model + "," + field_name
795 term = obj[field_name] or ''
798 push_translation(module, 'model', name, xml_name, encode(term))
800 # End of data for ir.model.data query results
802 cr.execute(query_models, query_param)
804 def push_constraint_msg(module, term_type, model, msg):
805 if not hasattr(msg, '__call__'):
806 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
808 def push_local_constraints(module, model, cons_type='sql_constraints'):
809 """Climb up the class hierarchy and ignore inherited constraints
810 from other modules"""
811 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
812 msg_pos = 2 if cons_type == 'sql_constraints' else 1
813 for cls in model.__class__.__mro__:
814 if getattr(cls, '_module', None) != module:
816 constraints = getattr(cls, '_local_' + cons_type, [])
817 for constraint in constraints:
818 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
820 for (_, model, module) in cr.fetchall():
821 if model not in registry:
822 _logger.error("Unable to find object %r", model)
825 model_obj = registry[model]
827 if model_obj._constraints:
828 push_local_constraints(module, model_obj, 'constraints')
830 if model_obj._sql_constraints:
831 push_local_constraints(module, model_obj, 'sql_constraints')
833 modobj = registry['ir.module.module']
834 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
835 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
837 path_list = list(openerp.modules.module.ad_paths)
838 # Also scan these non-addon paths
839 for bin_path in ['osv', 'report' ]:
840 path_list.append(os.path.join(config.config['root_path'], bin_path))
842 _logger.debug("Scanning modules at paths: %s", path_list)
844 mod_paths = list(path_list)
846 def get_module_from_path(path):
848 if path.startswith(mp) and (os.path.dirname(path) != mp):
849 path = path[len(mp)+1:]
850 return path.split(os.path.sep)[0]
851 return 'base' # files that are not in a module are considered as being in 'base' module
853 def verified_module_filepaths(fname, path, root):
854 fabsolutepath = join(root, fname)
855 frelativepath = fabsolutepath[len(path):]
856 display_path = "addons%s" % frelativepath
857 module = get_module_from_path(fabsolutepath)
858 if ('all' in modules or module in modules) and module in installed_modules:
859 return module, fabsolutepath, frelativepath, display_path
860 return None, None, None, None
862 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
863 extra_comments=None, extract_keywords={'_': None}):
864 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
865 extra_comments = extra_comments or []
867 src_file = open(fabsolutepath, 'r')
869 for extracted in extract.extract(extract_method, src_file,
870 keywords=extract_keywords):
871 # Babel 0.9.6 yields lineno, message, comments
872 # Babel 1.3 yields lineno, message, comments, context
873 lineno, message, comments = extracted[:3]
874 push_translation(module, trans_type, display_path, lineno,
875 encode(message), comments + extra_comments)
877 _logger.exception("Failed to extract terms from %s", fabsolutepath)
881 for path in path_list:
882 _logger.debug("Scanning files of modules at %s", path)
883 for root, dummy, files in osutil.walksymlinks(path):
884 for fname in fnmatch.filter(files, '*.py'):
885 babel_extract_terms(fname, path, root)
886 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
887 for fname in fnmatch.filter(files, '*.mako'):
888 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
889 # Javascript source files in the static/src/js directory, rest is ignored (libs)
890 if fnmatch.fnmatch(root, '*/static/src/js*'):
891 for fname in fnmatch.filter(files, '*.js'):
892 babel_extract_terms(fname, path, root, 'javascript',
893 extra_comments=[WEB_TRANSLATION_COMMENT],
894 extract_keywords={'_t': None, '_lt': None})
895 # QWeb template files
896 if fnmatch.fnmatch(root, '*/static/src/xml*'):
897 for fname in fnmatch.filter(files, '*.xml'):
898 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
899 extra_comments=[WEB_TRANSLATION_COMMENT])
903 # translate strings marked as to be translated
904 for module, source, name, id, type, comments in _to_translate:
905 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
906 out.append([module, type, name, id, source, encode(trans) or '', comments])
909 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
911 fileobj = misc.file_open(filename)
912 _logger.info("loading %s", filename)
913 fileformat = os.path.splitext(filename)[-1][1:].lower()
914 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
919 _logger.error("couldn't read translation file %s", filename)
922 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
923 """Populates the ir_translation table."""
925 _logger.info('loading translation file for language %s', lang)
929 registry = openerp.registry(db_name)
930 lang_obj = registry.get('res.lang')
931 trans_obj = registry.get('ir.translation')
932 iso_lang = misc.get_iso_codes(lang)
934 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
937 # lets create the language with locale information
938 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
940 # Parse also the POT: it will possibly provide additional targets.
941 # (Because the POT comments are correct on Launchpad but not the
942 # PO comments due to a Launchpad limitation. See LP bug 933496.)
945 # now, the serious things: we read the language file
947 if fileformat == 'csv':
948 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
949 # read the first line of the file (it contains columns titles)
953 elif fileformat == 'po':
954 reader = TinyPoFile(fileobj)
955 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
957 # Make a reader for the POT file and be somewhat defensive for the
959 if fileobj.name.endswith('.po'):
961 # Normally the path looks like /path/to/xxx/i18n/lang.po
962 # and we try to find the corresponding
963 # /path/to/xxx/i18n/xxx.pot file.
964 head, _ = os.path.split(fileobj.name)
965 head2, _ = os.path.split(head)
966 head3, tail3 = os.path.split(head2)
967 pot_handle = misc.file_open(os.path.join(head3, tail3, 'i18n', tail3 + '.pot'))
968 pot_reader = TinyPoFile(pot_handle)
973 _logger.error('Bad file format: %s', fileformat)
974 raise Exception(_('Bad file format'))
976 # Read the POT `reference` comments, and keep them indexed by source
979 for type, name, res_id, src, _, comments in pot_reader:
981 pot_targets.setdefault(src, {'value': None, 'targets': []})
982 pot_targets[src]['targets'].append((type, name, res_id))
984 # read the rest of the file
985 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
987 def process_row(row):
988 """Process a single PO (or POT) entry."""
989 # skip empty rows and rows where the translation field (=last fiefd) is empty
990 #if (not row) or (not row[-1]):
993 # dictionary which holds values for this line of the csv file
994 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
995 # 'src': ..., 'value': ..., 'module':...}
996 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
998 for i, field in enumerate(f):
1001 # Get the `reference` comments from the POT.
1003 if pot_reader and src in pot_targets:
1004 pot_targets[src]['targets'] = filter(lambda x: x != row[:3], pot_targets[src]['targets'])
1005 pot_targets[src]['value'] = row[4]
1006 if not pot_targets[src]['targets']:
1007 del pot_targets[src]
1009 # This would skip terms that fail to specify a res_id
1010 if not dic.get('res_id'):
1013 res_id = dic.pop('res_id')
1014 if res_id and isinstance(res_id, (int, long)) \
1015 or (isinstance(res_id, basestring) and res_id.isdigit()):
1016 dic['res_id'] = int(res_id)
1017 dic['module'] = module_name
1019 tmodel = dic['name'].split(',')[0]
1021 tmodule, tname = res_id.split('.', 1)
1025 dic['imd_model'] = tmodel
1026 dic['imd_name'] = tname
1027 dic['module'] = tmodule
1028 dic['res_id'] = None
1030 irt_cursor.push(dic)
1032 # First process the entries from the PO file (doing so also fills/removes
1033 # the entries from the POT file).
1037 # Then process the entries implied by the POT file (which is more
1038 # correct w.r.t. the targets) if some of them remain.
1040 for src in pot_targets:
1041 value = pot_targets[src]['value']
1042 for type, name, res_id in pot_targets[src]['targets']:
1043 pot_rows.append((type, name, res_id, src, value, comments))
1044 for row in pot_rows:
1048 trans_obj.clear_caches()
1050 _logger.info("translation file loaded succesfully")
1052 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
1053 _logger.exception("couldn't read translation file %s", filename)
1055 def get_locales(lang=None):
1057 lang = locale.getdefaultlocale()[0]
1060 lang = _LOCALE2WIN32.get(lang, lang)
1063 ln = locale._build_localename((lang, enc))
1065 nln = locale.normalize(ln)
1069 for x in process('utf8'): yield x
1071 prefenc = locale.getpreferredencoding()
1073 for x in process(prefenc): yield x
1077 'iso-8859-1': 'iso8859-15',
1079 }.get(prefenc.lower())
1081 for x in process(prefenc): yield x
1088 # locale.resetlocale is bugged with some locales.
1089 for ln in get_locales():
1091 return locale.setlocale(locale.LC_ALL, ln)
1092 except locale.Error:
1095 def load_language(cr, lang):
1096 """Loads a translation terms for a language.
1097 Used mainly to automate language loading at db initialization.
1099 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1102 registry = openerp.registry(cr.dbname)
1103 language_installer = registry['base.language.install']
1104 oid = language_installer.create(cr, SUPERUSER_ID, {'lang': lang})
1105 language_installer.lang_install(cr, SUPERUSER_ID, [oid], context=None)
1107 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: