1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
28 import openerp.sql_db as sql_db
34 from babel.messages import extract
35 from os.path import join
37 from datetime import datetime
38 from lxml import etree
42 from misc import SKIPPED_ELEMENT_TYPES
45 from openerp import SUPERUSER_ID
47 _logger = logging.getLogger(__name__)
49 # used to notify web client that these translations should be loaded in the UI
50 WEB_TRANSLATION_COMMENT = "openerp-web"
53 'af_ZA': 'Afrikaans_South Africa',
54 'sq_AL': 'Albanian_Albania',
55 'ar_SA': 'Arabic_Saudi Arabia',
56 'eu_ES': 'Basque_Spain',
57 'be_BY': 'Belarusian_Belarus',
58 'bs_BA': 'Serbian (Latin)',
59 'bg_BG': 'Bulgarian_Bulgaria',
60 'ca_ES': 'Catalan_Spain',
61 'hr_HR': 'Croatian_Croatia',
62 'zh_CN': 'Chinese_China',
63 'zh_TW': 'Chinese_Taiwan',
64 'cs_CZ': 'Czech_Czech Republic',
65 'da_DK': 'Danish_Denmark',
66 'nl_NL': 'Dutch_Netherlands',
67 'et_EE': 'Estonian_Estonia',
68 'fa_IR': 'Farsi_Iran',
69 'ph_PH': 'Filipino_Philippines',
70 'fi_FI': 'Finnish_Finland',
71 'fr_FR': 'French_France',
72 'fr_BE': 'French_France',
73 'fr_CH': 'French_France',
74 'fr_CA': 'French_France',
75 'ga': 'Scottish Gaelic',
76 'gl_ES': 'Galician_Spain',
77 'ka_GE': 'Georgian_Georgia',
78 'de_DE': 'German_Germany',
79 'el_GR': 'Greek_Greece',
80 'gu': 'Gujarati_India',
81 'he_IL': 'Hebrew_Israel',
83 'hu': 'Hungarian_Hungary',
84 'is_IS': 'Icelandic_Iceland',
85 'id_ID': 'Indonesian_indonesia',
86 'it_IT': 'Italian_Italy',
87 'ja_JP': 'Japanese_Japan',
90 'ko_KR': 'Korean_Korea',
92 'lt_LT': 'Lithuanian_Lithuania',
93 'lat': 'Latvian_Latvia',
94 'ml_IN': 'Malayalam_India',
96 'mn': 'Cyrillic_Mongolian',
97 'no_NO': 'Norwegian_Norway',
98 'nn_NO': 'Norwegian-Nynorsk_Norway',
99 'pl': 'Polish_Poland',
100 'pt_PT': 'Portuguese_Portugal',
101 'pt_BR': 'Portuguese_Brazil',
102 'ro_RO': 'Romanian_Romania',
103 'ru_RU': 'Russian_Russia',
104 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
105 'sk_SK': 'Slovak_Slovakia',
106 'sl_SI': 'Slovenian_Slovenia',
107 #should find more specific locales for spanish countries,
108 #but better than nothing
109 'es_AR': 'Spanish_Spain',
110 'es_BO': 'Spanish_Spain',
111 'es_CL': 'Spanish_Spain',
112 'es_CO': 'Spanish_Spain',
113 'es_CR': 'Spanish_Spain',
114 'es_DO': 'Spanish_Spain',
115 'es_EC': 'Spanish_Spain',
116 'es_ES': 'Spanish_Spain',
117 'es_GT': 'Spanish_Spain',
118 'es_HN': 'Spanish_Spain',
119 'es_MX': 'Spanish_Spain',
120 'es_NI': 'Spanish_Spain',
121 'es_PA': 'Spanish_Spain',
122 'es_PE': 'Spanish_Spain',
123 'es_PR': 'Spanish_Spain',
124 'es_PY': 'Spanish_Spain',
125 'es_SV': 'Spanish_Spain',
126 'es_UY': 'Spanish_Spain',
127 'es_VE': 'Spanish_Spain',
128 'sv_SE': 'Swedish_Sweden',
129 'ta_IN': 'English_Australia',
130 'th_TH': 'Thai_Thailand',
131 'tr_TR': 'Turkish_Turkey',
132 'uk_UA': 'Ukrainian_Ukraine',
133 'vi_VN': 'Vietnamese_Viet Nam',
134 'tlh_TLH': 'Klingon',
139 class UNIX_LINE_TERMINATOR(csv.excel):
140 lineterminator = '\n'
142 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
145 # Warning: better use self.pool.get('ir.translation')._get_source if you can
147 def translate(cr, name, source_type, lang, source=None):
149 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))
151 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
153 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
154 res_trans = cr.fetchone()
155 res = res_trans and res_trans[0] or False
158 class GettextAlias(object):
161 # find current DB based on thread/worker db name (see netsvc)
162 db_name = getattr(threading.currentThread(), 'dbname', None)
164 return sql_db.db_connect(db_name)
166 def _get_cr(self, frame, allow_create=True):
167 # try, in order: cr, cursor, self.env.cr, self.cr
168 if 'cr' in frame.f_locals:
169 return frame.f_locals['cr'], False
170 if 'cursor' in frame.f_locals:
171 return frame.f_locals['cursor'], False
172 s = frame.f_locals.get('self')
173 if hasattr(s, 'env'):
174 return s.env.cr, False
178 # create a new cursor
181 return db.cursor(), True
184 def _get_uid(self, frame):
185 # try, in order: uid, user, self.env.uid
186 if 'uid' in frame.f_locals:
187 return frame.f_locals['uid']
188 if 'user' in frame.f_locals:
189 return int(frame.f_locals['user']) # user may be a record
190 s = frame.f_locals.get('self')
193 def _get_lang(self, frame):
194 # try, in order: context.get('lang'), kwargs['context'].get('lang'),
195 # self.env.lang, self.localcontext.get('lang')
196 if 'context' in frame.f_locals:
197 return frame.f_locals['context'].get('lang')
198 kwargs = frame.f_locals.get('kwargs', {})
199 if 'context' in kwargs:
200 return kwargs['context'].get('lang')
201 s = frame.f_locals.get('self')
202 if hasattr(s, 'env'):
204 if hasattr(s, 'localcontext'):
205 return s.localcontext.get('lang')
206 # Last resort: attempt to guess the language of the user
207 # Pitfall: some operations are performed in sudo mode, and we
208 # don't know the originial uid, so the language may
209 # be wrong when the admin language differs.
210 pool = getattr(s, 'pool', None)
211 (cr, dummy) = self._get_cr(frame, allow_create=False)
212 uid = self._get_uid(frame)
213 if pool and cr and uid:
214 return pool['res.users'].context_get(cr, uid)['lang']
217 def __call__(self, source):
222 frame = inspect.currentframe()
228 lang = self._get_lang(frame)
230 cr, is_new_cr = self._get_cr(frame)
232 # Try to use ir.translation to benefit from global cache if possible
233 registry = openerp.registry(cr.dbname)
234 res = registry['ir.translation']._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
236 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
238 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
240 _logger.debug('translation went wrong for "%r", skipped', source)
241 # if so, double-check the root/base translations filenames
251 """Returns quoted PO term string, with special PO characters escaped"""
252 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
253 return '"%s"' % s.replace('\\','\\\\') \
254 .replace('"','\\"') \
255 .replace('\n', '\\n"\n"')
257 re_escaped_char = re.compile(r"(\\.)")
258 re_escaped_replacements = {'n': '\n', }
260 def _sub_replacement(match_obj):
261 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
264 """Returns unquoted PO term string, with special PO characters unescaped"""
265 return re_escaped_char.sub(_sub_replacement, str[1:-1])
267 # class to handle po files
268 class TinyPoFile(object):
269 def __init__(self, buffer):
272 def warn(self, msg, *args):
273 _logger.warning(msg, *args)
277 self.lines = self._get_lines()
278 self.lines_count = len(self.lines)
284 def _get_lines(self):
285 lines = self.buffer.readlines()
286 # remove the BOM (Byte Order Mark):
288 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
290 lines.append('') # ensure that the file ends with at least an empty line
294 return self.lines_count - len(self.lines)
297 trans_type = name = res_id = source = trad = None
299 trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0)
308 if 0 == len(self.lines):
309 raise StopIteration()
310 line = self.lines.pop(0).strip()
311 while line.startswith('#'):
312 if line.startswith('#~ '):
314 if line.startswith('#.'):
315 line = line[2:].strip()
316 if not line.startswith('module:'):
317 comments.append(line)
318 elif line.startswith('#:'):
319 for lpart in line[2:].strip().split(' '):
320 trans_info = lpart.strip().split(':',2)
321 if trans_info and len(trans_info) == 2:
322 # looks like the translation trans_type is missing, which is not
323 # unexpected because it is not a GetText standard. Default: 'code'
324 trans_info[:0] = ['code']
325 if trans_info and len(trans_info) == 3:
326 # this is a ref line holding the destination info (model, field, record)
327 targets.append(trans_info)
328 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
330 line = self.lines.pop(0).strip()
332 # allow empty lines between comments and msgid
333 line = self.lines.pop(0).strip()
334 if line.startswith('#~ '):
335 while line.startswith('#~ ') or not line.strip():
336 if 0 == len(self.lines):
337 raise StopIteration()
338 line = self.lines.pop(0)
339 # This has been a deprecated entry, don't return anything
342 if not line.startswith('msgid'):
343 raise Exception("malformed file: bad line: %s" % line)
344 source = unquote(line[6:])
345 line = self.lines.pop(0).strip()
346 if not source and self.first:
347 # if the source is "" and it's the first msgid, it's the special
348 # msgstr with the informations about the traduction and the
349 # traductor; we skip it
350 self.extra_lines = []
352 line = self.lines.pop(0).strip()
355 while not line.startswith('msgstr'):
357 raise Exception('malformed file at %d'% self.cur_line())
358 source += unquote(line)
359 line = self.lines.pop(0).strip()
361 trad = unquote(line[7:])
362 line = self.lines.pop(0).strip()
364 trad += unquote(line)
365 line = self.lines.pop(0).strip()
367 if targets and not fuzzy:
368 trans_type, name, res_id = targets.pop(0)
369 for t, n, r in targets:
370 if t == trans_type == 'code': continue
371 self.extra_lines.append((t, n, r, source, trad, comments))
377 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
378 self.cur_line(), source[:30])
380 return trans_type, name, res_id, source, trad, '\n'.join(comments)
382 def write_infos(self, modules):
383 import openerp.release as release
384 self.buffer.write("# Translation of %(project)s.\n" \
385 "# This file contains the translation of the following modules:\n" \
390 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
391 '''"Report-Msgid-Bugs-To: \\n"\n''' \
392 '''"POT-Creation-Date: %(now)s\\n"\n''' \
393 '''"PO-Revision-Date: %(now)s\\n"\n''' \
394 '''"Last-Translator: <>\\n"\n''' \
395 '''"Language-Team: \\n"\n''' \
396 '''"MIME-Version: 1.0\\n"\n''' \
397 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
398 '''"Content-Transfer-Encoding: \\n"\n''' \
399 '''"Plural-Forms: \\n"\n''' \
402 % { 'project': release.description,
403 'version': release.version,
404 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
405 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
409 def write(self, modules, tnrs, source, trad, comments=None):
411 plurial = len(modules) > 1 and 's' or ''
412 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
415 self.buffer.write(''.join(('#. %s\n' % c for c in comments)))
418 for typy, name, res_id in tnrs:
419 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
424 # only strings in python code are python formated
425 self.buffer.write("#, python-format\n")
427 if not isinstance(trad, unicode):
428 trad = unicode(trad, 'utf8')
429 if not isinstance(source, unicode):
430 source = unicode(source, 'utf8')
434 % (quote(source), quote(trad))
435 self.buffer.write(msg.encode('utf8'))
438 # Methods to export the translation file
440 def trans_export(lang, modules, buffer, format, cr):
442 def _process(format, modules, rows, buffer, lang):
444 writer = csv.writer(buffer, 'UNIX')
446 writer.writerow(("module","type","name","res_id","src","value"))
447 for module, type, name, res_id, src, trad, comments in rows:
448 # Comments are ignored by the CSV writer
449 writer.writerow((module, type, name, res_id, src, trad))
451 writer = TinyPoFile(buffer)
452 writer.write_infos(modules)
454 # we now group the translations by source. That means one translation per source.
456 for module, type, name, res_id, src, trad, comments in rows:
457 row = grouped_rows.setdefault(src, {})
458 row.setdefault('modules', set()).add(module)
459 if not row.get('translation') and trad != src:
460 row['translation'] = trad
461 row.setdefault('tnrs', []).append((type, name, res_id))
462 row.setdefault('comments', set()).update(comments)
464 for src, row in sorted(grouped_rows.items()):
466 # translation template, so no translation value
467 row['translation'] = ''
468 elif not row.get('translation'):
469 row['translation'] = src
470 writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
472 elif format == 'tgz':
476 rows_by_module.setdefault(module, []).append(row)
477 tmpdir = tempfile.mkdtemp()
478 for mod, modrows in rows_by_module.items():
479 tmpmoddir = join(tmpdir, mod, 'i18n')
480 os.makedirs(tmpmoddir)
481 pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
482 buf = file(join(tmpmoddir, pofilename), 'w')
483 _process('po', [mod], modrows, buf, lang)
486 tar = tarfile.open(fileobj=buffer, mode='w|gz')
491 raise Exception(_('Unrecognized extension: must be one of '
492 '.csv, .po, or .tgz (received .%s).' % format))
495 if not trans_lang and format == 'csv':
496 # CSV files are meant for translators and they need a starting point,
497 # so we at least put the original term in the translation column
499 translations = trans_generate(lang, modules, cr)
500 modules = set([t[0] for t in translations[1:]])
501 _process(format, modules, translations, buffer, lang)
504 def trans_parse_xsl(de):
505 return list(set(trans_parse_xsl_aux(de, False)))
507 def trans_parse_xsl_aux(de, t):
513 if isinstance(n, SKIPPED_ELEMENT_TYPES) or n.tag.startswith('{http://www.w3.org/1999/XSL/Transform}'):
516 l = n.text.strip().replace('\n',' ')
518 res.append(l.encode("utf8"))
520 l = n.tail.strip().replace('\n',' ')
522 res.append(l.encode("utf8"))
523 res.extend(trans_parse_xsl_aux(n, t))
526 def trans_parse_rml(de):
530 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
532 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
533 for s in string_list:
535 res.append(s.encode("utf8"))
536 res.extend(trans_parse_rml(n))
539 def trans_parse_view(de):
541 if not isinstance(de, SKIPPED_ELEMENT_TYPES) and de.text and de.text.strip():
542 res.append(de.text.strip().encode("utf8"))
543 if de.tail and de.tail.strip():
544 res.append(de.tail.strip().encode("utf8"))
545 if de.tag == 'attribute' and de.get("name") == 'string':
547 res.append(de.text.encode("utf8"))
549 res.append(de.get('string').encode("utf8"))
551 res.append(de.get('help').encode("utf8"))
553 res.append(de.get('sum').encode("utf8"))
554 if de.get("confirm"):
555 res.append(de.get('confirm').encode("utf8"))
556 if de.get("placeholder"):
557 res.append(de.get('placeholder').encode("utf8"))
559 res.extend(trans_parse_view(n))
562 # tests whether an object is in a list of modules
563 def in_modules(object_name, modules):
572 module = object_name.split('.')[0]
573 module = module_dict.get(module, module)
574 return module in modules
577 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
578 """Babel message extractor for qweb template files.
579 :param fileobj: the file-like object the messages should be extracted from
580 :param keywords: a list of keywords (i.e. function names) that should
581 be recognized as translation functions
582 :param comment_tags: a list of translator tags to search for and
583 include in the results
584 :param options: a dictionary of additional options (optional)
585 :return: an iterator over ``(lineno, funcname, message, comments)``
590 def handle_text(text, lineno):
591 text = (text or "").strip()
592 if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
593 result.append((lineno, None, text, []))
595 # not using elementTree.iterparse because we need to skip sub-trees in case
596 # the ancestor element had a reason to be skipped
597 def iter_elements(current_element):
598 for el in current_element:
599 if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
600 if "t-js" not in el.attrib and \
601 not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
602 not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
603 handle_text(el.text, el.sourceline)
604 for att in ('title', 'alt', 'label', 'placeholder'):
606 handle_text(el.attrib[att], el.sourceline)
608 handle_text(el.tail, el.sourceline)
610 tree = etree.parse(fileobj)
611 iter_elements(tree.getroot())
616 def trans_generate(lang, modules, cr):
619 registry = openerp.registry(dbname)
620 trans_obj = registry.get('ir.translation')
621 model_data_obj = registry.get('ir.model.data')
623 l = registry.models.items()
626 query = 'SELECT name, model, res_id, module' \
627 ' FROM ir_model_data'
629 query_models = """SELECT m.id, m.model, imd.module
630 FROM ir_model AS m, ir_model_data AS imd
631 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
633 if 'all_installed' in modules:
634 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
635 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
637 if 'all' not in modules:
638 query += ' WHERE module IN %s'
639 query_models += ' AND imd.module in %s'
640 query_param = (tuple(modules),)
641 query += ' ORDER BY module, model, name'
642 query_models += ' ORDER BY module, model'
644 cr.execute(query, query_param)
647 def push_translation(module, type, name, id, source, comments=None):
648 tuple = (module, source, name, id, type, comments or [])
649 # empty and one-letter terms are ignored, they probably are not meant to be
650 # translated, and would be very hard to translate anyway.
651 if not source or len(source.strip()) <= 1:
652 _logger.debug("Ignoring empty or 1-letter source term: %r", tuple)
654 if tuple not in _to_translate:
655 _to_translate.append(tuple)
658 if isinstance(s, unicode):
659 return s.encode('utf8')
662 for (xml_name,model,res_id,module) in cr.fetchall():
663 module = encode(module)
664 model = encode(model)
665 xml_name = "%s.%s" % (module, encode(xml_name))
667 if model not in registry:
668 _logger.error("Unable to find object %r", model)
671 if not registry[model]._translate:
672 # explicitly disabled
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 (objmodel is None or field_name not in objmodel._columns
696 or not objmodel._translate):
698 field_def = objmodel._columns[field_name]
700 name = "%s,%s" % (encode(obj.model), field_name)
701 push_translation(module, 'field', name, 0, encode(field_def.string))
704 push_translation(module, 'help', name, 0, encode(field_def.help))
706 if field_def.translate:
707 ids = objmodel.search(cr, uid, [])
708 obj_values = objmodel.read(cr, uid, ids, [field_name])
709 for obj_value in obj_values:
710 res_id = obj_value['id']
711 if obj.name in ('ir.model', 'ir.ui.menu'):
713 model_data_ids = model_data_obj.search(cr, uid, [
714 ('model', '=', model),
715 ('res_id', '=', res_id),
717 if not model_data_ids:
718 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
720 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
721 for dummy, val in field_def.selection:
722 push_translation(module, 'selection', name, 0, encode(val))
724 elif model=='ir.actions.report.xml':
725 name = encode(obj.report_name)
728 fname = obj.report_rml
729 parse_func = trans_parse_rml
730 report_type = "report"
732 fname = obj.report_xsl
733 parse_func = trans_parse_xsl
735 if fname and obj.report_type in ('pdf', 'xsl'):
737 report_file = misc.file_open(fname)
739 d = etree.parse(report_file)
740 for t in parse_func(d.iter()):
741 push_translation(module, report_type, name, 0, t)
744 except (IOError, etree.XMLSyntaxError):
745 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
747 for field_name, field_def in obj._columns.items():
748 if field_def.translate:
749 name = model + "," + field_name
751 trad = getattr(obj, field_name) or ''
754 push_translation(module, 'model', name, xml_name, encode(trad))
756 # End of data for ir.model.data query results
758 cr.execute(query_models, query_param)
760 def push_constraint_msg(module, term_type, model, msg):
761 if not hasattr(msg, '__call__'):
762 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
764 def push_local_constraints(module, model, cons_type='sql_constraints'):
765 """Climb up the class hierarchy and ignore inherited constraints
766 from other modules"""
767 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
768 msg_pos = 2 if cons_type == 'sql_constraints' else 1
769 for cls in model.__class__.__mro__:
770 if getattr(cls, '_module', None) != module:
772 constraints = getattr(cls, '_local_' + cons_type, [])
773 for constraint in constraints:
774 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
776 for (_, model, module) in cr.fetchall():
777 if model not in registry:
778 _logger.error("Unable to find object %r", model)
781 model_obj = registry[model]
783 if model_obj._constraints:
784 push_local_constraints(module, model_obj, 'constraints')
786 if model_obj._sql_constraints:
787 push_local_constraints(module, model_obj, 'sql_constraints')
790 modobj = registry['ir.module.module']
791 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
792 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
794 path_list = list(openerp.modules.module.ad_paths)
795 # Also scan these non-addon paths
796 for bin_path in ['osv', 'report' ]:
797 path_list.append(os.path.join(config.config['root_path'], bin_path))
799 _logger.debug("Scanning modules at paths: %s", path_list)
801 mod_paths = list(path_list)
803 def get_module_from_path(path):
805 if path.startswith(mp) and (os.path.dirname(path) != mp):
806 path = path[len(mp)+1:]
807 return path.split(os.path.sep)[0]
808 return 'base' # files that are not in a module are considered as being in 'base' module
810 def verified_module_filepaths(fname, path, root):
811 fabsolutepath = join(root, fname)
812 frelativepath = fabsolutepath[len(path):]
813 display_path = "addons%s" % frelativepath
814 module = get_module_from_path(fabsolutepath)
815 if ('all' in modules or module in modules) and module in installed_modules:
816 return module, fabsolutepath, frelativepath, display_path
817 return None, None, None, None
819 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
820 extra_comments=None, extract_keywords={'_': None}):
821 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
822 extra_comments = extra_comments or []
824 src_file = open(fabsolutepath, 'r')
826 for extracted in extract.extract(extract_method, src_file,
827 keywords=extract_keywords):
828 # Babel 0.9.6 yields lineno, message, comments
829 # Babel 1.3 yields lineno, message, comments, context
830 lineno, message, comments = extracted[:3]
831 push_translation(module, trans_type, display_path, lineno,
832 encode(message), comments + extra_comments)
834 _logger.exception("Failed to extract terms from %s", fabsolutepath)
838 for path in path_list:
839 _logger.debug("Scanning files of modules at %s", path)
840 for root, dummy, files in osutil.walksymlinks(path):
841 for fname in fnmatch.filter(files, '*.py'):
842 babel_extract_terms(fname, path, root)
843 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
844 for fname in fnmatch.filter(files, '*.mako'):
845 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
846 # Javascript source files in the static/src/js directory, rest is ignored (libs)
847 if fnmatch.fnmatch(root, '*/static/src/js*'):
848 for fname in fnmatch.filter(files, '*.js'):
849 babel_extract_terms(fname, path, root, 'javascript',
850 extra_comments=[WEB_TRANSLATION_COMMENT],
851 extract_keywords={'_t': None, '_lt': None})
852 # QWeb template files
853 if fnmatch.fnmatch(root, '*/static/src/xml*'):
854 for fname in fnmatch.filter(files, '*.xml'):
855 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
856 extra_comments=[WEB_TRANSLATION_COMMENT])
860 # translate strings marked as to be translated
861 for module, source, name, id, type, comments in _to_translate:
862 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
863 out.append([module, type, name, id, source, encode(trans) or '', comments])
866 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
868 fileobj = misc.file_open(filename)
869 _logger.info("loading %s", filename)
870 fileformat = os.path.splitext(filename)[-1][1:].lower()
871 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
876 _logger.error("couldn't read translation file %s", filename)
879 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
880 """Populates the ir_translation table."""
882 _logger.info('loading translation file for language %s', lang)
886 registry = openerp.registry(db_name)
887 lang_obj = registry.get('res.lang')
888 trans_obj = registry.get('ir.translation')
889 iso_lang = misc.get_iso_codes(lang)
891 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
894 # lets create the language with locale information
895 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
898 # now, the serious things: we read the language file
900 if fileformat == 'csv':
901 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
902 # read the first line of the file (it contains columns titles)
906 elif fileformat == 'po':
907 reader = TinyPoFile(fileobj)
908 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
910 _logger.error('Bad file format: %s', fileformat)
911 raise Exception(_('Bad file format'))
913 # read the rest of the file
915 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
919 # skip empty rows and rows where the translation field (=last fiefd) is empty
920 #if (not row) or (not row[-1]):
923 # dictionary which holds values for this line of the csv file
924 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
925 # 'src': ..., 'value': ..., 'module':...}
926 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
928 for i, field in enumerate(f):
931 # This would skip terms that fail to specify a res_id
932 if not dic.get('res_id'):
935 res_id = dic.pop('res_id')
936 if res_id and isinstance(res_id, (int, long)) \
937 or (isinstance(res_id, basestring) and res_id.isdigit()):
938 dic['res_id'] = int(res_id)
939 dic['module'] = module_name
941 tmodel = dic['name'].split(',')[0]
943 tmodule, tname = res_id.split('.', 1)
947 dic['imd_model'] = tmodel
948 dic['imd_name'] = tname
949 dic['module'] = tmodule
955 trans_obj.clear_caches()
957 _logger.info("translation file loaded succesfully")
959 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
960 _logger.exception("couldn't read translation file %s", filename)
962 def get_locales(lang=None):
964 lang = locale.getdefaultlocale()[0]
967 lang = _LOCALE2WIN32.get(lang, lang)
970 ln = locale._build_localename((lang, enc))
972 nln = locale.normalize(ln)
976 for x in process('utf8'): yield x
978 prefenc = locale.getpreferredencoding()
980 for x in process(prefenc): yield x
984 'iso-8859-1': 'iso8859-15',
986 }.get(prefenc.lower())
988 for x in process(prefenc): yield x
995 # locale.resetlocale is bugged with some locales.
996 for ln in get_locales():
998 return locale.setlocale(locale.LC_ALL, ln)
1002 def load_language(cr, lang):
1003 """Loads a translation terms for a language.
1004 Used mainly to automate language loading at db initialization.
1006 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1009 registry = openerp.registry(cr.dbname)
1010 language_installer = registry['base.language.install']
1011 oid = language_installer.create(cr, SUPERUSER_ID, {'lang': lang})
1012 language_installer.lang_install(cr, SUPERUSER_ID, [oid], context=None)
1014 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: