1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from os.path import join
27 from lxml import etree
28 import osv, tools, pooler
31 from tools.misc import UpdateableStr
33 import mx.DateTime as mxdt
40 'af_ZA': 'Afrikaans_South Africa',
41 'sq_AL': 'Albanian_Albania',
42 'ar_SA': 'Arabic_Saudi Arabia',
43 'eu_ES': 'Basque_Spain',
44 'be_BY': 'Belarusian_Belarus',
45 'bs_BA': 'Serbian (Latin)',
46 'bg_BG': 'Bulgarian_Bulgaria',
47 'ca_ES': 'Catalan_Spain',
48 'hr_HR': 'Croatian_Croatia',
49 'zh_CN': 'Chinese_China',
50 'zh_TW': 'Chinese_Taiwan',
51 'cs_CZ': 'Czech_Czech Republic',
52 'da_DK': 'Danish_Denmark',
53 'nl_NL': 'Dutch_Netherlands',
54 'et_EE': 'Estonian_Estonia',
55 'fa_IR': 'Farsi_Iran',
56 'ph_PH': 'Filipino_Philippines',
57 'fi_FI': 'Finnish_Finland',
58 'fr_FR': 'French_France',
59 'fr_BE': 'French_France',
60 'fr_CH': 'French_France',
61 'fr_CA': 'French_France',
62 'ga': 'Scottish Gaelic',
63 'gl_ES': 'Galician_Spain',
64 'ka_GE': 'Georgian_Georgia',
65 'de_DE': 'German_Germany',
66 'el_GR': 'Greek_Greece',
67 'gu': 'Gujarati_India',
68 'he_IL': 'Hebrew_Israel',
70 'hu': 'Hungarian_Hungary',
71 'is_IS': 'Icelandic_Iceland',
72 'id_ID': 'Indonesian_indonesia',
73 'it_IT': 'Italian_Italy',
74 'ja_JP': 'Japanese_Japan',
77 'ko_KR': 'Korean_Korea',
79 'lt_LT': 'Lithuanian_Lithuania',
80 'lat': 'Latvian_Latvia',
81 'ml_IN': 'Malayalam_India',
82 'id_ID': 'Indonesian_indonesia',
84 'mn': 'Cyrillic_Mongolian',
85 'no_NO': 'Norwegian_Norway',
86 'nn_NO': 'Norwegian-Nynorsk_Norway',
87 'pl': 'Polish_Poland',
88 'pt_PT': 'Portuguese_Portugal',
89 'pt_BR': 'Portuguese_Brazil',
90 'ro_RO': 'Romanian_Romania',
91 'ru_RU': 'Russian_Russia',
93 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
94 'sk_SK': 'Slovak_Slovakia',
95 'sl_SI': 'Slovenian_Slovenia',
96 'es_ES': 'Spanish_Spain',
97 'sv_SE': 'Swedish_Sweden',
98 'ta_IN': 'English_Australia',
99 'th_TH': 'Thai_Thailand',
101 'tr_TR': 'Turkish_Turkey',
102 'uk_UA': 'Ukrainian_Ukraine',
103 'vi_VN': 'Vietnamese_Viet Nam',
104 'tlh_TLH': 'Klingon',
109 class UNIX_LINE_TERMINATOR(csv.excel):
110 lineterminator = '\n'
112 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
115 # Warning: better use self.pool.get('ir.translation')._get_source if you can
117 def translate(cr, name, source_type, lang, source=None):
119 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))
121 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
123 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
124 res_trans = cr.fetchone()
125 res = res_trans and res_trans[0] or False
128 class GettextAlias(object):
129 def __call__(self, source):
131 frame = inspect.stack()[1][0]
135 cr = frame.f_locals.get('cr')
136 lang = (frame.f_locals.get('context') or {}).get('lang', False)
137 if not (lang and cr):
140 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, 'code', source))
141 res_trans = cr.fetchone()
142 return res_trans and res_trans[0] or source
146 # class to handle po files
147 class TinyPoFile(object):
148 def __init__(self, buffer):
153 self.lines = self._get_lines()
159 def _get_lines(self):
160 lines = self.buffer.readlines()
161 # remove the BOM (Byte Order Mark):
163 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
165 lines.append('') # ensure that the file ends with at least an empty line
170 return str[1:-1].replace("\\n", "\n") \
171 .replace("\\\\ ", "\\ ") \
174 type = name = res_id = source = trad = None
177 type, name, res_id, source, trad = self.tnrs.pop(0)
182 if 0 == len(self.lines):
183 raise StopIteration()
184 line = self.lines.pop(0).strip()
185 if line.startswith('#:'):
186 tmp_tnrs.append( line[2:].strip().split(':') )
187 if line.startswith('#'):
190 if not line.startswith('msgid'):
191 raise Exception("malformed file: bad line: %s" % line)
192 source = unquote(line[6:])
193 line = self.lines.pop(0).strip()
194 if not source and self.first:
195 # if the source is "" and it's the first msgid, it's the special
196 # msgstr with the informations about the traduction and the
197 # traductor; we skip it
200 line = self.lines.pop(0).strip()
203 while not line.startswith('msgstr'):
205 raise Exception('malformed file')
206 source += unquote(line)
207 line = self.lines.pop(0).strip()
209 trad = unquote(line[7:])
210 line = self.lines.pop(0).strip()
212 trad += unquote(line)
213 line = self.lines.pop(0).strip()
216 type, name, res_id = tmp_tnrs.pop(0)
217 for t, n, r in tmp_tnrs:
218 self.tnrs.append((t, n, r, source, trad))
221 return type, name, res_id, source, trad
223 def write_infos(self, modules):
225 self.buffer.write("# Translation of %(project)s.\n" \
226 "# This file contains the translation of the following modules:\n" \
231 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
232 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
233 '''"POT-Creation-Date: %(now)s\\n"\n''' \
234 '''"PO-Revision-Date: %(now)s\\n"\n''' \
235 '''"Last-Translator: <>\\n"\n''' \
236 '''"Language-Team: \\n"\n''' \
237 '''"MIME-Version: 1.0\\n"\n''' \
238 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
239 '''"Content-Transfer-Encoding: \\n"\n''' \
240 '''"Plural-Forms: \\n"\n''' \
243 % { 'project': release.description,
244 'version': release.version,
245 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
246 'bugmail': release.support_email,
247 'now': mxdt.ISO.strUTC(mxdt.ISO.DateTime.utc()),
251 def write(self, modules, tnrs, source, trad):
253 return '"%s"' % s.replace('"','\\"') \
254 .replace('\\ ','\\\\ ') \
255 .replace('\n', '\\n"\n"')
257 plurial = len(modules) > 1 and 's' or ''
258 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
262 for typy, name, res_id in tnrs:
263 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
268 # only strings in python code are python formated
269 self.buffer.write("#, python-format\n")
271 if not isinstance(trad, unicode):
272 trad = unicode(trad, 'utf8')
273 if not isinstance(source, unicode):
274 source = unicode(source, 'utf8')
278 % (quote(source), quote(trad))
279 self.buffer.write(msg.encode('utf8'))
282 # Methods to export the translation file
284 def trans_export(lang, modules, buffer, format, dbname=None):
286 def _process(format, modules, rows, buffer, lang, newlang):
288 writer=csv.writer(buffer, 'UNIX')
293 writer = tools.TinyPoFile(buffer)
294 writer.write_infos(modules)
296 # we now group the translations by source. That means one translation per source.
298 for module, type, name, res_id, src, trad in rows:
299 row = grouped_rows.setdefault(src, {})
300 row.setdefault('modules', set()).add(module)
301 if ('translation' not in row) or (not row['translation']):
302 row['translation'] = trad
303 row.setdefault('tnrs', []).append((type, name, res_id))
305 for src, row in grouped_rows.items():
306 writer.write(row['modules'], row['tnrs'], src, row['translation'])
308 elif format == 'tgz':
313 rows_by_module.setdefault(module, []).append(row)
315 tmpdir = tempfile.mkdtemp()
316 for mod, modrows in rows_by_module.items():
317 tmpmoddir = join(tmpdir, mod, 'i18n')
318 os.makedirs(tmpmoddir)
319 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
320 buf = file(join(tmpmoddir, pofilename), 'w')
321 _process('po', [mod], modrows, buf, lang, newlang)
324 tar = tarfile.open(fileobj=buffer, mode='w|gz')
329 raise Exception(_('Bad file format'))
331 newlang = not bool(lang)
334 trans = trans_generate(lang, modules, dbname)
335 modules = set([t[0] for t in trans[1:]])
336 _process(format, modules, trans, buffer, lang, newlang)
340 def trans_parse_xsl(de):
342 for n in [i for i in de.getchildren()]:
344 for m in [j for j in n.getchildren() if j.text]:
345 l = m.text.strip().replace('\n',' ')
347 res.append(l.encode("utf8"))
348 res.extend(trans_parse_xsl(n))
351 def trans_parse_rml(de):
353 for n in [i for i in de.getchildren()]:
354 for m in [j for j in n.getchildren() if j.text]:
355 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
356 for s in string_list:
358 res.append(s.encode("utf8"))
359 res.extend(trans_parse_rml(n))
362 def trans_parse_view(de):
367 res.append(s.encode("utf8"))
371 res.append(s.encode("utf8"))
372 for n in [i for i in de.getchildren()]:
373 res.extend(trans_parse_view(n))
376 # tests whether an object is in a list of modules
377 def in_modules(object_name, modules):
386 module = object_name.split('.')[0]
387 module = module_dict.get(module, module)
388 return module in modules
390 def trans_generate(lang, modules, dbname=None):
391 logger = netsvc.Logger()
393 dbname=tools.config['db_name']
397 pool = pooler.get_pool(dbname)
398 trans_obj = pool.get('ir.translation')
399 model_data_obj = pool.get('ir.model.data')
400 cr = pooler.get_db(dbname).cursor()
402 l = pool.obj_pool.items()
405 query = 'SELECT name, model, res_id, module' \
406 ' FROM ir_model_data'
407 if not 'all' in modules:
408 query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
409 query += ' ORDER BY module, model, name'
411 query_param = not 'all' in modules and modules or None
412 cr.execute(query, query_param)
415 def push_translation(module, type, name, id, source):
416 tuple = (module, source, name, id, type)
417 if source and tuple not in _to_translate:
418 _to_translate.append(tuple)
421 if isinstance(s, unicode):
422 return s.encode('utf8')
425 for (xml_name,model,res_id,module) in cr.fetchall():
426 module = encode(module)
427 model = encode(model)
428 xml_name = "%s.%s" % (module, encode(xml_name))
430 if not pool.get(model):
431 logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
434 exists = pool.get(model).exists(cr, uid, res_id)
436 logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
438 obj = pool.get(model).browse(cr, uid, res_id)
440 if model=='ir.ui.view':
441 d = etree.XML(encode(obj.arch))
442 for t in trans_parse_view(d):
443 push_translation(module, 'view', encode(obj.model), 0, t)
444 elif model=='ir.actions.wizard':
445 service_name = 'wizard.'+encode(obj.wiz_name)
446 if netsvc.SERVICES.get(service_name):
447 obj2 = netsvc.SERVICES[service_name]
448 for state_name, state_def in obj2.states.iteritems():
449 if 'result' in state_def:
450 result = state_def['result']
451 if result['type'] != 'form':
453 name = "%s,%s" % (encode(obj.wiz_name), state_name)
456 'string': ('wizard_field', lambda s: [encode(s)]),
457 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
458 'help': ('help', lambda s: [encode(s)]),
462 for field_name, field_def in result['fields'].iteritems():
463 res_name = name + ',' + field_name
465 for fn in def_params:
467 transtype, modifier = def_params[fn]
468 for val in modifier(field_def[fn]):
469 push_translation(module, transtype, res_name, 0, val)
472 arch = result['arch']
473 if arch and not isinstance(arch, UpdateableStr):
475 for t in trans_parse_view(d):
476 push_translation(module, 'wizard_view', name, 0, t)
478 # export button labels
479 for but_args in result['state']:
480 button_name = but_args[0]
481 button_label = but_args[1]
482 res_name = name + ',' + button_name
483 push_translation(module, 'wizard_button', res_name, 0, button_label)
485 elif model=='ir.model.fields':
486 field_name = encode(obj.name)
487 objmodel = pool.get(obj.model)
488 if not objmodel or not field_name in objmodel._columns:
490 field_def = objmodel._columns[field_name]
492 name = "%s,%s" % (encode(obj.model), field_name)
493 push_translation(module, 'field', name, 0, encode(field_def.string))
496 push_translation(module, 'help', name, 0, encode(field_def.help))
498 if field_def.translate:
499 ids = objmodel.search(cr, uid, [])
500 obj_values = objmodel.read(cr, uid, ids, [field_name])
501 for obj_value in obj_values:
502 res_id = obj_value['id']
503 if obj.name in ('ir.model', 'ir.ui.menu'):
505 model_data_ids = model_data_obj.search(cr, uid, [
506 ('model', '=', model),
507 ('res_id', '=', res_id),
509 if not model_data_ids:
510 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
512 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
513 for key, val in field_def.selection:
514 push_translation(module, 'selection', name, 0, encode(val))
516 elif model=='ir.actions.report.xml':
517 name = encode(obj.report_name)
520 fname = obj.report_rml
521 parse_func = trans_parse_rml
524 fname = obj.report_xsl
525 parse_func = trans_parse_xsl
528 xmlstr = tools.file_open(fname).read()
529 d = etree.XML(xmlstr)
530 for t in parse_func(d):
531 push_translation(module, report_type, name, 0, t)
532 except IOError, etree.XMLSyntaxError:
534 logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
536 for constraint in pool.get(model)._constraints:
538 push_translation(module, 'constraint', model, 0, encode(msg))
540 for field_name,field_def in pool.get(model)._columns.items():
541 if field_def.translate:
542 name = model + "," + field_name
543 trad = getattr(obj, field_name) or ''
544 push_translation(module, 'model', name, xml_name, encode(trad))
546 # parse source code for _() calls
547 def get_module_from_path(path):
548 relative_addons_path = tools.config['addons_path'][len(tools.config['root_path'])+1:]
549 if path.startswith(relative_addons_path) and (os.path.dirname(path) != relative_addons_path):
550 path = path[len(relative_addons_path)+1:]
551 return path.split(os.path.sep)[0]
552 return 'base' # files that are not in a module are considered as being in 'base' module
554 modobj = pool.get('ir.module.module')
555 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
556 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
558 root_path = os.path.join(tools.config['root_path'], 'addons')
560 if root_path in tools.config['addons_path'] :
561 path_list = [root_path]
563 path_list = [root_path,tools.config['addons_path']]
565 for path in path_list:
566 for root, dirs, files in tools.osutil.walksymlinks(path):
567 for fname in fnmatch.filter(files, '*.py'):
568 fabsolutepath = join(root, fname)
569 frelativepath = fabsolutepath[len(path):]
570 module = get_module_from_path(frelativepath)
571 is_mod_installed = module in installed_modules
572 if (('all' in modules) or (module in modules)) and is_mod_installed:
573 code_string = tools.file_open(fabsolutepath, subdir='').read()
574 iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
577 if module in installed_modules :
578 frelativepath =str("addons"+frelativepath)
580 push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
583 out = [["module","type","name","res_id","src","value"]] # header
585 # translate strings marked as to be translated
586 for module, source, name, id, type in _to_translate:
587 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
588 out.append([module, type, name, id, source, encode(trans) or ''])
593 def trans_load(db_name, filename, lang, strict=False, verbose=True):
594 logger = netsvc.Logger()
596 fileobj = open(filename,'r')
597 fileformat = os.path.splitext(filename)[-1][1:].lower()
598 r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
603 logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
606 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
607 logger = netsvc.Logger()
609 logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
610 pool = pooler.get_pool(db_name)
611 lang_obj = pool.get('res.lang')
612 trans_obj = pool.get('ir.translation')
613 model_data_obj = pool.get('ir.model.data')
616 cr = pooler.get_db(db_name).cursor()
617 ids = lang_obj.search(cr, uid, [('code','=', lang)])
620 # lets create the language with locale information
622 for ln in get_locales(lang):
624 locale.setlocale(locale.LC_ALL, str(ln))
630 lc = locale.getdefaultlocale()[0]
631 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
632 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc))
635 lang_name = tools.get_languages().get(lang, lang)
641 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
642 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
643 'decimal_point' : str(locale.localeconv()['decimal_point']).replace('\xa0', '\xc2\xa0'),
644 'thousands_sep' : str(locale.localeconv()['thousands_sep']).replace('\xa0', '\xc2\xa0'),
648 lang_obj.create(cr, uid, lang_info)
653 # now, the serious things: we read the language file
655 if fileformat == 'csv':
656 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
657 # read the first line of the file (it contains columns titles)
661 elif fileformat == 'po':
662 reader = TinyPoFile(fileobj)
663 f = ['type', 'name', 'res_id', 'src', 'value']
665 raise Exception(_('Bad file format'))
667 # read the rest of the file
671 # skip empty rows and rows where the translation field (=last fiefd) is empty
672 #if (not row) or (not row[-1]):
675 # dictionary which holds values for this line of the csv file
676 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
677 # 'src': ..., 'value': ...}
679 for i in range(len(f)):
680 if f[i] in ('module',):
685 dic['res_id'] = int(dic['res_id'])
687 model_data_ids = model_data_obj.search(cr, uid, [
688 ('model', '=', dic['name'].split(',')[0]),
689 ('module', '=', dic['res_id'].split('.', 1)[0]),
690 ('name', '=', dic['res_id'].split('.', 1)[1]),
693 dic['res_id'] = model_data_obj.browse(cr, uid,
694 model_data_ids[0]).res_id
696 dic['res_id'] = False
698 if dic['type'] == 'model' and not strict:
699 (model, field) = dic['name'].split(',')
701 # get the ids of the resources of this model which share
703 obj = pool.get(model)
705 ids = obj.search(cr, uid, [(field, '=', dic['src'])])
707 # if the resource id (res_id) is in that list, use it,
708 # otherwise use the whole list
709 ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
712 ids = trans_obj.search(cr, uid, [
714 ('type', '=', dic['type']),
715 ('name', '=', dic['name']),
716 ('src', '=', dic['src']),
717 ('res_id', '=', dic['res_id'])
720 trans_obj.write(cr, uid, ids, {'value': dic['value']})
722 trans_obj.create(cr, uid, dic)
724 ids = trans_obj.search(cr, uid, [
726 ('type', '=', dic['type']),
727 ('name', '=', dic['name']),
728 ('src', '=', dic['src'])
731 trans_obj.write(cr, uid, ids, {'value': dic['value']})
733 trans_obj.create(cr, uid, dic)
737 logger.notifyChannel("i18n", netsvc.LOG_INFO,
738 "translation file loaded succesfully")
740 filename = '[lang: %s][format: %s]' % (lang or 'new', fileformat)
741 logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
743 def get_locales(lang=None):
745 lang = locale.getdefaultlocale()[0]
748 lang = _LOCALE2WIN32.get(lang, lang)
751 ln = locale._build_localename((lang, enc))
753 nln = locale.normalize(ln)
757 for x in process('utf8'): yield x
759 prefenc = locale.getpreferredencoding()
761 for x in process(prefenc): yield x
765 'iso-8859-1': 'iso8859-15',
767 }.get(prefenc.lower())
769 for x in process(prefenc): yield x
776 # locale.resetlocale is bugged with some locales.
777 for ln in get_locales():
779 return locale.setlocale(locale.LC_ALL, ln)
783 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: