0f47b6f4051c513be608f73081bd419dafde6b3e
[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
37 from datetime import datetime
38 from lxml import etree
39
40 import tools
41 import netsvc
42 from tools.misc import UpdateableStr
43 from tools.misc import SKIPPED_ELEMENT_TYPES
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(db_name)
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                 # if so, double-check the root/base translations filenames
223         finally:
224             if cr and is_new_cr:
225                 cr.close()
226         return res
227
228 _ = GettextAlias()
229
230
231 def quote(s):
232     """Returns quoted PO term string, with special PO characters escaped"""
233     assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
234     return '"%s"' % s.replace('\\','\\\\') \
235                      .replace('"','\\"') \
236                      .replace('\n', '\\n"\n"')
237
238 re_escaped_char = re.compile(r"(\\.)")
239 re_escaped_replacements = {'n': '\n', }
240
241 def _sub_replacement(match_obj):
242     return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
243
244 def unquote(str):
245     """Returns unquoted PO term string, with special PO characters unescaped"""
246     return re_escaped_char.sub(_sub_replacement, str[1:-1])
247
248 # class to handle po files
249 class TinyPoFile(object):
250     def __init__(self, buffer):
251         self.logger = logging.getLogger('i18n')
252         self.buffer = buffer
253
254     def warn(self, msg, *args):
255         self.logger.warning(msg, *args)
256
257     def __iter__(self):
258         self.buffer.seek(0)
259         self.lines = self._get_lines()
260         self.lines_count = len(self.lines);
261
262         self.first = True
263         self.tnrs= []
264         return self
265
266     def _get_lines(self):
267         lines = self.buffer.readlines()
268         # remove the BOM (Byte Order Mark):
269         if len(lines):
270             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
271
272         lines.append('') # ensure that the file ends with at least an empty line
273         return lines
274
275     def cur_line(self):
276         return (self.lines_count - len(self.lines))
277
278     def next(self):
279         type = name = res_id = source = trad = None
280
281         if self.tnrs:
282             type, name, res_id, source, trad = self.tnrs.pop(0)
283             if not res_id:
284                 res_id = '0'
285         else:
286             tmp_tnrs = []
287             line = None
288             fuzzy = False
289             while (not line):
290                 if 0 == len(self.lines):
291                     raise StopIteration()
292                 line = self.lines.pop(0).strip()
293             while line.startswith('#'):
294                 if line.startswith('#~ '):
295                     break
296                 if line.startswith('#:'):
297                     if ' ' in line[2:].strip():
298                         for lpart in line[2:].strip().split(' '):
299                             tmp_tnrs.append(lpart.strip().split(':',2))
300                     else:
301                         tmp_tnrs.append( line[2:].strip().split(':',2) )
302                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
303                     fuzzy = True
304                 line = self.lines.pop(0).strip()
305             while not line:
306                 # allow empty lines between comments and msgid
307                 line = self.lines.pop(0).strip()
308             if line.startswith('#~ '):
309                 while line.startswith('#~ ') or not line.strip():
310                     if 0 == len(self.lines):
311                         raise StopIteration()
312                     line = self.lines.pop(0)
313                 # This has been a deprecated entry, don't return anything
314                 return self.next()
315
316             if not line.startswith('msgid'):
317                 raise Exception("malformed file: bad line: %s" % line)
318             source = unquote(line[6:])
319             line = self.lines.pop(0).strip()
320             if not source and self.first:
321                 # if the source is "" and it's the first msgid, it's the special
322                 # msgstr with the informations about the traduction and the
323                 # traductor; we skip it
324                 self.tnrs = []
325                 while line:
326                     line = self.lines.pop(0).strip()
327                 return self.next()
328
329             while not line.startswith('msgstr'):
330                 if not line:
331                     raise Exception('malformed file at %d'% self.cur_line())
332                 source += unquote(line)
333                 line = self.lines.pop(0).strip()
334
335             trad = unquote(line[7:])
336             line = self.lines.pop(0).strip()
337             while line:
338                 trad += unquote(line)
339                 line = self.lines.pop(0).strip()
340
341             if tmp_tnrs and not fuzzy:
342                 type, name, res_id = tmp_tnrs.pop(0)
343                 for t, n, r in tmp_tnrs:
344                     self.tnrs.append((t, n, r, source, trad))
345
346         self.first = False
347
348         if name is None:
349             if not fuzzy:
350                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
351                         self.cur_line(), source[:30])
352             return self.next()
353         return type, name, res_id, source, trad
354
355     def write_infos(self, modules):
356         import release
357         self.buffer.write("# Translation of %(project)s.\n" \
358                           "# This file contains the translation of the following modules:\n" \
359                           "%(modules)s" \
360                           "#\n" \
361                           "msgid \"\"\n" \
362                           "msgstr \"\"\n" \
363                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
364                           '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
365                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
366                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
367                           '''"Last-Translator: <>\\n"\n''' \
368                           '''"Language-Team: \\n"\n'''   \
369                           '''"MIME-Version: 1.0\\n"\n''' \
370                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
371                           '''"Content-Transfer-Encoding: \\n"\n'''       \
372                           '''"Plural-Forms: \\n"\n'''    \
373                           "\n"
374
375                           % { 'project': release.description,
376                               'version': release.version,
377                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
378                               'bugmail': release.support_email,
379                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
380                             }
381                           )
382
383     def write(self, modules, tnrs, source, trad):
384
385         plurial = len(modules) > 1 and 's' or ''
386         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
387
388
389         code = False
390         for typy, name, res_id in tnrs:
391             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
392             if typy == 'code':
393                 code = True
394
395         if code:
396             # only strings in python code are python formated
397             self.buffer.write("#, python-format\n")
398
399         if not isinstance(trad, unicode):
400             trad = unicode(trad, 'utf8')
401         if not isinstance(source, unicode):
402             source = unicode(source, 'utf8')
403
404         msg = "msgid %s\n"      \
405               "msgstr %s\n\n"   \
406                   % (quote(source), quote(trad))
407         self.buffer.write(msg.encode('utf8'))
408
409
410 # Methods to export the translation file
411
412 def trans_export(lang, modules, buffer, format, cr):
413
414     def _process(format, modules, rows, buffer, lang, newlang):
415         if format == 'csv':
416             writer=csv.writer(buffer, 'UNIX')
417             for row in rows:
418                 writer.writerow(row)
419         elif format == 'po':
420             rows.pop(0)
421             writer = tools.TinyPoFile(buffer)
422             writer.write_infos(modules)
423
424             # we now group the translations by source. That means one translation per source.
425             grouped_rows = {}
426             for module, type, name, res_id, src, trad in rows:
427                 row = grouped_rows.setdefault(src, {})
428                 row.setdefault('modules', set()).add(module)
429                 if ('translation' not in row) or (not row['translation']):
430                     row['translation'] = trad
431                 row.setdefault('tnrs', []).append((type, name, res_id))
432
433             for src, row in grouped_rows.items():
434                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
435
436         elif format == 'tgz':
437             rows.pop(0)
438             rows_by_module = {}
439             for row in rows:
440                 module = row[0]
441                 # first row is the "header", as in csv, it will be popped
442                 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
443                 rows_by_module[module].append(row)
444
445             tmpdir = tempfile.mkdtemp()
446             for mod, modrows in rows_by_module.items():
447                 tmpmoddir = join(tmpdir, mod, 'i18n')
448                 os.makedirs(tmpmoddir)
449                 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
450                 buf = file(join(tmpmoddir, pofilename), 'w')
451                 _process('po', [mod], modrows, buf, lang, newlang)
452                 buf.close()
453
454             tar = tarfile.open(fileobj=buffer, mode='w|gz')
455             tar.add(tmpdir, '')
456             tar.close()
457
458         else:
459             raise Exception(_('Bad file format'))
460
461     newlang = not bool(lang)
462     if newlang:
463         lang = 'en_US'
464     trans = trans_generate(lang, modules, cr)
465     if newlang and format!='csv':
466         for trx in trans:
467             trx[-1] = ''
468     modules = set([t[0] for t in trans[1:]])
469     _process(format, modules, trans, buffer, lang, newlang)
470     del trans
471
472 def trans_parse_xsl(de):
473     res = []
474     for n in de:
475         if n.get("t"):
476             for m in n:
477                 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
478                     continue
479                 l = m.text.strip().replace('\n',' ')
480                 if len(l):
481                     res.append(l.encode("utf8"))
482         res.extend(trans_parse_xsl(n))
483     return res
484
485 def trans_parse_rml(de):
486     res = []
487     for n in de:
488         for m in n:
489             if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
490                 continue
491             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
492             for s in string_list:
493                 if s:
494                     res.append(s.encode("utf8"))
495         res.extend(trans_parse_rml(n))
496     return res
497
498 def trans_parse_view(de):
499     res = []
500     if de.tag == 'attribute' and de.get("name") == 'string':
501         if de.text:
502             res.append(de.text.encode("utf8"))
503     if de.get("string"):
504         res.append(de.get('string').encode("utf8"))
505     if de.get("sum"):
506         res.append(de.get('sum').encode("utf8"))
507     if de.get("confirm"):
508         res.append(de.get('confirm').encode("utf8"))
509     for n in de:
510         res.extend(trans_parse_view(n))
511     return res
512
513 # tests whether an object is in a list of modules
514 def in_modules(object_name, modules):
515     if 'all' in modules:
516         return True
517
518     module_dict = {
519         'ir': 'base',
520         'res': 'base',
521         'workflow': 'base',
522     }
523     module = object_name.split('.')[0]
524     module = module_dict.get(module, module)
525     return module in modules
526
527 def trans_generate(lang, modules, cr):
528     logger = logging.getLogger('i18n')
529     dbname = cr.dbname
530
531     pool = pooler.get_pool(dbname)
532     trans_obj = pool.get('ir.translation')
533     model_data_obj = pool.get('ir.model.data')
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                     report_file = tools.file_open(fname)
681                     try:
682                         d = etree.parse(report_file)
683                         for t in parse_func(d.iter()):
684                             push_translation(module, report_type, name, 0, t)
685                     finally:
686                         report_file.close()
687                 except (IOError, etree.XMLSyntaxError):
688                     logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
689
690         for field_name,field_def in obj._table._columns.items():
691             if field_def.translate:
692                 name = model + "," + field_name
693                 try:
694                     trad = getattr(obj, field_name) or ''
695                 except:
696                     trad = ''
697                 push_translation(module, 'model', name, xml_name, encode(trad))
698
699         # End of data for ir.model.data query results
700
701     cr.execute(query_models, query_param)
702
703     def push_constraint_msg(module, term_type, model, msg):
704         # Check presence of __call__ directly instead of using
705         # callable() because it will be deprecated as of Python 3.0
706         if not hasattr(msg, '__call__'):
707             push_translation(module, term_type, model, 0, encode(msg))
708
709     for (model_id, model, module) in cr.fetchall():
710         module = encode(module)
711         model = encode(model)
712
713         model_obj = pool.get(model)
714
715         if not model_obj:
716             logging.getLogger("i18n").error("Unable to find object %r", model)
717             continue
718
719         for constraint in getattr(model_obj, '_constraints', []):
720             push_constraint_msg(module, 'constraint', model, constraint[1])
721
722         for constraint in getattr(model_obj, '_sql_constraints', []):
723             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
724
725     # parse source code for _() calls
726     def get_module_from_path(path, mod_paths=None):
727         if not mod_paths:
728             # First, construct a list of possible paths
729             def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
730             ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
731             mod_paths=[def_path]
732             for adp in ad_paths:
733                 mod_paths.append(adp)
734                 if not os.path.isabs(adp):
735                     mod_paths.append(adp)
736                 elif adp.startswith(def_path):
737                     mod_paths.append(adp[len(def_path)+1:])
738         for mp in mod_paths:
739             if path.startswith(mp) and (os.path.dirname(path) != mp):
740                 path = path[len(mp)+1:]
741                 return path.split(os.path.sep)[0]
742         return 'base'   # files that are not in a module are considered as being in 'base' module
743
744     modobj = pool.get('ir.module.module')
745     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
746     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
747
748     root_path = os.path.join(tools.config['root_path'], 'addons')
749
750     apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
751     if root_path in apaths:
752         path_list = apaths
753     else :
754         path_list = [root_path,] + apaths
755
756     # Also scan these non-addon paths
757     for bin_path in ['osv', 'report' ]:
758         path_list.append(os.path.join(tools.config['root_path'], bin_path))
759
760     logger.debug("Scanning modules at paths: ", path_list)
761
762     mod_paths = []
763     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
764     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
765     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
766     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
767
768     def export_code_terms_from_file(fname, path, root, terms_type):
769         fabsolutepath = join(root, fname)
770         frelativepath = fabsolutepath[len(path):]
771         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
772         is_mod_installed = module in installed_modules
773         if (('all' in modules) or (module in modules)) and is_mod_installed:
774             logger.debug("Scanning code of %s at module: %s", frelativepath, module)
775             src_file = tools.file_open(fabsolutepath, subdir='')
776             try:
777                 code_string = src_file.read()
778             finally:
779                 src_file.close()
780             if module in installed_modules:
781                 frelativepath = str("addons" + frelativepath)
782             ite = re_dquotes.finditer(code_string)
783             code_offset = 0
784             code_line = 1
785             for i in ite:
786                 src = i.group(1)
787                 if src.startswith('""'):
788                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
789                     src = src[2:-2]
790                 else:
791                     src = join_dquotes.sub(r'\1', src)
792                 # try to count the lines from the last pos to our place:
793                 code_line += code_string[code_offset:i.start(1)].count('\n')
794                 # now, since we did a binary read of a python source file, we
795                 # have to expand pythonic escapes like the interpreter does.
796                 src = src.decode('string_escape')
797                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
798                 code_line += i.group(1).count('\n')
799                 code_offset = i.end() # we have counted newlines up to the match end
800
801             ite = re_quotes.finditer(code_string)
802             code_offset = 0 #reset counters
803             code_line = 1
804             for i in ite:
805                 src = i.group(1)
806                 if src.startswith("''"):
807                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
808                     src = src[2:-2]
809                 else:
810                     src = join_quotes.sub(r'\1', src)
811                 code_line += code_string[code_offset:i.start(1)].count('\n')
812                 src = src.decode('string_escape')
813                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
814                 code_line += i.group(1).count('\n')
815                 code_offset = i.end() # we have counted newlines up to the match end
816
817     for path in path_list:
818         logger.debug("Scanning files of modules at %s", path)
819         for root, dummy, files in tools.osutil.walksymlinks(path):
820             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
821                 export_code_terms_from_file(fname, path, root, 'code')
822             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
823                 export_code_terms_from_file(fname, path, root, 'report')
824
825
826     out = [["module","type","name","res_id","src","value"]] # header
827     _to_translate.sort()
828     # translate strings marked as to be translated
829     for module, source, name, id, type in _to_translate:
830         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
831         out.append([module, type, name, id, source, encode(trans) or ''])
832
833     return out
834
835 def trans_load(cr, filename, lang, verbose=True, context=None):
836     logger = logging.getLogger('i18n')
837     try:
838         fileobj = open(filename,'r')
839         logger.info("loading %s", filename)
840         fileformat = os.path.splitext(filename)[-1][1:].lower()
841         r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
842         fileobj.close()
843         return r
844     except IOError:
845         if verbose:
846             logger.error("couldn't read translation file %s", filename)
847         return None
848
849 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
850     """Populates the ir_translation table. Fixing the res_ids so that they point
851     correctly to ir_model_data is done in a separate step, using the
852     'trans_update_res_ids' function below."""
853     logger = logging.getLogger('i18n')
854     if verbose:
855         logger.info('loading translation file for language %s', lang)
856     if context is None:
857         context = {}
858     db_name = cr.dbname
859     pool = pooler.get_pool(db_name)
860     lang_obj = pool.get('res.lang')
861     trans_obj = pool.get('ir.translation')
862     model_data_obj = pool.get('ir.model.data')
863     iso_lang = tools.get_iso_codes(lang)
864     try:
865         uid = 1
866         ids = lang_obj.search(cr, uid, [('code','=', lang)])
867
868         if not ids:
869             # lets create the language with locale information
870             lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
871
872
873         # now, the serious things: we read the language file
874         fileobj.seek(0)
875         if fileformat == 'csv':
876             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
877             # read the first line of the file (it contains columns titles)
878             for row in reader:
879                 f = row
880                 break
881         elif fileformat == 'po':
882             reader = TinyPoFile(fileobj)
883             f = ['type', 'name', 'res_id', 'src', 'value']
884         else:
885             logger.error('Bad file format: %s', fileformat)
886             raise Exception(_('Bad file format'))
887
888         # read the rest of the file
889         line = 1
890         for row in reader:
891             line += 1
892             # skip empty rows and rows where the translation field (=last fiefd) is empty
893             #if (not row) or (not row[-1]):
894             #    continue
895
896             # dictionary which holds values for this line of the csv file
897             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
898             #  'src': ..., 'value': ...}
899             dic = {'lang': lang}
900             for i in range(len(f)):
901                 if f[i] in ('module',):
902                     continue
903                 dic[f[i]] = row[i]
904
905             try:
906                 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
907                 dic['module'] = False
908                 dic['xml_id'] = False
909             except:
910                 split_id = dic['res_id'].split('.', 1)
911                 dic['module'] = split_id[0]
912                 dic['xml_id'] = split_id[1]
913                 dic['res_id'] = False
914
915             args = [
916                 ('lang', '=', lang),
917                 ('type', '=', dic['type']),
918                 ('name', '=', dic['name']),
919                 ('src', '=', dic['src']),
920             ]
921             if dic['type'] == 'model':
922                 args.append(('res_id', '=', dic['res_id']))
923             ids = trans_obj.search(cr, uid, args)
924             if ids:
925                 if context.get('overwrite') and dic['value']:
926                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
927             else:
928                 trans_obj.create(cr, uid, dic)
929         if verbose:
930             logger.info("translation file loaded succesfully")
931     except IOError:
932         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
933         logger.exception("couldn't read translation file %s", filename)
934
935 def trans_update_res_ids(cr):
936     cr.execute("""
937             UPDATE ir_translation
938             SET res_id = (SELECT ir_model_data.res_id
939                           FROM ir_model_data
940                           WHERE ir_translation.module = ir_model_data.module
941                               AND ir_translation.xml_id = ir_model_data.name)
942             WHERE ir_translation.module is not null
943                 AND ir_translation.xml_id is not null
944                 AND ir_translation.res_id = 0;
945     """)
946
947 def get_locales(lang=None):
948     if lang is None:
949         lang = locale.getdefaultlocale()[0]
950
951     if os.name == 'nt':
952         lang = _LOCALE2WIN32.get(lang, lang)
953
954     def process(enc):
955         ln = locale._build_localename((lang, enc))
956         yield ln
957         nln = locale.normalize(ln)
958         if nln != ln:
959             yield nln
960
961     for x in process('utf8'): yield x
962
963     prefenc = locale.getpreferredencoding()
964     if prefenc:
965         for x in process(prefenc): yield x
966
967         prefenc = {
968             'latin1': 'latin9',
969             'iso-8859-1': 'iso8859-15',
970             'cp1252': '1252',
971         }.get(prefenc.lower())
972         if prefenc:
973             for x in process(prefenc): yield x
974
975     yield lang
976
977
978
979 def resetlocale():
980     # locale.resetlocale is bugged with some locales.
981     for ln in get_locales():
982         try:
983             return locale.setlocale(locale.LC_ALL, ln)
984         except locale.Error:
985             continue
986
987 def load_language(cr, lang):
988     """Loads a translation terms for a language.
989     Used mainly to automate language loading at db initialization.
990     
991     :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
992     :type lang: str
993     """
994     pool = pooler.get_pool(cr.dbname)
995     language_installer = pool.get('base.language.install')
996     uid = 1
997     oid = language_installer.create(cr, uid, {'lang': lang})
998     language_installer.lang_install(cr, uid, [oid], context=None)
999
1000 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1001