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 return pooler.get_db_only(dbname)
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)
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)
472 def trans_parse_xsl(de):
476 for m in [j for j in n if j.text]:
477 l = m.text.strip().replace('\n',' ')
479 res.append(l.encode("utf8"))
480 res.extend(trans_parse_xsl(n))
483 def trans_parse_rml(de):
486 for m in [j for j in n if j.text]:
487 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
488 for s in string_list:
490 res.append(s.encode("utf8"))
491 res.extend(trans_parse_rml(n))
494 def trans_parse_view(de):
496 if de.tag == 'attribute' and de.get("name") == 'string':
498 res.append(de.text.encode("utf8"))
500 res.append(de.get('string').encode("utf8"))
502 res.append(de.get('sum').encode("utf8"))
503 if de.get("confirm"):
504 res.append(de.get('confirm').encode("utf8"))
506 res.extend(trans_parse_view(n))
509 # tests whether an object is in a list of modules
510 def in_modules(object_name, modules):
519 module = object_name.split('.')[0]
520 module = module_dict.get(module, module)
521 return module in modules
523 def trans_generate(lang, modules, dbname=None):
524 logger = logging.getLogger('i18n')
526 dbname=tools.config['db_name']
530 pool = pooler.get_pool(dbname)
531 trans_obj = pool.get('ir.translation')
532 model_data_obj = pool.get('ir.model.data')
533 cr = pooler.get_db(dbname).cursor()
535 l = pool.obj_pool.items()
538 query = 'SELECT name, model, res_id, module' \
539 ' FROM ir_model_data'
541 query_models = """SELECT m.id, m.model, imd.module
542 FROM ir_model AS m, ir_model_data AS imd
543 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
545 if 'all_installed' in modules:
546 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
547 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
549 if 'all' not in modules:
550 query += ' WHERE module IN %s'
551 query_models += ' AND imd.module in %s'
552 query_param = (tuple(modules),)
553 query += ' ORDER BY module, model, name'
554 query_models += ' ORDER BY module, model'
556 cr.execute(query, query_param)
559 def push_translation(module, type, name, id, source):
560 tuple = (module, source, name, id, type)
561 if source and tuple not in _to_translate:
562 _to_translate.append(tuple)
565 if isinstance(s, unicode):
566 return s.encode('utf8')
569 for (xml_name,model,res_id,module) in cr.fetchall():
570 module = encode(module)
571 model = encode(model)
572 xml_name = "%s.%s" % (module, encode(xml_name))
574 if not pool.get(model):
575 logger.error("Unable to find object %r", model)
578 exists = pool.get(model).exists(cr, uid, res_id)
580 logger.warning("Unable to find object %r with id %d", model, res_id)
582 obj = pool.get(model).browse(cr, uid, res_id)
584 if model=='ir.ui.view':
585 d = etree.XML(encode(obj.arch))
586 for t in trans_parse_view(d):
587 push_translation(module, 'view', encode(obj.model), 0, t)
588 elif model=='ir.actions.wizard':
589 service_name = 'wizard.'+encode(obj.wiz_name)
590 if netsvc.Service._services.get(service_name):
591 obj2 = netsvc.Service._services[service_name]
592 for state_name, state_def in obj2.states.iteritems():
593 if 'result' in state_def:
594 result = state_def['result']
595 if result['type'] != 'form':
597 name = "%s,%s" % (encode(obj.wiz_name), state_name)
600 'string': ('wizard_field', lambda s: [encode(s)]),
601 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
602 'help': ('help', lambda s: [encode(s)]),
606 if not result.has_key('fields'):
607 logger.warning("res has no fields: %r", result)
609 for field_name, field_def in result['fields'].iteritems():
610 res_name = name + ',' + field_name
612 for fn in def_params:
614 transtype, modifier = def_params[fn]
615 for val in modifier(field_def[fn]):
616 push_translation(module, transtype, res_name, 0, val)
619 arch = result['arch']
620 if arch and not isinstance(arch, UpdateableStr):
622 for t in trans_parse_view(d):
623 push_translation(module, 'wizard_view', name, 0, t)
625 # export button labels
626 for but_args in result['state']:
627 button_name = but_args[0]
628 button_label = but_args[1]
629 res_name = name + ',' + button_name
630 push_translation(module, 'wizard_button', res_name, 0, button_label)
632 elif model=='ir.model.fields':
634 field_name = encode(obj.name)
635 except AttributeError, exc:
636 logger.error("name error in %s: %s", xml_name, str(exc))
638 objmodel = pool.get(obj.model)
639 if not objmodel or not field_name in objmodel._columns:
641 field_def = objmodel._columns[field_name]
643 name = "%s,%s" % (encode(obj.model), field_name)
644 push_translation(module, 'field', name, 0, encode(field_def.string))
647 push_translation(module, 'help', name, 0, encode(field_def.help))
649 if field_def.translate:
650 ids = objmodel.search(cr, uid, [])
651 obj_values = objmodel.read(cr, uid, ids, [field_name])
652 for obj_value in obj_values:
653 res_id = obj_value['id']
654 if obj.name in ('ir.model', 'ir.ui.menu'):
656 model_data_ids = model_data_obj.search(cr, uid, [
657 ('model', '=', model),
658 ('res_id', '=', res_id),
660 if not model_data_ids:
661 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
663 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
664 for dummy, val in field_def.selection:
665 push_translation(module, 'selection', name, 0, encode(val))
667 elif model=='ir.actions.report.xml':
668 name = encode(obj.report_name)
671 fname = obj.report_rml
672 parse_func = trans_parse_rml
673 report_type = "report"
675 fname = obj.report_xsl
676 parse_func = trans_parse_xsl
678 if fname and obj.report_type in ('pdf', 'xsl'):
680 d = etree.parse(tools.file_open(fname))
681 for t in parse_func(d.iter()):
682 push_translation(module, report_type, name, 0, t)
683 except (IOError, etree.XMLSyntaxError):
684 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
686 for field_name,field_def in obj._table._columns.items():
687 if field_def.translate:
688 name = model + "," + field_name
690 trad = getattr(obj, field_name) or ''
693 push_translation(module, 'model', name, xml_name, encode(trad))
695 # End of data for ir.model.data query results
697 cr.execute(query_models, query_param)
699 def push_constraint_msg(module, term_type, model, msg):
700 # Check presence of __call__ directly instead of using
701 # callable() because it will be deprecated as of Python 3.0
702 if not hasattr(msg, '__call__'):
703 push_translation(module, term_type, model, 0, encode(msg))
705 for (model_id, model, module) in cr.fetchall():
706 module = encode(module)
707 model = encode(model)
709 model_obj = pool.get(model)
712 logging.getLogger("i18n").error("Unable to find object %r", model)
715 for constraint in getattr(model_obj, '_constraints', []):
716 push_constraint_msg(module, 'constraint', model, constraint[1])
718 for constraint in getattr(model_obj, '_sql_constraints', []):
719 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
721 # parse source code for _() calls
722 def get_module_from_path(path, mod_paths=None):
724 # First, construct a list of possible paths
725 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
726 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
729 mod_paths.append(adp)
730 if not os.path.isabs(adp):
731 mod_paths.append(adp)
732 elif adp.startswith(def_path):
733 mod_paths.append(adp[len(def_path)+1:])
735 if path.startswith(mp) and (os.path.dirname(path) != mp):
736 path = path[len(mp)+1:]
737 return path.split(os.path.sep)[0]
738 return 'base' # files that are not in a module are considered as being in 'base' module
740 modobj = pool.get('ir.module.module')
741 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
742 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
744 root_path = os.path.join(tools.config['root_path'], 'addons')
746 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
747 if root_path in apaths:
750 path_list = [root_path,] + apaths
752 logger.debug("Scanning modules at paths: ", path_list)
755 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
756 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
757 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
758 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
760 def export_code_terms_from_file(fname, path, root, terms_type):
761 fabsolutepath = join(root, fname)
762 frelativepath = fabsolutepath[len(path):]
763 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
764 is_mod_installed = module in installed_modules
765 if (('all' in modules) or (module in modules)) and is_mod_installed:
766 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
767 code_string = tools.file_open(fabsolutepath, subdir='').read()
768 if module in installed_modules:
769 frelativepath = str("addons" + frelativepath)
770 ite = re_dquotes.finditer(code_string)
773 if src.startswith('""'):
774 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
777 src = join_dquotes.sub(r'\1', src)
778 # now, since we did a binary read of a python source file, we
779 # have to expand pythonic escapes like the interpreter does.
780 src = src.decode('string_escape')
781 push_translation(module, terms_type, frelativepath, 0, encode(src))
782 ite = re_quotes.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_quotes.sub(r'\1', src)
790 src = src.decode('string_escape')
791 push_translation(module, terms_type, frelativepath, 0, encode(src))
793 for path in path_list:
794 logger.debug("Scanning files of modules at %s", path)
795 for root, dummy, files in tools.osutil.walksymlinks(path):
796 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
797 export_code_terms_from_file(fname, path, root, 'code')
798 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
799 export_code_terms_from_file(fname, path, root, 'report')
802 out = [["module","type","name","res_id","src","value"]] # header
804 # translate strings marked as to be translated
805 for module, source, name, id, type in _to_translate:
806 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
807 out.append([module, type, name, id, source, encode(trans) or ''])
812 def trans_load(db_name, filename, lang, verbose=True, context=None):
813 logger = logging.getLogger('i18n')
815 fileobj = open(filename,'r')
816 logger.info("loading %s", filename)
817 fileformat = os.path.splitext(filename)[-1][1:].lower()
818 r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
823 logger.error("couldn't read translation file %s", filename)
826 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
827 logger = logging.getLogger('i18n')
829 logger.info('loading translation file for language %s', lang)
832 pool = pooler.get_pool(db_name)
833 lang_obj = pool.get('res.lang')
834 trans_obj = pool.get('ir.translation')
835 model_data_obj = pool.get('ir.model.data')
836 iso_lang = tools.get_iso_codes(lang)
839 cr = pooler.get_db(db_name).cursor()
840 ids = lang_obj.search(cr, uid, [('code','=', lang)])
843 # lets create the language with locale information
845 for ln in get_locales(lang):
847 locale.setlocale(locale.LC_ALL, str(ln))
853 lc = locale.getdefaultlocale()[0]
854 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
855 logger.warning(msg, lang, lc)
858 lang_name = tools.get_languages().get(lang, lang)
867 'iso_code': iso_lang,
870 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
871 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
872 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
873 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
877 lang_obj.create(cr, uid, lang_info)
882 # now, the serious things: we read the language file
884 if fileformat == 'csv':
885 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
886 # read the first line of the file (it contains columns titles)
890 elif fileformat == 'po':
891 reader = TinyPoFile(fileobj)
892 f = ['type', 'name', 'res_id', 'src', 'value']
894 logger.error('Bad file format: %s', fileformat)
895 raise Exception(_('Bad file format'))
897 # read the rest of the file
901 # skip empty rows and rows where the translation field (=last fiefd) is empty
902 #if (not row) or (not row[-1]):
905 # dictionary which holds values for this line of the csv file
906 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
907 # 'src': ..., 'value': ...}
909 for i in range(len(f)):
910 if f[i] in ('module',):
915 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
917 model_data_ids = model_data_obj.search(cr, uid, [
918 ('model', '=', dic['name'].split(',')[0]),
919 ('module', '=', dic['res_id'].split('.', 1)[0]),
920 ('name', '=', dic['res_id'].split('.', 1)[1]),
923 dic['res_id'] = model_data_obj.browse(cr, uid,
924 model_data_ids[0]).res_id
926 dic['res_id'] = False
930 ('type', '=', dic['type']),
931 ('name', '=', dic['name']),
932 ('src', '=', dic['src']),
934 if dic['type'] == 'model':
935 args.append(('res_id', '=', dic['res_id']))
936 ids = trans_obj.search(cr, uid, args)
938 if context.get('overwrite'):
939 trans_obj.write(cr, uid, ids, {'value': dic['value']})
941 trans_obj.create(cr, uid, dic)
945 logger.info("translation file loaded succesfully")
947 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
948 logger.exception("couldn't read translation file %s", filename)
950 def get_locales(lang=None):
952 lang = locale.getdefaultlocale()[0]
955 lang = _LOCALE2WIN32.get(lang, lang)
958 ln = locale._build_localename((lang, enc))
960 nln = locale.normalize(ln)
964 for x in process('utf8'): yield x
966 prefenc = locale.getpreferredencoding()
968 for x in process(prefenc): yield x
972 'iso-8859-1': 'iso8859-15',
974 }.get(prefenc.lower())
976 for x in process(prefenc): yield x
983 # locale.resetlocale is bugged with some locales.
984 for ln in get_locales():
986 return locale.setlocale(locale.LC_ALL, ln)
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: