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 ##############################################################################
35 from os.path import join
37 from datetime import datetime
38 from lxml import etree
42 from tools.misc import UpdateableStr
43 from tools.misc import SKIPPED_ELEMENT_TYPES
46 'af_ZA': 'Afrikaans_South Africa',
47 'sq_AL': 'Albanian_Albania',
48 'ar_SA': 'Arabic_Saudi Arabia',
49 'eu_ES': 'Basque_Spain',
50 'be_BY': 'Belarusian_Belarus',
51 'bs_BA': 'Serbian (Latin)',
52 'bg_BG': 'Bulgarian_Bulgaria',
53 'ca_ES': 'Catalan_Spain',
54 'hr_HR': 'Croatian_Croatia',
55 'zh_CN': 'Chinese_China',
56 'zh_TW': 'Chinese_Taiwan',
57 'cs_CZ': 'Czech_Czech Republic',
58 'da_DK': 'Danish_Denmark',
59 'nl_NL': 'Dutch_Netherlands',
60 'et_EE': 'Estonian_Estonia',
61 'fa_IR': 'Farsi_Iran',
62 'ph_PH': 'Filipino_Philippines',
63 'fi_FI': 'Finnish_Finland',
64 'fr_FR': 'French_France',
65 'fr_BE': 'French_France',
66 'fr_CH': 'French_France',
67 'fr_CA': 'French_France',
68 'ga': 'Scottish Gaelic',
69 'gl_ES': 'Galician_Spain',
70 'ka_GE': 'Georgian_Georgia',
71 'de_DE': 'German_Germany',
72 'el_GR': 'Greek_Greece',
73 'gu': 'Gujarati_India',
74 'he_IL': 'Hebrew_Israel',
76 'hu': 'Hungarian_Hungary',
77 'is_IS': 'Icelandic_Iceland',
78 'id_ID': 'Indonesian_indonesia',
79 'it_IT': 'Italian_Italy',
80 'ja_JP': 'Japanese_Japan',
83 'ko_KR': 'Korean_Korea',
85 'lt_LT': 'Lithuanian_Lithuania',
86 'lat': 'Latvian_Latvia',
87 'ml_IN': 'Malayalam_India',
88 'id_ID': 'Indonesian_indonesia',
90 'mn': 'Cyrillic_Mongolian',
91 'no_NO': 'Norwegian_Norway',
92 'nn_NO': 'Norwegian-Nynorsk_Norway',
93 'pl': 'Polish_Poland',
94 'pt_PT': 'Portuguese_Portugal',
95 'pt_BR': 'Portuguese_Brazil',
96 'ro_RO': 'Romanian_Romania',
97 'ru_RU': 'Russian_Russia',
99 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
100 'sk_SK': 'Slovak_Slovakia',
101 'sl_SI': 'Slovenian_Slovenia',
102 #should find more specific locales for spanish countries,
103 #but better than nothing
104 'es_AR': 'Spanish_Spain',
105 'es_BO': 'Spanish_Spain',
106 'es_CL': 'Spanish_Spain',
107 'es_CO': 'Spanish_Spain',
108 'es_CR': 'Spanish_Spain',
109 'es_DO': 'Spanish_Spain',
110 'es_EC': 'Spanish_Spain',
111 'es_ES': 'Spanish_Spain',
112 'es_GT': 'Spanish_Spain',
113 'es_HN': 'Spanish_Spain',
114 'es_MX': 'Spanish_Spain',
115 'es_NI': 'Spanish_Spain',
116 'es_PA': 'Spanish_Spain',
117 'es_PE': 'Spanish_Spain',
118 'es_PR': 'Spanish_Spain',
119 'es_PY': 'Spanish_Spain',
120 'es_SV': 'Spanish_Spain',
121 'es_UY': 'Spanish_Spain',
122 'es_VE': 'Spanish_Spain',
123 'sv_SE': 'Swedish_Sweden',
124 'ta_IN': 'English_Australia',
125 'th_TH': 'Thai_Thailand',
127 'tr_TR': 'Turkish_Turkey',
128 'uk_UA': 'Ukrainian_Ukraine',
129 'vi_VN': 'Vietnamese_Viet Nam',
130 'tlh_TLH': 'Klingon',
135 class UNIX_LINE_TERMINATOR(csv.excel):
136 lineterminator = '\n'
138 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
141 # Warning: better use self.pool.get('ir.translation')._get_source if you can
143 def translate(cr, name, source_type, lang, source=None):
145 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))
147 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
149 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
150 res_trans = cr.fetchone()
151 res = res_trans and res_trans[0] or False
154 logger = logging.getLogger('translate')
156 class GettextAlias(object):
159 # find current DB based on thread/worker db name (see netsvc)
160 db_name = getattr(threading.currentThread(), 'dbname', None)
162 return pooler.get_db_only(db_name)
164 def _get_cr(self, frame):
166 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
168 s = frame.f_locals.get('self', {})
169 cr = getattr(s, 'cr', None)
177 def _get_lang(self, frame):
179 ctx = frame.f_locals.get('context')
181 kwargs = frame.f_locals.get('kwargs')
183 args = frame.f_locals.get('args')
184 if args and isinstance(args, (list, tuple)) \
185 and isinstance(args[-1], dict):
187 elif isinstance(kwargs, dict):
188 ctx = kwargs.get('context')
190 lang = ctx.get('lang')
192 s = frame.f_locals.get('self', {})
193 c = getattr(s, 'localcontext', None)
198 def __call__(self, source):
203 frame = inspect.currentframe()
209 lang = self._get_lang(frame)
211 cr, is_new_cr = self._get_cr(frame)
213 # Try to use ir.translation to benefit from global cache if possible
214 pool = pooler.get_pool(cr.dbname)
215 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
217 logger.debug('no context cursor detected, skipping translation for "%r"', source)
219 logger.debug('no translation language detected, skipping translation for "%r" ', source)
221 logger.debug('translation went wrong for "%r", skipped', source)
222 # if so, double-check the root/base translations filenames
232 """Returns quoted PO term string, with special PO characters escaped"""
233 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
234 return '"%s"' % s.replace('\\','\\\\') \
235 .replace('"','\\"') \
236 .replace('\n', '\\n"\n"')
238 re_escaped_char = re.compile(r"(\\.)")
239 re_escaped_replacements = {'n': '\n', }
241 def _sub_replacement(match_obj):
242 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
245 """Returns unquoted PO term string, with special PO characters unescaped"""
246 return re_escaped_char.sub(_sub_replacement, str[1:-1])
248 # class to handle po files
249 class TinyPoFile(object):
250 def __init__(self, buffer):
251 self.logger = logging.getLogger('i18n')
254 def warn(self, msg, *args):
255 self.logger.warning(msg, *args)
259 self.lines = self._get_lines()
260 self.lines_count = len(self.lines);
266 def _get_lines(self):
267 lines = self.buffer.readlines()
268 # remove the BOM (Byte Order Mark):
270 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
272 lines.append('') # ensure that the file ends with at least an empty line
276 return (self.lines_count - len(self.lines))
279 type = name = res_id = source = trad = None
282 type, name, res_id, source, trad = self.tnrs.pop(0)
290 if 0 == len(self.lines):
291 raise StopIteration()
292 line = self.lines.pop(0).strip()
293 while line.startswith('#'):
294 if line.startswith('#~ '):
296 if line.startswith('#:'):
297 if ' ' in line[2:].strip():
298 for lpart in line[2:].strip().split(' '):
299 tmp_tnrs.append(lpart.strip().split(':',2))
301 tmp_tnrs.append( line[2:].strip().split(':',2) )
302 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
304 line = self.lines.pop(0).strip()
306 # allow empty lines between comments and msgid
307 line = self.lines.pop(0).strip()
308 if line.startswith('#~ '):
309 while line.startswith('#~ ') or not line.strip():
310 if 0 == len(self.lines):
311 raise StopIteration()
312 line = self.lines.pop(0)
313 # This has been a deprecated entry, don't return anything
316 if not line.startswith('msgid'):
317 raise Exception("malformed file: bad line: %s" % line)
318 source = unquote(line[6:])
319 line = self.lines.pop(0).strip()
320 if not source and self.first:
321 # if the source is "" and it's the first msgid, it's the special
322 # msgstr with the informations about the traduction and the
323 # traductor; we skip it
326 line = self.lines.pop(0).strip()
329 while not line.startswith('msgstr'):
331 raise Exception('malformed file at %d'% self.cur_line())
332 source += unquote(line)
333 line = self.lines.pop(0).strip()
335 trad = unquote(line[7:])
336 line = self.lines.pop(0).strip()
338 trad += unquote(line)
339 line = self.lines.pop(0).strip()
341 if tmp_tnrs and not fuzzy:
342 type, name, res_id = tmp_tnrs.pop(0)
343 for t, n, r in tmp_tnrs:
344 self.tnrs.append((t, n, r, source, trad))
350 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
351 self.cur_line(), source[:30])
353 return type, name, res_id, source, trad
355 def write_infos(self, modules):
357 self.buffer.write("# Translation of %(project)s.\n" \
358 "# This file contains the translation of the following modules:\n" \
363 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
364 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
365 '''"POT-Creation-Date: %(now)s\\n"\n''' \
366 '''"PO-Revision-Date: %(now)s\\n"\n''' \
367 '''"Last-Translator: <>\\n"\n''' \
368 '''"Language-Team: \\n"\n''' \
369 '''"MIME-Version: 1.0\\n"\n''' \
370 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
371 '''"Content-Transfer-Encoding: \\n"\n''' \
372 '''"Plural-Forms: \\n"\n''' \
375 % { 'project': release.description,
376 'version': release.version,
377 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
378 'bugmail': release.support_email,
379 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
383 def write(self, modules, tnrs, source, trad):
385 plurial = len(modules) > 1 and 's' or ''
386 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
390 for typy, name, res_id in tnrs:
391 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
396 # only strings in python code are python formated
397 self.buffer.write("#, python-format\n")
399 if not isinstance(trad, unicode):
400 trad = unicode(trad, 'utf8')
401 if not isinstance(source, unicode):
402 source = unicode(source, 'utf8')
406 % (quote(source), quote(trad))
407 self.buffer.write(msg.encode('utf8'))
410 # Methods to export the translation file
412 def trans_export(lang, modules, buffer, format, cr):
414 def _process(format, modules, rows, buffer, lang, newlang):
416 writer=csv.writer(buffer, 'UNIX')
421 writer = tools.TinyPoFile(buffer)
422 writer.write_infos(modules)
424 # we now group the translations by source. That means one translation per source.
426 for module, type, name, res_id, src, trad in rows:
427 row = grouped_rows.setdefault(src, {})
428 row.setdefault('modules', set()).add(module)
429 if ('translation' not in row) or (not row['translation']):
430 row['translation'] = trad
431 row.setdefault('tnrs', []).append((type, name, res_id))
433 for src, row in grouped_rows.items():
434 writer.write(row['modules'], row['tnrs'], src, row['translation'])
436 elif format == 'tgz':
441 # first row is the "header", as in csv, it will be popped
442 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
443 rows_by_module[module].append(row)
445 tmpdir = tempfile.mkdtemp()
446 for mod, modrows in rows_by_module.items():
447 tmpmoddir = join(tmpdir, mod, 'i18n')
448 os.makedirs(tmpmoddir)
449 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
450 buf = file(join(tmpmoddir, pofilename), 'w')
451 _process('po', [mod], modrows, buf, lang, newlang)
454 tar = tarfile.open(fileobj=buffer, mode='w|gz')
459 raise Exception(_('Bad file format'))
461 newlang = not bool(lang)
464 trans = trans_generate(lang, modules, cr)
465 if newlang and format!='csv':
468 modules = set([t[0] for t in trans[1:]])
469 _process(format, modules, trans, buffer, lang, newlang)
472 def trans_parse_xsl(de):
477 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
479 l = m.text.strip().replace('\n',' ')
481 res.append(l.encode("utf8"))
482 res.extend(trans_parse_xsl(n))
485 def trans_parse_rml(de):
489 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
491 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
492 for s in string_list:
494 res.append(s.encode("utf8"))
495 res.extend(trans_parse_rml(n))
498 def trans_parse_view(de):
500 if de.tag == 'attribute' and de.get("name") == 'string':
502 res.append(de.text.encode("utf8"))
504 res.append(de.get('string').encode("utf8"))
506 res.append(de.get('sum').encode("utf8"))
507 if de.get("confirm"):
508 res.append(de.get('confirm').encode("utf8"))
510 res.extend(trans_parse_view(n))
513 # tests whether an object is in a list of modules
514 def in_modules(object_name, modules):
523 module = object_name.split('.')[0]
524 module = module_dict.get(module, module)
525 return module in modules
527 def trans_generate(lang, modules, cr):
528 logger = logging.getLogger('i18n')
531 pool = pooler.get_pool(dbname)
532 trans_obj = pool.get('ir.translation')
533 model_data_obj = pool.get('ir.model.data')
535 l = pool.obj_pool.items()
538 query = 'SELECT name, model, res_id, module' \
539 ' FROM ir_model_data'
541 query_models = """SELECT m.id, m.model, imd.module
542 FROM ir_model AS m, ir_model_data AS imd
543 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
545 if 'all_installed' in modules:
546 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
547 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
549 if 'all' not in modules:
550 query += ' WHERE module IN %s'
551 query_models += ' AND imd.module in %s'
552 query_param = (tuple(modules),)
553 query += ' ORDER BY module, model, name'
554 query_models += ' ORDER BY module, model'
556 cr.execute(query, query_param)
559 def push_translation(module, type, name, id, source):
560 tuple = (module, source, name, id, type)
561 if source and tuple not in _to_translate:
562 _to_translate.append(tuple)
565 if isinstance(s, unicode):
566 return s.encode('utf8')
569 for (xml_name,model,res_id,module) in cr.fetchall():
570 module = encode(module)
571 model = encode(model)
572 xml_name = "%s.%s" % (module, encode(xml_name))
574 if not pool.get(model):
575 logger.error("Unable to find object %r", model)
578 exists = pool.get(model).exists(cr, uid, res_id)
580 logger.warning("Unable to find object %r with id %d", model, res_id)
582 obj = pool.get(model).browse(cr, uid, res_id)
584 if model=='ir.ui.view':
585 d = etree.XML(encode(obj.arch))
586 for t in trans_parse_view(d):
587 push_translation(module, 'view', encode(obj.model), 0, t)
588 elif model=='ir.actions.wizard':
589 service_name = 'wizard.'+encode(obj.wiz_name)
590 if netsvc.Service._services.get(service_name):
591 obj2 = netsvc.Service._services[service_name]
592 for state_name, state_def in obj2.states.iteritems():
593 if 'result' in state_def:
594 result = state_def['result']
595 if result['type'] != 'form':
597 name = "%s,%s" % (encode(obj.wiz_name), state_name)
600 'string': ('wizard_field', lambda s: [encode(s)]),
601 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
602 'help': ('help', lambda s: [encode(s)]),
606 if not result.has_key('fields'):
607 logger.warning("res has no fields: %r", result)
609 for field_name, field_def in result['fields'].iteritems():
610 res_name = name + ',' + field_name
612 for fn in def_params:
614 transtype, modifier = def_params[fn]
615 for val in modifier(field_def[fn]):
616 push_translation(module, transtype, res_name, 0, val)
619 arch = result['arch']
620 if arch and not isinstance(arch, UpdateableStr):
622 for t in trans_parse_view(d):
623 push_translation(module, 'wizard_view', name, 0, t)
625 # export button labels
626 for but_args in result['state']:
627 button_name = but_args[0]
628 button_label = but_args[1]
629 res_name = name + ',' + button_name
630 push_translation(module, 'wizard_button', res_name, 0, button_label)
632 elif model=='ir.model.fields':
634 field_name = encode(obj.name)
635 except AttributeError, exc:
636 logger.error("name error in %s: %s", xml_name, str(exc))
638 objmodel = pool.get(obj.model)
639 if not objmodel or not field_name in objmodel._columns:
641 field_def = objmodel._columns[field_name]
643 name = "%s,%s" % (encode(obj.model), field_name)
644 push_translation(module, 'field', name, 0, encode(field_def.string))
647 push_translation(module, 'help', name, 0, encode(field_def.help))
649 if field_def.translate:
650 ids = objmodel.search(cr, uid, [])
651 obj_values = objmodel.read(cr, uid, ids, [field_name])
652 for obj_value in obj_values:
653 res_id = obj_value['id']
654 if obj.name in ('ir.model', 'ir.ui.menu'):
656 model_data_ids = model_data_obj.search(cr, uid, [
657 ('model', '=', model),
658 ('res_id', '=', res_id),
660 if not model_data_ids:
661 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
663 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
664 for dummy, val in field_def.selection:
665 push_translation(module, 'selection', name, 0, encode(val))
667 elif model=='ir.actions.report.xml':
668 name = encode(obj.report_name)
671 fname = obj.report_rml
672 parse_func = trans_parse_rml
673 report_type = "report"
675 fname = obj.report_xsl
676 parse_func = trans_parse_xsl
678 if fname and obj.report_type in ('pdf', 'xsl'):
680 report_file = tools.file_open(fname)
682 d = etree.parse(report_file)
683 for t in parse_func(d.iter()):
684 push_translation(module, report_type, name, 0, t)
687 except (IOError, etree.XMLSyntaxError):
688 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
690 for field_name,field_def in obj._table._columns.items():
691 if field_def.translate:
692 name = model + "," + field_name
694 trad = getattr(obj, field_name) or ''
697 push_translation(module, 'model', name, xml_name, encode(trad))
699 # End of data for ir.model.data query results
701 cr.execute(query_models, query_param)
703 def push_constraint_msg(module, term_type, model, msg):
704 # Check presence of __call__ directly instead of using
705 # callable() because it will be deprecated as of Python 3.0
706 if not hasattr(msg, '__call__'):
707 push_translation(module, term_type, model, 0, encode(msg))
709 for (model_id, model, module) in cr.fetchall():
710 module = encode(module)
711 model = encode(model)
713 model_obj = pool.get(model)
716 logging.getLogger("i18n").error("Unable to find object %r", model)
719 for constraint in getattr(model_obj, '_constraints', []):
720 push_constraint_msg(module, 'constraint', model, constraint[1])
722 for constraint in getattr(model_obj, '_sql_constraints', []):
723 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
725 # parse source code for _() calls
726 def get_module_from_path(path, mod_paths=None):
728 # First, construct a list of possible paths
729 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
730 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
733 mod_paths.append(adp)
734 if not os.path.isabs(adp):
735 mod_paths.append(adp)
736 elif adp.startswith(def_path):
737 mod_paths.append(adp[len(def_path)+1:])
739 if path.startswith(mp) and (os.path.dirname(path) != mp):
740 path = path[len(mp)+1:]
741 return path.split(os.path.sep)[0]
742 return 'base' # files that are not in a module are considered as being in 'base' module
744 modobj = pool.get('ir.module.module')
745 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
746 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
748 root_path = os.path.join(tools.config['root_path'], 'addons')
750 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
751 if root_path in apaths:
754 path_list = [root_path,] + apaths
756 # Also scan these non-addon paths
757 for bin_path in ['osv', 'report' ]:
758 path_list.append(os.path.join(tools.config['root_path'], bin_path))
760 logger.debug("Scanning modules at paths: ", path_list)
763 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
764 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
765 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
766 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
768 def export_code_terms_from_file(fname, path, root, terms_type):
769 fabsolutepath = join(root, fname)
770 frelativepath = fabsolutepath[len(path):]
771 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
772 is_mod_installed = module in installed_modules
773 if (('all' in modules) or (module in modules)) and is_mod_installed:
774 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
775 src_file = tools.file_open(fabsolutepath, subdir='')
777 code_string = src_file.read()
780 if module in installed_modules:
781 frelativepath = str("addons" + frelativepath)
782 ite = re_dquotes.finditer(code_string)
787 if src.startswith('""'):
788 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
791 src = join_dquotes.sub(r'\1', src)
792 # try to count the lines from the last pos to our place:
793 code_line += code_string[code_offset:i.start(1)].count('\n')
794 # now, since we did a binary read of a python source file, we
795 # have to expand pythonic escapes like the interpreter does.
796 src = src.decode('string_escape')
797 push_translation(module, terms_type, frelativepath, code_line, encode(src))
798 code_line += i.group(1).count('\n')
799 code_offset = i.end() # we have counted newlines up to the match end
801 ite = re_quotes.finditer(code_string)
802 code_offset = 0 #reset counters
806 if src.startswith("''"):
807 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
810 src = join_quotes.sub(r'\1', src)
811 code_line += code_string[code_offset:i.start(1)].count('\n')
812 src = src.decode('string_escape')
813 push_translation(module, terms_type, frelativepath, code_line, encode(src))
814 code_line += i.group(1).count('\n')
815 code_offset = i.end() # we have counted newlines up to the match end
817 for path in path_list:
818 logger.debug("Scanning files of modules at %s", path)
819 for root, dummy, files in tools.osutil.walksymlinks(path):
820 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
821 export_code_terms_from_file(fname, path, root, 'code')
822 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
823 export_code_terms_from_file(fname, path, root, 'report')
826 out = [["module","type","name","res_id","src","value"]] # header
828 # translate strings marked as to be translated
829 for module, source, name, id, type in _to_translate:
830 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
831 out.append([module, type, name, id, source, encode(trans) or ''])
835 def trans_load(cr, filename, lang, verbose=True, context=None):
836 logger = logging.getLogger('i18n')
838 fileobj = open(filename,'r')
839 logger.info("loading %s", filename)
840 fileformat = os.path.splitext(filename)[-1][1:].lower()
841 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
846 logger.error("couldn't read translation file %s", filename)
849 # Populates the ir_translation table. Fixing the res_ids so that they point
850 # correctly to ir_model_data is done in a separate step, using the
851 # 'trans_update_res_ids' function below.
852 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
853 logger = logging.getLogger('i18n')
855 logger.info('loading translation file for language %s', lang)
859 pool = pooler.get_pool(db_name)
860 lang_obj = pool.get('res.lang')
861 trans_obj = pool.get('ir.translation')
862 model_data_obj = pool.get('ir.model.data')
863 iso_lang = tools.get_iso_codes(lang)
866 ids = lang_obj.search(cr, uid, [('code','=', lang)])
869 # lets create the language with locale information
871 for ln in get_locales(lang):
873 locale.setlocale(locale.LC_ALL, str(ln))
879 lc = locale.getdefaultlocale()[0]
880 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
881 logger.warning(msg, lang, lc)
884 lang_name = tools.get_languages().get(lang, lang)
893 'iso_code': iso_lang,
896 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
897 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
898 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
899 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
903 lang_obj.create(cr, uid, lang_info)
908 # now, the serious things: we read the language file
910 if fileformat == 'csv':
911 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
912 # read the first line of the file (it contains columns titles)
916 elif fileformat == 'po':
917 reader = TinyPoFile(fileobj)
918 f = ['type', 'name', 'res_id', 'src', 'value']
920 logger.error('Bad file format: %s', fileformat)
921 raise Exception(_('Bad file format'))
923 # read the rest of the file
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': ...}
935 for i in range(len(f)):
936 if f[i] in ('module',):
941 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
942 dic['module'] = False
943 dic['xml_id'] = False
945 splitted = dic['res_id'].split('.', 1)
946 dic['module'] = splitted[0]
947 dic['xml_id'] = splitted[1]
948 dic['res_id'] = False
952 ('type', '=', dic['type']),
953 ('name', '=', dic['name']),
954 ('src', '=', dic['src']),
956 if dic['type'] == 'model':
957 args.append(('res_id', '=', dic['res_id']))
958 ids = trans_obj.search(cr, uid, args)
960 if context.get('overwrite') and dic['value']:
961 trans_obj.write(cr, uid, ids, {'value': dic['value']})
963 trans_obj.create(cr, uid, dic)
965 logger.info("translation file loaded succesfully")
967 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
968 logger.exception("couldn't read translation file %s", filename)
970 def trans_update_res_ids(cr):
972 update ir_translation
973 set res_id = ir_model_data.res_id
975 where ir_translation.module = ir_model_data.module
976 and ir_translation.xml_id = ir_model_data.name
977 and ir_translation.module is not null
978 and ir_translation.xml_id is not null;
981 def get_locales(lang=None):
983 lang = locale.getdefaultlocale()[0]
986 lang = _LOCALE2WIN32.get(lang, lang)
989 ln = locale._build_localename((lang, enc))
991 nln = locale.normalize(ln)
995 for x in process('utf8'): yield x
997 prefenc = locale.getpreferredencoding()
999 for x in process(prefenc): yield x
1003 'iso-8859-1': 'iso8859-15',
1005 }.get(prefenc.lower())
1007 for x in process(prefenc): yield x
1014 # locale.resetlocale is bugged with some locales.
1015 for ln in get_locales():
1017 return locale.setlocale(locale.LC_ALL, ln)
1018 except locale.Error:
1021 def load_language(cr, lang):
1022 """Loads a translation terms for a language.
1023 Used mainly to automate language loading at db initialization.
1025 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1028 pool = pooler.get_pool(cr.dbname)
1029 language_installer = pool.get('base.language.install')
1031 oid = language_installer.create(cr, uid, {'lang': lang})
1032 language_installer.lang_install(cr, uid, [oid], context=None)
1034 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: