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
35 from os.path import join
37 from datetime import datetime
38 from lxml import etree
42 from misc import UpdateableStr
43 from misc import SKIPPED_ELEMENT_TYPES
47 'af_ZA': 'Afrikaans_South Africa',
48 'sq_AL': 'Albanian_Albania',
49 'ar_SA': 'Arabic_Saudi Arabia',
50 'eu_ES': 'Basque_Spain',
51 'be_BY': 'Belarusian_Belarus',
52 'bs_BA': 'Serbian (Latin)',
53 'bg_BG': 'Bulgarian_Bulgaria',
54 'ca_ES': 'Catalan_Spain',
55 'hr_HR': 'Croatian_Croatia',
56 'zh_CN': 'Chinese_China',
57 'zh_TW': 'Chinese_Taiwan',
58 'cs_CZ': 'Czech_Czech Republic',
59 'da_DK': 'Danish_Denmark',
60 'nl_NL': 'Dutch_Netherlands',
61 'et_EE': 'Estonian_Estonia',
62 'fa_IR': 'Farsi_Iran',
63 'ph_PH': 'Filipino_Philippines',
64 'fi_FI': 'Finnish_Finland',
65 'fr_FR': 'French_France',
66 'fr_BE': 'French_France',
67 'fr_CH': 'French_France',
68 'fr_CA': 'French_France',
69 'ga': 'Scottish Gaelic',
70 'gl_ES': 'Galician_Spain',
71 'ka_GE': 'Georgian_Georgia',
72 'de_DE': 'German_Germany',
73 'el_GR': 'Greek_Greece',
74 'gu': 'Gujarati_India',
75 'he_IL': 'Hebrew_Israel',
77 'hu': 'Hungarian_Hungary',
78 'is_IS': 'Icelandic_Iceland',
79 'id_ID': 'Indonesian_indonesia',
80 'it_IT': 'Italian_Italy',
81 'ja_JP': 'Japanese_Japan',
84 'ko_KR': 'Korean_Korea',
86 'lt_LT': 'Lithuanian_Lithuania',
87 'lat': 'Latvian_Latvia',
88 'ml_IN': 'Malayalam_India',
89 'id_ID': 'Indonesian_indonesia',
91 'mn': 'Cyrillic_Mongolian',
92 'no_NO': 'Norwegian_Norway',
93 'nn_NO': 'Norwegian-Nynorsk_Norway',
94 'pl': 'Polish_Poland',
95 'pt_PT': 'Portuguese_Portugal',
96 'pt_BR': 'Portuguese_Brazil',
97 'ro_RO': 'Romanian_Romania',
98 'ru_RU': 'Russian_Russia',
100 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
101 'sk_SK': 'Slovak_Slovakia',
102 'sl_SI': 'Slovenian_Slovenia',
103 #should find more specific locales for spanish countries,
104 #but better than nothing
105 'es_AR': 'Spanish_Spain',
106 'es_BO': 'Spanish_Spain',
107 'es_CL': 'Spanish_Spain',
108 'es_CO': 'Spanish_Spain',
109 'es_CR': 'Spanish_Spain',
110 'es_DO': 'Spanish_Spain',
111 'es_EC': 'Spanish_Spain',
112 'es_ES': 'Spanish_Spain',
113 'es_GT': 'Spanish_Spain',
114 'es_HN': 'Spanish_Spain',
115 'es_MX': 'Spanish_Spain',
116 'es_NI': 'Spanish_Spain',
117 'es_PA': 'Spanish_Spain',
118 'es_PE': 'Spanish_Spain',
119 'es_PR': 'Spanish_Spain',
120 'es_PY': 'Spanish_Spain',
121 'es_SV': 'Spanish_Spain',
122 'es_UY': 'Spanish_Spain',
123 'es_VE': 'Spanish_Spain',
124 'sv_SE': 'Swedish_Sweden',
125 'ta_IN': 'English_Australia',
126 'th_TH': 'Thai_Thailand',
128 'tr_TR': 'Turkish_Turkey',
129 'uk_UA': 'Ukrainian_Ukraine',
130 'vi_VN': 'Vietnamese_Viet Nam',
131 'tlh_TLH': 'Klingon',
136 class UNIX_LINE_TERMINATOR(csv.excel):
137 lineterminator = '\n'
139 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
142 # Warning: better use self.pool.get('ir.translation')._get_source if you can
144 def translate(cr, name, source_type, lang, source=None):
146 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))
148 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
150 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
151 res_trans = cr.fetchone()
152 res = res_trans and res_trans[0] or False
155 logger = logging.getLogger('translate')
157 class GettextAlias(object):
160 # find current DB based on thread/worker db name (see netsvc)
161 db_name = getattr(threading.currentThread(), 'dbname', None)
163 return pooler.get_db_only(db_name)
165 def _get_cr(self, frame):
167 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
169 s = frame.f_locals.get('self', {})
170 cr = getattr(s, 'cr', None)
178 def _get_lang(self, frame):
180 ctx = frame.f_locals.get('context')
182 kwargs = frame.f_locals.get('kwargs')
184 args = frame.f_locals.get('args')
185 if args and isinstance(args, (list, tuple)) \
186 and isinstance(args[-1], dict):
188 elif isinstance(kwargs, dict):
189 ctx = kwargs.get('context')
191 lang = ctx.get('lang')
193 s = frame.f_locals.get('self', {})
194 c = getattr(s, 'localcontext', None)
199 def __call__(self, source):
204 frame = inspect.currentframe()
210 lang = self._get_lang(frame)
212 cr, is_new_cr = self._get_cr(frame)
214 # Try to use ir.translation to benefit from global cache if possible
215 pool = pooler.get_pool(cr.dbname)
216 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
218 logger.debug('no context cursor detected, skipping translation for "%r"', source)
220 logger.debug('no translation language detected, skipping translation for "%r" ', source)
222 logger.debug('translation went wrong for "%r", skipped', source)
223 # if so, double-check the root/base translations filenames
233 """Returns quoted PO term string, with special PO characters escaped"""
234 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
235 return '"%s"' % s.replace('\\','\\\\') \
236 .replace('"','\\"') \
237 .replace('\n', '\\n"\n"')
239 re_escaped_char = re.compile(r"(\\.)")
240 re_escaped_replacements = {'n': '\n', }
242 def _sub_replacement(match_obj):
243 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
246 """Returns unquoted PO term string, with special PO characters unescaped"""
247 return re_escaped_char.sub(_sub_replacement, str[1:-1])
249 # class to handle po files
250 class TinyPoFile(object):
251 def __init__(self, buffer):
252 self.logger = logging.getLogger('i18n')
255 def warn(self, msg, *args):
256 self.logger.warning(msg, *args)
260 self.lines = self._get_lines()
261 self.lines_count = len(self.lines);
267 def _get_lines(self):
268 lines = self.buffer.readlines()
269 # remove the BOM (Byte Order Mark):
271 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
273 lines.append('') # ensure that the file ends with at least an empty line
277 return (self.lines_count - len(self.lines))
280 type = name = res_id = source = trad = None
283 type, name, res_id, source, trad = self.tnrs.pop(0)
291 if 0 == len(self.lines):
292 raise StopIteration()
293 line = self.lines.pop(0).strip()
294 while line.startswith('#'):
295 if line.startswith('#~ '):
297 if line.startswith('#:'):
298 if ' ' in line[2:].strip():
299 for lpart in line[2:].strip().split(' '):
300 tmp_tnrs.append(lpart.strip().split(':',2))
302 tmp_tnrs.append( line[2:].strip().split(':',2) )
303 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
305 line = self.lines.pop(0).strip()
307 # allow empty lines between comments and msgid
308 line = self.lines.pop(0).strip()
309 if line.startswith('#~ '):
310 while line.startswith('#~ ') or not line.strip():
311 if 0 == len(self.lines):
312 raise StopIteration()
313 line = self.lines.pop(0)
314 # This has been a deprecated entry, don't return anything
317 if not line.startswith('msgid'):
318 raise Exception("malformed file: bad line: %s" % line)
319 source = unquote(line[6:])
320 line = self.lines.pop(0).strip()
321 if not source and self.first:
322 # if the source is "" and it's the first msgid, it's the special
323 # msgstr with the informations about the traduction and the
324 # traductor; we skip it
327 line = self.lines.pop(0).strip()
330 while not line.startswith('msgstr'):
332 raise Exception('malformed file at %d'% self.cur_line())
333 source += unquote(line)
334 line = self.lines.pop(0).strip()
336 trad = unquote(line[7:])
337 line = self.lines.pop(0).strip()
339 trad += unquote(line)
340 line = self.lines.pop(0).strip()
342 if tmp_tnrs and not fuzzy:
343 type, name, res_id = tmp_tnrs.pop(0)
344 for t, n, r in tmp_tnrs:
345 self.tnrs.append((t, n, r, source, trad))
351 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
352 self.cur_line(), source[:30])
354 return type, name, res_id, source, trad
356 def write_infos(self, modules):
357 import openerp.release as release
358 self.buffer.write("# Translation of %(project)s.\n" \
359 "# This file contains the translation of the following modules:\n" \
364 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
365 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
366 '''"POT-Creation-Date: %(now)s\\n"\n''' \
367 '''"PO-Revision-Date: %(now)s\\n"\n''' \
368 '''"Last-Translator: <>\\n"\n''' \
369 '''"Language-Team: \\n"\n''' \
370 '''"MIME-Version: 1.0\\n"\n''' \
371 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
372 '''"Content-Transfer-Encoding: \\n"\n''' \
373 '''"Plural-Forms: \\n"\n''' \
376 % { 'project': release.description,
377 'version': release.version,
378 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
379 'bugmail': release.support_email,
380 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
384 def write(self, modules, tnrs, source, trad):
386 plurial = len(modules) > 1 and 's' or ''
387 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
391 for typy, name, res_id in tnrs:
392 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
397 # only strings in python code are python formated
398 self.buffer.write("#, python-format\n")
400 if not isinstance(trad, unicode):
401 trad = unicode(trad, 'utf8')
402 if not isinstance(source, unicode):
403 source = unicode(source, 'utf8')
407 % (quote(source), quote(trad))
408 self.buffer.write(msg.encode('utf8'))
411 # Methods to export the translation file
413 def trans_export(lang, modules, buffer, format, cr):
415 def _process(format, modules, rows, buffer, lang, newlang):
417 writer=csv.writer(buffer, 'UNIX')
422 writer = TinyPoFile(buffer)
423 writer.write_infos(modules)
425 # we now group the translations by source. That means one translation per source.
427 for module, type, name, res_id, src, trad in rows:
428 row = grouped_rows.setdefault(src, {})
429 row.setdefault('modules', set()).add(module)
430 if ('translation' not in row) or (not row['translation']):
431 row['translation'] = trad
432 row.setdefault('tnrs', []).append((type, name, res_id))
434 for src, row in grouped_rows.items():
435 writer.write(row['modules'], row['tnrs'], src, row['translation'])
437 elif format == 'tgz':
442 # first row is the "header", as in csv, it will be popped
443 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
444 rows_by_module[module].append(row)
446 tmpdir = tempfile.mkdtemp()
447 for mod, modrows in rows_by_module.items():
448 tmpmoddir = join(tmpdir, mod, 'i18n')
449 os.makedirs(tmpmoddir)
450 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
451 buf = file(join(tmpmoddir, pofilename), 'w')
452 _process('po', [mod], modrows, buf, lang, newlang)
455 tar = tarfile.open(fileobj=buffer, mode='w|gz')
460 raise Exception(_('Bad file format'))
462 newlang = not bool(lang)
465 trans = trans_generate(lang, modules, cr)
466 if newlang and format!='csv':
469 modules = set([t[0] for t in trans[1:]])
470 _process(format, modules, trans, buffer, lang, newlang)
473 def trans_parse_xsl(de):
478 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
480 l = m.text.strip().replace('\n',' ')
482 res.append(l.encode("utf8"))
483 res.extend(trans_parse_xsl(n))
486 def trans_parse_rml(de):
490 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
492 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
493 for s in string_list:
495 res.append(s.encode("utf8"))
496 res.extend(trans_parse_rml(n))
499 def trans_parse_view(de):
501 if de.tag == 'attribute' and de.get("name") == 'string':
503 res.append(de.text.encode("utf8"))
505 res.append(de.get('string').encode("utf8"))
507 res.append(de.get('help').encode("utf8"))
509 res.append(de.get('sum').encode("utf8"))
510 if de.get("confirm"):
511 res.append(de.get('confirm').encode("utf8"))
513 res.extend(trans_parse_view(n))
516 # tests whether an object is in a list of modules
517 def in_modules(object_name, modules):
526 module = object_name.split('.')[0]
527 module = module_dict.get(module, module)
528 return module in modules
530 def trans_generate(lang, modules, cr):
531 logger = logging.getLogger('i18n')
534 pool = pooler.get_pool(dbname)
535 trans_obj = pool.get('ir.translation')
536 model_data_obj = pool.get('ir.model.data')
538 l = pool.obj_pool.items()
541 query = 'SELECT name, model, res_id, module' \
542 ' FROM ir_model_data'
544 query_models = """SELECT m.id, m.model, imd.module
545 FROM ir_model AS m, ir_model_data AS imd
546 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
548 if 'all_installed' in modules:
549 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
550 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
552 if 'all' not in modules:
553 query += ' WHERE module IN %s'
554 query_models += ' AND imd.module in %s'
555 query_param = (tuple(modules),)
556 query += ' ORDER BY module, model, name'
557 query_models += ' ORDER BY module, model'
559 cr.execute(query, query_param)
562 def push_translation(module, type, name, id, source):
563 tuple = (module, source, name, id, type)
564 if source and tuple not in _to_translate:
565 _to_translate.append(tuple)
568 if isinstance(s, unicode):
569 return s.encode('utf8')
572 for (xml_name,model,res_id,module) in cr.fetchall():
573 module = encode(module)
574 model = encode(model)
575 xml_name = "%s.%s" % (module, encode(xml_name))
577 if not pool.get(model):
578 logger.error("Unable to find object %r", model)
581 exists = pool.get(model).exists(cr, uid, res_id)
583 logger.warning("Unable to find object %r with id %d", model, res_id)
585 obj = pool.get(model).browse(cr, uid, res_id)
587 if model=='ir.ui.view':
588 d = etree.XML(encode(obj.arch))
589 for t in trans_parse_view(d):
590 push_translation(module, 'view', encode(obj.model), 0, t)
591 elif model=='ir.actions.wizard':
592 service_name = 'wizard.'+encode(obj.wiz_name)
593 import openerp.netsvc as netsvc
594 if netsvc.Service._services.get(service_name):
595 obj2 = netsvc.Service._services[service_name]
596 for state_name, state_def in obj2.states.iteritems():
597 if 'result' in state_def:
598 result = state_def['result']
599 if result['type'] != 'form':
601 name = "%s,%s" % (encode(obj.wiz_name), state_name)
604 'string': ('wizard_field', lambda s: [encode(s)]),
605 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
606 'help': ('help', lambda s: [encode(s)]),
610 if not result.has_key('fields'):
611 logger.warning("res has no fields: %r", result)
613 for field_name, field_def in result['fields'].iteritems():
614 res_name = name + ',' + field_name
616 for fn in def_params:
618 transtype, modifier = def_params[fn]
619 for val in modifier(field_def[fn]):
620 push_translation(module, transtype, res_name, 0, val)
623 arch = result['arch']
624 if arch and not isinstance(arch, UpdateableStr):
626 for t in trans_parse_view(d):
627 push_translation(module, 'wizard_view', name, 0, t)
629 # export button labels
630 for but_args in result['state']:
631 button_name = but_args[0]
632 button_label = but_args[1]
633 res_name = name + ',' + button_name
634 push_translation(module, 'wizard_button', res_name, 0, button_label)
636 elif model=='ir.model.fields':
638 field_name = encode(obj.name)
639 except AttributeError, exc:
640 logger.error("name error in %s: %s", xml_name, str(exc))
642 objmodel = pool.get(obj.model)
643 if not objmodel or not field_name in objmodel._columns:
645 field_def = objmodel._columns[field_name]
647 name = "%s,%s" % (encode(obj.model), field_name)
648 push_translation(module, 'field', name, 0, encode(field_def.string))
651 push_translation(module, 'help', name, 0, encode(field_def.help))
653 if field_def.translate:
654 ids = objmodel.search(cr, uid, [])
655 obj_values = objmodel.read(cr, uid, ids, [field_name])
656 for obj_value in obj_values:
657 res_id = obj_value['id']
658 if obj.name in ('ir.model', 'ir.ui.menu'):
660 model_data_ids = model_data_obj.search(cr, uid, [
661 ('model', '=', model),
662 ('res_id', '=', res_id),
664 if not model_data_ids:
665 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
667 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
668 for dummy, val in field_def.selection:
669 push_translation(module, 'selection', name, 0, encode(val))
671 elif model=='ir.actions.report.xml':
672 name = encode(obj.report_name)
675 fname = obj.report_rml
676 parse_func = trans_parse_rml
677 report_type = "report"
679 fname = obj.report_xsl
680 parse_func = trans_parse_xsl
682 if fname and obj.report_type in ('pdf', 'xsl'):
684 report_file = misc.file_open(fname)
686 d = etree.parse(report_file)
687 for t in parse_func(d.iter()):
688 push_translation(module, report_type, name, 0, t)
691 except (IOError, etree.XMLSyntaxError):
692 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
694 for field_name,field_def in obj._table._columns.items():
695 if field_def.translate:
696 name = model + "," + field_name
698 trad = getattr(obj, field_name) or ''
701 push_translation(module, 'model', name, xml_name, encode(trad))
703 # End of data for ir.model.data query results
705 cr.execute(query_models, query_param)
707 def push_constraint_msg(module, term_type, model, msg):
708 # Check presence of __call__ directly instead of using
709 # callable() because it will be deprecated as of Python 3.0
710 if not hasattr(msg, '__call__'):
711 push_translation(module, term_type, model, 0, encode(msg))
713 for (model_id, model, module) in cr.fetchall():
714 module = encode(module)
715 model = encode(model)
717 model_obj = pool.get(model)
720 logging.getLogger("i18n").error("Unable to find object %r", model)
723 for constraint in getattr(model_obj, '_constraints', []):
724 push_constraint_msg(module, 'constraint', model, constraint[1])
726 for constraint in getattr(model_obj, '_sql_constraints', []):
727 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
729 # parse source code for _() calls
730 def get_module_from_path(path, mod_paths=None):
732 # First, construct a list of possible paths
733 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
734 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
737 mod_paths.append(adp)
738 if not os.path.isabs(adp):
739 mod_paths.append(adp)
740 elif adp.startswith(def_path):
741 mod_paths.append(adp[len(def_path)+1:])
743 if path.startswith(mp) and (os.path.dirname(path) != mp):
744 path = path[len(mp)+1:]
745 return path.split(os.path.sep)[0]
746 return 'base' # files that are not in a module are considered as being in 'base' module
748 modobj = pool.get('ir.module.module')
749 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
750 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
752 root_path = os.path.join(config.config['root_path'], 'addons')
754 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
755 if root_path in apaths:
758 path_list = [root_path,] + apaths
760 # Also scan these non-addon paths
761 for bin_path in ['osv', 'report' ]:
762 path_list.append(os.path.join(config.config['root_path'], bin_path))
764 logger.debug("Scanning modules at paths: ", path_list)
767 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
768 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
769 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
770 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
772 def export_code_terms_from_file(fname, path, root, terms_type):
773 fabsolutepath = join(root, fname)
774 frelativepath = fabsolutepath[len(path):]
775 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
776 is_mod_installed = module in installed_modules
777 if (('all' in modules) or (module in modules)) and is_mod_installed:
778 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
779 src_file = misc.file_open(fabsolutepath, subdir='')
781 code_string = src_file.read()
784 if module in installed_modules:
785 frelativepath = str("addons" + frelativepath)
786 ite = re_dquotes.finditer(code_string)
791 if src.startswith('""'):
792 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
795 src = join_dquotes.sub(r'\1', src)
796 # try to count the lines from the last pos to our place:
797 code_line += code_string[code_offset:i.start(1)].count('\n')
798 # now, since we did a binary read of a python source file, we
799 # have to expand pythonic escapes like the interpreter does.
800 src = src.decode('string_escape')
801 push_translation(module, terms_type, frelativepath, code_line, encode(src))
802 code_line += i.group(1).count('\n')
803 code_offset = i.end() # we have counted newlines up to the match end
805 ite = re_quotes.finditer(code_string)
806 code_offset = 0 #reset counters
810 if src.startswith("''"):
811 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
814 src = join_quotes.sub(r'\1', src)
815 code_line += code_string[code_offset:i.start(1)].count('\n')
816 src = src.decode('string_escape')
817 push_translation(module, terms_type, frelativepath, code_line, encode(src))
818 code_line += i.group(1).count('\n')
819 code_offset = i.end() # we have counted newlines up to the match end
821 for path in path_list:
822 logger.debug("Scanning files of modules at %s", path)
823 for root, dummy, files in osutil.walksymlinks(path):
824 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
825 export_code_terms_from_file(fname, path, root, 'code')
826 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
827 export_code_terms_from_file(fname, path, root, 'report')
830 out = [["module","type","name","res_id","src","value"]] # header
832 # translate strings marked as to be translated
833 for module, source, name, id, type in _to_translate:
834 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
835 out.append([module, type, name, id, source, encode(trans) or ''])
839 def trans_load(cr, filename, lang, verbose=True, context=None):
840 logger = logging.getLogger('i18n')
842 fileobj = open(filename,'r')
843 logger.info("loading %s", filename)
844 fileformat = os.path.splitext(filename)[-1][1:].lower()
845 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
850 logger.error("couldn't read translation file %s", filename)
853 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
854 """Populates the ir_translation table. Fixing the res_ids so that they point
855 correctly to ir_model_data is done in a separate step, using the
856 'trans_update_res_ids' function below."""
857 logger = logging.getLogger('i18n')
859 logger.info('loading translation file for language %s', lang)
863 pool = pooler.get_pool(db_name)
864 lang_obj = pool.get('res.lang')
865 trans_obj = pool.get('ir.translation')
866 model_data_obj = pool.get('ir.model.data')
867 iso_lang = misc.get_iso_codes(lang)
870 ids = lang_obj.search(cr, uid, [('code','=', lang)])
873 # lets create the language with locale information
874 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
877 # now, the serious things: we read the language file
879 if fileformat == 'csv':
880 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
881 # read the first line of the file (it contains columns titles)
885 elif fileformat == 'po':
886 reader = TinyPoFile(fileobj)
887 f = ['type', 'name', 'res_id', 'src', 'value']
889 logger.error('Bad file format: %s', fileformat)
890 raise Exception(_('Bad file format'))
892 # read the rest of the file
896 # skip empty rows and rows where the translation field (=last fiefd) is empty
897 #if (not row) or (not row[-1]):
900 # dictionary which holds values for this line of the csv file
901 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
902 # 'src': ..., 'value': ...}
904 for i in range(len(f)):
905 if f[i] in ('module',):
910 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
911 dic['module'] = False
912 dic['xml_id'] = False
914 split_id = dic['res_id'].split('.', 1)
915 dic['module'] = split_id[0]
916 dic['xml_id'] = split_id[1]
917 dic['res_id'] = False
921 ('type', '=', dic['type']),
922 ('name', '=', dic['name']),
923 ('src', '=', dic['src']),
925 if dic['type'] == 'model':
926 if dic['res_id'] is False:
927 args.append(('module', '=', dic['module']))
928 args.append(('xml_id', '=', dic['xml_id']))
930 args.append(('res_id', '=', dic['res_id']))
931 ids = trans_obj.search(cr, uid, args)
933 if context.get('overwrite') and dic['value']:
934 trans_obj.write(cr, uid, ids, {'value': dic['value']})
936 trans_obj.create(cr, uid, dic)
938 logger.info("translation file loaded succesfully")
940 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
941 logger.exception("couldn't read translation file %s", filename)
943 def trans_update_res_ids(cr):
945 UPDATE ir_translation
946 SET res_id = COALESCE ((SELECT ir_model_data.res_id
948 WHERE ir_translation.module = ir_model_data.module
949 AND ir_translation.xml_id = ir_model_data.name), 0)
950 WHERE ir_translation.module is not null
951 AND ir_translation.xml_id is not null
952 AND ir_translation.res_id = 0;
955 def get_locales(lang=None):
957 lang = locale.getdefaultlocale()[0]
960 lang = _LOCALE2WIN32.get(lang, lang)
963 ln = locale._build_localename((lang, enc))
965 nln = locale.normalize(ln)
969 for x in process('utf8'): yield x
971 prefenc = locale.getpreferredencoding()
973 for x in process(prefenc): yield x
977 'iso-8859-1': 'iso8859-15',
979 }.get(prefenc.lower())
981 for x in process(prefenc): yield x
988 # locale.resetlocale is bugged with some locales.
989 for ln in get_locales():
991 return locale.setlocale(locale.LC_ALL, ln)
995 def load_language(cr, lang):
996 """Loads a translation terms for a language.
997 Used mainly to automate language loading at db initialization.
999 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1002 pool = pooler.get_pool(cr.dbname)
1003 language_installer = pool.get('base.language.install')
1005 oid = language_installer.create(cr, uid, {'lang': lang})
1006 language_installer.lang_install(cr, uid, [oid], context=None)
1008 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: