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):
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_only(dbname)
165 def _get_cr(self, frame):
167 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
169 s = frame.f_locals.get('self', {})
170 cr = getattr(s, 'cr', None)
178 def _get_lang(self, frame):
180 ctx = frame.f_locals.get('context')
182 kwargs = frame.f_locals.get('kwargs')
184 args = frame.f_locals.get('args')
185 if args and isinstance(args, (list, tuple)) \
186 and isinstance(args[-1], dict):
188 elif isinstance(kwargs, dict):
189 ctx = kwargs.get('context')
191 lang = ctx.get('lang')
193 s = frame.f_locals.get('self', {})
194 c = getattr(s, 'localcontext', None)
199 def __call__(self, source):
204 frame = inspect.currentframe()
210 lang = self._get_lang(frame)
212 cr, is_new_cr = self._get_cr(frame)
214 # Try to use ir.translation to benefit from global cache if possible
215 pool = pooler.get_pool(cr.dbname)
216 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
218 logger.debug('no context cursor detected, skipping translation for "%r"', source)
220 logger.debug('no translation language detected, skipping translation for "%r" ', source)
222 logger.debug('translation went wrong for "%r", skipped', source)
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)
473 def trans_parse_xsl(de):
477 for m in [j for j in n if j.text]:
478 l = m.text.strip().replace('\n',' ')
480 res.append(l.encode("utf8"))
481 res.extend(trans_parse_xsl(n))
484 def trans_parse_rml(de):
487 for m in [j for j in n if j.text]:
488 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
489 for s in string_list:
491 res.append(s.encode("utf8"))
492 res.extend(trans_parse_rml(n))
495 def trans_parse_view(de):
497 if de.tag == 'attribute' and de.get("name") == 'string':
499 res.append(de.text.encode("utf8"))
501 res.append(de.get('string').encode("utf8"))
503 res.append(de.get('sum').encode("utf8"))
504 if de.get("confirm"):
505 res.append(de.get('confirm').encode("utf8"))
507 res.extend(trans_parse_view(n))
510 # tests whether an object is in a list of modules
511 def in_modules(object_name, modules):
520 module = object_name.split('.')[0]
521 module = module_dict.get(module, module)
522 return module in modules
524 def trans_generate(lang, modules, dbname=None):
525 logger = logging.getLogger('i18n')
527 dbname=tools.config['db_name']
531 pool = pooler.get_pool(dbname)
532 trans_obj = pool.get('ir.translation')
533 model_data_obj = pool.get('ir.model.data')
534 cr = pooler.get_db(dbname).cursor()
536 l = pool.obj_pool.items()
539 query = 'SELECT name, model, res_id, module' \
540 ' FROM ir_model_data'
542 query_models = """SELECT m.id, m.model, imd.module
543 FROM ir_model AS m, ir_model_data AS imd
544 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
546 if 'all_installed' in modules:
547 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
548 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
550 if 'all' not in modules:
551 query += ' WHERE module IN %s'
552 query_models += ' AND imd.module in %s'
553 query_param = (tuple(modules),)
554 query += ' ORDER BY module, model, name'
555 query_models += ' ORDER BY module, model'
557 cr.execute(query, query_param)
560 def push_translation(module, type, name, id, source):
561 tuple = (module, source, name, id, type)
562 if source and tuple not in _to_translate:
563 _to_translate.append(tuple)
566 if isinstance(s, unicode):
567 return s.encode('utf8')
570 for (xml_name,model,res_id,module) in cr.fetchall():
571 module = encode(module)
572 model = encode(model)
573 xml_name = "%s.%s" % (module, encode(xml_name))
575 if not pool.get(model):
576 logger.error("Unable to find object %r", model)
579 exists = pool.get(model).exists(cr, uid, res_id)
581 logger.warning("Unable to find object %r with id %d", model, res_id)
583 obj = pool.get(model).browse(cr, uid, res_id)
585 if model=='ir.ui.view':
586 d = etree.XML(encode(obj.arch))
587 for t in trans_parse_view(d):
588 push_translation(module, 'view', encode(obj.model), 0, t)
589 elif model=='ir.actions.wizard':
590 service_name = 'wizard.'+encode(obj.wiz_name)
591 if netsvc.Service._services.get(service_name):
592 obj2 = netsvc.Service._services[service_name]
593 for state_name, state_def in obj2.states.iteritems():
594 if 'result' in state_def:
595 result = state_def['result']
596 if result['type'] != 'form':
598 name = "%s,%s" % (encode(obj.wiz_name), state_name)
601 'string': ('wizard_field', lambda s: [encode(s)]),
602 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
603 'help': ('help', lambda s: [encode(s)]),
607 if not result.has_key('fields'):
608 logger.warning("res has no fields: %r", result)
610 for field_name, field_def in result['fields'].iteritems():
611 res_name = name + ',' + field_name
613 for fn in def_params:
615 transtype, modifier = def_params[fn]
616 for val in modifier(field_def[fn]):
617 push_translation(module, transtype, res_name, 0, val)
620 arch = result['arch']
621 if arch and not isinstance(arch, UpdateableStr):
623 for t in trans_parse_view(d):
624 push_translation(module, 'wizard_view', name, 0, t)
626 # export button labels
627 for but_args in result['state']:
628 button_name = but_args[0]
629 button_label = but_args[1]
630 res_name = name + ',' + button_name
631 push_translation(module, 'wizard_button', res_name, 0, button_label)
633 elif model=='ir.model.fields':
635 field_name = encode(obj.name)
636 except AttributeError, exc:
637 logger.error("name error in %s: %s", xml_name, str(exc))
639 objmodel = pool.get(obj.model)
640 if not objmodel or not field_name in objmodel._columns:
642 field_def = objmodel._columns[field_name]
644 name = "%s,%s" % (encode(obj.model), field_name)
645 push_translation(module, 'field', name, 0, encode(field_def.string))
648 push_translation(module, 'help', name, 0, encode(field_def.help))
650 if field_def.translate:
651 ids = objmodel.search(cr, uid, [])
652 obj_values = objmodel.read(cr, uid, ids, [field_name])
653 for obj_value in obj_values:
654 res_id = obj_value['id']
655 if obj.name in ('ir.model', 'ir.ui.menu'):
657 model_data_ids = model_data_obj.search(cr, uid, [
658 ('model', '=', model),
659 ('res_id', '=', res_id),
661 if not model_data_ids:
662 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
664 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
665 for dummy, val in field_def.selection:
666 push_translation(module, 'selection', name, 0, encode(val))
668 elif model=='ir.actions.report.xml':
669 name = encode(obj.report_name)
672 fname = obj.report_rml
673 parse_func = trans_parse_rml
674 report_type = "report"
676 fname = obj.report_xsl
677 parse_func = trans_parse_xsl
679 if fname and obj.report_type in ('pdf', 'xsl'):
681 d = etree.parse(tools.file_open(fname))
682 for t in parse_func(d.iter()):
683 push_translation(module, report_type, name, 0, t)
684 except (IOError, etree.XMLSyntaxError):
685 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
687 for field_name,field_def in obj._table._columns.items():
688 if field_def.translate:
689 name = model + "," + field_name
691 trad = getattr(obj, field_name) or ''
694 push_translation(module, 'model', name, xml_name, encode(trad))
696 # End of data for ir.model.data query results
698 cr.execute(query_models, query_param)
700 def push_constraint_msg(module, term_type, model, msg):
701 # Check presence of __call__ directly instead of using
702 # callable() because it will be deprecated as of Python 3.0
703 if not hasattr(msg, '__call__'):
704 push_translation(module, term_type, model, 0, encode(msg))
706 for (model_id, model, module) in cr.fetchall():
707 module = encode(module)
708 model = encode(model)
710 model_obj = pool.get(model)
713 logging.getLogger("i18n").error("Unable to find object %r", model)
716 for constraint in getattr(model_obj, '_constraints', []):
717 push_constraint_msg(module, 'constraint', model, constraint[1])
719 for constraint in getattr(model_obj, '_sql_constraints', []):
720 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
722 # parse source code for _() calls
723 def get_module_from_path(path, mod_paths=None):
725 # First, construct a list of possible paths
726 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
727 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
730 mod_paths.append(adp)
731 if not os.path.isabs(adp):
732 mod_paths.append(adp)
733 elif adp.startswith(def_path):
734 mod_paths.append(adp[len(def_path)+1:])
736 if path.startswith(mp) and (os.path.dirname(path) != mp):
737 path = path[len(mp)+1:]
738 return path.split(os.path.sep)[0]
739 return 'base' # files that are not in a module are considered as being in 'base' module
741 modobj = pool.get('ir.module.module')
742 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
743 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
745 root_path = os.path.join(tools.config['root_path'], 'addons')
747 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
748 if root_path in apaths:
751 path_list = [root_path,] + apaths
753 logger.debug("Scanning modules at paths: ", path_list)
756 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
757 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
758 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
759 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
761 def export_code_terms_from_file(fname, path, root, terms_type):
762 fabsolutepath = join(root, fname)
763 frelativepath = fabsolutepath[len(path):]
764 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
765 is_mod_installed = module in installed_modules
766 if (('all' in modules) or (module in modules)) and is_mod_installed:
767 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
768 code_string = tools.file_open(fabsolutepath, subdir='').read()
769 if module in installed_modules:
770 frelativepath = str("addons" + frelativepath)
771 ite = re_dquotes.finditer(code_string)
774 if src.startswith('""'):
775 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
778 src = join_dquotes.sub(r'\1', src)
779 # now, since we did a binary read of a python source file, we
780 # have to expand pythonic escapes like the interpreter does.
781 src = src.decode('string_escape')
782 push_translation(module, terms_type, frelativepath, 0, encode(src))
783 ite = re_quotes.finditer(code_string)
786 if src.startswith("''"):
787 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
790 src = join_quotes.sub(r'\1', src)
791 src = src.decode('string_escape')
792 push_translation(module, terms_type, frelativepath, 0, encode(src))
794 for path in path_list:
795 logger.debug("Scanning files of modules at %s", path)
796 for root, dummy, files in tools.osutil.walksymlinks(path):
797 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
798 export_code_terms_from_file(fname, path, root, 'code')
799 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
800 export_code_terms_from_file(fname, path, root, 'report')
803 out = [["module","type","name","res_id","src","value"]] # header
805 # translate strings marked as to be translated
806 for module, source, name, id, type in _to_translate:
807 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
808 out.append([module, type, name, id, source, encode(trans) or ''])
813 def trans_load(db_name, filename, lang, verbose=True, context=None):
814 logger = logging.getLogger('i18n')
816 fileobj = open(filename,'r')
817 logger.info("loading %s", filename)
818 fileformat = os.path.splitext(filename)[-1][1:].lower()
819 r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
824 logger.error("couldn't read translation file %s", filename)
827 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
828 logger = logging.getLogger('i18n')
830 logger.info('loading translation file for language %s', lang)
833 pool = pooler.get_pool(db_name)
834 lang_obj = pool.get('res.lang')
835 trans_obj = pool.get('ir.translation')
836 model_data_obj = pool.get('ir.model.data')
837 iso_lang = tools.get_iso_codes(lang)
840 cr = pooler.get_db(db_name).cursor()
841 ids = lang_obj.search(cr, uid, [('code','=', lang)])
844 # lets create the language with locale information
846 for ln in get_locales(lang):
848 locale.setlocale(locale.LC_ALL, str(ln))
854 lc = locale.getdefaultlocale()[0]
855 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
856 logger.warning(msg, lang, lc)
859 lang_name = tools.get_languages().get(lang, lang)
868 'iso_code': iso_lang,
871 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
872 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
873 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
874 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
878 lang_obj.create(cr, uid, lang_info)
883 # now, the serious things: we read the language file
885 if fileformat == 'csv':
886 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
887 # read the first line of the file (it contains columns titles)
891 elif fileformat == 'po':
892 reader = TinyPoFile(fileobj)
893 f = ['type', 'name', 'res_id', 'src', 'value']
895 logger.error('Bad file format: %s', fileformat)
896 raise Exception(_('Bad file format'))
898 # read the rest of the file
902 # skip empty rows and rows where the translation field (=last fiefd) is empty
903 #if (not row) or (not row[-1]):
906 # dictionary which holds values for this line of the csv file
907 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
908 # 'src': ..., 'value': ...}
910 for i in range(len(f)):
911 if f[i] in ('module',):
916 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
918 model_data_ids = model_data_obj.search(cr, uid, [
919 ('model', '=', dic['name'].split(',')[0]),
920 ('module', '=', dic['res_id'].split('.', 1)[0]),
921 ('name', '=', dic['res_id'].split('.', 1)[1]),
924 dic['res_id'] = model_data_obj.browse(cr, uid,
925 model_data_ids[0]).res_id
927 dic['res_id'] = False
931 ('type', '=', dic['type']),
932 ('name', '=', dic['name']),
933 ('src', '=', dic['src']),
935 if dic['type'] == 'model':
936 args.append(('res_id', '=', dic['res_id']))
937 ids = trans_obj.search(cr, uid, args)
939 if context.get('overwrite'):
940 trans_obj.write(cr, uid, ids, {'value': dic['value']})
942 trans_obj.create(cr, uid, dic)
946 logger.info("translation file loaded succesfully")
948 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
949 logger.exception("couldn't read translation file %s", filename)
951 def get_locales(lang=None):
953 lang = locale.getdefaultlocale()[0]
956 lang = _LOCALE2WIN32.get(lang, lang)
959 ln = locale._build_localename((lang, enc))
961 nln = locale.normalize(ln)
965 for x in process('utf8'): yield x
967 prefenc = locale.getpreferredencoding()
969 for x in process(prefenc): yield x
973 'iso-8859-1': 'iso8859-15',
975 }.get(prefenc.lower())
977 for x in process(prefenc): yield x
984 # locale.resetlocale is bugged with some locales.
985 for ln in get_locales():
987 return locale.setlocale(locale.LC_ALL, ln)
991 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: