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 sorted(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 elif model == 'ir.model':
789 model_pool = pool.get(obj.model)
791 push_translation(module, 'code', '_description', 0, model_pool._description)
793 for field_name,field_def in obj._table._columns.items():
794 if field_def.translate:
795 name = model + "," + field_name
797 trad = getattr(obj, field_name) or ''
800 push_translation(module, 'model', name, xml_name, encode(trad))
802 # End of data for ir.model.data query results
804 cr.execute(query_models, query_param)
806 def push_constraint_msg(module, term_type, model, msg):
807 if not hasattr(msg, '__call__'):
808 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
810 def push_local_constraints(module, model, cons_type='sql_constraints'):
811 """Climb up the class hierarchy and ignore inherited constraints
812 from other modules"""
813 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
814 msg_pos = 2 if cons_type == 'sql_constraints' else 1
815 for cls in model.__class__.__mro__:
816 if getattr(cls, '_module', None) != module:
818 constraints = getattr(cls, '_local_' + cons_type, [])
819 for constraint in constraints:
820 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
822 for (_, model, module) in cr.fetchall():
823 model_obj = pool.get(model)
826 _logger.error("Unable to find object %r", model)
829 if model_obj._constraints:
830 push_local_constraints(module, model_obj, 'constraints')
832 if model_obj._sql_constraints:
833 push_local_constraints(module, model_obj, 'sql_constraints')
835 def get_module_paths():
836 # default addons path (base)
837 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))
838 mod_paths = set([ def_path ])
839 ad_paths = map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
842 if not os.path.isabs(adp):
844 elif adp != def_path and adp.startswith(def_path):
845 mod_paths.add(adp[len(def_path)+1:])
846 return list(mod_paths)
848 def get_module_from_path(path, mod_paths):
850 if path.startswith(mp) and (os.path.dirname(path) != mp):
851 path = path[len(mp)+1:]
852 return path.split(os.path.sep)[0]
853 return 'base' # files that are not in a module are considered as being in 'base' module
855 modobj = pool.get('ir.module.module')
856 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
857 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
859 root_path = os.path.join(config.config['root_path'], 'addons')
861 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
862 if root_path in apaths:
865 path_list = [root_path,] + apaths
867 # Also scan these non-addon paths
868 for bin_path in ['osv', 'report' ]:
869 path_list.append(os.path.join(config.config['root_path'], bin_path))
871 _logger.debug("Scanning modules at paths: %s", path_list)
873 mod_paths = get_module_paths()
875 def verified_module_filepaths(fname, path, root):
876 fabsolutepath = join(root, fname)
877 frelativepath = fabsolutepath[len(path):]
878 display_path = "addons%s" % frelativepath
879 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
880 if ('all' in modules or module in modules) and module in installed_modules:
881 return module, fabsolutepath, frelativepath, display_path
882 return None, None, None, None
884 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
885 extra_comments=None, extract_keywords={'_': None}):
886 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
887 extra_comments = extra_comments or []
889 src_file = open(fabsolutepath, 'r')
891 for extracted in extract.extract(extract_method, src_file,
892 keywords=extract_keywords):
893 # Babel 0.9.6 yields lineno, message, comments
894 # Babel 1.3 yields lineno, message, comments, context
895 lineno, message, comments = extracted[:3]
896 push_translation(module, trans_type, display_path, lineno,
897 encode(message), comments + extra_comments)
899 _logger.exception("Failed to extract terms from %s", fabsolutepath)
903 for path in path_list:
904 _logger.debug("Scanning files of modules at %s", path)
905 for root, dummy, files in osutil.walksymlinks(path):
906 for fname in fnmatch.filter(files, '*.py'):
907 babel_extract_terms(fname, path, root)
908 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
909 for fname in fnmatch.filter(files, '*.mako'):
910 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
911 # Javascript source files in the static/src/js directory, rest is ignored (libs)
912 if fnmatch.fnmatch(root, '*/static/src/js*'):
913 for fname in fnmatch.filter(files, '*.js'):
914 babel_extract_terms(fname, path, root, 'javascript',
915 extra_comments=[WEB_TRANSLATION_COMMENT],
916 extract_keywords={'_t': None, '_lt': None})
917 # QWeb template files
918 if fnmatch.fnmatch(root, '*/static/src/xml*'):
919 for fname in fnmatch.filter(files, '*.xml'):
920 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
921 extra_comments=[WEB_TRANSLATION_COMMENT])
925 # translate strings marked as to be translated
926 for module, source, name, id, type, comments in _to_translate:
927 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
928 out.append([module, type, name, id, source, encode(trans) or '', comments])
931 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
933 fileobj = misc.file_open(filename)
934 _logger.info("loading %s", filename)
935 fileformat = os.path.splitext(filename)[-1][1:].lower()
936 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
941 _logger.error("couldn't read translation file %s", filename)
944 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
945 """Populates the ir_translation table."""
947 _logger.info('loading translation file for language %s', lang)
951 pool = pooler.get_pool(db_name)
952 lang_obj = pool.get('res.lang')
953 trans_obj = pool.get('ir.translation')
954 iso_lang = misc.get_iso_codes(lang)
956 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
959 # lets create the language with locale information
960 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
962 # Parse also the POT: it will possibly provide additional targets.
963 # (Because the POT comments are correct on Launchpad but not the
964 # PO comments due to a Launchpad limitation. See LP bug 933496.)
967 # now, the serious things: we read the language file
969 if fileformat == 'csv':
970 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
971 # read the first line of the file (it contains columns titles)
975 elif fileformat == 'po':
976 reader = TinyPoFile(fileobj)
977 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
979 # Make a reader for the POT file and be somewhat defensive for the
981 if fileobj.name.endswith('.po'):
983 # Normally the path looks like /path/to/xxx/i18n/lang.po
984 # and we try to find the corresponding
985 # /path/to/xxx/i18n/xxx.pot file.
986 head, _ = os.path.split(fileobj.name)
987 head2, _ = os.path.split(head)
988 head3, tail3 = os.path.split(head2)
989 pot_handle = misc.file_open(os.path.join(head3, tail3, 'i18n', tail3 + '.pot'))
990 pot_reader = TinyPoFile(pot_handle)
995 _logger.error('Bad file format: %s', fileformat)
996 raise Exception(_('Bad file format'))
998 # Read the POT `reference` comments, and keep them indexed by source
1001 for type, name, res_id, src, _, comments in pot_reader:
1002 if type is not None:
1003 pot_targets.setdefault(src, {'value': None, 'targets': []})
1004 pot_targets[src]['targets'].append((type, name, res_id))
1006 # read the rest of the file
1007 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
1009 def process_row(row):
1010 """Process a single PO (or POT) entry."""
1011 # skip empty rows and rows where the translation field (=last fiefd) is empty
1012 #if (not row) or (not row[-1]):
1015 # dictionary which holds values for this line of the csv file
1016 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
1017 # 'src': ..., 'value': ..., 'module':...}
1018 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
1020 for i, field in enumerate(f):
1023 # Get the `reference` comments from the POT.
1025 if pot_reader and src in pot_targets:
1026 pot_targets[src]['targets'] = filter(lambda x: x != row[:3], pot_targets[src]['targets'])
1027 pot_targets[src]['value'] = row[4]
1028 if not pot_targets[src]['targets']:
1029 del pot_targets[src]
1031 # This would skip terms that fail to specify a res_id
1032 if not dic.get('res_id'):
1035 res_id = dic.pop('res_id')
1036 if res_id and isinstance(res_id, (int, long)) \
1037 or (isinstance(res_id, basestring) and res_id.isdigit()):
1038 dic['res_id'] = int(res_id)
1039 dic['module'] = module_name
1041 tmodel = dic['name'].split(',')[0]
1043 tmodule, tname = res_id.split('.', 1)
1047 dic['imd_model'] = tmodel
1048 dic['imd_name'] = tname
1049 dic['module'] = tmodule
1050 dic['res_id'] = None
1052 irt_cursor.push(dic)
1054 # First process the entries from the PO file (doing so also fills/removes
1055 # the entries from the POT file).
1059 # Then process the entries implied by the POT file (which is more
1060 # correct w.r.t. the targets) if some of them remain.
1062 for src in pot_targets:
1063 value = pot_targets[src]['value']
1064 for type, name, res_id in pot_targets[src]['targets']:
1065 pot_rows.append((type, name, res_id, src, value, comments))
1066 for row in pot_rows:
1070 trans_obj.clear_caches()
1072 _logger.info("translation file loaded succesfully")
1074 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
1075 _logger.exception("couldn't read translation file %s", filename)
1077 def get_locales(lang=None):
1079 lang = locale.getdefaultlocale()[0]
1082 lang = _LOCALE2WIN32.get(lang, lang)
1085 ln = locale._build_localename((lang, enc))
1087 nln = locale.normalize(ln)
1091 for x in process('utf8'): yield x
1093 prefenc = locale.getpreferredencoding()
1095 for x in process(prefenc): yield x
1099 'iso-8859-1': 'iso8859-15',
1101 }.get(prefenc.lower())
1103 for x in process(prefenc): yield x
1110 # locale.resetlocale is bugged with some locales.
1111 for ln in get_locales():
1113 return locale.setlocale(locale.LC_ALL, ln)
1114 except locale.Error:
1117 def load_language(cr, lang):
1118 """Loads a translation terms for a language.
1119 Used mainly to automate language loading at db initialization.
1121 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1124 pool = pooler.get_pool(cr.dbname)
1125 language_installer = pool.get('base.language.install')
1127 oid = language_installer.create(cr, uid, {'lang': lang})
1128 language_installer.lang_install(cr, uid, [oid], context=None)
1130 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: