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 UpdateableStr
43 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['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 registry = openerp.registry(cr.dbname)
231 res = registry['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 registry = openerp.registry(dbname)
624 trans_obj = registry.get('ir.translation')
625 model_data_obj = registry.get('ir.model.data')
627 l = registry.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 model not in registry:
672 _logger.error("Unable to find object %r", model)
675 exists = registry[model].exists(cr, uid, res_id)
677 _logger.warning("Unable to find object %r with id %d", model, res_id)
679 obj = registry[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 pass # TODO Can model really be 'ir.actions.wizard' ?
688 elif model=='ir.model.fields':
690 field_name = encode(obj.name)
691 except AttributeError, exc:
692 _logger.error("name error in %s: %s", xml_name, str(exc))
694 objmodel = registry.get(obj.model)
695 if not objmodel or not field_name in objmodel._columns:
697 field_def = objmodel._columns[field_name]
699 name = "%s,%s" % (encode(obj.model), field_name)
700 push_translation(module, 'field', name, 0, encode(field_def.string))
703 push_translation(module, 'help', name, 0, encode(field_def.help))
705 if field_def.translate:
706 ids = objmodel.search(cr, uid, [])
707 obj_values = objmodel.read(cr, uid, ids, [field_name])
708 for obj_value in obj_values:
709 res_id = obj_value['id']
710 if obj.name in ('ir.model', 'ir.ui.menu'):
712 model_data_ids = model_data_obj.search(cr, uid, [
713 ('model', '=', model),
714 ('res_id', '=', res_id),
716 if not model_data_ids:
717 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
719 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
720 for dummy, val in field_def.selection:
721 push_translation(module, 'selection', name, 0, encode(val))
723 elif model=='ir.actions.report.xml':
724 name = encode(obj.report_name)
727 fname = obj.report_rml
728 parse_func = trans_parse_rml
729 report_type = "report"
731 fname = obj.report_xsl
732 parse_func = trans_parse_xsl
734 if fname and obj.report_type in ('pdf', 'xsl'):
736 report_file = misc.file_open(fname)
738 d = etree.parse(report_file)
739 for t in parse_func(d.iter()):
740 push_translation(module, report_type, name, 0, t)
743 except (IOError, etree.XMLSyntaxError):
744 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
746 for field_name,field_def in obj._table._columns.items():
747 if field_def.translate:
748 name = model + "," + field_name
750 trad = getattr(obj, field_name) or ''
753 push_translation(module, 'model', name, xml_name, encode(trad))
755 # End of data for ir.model.data query results
757 cr.execute(query_models, query_param)
759 def push_constraint_msg(module, term_type, model, msg):
760 if not hasattr(msg, '__call__'):
761 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
763 def push_local_constraints(module, model, cons_type='sql_constraints'):
764 """Climb up the class hierarchy and ignore inherited constraints
765 from other modules"""
766 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
767 msg_pos = 2 if cons_type == 'sql_constraints' else 1
768 for cls in model.__class__.__mro__:
769 if getattr(cls, '_module', None) != module:
771 constraints = getattr(cls, '_local_' + cons_type, [])
772 for constraint in constraints:
773 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
775 for (_, model, module) in cr.fetchall():
776 if model not in registry:
777 _logger.error("Unable to find object %r", model)
780 model_obj = registry[model]
782 if model_obj._constraints:
783 push_local_constraints(module, model_obj, 'constraints')
785 if model_obj._sql_constraints:
786 push_local_constraints(module, model_obj, 'sql_constraints')
788 def get_module_paths():
789 # default addons path (base)
790 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))
791 mod_paths = { def_path }
792 ad_paths = map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
795 if not os.path.isabs(adp):
797 elif adp != def_path and adp.startswith(def_path):
798 mod_paths.add(adp[len(def_path)+1:])
801 def get_module_from_path(path, mod_paths):
803 if path.startswith(mp) and (os.path.dirname(path) != mp):
804 path = path[len(mp)+1:]
805 return path.split(os.path.sep)[0]
806 return 'base' # files that are not in a module are considered as being in 'base' module
808 modobj = registry['ir.module.module']
809 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
810 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
812 root_path = os.path.join(config.config['root_path'], 'addons')
814 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
815 if root_path in apaths:
818 path_list = [root_path,] + apaths
820 # Also scan these non-addon paths
821 for bin_path in ['osv', 'report' ]:
822 path_list.append(os.path.join(config.config['root_path'], bin_path))
824 _logger.debug("Scanning modules at paths: %s", path_list)
826 mod_paths = get_module_paths()
828 def verified_module_filepaths(fname, path, root):
829 fabsolutepath = join(root, fname)
830 frelativepath = fabsolutepath[len(path):]
831 display_path = "addons%s" % frelativepath
832 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
833 if ('all' in modules or module in modules) and module in installed_modules:
834 return module, fabsolutepath, frelativepath, display_path
835 return None, None, None, None
837 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
838 extra_comments=None, extract_keywords={'_': None}):
839 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
840 extra_comments = extra_comments or []
842 src_file = open(fabsolutepath, 'r')
844 for extracted in extract.extract(extract_method, src_file,
845 keywords=extract_keywords):
846 # Babel 0.9.6 yields lineno, message, comments
847 # Babel 1.3 yields lineno, message, comments, context
848 lineno, message, comments = extracted[:3]
849 push_translation(module, trans_type, display_path, lineno,
850 encode(message), comments + extra_comments)
852 _logger.exception("Failed to extract terms from %s", fabsolutepath)
856 for path in path_list:
857 _logger.debug("Scanning files of modules at %s", path)
858 for root, dummy, files in osutil.walksymlinks(path):
859 for fname in fnmatch.filter(files, '*.py'):
860 babel_extract_terms(fname, path, root)
861 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
862 for fname in fnmatch.filter(files, '*.mako'):
863 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
864 # Javascript source files in the static/src/js directory, rest is ignored (libs)
865 if fnmatch.fnmatch(root, '*/static/src/js*'):
866 for fname in fnmatch.filter(files, '*.js'):
867 babel_extract_terms(fname, path, root, 'javascript',
868 extra_comments=[WEB_TRANSLATION_COMMENT],
869 extract_keywords={'_t': None, '_lt': None})
870 # QWeb template files
871 if fnmatch.fnmatch(root, '*/static/src/xml*'):
872 for fname in fnmatch.filter(files, '*.xml'):
873 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
874 extra_comments=[WEB_TRANSLATION_COMMENT])
878 # translate strings marked as to be translated
879 for module, source, name, id, type, comments in _to_translate:
880 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
881 out.append([module, type, name, id, source, encode(trans) or '', comments])
884 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
886 fileobj = misc.file_open(filename)
887 _logger.info("loading %s", filename)
888 fileformat = os.path.splitext(filename)[-1][1:].lower()
889 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
894 _logger.error("couldn't read translation file %s", filename)
897 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
898 """Populates the ir_translation table."""
900 _logger.info('loading translation file for language %s', lang)
904 registry = openerp.registry(db_name)
905 lang_obj = registry.get('res.lang')
906 trans_obj = registry.get('ir.translation')
907 iso_lang = misc.get_iso_codes(lang)
909 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
912 # lets create the language with locale information
913 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
915 # Parse also the POT: it will possibly provide additional targets.
916 # (Because the POT comments are correct on Launchpad but not the
917 # PO comments due to a Launchpad limitation. See LP bug 933496.)
920 # now, the serious things: we read the language file
922 if fileformat == 'csv':
923 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
924 # read the first line of the file (it contains columns titles)
928 elif fileformat == 'po':
929 reader = TinyPoFile(fileobj)
930 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
932 # Make a reader for the POT file and be somewhat defensive for the
934 if fileobj.name.endswith('.po'):
936 # Normally the path looks like /path/to/xxx/i18n/lang.po
937 # and we try to find the corresponding
938 # /path/to/xxx/i18n/xxx.pot file.
939 head, _ = os.path.split(fileobj.name)
940 head2, _ = os.path.split(head)
941 head3, tail3 = os.path.split(head2)
942 pot_handle = misc.file_open(os.path.join(head3, tail3, 'i18n', tail3 + '.pot'))
943 pot_reader = TinyPoFile(pot_handle)
948 _logger.error('Bad file format: %s', fileformat)
949 raise Exception(_('Bad file format'))
951 # Read the POT `reference` comments, and keep them indexed by source
954 for type, name, res_id, src, _, comments in pot_reader:
956 pot_targets.setdefault(src, {'value': None, 'targets': []})
957 pot_targets[src]['targets'].append((type, name, res_id))
959 # read the rest of the file
960 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
962 def process_row(row):
963 """Process a single PO (or POT) entry."""
964 # skip empty rows and rows where the translation field (=last fiefd) is empty
965 #if (not row) or (not row[-1]):
968 # dictionary which holds values for this line of the csv file
969 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
970 # 'src': ..., 'value': ..., 'module':...}
971 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
973 for i, field in enumerate(f):
976 # Get the `reference` comments from the POT.
978 if pot_reader and src in pot_targets:
979 pot_targets[src]['targets'] = filter(lambda x: x != row[:3], pot_targets[src]['targets'])
980 pot_targets[src]['value'] = row[4]
981 if not pot_targets[src]['targets']:
984 # This would skip terms that fail to specify a res_id
985 if not dic.get('res_id'):
988 res_id = dic.pop('res_id')
989 if res_id and isinstance(res_id, (int, long)) \
990 or (isinstance(res_id, basestring) and res_id.isdigit()):
991 dic['res_id'] = int(res_id)
992 dic['module'] = module_name
994 tmodel = dic['name'].split(',')[0]
996 tmodule, tname = res_id.split('.', 1)
1000 dic['imd_model'] = tmodel
1001 dic['imd_name'] = tname
1002 dic['module'] = tmodule
1003 dic['res_id'] = None
1005 irt_cursor.push(dic)
1007 # First process the entries from the PO file (doing so also fills/removes
1008 # the entries from the POT file).
1012 # Then process the entries implied by the POT file (which is more
1013 # correct w.r.t. the targets) if some of them remain.
1015 for src in pot_targets:
1016 value = pot_targets[src]['value']
1017 for type, name, res_id in pot_targets[src]['targets']:
1018 pot_rows.append((type, name, res_id, src, value, comments))
1019 for row in pot_rows:
1023 trans_obj.clear_caches()
1025 _logger.info("translation file loaded succesfully")
1027 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
1028 _logger.exception("couldn't read translation file %s", filename)
1030 def get_locales(lang=None):
1032 lang = locale.getdefaultlocale()[0]
1035 lang = _LOCALE2WIN32.get(lang, lang)
1038 ln = locale._build_localename((lang, enc))
1040 nln = locale.normalize(ln)
1044 for x in process('utf8'): yield x
1046 prefenc = locale.getpreferredencoding()
1048 for x in process(prefenc): yield x
1052 'iso-8859-1': 'iso8859-15',
1054 }.get(prefenc.lower())
1056 for x in process(prefenc): yield x
1063 # locale.resetlocale is bugged with some locales.
1064 for ln in get_locales():
1066 return locale.setlocale(locale.LC_ALL, ln)
1067 except locale.Error:
1070 def load_language(cr, lang):
1071 """Loads a translation terms for a language.
1072 Used mainly to automate language loading at db initialization.
1074 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1077 registry = openerp.registry(cr.dbname)
1078 language_installer = registry['base.language.install']
1079 oid = language_installer.create(cr, SUPERUSER_ID, {'lang': lang})
1080 language_installer.lang_install(cr, SUPERUSER_ID, [oid], context=None)
1082 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: