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 ##############################################################################
33 from os.path import join
36 from datetime import datetime
37 from lxml import etree
41 from tools.misc import UpdateableStr
44 'af_ZA': 'Afrikaans_South Africa',
45 'sq_AL': 'Albanian_Albania',
46 'ar_SA': 'Arabic_Saudi Arabia',
47 'eu_ES': 'Basque_Spain',
48 'be_BY': 'Belarusian_Belarus',
49 'bs_BA': 'Serbian (Latin)',
50 'bg_BG': 'Bulgarian_Bulgaria',
51 'ca_ES': 'Catalan_Spain',
52 'hr_HR': 'Croatian_Croatia',
53 'zh_CN': 'Chinese_China',
54 'zh_TW': 'Chinese_Taiwan',
55 'cs_CZ': 'Czech_Czech Republic',
56 'da_DK': 'Danish_Denmark',
57 'nl_NL': 'Dutch_Netherlands',
58 'et_EE': 'Estonian_Estonia',
59 'fa_IR': 'Farsi_Iran',
60 'ph_PH': 'Filipino_Philippines',
61 'fi_FI': 'Finnish_Finland',
62 'fr_FR': 'French_France',
63 'fr_BE': 'French_France',
64 'fr_CH': 'French_France',
65 'fr_CA': 'French_France',
66 'ga': 'Scottish Gaelic',
67 'gl_ES': 'Galician_Spain',
68 'ka_GE': 'Georgian_Georgia',
69 'de_DE': 'German_Germany',
70 'el_GR': 'Greek_Greece',
71 'gu': 'Gujarati_India',
72 'he_IL': 'Hebrew_Israel',
74 'hu': 'Hungarian_Hungary',
75 'is_IS': 'Icelandic_Iceland',
76 'id_ID': 'Indonesian_indonesia',
77 'it_IT': 'Italian_Italy',
78 'ja_JP': 'Japanese_Japan',
81 'ko_KR': 'Korean_Korea',
83 'lt_LT': 'Lithuanian_Lithuania',
84 'lat': 'Latvian_Latvia',
85 'ml_IN': 'Malayalam_India',
86 'id_ID': 'Indonesian_indonesia',
88 'mn': 'Cyrillic_Mongolian',
89 'no_NO': 'Norwegian_Norway',
90 'nn_NO': 'Norwegian-Nynorsk_Norway',
91 'pl': 'Polish_Poland',
92 'pt_PT': 'Portuguese_Portugal',
93 'pt_BR': 'Portuguese_Brazil',
94 'ro_RO': 'Romanian_Romania',
95 'ru_RU': 'Russian_Russia',
97 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
98 'sk_SK': 'Slovak_Slovakia',
99 'sl_SI': 'Slovenian_Slovenia',
100 #should find more specific locales for spanish countries,
101 #but better than nothing
102 'es_AR': 'Spanish_Spain',
103 'es_BO': 'Spanish_Spain',
104 'es_CL': 'Spanish_Spain',
105 'es_CO': 'Spanish_Spain',
106 'es_CR': 'Spanish_Spain',
107 'es_DO': 'Spanish_Spain',
108 'es_EC': 'Spanish_Spain',
109 'es_ES': 'Spanish_Spain',
110 'es_GT': 'Spanish_Spain',
111 'es_HN': 'Spanish_Spain',
112 'es_MX': 'Spanish_Spain',
113 'es_NI': 'Spanish_Spain',
114 'es_PA': 'Spanish_Spain',
115 'es_PE': 'Spanish_Spain',
116 'es_PR': 'Spanish_Spain',
117 'es_PY': 'Spanish_Spain',
118 'es_SV': 'Spanish_Spain',
119 'es_UY': 'Spanish_Spain',
120 'es_VE': 'Spanish_Spain',
121 'sv_SE': 'Swedish_Sweden',
122 'ta_IN': 'English_Australia',
123 'th_TH': 'Thai_Thailand',
125 'tr_TR': 'Turkish_Turkey',
126 'uk_UA': 'Ukrainian_Ukraine',
127 'vi_VN': 'Vietnamese_Viet Nam',
128 'tlh_TLH': 'Klingon',
133 class UNIX_LINE_TERMINATOR(csv.excel):
134 lineterminator = '\n'
136 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
139 # Warning: better use self.pool.get('ir.translation')._get_source if you can
141 def translate(cr, name, source_type, lang, source=None):
143 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))
145 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
147 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
148 res_trans = cr.fetchone()
149 res = res_trans and res_trans[0] or False
152 logger = logging.getLogger('translate')
154 class GettextAlias(object):
156 def _get_cr(self, frame):
158 cr = frame.f_locals.get('cr')
160 s = frame.f_locals.get('self', {})
161 cr = getattr(s, 'cr', False)
163 if frame.f_globals.get('pooler', False):
164 # TODO: we should probably get rid of the 'is_new_cr' case: no cr in locals -> no translation for you
165 dbs = frame.f_globals['pooler'].pool_dic.keys()
167 cr = pooler.get_db(dbs[0]).cursor()
171 def _get_lang(self, frame):
172 lang = frame.f_locals.get('context', {}).get('lang', False)
174 args = frame.f_locals.get('args', False)
176 lang = args[-1].get('lang', False)
178 s = frame.f_locals.get('self', {})
179 c = getattr(s, 'localcontext', {})
180 lang = c.get('lang', False)
183 def __call__(self, source):
187 frame = inspect.stack()[1][0]
188 cr, is_new_cr = self._get_cr(frame)
189 lang = self._get_lang(frame)
191 cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
192 res_trans = cr.fetchone()
193 res = res_trans and res_trans[0] or source
195 logger.debug('translation went wrong for string %s', repr(source))
205 """Returns quoted PO term string, with special PO characters escaped"""
206 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines"
207 return '"%s"' % s.replace('\\','\\\\') \
208 .replace('"','\\"') \
209 .replace('\n', '\\n"\n"')
211 re_escaped_char = re.compile(r"(\\.)")
212 re_escaped_replacements = {'n': '\n', }
214 def _sub_replacement(match_obj):
215 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
218 """Returns unquoted PO term string, with special PO characters unescaped"""
219 return re_escaped_char.sub(_sub_replacement, str[1:-1])
221 # class to handle po files
222 class TinyPoFile(object):
223 def __init__(self, buffer):
224 self.logger = logging.getLogger('i18n')
228 self.logger.warning(msg)
232 self.lines = self._get_lines()
233 self.lines_count = len(self.lines);
239 def _get_lines(self):
240 lines = self.buffer.readlines()
241 # remove the BOM (Byte Order Mark):
243 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
245 lines.append('') # ensure that the file ends with at least an empty line
249 return (self.lines_count - len(self.lines))
252 type = name = res_id = source = trad = None
255 type, name, res_id, source, trad = self.tnrs.pop(0)
261 if 0 == len(self.lines):
262 raise StopIteration()
263 line = self.lines.pop(0).strip()
264 while line.startswith('#'):
265 if line.startswith('#~ '):
267 if line.startswith('#:'):
268 if ' ' in line[2:].strip():
269 for lpart in line[2:].strip().split(' '):
270 tmp_tnrs.append(lpart.strip().split(':',2))
272 tmp_tnrs.append( line[2:].strip().split(':',2) )
273 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
275 line = self.lines.pop(0).strip()
277 # allow empty lines between comments and msgid
278 line = self.lines.pop(0).strip()
279 if line.startswith('#~ '):
280 while line.startswith('#~ ') or not line.strip():
281 if 0 == len(self.lines):
282 raise StopIteration()
283 line = self.lines.pop(0)
284 # This has been a deprecated entry, don't return anything
287 if not line.startswith('msgid'):
288 raise Exception("malformed file: bad line: %s" % line)
289 source = unquote(line[6:])
290 line = self.lines.pop(0).strip()
291 if not source and self.first:
292 # if the source is "" and it's the first msgid, it's the special
293 # msgstr with the informations about the traduction and the
294 # traductor; we skip it
297 line = self.lines.pop(0).strip()
300 while not line.startswith('msgstr'):
302 raise Exception('malformed file at %d'% self.cur_line())
303 source += unquote(line)
304 line = self.lines.pop(0).strip()
306 trad = unquote(line[7:])
307 line = self.lines.pop(0).strip()
309 trad += unquote(line)
310 line = self.lines.pop(0).strip()
312 if tmp_tnrs and not fuzzy:
313 type, name, res_id = tmp_tnrs.pop(0)
314 for t, n, r in tmp_tnrs:
315 self.tnrs.append((t, n, r, source, trad))
321 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
322 self.cur_line(), source[:30])
324 return type, name, res_id, source, trad
326 def write_infos(self, modules):
328 self.buffer.write("# Translation of %(project)s.\n" \
329 "# This file contains the translation of the following modules:\n" \
334 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
335 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
336 '''"POT-Creation-Date: %(now)s\\n"\n''' \
337 '''"PO-Revision-Date: %(now)s\\n"\n''' \
338 '''"Last-Translator: <>\\n"\n''' \
339 '''"Language-Team: \\n"\n''' \
340 '''"MIME-Version: 1.0\\n"\n''' \
341 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
342 '''"Content-Transfer-Encoding: \\n"\n''' \
343 '''"Plural-Forms: \\n"\n''' \
346 % { 'project': release.description,
347 'version': release.version,
348 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
349 'bugmail': release.support_email,
350 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
354 def write(self, modules, tnrs, source, trad):
356 plurial = len(modules) > 1 and 's' or ''
357 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
361 for typy, name, res_id in tnrs:
362 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
367 # only strings in python code are python formated
368 self.buffer.write("#, python-format\n")
370 if not isinstance(trad, unicode):
371 trad = unicode(trad, 'utf8')
372 if not isinstance(source, unicode):
373 source = unicode(source, 'utf8')
377 % (quote(source), quote(trad))
378 self.buffer.write(msg.encode('utf8'))
381 # Methods to export the translation file
383 def trans_export(lang, modules, buffer, format, dbname=None):
385 def _process(format, modules, rows, buffer, lang, newlang):
387 writer=csv.writer(buffer, 'UNIX')
392 writer = tools.TinyPoFile(buffer)
393 writer.write_infos(modules)
395 # we now group the translations by source. That means one translation per source.
397 for module, type, name, res_id, src, trad in rows:
398 row = grouped_rows.setdefault(src, {})
399 row.setdefault('modules', set()).add(module)
400 if ('translation' not in row) or (not row['translation']):
401 row['translation'] = trad
402 row.setdefault('tnrs', []).append((type, name, res_id))
404 for src, row in grouped_rows.items():
405 writer.write(row['modules'], row['tnrs'], src, row['translation'])
407 elif format == 'tgz':
412 rows_by_module.setdefault(module, []).append(row)
414 tmpdir = tempfile.mkdtemp()
415 for mod, modrows in rows_by_module.items():
416 tmpmoddir = join(tmpdir, mod, 'i18n')
417 os.makedirs(tmpmoddir)
418 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
419 buf = file(join(tmpmoddir, pofilename), 'w')
420 _process('po', [mod], modrows, buf, lang, newlang)
423 tar = tarfile.open(fileobj=buffer, mode='w|gz')
428 raise Exception(_('Bad file format'))
430 newlang = not bool(lang)
433 trans = trans_generate(lang, modules, dbname)
434 if newlang and format!='csv':
437 modules = set([t[0] for t in trans[1:]])
438 _process(format, modules, trans, buffer, lang, newlang)
442 def trans_parse_xsl(de):
446 for m in [j for j in n if j.text]:
447 l = m.text.strip().replace('\n',' ')
449 res.append(l.encode("utf8"))
450 res.extend(trans_parse_xsl(n))
453 def trans_parse_rml(de):
456 for m in [j for j in n if j.text]:
457 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
458 for s in string_list:
460 res.append(s.encode("utf8"))
461 res.extend(trans_parse_rml(n))
464 def trans_parse_view(de):
467 res.append(de.get('string').encode("utf8"))
469 res.append(de.get('sum').encode("utf8"))
471 res.extend(trans_parse_view(n))
474 # tests whether an object is in a list of modules
475 def in_modules(object_name, modules):
484 module = object_name.split('.')[0]
485 module = module_dict.get(module, module)
486 return module in modules
488 def trans_generate(lang, modules, dbname=None):
489 logger = logging.getLogger('i18n')
491 dbname=tools.config['db_name']
495 pool = pooler.get_pool(dbname)
496 trans_obj = pool.get('ir.translation')
497 model_data_obj = pool.get('ir.model.data')
498 cr = pooler.get_db(dbname).cursor()
500 l = pool.obj_pool.items()
503 query = 'SELECT name, model, res_id, module' \
504 ' FROM ir_model_data'
506 query_models = """SELECT m.id, m.model, imd.module
507 FROM ir_model AS m, ir_model_data AS imd
508 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
510 if 'all_installed' in modules:
511 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
512 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
514 if 'all' not in modules:
515 query += ' WHERE module IN %s'
516 query_models += ' AND imd.module in %s'
517 query_param = (tuple(modules),)
518 query += ' ORDER BY module, model, name'
519 query_models += ' ORDER BY module, model'
521 cr.execute(query, query_param)
524 def push_translation(module, type, name, id, source):
525 tuple = (module, source, name, id, type)
526 if source and tuple not in _to_translate:
527 _to_translate.append(tuple)
530 if isinstance(s, unicode):
531 return s.encode('utf8')
534 for (xml_name,model,res_id,module) in cr.fetchall():
535 module = encode(module)
536 model = encode(model)
537 xml_name = "%s.%s" % (module, encode(xml_name))
539 if not pool.get(model):
540 logger.error("Unable to find object %r", model)
543 exists = pool.get(model).exists(cr, uid, res_id)
545 logger.warning("Unable to find object %r with id %d", model, res_id)
547 obj = pool.get(model).browse(cr, uid, res_id)
549 if model=='ir.ui.view':
550 d = etree.XML(encode(obj.arch))
551 for t in trans_parse_view(d):
552 push_translation(module, 'view', encode(obj.model), 0, t)
553 elif model=='ir.actions.wizard':
554 service_name = 'wizard.'+encode(obj.wiz_name)
555 if netsvc.Service._services.get(service_name):
556 obj2 = netsvc.Service._services[service_name]
557 for state_name, state_def in obj2.states.iteritems():
558 if 'result' in state_def:
559 result = state_def['result']
560 if result['type'] != 'form':
562 name = "%s,%s" % (encode(obj.wiz_name), state_name)
565 'string': ('wizard_field', lambda s: [encode(s)]),
566 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
567 'help': ('help', lambda s: [encode(s)]),
571 if not result.has_key('fields'):
572 logger.warning("res has no fields: %r", result)
574 for field_name, field_def in result['fields'].iteritems():
575 res_name = name + ',' + field_name
577 for fn in def_params:
579 transtype, modifier = def_params[fn]
580 for val in modifier(field_def[fn]):
581 push_translation(module, transtype, res_name, 0, val)
584 arch = result['arch']
585 if arch and not isinstance(arch, UpdateableStr):
587 for t in trans_parse_view(d):
588 push_translation(module, 'wizard_view', name, 0, t)
590 # export button labels
591 for but_args in result['state']:
592 button_name = but_args[0]
593 button_label = but_args[1]
594 res_name = name + ',' + button_name
595 push_translation(module, 'wizard_button', res_name, 0, button_label)
597 elif model=='ir.model.fields':
599 field_name = encode(obj.name)
600 except AttributeError, exc:
601 logger.error("name error in %s: %s", xml_name, str(exc))
603 objmodel = pool.get(obj.model)
604 if not objmodel or not field_name in objmodel._columns:
606 field_def = objmodel._columns[field_name]
608 name = "%s,%s" % (encode(obj.model), field_name)
609 push_translation(module, 'field', name, 0, encode(field_def.string))
612 push_translation(module, 'help', name, 0, encode(field_def.help))
614 if field_def.translate:
615 ids = objmodel.search(cr, uid, [])
616 obj_values = objmodel.read(cr, uid, ids, [field_name])
617 for obj_value in obj_values:
618 res_id = obj_value['id']
619 if obj.name in ('ir.model', 'ir.ui.menu'):
621 model_data_ids = model_data_obj.search(cr, uid, [
622 ('model', '=', model),
623 ('res_id', '=', res_id),
625 if not model_data_ids:
626 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
628 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
629 for dummy, val in field_def.selection:
630 push_translation(module, 'selection', name, 0, encode(val))
632 elif model=='ir.actions.report.xml':
633 name = encode(obj.report_name)
636 fname = obj.report_rml
637 parse_func = trans_parse_rml
638 report_type = "report"
640 fname = obj.report_xsl
641 parse_func = trans_parse_xsl
643 if fname and obj.report_type in ('pdf', 'xsl'):
645 d = etree.parse(tools.file_open(fname))
646 for t in parse_func(d.iter()):
647 push_translation(module, report_type, name, 0, t)
648 except (IOError, etree.XMLSyntaxError):
649 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
651 for field_name,field_def in obj._table._columns.items():
652 if field_def.translate:
653 name = model + "," + field_name
655 trad = getattr(obj, field_name) or ''
658 push_translation(module, 'model', name, xml_name, encode(trad))
660 # End of data for ir.model.data query results
662 cr.execute(query_models, query_param)
664 def push_constraint_msg(module, term_type, model, msg):
665 # Check presence of __call__ directly instead of using
666 # callable() because it will be deprecated as of Python 3.0
667 if not hasattr(msg, '__call__'):
668 push_translation(module, term_type, model, 0, encode(msg))
670 for (model_id, model, module) in cr.fetchall():
671 module = encode(module)
672 model = encode(model)
674 model_obj = pool.get(model)
677 logging.getLogger("i18n").error("Unable to find object %r", model)
680 for constraint in getattr(model_obj, '_constraints', []):
681 push_constraint_msg(module, 'constraint', model, constraint[1])
683 for constraint in getattr(model_obj, '_sql_constraints', []):
684 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
686 # parse source code for _() calls
687 def get_module_from_path(path, mod_paths=None):
689 # First, construct a list of possible paths
690 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
691 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
694 mod_paths.append(adp)
695 if not os.path.isabs(adp):
696 mod_paths.append(adp)
697 elif adp.startswith(def_path):
698 mod_paths.append(adp[len(def_path)+1:])
700 if path.startswith(mp) and (os.path.dirname(path) != mp):
701 path = path[len(mp)+1:]
702 return path.split(os.path.sep)[0]
703 return 'base' # files that are not in a module are considered as being in 'base' module
705 modobj = pool.get('ir.module.module')
706 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
707 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
709 root_path = os.path.join(tools.config['root_path'], 'addons')
711 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
712 if root_path in apaths:
715 path_list = [root_path,] + apaths
717 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning modules at paths: %s" % (' '.join(path_list),))
720 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
721 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
722 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
723 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
725 def export_code_terms_from_file(fname, path, root, terms_type):
726 fabsolutepath = join(root, fname)
727 frelativepath = fabsolutepath[len(path):]
728 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
729 is_mod_installed = module in installed_modules
730 if (('all' in modules) or (module in modules)) and is_mod_installed:
731 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning code of %s at module: %s" % (frelativepath, module))
732 code_string = tools.file_open(fabsolutepath, subdir='').read()
733 if module in installed_modules:
734 frelativepath = str("addons" + frelativepath)
735 ite = re_dquotes.finditer(code_string)
738 if src.startswith('""'):
739 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
742 src = join_dquotes.sub(r'\1', src)
743 # now, since we did a binary read of a python source file, we
744 # have to expand pythonic escapes like the interpreter does.
745 src = src.decode('string_escape')
746 push_translation(module, terms_type, frelativepath, 0, encode(src))
747 ite = re_quotes.finditer(code_string)
750 if src.startswith("''"):
751 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
754 src = join_quotes.sub(r'\1', src)
755 src = src.decode('string_escape')
756 push_translation(module, terms_type, frelativepath, 0, encode(src))
758 for path in path_list:
759 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning files of modules at %s" % path)
760 for root, dummy, files in tools.osutil.walksymlinks(path):
761 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
762 export_code_terms_from_file(fname, path, root, 'code')
763 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
764 export_code_terms_from_file(fname, path, root, 'report')
767 out = [["module","type","name","res_id","src","value"]] # header
769 # translate strings marked as to be translated
770 for module, source, name, id, type in _to_translate:
771 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
772 out.append([module, type, name, id, source, encode(trans) or ''])
777 def trans_load(db_name, filename, lang, strict=False, verbose=True, context={}):
778 logger = logging.getLogger('i18n')
780 fileobj = open(filename,'r')
781 logger.info("loading %s", filename)
782 fileformat = os.path.splitext(filename)[-1][1:].lower()
783 r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
788 logger.error("couldn't read translation file %s", filename)
791 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context={}):
792 logger = logging.getLogger('i18n')
794 logger.info('loading translation file for language %s', lang)
795 pool = pooler.get_pool(db_name)
796 lang_obj = pool.get('res.lang')
797 trans_obj = pool.get('ir.translation')
798 model_data_obj = pool.get('ir.model.data')
799 iso_lang = tools.get_iso_codes(lang)
802 cr = pooler.get_db(db_name).cursor()
803 ids = lang_obj.search(cr, uid, [('code','=', lang)])
806 # lets create the language with locale information
808 for ln in get_locales(lang):
810 locale.setlocale(locale.LC_ALL, str(ln))
816 lc = locale.getdefaultlocale()[0]
817 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
818 logger.warning(msg, lang, lc)
821 lang_name = tools.get_languages().get(lang, lang)
830 'iso_code': iso_lang,
833 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
834 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
835 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
836 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
840 lang_obj.create(cr, uid, lang_info)
845 # now, the serious things: we read the language file
847 if fileformat == 'csv':
848 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
849 # read the first line of the file (it contains columns titles)
853 elif fileformat == 'po':
854 reader = TinyPoFile(fileobj)
855 f = ['type', 'name', 'res_id', 'src', 'value']
857 logger.error('Bad file format: %s', fileformat)
858 raise Exception(_('Bad file format'))
860 # read the rest of the file
864 # skip empty rows and rows where the translation field (=last fiefd) is empty
865 #if (not row) or (not row[-1]):
868 # dictionary which holds values for this line of the csv file
869 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
870 # 'src': ..., 'value': ...}
872 for i in range(len(f)):
873 if f[i] in ('module',):
878 dic['res_id'] = int(dic['res_id'])
880 model_data_ids = model_data_obj.search(cr, uid, [
881 ('model', '=', dic['name'].split(',')[0]),
882 ('module', '=', dic['res_id'].split('.', 1)[0]),
883 ('name', '=', dic['res_id'].split('.', 1)[1]),
886 dic['res_id'] = model_data_obj.browse(cr, uid,
887 model_data_ids[0]).res_id
889 dic['res_id'] = False
891 if dic['type'] == 'model' and not strict:
892 (model, field) = dic['name'].split(',')
894 # get the ids of the resources of this model which share
896 obj = pool.get(model)
898 if field not in obj.fields_get_keys(cr, uid):
900 ids = obj.search(cr, uid, [(field, '=', dic['src'])])
902 # if the resource id (res_id) is in that list, use it,
903 # otherwise use the whole list
906 ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
909 ids = trans_obj.search(cr, uid, [
911 ('type', '=', dic['type']),
912 ('name', '=', dic['name']),
913 ('src', '=', dic['src']),
914 ('res_id', '=', dic['res_id'])
917 if context.get('overwrite', False):
918 trans_obj.write(cr, uid, ids, {'value': dic['value']})
920 trans_obj.create(cr, uid, dic)
922 ids = trans_obj.search(cr, uid, [
924 ('type', '=', dic['type']),
925 ('name', '=', dic['name']),
926 ('src', '=', dic['src'])
929 if context.get('overwrite', False):
930 trans_obj.write(cr, uid, ids, {'value': dic['value']})
932 trans_obj.create(cr, uid, dic)
936 logger.info("translation file loaded succesfully")
938 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
939 logger.exception("couldn't read translation file %s", filename)
941 def get_locales(lang=None):
943 lang = locale.getdefaultlocale()[0]
946 lang = _LOCALE2WIN32.get(lang, lang)
949 ln = locale._build_localename((lang, enc))
951 nln = locale.normalize(ln)
955 for x in process('utf8'): yield x
957 prefenc = locale.getpreferredencoding()
959 for x in process(prefenc): yield x
963 'iso-8859-1': 'iso8859-15',
965 }.get(prefenc.lower())
967 for x in process(prefenc): yield x
974 # locale.resetlocale is bugged with some locales.
975 for ln in get_locales():
977 return locale.setlocale(locale.LC_ALL, ln)
981 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: