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
38 from datetime import datetime
39 from lxml import etree
43 from tools.misc import UpdateableStr
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):
158 def _get_db_pool(self):
159 # find current DB based on thread/worker db name (see netsvc)
160 db_name = getattr(threading.currentThread(), 'dbname', None)
162 dbname = getattr(threading.currentThread(), 'dbname')
163 return pooler.get_db_and_pool(dbname)
166 def _get_cr(self, frame):
168 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
170 s = frame.f_locals.get('self', {})
171 cr = getattr(s, 'cr', None)
173 db, _ = self._get_db_pool()
179 def _get_lang(self, frame):
181 ctx = frame.f_locals.get('context')
183 kwargs = frame.f_locals.get('kwargs')
185 args = frame.f_locals.get('args')
186 if args and isinstance(args, (list, tuple)) \
187 and isinstance(args[-1], dict):
189 elif isinstance(kwargs, dict):
190 ctx = kwargs.get('context')
192 lang = ctx.get('lang')
194 s = frame.f_locals.get('self', {})
195 c = getattr(s, 'localcontext', None)
200 def __call__(self, source):
205 frame = inspect.currentframe()
211 lang = self._get_lang(frame)
213 cr, new_cr = self._get_cr(frame)
215 # Try to use ir.translation to benefit from global cache if possible
216 _, pool = self._get_db_pool()
218 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
220 cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
221 res_trans = cr.fetchone()
222 res = res_trans and res_trans[0] or source
224 logger.debug('no context cursor detected, skipping translation for "%r"', source)
226 logger.debug('no translation language detected, skipping translation for "%r" ', source)
228 logger.debug('translation went wrong for "%r", skipped', source)
238 """Returns quoted PO term string, with special PO characters escaped"""
239 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
240 return '"%s"' % s.replace('\\','\\\\') \
241 .replace('"','\\"') \
242 .replace('\n', '\\n"\n"')
244 re_escaped_char = re.compile(r"(\\.)")
245 re_escaped_replacements = {'n': '\n', }
247 def _sub_replacement(match_obj):
248 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
251 """Returns unquoted PO term string, with special PO characters unescaped"""
252 return re_escaped_char.sub(_sub_replacement, str[1:-1])
254 # class to handle po files
255 class TinyPoFile(object):
256 def __init__(self, buffer):
257 self.logger = logging.getLogger('i18n')
261 self.logger.warning(msg)
265 self.lines = self._get_lines()
266 self.lines_count = len(self.lines);
272 def _get_lines(self):
273 lines = self.buffer.readlines()
274 # remove the BOM (Byte Order Mark):
276 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
278 lines.append('') # ensure that the file ends with at least an empty line
282 return (self.lines_count - len(self.lines))
285 type = name = res_id = source = trad = None
288 type, name, res_id, source, trad = self.tnrs.pop(0)
296 if 0 == len(self.lines):
297 raise StopIteration()
298 line = self.lines.pop(0).strip()
299 while line.startswith('#'):
300 if line.startswith('#~ '):
302 if line.startswith('#:'):
303 if ' ' in line[2:].strip():
304 for lpart in line[2:].strip().split(' '):
305 tmp_tnrs.append(lpart.strip().split(':',2))
307 tmp_tnrs.append( line[2:].strip().split(':',2) )
308 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
310 line = self.lines.pop(0).strip()
312 # allow empty lines between comments and msgid
313 line = self.lines.pop(0).strip()
314 if line.startswith('#~ '):
315 while line.startswith('#~ ') or not line.strip():
316 if 0 == len(self.lines):
317 raise StopIteration()
318 line = self.lines.pop(0)
319 # This has been a deprecated entry, don't return anything
322 if not line.startswith('msgid'):
323 raise Exception("malformed file: bad line: %s" % line)
324 source = unquote(line[6:])
325 line = self.lines.pop(0).strip()
326 if not source and self.first:
327 # if the source is "" and it's the first msgid, it's the special
328 # msgstr with the informations about the traduction and the
329 # traductor; we skip it
332 line = self.lines.pop(0).strip()
335 while not line.startswith('msgstr'):
337 raise Exception('malformed file at %d'% self.cur_line())
338 source += unquote(line)
339 line = self.lines.pop(0).strip()
341 trad = unquote(line[7:])
342 line = self.lines.pop(0).strip()
344 trad += unquote(line)
345 line = self.lines.pop(0).strip()
347 if tmp_tnrs and not fuzzy:
348 type, name, res_id = tmp_tnrs.pop(0)
349 for t, n, r in tmp_tnrs:
350 self.tnrs.append((t, n, r, source, trad))
356 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
357 self.cur_line(), source[:30])
359 return type, name, res_id, source, trad
361 def write_infos(self, modules):
363 self.buffer.write("# Translation of %(project)s.\n" \
364 "# This file contains the translation of the following modules:\n" \
369 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
370 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
371 '''"POT-Creation-Date: %(now)s\\n"\n''' \
372 '''"PO-Revision-Date: %(now)s\\n"\n''' \
373 '''"Last-Translator: <>\\n"\n''' \
374 '''"Language-Team: \\n"\n''' \
375 '''"MIME-Version: 1.0\\n"\n''' \
376 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
377 '''"Content-Transfer-Encoding: \\n"\n''' \
378 '''"Plural-Forms: \\n"\n''' \
381 % { 'project': release.description,
382 'version': release.version,
383 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
384 'bugmail': release.support_email,
385 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
389 def write(self, modules, tnrs, source, trad):
391 plurial = len(modules) > 1 and 's' or ''
392 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
396 for typy, name, res_id in tnrs:
397 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
402 # only strings in python code are python formated
403 self.buffer.write("#, python-format\n")
405 if not isinstance(trad, unicode):
406 trad = unicode(trad, 'utf8')
407 if not isinstance(source, unicode):
408 source = unicode(source, 'utf8')
412 % (quote(source), quote(trad))
413 self.buffer.write(msg.encode('utf8'))
416 # Methods to export the translation file
418 def trans_export(lang, modules, buffer, format, dbname=None):
420 def _process(format, modules, rows, buffer, lang, newlang):
422 writer=csv.writer(buffer, 'UNIX')
427 writer = tools.TinyPoFile(buffer)
428 writer.write_infos(modules)
430 # we now group the translations by source. That means one translation per source.
432 for module, type, name, res_id, src, trad in rows:
433 row = grouped_rows.setdefault(src, {})
434 row.setdefault('modules', set()).add(module)
435 if ('translation' not in row) or (not row['translation']):
436 row['translation'] = trad
437 row.setdefault('tnrs', []).append((type, name, res_id))
439 for src, row in grouped_rows.items():
440 writer.write(row['modules'], row['tnrs'], src, row['translation'])
442 elif format == 'tgz':
447 rows_by_module.setdefault(module, []).append(row)
449 tmpdir = tempfile.mkdtemp()
450 for mod, modrows in rows_by_module.items():
451 tmpmoddir = join(tmpdir, mod, 'i18n')
452 os.makedirs(tmpmoddir)
453 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
454 buf = file(join(tmpmoddir, pofilename), 'w')
455 _process('po', [mod], modrows, buf, lang, newlang)
458 tar = tarfile.open(fileobj=buffer, mode='w|gz')
463 raise Exception(_('Bad file format'))
465 newlang = not bool(lang)
468 trans = trans_generate(lang, modules, dbname)
469 if newlang and format!='csv':
472 modules = set([t[0] for t in trans[1:]])
473 _process(format, modules, trans, buffer, lang, newlang)
477 def trans_parse_xsl(de):
481 for m in [j for j in n if j.text]:
482 l = m.text.strip().replace('\n',' ')
484 res.append(l.encode("utf8"))
485 res.extend(trans_parse_xsl(n))
488 def trans_parse_rml(de):
491 for m in [j for j in n if j.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('sum').encode("utf8"))
508 if de.get("confirm"):
509 res.append(de.get('confirm').encode("utf8"))
511 res.extend(trans_parse_view(n))
514 # tests whether an object is in a list of modules
515 def in_modules(object_name, modules):
524 module = object_name.split('.')[0]
525 module = module_dict.get(module, module)
526 return module in modules
528 def trans_generate(lang, modules, dbname=None):
529 logger = logging.getLogger('i18n')
531 dbname=tools.config['db_name']
535 pool = pooler.get_pool(dbname)
536 trans_obj = pool.get('ir.translation')
537 model_data_obj = pool.get('ir.model.data')
538 cr = pooler.get_db(dbname).cursor()
540 l = pool.obj_pool.items()
543 query = 'SELECT name, model, res_id, module' \
544 ' FROM ir_model_data'
546 query_models = """SELECT m.id, m.model, imd.module
547 FROM ir_model AS m, ir_model_data AS imd
548 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
550 if 'all_installed' in modules:
551 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
552 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
554 if 'all' not in modules:
555 query += ' WHERE module IN %s'
556 query_models += ' AND imd.module in %s'
557 query_param = (tuple(modules),)
558 query += ' ORDER BY module, model, name'
559 query_models += ' ORDER BY module, model'
561 cr.execute(query, query_param)
564 def push_translation(module, type, name, id, source):
565 tuple = (module, source, name, id, type)
566 if source and tuple not in _to_translate:
567 _to_translate.append(tuple)
570 if isinstance(s, unicode):
571 return s.encode('utf8')
574 for (xml_name,model,res_id,module) in cr.fetchall():
575 module = encode(module)
576 model = encode(model)
577 xml_name = "%s.%s" % (module, encode(xml_name))
579 if not pool.get(model):
580 logger.error("Unable to find object %r", model)
583 exists = pool.get(model).exists(cr, uid, res_id)
585 logger.warning("Unable to find object %r with id %d", model, res_id)
587 obj = pool.get(model).browse(cr, uid, res_id)
589 if model=='ir.ui.view':
590 d = etree.XML(encode(obj.arch))
591 for t in trans_parse_view(d):
592 push_translation(module, 'view', encode(obj.model), 0, t)
593 elif model=='ir.actions.wizard':
594 service_name = 'wizard.'+encode(obj.wiz_name)
595 if netsvc.Service._services.get(service_name):
596 obj2 = netsvc.Service._services[service_name]
597 for state_name, state_def in obj2.states.iteritems():
598 if 'result' in state_def:
599 result = state_def['result']
600 if result['type'] != 'form':
602 name = "%s,%s" % (encode(obj.wiz_name), state_name)
605 'string': ('wizard_field', lambda s: [encode(s)]),
606 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
607 'help': ('help', lambda s: [encode(s)]),
611 if not result.has_key('fields'):
612 logger.warning("res has no fields: %r", result)
614 for field_name, field_def in result['fields'].iteritems():
615 res_name = name + ',' + field_name
617 for fn in def_params:
619 transtype, modifier = def_params[fn]
620 for val in modifier(field_def[fn]):
621 push_translation(module, transtype, res_name, 0, val)
624 arch = result['arch']
625 if arch and not isinstance(arch, UpdateableStr):
627 for t in trans_parse_view(d):
628 push_translation(module, 'wizard_view', name, 0, t)
630 # export button labels
631 for but_args in result['state']:
632 button_name = but_args[0]
633 button_label = but_args[1]
634 res_name = name + ',' + button_name
635 push_translation(module, 'wizard_button', res_name, 0, button_label)
637 elif model=='ir.model.fields':
639 field_name = encode(obj.name)
640 except AttributeError, exc:
641 logger.error("name error in %s: %s", xml_name, str(exc))
643 objmodel = pool.get(obj.model)
644 if not objmodel or not field_name in objmodel._columns:
646 field_def = objmodel._columns[field_name]
648 name = "%s,%s" % (encode(obj.model), field_name)
649 push_translation(module, 'field', name, 0, encode(field_def.string))
652 push_translation(module, 'help', name, 0, encode(field_def.help))
654 if field_def.translate:
655 ids = objmodel.search(cr, uid, [])
656 obj_values = objmodel.read(cr, uid, ids, [field_name])
657 for obj_value in obj_values:
658 res_id = obj_value['id']
659 if obj.name in ('ir.model', 'ir.ui.menu'):
661 model_data_ids = model_data_obj.search(cr, uid, [
662 ('model', '=', model),
663 ('res_id', '=', res_id),
665 if not model_data_ids:
666 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
668 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
669 for dummy, val in field_def.selection:
670 push_translation(module, 'selection', name, 0, encode(val))
672 elif model=='ir.actions.report.xml':
673 name = encode(obj.report_name)
676 fname = obj.report_rml
677 parse_func = trans_parse_rml
678 report_type = "report"
680 fname = obj.report_xsl
681 parse_func = trans_parse_xsl
683 if fname and obj.report_type in ('pdf', 'xsl'):
685 d = etree.parse(tools.file_open(fname))
686 for t in parse_func(d.iter()):
687 push_translation(module, report_type, name, 0, t)
688 except (IOError, etree.XMLSyntaxError):
689 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
691 for field_name,field_def in obj._table._columns.items():
692 if field_def.translate:
693 name = model + "," + field_name
695 trad = getattr(obj, field_name) or ''
698 push_translation(module, 'model', name, xml_name, encode(trad))
700 # End of data for ir.model.data query results
702 cr.execute(query_models, query_param)
704 def push_constraint_msg(module, term_type, model, msg):
705 # Check presence of __call__ directly instead of using
706 # callable() because it will be deprecated as of Python 3.0
707 if not hasattr(msg, '__call__'):
708 push_translation(module, term_type, model, 0, encode(msg))
710 for (model_id, model, module) in cr.fetchall():
711 module = encode(module)
712 model = encode(model)
714 model_obj = pool.get(model)
717 logging.getLogger("i18n").error("Unable to find object %r", model)
720 for constraint in getattr(model_obj, '_constraints', []):
721 push_constraint_msg(module, 'constraint', model, constraint[1])
723 for constraint in getattr(model_obj, '_sql_constraints', []):
724 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
726 # parse source code for _() calls
727 def get_module_from_path(path, mod_paths=None):
729 # First, construct a list of possible paths
730 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
731 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
734 mod_paths.append(adp)
735 if not os.path.isabs(adp):
736 mod_paths.append(adp)
737 elif adp.startswith(def_path):
738 mod_paths.append(adp[len(def_path)+1:])
740 if path.startswith(mp) and (os.path.dirname(path) != mp):
741 path = path[len(mp)+1:]
742 return path.split(os.path.sep)[0]
743 return 'base' # files that are not in a module are considered as being in 'base' module
745 modobj = pool.get('ir.module.module')
746 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
747 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
749 root_path = os.path.join(tools.config['root_path'], 'addons')
751 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
752 if root_path in apaths:
755 path_list = [root_path,] + apaths
757 logger.debug("Scanning modules at paths: ", path_list)
760 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
761 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
762 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
763 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
765 def export_code_terms_from_file(fname, path, root, terms_type):
766 fabsolutepath = join(root, fname)
767 frelativepath = fabsolutepath[len(path):]
768 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
769 is_mod_installed = module in installed_modules
770 if (('all' in modules) or (module in modules)) and is_mod_installed:
771 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
772 code_string = tools.file_open(fabsolutepath, subdir='').read()
773 if module in installed_modules:
774 frelativepath = str("addons" + frelativepath)
775 ite = re_dquotes.finditer(code_string)
778 if src.startswith('""'):
779 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
782 src = join_dquotes.sub(r'\1', src)
783 # now, since we did a binary read of a python source file, we
784 # have to expand pythonic escapes like the interpreter does.
785 src = src.decode('string_escape')
786 push_translation(module, terms_type, frelativepath, 0, encode(src))
787 ite = re_quotes.finditer(code_string)
790 if src.startswith("''"):
791 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
794 src = join_quotes.sub(r'\1', src)
795 src = src.decode('string_escape')
796 push_translation(module, terms_type, frelativepath, 0, encode(src))
798 for path in path_list:
799 logger.debug("Scanning files of modules at %s", path)
800 for root, dummy, files in tools.osutil.walksymlinks(path):
801 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
802 export_code_terms_from_file(fname, path, root, 'code')
803 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
804 export_code_terms_from_file(fname, path, root, 'report')
807 out = [["module","type","name","res_id","src","value"]] # header
809 # translate strings marked as to be translated
810 for module, source, name, id, type in _to_translate:
811 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
812 out.append([module, type, name, id, source, encode(trans) or ''])
817 def trans_load(db_name, filename, lang, strict=False, verbose=True, context=None):
818 logger = logging.getLogger('i18n')
820 fileobj = open(filename,'r')
821 logger.info("loading %s", filename)
822 fileformat = os.path.splitext(filename)[-1][1:].lower()
823 r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
828 logger.error("couldn't read translation file %s", filename)
831 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context=None):
832 logger = logging.getLogger('i18n')
834 logger.info('loading translation file for language %s', lang)
837 pool = pooler.get_pool(db_name)
838 lang_obj = pool.get('res.lang')
839 trans_obj = pool.get('ir.translation')
840 model_data_obj = pool.get('ir.model.data')
841 iso_lang = tools.get_iso_codes(lang)
844 cr = pooler.get_db(db_name).cursor()
845 ids = lang_obj.search(cr, uid, [('code','=', lang)])
848 # lets create the language with locale information
850 for ln in get_locales(lang):
852 locale.setlocale(locale.LC_ALL, str(ln))
858 lc = locale.getdefaultlocale()[0]
859 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
860 logger.warning(msg, lang, lc)
863 lang_name = tools.get_languages().get(lang, lang)
872 'iso_code': iso_lang,
875 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
876 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
877 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
878 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
882 lang_obj.create(cr, uid, lang_info)
887 # now, the serious things: we read the language file
889 if fileformat == 'csv':
890 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
891 # read the first line of the file (it contains columns titles)
895 elif fileformat == 'po':
896 reader = TinyPoFile(fileobj)
897 f = ['type', 'name', 'res_id', 'src', 'value']
899 logger.error('Bad file format: %s', fileformat)
900 raise Exception(_('Bad file format'))
902 # read the rest of the file
906 # skip empty rows and rows where the translation field (=last fiefd) is empty
907 #if (not row) or (not row[-1]):
910 # dictionary which holds values for this line of the csv file
911 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
912 # 'src': ..., 'value': ...}
914 for i in range(len(f)):
915 if f[i] in ('module',):
920 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
922 model_data_ids = model_data_obj.search(cr, uid, [
923 ('model', '=', dic['name'].split(',')[0]),
924 ('module', '=', dic['res_id'].split('.', 1)[0]),
925 ('name', '=', dic['res_id'].split('.', 1)[1]),
928 dic['res_id'] = model_data_obj.browse(cr, uid,
929 model_data_ids[0]).res_id
931 dic['res_id'] = False
933 if dic['type'] == 'model' and not strict:
934 (model, field) = dic['name'].split(',')
936 # get the ids of the resources of this model which share
938 obj = pool.get(model)
940 if field not in obj.fields_get_keys(cr, uid):
942 ids = obj.search(cr, uid, [(field, '=', dic['src'])])
944 # if the resource id (res_id) is in that list, use it,
945 # otherwise use the whole list
948 ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
951 ids = trans_obj.search(cr, uid, [
953 ('type', '=', dic['type']),
954 ('name', '=', dic['name']),
955 ('src', '=', dic['src']),
956 ('res_id', '=', dic['res_id'])
959 if context.get('overwrite', False):
960 trans_obj.write(cr, uid, ids, {'value': dic['value']})
962 trans_obj.create(cr, uid, dic)
964 ids = trans_obj.search(cr, uid, [
966 ('type', '=', dic['type']),
967 ('name', '=', dic['name']),
968 ('src', '=', dic['src'])
971 if context.get('overwrite', False):
972 trans_obj.write(cr, uid, ids, {'value': dic['value']})
974 trans_obj.create(cr, uid, dic)
978 logger.info("translation file loaded succesfully")
980 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
981 logger.exception("couldn't read translation file %s", filename)
983 def get_locales(lang=None):
985 lang = locale.getdefaultlocale()[0]
988 lang = _LOCALE2WIN32.get(lang, lang)
991 ln = locale._build_localename((lang, enc))
993 nln = locale.normalize(ln)
997 for x in process('utf8'): yield x
999 prefenc = locale.getpreferredencoding()
1001 for x in process(prefenc): yield x
1005 'iso-8859-1': 'iso8859-15',
1007 }.get(prefenc.lower())
1009 for x in process(prefenc): yield x
1016 # locale.resetlocale is bugged with some locales.
1017 for ln in get_locales():
1019 return locale.setlocale(locale.LC_ALL, ln)
1020 except locale.Error:
1023 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: