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',
96 'id_ID': 'Indonesian_indonesia',
98 'mn': 'Cyrillic_Mongolian',
99 'no_NO': 'Norwegian_Norway',
100 'nn_NO': 'Norwegian-Nynorsk_Norway',
101 'pl': 'Polish_Poland',
102 'pt_PT': 'Portuguese_Portugal',
103 'pt_BR': 'Portuguese_Brazil',
104 'ro_RO': 'Romanian_Romania',
105 'ru_RU': 'Russian_Russia',
107 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
108 'sk_SK': 'Slovak_Slovakia',
109 'sl_SI': 'Slovenian_Slovenia',
110 #should find more specific locales for spanish countries,
111 #but better than nothing
112 'es_AR': 'Spanish_Spain',
113 'es_BO': 'Spanish_Spain',
114 'es_CL': 'Spanish_Spain',
115 'es_CO': 'Spanish_Spain',
116 'es_CR': 'Spanish_Spain',
117 'es_DO': 'Spanish_Spain',
118 'es_EC': 'Spanish_Spain',
119 'es_ES': 'Spanish_Spain',
120 'es_GT': 'Spanish_Spain',
121 'es_HN': 'Spanish_Spain',
122 'es_MX': 'Spanish_Spain',
123 'es_NI': 'Spanish_Spain',
124 'es_PA': 'Spanish_Spain',
125 'es_PE': 'Spanish_Spain',
126 'es_PR': 'Spanish_Spain',
127 'es_PY': 'Spanish_Spain',
128 'es_SV': 'Spanish_Spain',
129 'es_UY': 'Spanish_Spain',
130 'es_VE': 'Spanish_Spain',
131 'sv_SE': 'Swedish_Sweden',
132 'ta_IN': 'English_Australia',
133 'th_TH': 'Thai_Thailand',
135 'tr_TR': 'Turkish_Turkey',
136 'uk_UA': 'Ukrainian_Ukraine',
137 'vi_VN': 'Vietnamese_Viet Nam',
138 'tlh_TLH': 'Klingon',
143 class UNIX_LINE_TERMINATOR(csv.excel):
144 lineterminator = '\n'
146 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
149 # Warning: better use self.pool.get('ir.translation')._get_source if you can
151 def translate(cr, name, source_type, lang, source=None):
153 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))
155 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
157 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
158 res_trans = cr.fetchone()
159 res = res_trans and res_trans[0] or False
162 class GettextAlias(object):
165 # find current DB based on thread/worker db name (see netsvc)
166 db_name = getattr(threading.currentThread(), 'dbname', None)
168 return sql_db.db_connect(db_name)
170 def _get_cr(self, frame, allow_create=True):
172 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
174 s = frame.f_locals.get('self', {})
175 cr = getattr(s, 'cr', None)
176 if not cr and allow_create:
183 def _get_uid(self, frame):
184 return frame.f_locals.get('uid') or frame.f_locals.get('user')
186 def _get_lang(self, frame):
188 ctx = frame.f_locals.get('context')
190 kwargs = frame.f_locals.get('kwargs')
192 args = frame.f_locals.get('args')
193 if args and isinstance(args, (list, tuple)) \
194 and isinstance(args[-1], dict):
196 elif isinstance(kwargs, dict):
197 ctx = kwargs.get('context')
199 lang = ctx.get('lang')
200 s = frame.f_locals.get('self', {})
202 c = getattr(s, 'localcontext', None)
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 lang = pool.get('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 pool = pooler.get_pool(cr.dbname)
234 res = pool.get('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 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"))
557 res.extend(trans_parse_view(n))
560 # tests whether an object is in a list of modules
561 def in_modules(object_name, modules):
570 module = object_name.split('.')[0]
571 module = module_dict.get(module, module)
572 return module in modules
575 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
576 """Babel message extractor for qweb template files.
577 :param fileobj: the file-like object the messages should be extracted from
578 :param keywords: a list of keywords (i.e. function names) that should
579 be recognized as translation functions
580 :param comment_tags: a list of translator tags to search for and
581 include in the results
582 :param options: a dictionary of additional options (optional)
583 :return: an iterator over ``(lineno, funcname, message, comments)``
588 def handle_text(text, lineno):
589 text = (text or "").strip()
590 if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
591 result.append((lineno, None, text, []))
593 # not using elementTree.iterparse because we need to skip sub-trees in case
594 # the ancestor element had a reason to be skipped
595 def iter_elements(current_element):
596 for el in current_element:
597 if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
598 if "t-js" not in el.attrib and \
599 not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
600 not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
601 handle_text(el.text, el.sourceline)
602 for att in ('title', 'alt', 'label', 'placeholder'):
604 handle_text(el.attrib[att], el.sourceline)
606 handle_text(el.tail, el.sourceline)
608 tree = etree.parse(fileobj)
609 iter_elements(tree.getroot())
614 def trans_generate(lang, modules, cr):
617 pool = pooler.get_pool(dbname)
618 trans_obj = pool.get('ir.translation')
619 model_data_obj = pool.get('ir.model.data')
621 l = pool.models.items()
624 query = 'SELECT name, model, res_id, module' \
625 ' FROM ir_model_data'
627 query_models = """SELECT m.id, m.model, imd.module
628 FROM ir_model AS m, ir_model_data AS imd
629 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
631 if 'all_installed' in modules:
632 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
633 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
635 if 'all' not in modules:
636 query += ' WHERE module IN %s'
637 query_models += ' AND imd.module in %s'
638 query_param = (tuple(modules),)
639 query += ' ORDER BY module, model, name'
640 query_models += ' ORDER BY module, model'
642 cr.execute(query, query_param)
645 def push_translation(module, type, name, id, source, comments=None):
646 tuple = (module, source, name, id, type, comments or [])
647 # empty and one-letter terms are ignored, they probably are not meant to be
648 # translated, and would be very hard to translate anyway.
649 if not source or len(source.strip()) <= 1:
650 _logger.debug("Ignoring empty or 1-letter source term: %r", tuple)
652 if tuple not in _to_translate:
653 _to_translate.append(tuple)
656 if isinstance(s, unicode):
657 return s.encode('utf8')
660 for (xml_name,model,res_id,module) in cr.fetchall():
661 module = encode(module)
662 model = encode(model)
663 xml_name = "%s.%s" % (module, encode(xml_name))
665 if not pool.get(model):
666 _logger.error("Unable to find object %r", model)
669 exists = pool.get(model).exists(cr, uid, res_id)
671 _logger.warning("Unable to find object %r with id %d", model, res_id)
673 obj = pool.get(model).browse(cr, uid, res_id)
675 if model=='ir.ui.view':
676 d = etree.XML(encode(obj.arch))
677 for t in trans_parse_view(d):
678 push_translation(module, 'view', encode(obj.model), 0, t)
679 elif model=='ir.actions.wizard':
680 service_name = 'wizard.'+encode(obj.wiz_name)
681 import openerp.netsvc as netsvc
682 if netsvc.Service._services.get(service_name):
683 obj2 = netsvc.Service._services[service_name]
684 for state_name, state_def in obj2.states.iteritems():
685 if 'result' in state_def:
686 result = state_def['result']
687 if result['type'] != 'form':
689 name = "%s,%s" % (encode(obj.wiz_name), state_name)
692 'string': ('wizard_field', lambda s: [encode(s)]),
693 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
694 'help': ('help', lambda s: [encode(s)]),
698 if not result.has_key('fields'):
699 _logger.warning("res has no fields: %r", result)
701 for field_name, field_def in result['fields'].iteritems():
702 res_name = name + ',' + field_name
704 for fn in def_params:
706 transtype, modifier = def_params[fn]
707 for val in modifier(field_def[fn]):
708 push_translation(module, transtype, res_name, 0, val)
711 arch = result['arch']
712 if arch and not isinstance(arch, UpdateableStr):
714 for t in trans_parse_view(d):
715 push_translation(module, 'wizard_view', name, 0, t)
717 # export button labels
718 for but_args in result['state']:
719 button_name = but_args[0]
720 button_label = but_args[1]
721 res_name = name + ',' + button_name
722 push_translation(module, 'wizard_button', res_name, 0, button_label)
724 elif model=='ir.model.fields':
726 field_name = encode(obj.name)
727 except AttributeError, exc:
728 _logger.error("name error in %s: %s", xml_name, str(exc))
730 objmodel = pool.get(obj.model)
731 if not objmodel or not field_name in objmodel._columns:
733 field_def = objmodel._columns[field_name]
735 name = "%s,%s" % (encode(obj.model), field_name)
736 push_translation(module, 'field', name, 0, encode(field_def.string))
739 push_translation(module, 'help', name, 0, encode(field_def.help))
741 if field_def.translate:
742 ids = objmodel.search(cr, uid, [])
743 obj_values = objmodel.read(cr, uid, ids, [field_name])
744 for obj_value in obj_values:
745 res_id = obj_value['id']
746 if obj.name in ('ir.model', 'ir.ui.menu'):
748 model_data_ids = model_data_obj.search(cr, uid, [
749 ('model', '=', model),
750 ('res_id', '=', res_id),
752 if not model_data_ids:
753 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
755 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
756 for dummy, val in field_def.selection:
757 push_translation(module, 'selection', name, 0, encode(val))
759 elif model=='ir.actions.report.xml':
760 name = encode(obj.report_name)
763 fname = obj.report_rml
764 parse_func = trans_parse_rml
765 report_type = "report"
767 fname = obj.report_xsl
768 parse_func = trans_parse_xsl
770 if fname and obj.report_type in ('pdf', 'xsl'):
772 report_file = misc.file_open(fname)
774 d = etree.parse(report_file)
775 for t in parse_func(d.iter()):
776 push_translation(module, report_type, name, 0, t)
779 except (IOError, etree.XMLSyntaxError):
780 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
782 for field_name,field_def in obj._table._columns.items():
783 if field_def.translate:
784 name = model + "," + field_name
786 trad = getattr(obj, field_name) or ''
789 push_translation(module, 'model', name, xml_name, encode(trad))
791 # End of data for ir.model.data query results
793 cr.execute(query_models, query_param)
795 def push_constraint_msg(module, term_type, model, msg):
796 # Check presence of __call__ directly instead of using
797 # callable() because it will be deprecated as of Python 3.0
798 if not hasattr(msg, '__call__'):
799 push_translation(module, term_type, model, 0, encode(msg))
801 for (_, model, module) in cr.fetchall():
802 module = encode(module)
803 model = encode(model)
805 model_obj = pool.get(model)
808 _logger.error("Unable to find object %r", model)
811 for constraint in getattr(model_obj, '_constraints', []):
812 push_constraint_msg(module, 'constraint', model, constraint[1])
814 for constraint in getattr(model_obj, '_sql_constraints', []):
815 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
817 def get_module_from_path(path, mod_paths=None):
819 # First, construct a list of possible paths
820 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
821 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
824 mod_paths.append(adp)
825 if not os.path.isabs(adp):
826 mod_paths.append(adp)
827 elif adp.startswith(def_path):
828 mod_paths.append(adp[len(def_path)+1:])
830 if path.startswith(mp) and (os.path.dirname(path) != mp):
831 path = path[len(mp)+1:]
832 return path.split(os.path.sep)[0]
833 return 'base' # files that are not in a module are considered as being in 'base' module
835 modobj = pool.get('ir.module.module')
836 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
837 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
839 root_path = os.path.join(config.config['root_path'], 'addons')
841 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
842 if root_path in apaths:
845 path_list = [root_path,] + apaths
847 # Also scan these non-addon paths
848 for bin_path in ['osv', 'report' ]:
849 path_list.append(os.path.join(config.config['root_path'], bin_path))
851 _logger.debug("Scanning modules at paths: ", path_list)
855 def verified_module_filepaths(fname, path, root):
856 fabsolutepath = join(root, fname)
857 frelativepath = fabsolutepath[len(path):]
858 display_path = "addons%s" % frelativepath
859 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
860 if (('all' in modules) or (module in modules)) and module in installed_modules:
861 return module, fabsolutepath, frelativepath, display_path
862 return None, None, None, None
864 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
865 extra_comments=None, extract_keywords={'_': None}):
866 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
867 extra_comments = extra_comments or []
869 src_file = open(fabsolutepath, 'r')
871 for lineno, message, comments in extract.extract(extract_method, src_file,
872 keywords=extract_keywords):
873 push_translation(module, trans_type, display_path, lineno,
874 encode(message), comments + extra_comments)
876 _logger.exception("Failed to extract terms from %s", fabsolutepath)
880 for path in path_list:
881 _logger.debug("Scanning files of modules at %s", path)
882 for root, dummy, files in osutil.walksymlinks(path):
883 for fname in fnmatch.filter(files, '*.py'):
884 babel_extract_terms(fname, path, root)
885 for fname in fnmatch.filter(files, '*.mako'):
886 babel_extract_terms(fname, path, root, trans_type='report')
887 # Javascript source files in the static/src/js directory, rest is ignored (libs)
888 if fnmatch.fnmatch(root, '*/static/src/js*'):
889 for fname in fnmatch.filter(files, '*.js'):
890 babel_extract_terms(fname, path, root, 'javascript',
891 extra_comments=[WEB_TRANSLATION_COMMENT],
892 extract_keywords={'_t': None, '_lt': None})
893 # QWeb template files
894 if fnmatch.fnmatch(root, '*/static/src/xml*'):
895 for fname in fnmatch.filter(files, '*.xml'):
896 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
897 extra_comments=[WEB_TRANSLATION_COMMENT])
901 # translate strings marked as to be translated
902 for module, source, name, id, type, comments in _to_translate:
903 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
904 out.append([module, type, name, id, source, encode(trans) or '', comments])
907 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
909 fileobj = misc.file_open(filename)
910 _logger.info("loading %s", filename)
911 fileformat = os.path.splitext(filename)[-1][1:].lower()
912 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
917 _logger.error("couldn't read translation file %s", filename)
920 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
921 """Populates the ir_translation table."""
923 _logger.info('loading translation file for language %s', lang)
927 pool = pooler.get_pool(db_name)
928 lang_obj = pool.get('res.lang')
929 trans_obj = pool.get('ir.translation')
930 iso_lang = misc.get_iso_codes(lang)
932 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
935 # lets create the language with locale information
936 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
939 # now, the serious things: we read the language file
941 if fileformat == 'csv':
942 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
943 # read the first line of the file (it contains columns titles)
947 elif fileformat == 'po':
948 reader = TinyPoFile(fileobj)
949 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
951 _logger.error('Bad file format: %s', fileformat)
952 raise Exception(_('Bad file format'))
954 # read the rest of the file
956 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
960 # skip empty rows and rows where the translation field (=last fiefd) is empty
961 #if (not row) or (not row[-1]):
964 # dictionary which holds values for this line of the csv file
965 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
966 # 'src': ..., 'value': ..., 'module':...}
967 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
969 for i, field in enumerate(f):
972 # This would skip terms that fail to specify a res_id
973 if not dic.get('res_id'):
976 res_id = dic.pop('res_id')
977 if res_id and isinstance(res_id, (int, long)) \
978 or (isinstance(res_id, basestring) and res_id.isdigit()):
979 dic['res_id'] = int(res_id)
980 dic['module'] = module_name
982 tmodel = dic['name'].split(',')[0]
984 tmodule, tname = res_id.split('.', 1)
988 dic['imd_model'] = tmodel
989 dic['imd_name'] = tname
990 dic['module'] = tmodule
997 _logger.info("translation file loaded succesfully")
999 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
1000 _logger.exception("couldn't read translation file %s", filename)
1002 def get_locales(lang=None):
1004 lang = locale.getdefaultlocale()[0]
1007 lang = _LOCALE2WIN32.get(lang, lang)
1010 ln = locale._build_localename((lang, enc))
1012 nln = locale.normalize(ln)
1016 for x in process('utf8'): yield x
1018 prefenc = locale.getpreferredencoding()
1020 for x in process(prefenc): yield x
1024 'iso-8859-1': 'iso8859-15',
1026 }.get(prefenc.lower())
1028 for x in process(prefenc): yield x
1035 # locale.resetlocale is bugged with some locales.
1036 for ln in get_locales():
1038 return locale.setlocale(locale.LC_ALL, ln)
1039 except locale.Error:
1042 def load_language(cr, lang):
1043 """Loads a translation terms for a language.
1044 Used mainly to automate language loading at db initialization.
1046 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1049 pool = pooler.get_pool(cr.dbname)
1050 language_installer = pool.get('base.language.install')
1052 oid = language_installer.create(cr, uid, {'lang': lang})
1053 language_installer.lang_install(cr, uid, [oid], context=None)
1055 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: