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 ##############################################################################
32 from os.path import join
35 from datetime import datetime
36 from lxml import etree
40 from tools.misc import UpdateableStr
43 'af_ZA': 'Afrikaans_South Africa',
44 'sq_AL': 'Albanian_Albania',
45 'ar_SA': 'Arabic_Saudi Arabia',
46 'eu_ES': 'Basque_Spain',
47 'be_BY': 'Belarusian_Belarus',
48 'bs_BA': 'Serbian (Latin)',
49 'bg_BG': 'Bulgarian_Bulgaria',
50 'ca_ES': 'Catalan_Spain',
51 'hr_HR': 'Croatian_Croatia',
52 'zh_CN': 'Chinese_China',
53 'zh_TW': 'Chinese_Taiwan',
54 'cs_CZ': 'Czech_Czech Republic',
55 'da_DK': 'Danish_Denmark',
56 'nl_NL': 'Dutch_Netherlands',
57 'et_EE': 'Estonian_Estonia',
58 'fa_IR': 'Farsi_Iran',
59 'ph_PH': 'Filipino_Philippines',
60 'fi_FI': 'Finnish_Finland',
61 'fr_FR': 'French_France',
62 'fr_BE': 'French_France',
63 'fr_CH': 'French_France',
64 'fr_CA': 'French_France',
65 'ga': 'Scottish Gaelic',
66 'gl_ES': 'Galician_Spain',
67 'ka_GE': 'Georgian_Georgia',
68 'de_DE': 'German_Germany',
69 'el_GR': 'Greek_Greece',
70 'gu': 'Gujarati_India',
71 'he_IL': 'Hebrew_Israel',
73 'hu': 'Hungarian_Hungary',
74 'is_IS': 'Icelandic_Iceland',
75 'id_ID': 'Indonesian_indonesia',
76 'it_IT': 'Italian_Italy',
77 'ja_JP': 'Japanese_Japan',
80 'ko_KR': 'Korean_Korea',
82 'lt_LT': 'Lithuanian_Lithuania',
83 'lat': 'Latvian_Latvia',
84 'ml_IN': 'Malayalam_India',
85 'id_ID': 'Indonesian_indonesia',
87 'mn': 'Cyrillic_Mongolian',
88 'no_NO': 'Norwegian_Norway',
89 'nn_NO': 'Norwegian-Nynorsk_Norway',
90 'pl': 'Polish_Poland',
91 'pt_PT': 'Portuguese_Portugal',
92 'pt_BR': 'Portuguese_Brazil',
93 'ro_RO': 'Romanian_Romania',
94 'ru_RU': 'Russian_Russia',
96 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
97 'sk_SK': 'Slovak_Slovakia',
98 'sl_SI': 'Slovenian_Slovenia',
99 #should find more specific locales for spanish countries,
100 #but better than nothing
101 'es_AR': 'Spanish_Spain',
102 'es_BO': 'Spanish_Spain',
103 'es_CL': 'Spanish_Spain',
104 'es_CO': 'Spanish_Spain',
105 'es_CR': 'Spanish_Spain',
106 'es_DO': 'Spanish_Spain',
107 'es_EC': 'Spanish_Spain',
108 'es_ES': 'Spanish_Spain',
109 'es_GT': 'Spanish_Spain',
110 'es_HN': 'Spanish_Spain',
111 'es_MX': 'Spanish_Spain',
112 'es_NI': 'Spanish_Spain',
113 'es_PA': 'Spanish_Spain',
114 'es_PE': 'Spanish_Spain',
115 'es_PR': 'Spanish_Spain',
116 'es_PY': 'Spanish_Spain',
117 'es_SV': 'Spanish_Spain',
118 'es_UY': 'Spanish_Spain',
119 'es_VE': 'Spanish_Spain',
120 'sv_SE': 'Swedish_Sweden',
121 'ta_IN': 'English_Australia',
122 'th_TH': 'Thai_Thailand',
124 'tr_TR': 'Turkish_Turkey',
125 'uk_UA': 'Ukrainian_Ukraine',
126 'vi_VN': 'Vietnamese_Viet Nam',
127 'tlh_TLH': 'Klingon',
132 class UNIX_LINE_TERMINATOR(csv.excel):
133 lineterminator = '\n'
135 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
138 # Warning: better use self.pool.get('ir.translation')._get_source if you can
140 def translate(cr, name, source_type, lang, source=None):
142 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))
144 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
146 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
147 res_trans = cr.fetchone()
148 res = res_trans and res_trans[0] or False
151 logger = logging.getLogger('translate')
153 class GettextAlias(object):
155 def _get_cr(self, frame):
157 cr = frame.f_locals.get('cr')
159 s = frame.f_locals.get('self', {})
160 cr = getattr(s, 'cr', False)
162 if frame.f_globals.get('pooler', False):
163 # TODO: we should probably get rid of the 'is_new_cr' case: no cr in locals -> no translation for you
164 dbs = frame.f_globals['pooler'].pool_dic.keys()
166 cr = pooler.get_db(dbs[0]).cursor()
170 def _get_lang(self, frame):
171 lang = frame.f_locals.get('context', {}).get('lang', False)
173 args = frame.f_locals.get('args', False)
175 lang = args[-1].get('lang', False)
177 s = frame.f_locals.get('self', {})
178 c = getattr(s, 'localcontext', {})
179 lang = c.get('lang', False)
182 def __call__(self, source):
186 frame = inspect.stack()[1][0]
187 cr, is_new_cr = self._get_cr(frame)
188 lang = self._get_lang(frame)
190 cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
191 res_trans = cr.fetchone()
192 res = res_trans and res_trans[0] or source
194 logger.debug('translation went wrong for string %s', repr(source))
204 """Returns quoted PO term string, with special PO characters escaped"""
205 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
206 return '"%s"' % s.replace('\\','\\\\') \
207 .replace('"','\\"') \
208 .replace('\n', '\\n"\n"')
210 re_escaped_char = re.compile(r"(\\.)")
211 re_escaped_replacements = {'n': '\n', }
213 def _sub_replacement(match_obj):
214 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
217 """Returns unquoted PO term string, with special PO characters unescaped"""
218 return re_escaped_char.sub(_sub_replacement, str[1:-1])
220 # class to handle po files
221 class TinyPoFile(object):
222 def __init__(self, buffer):
223 self.logger = netsvc.Logger()
227 self.logger.notifyChannel("i18n", netsvc.LOG_WARNING, msg)
231 self.lines = self._get_lines()
232 self.lines_count = len(self.lines);
238 def _get_lines(self):
239 lines = self.buffer.readlines()
240 # remove the BOM (Byte Order Mark):
242 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
244 lines.append('') # ensure that the file ends with at least an empty line
248 return (self.lines_count - len(self.lines))
251 type = name = res_id = source = trad = None
254 type, name, res_id, source, trad = self.tnrs.pop(0)
260 if 0 == len(self.lines):
261 raise StopIteration()
262 line = self.lines.pop(0).strip()
263 while line.startswith('#'):
264 if line.startswith('#~ '):
266 if line.startswith('#:'):
267 if ' ' in line[2:].strip():
268 for lpart in line[2:].strip().split(' '):
269 tmp_tnrs.append(lpart.strip().split(':',2))
271 tmp_tnrs.append( line[2:].strip().split(':',2) )
272 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
274 line = self.lines.pop(0).strip()
276 # allow empty lines between comments and msgid
277 line = self.lines.pop(0).strip()
278 if line.startswith('#~ '):
279 while line.startswith('#~ ') or not line.strip():
280 if 0 == len(self.lines):
281 raise StopIteration()
282 line = self.lines.pop(0)
283 # This has been a deprecated entry, don't return anything
286 if not line.startswith('msgid'):
287 raise Exception("malformed file: bad line: %s" % line)
288 source = unquote(line[6:])
289 line = self.lines.pop(0).strip()
290 if not source and self.first:
291 # if the source is "" and it's the first msgid, it's the special
292 # msgstr with the informations about the traduction and the
293 # traductor; we skip it
296 line = self.lines.pop(0).strip()
299 while not line.startswith('msgstr'):
301 raise Exception('malformed file at %d'% self.cur_line())
302 source += unquote(line)
303 line = self.lines.pop(0).strip()
305 trad = unquote(line[7:])
306 line = self.lines.pop(0).strip()
308 trad += unquote(line)
309 line = self.lines.pop(0).strip()
311 if tmp_tnrs and not fuzzy:
312 type, name, res_id = tmp_tnrs.pop(0)
313 for t, n, r in tmp_tnrs:
314 self.tnrs.append((t, n, r, source, trad))
319 self.warn('Missing "#:" formated comment for the following source:\n\t%s' % (source,))
321 return type, name, res_id, source, trad
323 def write_infos(self, modules):
325 self.buffer.write("# Translation of %(project)s.\n" \
326 "# This file contains the translation of the following modules:\n" \
331 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
332 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
333 '''"POT-Creation-Date: %(now)s\\n"\n''' \
334 '''"PO-Revision-Date: %(now)s\\n"\n''' \
335 '''"Last-Translator: <>\\n"\n''' \
336 '''"Language-Team: \\n"\n''' \
337 '''"MIME-Version: 1.0\\n"\n''' \
338 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
339 '''"Content-Transfer-Encoding: \\n"\n''' \
340 '''"Plural-Forms: \\n"\n''' \
343 % { 'project': release.description,
344 'version': release.version,
345 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
346 'bugmail': release.support_email,
347 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
351 def write(self, modules, tnrs, source, trad):
353 plurial = len(modules) > 1 and 's' or ''
354 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
358 for typy, name, res_id in tnrs:
359 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
364 # only strings in python code are python formated
365 self.buffer.write("#, python-format\n")
367 if not isinstance(trad, unicode):
368 trad = unicode(trad, 'utf8')
369 if not isinstance(source, unicode):
370 source = unicode(source, 'utf8')
374 % (quote(source), quote(trad))
375 self.buffer.write(msg.encode('utf8'))
378 # Methods to export the translation file
380 def trans_export(lang, modules, buffer, format, dbname=None):
382 def _process(format, modules, rows, buffer, lang, newlang):
384 writer=csv.writer(buffer, 'UNIX')
389 writer = tools.TinyPoFile(buffer)
390 writer.write_infos(modules)
392 # we now group the translations by source. That means one translation per source.
394 for module, type, name, res_id, src, trad in rows:
395 row = grouped_rows.setdefault(src, {})
396 row.setdefault('modules', set()).add(module)
397 if ('translation' not in row) or (not row['translation']):
398 row['translation'] = trad
399 row.setdefault('tnrs', []).append((type, name, res_id))
401 for src, row in grouped_rows.items():
402 writer.write(row['modules'], row['tnrs'], src, row['translation'])
404 elif format == 'tgz':
409 rows_by_module.setdefault(module, []).append(row)
411 tmpdir = tempfile.mkdtemp()
412 for mod, modrows in rows_by_module.items():
413 tmpmoddir = join(tmpdir, mod, 'i18n')
414 os.makedirs(tmpmoddir)
415 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
416 buf = file(join(tmpmoddir, pofilename), 'w')
417 _process('po', [mod], modrows, buf, lang, newlang)
420 tar = tarfile.open(fileobj=buffer, mode='w|gz')
425 raise Exception(_('Bad file format'))
427 newlang = not bool(lang)
430 trans = trans_generate(lang, modules, dbname)
431 if newlang and format!='csv':
434 modules = set([t[0] for t in trans[1:]])
435 _process(format, modules, trans, buffer, lang, newlang)
439 def trans_parse_xsl(de):
443 for m in [j for j in n if j.text]:
444 l = m.text.strip().replace('\n',' ')
446 res.append(l.encode("utf8"))
447 res.extend(trans_parse_xsl(n))
450 def trans_parse_rml(de):
453 for m in [j for j in n if j.text]:
454 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
455 for s in string_list:
457 res.append(s.encode("utf8"))
458 res.extend(trans_parse_rml(n))
461 def trans_parse_view(de):
464 res.append(de.get('string').encode("utf8"))
466 res.append(de.get('sum').encode("utf8"))
468 res.extend(trans_parse_view(n))
471 # tests whether an object is in a list of modules
472 def in_modules(object_name, modules):
481 module = object_name.split('.')[0]
482 module = module_dict.get(module, module)
483 return module in modules
485 def trans_generate(lang, modules, dbname=None):
486 logger = netsvc.Logger()
488 dbname=tools.config['db_name']
492 pool = pooler.get_pool(dbname)
493 trans_obj = pool.get('ir.translation')
494 model_data_obj = pool.get('ir.model.data')
495 cr = pooler.get_db(dbname).cursor()
497 l = pool.obj_pool.items()
500 query = 'SELECT name, model, res_id, module' \
501 ' FROM ir_model_data'
502 if 'all_installed' in modules:
503 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
505 if 'all' not in modules:
506 query += ' WHERE module IN %s'
507 query_param = (tuple(modules),)
508 query += ' ORDER BY module, model, name'
510 cr.execute(query, query_param)
513 def push_translation(module, type, name, id, source):
514 tuple = (module, source, name, id, type)
515 if source and tuple not in _to_translate:
516 _to_translate.append(tuple)
519 if isinstance(s, unicode):
520 return s.encode('utf8')
523 for (xml_name,model,res_id,module) in cr.fetchall():
524 module = encode(module)
525 model = encode(model)
526 xml_name = "%s.%s" % (module, encode(xml_name))
528 if not pool.get(model):
529 logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
532 exists = pool.get(model).exists(cr, uid, res_id)
534 logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
536 obj = pool.get(model).browse(cr, uid, res_id)
538 if model=='ir.ui.view':
539 d = etree.XML(encode(obj.arch))
540 for t in trans_parse_view(d):
541 push_translation(module, 'view', encode(obj.model), 0, t)
542 elif model=='ir.actions.wizard':
543 service_name = 'wizard.'+encode(obj.wiz_name)
544 if netsvc.Service._services.get(service_name):
545 obj2 = netsvc.Service._services[service_name]
546 for state_name, state_def in obj2.states.iteritems():
547 if 'result' in state_def:
548 result = state_def['result']
549 if result['type'] != 'form':
551 name = "%s,%s" % (encode(obj.wiz_name), state_name)
554 'string': ('wizard_field', lambda s: [encode(s)]),
555 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
556 'help': ('help', lambda s: [encode(s)]),
560 if not result.has_key('fields'):
561 logger.notifyChannel("db",netsvc.LOG_WARNING,"res has no fields: %r" % result)
563 for field_name, field_def in result['fields'].iteritems():
564 res_name = name + ',' + field_name
566 for fn in def_params:
568 transtype, modifier = def_params[fn]
569 for val in modifier(field_def[fn]):
570 push_translation(module, transtype, res_name, 0, val)
573 arch = result['arch']
574 if arch and not isinstance(arch, UpdateableStr):
576 for t in trans_parse_view(d):
577 push_translation(module, 'wizard_view', name, 0, t)
579 # export button labels
580 for but_args in result['state']:
581 button_name = but_args[0]
582 button_label = but_args[1]
583 res_name = name + ',' + button_name
584 push_translation(module, 'wizard_button', res_name, 0, button_label)
586 elif model=='ir.model.fields':
588 field_name = encode(obj.name)
589 except AttributeError, exc:
590 logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
592 objmodel = pool.get(obj.model)
593 if not objmodel or not field_name in objmodel._columns:
595 field_def = objmodel._columns[field_name]
597 name = "%s,%s" % (encode(obj.model), field_name)
598 push_translation(module, 'field', name, 0, encode(field_def.string))
601 push_translation(module, 'help', name, 0, encode(field_def.help))
603 if field_def.translate:
604 ids = objmodel.search(cr, uid, [])
605 obj_values = objmodel.read(cr, uid, ids, [field_name])
606 for obj_value in obj_values:
607 res_id = obj_value['id']
608 if obj.name in ('ir.model', 'ir.ui.menu'):
610 model_data_ids = model_data_obj.search(cr, uid, [
611 ('model', '=', model),
612 ('res_id', '=', res_id),
614 if not model_data_ids:
615 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
617 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
618 for dummy, val in field_def.selection:
619 push_translation(module, 'selection', name, 0, encode(val))
621 elif model=='ir.actions.report.xml':
622 name = encode(obj.report_name)
625 fname = obj.report_rml
626 parse_func = trans_parse_rml
627 report_type = "report"
629 fname = obj.report_xsl
630 parse_func = trans_parse_xsl
632 if fname and obj.report_type in ('pdf', 'xsl'):
634 d = etree.parse(tools.file_open(fname))
635 for t in parse_func(d.iter()):
636 push_translation(module, report_type, name, 0, t)
637 except (IOError, etree.XMLSyntaxError):
638 logging.getLogger("i18n").exception("couldn't export translation for report %s %s %s", name, report_type, fname)
640 model_obj = pool.get(model)
641 def push_constraint_msg(module, term_type, model, msg):
642 # Check presence of __call__ directly instead of using
643 # callable() because it will be deprecated as of Python 3.0
644 if not hasattr(msg, '__call__'):
645 push_translation(module, term_type, model, 0, encode(msg))
647 for constraint in model_obj._constraints:
648 push_constraint_msg(module, 'constraint', model, constraint[1])
650 for constraint in model_obj._sql_constraints:
651 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
653 for field_name,field_def in model_obj._columns.items():
654 if field_def.translate:
655 name = model + "," + field_name
657 trad = getattr(obj, field_name) or ''
660 push_translation(module, 'model', name, xml_name, encode(trad))
662 # parse source code for _() calls
663 def get_module_from_path(path, mod_paths=None):
665 # First, construct a list of possible paths
666 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
667 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
670 mod_paths.append(adp)
671 if not os.path.isabs(adp):
672 mod_paths.append(adp)
673 elif adp.startswith(def_path):
674 mod_paths.append(adp[len(def_path)+1:])
676 if path.startswith(mp) and (os.path.dirname(path) != mp):
677 path = path[len(mp)+1:]
678 return path.split(os.path.sep)[0]
679 return 'base' # files that are not in a module are considered as being in 'base' module
681 modobj = pool.get('ir.module.module')
682 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
683 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
685 root_path = os.path.join(tools.config['root_path'], 'addons')
687 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
688 if root_path in apaths:
691 path_list = [root_path,] + apaths
693 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning modules at paths: %s" % (' '.join(path_list),))
696 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
697 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
698 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
699 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
701 def export_code_terms_from_file(fname, path, root, terms_type):
702 fabsolutepath = join(root, fname)
703 frelativepath = fabsolutepath[len(path):]
704 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
705 is_mod_installed = module in installed_modules
706 if (('all' in modules) or (module in modules)) and is_mod_installed:
707 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning code of %s at module: %s" % (frelativepath, module))
708 code_string = tools.file_open(fabsolutepath, subdir='').read()
709 if module in installed_modules:
710 frelativepath = str("addons" + frelativepath)
711 ite = re_dquotes.finditer(code_string)
714 if src.startswith('""'):
715 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
718 src = join_dquotes.sub(r'\1', src)
719 # now, since we did a binary read of a python source file, we
720 # have to expand pythonic escapes like the interpreter does.
721 src = src.decode('string_escape')
722 push_translation(module, terms_type, frelativepath, 0, encode(src))
723 ite = re_quotes.finditer(code_string)
726 if src.startswith("''"):
727 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
730 src = join_quotes.sub(r'\1', src)
731 src = src.decode('string_escape')
732 push_translation(module, terms_type, frelativepath, 0, encode(src))
734 for path in path_list:
735 logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning files of modules at %s" % path)
736 for root, dummy, files in tools.osutil.walksymlinks(path):
737 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
738 export_code_terms_from_file(fname, path, root, 'code')
739 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
740 export_code_terms_from_file(fname, path, root, 'report')
743 out = [["module","type","name","res_id","src","value"]] # header
745 # translate strings marked as to be translated
746 for module, source, name, id, type in _to_translate:
747 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
748 out.append([module, type, name, id, source, encode(trans) or ''])
753 def trans_load(db_name, filename, lang, strict=False, verbose=True, context={}):
754 logger = netsvc.Logger()
756 fileobj = open(filename,'r')
757 fileformat = os.path.splitext(filename)[-1][1:].lower()
758 r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
763 logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
766 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context={}):
767 logger = netsvc.Logger()
769 logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
770 pool = pooler.get_pool(db_name)
771 lang_obj = pool.get('res.lang')
772 trans_obj = pool.get('ir.translation')
773 model_data_obj = pool.get('ir.model.data')
774 iso_lang = tools.get_iso_codes(lang)
777 cr = pooler.get_db(db_name).cursor()
778 ids = lang_obj.search(cr, uid, [('code','=', lang)])
781 # lets create the language with locale information
783 for ln in get_locales(lang):
785 locale.setlocale(locale.LC_ALL, str(ln))
791 lc = locale.getdefaultlocale()[0]
792 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
793 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc))
796 lang_name = tools.get_languages().get(lang, lang)
805 'iso_code': iso_lang,
808 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
809 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
810 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
811 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
815 lang_obj.create(cr, uid, lang_info)
820 # now, the serious things: we read the language file
822 if fileformat == 'csv':
823 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
824 # read the first line of the file (it contains columns titles)
828 elif fileformat == 'po':
829 reader = TinyPoFile(fileobj)
830 f = ['type', 'name', 'res_id', 'src', 'value']
832 raise Exception(_('Bad file format'))
834 # read the rest of the file
838 # skip empty rows and rows where the translation field (=last fiefd) is empty
839 #if (not row) or (not row[-1]):
842 # dictionary which holds values for this line of the csv file
843 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
844 # 'src': ..., 'value': ...}
846 for i in range(len(f)):
847 if f[i] in ('module',):
852 dic['res_id'] = int(dic['res_id'])
854 model_data_ids = model_data_obj.search(cr, uid, [
855 ('model', '=', dic['name'].split(',')[0]),
856 ('module', '=', dic['res_id'].split('.', 1)[0]),
857 ('name', '=', dic['res_id'].split('.', 1)[1]),
860 dic['res_id'] = model_data_obj.browse(cr, uid,
861 model_data_ids[0]).res_id
863 dic['res_id'] = False
865 if dic['type'] == 'model' and not strict:
866 (model, field) = dic['name'].split(',')
868 # get the ids of the resources of this model which share
870 obj = pool.get(model)
872 if field not in obj.fields_get_keys(cr, uid):
874 ids = obj.search(cr, uid, [(field, '=', dic['src'])])
876 # if the resource id (res_id) is in that list, use it,
877 # otherwise use the whole list
880 ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
883 ids = trans_obj.search(cr, uid, [
885 ('type', '=', dic['type']),
886 ('name', '=', dic['name']),
887 ('src', '=', dic['src']),
888 ('res_id', '=', dic['res_id'])
891 if context.get('overwrite', False):
892 trans_obj.write(cr, uid, ids, {'value': dic['value']})
894 trans_obj.create(cr, uid, dic)
896 ids = trans_obj.search(cr, uid, [
898 ('type', '=', dic['type']),
899 ('name', '=', dic['name']),
900 ('src', '=', dic['src'])
903 if context.get('overwrite', False):
904 trans_obj.write(cr, uid, ids, {'value': dic['value']})
906 trans_obj.create(cr, uid, dic)
910 logger.notifyChannel("i18n", netsvc.LOG_INFO,
911 "translation file loaded succesfully")
913 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
914 logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
916 def get_locales(lang=None):
918 lang = locale.getdefaultlocale()[0]
921 lang = _LOCALE2WIN32.get(lang, lang)
924 ln = locale._build_localename((lang, enc))
926 nln = locale.normalize(ln)
930 for x in process('utf8'): yield x
932 prefenc = locale.getpreferredencoding()
934 for x in process(prefenc): yield x
938 'iso-8859-1': 'iso8859-15',
940 }.get(prefenc.lower())
942 for x in process(prefenc): yield x
949 # locale.resetlocale is bugged with some locales.
950 for ln in get_locales():
952 return locale.setlocale(locale.LC_ALL, ln)
956 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: