[MERGE] tools.translate, test_reports, res_config: several bugfixes (see buglinks)
[odoo/odoo.git] / bin / tools / translate.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import codecs
23 import csv
24 import fnmatch
25 import inspect
26 import itertools
27 import locale
28 import os
29 import pooler
30 import re
31 import logging
32 import tarfile
33 import tempfile
34 import threading
35 from os.path import join
36 import logging
37
38 from datetime import datetime
39 from lxml import etree
40
41 import tools, pooler
42 import netsvc
43 from tools.misc import UpdateableStr
44
45 _LOCALE2WIN32 = {
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',
75     'hi_IN': 'Hindi',
76     'hu': 'Hungarian_Hungary',
77     'is_IS': 'Icelandic_Iceland',
78     'id_ID': 'Indonesian_indonesia',
79     'it_IT': 'Italian_Italy',
80     'ja_JP': 'Japanese_Japan',
81     'kn_IN': 'Kannada',
82     'km_KH': 'Khmer',
83     'ko_KR': 'Korean_Korea',
84     'lo_LA': 'Lao_Laos',
85     'lt_LT': 'Lithuanian_Lithuania',
86     'lat': 'Latvian_Latvia',
87     'ml_IN': 'Malayalam_India',
88     'id_ID': 'Indonesian_indonesia',
89     'mi_NZ': 'Maori',
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',
98     'mi_NZ': 'Maori',
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',
126     'mi_NZ': 'Maori',
127     'tr_TR': 'Turkish_Turkey',
128     'uk_UA': 'Ukrainian_Ukraine',
129     'vi_VN': 'Vietnamese_Viet Nam',
130     'tlh_TLH': 'Klingon',
131
132 }
133
134
135 class UNIX_LINE_TERMINATOR(csv.excel):
136     lineterminator = '\n'
137
138 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
139
140 #
141 # Warning: better use self.pool.get('ir.translation')._get_source if you can
142 #
143 def translate(cr, name, source_type, lang, source=None):
144     if source and name:
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))
146     elif name:
147         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
148     elif source:
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
152     return res
153
154 logger = logging.getLogger('translate')
155
156 class GettextAlias(object):
157
158     def _get_db(self):
159         # find current DB based on thread/worker db name (see netsvc)
160         db_name = getattr(threading.currentThread(), 'dbname', None)
161         if db_name:
162             return pooler.get_db_only(dbname)
163
164     def _get_cr(self, frame):
165         is_new_cr = False
166         cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
167         if not cr:
168             s = frame.f_locals.get('self', {})
169             cr = getattr(s, 'cr', None)
170         if not cr:
171             db = self._get_db()
172             if db:
173                 cr = db.cursor()
174                 is_new_cr = True
175         return cr, is_new_cr
176
177     def _get_lang(self, frame):
178         lang = None
179         ctx = frame.f_locals.get('context')
180         if not ctx:
181             kwargs = frame.f_locals.get('kwargs')
182             if kwargs is None:
183                 args = frame.f_locals.get('args')
184                 if args and isinstance(args, (list, tuple)) \
185                         and isinstance(args[-1], dict):
186                     ctx = args[-1]
187             elif isinstance(kwargs, dict):
188                 ctx = kwargs.get('context')
189         if ctx:
190             lang = ctx.get('lang')
191         if not lang:
192             s = frame.f_locals.get('self', {})
193             c = getattr(s, 'localcontext', None)
194             if c:
195                 lang = c.get('lang')
196         return lang
197
198     def __call__(self, source):
199         res = source
200         cr = None
201         is_new_cr = False
202         try:
203             frame = inspect.currentframe()
204             if frame is None:
205                 return source
206             frame = frame.f_back
207             if not frame:
208                 return source
209             lang = self._get_lang(frame)
210             if lang:
211                 cr, is_new_cr = self._get_cr(frame)
212                 if cr:
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)
216                 else:
217                     logger.debug('no context cursor detected, skipping translation for "%r"', source)
218             else:
219                 logger.debug('no translation language detected, skipping translation for "%r" ', source)
220         except Exception:
221             logger.debug('translation went wrong for "%r", skipped', source)
222         finally:
223             if cr and is_new_cr:
224                 cr.close()
225         return res
226
227 _ = GettextAlias()
228
229
230 def quote(s):
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"')
236
237 re_escaped_char = re.compile(r"(\\.)")
238 re_escaped_replacements = {'n': '\n', }
239
240 def _sub_replacement(match_obj):
241     return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
242
243 def unquote(str):
244     """Returns unquoted PO term string, with special PO characters unescaped"""
245     return re_escaped_char.sub(_sub_replacement, str[1:-1])
246
247 # class to handle po files
248 class TinyPoFile(object):
249     def __init__(self, buffer):
250         self.logger = logging.getLogger('i18n')
251         self.buffer = buffer
252
253     def warn(self, msg, *args):
254         self.logger.warning(msg, *args)
255
256     def __iter__(self):
257         self.buffer.seek(0)
258         self.lines = self._get_lines()
259         self.lines_count = len(self.lines);
260
261         self.first = True
262         self.tnrs= []
263         return self
264
265     def _get_lines(self):
266         lines = self.buffer.readlines()
267         # remove the BOM (Byte Order Mark):
268         if len(lines):
269             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
270
271         lines.append('') # ensure that the file ends with at least an empty line
272         return lines
273
274     def cur_line(self):
275         return (self.lines_count - len(self.lines))
276
277     def next(self):
278         type = name = res_id = source = trad = None
279
280         if self.tnrs:
281             type, name, res_id, source, trad = self.tnrs.pop(0)
282             if not res_id:
283                 res_id = '0'
284         else:
285             tmp_tnrs = []
286             line = None
287             fuzzy = False
288             while (not line):
289                 if 0 == len(self.lines):
290                     raise StopIteration()
291                 line = self.lines.pop(0).strip()
292             while line.startswith('#'):
293                 if line.startswith('#~ '):
294                     break
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))
299                     else:
300                         tmp_tnrs.append( line[2:].strip().split(':',2) )
301                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
302                     fuzzy = True
303                 line = self.lines.pop(0).strip()
304             while not line:
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
313                 return self.next()
314
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
323                 self.tnrs = []
324                 while line:
325                     line = self.lines.pop(0).strip()
326                 return self.next()
327
328             while not line.startswith('msgstr'):
329                 if not line:
330                     raise Exception('malformed file at %d'% self.cur_line())
331                 source += unquote(line)
332                 line = self.lines.pop(0).strip()
333
334             trad = unquote(line[7:])
335             line = self.lines.pop(0).strip()
336             while line:
337                 trad += unquote(line)
338                 line = self.lines.pop(0).strip()
339
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))
344
345         self.first = False
346
347         if name is None:
348             if not fuzzy:
349                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', 
350                         self.cur_line(), source[:30])
351             return self.next()
352         return type, name, res_id, source, trad
353
354     def write_infos(self, modules):
355         import release
356         self.buffer.write("# Translation of %(project)s.\n" \
357                           "# This file contains the translation of the following modules:\n" \
358                           "%(modules)s" \
359                           "#\n" \
360                           "msgid \"\"\n" \
361                           "msgstr \"\"\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'''    \
372                           "\n"
373
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",
379                             }
380                           )
381
382     def write(self, modules, tnrs, source, trad):
383
384         plurial = len(modules) > 1 and 's' or ''
385         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
386
387
388         code = False
389         for typy, name, res_id in tnrs:
390             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
391             if typy == 'code':
392                 code = True
393
394         if code:
395             # only strings in python code are python formated
396             self.buffer.write("#, python-format\n")
397
398         if not isinstance(trad, unicode):
399             trad = unicode(trad, 'utf8')
400         if not isinstance(source, unicode):
401             source = unicode(source, 'utf8')
402
403         msg = "msgid %s\n"      \
404               "msgstr %s\n\n"   \
405                   % (quote(source), quote(trad))
406         self.buffer.write(msg.encode('utf8'))
407
408
409 # Methods to export the translation file
410
411 def trans_export(lang, modules, buffer, format, dbname=None):
412
413     def _process(format, modules, rows, buffer, lang, newlang):
414         if format == 'csv':
415             writer=csv.writer(buffer, 'UNIX')
416             for row in rows:
417                 writer.writerow(row)
418         elif format == 'po':
419             rows.pop(0)
420             writer = tools.TinyPoFile(buffer)
421             writer.write_infos(modules)
422
423             # we now group the translations by source. That means one translation per source.
424             grouped_rows = {}
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))
431
432             for src, row in grouped_rows.items():
433                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
434
435         elif format == 'tgz':
436             rows.pop(0)
437             rows_by_module = {}
438             for row in rows:
439                 module = row[0]
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)
443
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)
451                 buf.close()
452
453             tar = tarfile.open(fileobj=buffer, mode='w|gz')
454             tar.add(tmpdir, '')
455             tar.close()
456
457         else:
458             raise Exception(_('Bad file format'))
459
460     newlang = not bool(lang)
461     if newlang:
462         lang = 'en_US'
463     trans = trans_generate(lang, modules, dbname)
464     if newlang and format!='csv':
465         for trx in trans:
466             trx[-1] = ''
467     modules = set([t[0] for t in trans[1:]])
468     _process(format, modules, trans, buffer, lang, newlang)
469     del trans
470
471
472 def trans_parse_xsl(de):
473     res = []
474     for n in de:
475         if n.get("t"):
476             for m in [j for j in n if j.text]:
477                 l = m.text.strip().replace('\n',' ')
478                 if len(l):
479                     res.append(l.encode("utf8"))
480         res.extend(trans_parse_xsl(n))
481     return res
482
483 def trans_parse_rml(de):
484     res = []
485     for n in 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:
489                 if s:
490                     res.append(s.encode("utf8"))
491         res.extend(trans_parse_rml(n))
492     return res
493
494 def trans_parse_view(de):
495     res = []
496     if de.tag == 'attribute' and de.get("name") == 'string':
497         if de.text:
498             res.append(de.text.encode("utf8"))
499     if de.get("string"):
500         res.append(de.get('string').encode("utf8"))
501     if de.get("sum"):
502         res.append(de.get('sum').encode("utf8"))
503     if de.get("confirm"):
504         res.append(de.get('confirm').encode("utf8"))
505     for n in de:
506         res.extend(trans_parse_view(n))
507     return res
508
509 # tests whether an object is in a list of modules
510 def in_modules(object_name, modules):
511     if 'all' in modules:
512         return True
513
514     module_dict = {
515         'ir': 'base',
516         'res': 'base',
517         'workflow': 'base',
518     }
519     module = object_name.split('.')[0]
520     module = module_dict.get(module, module)
521     return module in modules
522
523 def trans_generate(lang, modules, dbname=None):
524     logger = logging.getLogger('i18n')
525     if not dbname:
526         dbname=tools.config['db_name']
527         if not modules:
528             modules = ['all']
529
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()
534     uid = 1
535     l = pool.obj_pool.items()
536     l.sort()
537
538     query = 'SELECT name, model, res_id, module'    \
539             '  FROM ir_model_data'
540             
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' """
544
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') "
548     query_param = None
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'
555
556     cr.execute(query, query_param)
557
558     _to_translate = []
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)
563
564     def encode(s):
565         if isinstance(s, unicode):
566             return s.encode('utf8')
567         return s
568
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))
573
574         if not pool.get(model):
575             logger.error("Unable to find object %r", model)
576             continue
577
578         exists = pool.get(model).exists(cr, uid, res_id)
579         if not exists:
580             logger.warning("Unable to find object %r with id %d", model, res_id)
581             continue
582         obj = pool.get(model).browse(cr, uid, res_id)
583
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':
596                             continue
597                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
598
599                         def_params = {
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)]),
603                         }
604
605                         # export fields
606                         if not result.has_key('fields'):
607                             logger.warning("res has no fields: %r", result)
608                             continue
609                         for field_name, field_def in result['fields'].iteritems():
610                             res_name = name + ',' + field_name
611
612                             for fn in def_params:
613                                 if fn in field_def:
614                                     transtype, modifier = def_params[fn]
615                                     for val in modifier(field_def[fn]):
616                                         push_translation(module, transtype, res_name, 0, val)
617
618                         # export arch
619                         arch = result['arch']
620                         if arch and not isinstance(arch, UpdateableStr):
621                             d = etree.XML(arch)
622                             for t in trans_parse_view(d):
623                                 push_translation(module, 'wizard_view', name, 0, t)
624
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)
631
632         elif model=='ir.model.fields':
633             try:
634                 field_name = encode(obj.name)
635             except AttributeError, exc:
636                 logger.error("name error in %s: %s", xml_name, str(exc))
637                 continue
638             objmodel = pool.get(obj.model)
639             if not objmodel or not field_name in objmodel._columns:
640                 continue
641             field_def = objmodel._columns[field_name]
642
643             name = "%s,%s" % (encode(obj.model), field_name)
644             push_translation(module, 'field', name, 0, encode(field_def.string))
645
646             if field_def.help:
647                 push_translation(module, 'help', name, 0, encode(field_def.help))
648
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'):
655                         res_id = 0
656                     model_data_ids = model_data_obj.search(cr, uid, [
657                         ('model', '=', model),
658                         ('res_id', '=', res_id),
659                         ])
660                     if not model_data_ids:
661                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
662
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))
666
667         elif model=='ir.actions.report.xml':
668             name = encode(obj.report_name)
669             fname = ""
670             if obj.report_rml:
671                 fname = obj.report_rml
672                 parse_func = trans_parse_rml
673                 report_type = "report"
674             elif obj.report_xsl:
675                 fname = obj.report_xsl
676                 parse_func = trans_parse_xsl
677                 report_type = "xsl"
678             if fname and obj.report_type in ('pdf', 'xsl'):
679                 try:
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)
685
686         for field_name,field_def in obj._table._columns.items():
687             if field_def.translate:
688                 name = model + "," + field_name
689                 try:
690                     trad = getattr(obj, field_name) or ''
691                 except:
692                     trad = ''
693                 push_translation(module, 'model', name, xml_name, encode(trad))
694
695         # End of data for ir.model.data query results
696
697     cr.execute(query_models, query_param)
698
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))
704
705     for (model_id, model, module) in cr.fetchall():
706         module = encode(module)
707         model = encode(model)
708
709         model_obj = pool.get(model)
710
711         if not model_obj:
712             logging.getLogger("i18n").error("Unable to find object %r", model)
713             continue
714
715         for constraint in getattr(model_obj, '_constraints', []):
716             push_constraint_msg(module, 'constraint', model, constraint[1])
717
718         for constraint in getattr(model_obj, '_sql_constraints', []):
719             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
720
721     # parse source code for _() calls
722     def get_module_from_path(path, mod_paths=None):
723         if not mod_paths:
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(','))
727             mod_paths=[def_path]
728             for adp in ad_paths:
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:])
734         for mp in mod_paths:
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
739
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']))
743
744     root_path = os.path.join(tools.config['root_path'], 'addons')
745
746     apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
747     if root_path in apaths:
748         path_list = apaths
749     else :
750         path_list = [root_path,] + apaths
751
752     logger.debug("Scanning modules at paths: ", path_list)
753
754     mod_paths = []
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)
759
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)
771             for i in ite:
772                 src = i.group(1)
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])
775                     src = src[2:-2]
776                 else:
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)
783             for i in ite:
784                 src = i.group(1)
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])
787                     src = src[2:-2]
788                 else:
789                     src = join_quotes.sub(r'\1', src)
790                 src = src.decode('string_escape')
791                 push_translation(module, terms_type, frelativepath, 0, encode(src))
792
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')
800
801
802     out = [["module","type","name","res_id","src","value"]] # header
803     _to_translate.sort()
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 ''])
808
809     cr.close()
810     return out
811
812 def trans_load(db_name, filename, lang, verbose=True, context=None):
813     logger = logging.getLogger('i18n')
814     try:
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)
819         fileobj.close()
820         return r
821     except IOError:
822         if verbose:
823             logger.error("couldn't read translation file %s", filename)
824         return None
825
826 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
827     logger = logging.getLogger('i18n')
828     if verbose:
829         logger.info('loading translation file for language %s', lang)
830     if context is None:
831         context = {}
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)
837     try:
838         uid = 1
839         cr = pooler.get_db(db_name).cursor()
840         ids = lang_obj.search(cr, uid, [('code','=', lang)])
841
842         if not ids:
843             # lets create the language with locale information
844             fail = True
845             for ln in get_locales(lang):
846                 try:
847                     locale.setlocale(locale.LC_ALL, str(ln))
848                     fail = False
849                     break
850                 except locale.Error:
851                     continue
852             if fail:
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)
856
857             if not lang_name:
858                 lang_name = tools.get_languages().get(lang, lang)
859
860             def fix_xa0(s):
861                 if s == '\xa0':
862                     return '\xc2\xa0'
863                 return s
864
865             lang_info = {
866                 'code': lang,
867                 'iso_code': iso_lang,
868                 'name': lang_name,
869                 'translatable': 1,
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'])),
874             }
875
876             try:
877                 lang_obj.create(cr, uid, lang_info)
878             finally:
879                 resetlocale()
880
881
882         # now, the serious things: we read the language file
883         fileobj.seek(0)
884         if fileformat == 'csv':
885             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
886             # read the first line of the file (it contains columns titles)
887             for row in reader:
888                 f = row
889                 break
890         elif fileformat == 'po':
891             reader = TinyPoFile(fileobj)
892             f = ['type', 'name', 'res_id', 'src', 'value']
893         else:
894             logger.error('Bad file format: %s', fileformat)
895             raise Exception(_('Bad file format'))
896
897         # read the rest of the file
898         line = 1
899         for row in reader:
900             line += 1
901             # skip empty rows and rows where the translation field (=last fiefd) is empty
902             #if (not row) or (not row[-1]):
903             #    continue
904
905             # dictionary which holds values for this line of the csv file
906             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
907             #  'src': ..., 'value': ...}
908             dic = {'lang': lang}
909             for i in range(len(f)):
910                 if f[i] in ('module',):
911                     continue
912                 dic[f[i]] = row[i]
913
914             try:
915                 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
916             except:
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]),
921                     ])
922                 if model_data_ids:
923                     dic['res_id'] = model_data_obj.browse(cr, uid,
924                             model_data_ids[0]).res_id
925                 else:
926                     dic['res_id'] = False
927
928             args = [
929                 ('lang', '=', lang),
930                 ('type', '=', dic['type']),
931                 ('name', '=', dic['name']),
932                 ('src', '=', dic['src']),
933             ]
934             if dic['type'] == 'model':
935                 args.append(('res_id', '=', dic['res_id']))
936             ids = trans_obj.search(cr, uid, args)
937             if ids:
938                 if context.get('overwrite'):
939                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
940             else:
941                 trans_obj.create(cr, uid, dic)
942             cr.commit()
943         cr.close()
944         if verbose:
945             logger.info("translation file loaded succesfully")
946     except IOError:
947         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
948         logger.exception("couldn't read translation file %s", filename)
949
950 def get_locales(lang=None):
951     if lang is None:
952         lang = locale.getdefaultlocale()[0]
953
954     if os.name == 'nt':
955         lang = _LOCALE2WIN32.get(lang, lang)
956
957     def process(enc):
958         ln = locale._build_localename((lang, enc))
959         yield ln
960         nln = locale.normalize(ln)
961         if nln != ln:
962             yield nln
963
964     for x in process('utf8'): yield x
965
966     prefenc = locale.getpreferredencoding()
967     if prefenc:
968         for x in process(prefenc): yield x
969
970         prefenc = {
971             'latin1': 'latin9',
972             'iso-8859-1': 'iso8859-15',
973             'cp1252': '1252',
974         }.get(prefenc.lower())
975         if prefenc:
976             for x in process(prefenc): yield x
977
978     yield lang
979
980
981
982 def resetlocale():
983     # locale.resetlocale is bugged with some locales.
984     for ln in get_locales():
985         try:
986             return locale.setlocale(locale.LC_ALL, ln)
987         except locale.Error:
988             continue
989
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
991