1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 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
26 import csv, xml.dom, re
27 import osv, tools, pooler
30 from tools.misc import UpdateableStr
32 import mx.DateTime as mxdt
38 class UNIX_LINE_TERMINATOR(csv.excel):
41 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
44 # TODO: a caching method
46 def translate(cr, name, source_type, lang, source=None):
48 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))
50 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
52 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
53 res_trans = cr.fetchone()
54 res = res_trans and res_trans[0] or False
57 class GettextAlias(object):
58 def __call__(self, source):
59 frame = inspect.stack()[1][0]
60 cr = frame.f_locals.get('cr')
62 lang = frame.f_locals.get('context', {}).get('lang', False)
67 return translate(cr, None, 'code', lang, source) or source
72 # class to handle po files
73 class TinyPoFile(object):
74 def __init__(self, buffer):
79 self.lines = self._get_lines()
86 lines = self.buffer.readlines()
87 # remove the BOM (Byte Order Mark):
89 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
91 lines.append('') # ensure that the file ends with at least an empty line
96 return str[1:-1].replace("\\n", "\n") \
99 type = name = res_id = source = trad = None
102 type, name, res_id, source, trad = self.tnrs.pop(0)
108 if 0 == len(self.lines):
109 raise StopIteration()
110 line = self.lines.pop(0).strip()
112 while line.startswith('#'):
113 if line.startswith('#:'):
114 tmp_tnrs.append( line[2:].strip().split(':') )
115 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
117 line = self.lines.pop(0).strip()
119 # allow empty lines between comments and msgid
120 line = self.lines.pop(0).strip()
121 if not line.startswith('msgid'):
122 raise Exception("malformed file: bad line: %s" % line)
123 source = unquote(line[6:])
124 line = self.lines.pop(0).strip()
125 if not source and self.first:
126 # if the source is "" and it's the first msgid, it's the special
127 # msgstr with the informations about the traduction and the
128 # traductor; we skip it
131 line = self.lines.pop(0).strip()
134 while not line.startswith('msgstr'):
136 raise Exception('malformed file')
137 source += unquote(line)
138 line = self.lines.pop(0).strip()
140 trad = unquote(line[7:])
141 line = self.lines.pop(0).strip()
143 trad += unquote(line)
144 line = self.lines.pop(0).strip()
146 if tmp_tnrs and not fuzzy:
147 type, name, res_id = tmp_tnrs.pop(0)
148 for t, n, r in tmp_tnrs:
149 self.tnrs.append((t, n, r, source, trad))
155 return type, name, res_id, source, trad
157 def write_infos(self, modules):
159 self.buffer.write("# Translation of %(project)s.\n" \
160 "# This file containt the translation of the following modules:\n" \
165 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
166 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
167 '''"POT-Creation-Date: %(now)s\\n"\n''' \
168 '''"PO-Revision-Date: %(now)s\\n"\n''' \
169 '''"Last-Translator: <>\\n"\n''' \
170 '''"Language-Team: \\n"\n''' \
171 '''"MIME-Version: 1.0\\n"\n''' \
172 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
173 '''"Content-Transfer-Encoding: \\n"\n''' \
174 '''"Plural-Forms: \\n"\n''' \
177 % { 'project': release.description,
178 'version': release.version,
179 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
180 'bugmail': release.support_email,
181 'now': mxdt.ISO.strUTC(mxdt.ISO.DateTime.utc()),
185 def write(self, modules, tnrs, source, trad):
187 return '"%s"' % s.replace('"','\\"') \
188 .replace('\n', '\\n"\n"')
190 plurial = len(modules) > 1 and 's' or ''
191 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
195 for typy, name, res_id in tnrs:
196 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
201 # only strings in python code are python formated
202 self.buffer.write("#, python-format\n")
204 if not isinstance(trad, unicode):
205 trad = unicode(trad, 'utf8')
206 if not isinstance(source, unicode):
207 source = unicode(source, 'utf8')
211 % (quote(source), quote(trad))
212 self.buffer.write(msg.encode('utf8'))
215 # Methods to export the translation file
217 def trans_export(lang, modules, buffer, format, dbname=None):
219 def _process(format, modules, rows, buffer, lang, newlang):
221 writer=csv.writer(buffer, 'UNIX')
226 writer = tools.TinyPoFile(buffer)
227 writer.write_infos(modules)
229 # we now group the translations by source. That means one translation per source.
231 for module, type, name, res_id, src, trad in rows:
232 row = grouped_rows.setdefault(src, {})
233 row.setdefault('modules', set()).add(module)
234 if ('translation' not in row) or (not row['translation']):
235 row['translation'] = trad
236 row.setdefault('tnrs', []).append((type, name, res_id))
238 for src, row in grouped_rows.items():
239 writer.write(row['modules'], row['tnrs'], src, row['translation'])
241 elif format == 'tgz':
246 rows_by_module.setdefault(module, []).append(row)
248 tmpdir = tempfile.mkdtemp()
249 for mod, modrows in rows_by_module.items():
250 tmpmoddir = join(tmpdir, mod, 'i18n')
251 os.makedirs(tmpmoddir)
252 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
253 buf = open(join(tmpmoddir, pofilename), 'w')
254 _process('po', [mod], modrows, buf, lang, newlang)
256 tar = tarfile.open(fileobj=buffer, mode='w|gz')
261 raise Exception(_('Bad file format'))
263 newlang = not bool(lang)
266 trans = trans_generate(lang, modules, dbname)
267 modules = set([t[0] for t in trans[1:]])
268 _process(format, modules, trans, buffer, lang, newlang)
272 def trans_parse_xsl(de):
274 for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
275 if n.hasAttribute("t"):
276 for m in [j for j in n.childNodes if (j.nodeType == j.TEXT_NODE)]:
277 l = m.data.strip().replace('\n',' ')
279 res.append(l.encode("utf8"))
280 res.extend(trans_parse_xsl(n))
283 def trans_parse_rml(de):
285 for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
286 for m in [j for j in n.childNodes if (j.nodeType == j.TEXT_NODE)]:
287 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.data)]
288 for s in string_list:
290 res.append(s.encode("utf8"))
291 res.extend(trans_parse_rml(n))
294 def trans_parse_view(de):
296 if de.hasAttribute("string"):
297 s = de.getAttribute('string')
299 res.append(s.encode("utf8"))
300 if de.hasAttribute("sum"):
301 s = de.getAttribute('sum')
303 res.append(s.encode("utf8"))
304 for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
305 res.extend(trans_parse_view(n))
308 # tests whether an object is in a list of modules
309 def in_modules(object_name, modules):
318 module = object_name.split('.')[0]
319 module = module_dict.get(module, module)
320 return module in modules
322 def trans_generate(lang, modules, dbname=None):
323 logger = netsvc.Logger()
325 dbname=tools.config['db_name']
329 pool = pooler.get_pool(dbname)
330 trans_obj = pool.get('ir.translation')
331 model_data_obj = pool.get('ir.model.data')
332 cr = pooler.get_db(dbname).cursor()
334 l = pool.obj_pool.items()
337 query = 'SELECT name, model, res_id, module' \
338 ' FROM ir_model_data'
339 if not 'all' in modules:
340 query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
341 query += ' ORDER BY module, model, name'
343 query_param = not 'all' in modules and modules or None
344 cr.execute(query, query_param)
347 def push_translation(module, type, name, id, source):
348 tuple = (module, type, name, id, source)
349 if source and tuple not in _to_translate:
350 _to_translate.append(tuple)
353 for (xml_name,model,res_id,module) in cr.fetchall():
354 xml_name = module+'.'+xml_name
355 if not pool.get(model):
356 logger.notifyChannel("db", netsvc.LOG_ERROR, "unable to find object %r" % (model,))
358 obj = pool.get(model).browse(cr, uid, res_id)
359 if model=='ir.ui.view':
360 d = xml.dom.minidom.parseString(obj.arch)
361 for t in trans_parse_view(d.documentElement):
362 push_translation(module, 'view', obj.model, 0, t)
363 elif model=='ir.actions.wizard':
364 service_name = 'wizard.'+obj.wiz_name
366 obj2 = netsvc._service[service_name]
367 except KeyError, exc:
368 logger.notifyChannel("db", netsvc.LOG_ERROR, "key error in %s: %s" % (xml_name,str(exc)))
370 for state_name, state_def in obj2.states.iteritems():
371 if 'result' in state_def:
372 result = state_def['result']
373 if result['type'] != 'form':
375 name = obj.wiz_name + ',' + state_name
378 for field_name, field_def in result['fields'].iteritems():
379 res_name = name + ',' + field_name
380 if 'string' in field_def:
381 source = field_def['string']
382 push_translation(module, 'wizard_field', res_name, 0, source.encode('utf8'))
384 if 'selection' in field_def:
385 for key, val in field_def['selection']:
386 push_translation(module, 'selection', res_name, 0, val.encode('utf8'))
389 arch = result['arch']
390 if arch and not isinstance(arch, UpdateableStr):
391 d = xml.dom.minidom.parseString(arch)
392 for t in trans_parse_view(d.documentElement):
393 push_translation(module, 'wizard_view', name, 0, t)
395 # export button labels
396 for but_args in result['state']:
397 button_name = but_args[0]
398 button_label = but_args[1]
399 res_name = name + ',' + button_name
400 push_translation(module, 'wizard_button', res_name, 0, button_label)
402 elif model=='ir.model.fields':
404 field_name = obj.name
405 except AttributeError, exc:
406 logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
408 objmodel = pool.get(obj.model)
409 if not objmodel or not field_name in objmodel._columns:
411 field_def = objmodel._columns[field_name]
413 name = obj.model + "," + field_name
414 push_translation(module, 'field', name, 0, field_def.string.encode('utf8'))
417 push_translation(module, 'help', name, 0, field_def.help.encode('utf8'))
419 if field_def.translate:
420 ids = objmodel.search(cr, uid, [])
421 obj_values = objmodel.read(cr, uid, ids, [field_name])
422 for obj_value in obj_values:
423 res_id = obj_value['id']
424 if obj.name in ('ir.model', 'ir.ui.menu'):
426 model_data_ids = model_data_obj.search(cr, uid, [
427 ('model', '=', model),
428 ('res_id', '=', res_id),
430 if not model_data_ids:
431 push_translation(module, 'model', name, 0, obj_value[field_name])
433 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
434 for key, val in field_def.selection:
435 push_translation(module, 'selection', name, 0, val.encode('utf8'))
437 elif model=='ir.actions.report.xml':
438 name = obj.report_name
441 fname = obj.report_rml
442 parse_func = trans_parse_rml
445 fname = obj.report_xsl
446 parse_func = trans_parse_xsl
449 xmlstr = tools.file_open(fname).read()
450 d = xml.dom.minidom.parseString(xmlstr)
451 for t in parse_func(d.documentElement):
452 push_translation(module, report_type, name, 0, t)
455 logger.notifyChannel("init", netsvc.LOG_WARNING, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
457 for constraint in pool.get(model)._constraints:
459 push_translation(module, 'constraint', model, 0, msg.encode('utf8'))
461 for field_name,field_def in pool.get(model)._columns.items():
462 if field_def.translate:
463 name = model + "," + field_name
465 trad = getattr(obj, field_name) or ''
468 push_translation(module, 'model', name, xml_name, trad)
470 # parse source code for _() calls
471 def get_module_from_path(path):
472 relative_addons_path = tools.config['addons_path'][len(tools.config['root_path'])+1:]
473 if path.startswith(relative_addons_path) and (os.path.dirname(path) != relative_addons_path):
474 path = path[len(relative_addons_path)+1:]
475 return path.split(os.path.sep)[0]
476 return 'base' # files that are not in a module are considered as being in 'base' module
478 modobj = pool.get('ir.module.module')
479 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
480 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
482 for root, dirs, files in tools.osutil.walksymlinks(tools.config['root_path']):
483 for fname in fnmatch.filter(files, '*.py'):
484 fabsolutepath = join(root, fname)
485 frelativepath = fabsolutepath[len(tools.config['root_path'])+1:]
486 module = get_module_from_path(frelativepath)
487 is_mod_installed = module in installed_modules
488 if (('all' in modules) or (module in modules)) and is_mod_installed:
489 code_string = tools.file_open(fabsolutepath, subdir='').read()
491 '[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
494 push_translation(module, 'code', frelativepath, 0, i.group(1).encode('utf8'))
497 out = [["module","type","name","res_id","src","value"]] # header
498 # translate strings marked as to be translated
499 for module, type, name, id, source in _to_translate:
500 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
501 out.append([module, type, name, id, source, trans or ''])
506 def trans_load(db_name, filename, lang, strict=False, verbose=True):
507 logger = netsvc.Logger()
509 fileobj = open(filename,'r')
510 fileformat = os.path.splitext(filename)[-1][1:].lower()
511 r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
516 logger.notifyChannel("init", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,)) # FIXME translate message
519 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
520 logger = netsvc.Logger()
522 logger.notifyChannel("init", netsvc.LOG_INFO,
523 'loading translation file for language %s' % (lang))
524 pool = pooler.get_pool(db_name)
525 lang_obj = pool.get('res.lang')
526 trans_obj = pool.get('ir.translation')
527 model_data_obj = pool.get('ir.model.data')
530 cr = pooler.get_db(db_name).cursor()
532 ids = lang_obj.search(cr, uid, [('code','=',lang)])
536 languages=tools.get_languages()
537 lang_name = languages.get(lang, lang)
538 ids = lang_obj.create(cr, uid, {
544 lang_obj.write(cr, uid, ids, {'translatable':1})
545 lang_ids = lang_obj.search(cr, uid, [])
546 langs = lang_obj.read(cr, uid, lang_ids)
547 ls = map(lambda x: (x['code'],x['name']), langs)
551 if fileformat == 'csv':
552 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
553 # read the first line of the file (it contains columns titles)
557 elif fileformat == 'po':
558 reader = TinyPoFile(fileobj)
559 f = ['type', 'name', 'res_id', 'src', 'value']
561 raise Exception(_('Bad file format'))
563 # read the rest of the file
567 # skip empty rows and rows where the translation field (=last fiefd) is empty
568 if (not row) or (not row[-1]):
569 #print "translate: skip %s" % repr(row)
572 # dictionary which holds values for this line of the csv file
573 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
574 # 'src': ..., 'value': ...}
576 for i in range(len(f)):
577 if f[i] in ('module',):
582 dic['res_id'] = int(dic['res_id'])
584 model_data_ids = model_data_obj.search(cr, uid, [
585 ('model', '=', dic['name'].split(',')[0]),
586 ('module', '=', dic['res_id'].split('.', 1)[0]),
587 ('name', '=', dic['res_id'].split('.', 1)[1]),
590 dic['res_id'] = model_data_obj.browse(cr, uid,
591 model_data_ids[0]).res_id
593 dic['res_id'] = False
595 if dic['type'] == 'model' and not strict:
596 (model, field) = dic['name'].split(',')
598 # get the ids of the resources of this model which share
600 obj = pool.get(model)
602 ids = obj.search(cr, uid, [(field, '=', dic['src'])])
604 # if the resource id (res_id) is in that list, use it,
605 # otherwise use the whole list
606 ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
609 ids = trans_obj.search(cr, uid, [
611 ('type', '=', dic['type']),
612 ('name', '=', dic['name']),
613 ('src', '=', dic['src']),
614 ('res_id', '=', dic['res_id'])
617 trans_obj.write(cr, uid, ids, {'value': dic['value']})
619 trans_obj.create(cr, uid, dic)
621 ids = trans_obj.search(cr, uid, [
623 ('type', '=', dic['type']),
624 ('name', '=', dic['name']),
625 ('src', '=', dic['src'])
628 trans_obj.write(cr, uid, ids, {'value': dic['value']})
630 trans_obj.create(cr, uid, dic)
634 logger.notifyChannel("init", netsvc.LOG_INFO,
635 "translation file loaded succesfully")
637 filename = '[lang: %s][format: %s]' % (lang or 'new', fileformat)
638 logger.notifyChannel("init", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
641 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: