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 ##############################################################################
29 import openerp.pooler as pooler
30 import openerp.sql_db as sql_db
36 from babel.messages import extract
37 from os.path import join
39 from datetime import datetime
40 from lxml import etree
44 from misc import UpdateableStr
45 from misc import SKIPPED_ELEMENT_TYPES
47 from openerp import SUPERUSER_ID
49 _logger = logging.getLogger(__name__)
51 # used to notify web client that these translations should be loaded in the UI
52 WEB_TRANSLATION_COMMENT = "openerp-web"
55 'af_ZA': 'Afrikaans_South Africa',
56 'sq_AL': 'Albanian_Albania',
57 'ar_SA': 'Arabic_Saudi Arabia',
58 'eu_ES': 'Basque_Spain',
59 'be_BY': 'Belarusian_Belarus',
60 'bs_BA': 'Serbian (Latin)',
61 'bg_BG': 'Bulgarian_Bulgaria',
62 'ca_ES': 'Catalan_Spain',
63 'hr_HR': 'Croatian_Croatia',
64 'zh_CN': 'Chinese_China',
65 'zh_TW': 'Chinese_Taiwan',
66 'cs_CZ': 'Czech_Czech Republic',
67 'da_DK': 'Danish_Denmark',
68 'nl_NL': 'Dutch_Netherlands',
69 'et_EE': 'Estonian_Estonia',
70 'fa_IR': 'Farsi_Iran',
71 'ph_PH': 'Filipino_Philippines',
72 'fi_FI': 'Finnish_Finland',
73 'fr_FR': 'French_France',
74 'fr_BE': 'French_France',
75 'fr_CH': 'French_France',
76 'fr_CA': 'French_France',
77 'ga': 'Scottish Gaelic',
78 'gl_ES': 'Galician_Spain',
79 'ka_GE': 'Georgian_Georgia',
80 'de_DE': 'German_Germany',
81 'el_GR': 'Greek_Greece',
82 'gu': 'Gujarati_India',
83 'he_IL': 'Hebrew_Israel',
85 'hu': 'Hungarian_Hungary',
86 'is_IS': 'Icelandic_Iceland',
87 'id_ID': 'Indonesian_indonesia',
88 'it_IT': 'Italian_Italy',
89 'ja_JP': 'Japanese_Japan',
92 'ko_KR': 'Korean_Korea',
94 'lt_LT': 'Lithuanian_Lithuania',
95 'lat': 'Latvian_Latvia',
96 'ml_IN': 'Malayalam_India',
97 'id_ID': 'Indonesian_indonesia',
99 'mn': 'Cyrillic_Mongolian',
100 'no_NO': 'Norwegian_Norway',
101 'nn_NO': 'Norwegian-Nynorsk_Norway',
102 'pl': 'Polish_Poland',
103 'pt_PT': 'Portuguese_Portugal',
104 'pt_BR': 'Portuguese_Brazil',
105 'ro_RO': 'Romanian_Romania',
106 'ru_RU': 'Russian_Russia',
108 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
109 'sk_SK': 'Slovak_Slovakia',
110 'sl_SI': 'Slovenian_Slovenia',
111 #should find more specific locales for spanish countries,
112 #but better than nothing
113 'es_AR': 'Spanish_Spain',
114 'es_BO': 'Spanish_Spain',
115 'es_CL': 'Spanish_Spain',
116 'es_CO': 'Spanish_Spain',
117 'es_CR': 'Spanish_Spain',
118 'es_DO': 'Spanish_Spain',
119 'es_EC': 'Spanish_Spain',
120 'es_ES': 'Spanish_Spain',
121 'es_GT': 'Spanish_Spain',
122 'es_HN': 'Spanish_Spain',
123 'es_MX': 'Spanish_Spain',
124 'es_NI': 'Spanish_Spain',
125 'es_PA': 'Spanish_Spain',
126 'es_PE': 'Spanish_Spain',
127 'es_PR': 'Spanish_Spain',
128 'es_PY': 'Spanish_Spain',
129 'es_SV': 'Spanish_Spain',
130 'es_UY': 'Spanish_Spain',
131 'es_VE': 'Spanish_Spain',
132 'sv_SE': 'Swedish_Sweden',
133 'ta_IN': 'English_Australia',
134 'th_TH': 'Thai_Thailand',
136 'tr_TR': 'Turkish_Turkey',
137 'uk_UA': 'Ukrainian_Ukraine',
138 'vi_VN': 'Vietnamese_Viet Nam',
139 'tlh_TLH': 'Klingon',
144 class UNIX_LINE_TERMINATOR(csv.excel):
145 lineterminator = '\n'
147 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
150 # Warning: better use self.pool.get('ir.translation')._get_source if you can
152 def translate(cr, name, source_type, lang, source=None):
154 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))
156 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
158 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
159 res_trans = cr.fetchone()
160 res = res_trans and res_trans[0] or False
163 class GettextAlias(object):
166 # find current DB based on thread/worker db name (see netsvc)
167 db_name = getattr(threading.currentThread(), 'dbname', None)
169 return sql_db.db_connect(db_name)
171 def _get_cr(self, frame):
173 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
175 s = frame.f_locals.get('self', {})
176 cr = getattr(s, 'cr', None)
184 def _get_lang(self, frame):
186 ctx = frame.f_locals.get('context')
188 kwargs = frame.f_locals.get('kwargs')
190 args = frame.f_locals.get('args')
191 if args and isinstance(args, (list, tuple)) \
192 and isinstance(args[-1], dict):
194 elif isinstance(kwargs, dict):
195 ctx = kwargs.get('context')
197 lang = ctx.get('lang')
199 s = frame.f_locals.get('self', {})
200 c = getattr(s, 'localcontext', None)
205 def __call__(self, source):
210 frame = inspect.currentframe()
216 lang = self._get_lang(frame)
218 cr, is_new_cr = self._get_cr(frame)
220 # Try to use ir.translation to benefit from global cache if possible
221 pool = pooler.get_pool(cr.dbname)
222 res = pool.get('ir.translation')._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
224 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
226 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
228 _logger.debug('translation went wrong for "%r", skipped', source)
229 # if so, double-check the root/base translations filenames
239 """Returns quoted PO term string, with special PO characters escaped"""
240 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
241 return '"%s"' % s.replace('\\','\\\\') \
242 .replace('"','\\"') \
243 .replace('\n', '\\n"\n"')
245 re_escaped_char = re.compile(r"(\\.)")
246 re_escaped_replacements = {'n': '\n', }
248 def _sub_replacement(match_obj):
249 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
252 """Returns unquoted PO term string, with special PO characters unescaped"""
253 return re_escaped_char.sub(_sub_replacement, str[1:-1])
255 # class to handle po files
256 class TinyPoFile(object):
257 def __init__(self, buffer):
260 def warn(self, msg, *args):
261 _logger.warning(msg, *args)
265 self.lines = self._get_lines()
266 self.lines_count = len(self.lines);
272 def _get_lines(self):
273 lines = self.buffer.readlines()
274 # remove the BOM (Byte Order Mark):
276 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
278 lines.append('') # ensure that the file ends with at least an empty line
282 return (self.lines_count - len(self.lines))
285 trans_type = name = res_id = source = trad = None
287 trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0)
296 if 0 == len(self.lines):
297 raise StopIteration()
298 line = self.lines.pop(0).strip()
299 while line.startswith('#'):
300 if line.startswith('#~ '):
302 if line.startswith('#.'):
303 line = line[2:].strip()
304 if not line.startswith('module:'):
305 comments.append(line)
306 elif line.startswith('#:'):
307 for lpart in line[2:].strip().split(' '):
308 trans_info = lpart.strip().split(':',2)
309 if trans_info and len(trans_info) == 2:
310 # looks like the translation trans_type is missing, which is not
311 # unexpected because it is not a GetText standard. Default: 'code'
312 trans_info[:0] = ['code']
313 if trans_info and len(trans_info) == 3:
314 # this is a ref line holding the destination info (model, field, record)
315 targets.append(trans_info)
316 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
318 line = self.lines.pop(0).strip()
320 # allow empty lines between comments and msgid
321 line = self.lines.pop(0).strip()
322 if line.startswith('#~ '):
323 while line.startswith('#~ ') or not line.strip():
324 if 0 == len(self.lines):
325 raise StopIteration()
326 line = self.lines.pop(0)
327 # This has been a deprecated entry, don't return anything
330 if not line.startswith('msgid'):
331 raise Exception("malformed file: bad line: %s" % line)
332 source = unquote(line[6:])
333 line = self.lines.pop(0).strip()
334 if not source and self.first:
335 # if the source is "" and it's the first msgid, it's the special
336 # msgstr with the informations about the traduction and the
337 # traductor; we skip it
338 self.extra_lines = []
340 line = self.lines.pop(0).strip()
343 while not line.startswith('msgstr'):
345 raise Exception('malformed file at %d'% self.cur_line())
346 source += unquote(line)
347 line = self.lines.pop(0).strip()
349 trad = unquote(line[7:])
350 line = self.lines.pop(0).strip()
352 trad += unquote(line)
353 line = self.lines.pop(0).strip()
355 if targets and not fuzzy:
356 trans_type, name, res_id = targets.pop(0)
357 for t, n, r in targets:
358 if t == trans_type == 'code': continue
359 self.extra_lines.append((t, n, r, source, trad, comments))
365 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
366 self.cur_line(), source[:30])
368 return trans_type, name, res_id, source, trad, '\n'.join(comments)
370 def write_infos(self, modules):
371 import openerp.release as release
372 self.buffer.write("# Translation of %(project)s.\n" \
373 "# This file contains the translation of the following modules:\n" \
378 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
379 '''"Report-Msgid-Bugs-To: \\n"\n''' \
380 '''"POT-Creation-Date: %(now)s\\n"\n''' \
381 '''"PO-Revision-Date: %(now)s\\n"\n''' \
382 '''"Last-Translator: <>\\n"\n''' \
383 '''"Language-Team: \\n"\n''' \
384 '''"MIME-Version: 1.0\\n"\n''' \
385 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
386 '''"Content-Transfer-Encoding: \\n"\n''' \
387 '''"Plural-Forms: \\n"\n''' \
390 % { 'project': release.description,
391 'version': release.version,
392 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
393 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
397 def write(self, modules, tnrs, source, trad, comments=None):
399 plurial = len(modules) > 1 and 's' or ''
400 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
403 self.buffer.write(''.join(('#. %s\n' % c for c in comments)))
406 for typy, name, res_id in tnrs:
407 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
412 # only strings in python code are python formated
413 self.buffer.write("#, python-format\n")
415 if not isinstance(trad, unicode):
416 trad = unicode(trad, 'utf8')
417 if not isinstance(source, unicode):
418 source = unicode(source, 'utf8')
422 % (quote(source), quote(trad))
423 self.buffer.write(msg.encode('utf8'))
426 # Methods to export the translation file
428 def trans_export(lang, modules, buffer, format, cr):
430 def _process(format, modules, rows, buffer, lang):
432 writer = csv.writer(buffer, 'UNIX')
434 writer.writerow(("module","type","name","res_id","src","value"))
435 for module, type, name, res_id, src, trad, comments in rows:
436 # Comments are ignored by the CSV writer
437 writer.writerow((module, type, name, res_id, src, trad))
439 writer = TinyPoFile(buffer)
440 writer.write_infos(modules)
442 # we now group the translations by source. That means one translation per source.
444 for module, type, name, res_id, src, trad, comments in rows:
445 row = grouped_rows.setdefault(src, {})
446 row.setdefault('modules', set()).add(module)
447 if ('translation' not in row) or (not row['translation']):
448 row['translation'] = trad
449 row.setdefault('tnrs', []).append((type, name, res_id))
450 row.setdefault('comments', set()).update(comments)
452 for src, row in grouped_rows.items():
453 writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
455 elif format == 'tgz':
459 rows_by_module.setdefault(module, []).append(row)
460 tmpdir = tempfile.mkdtemp()
461 for mod, modrows in rows_by_module.items():
462 tmpmoddir = join(tmpdir, mod, 'i18n')
463 os.makedirs(tmpmoddir)
464 pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
465 buf = file(join(tmpmoddir, pofilename), 'w')
466 _process('po', [mod], modrows, buf, lang)
469 tar = tarfile.open(fileobj=buffer, mode='w|gz')
474 raise Exception(_('Unrecognized extension: must be one of '
475 '.csv, .po, or .tgz (received .%s).' % format))
478 if not trans_lang and format == 'csv':
479 # CSV files are meant for translators and they need a starting point,
480 # so we at least put the original term in the translation column
482 translations = trans_generate(lang, modules, cr)
483 modules = set([t[0] for t in translations[1:]])
484 _process(format, modules, translations, buffer, lang)
487 def trans_parse_xsl(de):
492 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
494 l = m.text.strip().replace('\n',' ')
496 res.append(l.encode("utf8"))
497 res.extend(trans_parse_xsl(n))
500 def trans_parse_rml(de):
504 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
506 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
507 for s in string_list:
509 res.append(s.encode("utf8"))
510 res.extend(trans_parse_rml(n))
513 def trans_parse_view(de):
515 if de.text and de.text.strip():
516 res.append(de.text.strip().encode("utf8"))
517 if de.tail and de.tail.strip():
518 res.append(de.tail.strip().encode("utf8"))
519 if de.tag == 'attribute' and de.get("name") == 'string':
521 res.append(de.text.encode("utf8"))
523 res.append(de.get('string').encode("utf8"))
525 res.append(de.get('help').encode("utf8"))
527 res.append(de.get('sum').encode("utf8"))
528 if de.get("confirm"):
529 res.append(de.get('confirm').encode("utf8"))
531 res.extend(trans_parse_view(n))
534 # tests whether an object is in a list of modules
535 def in_modules(object_name, modules):
544 module = object_name.split('.')[0]
545 module = module_dict.get(module, module)
546 return module in modules
549 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
550 """Babel message extractor for qweb template files.
551 :param fileobj: the file-like object the messages should be extracted from
552 :param keywords: a list of keywords (i.e. function names) that should
553 be recognized as translation functions
554 :param comment_tags: a list of translator tags to search for and
555 include in the results
556 :param options: a dictionary of additional options (optional)
557 :return: an iterator over ``(lineno, funcname, message, comments)``
562 def handle_text(text, lineno):
563 text = (text or "").strip()
564 if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
565 result.append((lineno, None, text, []))
567 # not using elementTree.iterparse because we need to skip sub-trees in case
568 # the ancestor element had a reason to be skipped
569 def iter_elements(current_element):
570 for el in current_element:
571 if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
572 if "t-js" not in el.attrib and \
573 not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
574 not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
575 handle_text(el.text, el.sourceline)
576 for att in ('title', 'alt', 'label', 'placeholder'):
578 handle_text(el.attrib[att], el.sourceline)
580 handle_text(el.tail, el.sourceline)
582 tree = etree.parse(fileobj)
583 iter_elements(tree.getroot())
588 def trans_generate(lang, modules, cr):
591 pool = pooler.get_pool(dbname)
592 trans_obj = pool.get('ir.translation')
593 model_data_obj = pool.get('ir.model.data')
595 l = pool.models.items()
598 query = 'SELECT name, model, res_id, module' \
599 ' FROM ir_model_data'
601 query_models = """SELECT m.id, m.model, imd.module
602 FROM ir_model AS m, ir_model_data AS imd
603 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
605 if 'all_installed' in modules:
606 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
607 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
609 if 'all' not in modules:
610 query += ' WHERE module IN %s'
611 query_models += ' AND imd.module in %s'
612 query_param = (tuple(modules),)
613 query += ' ORDER BY module, model, name'
614 query_models += ' ORDER BY module, model'
616 cr.execute(query, query_param)
619 def push_translation(module, type, name, id, source, comments=None):
620 tuple = (module, source, name, id, type, comments or [])
621 if source and tuple not in _to_translate:
622 _to_translate.append(tuple)
625 if isinstance(s, unicode):
626 return s.encode('utf8')
629 for (xml_name,model,res_id,module) in cr.fetchall():
630 module = encode(module)
631 model = encode(model)
632 xml_name = "%s.%s" % (module, encode(xml_name))
634 if not pool.get(model):
635 _logger.error("Unable to find object %r", model)
638 exists = pool.get(model).exists(cr, uid, res_id)
640 _logger.warning("Unable to find object %r with id %d", model, res_id)
642 obj = pool.get(model).browse(cr, uid, res_id)
644 if model=='ir.ui.view':
645 d = etree.XML(encode(obj.arch))
646 for t in trans_parse_view(d):
647 push_translation(module, 'view', encode(obj.model), 0, t)
648 elif model=='ir.actions.wizard':
649 service_name = 'wizard.'+encode(obj.wiz_name)
650 import openerp.netsvc as netsvc
651 if netsvc.Service._services.get(service_name):
652 obj2 = netsvc.Service._services[service_name]
653 for state_name, state_def in obj2.states.iteritems():
654 if 'result' in state_def:
655 result = state_def['result']
656 if result['type'] != 'form':
658 name = "%s,%s" % (encode(obj.wiz_name), state_name)
661 'string': ('wizard_field', lambda s: [encode(s)]),
662 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
663 'help': ('help', lambda s: [encode(s)]),
667 if not result.has_key('fields'):
668 _logger.warning("res has no fields: %r", result)
670 for field_name, field_def in result['fields'].iteritems():
671 res_name = name + ',' + field_name
673 for fn in def_params:
675 transtype, modifier = def_params[fn]
676 for val in modifier(field_def[fn]):
677 push_translation(module, transtype, res_name, 0, val)
680 arch = result['arch']
681 if arch and not isinstance(arch, UpdateableStr):
683 for t in trans_parse_view(d):
684 push_translation(module, 'wizard_view', name, 0, t)
686 # export button labels
687 for but_args in result['state']:
688 button_name = but_args[0]
689 button_label = but_args[1]
690 res_name = name + ',' + button_name
691 push_translation(module, 'wizard_button', res_name, 0, button_label)
693 elif model=='ir.model.fields':
695 field_name = encode(obj.name)
696 except AttributeError, exc:
697 _logger.error("name error in %s: %s", xml_name, str(exc))
699 objmodel = pool.get(obj.model)
700 if not objmodel or not field_name in objmodel._columns:
702 field_def = objmodel._columns[field_name]
704 name = "%s,%s" % (encode(obj.model), field_name)
705 push_translation(module, 'field', name, 0, encode(field_def.string))
708 push_translation(module, 'help', name, 0, encode(field_def.help))
710 if field_def.translate:
711 ids = objmodel.search(cr, uid, [])
712 obj_values = objmodel.read(cr, uid, ids, [field_name])
713 for obj_value in obj_values:
714 res_id = obj_value['id']
715 if obj.name in ('ir.model', 'ir.ui.menu'):
717 model_data_ids = model_data_obj.search(cr, uid, [
718 ('model', '=', model),
719 ('res_id', '=', res_id),
721 if not model_data_ids:
722 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
724 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
725 for dummy, val in field_def.selection:
726 push_translation(module, 'selection', name, 0, encode(val))
728 elif model=='ir.actions.report.xml':
729 name = encode(obj.report_name)
732 fname = obj.report_rml
733 parse_func = trans_parse_rml
734 report_type = "report"
736 fname = obj.report_xsl
737 parse_func = trans_parse_xsl
739 if fname and obj.report_type in ('pdf', 'xsl'):
741 report_file = misc.file_open(fname)
743 d = etree.parse(report_file)
744 for t in parse_func(d.iter()):
745 push_translation(module, report_type, name, 0, t)
748 except (IOError, etree.XMLSyntaxError):
749 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
751 for field_name,field_def in obj._table._columns.items():
752 if field_def.translate:
753 name = model + "," + field_name
755 trad = getattr(obj, field_name) or ''
758 push_translation(module, 'model', name, xml_name, encode(trad))
760 # End of data for ir.model.data query results
762 cr.execute(query_models, query_param)
764 def push_constraint_msg(module, term_type, model, msg):
765 # Check presence of __call__ directly instead of using
766 # callable() because it will be deprecated as of Python 3.0
767 if not hasattr(msg, '__call__'):
768 push_translation(module, term_type, model, 0, encode(msg))
770 for (_, model, module) in cr.fetchall():
771 module = encode(module)
772 model = encode(model)
774 model_obj = pool.get(model)
777 _logger.error("Unable to find object %r", model)
780 for constraint in getattr(model_obj, '_constraints', []):
781 push_constraint_msg(module, 'constraint', model, constraint[1])
783 for constraint in getattr(model_obj, '_sql_constraints', []):
784 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
786 def get_module_from_path(path, mod_paths=None):
788 # First, construct a list of possible paths
789 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
790 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
793 mod_paths.append(adp)
794 if not os.path.isabs(adp):
795 mod_paths.append(adp)
796 elif adp.startswith(def_path):
797 mod_paths.append(adp[len(def_path)+1:])
799 if path.startswith(mp) and (os.path.dirname(path) != mp):
800 path = path[len(mp)+1:]
801 return path.split(os.path.sep)[0]
802 return 'base' # files that are not in a module are considered as being in 'base' module
804 modobj = pool.get('ir.module.module')
805 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
806 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
808 root_path = os.path.join(config.config['root_path'], 'addons')
810 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
811 if root_path in apaths:
814 path_list = [root_path,] + apaths
816 # Also scan these non-addon paths
817 for bin_path in ['osv', 'report' ]:
818 path_list.append(os.path.join(config.config['root_path'], bin_path))
820 _logger.debug("Scanning modules at paths: ", path_list)
824 def verified_module_filepaths(fname, path, root):
825 fabsolutepath = join(root, fname)
826 frelativepath = fabsolutepath[len(path):]
827 display_path = "addons%s" % frelativepath
828 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
829 if (('all' in modules) or (module in modules)) and module in installed_modules:
830 return module, fabsolutepath, frelativepath, display_path
831 return None, None, None, None
833 def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
834 extra_comments=None, extract_keywords={'_': None}):
835 module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
836 extra_comments = extra_comments or []
838 src_file = open(fabsolutepath, 'r')
840 for lineno, message, comments in extract.extract(extract_method, src_file,
841 keywords=extract_keywords):
842 push_translation(module, trans_type, display_path, lineno,
843 encode(message), comments + extra_comments)
847 for path in path_list:
848 _logger.debug("Scanning files of modules at %s", path)
849 for root, dummy, files in osutil.walksymlinks(path):
850 for fname in fnmatch.filter(files, '*.py'):
851 babel_extract_terms(fname, path, root)
852 for fname in fnmatch.filter(files, '*.mako'):
853 babel_extract_terms(fname, path, root, trans_type='report')
854 # Javascript source files in the static/src/js directory, rest is ignored (libs)
855 if fnmatch.fnmatch(root, '*/static/src/js*'):
856 for fname in fnmatch.filter(files, '*.js'):
857 babel_extract_terms(fname, path, root, 'javascript',
858 extra_comments=[WEB_TRANSLATION_COMMENT],
859 extract_keywords={'_t': None})
860 # QWeb template files
861 if fnmatch.fnmatch(root, '*/static/src/xml*'):
862 for fname in fnmatch.filter(files, '*.xml'):
863 babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
864 extra_comments=[WEB_TRANSLATION_COMMENT])
868 # translate strings marked as to be translated
869 for module, source, name, id, type, comments in _to_translate:
870 trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
871 out.append([module, type, name, id, source, encode(trans) or '', comments])
874 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
876 fileobj = misc.file_open(filename)
877 _logger.info("loading %s", filename)
878 fileformat = os.path.splitext(filename)[-1][1:].lower()
879 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
884 _logger.error("couldn't read translation file %s", filename)
887 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
888 """Populates the ir_translation table."""
890 _logger.info('loading translation file for language %s', lang)
894 pool = pooler.get_pool(db_name)
895 lang_obj = pool.get('res.lang')
896 trans_obj = pool.get('ir.translation')
897 iso_lang = misc.get_iso_codes(lang)
899 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
902 # lets create the language with locale information
903 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
906 # now, the serious things: we read the language file
908 if fileformat == 'csv':
909 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
910 # read the first line of the file (it contains columns titles)
914 elif fileformat == 'po':
915 reader = TinyPoFile(fileobj)
916 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
918 _logger.error('Bad file format: %s', fileformat)
919 raise Exception(_('Bad file format'))
921 # read the rest of the file
923 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
927 # skip empty rows and rows where the translation field (=last fiefd) is empty
928 #if (not row) or (not row[-1]):
931 # dictionary which holds values for this line of the csv file
932 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
933 # 'src': ..., 'value': ..., 'module':...}
934 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
936 for i, field in enumerate(f):
939 # This would skip terms that fail to specify a res_id
940 if not dic.get('res_id'):
943 res_id = dic.pop('res_id')
944 if res_id and isinstance(res_id, (int, long)) \
945 or (isinstance(res_id, basestring) and res_id.isdigit()):
946 dic['res_id'] = int(res_id)
947 dic['module'] = module_name
949 tmodel = dic['name'].split(',')[0]
951 tmodule, tname = res_id.split('.', 1)
955 dic['imd_model'] = tmodel
956 dic['imd_name'] = tname
957 dic['module'] = tmodule
964 _logger.info("translation file loaded succesfully")
966 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
967 _logger.exception("couldn't read translation file %s", filename)
969 def get_locales(lang=None):
971 lang = locale.getdefaultlocale()[0]
974 lang = _LOCALE2WIN32.get(lang, lang)
977 ln = locale._build_localename((lang, enc))
979 nln = locale.normalize(ln)
983 for x in process('utf8'): yield x
985 prefenc = locale.getpreferredencoding()
987 for x in process(prefenc): yield x
991 'iso-8859-1': 'iso8859-15',
993 }.get(prefenc.lower())
995 for x in process(prefenc): yield x
1002 # locale.resetlocale is bugged with some locales.
1003 for ln in get_locales():
1005 return locale.setlocale(locale.LC_ALL, ln)
1006 except locale.Error:
1009 def load_language(cr, lang):
1010 """Loads a translation terms for a language.
1011 Used mainly to automate language loading at db initialization.
1013 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1016 pool = pooler.get_pool(cr.dbname)
1017 language_installer = pool.get('base.language.install')
1019 oid = language_installer.create(cr, uid, {'lang': lang})
1020 language_installer.lang_install(cr, uid, [oid], context=None)
1022 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: