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, dbname=None):
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, dbname)
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, dbname=None):
528 logger = logging.getLogger('i18n')
530 dbname=tools.config['db_name']
534 pool = pooler.get_pool(dbname)
535 trans_obj = pool.get('ir.translation')
536 model_data_obj = pool.get('ir.model.data')
537 cr = pooler.get_db(dbname).cursor()
539 l = pool.obj_pool.items()
542 query = 'SELECT name, model, res_id, module' \
543 ' FROM ir_model_data'
545 query_models = """SELECT m.id, m.model, imd.module
546 FROM ir_model AS m, ir_model_data AS imd
547 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
549 if 'all_installed' in modules:
550 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
551 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
553 if 'all' not in modules:
554 query += ' WHERE module IN %s'
555 query_models += ' AND imd.module in %s'
556 query_param = (tuple(modules),)
557 query += ' ORDER BY module, model, name'
558 query_models += ' ORDER BY module, model'
560 cr.execute(query, query_param)
563 def push_translation(module, type, name, id, source):
564 tuple = (module, source, name, id, type)
565 if source and tuple not in _to_translate:
566 _to_translate.append(tuple)
569 if isinstance(s, unicode):
570 return s.encode('utf8')
573 for (xml_name,model,res_id,module) in cr.fetchall():
574 module = encode(module)
575 model = encode(model)
576 xml_name = "%s.%s" % (module, encode(xml_name))
578 if not pool.get(model):
579 logger.error("Unable to find object %r", model)
582 exists = pool.get(model).exists(cr, uid, res_id)
584 logger.warning("Unable to find object %r with id %d", model, res_id)
586 obj = pool.get(model).browse(cr, uid, res_id)
588 if model=='ir.ui.view':
589 d = etree.XML(encode(obj.arch))
590 for t in trans_parse_view(d):
591 push_translation(module, 'view', encode(obj.model), 0, t)
592 elif model=='ir.actions.wizard':
593 service_name = 'wizard.'+encode(obj.wiz_name)
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 d = etree.parse(tools.file_open(fname))
685 for t in parse_func(d.iter()):
686 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 code_string = tools.file_open(fabsolutepath, subdir='').read()
776 if module in installed_modules:
777 frelativepath = str("addons" + frelativepath)
778 ite = re_dquotes.finditer(code_string)
783 if src.startswith('""'):
784 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
787 src = join_dquotes.sub(r'\1', src)
788 # try to count the lines from the last pos to our place:
789 code_line += code_string[code_offset:i.start(1)].count('\n')
790 # now, since we did a binary read of a python source file, we
791 # have to expand pythonic escapes like the interpreter does.
792 src = src.decode('string_escape')
793 push_translation(module, terms_type, frelativepath, code_line, encode(src))
794 code_line += i.group(1).count('\n')
795 code_offset = i.end() # we have counted newlines up to the match end
797 ite = re_quotes.finditer(code_string)
798 code_offset = 0 #reset counters
802 if src.startswith("''"):
803 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
806 src = join_quotes.sub(r'\1', src)
807 code_line += code_string[code_offset:i.start(1)].count('\n')
808 src = src.decode('string_escape')
809 push_translation(module, terms_type, frelativepath, code_line, encode(src))
810 code_line += i.group(1).count('\n')
811 code_offset = i.end() # we have counted newlines up to the match end
813 for path in path_list:
814 logger.debug("Scanning files of modules at %s", path)
815 for root, dummy, files in tools.osutil.walksymlinks(path):
816 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
817 export_code_terms_from_file(fname, path, root, 'code')
818 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
819 export_code_terms_from_file(fname, path, root, 'report')
822 out = [["module","type","name","res_id","src","value"]] # header
824 # translate strings marked as to be translated
825 for module, source, name, id, type in _to_translate:
826 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
827 out.append([module, type, name, id, source, encode(trans) or ''])
832 def trans_load(db_name, filename, lang, verbose=True, context=None):
833 logger = logging.getLogger('i18n')
835 fileobj = open(filename,'r')
836 logger.info("loading %s", filename)
837 fileformat = os.path.splitext(filename)[-1][1:].lower()
838 r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
843 logger.error("couldn't read translation file %s", filename)
846 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
847 logger = logging.getLogger('i18n')
849 logger.info('loading translation file for language %s', lang)
852 pool = pooler.get_pool(db_name)
853 lang_obj = pool.get('res.lang')
854 trans_obj = pool.get('ir.translation')
855 model_data_obj = pool.get('ir.model.data')
856 iso_lang = tools.get_iso_codes(lang)
859 cr = pooler.get_db(db_name).cursor()
860 ids = lang_obj.search(cr, uid, [('code','=', lang)])
863 # lets create the language with locale information
865 for ln in get_locales(lang):
867 locale.setlocale(locale.LC_ALL, str(ln))
873 lc = locale.getdefaultlocale()[0]
874 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
875 logger.warning(msg, lang, lc)
878 lang_name = tools.get_languages().get(lang, lang)
887 'iso_code': iso_lang,
890 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
891 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
892 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
893 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
897 lang_obj.create(cr, uid, lang_info)
902 # now, the serious things: we read the language file
904 if fileformat == 'csv':
905 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
906 # read the first line of the file (it contains columns titles)
910 elif fileformat == 'po':
911 reader = TinyPoFile(fileobj)
912 f = ['type', 'name', 'res_id', 'src', 'value']
914 logger.error('Bad file format: %s', fileformat)
915 raise Exception(_('Bad file format'))
917 # read the rest of the file
921 # skip empty rows and rows where the translation field (=last fiefd) is empty
922 #if (not row) or (not row[-1]):
925 # dictionary which holds values for this line of the csv file
926 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
927 # 'src': ..., 'value': ...}
929 for i in range(len(f)):
930 if f[i] in ('module',):
935 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
937 model_data_ids = model_data_obj.search(cr, uid, [
938 ('model', '=', dic['name'].split(',')[0]),
939 ('module', '=', dic['res_id'].split('.', 1)[0]),
940 ('name', '=', dic['res_id'].split('.', 1)[1]),
943 dic['res_id'] = model_data_obj.browse(cr, uid,
944 model_data_ids[0]).res_id
946 dic['res_id'] = False
950 ('type', '=', dic['type']),
951 ('name', '=', dic['name']),
952 ('src', '=', dic['src']),
954 if dic['type'] == 'model':
955 args.append(('res_id', '=', dic['res_id']))
956 ids = trans_obj.search(cr, uid, args)
958 if context.get('overwrite'):
959 trans_obj.write(cr, uid, ids, {'value': dic['value']})
961 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 get_locales(lang=None):
972 lang = locale.getdefaultlocale()[0]
975 lang = _LOCALE2WIN32.get(lang, lang)
978 ln = locale._build_localename((lang, enc))
980 nln = locale.normalize(ln)
984 for x in process('utf8'): yield x
986 prefenc = locale.getpreferredencoding()
988 for x in process(prefenc): yield x
992 'iso-8859-1': 'iso8859-15',
994 }.get(prefenc.lower())
996 for x in process(prefenc): yield x
1003 # locale.resetlocale is bugged with some locales.
1004 for ln in get_locales():
1006 return locale.setlocale(locale.LC_ALL, ln)
1007 except locale.Error:
1010 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: