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 exists = registry[model].exists(cr, uid, res_id)
673 _logger.warning("Unable to find object %r with id %d", model, res_id)
675 obj = registry[model].browse(cr, uid, res_id)
677 if model=='ir.ui.view':
678 d = etree.XML(encode(obj.arch))
679 for t in trans_parse_view(d):
680 push_translation(module, 'view', encode(obj.model), 0, t)
681 elif model=='ir.actions.wizard':
682 pass # TODO Can model really be 'ir.actions.wizard' ?
684 elif model=='ir.model.fields':
686 field_name = encode(obj.name)
687 except AttributeError, exc:
688 _logger.error("name error in %s: %s", xml_name, str(exc))
690 objmodel = registry.get(obj.model)
691 if objmodel is None or field_name not in objmodel._columns:
693 field_def = objmodel._columns[field_name]
695 name = "%s,%s" % (encode(obj.model), field_name)
696 push_translation(module, 'field', name, 0, encode(field_def.string))
699 push_translation(module, 'help', name, 0, encode(field_def.help))
701 if field_def.translate:
702 ids = objmodel.search(cr, uid, [])
703 obj_values = objmodel.read(cr, uid, ids, [field_name])
704 for obj_value in obj_values:
705 res_id = obj_value['id']
706 if obj.name in ('ir.model', 'ir.ui.menu'):
708 model_data_ids = model_data_obj.search(cr, uid, [
709 ('model', '=', model),
710 ('res_id', '=', res_id),
712 if not model_data_ids:
713 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
715 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
716 for dummy, val in field_def.selection:
717 push_translation(module, 'selection', name, 0, encode(val))
719 elif model=='ir.actions.report.xml':
720 name = encode(obj.report_name)
723 fname = obj.report_rml
724 parse_func = trans_parse_rml
725 report_type = "report"
727 fname = obj.report_xsl
728 parse_func = trans_parse_xsl
730 if fname and obj.report_type in ('pdf', 'xsl'):
732 report_file = misc.file_open(fname)
734 d = etree.parse(report_file)
735 for t in parse_func(d.iter()):
736 push_translation(module, report_type, name, 0, t)
739 except (IOError, etree.XMLSyntaxError):
740 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
742 for field_name, field_def in obj._columns.items():
743 if field_def.translate:
744 name = model + "," + field_name
746 trad = getattr(obj, field_name) or ''
749 push_translation(module, 'model', name, xml_name, encode(trad))
751 # End of data for ir.model.data query results
753 cr.execute(query_models, query_param)
755 def push_constraint_msg(module, term_type, model, msg):
756 if not hasattr(msg, '__call__'):
757 push_translation(encode(module), term_type, encode(model), 0, encode(msg))
759 def push_local_constraints(module, model, cons_type='sql_constraints'):
760 """Climb up the class hierarchy and ignore inherited constraints
761 from other modules"""
762 term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
763 msg_pos = 2 if cons_type == 'sql_constraints' else 1
764 for cls in model.__class__.__mro__:
765 if getattr(cls, '_module', None) != module:
767 constraints = getattr(cls, '_local_' + cons_type, [])
768 for constraint in constraints:
769 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
771 for (_, model, module) in cr.fetchall():
772 if model not in registry:
773 _logger.error("Unable to find object %r", model)
776 model_obj = registry[model]
778 if model_obj._constraints:
779 push_local_constraints(module, model_obj, 'constraints')
781 if model_obj._sql_constraints:
782 push_local_constraints(module, model_obj, 'sql_constraints')
785 modobj = registry['ir.module.module']
786 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
787 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
789 path_list = list(openerp.modules.module.ad_paths)
790 # Also scan these non-addon paths
791 for bin_path in ['osv', 'report' ]:
792 path_list.append(os.path.join(config.config['root_path'], bin_path))
794 _logger.debug("Scanning modules at paths: ", path_list)
796 mod_paths = list(path_list)
798 def get_module_from_path(path):
800 if path.startswith(mp) and (os.path.dirname(path) != mp):
801 path = path[len(mp)+1:]
802 return path.split(os.path.sep)[0]
803 return 'base' # files that are not in a module are considered as being in 'base' module
805 def verified_module_filepaths(fname, path, root):
806 fabsolutepath = join(root, fname)
807 frelativepath = fabsolutepath[len(path):]
808 display_path = "addons%s" % frelativepath
809 module = get_module_from_path(fabsolutepath)
810 if ('all' in modules or module in modules) and module in installed_modules:
811 return module, fabsolutepath, frelativepath, display_path
812 return None, None, None, None
814 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
815 extra_comments=None, extract_keywords={'_': None}):
816 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
817 extra_comments = extra_comments or []
819 src_file = open(fabsolutepath, 'r')
821 for extracted in extract.extract(extract_method, src_file,
822 keywords=extract_keywords):
823 # Babel 0.9.6 yields lineno, message, comments
824 # Babel 1.3 yields lineno, message, comments, context
825 lineno, message, comments = extracted[:3]
826 push_translation(module, trans_type, display_path, lineno,
827 encode(message), comments + extra_comments)
829 _logger.exception("Failed to extract terms from %s", fabsolutepath)
833 for path in path_list:
834 _logger.debug("Scanning files of modules at %s", path)
835 for root, dummy, files in osutil.walksymlinks(path):
836 for fname in fnmatch.filter(files, '*.py'):
837 babel_extract_terms(fname, path, root)
838 # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
839 for fname in fnmatch.filter(files, '*.mako'):
840 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
841 # Javascript source files in the static/src/js directory, rest is ignored (libs)
842 if fnmatch.fnmatch(root, '*/static/src/js*'):
843 for fname in fnmatch.filter(files, '*.js'):
844 babel_extract_terms(fname, path, root, 'javascript',
845 extra_comments=[WEB_TRANSLATION_COMMENT],
846 extract_keywords={'_t': None, '_lt': None})
847 # QWeb template files
848 if fnmatch.fnmatch(root, '*/static/src/xml*'):
849 for fname in fnmatch.filter(files, '*.xml'):
850 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
851 extra_comments=[WEB_TRANSLATION_COMMENT])
855 # translate strings marked as to be translated
856 for module, source, name, id, type, comments in _to_translate:
857 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
858 out.append([module, type, name, id, source, encode(trans) or '', comments])
861 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
863 fileobj = misc.file_open(filename)
864 _logger.info("loading %s", filename)
865 fileformat = os.path.splitext(filename)[-1][1:].lower()
866 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
871 _logger.error("couldn't read translation file %s", filename)
874 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
875 """Populates the ir_translation table."""
877 _logger.info('loading translation file for language %s', lang)
881 registry = openerp.registry(db_name)
882 lang_obj = registry.get('res.lang')
883 trans_obj = registry.get('ir.translation')
884 iso_lang = misc.get_iso_codes(lang)
886 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
889 # lets create the language with locale information
890 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
893 # now, the serious things: we read the language file
895 if fileformat == 'csv':
896 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
897 # read the first line of the file (it contains columns titles)
901 elif fileformat == 'po':
902 reader = TinyPoFile(fileobj)
903 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
905 _logger.error('Bad file format: %s', fileformat)
906 raise Exception(_('Bad file format'))
908 # read the rest of the file
910 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
914 # skip empty rows and rows where the translation field (=last fiefd) is empty
915 #if (not row) or (not row[-1]):
918 # dictionary which holds values for this line of the csv file
919 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
920 # 'src': ..., 'value': ..., 'module':...}
921 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
923 for i, field in enumerate(f):
926 # This would skip terms that fail to specify a res_id
927 if not dic.get('res_id'):
930 res_id = dic.pop('res_id')
931 if res_id and isinstance(res_id, (int, long)) \
932 or (isinstance(res_id, basestring) and res_id.isdigit()):
933 dic['res_id'] = int(res_id)
934 dic['module'] = module_name
936 tmodel = dic['name'].split(',')[0]
938 tmodule, tname = res_id.split('.', 1)
942 dic['imd_model'] = tmodel
943 dic['imd_name'] = tname
944 dic['module'] = tmodule
950 trans_obj.clear_caches()
952 _logger.info("translation file loaded succesfully")
954 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
955 _logger.exception("couldn't read translation file %s", filename)
957 def get_locales(lang=None):
959 lang = locale.getdefaultlocale()[0]
962 lang = _LOCALE2WIN32.get(lang, lang)
965 ln = locale._build_localename((lang, enc))
967 nln = locale.normalize(ln)
971 for x in process('utf8'): yield x
973 prefenc = locale.getpreferredencoding()
975 for x in process(prefenc): yield x
979 'iso-8859-1': 'iso8859-15',
981 }.get(prefenc.lower())
983 for x in process(prefenc): yield x
990 # locale.resetlocale is bugged with some locales.
991 for ln in get_locales():
993 return locale.setlocale(locale.LC_ALL, ln)
997 def load_language(cr, lang):
998 """Loads a translation terms for a language.
999 Used mainly to automate language loading at db initialization.
1001 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1004 registry = openerp.registry(cr.dbname)
1005 language_installer = registry['base.language.install']
1006 oid = language_installer.create(cr, SUPERUSER_ID, {'lang': lang})
1007 language_installer.lang_install(cr, SUPERUSER_ID, [oid], context=None)
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: