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.pooler as pooler
29 import openerp.sql_db as sql_db
35 from babel.messages import extract
36 from os.path import join
38 from datetime import datetime
39 from lxml import etree
43 from misc import UpdateableStr
44 from misc import SKIPPED_ELEMENT_TYPES
46 from openerp import SUPERUSER_ID
48 _logger = logging.getLogger(__name__)
50 # used to notify web client that these translations should be loaded in the UI
51 WEB_TRANSLATION_COMMENT = "openerp-web"
54 'af_ZA': 'Afrikaans_South Africa',
55 'sq_AL': 'Albanian_Albania',
56 'ar_SA': 'Arabic_Saudi Arabia',
57 'eu_ES': 'Basque_Spain',
58 'be_BY': 'Belarusian_Belarus',
59 'bs_BA': 'Serbian (Latin)',
60 'bg_BG': 'Bulgarian_Bulgaria',
61 'ca_ES': 'Catalan_Spain',
62 'hr_HR': 'Croatian_Croatia',
63 'zh_CN': 'Chinese_China',
64 'zh_TW': 'Chinese_Taiwan',
65 'cs_CZ': 'Czech_Czech Republic',
66 'da_DK': 'Danish_Denmark',
67 'nl_NL': 'Dutch_Netherlands',
68 'et_EE': 'Estonian_Estonia',
69 'fa_IR': 'Farsi_Iran',
70 'ph_PH': 'Filipino_Philippines',
71 'fi_FI': 'Finnish_Finland',
72 'fr_FR': 'French_France',
73 'fr_BE': 'French_France',
74 'fr_CH': 'French_France',
75 'fr_CA': 'French_France',
76 'ga': 'Scottish Gaelic',
77 'gl_ES': 'Galician_Spain',
78 'ka_GE': 'Georgian_Georgia',
79 'de_DE': 'German_Germany',
80 'el_GR': 'Greek_Greece',
81 'gu': 'Gujarati_India',
82 'he_IL': 'Hebrew_Israel',
84 'hu': 'Hungarian_Hungary',
85 'is_IS': 'Icelandic_Iceland',
86 'id_ID': 'Indonesian_indonesia',
87 'it_IT': 'Italian_Italy',
88 'ja_JP': 'Japanese_Japan',
91 'ko_KR': 'Korean_Korea',
93 'lt_LT': 'Lithuanian_Lithuania',
94 'lat': 'Latvian_Latvia',
95 'ml_IN': 'Malayalam_India',
97 'mn': 'Cyrillic_Mongolian',
98 'no_NO': 'Norwegian_Norway',
99 'nn_NO': 'Norwegian-Nynorsk_Norway',
100 'pl': 'Polish_Poland',
101 'pt_PT': 'Portuguese_Portugal',
102 'pt_BR': 'Portuguese_Brazil',
103 'ro_RO': 'Romanian_Romania',
104 'ru_RU': 'Russian_Russia',
105 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
106 'sk_SK': 'Slovak_Slovakia',
107 'sl_SI': 'Slovenian_Slovenia',
108 #should find more specific locales for spanish countries,
109 #but better than nothing
110 'es_AR': 'Spanish_Spain',
111 'es_BO': 'Spanish_Spain',
112 'es_CL': 'Spanish_Spain',
113 'es_CO': 'Spanish_Spain',
114 'es_CR': 'Spanish_Spain',
115 'es_DO': 'Spanish_Spain',
116 'es_EC': 'Spanish_Spain',
117 'es_ES': 'Spanish_Spain',
118 'es_GT': 'Spanish_Spain',
119 'es_HN': 'Spanish_Spain',
120 'es_MX': 'Spanish_Spain',
121 'es_NI': 'Spanish_Spain',
122 'es_PA': 'Spanish_Spain',
123 'es_PE': 'Spanish_Spain',
124 'es_PR': 'Spanish_Spain',
125 'es_PY': 'Spanish_Spain',
126 'es_SV': 'Spanish_Spain',
127 'es_UY': 'Spanish_Spain',
128 'es_VE': 'Spanish_Spain',
129 'sv_SE': 'Swedish_Sweden',
130 'ta_IN': 'English_Australia',
131 'th_TH': 'Thai_Thailand',
132 'tr_TR': 'Turkish_Turkey',
133 'uk_UA': 'Ukrainian_Ukraine',
134 'vi_VN': 'Vietnamese_Viet Nam',
135 'tlh_TLH': 'Klingon',
140 class UNIX_LINE_TERMINATOR(csv.excel):
141 lineterminator = '\n'
143 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
146 # Warning: better use self.pool.get('ir.translation')._get_source if you can
148 def translate(cr, name, source_type, lang, source=None):
150 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))
152 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
154 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
155 res_trans = cr.fetchone()
156 res = res_trans and res_trans[0] or False
159 class GettextAlias(object):
162 # find current DB based on thread/worker db name (see netsvc)
163 db_name = getattr(threading.currentThread(), 'dbname', None)
165 return sql_db.db_connect(db_name)
167 def _get_cr(self, frame, allow_create=True):
169 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
171 s = frame.f_locals.get('self', {})
172 cr = getattr(s, 'cr', None)
173 if not cr and allow_create:
180 def _get_uid(self, frame):
181 return frame.f_locals.get('uid') or frame.f_locals.get('user')
183 def _get_lang(self, frame):
185 ctx = frame.f_locals.get('context')
187 kwargs = frame.f_locals.get('kwargs')
189 args = frame.f_locals.get('args')
190 if args and isinstance(args, (list, tuple)) \
191 and isinstance(args[-1], dict):
193 elif isinstance(kwargs, dict):
194 ctx = kwargs.get('context')
196 lang = ctx.get('lang')
197 s = frame.f_locals.get('self', {})
199 c = getattr(s, 'localcontext', None)
203 # Last resort: attempt to guess the language of the user
204 # Pitfall: some operations are performed in sudo mode, and we
205 # don't know the originial uid, so the language may
206 # be wrong when the admin language differs.
207 pool = getattr(s, 'pool', None)
208 (cr, dummy) = self._get_cr(frame, allow_create=False)
209 uid = self._get_uid(frame)
210 if pool and cr and uid:
211 lang = pool.get('res.users').context_get(cr, uid)['lang']
214 def __call__(self, source):
219 frame = inspect.currentframe()
225 lang = self._get_lang(frame)
227 cr, is_new_cr = self._get_cr(frame)
229 # Try to use ir.translation to benefit from global cache if possible
230 pool = pooler.get_pool(cr.dbname)
231 res = pool.get('ir.translation')._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
233 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
235 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
237 _logger.debug('translation went wrong for "%r", skipped', source)
238 # if so, double-check the root/base translations filenames
248 """Returns quoted PO term string, with special PO characters escaped"""
249 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
250 return '"%s"' % s.replace('\\','\\\\') \
251 .replace('"','\\"') \
252 .replace('\n', '\\n"\n"')
254 re_escaped_char = re.compile(r"(\\.)")
255 re_escaped_replacements = {'n': '\n', }
257 def _sub_replacement(match_obj):
258 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
261 """Returns unquoted PO term string, with special PO characters unescaped"""
262 return re_escaped_char.sub(_sub_replacement, str[1:-1])
264 # class to handle po files
265 class TinyPoFile(object):
266 def __init__(self, buffer):
269 def warn(self, msg, *args):
270 _logger.warning(msg, *args)
274 self.lines = self._get_lines()
275 self.lines_count = len(self.lines)
281 def _get_lines(self):
282 lines = self.buffer.readlines()
283 # remove the BOM (Byte Order Mark):
285 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
287 lines.append('') # ensure that the file ends with at least an empty line
291 return self.lines_count - len(self.lines)
294 trans_type = name = res_id = source = trad = None
296 trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0)
305 if 0 == len(self.lines):
306 raise StopIteration()
307 line = self.lines.pop(0).strip()
308 while line.startswith('#'):
309 if line.startswith('#~ '):
311 if line.startswith('#.'):
312 line = line[2:].strip()
313 if not line.startswith('module:'):
314 comments.append(line)
315 elif line.startswith('#:'):
316 # Process the `reference` comments. Each line can specify
317 # multiple targets (e.g. model, view, code, selection,
318 # ...). For each target, we will return an additional
320 for lpart in line[2:].strip().split(' '):
321 trans_info = lpart.strip().split(':',2)
322 if trans_info and len(trans_info) == 2:
323 # looks like the translation trans_type is missing, which is not
324 # unexpected because it is not a GetText standard. Default: 'code'
325 trans_info[:0] = ['code']
326 if trans_info and len(trans_info) == 3:
327 # this is a ref line holding the destination info (model, field, record)
328 targets.append(trans_info)
329 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
331 line = self.lines.pop(0).strip()
333 # allow empty lines between comments and msgid
334 line = self.lines.pop(0).strip()
335 if line.startswith('#~ '):
336 while line.startswith('#~ ') or not line.strip():
337 if 0 == len(self.lines):
338 raise StopIteration()
339 line = self.lines.pop(0)
340 # This has been a deprecated entry, don't return anything
343 if not line.startswith('msgid'):
344 raise Exception("malformed file: bad line: %s" % line)
345 source = unquote(line[6:])
346 line = self.lines.pop(0).strip()
347 if not source and self.first:
348 # if the source is "" and it's the first msgid, it's the special
349 # msgstr with the informations about the traduction and the
350 # traductor; we skip it
351 self.extra_lines = []
353 line = self.lines.pop(0).strip()
356 while not line.startswith('msgstr'):
358 raise Exception('malformed file at %d'% self.cur_line())
359 source += unquote(line)
360 line = self.lines.pop(0).strip()
362 trad = unquote(line[7:])
363 line = self.lines.pop(0).strip()
365 trad += unquote(line)
366 line = self.lines.pop(0).strip()
368 if targets and not fuzzy:
369 # Use the first target for the current entry (returned at the
370 # end of this next() call), and keep the others to generate
371 # additional entries (returned the next next() calls).
372 trans_type, name, res_id = targets.pop(0)
373 for t, n, r in targets:
374 if t == trans_type == 'code': continue
375 self.extra_lines.append((t, n, r, source, trad, comments))
381 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
382 self.cur_line(), source[:30])
384 return trans_type, name, res_id, source, trad, '\n'.join(comments)
386 def write_infos(self, modules):
387 import openerp.release as release
388 self.buffer.write("# Translation of %(project)s.\n" \
389 "# This file contains the translation of the following modules:\n" \
394 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
395 '''"Report-Msgid-Bugs-To: \\n"\n''' \
396 '''"POT-Creation-Date: %(now)s\\n"\n''' \
397 '''"PO-Revision-Date: %(now)s\\n"\n''' \
398 '''"Last-Translator: <>\\n"\n''' \
399 '''"Language-Team: \\n"\n''' \
400 '''"MIME-Version: 1.0\\n"\n''' \
401 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
402 '''"Content-Transfer-Encoding: \\n"\n''' \
403 '''"Plural-Forms: \\n"\n''' \
406 % { 'project': release.description,
407 'version': release.version,
408 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
409 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
413 def write(self, modules, tnrs, source, trad, comments=None):
415 plurial = len(modules) > 1 and 's' or ''
416 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
419 self.buffer.write(''.join(('#. %s\n' % c for c in comments)))
422 for typy, name, res_id in tnrs:
423 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
428 # only strings in python code are python formated
429 self.buffer.write("#, python-format\n")
431 if not isinstance(trad, unicode):
432 trad = unicode(trad, 'utf8')
433 if not isinstance(source, unicode):
434 source = unicode(source, 'utf8')
438 % (quote(source), quote(trad))
439 self.buffer.write(msg.encode('utf8'))
442 # Methods to export the translation file
444 def trans_export(lang, modules, buffer, format, cr):
446 def _process(format, modules, rows, buffer, lang):
448 writer = csv.writer(buffer, 'UNIX')
450 writer.writerow(("module","type","name","res_id","src","value"))
451 for module, type, name, res_id, src, trad, comments in rows:
452 # Comments are ignored by the CSV writer
453 writer.writerow((module, type, name, res_id, src, trad))
455 writer = TinyPoFile(buffer)
456 writer.write_infos(modules)
458 # we now group the translations by source. That means one translation per source.
460 for module, type, name, res_id, src, trad, comments in rows:
461 row = grouped_rows.setdefault(src, {})
462 row.setdefault('modules', set()).add(module)
463 if not row.get('translation') and trad != src:
464 row['translation'] = trad
465 row.setdefault('tnrs', []).append((type, name, res_id))
466 row.setdefault('comments', set()).update(comments)
468 for src, row in grouped_rows.items():
470 # translation template, so no translation value
471 row['translation'] = ''
472 elif not row.get('translation'):
473 row['translation'] = src
474 writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
476 elif format == 'tgz':
480 rows_by_module.setdefault(module, []).append(row)
481 tmpdir = tempfile.mkdtemp()
482 for mod, modrows in rows_by_module.items():
483 tmpmoddir = join(tmpdir, mod, 'i18n')
484 os.makedirs(tmpmoddir)
485 pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
486 buf = file(join(tmpmoddir, pofilename), 'w')
487 _process('po', [mod], modrows, buf, lang)
490 tar = tarfile.open(fileobj=buffer, mode='w|gz')
495 raise Exception(_('Unrecognized extension: must be one of '
496 '.csv, .po, or .tgz (received .%s).' % format))
499 if not trans_lang and format == 'csv':
500 # CSV files are meant for translators and they need a starting point,
501 # so we at least put the original term in the translation column
503 translations = trans_generate(lang, modules, cr)
504 modules = set([t[0] for t in translations[1:]])
505 _process(format, modules, translations, buffer, lang)
508 def trans_parse_xsl(de):
509 return list(set(trans_parse_xsl_aux(de, False)))
511 def trans_parse_xsl_aux(de, t):
517 if isinstance(n, SKIPPED_ELEMENT_TYPES) or n.tag.startswith('{http://www.w3.org/1999/XSL/Transform}'):
520 l = n.text.strip().replace('\n',' ')
522 res.append(l.encode("utf8"))
524 l = n.tail.strip().replace('\n',' ')
526 res.append(l.encode("utf8"))
527 res.extend(trans_parse_xsl_aux(n, t))
530 def trans_parse_rml(de):
534 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
536 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
537 for s in string_list:
539 res.append(s.encode("utf8"))
540 res.extend(trans_parse_rml(n))
543 def trans_parse_view(de):
545 if not isinstance(de, SKIPPED_ELEMENT_TYPES) and de.text and de.text.strip():
546 res.append(de.text.strip().encode("utf8"))
547 if de.tail and de.tail.strip():
548 res.append(de.tail.strip().encode("utf8"))
549 if de.tag == 'attribute' and de.get("name") == 'string':
551 res.append(de.text.encode("utf8"))
553 res.append(de.get('string').encode("utf8"))
555 res.append(de.get('help').encode("utf8"))
557 res.append(de.get('sum').encode("utf8"))
558 if de.get("confirm"):
559 res.append(de.get('confirm').encode("utf8"))
560 if de.get("placeholder"):
561 res.append(de.get('placeholder').encode("utf8"))
563 res.extend(trans_parse_view(n))
566 # tests whether an object is in a list of modules
567 def in_modules(object_name, modules):
576 module = object_name.split('.')[0]
577 module = module_dict.get(module, module)
578 return module in modules
581 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
582 """Babel message extractor for qweb template files.
583 :param fileobj: the file-like object the messages should be extracted from
584 :param keywords: a list of keywords (i.e. function names) that should
585 be recognized as translation functions
586 :param comment_tags: a list of translator tags to search for and
587 include in the results
588 :param options: a dictionary of additional options (optional)
589 :return: an iterator over ``(lineno, funcname, message, comments)``
594 def handle_text(text, lineno):
595 text = (text or "").strip()
596 if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
597 result.append((lineno, None, text, []))
599 # not using elementTree.iterparse because we need to skip sub-trees in case
600 # the ancestor element had a reason to be skipped
601 def iter_elements(current_element):
602 for el in current_element:
603 if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
604 if "t-js" not in el.attrib and \
605 not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
606 not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
607 handle_text(el.text, el.sourceline)
608 for att in ('title', 'alt', 'label', 'placeholder'):
610 handle_text(el.attrib[att], el.sourceline)
612 handle_text(el.tail, el.sourceline)
614 tree = etree.parse(fileobj)
615 iter_elements(tree.getroot())
620 def trans_generate(lang, modules, cr):
623 pool = pooler.get_pool(dbname)
624 trans_obj = pool.get('ir.translation')
625 model_data_obj = pool.get('ir.model.data')
627 l = pool.models.items()
630 query = 'SELECT name, model, res_id, module' \
631 ' FROM ir_model_data'
633 query_models = """SELECT m.id, m.model, imd.module
634 FROM ir_model AS m, ir_model_data AS imd
635 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
637 if 'all_installed' in modules:
638 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
639 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
641 if 'all' not in modules:
642 query += ' WHERE module IN %s'
643 query_models += ' AND imd.module in %s'
644 query_param = (tuple(modules),)
645 query += ' ORDER BY module, model, name'
646 query_models += ' ORDER BY module, model'
648 cr.execute(query, query_param)
651 def push_translation(module, type, name, id, source, comments=None):
652 tuple = (module, source, name, id, type, comments or [])
653 # empty and one-letter terms are ignored, they probably are not meant to be
654 # translated, and would be very hard to translate anyway.
655 if not source or len(source.strip()) <= 1:
656 _logger.debug("Ignoring empty or 1-letter source term: %r", tuple)
658 if tuple not in _to_translate:
659 _to_translate.append(tuple)
662 if isinstance(s, unicode):
663 return s.encode('utf8')
666 for (xml_name,model,res_id,module) in cr.fetchall():
667 module = encode(module)
668 model = encode(model)
669 xml_name = "%s.%s" % (module, encode(xml_name))
671 if not pool.get(model):
672 _logger.error("Unable to find object %r", model)
675 exists = pool.get(model).exists(cr, uid, res_id)
677 _logger.warning("Unable to find object %r with id %d", model, res_id)
679 obj = pool.get(model).browse(cr, uid, res_id)
681 if model=='ir.ui.view':
682 d = etree.XML(encode(obj.arch))
683 for t in trans_parse_view(d):
684 push_translation(module, 'view', encode(obj.model), 0, t)
685 elif model=='ir.actions.wizard':
686 service_name = 'wizard.'+encode(obj.wiz_name)
687 import openerp.netsvc as netsvc
688 if netsvc.Service._services.get(service_name):
689 obj2 = netsvc.Service._services[service_name]
690 for state_name, state_def in obj2.states.iteritems():
691 if 'result' in state_def:
692 result = state_def['result']
693 if result['type'] != 'form':
695 name = "%s,%s" % (encode(obj.wiz_name), state_name)
698 'string': ('wizard_field', lambda s: [encode(s)]),
699 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
700 'help': ('help', lambda s: [encode(s)]),
704 if not result.has_key('fields'):
705 _logger.warning("res has no fields: %r", result)
707 for field_name, field_def in result['fields'].iteritems():
708 res_name = name + ',' + field_name
710 for fn in def_params:
712 transtype, modifier = def_params[fn]
713 for val in modifier(field_def[fn]):
714 push_translation(module, transtype, res_name, 0, val)
717 arch = result['arch']
718 if arch and not isinstance(arch, UpdateableStr):
720 for t in trans_parse_view(d):
721 push_translation(module, 'wizard_view', name, 0, t)
723 # export button labels
724 for but_args in result['state']:
725 button_name = but_args[0]
726 button_label = but_args[1]
727 res_name = name + ',' + button_name
728 push_translation(module, 'wizard_button', res_name, 0, button_label)
730 elif model=='ir.model.fields':
732 field_name = encode(obj.name)
733 except AttributeError, exc:
734 _logger.error("name error in %s: %s", xml_name, str(exc))
736 objmodel = pool.get(obj.model)
737 if not objmodel or not field_name in objmodel._columns:
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._table._columns.items():
789 if field_def.translate:
790 name = model + "," + field_name
792 trad = getattr(obj, field_name) or ''
795 push_translation(module, 'model', name, xml_name, encode(trad))
797 # End of data for ir.model.data query results
799 cr.execute(query_models, query_param)
801 def push_constraint_msg(module, term_type, model, msg):
802 if not hasattr(msg, '__call__'):
803 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
805 def push_local_constraints(module, model, cons_type='sql_constraints'):
806 """Climb up the class hierarchy and ignore inherited constraints
807 from other modules"""
808 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
809 msg_pos = 2 if cons_type == 'sql_constraints' else 1
810 for cls in model.__class__.__mro__:
811 if getattr(cls, '_module', None) != module:
813 constraints = getattr(cls, '_local_' + cons_type, [])
814 for constraint in constraints:
815 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
817 for (_, model, module) in cr.fetchall():
818 model_obj = pool.get(model)
821 _logger.error("Unable to find object %r", model)
824 if model_obj._constraints:
825 push_local_constraints(module, model_obj, 'constraints')
827 if model_obj._sql_constraints:
828 push_local_constraints(module, model_obj, 'sql_constraints')
830 def get_module_paths():
831 # default addons path (base)
832 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))
833 mod_paths = set([ def_path ])
834 ad_paths = map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
837 if not os.path.isabs(adp):
839 elif adp != def_path and adp.startswith(def_path):
840 mod_paths.add(adp[len(def_path)+1:])
841 return list(mod_paths)
843 def get_module_from_path(path, mod_paths):
845 if path.startswith(mp) and (os.path.dirname(path) != mp):
846 path = path[len(mp)+1:]
847 return path.split(os.path.sep)[0]
848 return 'base' # files that are not in a module are considered as being in 'base' module
850 modobj = pool.get('ir.module.module')
851 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
852 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
854 root_path = os.path.join(config.config['root_path'], 'addons')
856 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
857 if root_path in apaths:
860 path_list = [root_path,] + apaths
862 # Also scan these non-addon paths
863 for bin_path in ['osv', 'report' ]:
864 path_list.append(os.path.join(config.config['root_path'], bin_path))
866 _logger.debug("Scanning modules at paths: %s", path_list)
868 mod_paths = get_module_paths()
870 def verified_module_filepaths(fname, path, root):
871 fabsolutepath = join(root, fname)
872 frelativepath = fabsolutepath[len(path):]
873 display_path = "addons%s" % frelativepath
874 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
875 if ('all' in modules or module in modules) and module in installed_modules:
876 return module, fabsolutepath, frelativepath, display_path
877 return None, None, None, None
879 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
880 extra_comments=None, extract_keywords={'_': None}):
881 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
882 extra_comments = extra_comments or []
884 src_file = open(fabsolutepath, 'r')
886 for extracted in extract.extract(extract_method, src_file,
887 keywords=extract_keywords):
888 # Babel 0.9.6 yields lineno, message, comments
889 # Babel 1.3 yields lineno, message, comments, context
890 lineno, message, comments = extracted[:3]
891 push_translation(module, trans_type, display_path, lineno,
892 encode(message), comments + extra_comments)
894 _logger.exception("Failed to extract terms from %s", fabsolutepath)
898 for path in path_list:
899 _logger.debug("Scanning files of modules at %s", path)
900 for root, dummy, files in osutil.walksymlinks(path):
901 for fname in fnmatch.filter(files, '*.py'):
902 babel_extract_terms(fname, path, root)
903 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
904 for fname in fnmatch.filter(files, '*.mako'):
905 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
906 # Javascript source files in the static/src/js directory, rest is ignored (libs)
907 if fnmatch.fnmatch(root, '*/static/src/js*'):
908 for fname in fnmatch.filter(files, '*.js'):
909 babel_extract_terms(fname, path, root, 'javascript',
910 extra_comments=[WEB_TRANSLATION_COMMENT],
911 extract_keywords={'_t': None, '_lt': None})
912 # QWeb template files
913 if fnmatch.fnmatch(root, '*/static/src/xml*'):
914 for fname in fnmatch.filter(files, '*.xml'):
915 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
916 extra_comments=[WEB_TRANSLATION_COMMENT])
920 # translate strings marked as to be translated
921 for module, source, name, id, type, comments in _to_translate:
922 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
923 out.append([module, type, name, id, source, encode(trans) or '', comments])
926 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
928 fileobj = misc.file_open(filename)
929 _logger.info("loading %s", filename)
930 fileformat = os.path.splitext(filename)[-1][1:].lower()
931 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
936 _logger.error("couldn't read translation file %s", filename)
939 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
940 """Populates the ir_translation table."""
942 _logger.info('loading translation file for language %s', lang)
946 pool = pooler.get_pool(db_name)
947 lang_obj = pool.get('res.lang')
948 trans_obj = pool.get('ir.translation')
949 iso_lang = misc.get_iso_codes(lang)
951 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
954 # lets create the language with locale information
955 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
957 # Parse also the POT: it will possibly provide additional targets.
958 # (Because the POT comments are correct on Launchpad but not the
959 # PO comments due to a Launchpad limitation. See LP bug 933496.)
962 # now, the serious things: we read the language file
964 if fileformat == 'csv':
965 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
966 # read the first line of the file (it contains columns titles)
970 elif fileformat == 'po':
971 reader = TinyPoFile(fileobj)
972 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
974 # Make a reader for the POT file and be somewhat defensive for the
976 if fileobj.name.endswith('.po'):
978 # Normally the path looks like /path/to/xxx/i18n/lang.po
979 # and we try to find the corresponding
980 # /path/to/xxx/i18n/xxx.pot file.
981 head, _ = os.path.split(fileobj.name)
982 head2, _ = os.path.split(head)
983 head3, tail3 = os.path.split(head2)
984 pot_handle = misc.file_open(os.path.join(head3, tail3, 'i18n', tail3 + '.pot'))
985 pot_reader = TinyPoFile(pot_handle)
990 _logger.error('Bad file format: %s', fileformat)
991 raise Exception(_('Bad file format'))
993 # Read the POT `reference` comments, and keep them indexed by source
996 for type, name, res_id, src, _, comments in pot_reader:
998 pot_targets.setdefault(src, {'value': None, 'targets': []})
999 pot_targets[src]['targets'].append((type, name, res_id))
1001 # read the rest of the file
1002 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
1004 def process_row(row):
1005 """Process a single PO (or POT) entry."""
1006 # skip empty rows and rows where the translation field (=last fiefd) is empty
1007 #if (not row) or (not row[-1]):
1010 # dictionary which holds values for this line of the csv file
1011 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
1012 # 'src': ..., 'value': ..., 'module':...}
1013 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
1015 for i, field in enumerate(f):
1018 # Get the `reference` comments from the POT.
1020 if pot_reader and src in pot_targets:
1021 pot_targets[src]['targets'] = filter(lambda x: x != row[:3], pot_targets[src]['targets'])
1022 pot_targets[src]['value'] = row[4]
1023 if not pot_targets[src]['targets']:
1024 del pot_targets[src]
1026 # This would skip terms that fail to specify a res_id
1027 if not dic.get('res_id'):
1030 res_id = dic.pop('res_id')
1031 if res_id and isinstance(res_id, (int, long)) \
1032 or (isinstance(res_id, basestring) and res_id.isdigit()):
1033 dic['res_id'] = int(res_id)
1034 dic['module'] = module_name
1036 tmodel = dic['name'].split(',')[0]
1038 tmodule, tname = res_id.split('.', 1)
1042 dic['imd_model'] = tmodel
1043 dic['imd_name'] = tname
1044 dic['module'] = tmodule
1045 dic['res_id'] = None
1047 irt_cursor.push(dic)
1049 # First process the entries from the PO file (doing so also fills/removes
1050 # the entries from the POT file).
1054 # Then process the entries implied by the POT file (which is more
1055 # correct w.r.t. the targets) if some of them remain.
1057 for src in pot_targets:
1058 value = pot_targets[src]['value']
1059 for type, name, res_id in pot_targets[src]['targets']:
1060 pot_rows.append((type, name, res_id, src, value, comments))
1061 for row in pot_rows:
1065 trans_obj.clear_caches()
1067 _logger.info("translation file loaded succesfully")
1069 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
1070 _logger.exception("couldn't read translation file %s", filename)
1072 def get_locales(lang=None):
1074 lang = locale.getdefaultlocale()[0]
1077 lang = _LOCALE2WIN32.get(lang, lang)
1080 ln = locale._build_localename((lang, enc))
1082 nln = locale.normalize(ln)
1086 for x in process('utf8'): yield x
1088 prefenc = locale.getpreferredencoding()
1090 for x in process(prefenc): yield x
1094 'iso-8859-1': 'iso8859-15',
1096 }.get(prefenc.lower())
1098 for x in process(prefenc): yield x
1105 # locale.resetlocale is bugged with some locales.
1106 for ln in get_locales():
1108 return locale.setlocale(locale.LC_ALL, ln)
1109 except locale.Error:
1112 def load_language(cr, lang):
1113 """Loads a translation terms for a language.
1114 Used mainly to automate language loading at db initialization.
1116 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1119 pool = pooler.get_pool(cr.dbname)
1120 language_installer = pool.get('base.language.install')
1122 oid = language_installer.create(cr, uid, {'lang': lang})
1123 language_installer.lang_install(cr, uid, [oid], context=None)
1125 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: