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
45 'af_ZA': 'Afrikaans_South Africa',
46 'sq_AL': 'Albanian_Albania',
47 'ar_SA': 'Arabic_Saudi Arabia',
48 'eu_ES': 'Basque_Spain',
49 'be_BY': 'Belarusian_Belarus',
50 'bs_BA': 'Serbian (Latin)',
51 'bg_BG': 'Bulgarian_Bulgaria',
52 'ca_ES': 'Catalan_Spain',
53 'hr_HR': 'Croatian_Croatia',
54 'zh_CN': 'Chinese_China',
55 'zh_TW': 'Chinese_Taiwan',
56 'cs_CZ': 'Czech_Czech Republic',
57 'da_DK': 'Danish_Denmark',
58 'nl_NL': 'Dutch_Netherlands',
59 'et_EE': 'Estonian_Estonia',
60 'fa_IR': 'Farsi_Iran',
61 'ph_PH': 'Filipino_Philippines',
62 'fi_FI': 'Finnish_Finland',
63 'fr_FR': 'French_France',
64 'fr_BE': 'French_France',
65 'fr_CH': 'French_France',
66 'fr_CA': 'French_France',
67 'ga': 'Scottish Gaelic',
68 'gl_ES': 'Galician_Spain',
69 'ka_GE': 'Georgian_Georgia',
70 'de_DE': 'German_Germany',
71 'el_GR': 'Greek_Greece',
72 'gu': 'Gujarati_India',
73 'he_IL': 'Hebrew_Israel',
75 'hu': 'Hungarian_Hungary',
76 'is_IS': 'Icelandic_Iceland',
77 'id_ID': 'Indonesian_indonesia',
78 'it_IT': 'Italian_Italy',
79 'ja_JP': 'Japanese_Japan',
82 'ko_KR': 'Korean_Korea',
84 'lt_LT': 'Lithuanian_Lithuania',
85 'lat': 'Latvian_Latvia',
86 'ml_IN': 'Malayalam_India',
87 'id_ID': 'Indonesian_indonesia',
89 'mn': 'Cyrillic_Mongolian',
90 'no_NO': 'Norwegian_Norway',
91 'nn_NO': 'Norwegian-Nynorsk_Norway',
92 'pl': 'Polish_Poland',
93 'pt_PT': 'Portuguese_Portugal',
94 'pt_BR': 'Portuguese_Brazil',
95 'ro_RO': 'Romanian_Romania',
96 'ru_RU': 'Russian_Russia',
98 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
99 'sk_SK': 'Slovak_Slovakia',
100 'sl_SI': 'Slovenian_Slovenia',
101 #should find more specific locales for spanish countries,
102 #but better than nothing
103 'es_AR': 'Spanish_Spain',
104 'es_BO': 'Spanish_Spain',
105 'es_CL': 'Spanish_Spain',
106 'es_CO': 'Spanish_Spain',
107 'es_CR': 'Spanish_Spain',
108 'es_DO': 'Spanish_Spain',
109 'es_EC': 'Spanish_Spain',
110 'es_ES': 'Spanish_Spain',
111 'es_GT': 'Spanish_Spain',
112 'es_HN': 'Spanish_Spain',
113 'es_MX': 'Spanish_Spain',
114 'es_NI': 'Spanish_Spain',
115 'es_PA': 'Spanish_Spain',
116 'es_PE': 'Spanish_Spain',
117 'es_PR': 'Spanish_Spain',
118 'es_PY': 'Spanish_Spain',
119 'es_SV': 'Spanish_Spain',
120 'es_UY': 'Spanish_Spain',
121 'es_VE': 'Spanish_Spain',
122 'sv_SE': 'Swedish_Sweden',
123 'ta_IN': 'English_Australia',
124 'th_TH': 'Thai_Thailand',
126 'tr_TR': 'Turkish_Turkey',
127 'uk_UA': 'Ukrainian_Ukraine',
128 'vi_VN': 'Vietnamese_Viet Nam',
129 'tlh_TLH': 'Klingon',
134 class UNIX_LINE_TERMINATOR(csv.excel):
135 lineterminator = '\n'
137 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
140 # Warning: better use self.pool.get('ir.translation')._get_source if you can
142 def translate(cr, name, source_type, lang, source=None):
144 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))
146 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
148 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
149 res_trans = cr.fetchone()
150 res = res_trans and res_trans[0] or False
153 logger = logging.getLogger('translate')
155 class GettextAlias(object):
158 # find current DB based on thread/worker db name (see netsvc)
159 db_name = getattr(threading.currentThread(), 'dbname', None)
161 return pooler.get_db_only(db_name)
163 def _get_cr(self, frame):
165 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
167 s = frame.f_locals.get('self', {})
168 cr = getattr(s, 'cr', None)
176 def _get_lang(self, frame):
178 ctx = frame.f_locals.get('context')
180 kwargs = frame.f_locals.get('kwargs')
182 args = frame.f_locals.get('args')
183 if args and isinstance(args, (list, tuple)) \
184 and isinstance(args[-1], dict):
186 elif isinstance(kwargs, dict):
187 ctx = kwargs.get('context')
189 lang = ctx.get('lang')
191 s = frame.f_locals.get('self', {})
192 c = getattr(s, 'localcontext', None)
197 def __call__(self, source):
202 frame = inspect.currentframe()
208 lang = self._get_lang(frame)
210 cr, is_new_cr = self._get_cr(frame)
212 # Try to use ir.translation to benefit from global cache if possible
213 pool = pooler.get_pool(cr.dbname)
214 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
216 logger.debug('no context cursor detected, skipping translation for "%r"', source)
218 logger.debug('no translation language detected, skipping translation for "%r" ', source)
220 logger.debug('translation went wrong for "%r", skipped', source)
221 # if so, double-check the root/base translations filenames
231 """Returns quoted PO term string, with special PO characters escaped"""
232 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
233 return '"%s"' % s.replace('\\','\\\\') \
234 .replace('"','\\"') \
235 .replace('\n', '\\n"\n"')
237 re_escaped_char = re.compile(r"(\\.)")
238 re_escaped_replacements = {'n': '\n', }
240 def _sub_replacement(match_obj):
241 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
244 """Returns unquoted PO term string, with special PO characters unescaped"""
245 return re_escaped_char.sub(_sub_replacement, str[1:-1])
247 # class to handle po files
248 class TinyPoFile(object):
249 def __init__(self, buffer):
250 self.logger = logging.getLogger('i18n')
253 def warn(self, msg, *args):
254 self.logger.warning(msg, *args)
258 self.lines = self._get_lines()
259 self.lines_count = len(self.lines);
265 def _get_lines(self):
266 lines = self.buffer.readlines()
267 # remove the BOM (Byte Order Mark):
269 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
271 lines.append('') # ensure that the file ends with at least an empty line
275 return (self.lines_count - len(self.lines))
278 type = name = res_id = source = trad = None
281 type, name, res_id, source, trad = self.tnrs.pop(0)
289 if 0 == len(self.lines):
290 raise StopIteration()
291 line = self.lines.pop(0).strip()
292 while line.startswith('#'):
293 if line.startswith('#~ '):
295 if line.startswith('#:'):
296 if ' ' in line[2:].strip():
297 for lpart in line[2:].strip().split(' '):
298 tmp_tnrs.append(lpart.strip().split(':',2))
300 tmp_tnrs.append( line[2:].strip().split(':',2) )
301 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
303 line = self.lines.pop(0).strip()
305 # allow empty lines between comments and msgid
306 line = self.lines.pop(0).strip()
307 if line.startswith('#~ '):
308 while line.startswith('#~ ') or not line.strip():
309 if 0 == len(self.lines):
310 raise StopIteration()
311 line = self.lines.pop(0)
312 # This has been a deprecated entry, don't return anything
315 if not line.startswith('msgid'):
316 raise Exception("malformed file: bad line: %s" % line)
317 source = unquote(line[6:])
318 line = self.lines.pop(0).strip()
319 if not source and self.first:
320 # if the source is "" and it's the first msgid, it's the special
321 # msgstr with the informations about the traduction and the
322 # traductor; we skip it
325 line = self.lines.pop(0).strip()
328 while not line.startswith('msgstr'):
330 raise Exception('malformed file at %d'% self.cur_line())
331 source += unquote(line)
332 line = self.lines.pop(0).strip()
334 trad = unquote(line[7:])
335 line = self.lines.pop(0).strip()
337 trad += unquote(line)
338 line = self.lines.pop(0).strip()
340 if tmp_tnrs and not fuzzy:
341 type, name, res_id = tmp_tnrs.pop(0)
342 for t, n, r in tmp_tnrs:
343 self.tnrs.append((t, n, r, source, trad))
349 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
350 self.cur_line(), source[:30])
352 return type, name, res_id, source, trad
354 def write_infos(self, modules):
356 self.buffer.write("# Translation of %(project)s.\n" \
357 "# This file contains the translation of the following modules:\n" \
362 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
363 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
364 '''"POT-Creation-Date: %(now)s\\n"\n''' \
365 '''"PO-Revision-Date: %(now)s\\n"\n''' \
366 '''"Last-Translator: <>\\n"\n''' \
367 '''"Language-Team: \\n"\n''' \
368 '''"MIME-Version: 1.0\\n"\n''' \
369 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
370 '''"Content-Transfer-Encoding: \\n"\n''' \
371 '''"Plural-Forms: \\n"\n''' \
374 % { 'project': release.description,
375 'version': release.version,
376 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
377 'bugmail': release.support_email,
378 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
382 def write(self, modules, tnrs, source, trad):
384 plurial = len(modules) > 1 and 's' or ''
385 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
389 for typy, name, res_id in tnrs:
390 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
395 # only strings in python code are python formated
396 self.buffer.write("#, python-format\n")
398 if not isinstance(trad, unicode):
399 trad = unicode(trad, 'utf8')
400 if not isinstance(source, unicode):
401 source = unicode(source, 'utf8')
405 % (quote(source), quote(trad))
406 self.buffer.write(msg.encode('utf8'))
409 # Methods to export the translation file
411 def trans_export(lang, modules, buffer, format, dbname=None):
413 def _process(format, modules, rows, buffer, lang, newlang):
415 writer=csv.writer(buffer, 'UNIX')
420 writer = tools.TinyPoFile(buffer)
421 writer.write_infos(modules)
423 # we now group the translations by source. That means one translation per source.
425 for module, type, name, res_id, src, trad in rows:
426 row = grouped_rows.setdefault(src, {})
427 row.setdefault('modules', set()).add(module)
428 if ('translation' not in row) or (not row['translation']):
429 row['translation'] = trad
430 row.setdefault('tnrs', []).append((type, name, res_id))
432 for src, row in grouped_rows.items():
433 writer.write(row['modules'], row['tnrs'], src, row['translation'])
435 elif format == 'tgz':
440 # first row is the "header", as in csv, it will be popped
441 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
442 rows_by_module[module].append(row)
444 tmpdir = tempfile.mkdtemp()
445 for mod, modrows in rows_by_module.items():
446 tmpmoddir = join(tmpdir, mod, 'i18n')
447 os.makedirs(tmpmoddir)
448 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
449 buf = file(join(tmpmoddir, pofilename), 'w')
450 _process('po', [mod], modrows, buf, lang, newlang)
453 tar = tarfile.open(fileobj=buffer, mode='w|gz')
458 raise Exception(_('Bad file format'))
460 newlang = not bool(lang)
463 trans = trans_generate(lang, modules, dbname)
464 if newlang and format!='csv':
467 modules = set([t[0] for t in trans[1:]])
468 _process(format, modules, trans, buffer, lang, newlang)
471 # We'd better not import orm here, so copy this from osv.orm:
472 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
474 def trans_parse_xsl(de):
479 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
481 l = m.text.strip().replace('\n',' ')
483 res.append(l.encode("utf8"))
484 res.extend(trans_parse_xsl(n))
487 def trans_parse_rml(de):
491 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
493 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
494 for s in string_list:
496 res.append(s.encode("utf8"))
497 res.extend(trans_parse_rml(n))
500 def trans_parse_view(de):
502 if de.tag == 'attribute' and de.get("name") == 'string':
504 res.append(de.text.encode("utf8"))
506 res.append(de.get('string').encode("utf8"))
508 res.append(de.get('sum').encode("utf8"))
509 if de.get("confirm"):
510 res.append(de.get('confirm').encode("utf8"))
512 res.extend(trans_parse_view(n))
515 # tests whether an object is in a list of modules
516 def in_modules(object_name, modules):
525 module = object_name.split('.')[0]
526 module = module_dict.get(module, module)
527 return module in modules
529 def trans_generate(lang, modules, dbname=None):
530 logger = logging.getLogger('i18n')
532 dbname=tools.config['db_name']
536 pool = pooler.get_pool(dbname)
537 trans_obj = pool.get('ir.translation')
538 model_data_obj = pool.get('ir.model.data')
539 cr = pooler.get_db(dbname).cursor()
541 l = pool.obj_pool.items()
544 query = 'SELECT name, model, res_id, module' \
545 ' FROM ir_model_data'
547 query_models = """SELECT m.id, m.model, imd.module
548 FROM ir_model AS m, ir_model_data AS imd
549 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
551 if 'all_installed' in modules:
552 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
553 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
555 if 'all' not in modules:
556 query += ' WHERE module IN %s'
557 query_models += ' AND imd.module in %s'
558 query_param = (tuple(modules),)
559 query += ' ORDER BY module, model, name'
560 query_models += ' ORDER BY module, model'
562 cr.execute(query, query_param)
565 def push_translation(module, type, name, id, source):
566 tuple = (module, source, name, id, type)
567 if source and tuple not in _to_translate:
568 _to_translate.append(tuple)
571 if isinstance(s, unicode):
572 return s.encode('utf8')
575 for (xml_name,model,res_id,module) in cr.fetchall():
576 module = encode(module)
577 model = encode(model)
578 xml_name = "%s.%s" % (module, encode(xml_name))
580 if not pool.get(model):
581 logger.error("Unable to find object %r", model)
584 exists = pool.get(model).exists(cr, uid, res_id)
586 logger.warning("Unable to find object %r with id %d", model, res_id)
588 obj = pool.get(model).browse(cr, uid, res_id)
590 if model=='ir.ui.view':
591 d = etree.XML(encode(obj.arch))
592 for t in trans_parse_view(d):
593 push_translation(module, 'view', encode(obj.model), 0, t)
594 elif model=='ir.actions.wizard':
595 service_name = 'wizard.'+encode(obj.wiz_name)
596 if netsvc.Service._services.get(service_name):
597 obj2 = netsvc.Service._services[service_name]
598 for state_name, state_def in obj2.states.iteritems():
599 if 'result' in state_def:
600 result = state_def['result']
601 if result['type'] != 'form':
603 name = "%s,%s" % (encode(obj.wiz_name), state_name)
606 'string': ('wizard_field', lambda s: [encode(s)]),
607 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
608 'help': ('help', lambda s: [encode(s)]),
612 if not result.has_key('fields'):
613 logger.warning("res has no fields: %r", result)
615 for field_name, field_def in result['fields'].iteritems():
616 res_name = name + ',' + field_name
618 for fn in def_params:
620 transtype, modifier = def_params[fn]
621 for val in modifier(field_def[fn]):
622 push_translation(module, transtype, res_name, 0, val)
625 arch = result['arch']
626 if arch and not isinstance(arch, UpdateableStr):
628 for t in trans_parse_view(d):
629 push_translation(module, 'wizard_view', name, 0, t)
631 # export button labels
632 for but_args in result['state']:
633 button_name = but_args[0]
634 button_label = but_args[1]
635 res_name = name + ',' + button_name
636 push_translation(module, 'wizard_button', res_name, 0, button_label)
638 elif model=='ir.model.fields':
640 field_name = encode(obj.name)
641 except AttributeError, exc:
642 logger.error("name error in %s: %s", xml_name, str(exc))
644 objmodel = pool.get(obj.model)
645 if not objmodel or not field_name in objmodel._columns:
647 field_def = objmodel._columns[field_name]
649 name = "%s,%s" % (encode(obj.model), field_name)
650 push_translation(module, 'field', name, 0, encode(field_def.string))
653 push_translation(module, 'help', name, 0, encode(field_def.help))
655 if field_def.translate:
656 ids = objmodel.search(cr, uid, [])
657 obj_values = objmodel.read(cr, uid, ids, [field_name])
658 for obj_value in obj_values:
659 res_id = obj_value['id']
660 if obj.name in ('ir.model', 'ir.ui.menu'):
662 model_data_ids = model_data_obj.search(cr, uid, [
663 ('model', '=', model),
664 ('res_id', '=', res_id),
666 if not model_data_ids:
667 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
669 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
670 for dummy, val in field_def.selection:
671 push_translation(module, 'selection', name, 0, encode(val))
673 elif model=='ir.actions.report.xml':
674 name = encode(obj.report_name)
677 fname = obj.report_rml
678 parse_func = trans_parse_rml
679 report_type = "report"
681 fname = obj.report_xsl
682 parse_func = trans_parse_xsl
684 if fname and obj.report_type in ('pdf', 'xsl'):
686 d = etree.parse(tools.file_open(fname))
687 for t in parse_func(d.iter()):
688 push_translation(module, report_type, name, 0, t)
689 except (IOError, etree.XMLSyntaxError):
690 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
692 for field_name,field_def in obj._table._columns.items():
693 if field_def.translate:
694 name = model + "," + field_name
696 trad = getattr(obj, field_name) or ''
699 push_translation(module, 'model', name, xml_name, encode(trad))
701 # End of data for ir.model.data query results
703 cr.execute(query_models, query_param)
705 def push_constraint_msg(module, term_type, model, msg):
706 # Check presence of __call__ directly instead of using
707 # callable() because it will be deprecated as of Python 3.0
708 if not hasattr(msg, '__call__'):
709 push_translation(module, term_type, model, 0, encode(msg))
711 for (model_id, model, module) in cr.fetchall():
712 module = encode(module)
713 model = encode(model)
715 model_obj = pool.get(model)
718 logging.getLogger("i18n").error("Unable to find object %r", model)
721 for constraint in getattr(model_obj, '_constraints', []):
722 push_constraint_msg(module, 'constraint', model, constraint[1])
724 for constraint in getattr(model_obj, '_sql_constraints', []):
725 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
727 # parse source code for _() calls
728 def get_module_from_path(path, mod_paths=None):
730 # First, construct a list of possible paths
731 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
732 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
735 mod_paths.append(adp)
736 if not os.path.isabs(adp):
737 mod_paths.append(adp)
738 elif adp.startswith(def_path):
739 mod_paths.append(adp[len(def_path)+1:])
741 if path.startswith(mp) and (os.path.dirname(path) != mp):
742 path = path[len(mp)+1:]
743 return path.split(os.path.sep)[0]
744 return 'base' # files that are not in a module are considered as being in 'base' module
746 modobj = pool.get('ir.module.module')
747 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
748 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
750 root_path = os.path.join(tools.config['root_path'], 'addons')
752 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
753 if root_path in apaths:
756 path_list = [root_path,] + apaths
758 # Also scan these non-addon paths
759 for bin_path in ['osv', 'report' ]:
760 path_list.append(os.path.join(tools.config['root_path'], bin_path))
762 logger.debug("Scanning modules at paths: ", path_list)
765 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
766 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
767 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
768 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
770 def export_code_terms_from_file(fname, path, root, terms_type):
771 fabsolutepath = join(root, fname)
772 frelativepath = fabsolutepath[len(path):]
773 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
774 is_mod_installed = module in installed_modules
775 if (('all' in modules) or (module in modules)) and is_mod_installed:
776 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
777 code_string = tools.file_open(fabsolutepath, subdir='').read()
778 if module in installed_modules:
779 frelativepath = str("addons" + frelativepath)
780 ite = re_dquotes.finditer(code_string)
785 if src.startswith('""'):
786 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
789 src = join_dquotes.sub(r'\1', src)
790 # try to count the lines from the last pos to our place:
791 code_line += code_string[code_offset:i.start(1)].count('\n')
792 # now, since we did a binary read of a python source file, we
793 # have to expand pythonic escapes like the interpreter does.
794 src = src.decode('string_escape')
795 push_translation(module, terms_type, frelativepath, code_line, encode(src))
796 code_line += i.group(1).count('\n')
797 code_offset = i.end() # we have counted newlines up to the match end
799 ite = re_quotes.finditer(code_string)
800 code_offset = 0 #reset counters
804 if src.startswith("''"):
805 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
808 src = join_quotes.sub(r'\1', src)
809 code_line += code_string[code_offset:i.start(1)].count('\n')
810 src = src.decode('string_escape')
811 push_translation(module, terms_type, frelativepath, code_line, encode(src))
812 code_line += i.group(1).count('\n')
813 code_offset = i.end() # we have counted newlines up to the match end
815 for path in path_list:
816 logger.debug("Scanning files of modules at %s", path)
817 for root, dummy, files in tools.osutil.walksymlinks(path):
818 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
819 export_code_terms_from_file(fname, path, root, 'code')
820 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
821 export_code_terms_from_file(fname, path, root, 'report')
824 out = [["module","type","name","res_id","src","value"]] # header
826 # translate strings marked as to be translated
827 for module, source, name, id, type in _to_translate:
828 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
829 out.append([module, type, name, id, source, encode(trans) or ''])
834 def trans_load(db_name, filename, lang, verbose=True, context=None):
835 logger = logging.getLogger('i18n')
837 fileobj = open(filename,'r')
838 logger.info("loading %s", filename)
839 fileformat = os.path.splitext(filename)[-1][1:].lower()
840 r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
845 logger.error("couldn't read translation file %s", filename)
848 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
849 logger = logging.getLogger('i18n')
851 logger.info('loading translation file for language %s', lang)
854 pool = pooler.get_pool(db_name)
855 lang_obj = pool.get('res.lang')
856 trans_obj = pool.get('ir.translation')
857 model_data_obj = pool.get('ir.model.data')
858 iso_lang = tools.get_iso_codes(lang)
861 cr = pooler.get_db(db_name).cursor()
862 ids = lang_obj.search(cr, uid, [('code','=', lang)])
865 # lets create the language with locale information
867 for ln in get_locales(lang):
869 locale.setlocale(locale.LC_ALL, str(ln))
875 lc = locale.getdefaultlocale()[0]
876 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
877 logger.warning(msg, lang, lc)
880 lang_name = tools.get_languages().get(lang, lang)
889 'iso_code': iso_lang,
892 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
893 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
894 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
895 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
899 lang_obj.create(cr, uid, lang_info)
904 # now, the serious things: we read the language file
906 if fileformat == 'csv':
907 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
908 # read the first line of the file (it contains columns titles)
912 elif fileformat == 'po':
913 reader = TinyPoFile(fileobj)
914 f = ['type', 'name', 'res_id', 'src', 'value']
916 logger.error('Bad file format: %s', fileformat)
917 raise Exception(_('Bad file format'))
919 # read the rest of the file
923 # skip empty rows and rows where the translation field (=last fiefd) is empty
924 #if (not row) or (not row[-1]):
927 # dictionary which holds values for this line of the csv file
928 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
929 # 'src': ..., 'value': ...}
931 for i in range(len(f)):
932 if f[i] in ('module',):
937 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
939 model_data_ids = model_data_obj.search(cr, uid, [
940 ('model', '=', dic['name'].split(',')[0]),
941 ('module', '=', dic['res_id'].split('.', 1)[0]),
942 ('name', '=', dic['res_id'].split('.', 1)[1]),
945 dic['res_id'] = model_data_obj.browse(cr, uid,
946 model_data_ids[0]).res_id
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'):
961 trans_obj.write(cr, uid, ids, {'value': dic['value']})
963 trans_obj.create(cr, uid, dic)
967 logger.info("translation file loaded succesfully")
969 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
970 logger.exception("couldn't read translation file %s", filename)
972 def get_locales(lang=None):
974 lang = locale.getdefaultlocale()[0]
977 lang = _LOCALE2WIN32.get(lang, lang)
980 ln = locale._build_localename((lang, enc))
982 nln = locale.normalize(ln)
986 for x in process('utf8'): yield x
988 prefenc = locale.getpreferredencoding()
990 for x in process(prefenc): yield x
994 'iso-8859-1': 'iso8859-15',
996 }.get(prefenc.lower())
998 for x in process(prefenc): yield x
1005 # locale.resetlocale is bugged with some locales.
1006 for ln in get_locales():
1008 return locale.setlocale(locale.LC_ALL, ln)
1009 except locale.Error:
1012 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: