translate: implement a counter to export the code line number
[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
44 _LOCALE2WIN32 = {
45     'af_ZA': 'Afrikaans_South Africa',
46     'sq_AL': 'Albanian_Albania',
47     'ar_SA': 'Arabic_Saudi Arabia',
48     'eu_ES': 'Basque_Spain',
49     'be_BY': 'Belarusian_Belarus',
50     'bs_BA': 'Serbian (Latin)',
51     'bg_BG': 'Bulgarian_Bulgaria',
52     'ca_ES': 'Catalan_Spain',
53     'hr_HR': 'Croatian_Croatia',
54     'zh_CN': 'Chinese_China',
55     'zh_TW': 'Chinese_Taiwan',
56     'cs_CZ': 'Czech_Czech Republic',
57     'da_DK': 'Danish_Denmark',
58     'nl_NL': 'Dutch_Netherlands',
59     'et_EE': 'Estonian_Estonia',
60     'fa_IR': 'Farsi_Iran',
61     'ph_PH': 'Filipino_Philippines',
62     'fi_FI': 'Finnish_Finland',
63     'fr_FR': 'French_France',
64     'fr_BE': 'French_France',
65     'fr_CH': 'French_France',
66     'fr_CA': 'French_France',
67     'ga': 'Scottish Gaelic',
68     'gl_ES': 'Galician_Spain',
69     'ka_GE': 'Georgian_Georgia',
70     'de_DE': 'German_Germany',
71     'el_GR': 'Greek_Greece',
72     'gu': 'Gujarati_India',
73     'he_IL': 'Hebrew_Israel',
74     'hi_IN': 'Hindi',
75     'hu': 'Hungarian_Hungary',
76     'is_IS': 'Icelandic_Iceland',
77     'id_ID': 'Indonesian_indonesia',
78     'it_IT': 'Italian_Italy',
79     'ja_JP': 'Japanese_Japan',
80     'kn_IN': 'Kannada',
81     'km_KH': 'Khmer',
82     'ko_KR': 'Korean_Korea',
83     'lo_LA': 'Lao_Laos',
84     'lt_LT': 'Lithuanian_Lithuania',
85     'lat': 'Latvian_Latvia',
86     'ml_IN': 'Malayalam_India',
87     'id_ID': 'Indonesian_indonesia',
88     'mi_NZ': 'Maori',
89     'mn': 'Cyrillic_Mongolian',
90     'no_NO': 'Norwegian_Norway',
91     'nn_NO': 'Norwegian-Nynorsk_Norway',
92     'pl': 'Polish_Poland',
93     'pt_PT': 'Portuguese_Portugal',
94     'pt_BR': 'Portuguese_Brazil',
95     'ro_RO': 'Romanian_Romania',
96     'ru_RU': 'Russian_Russia',
97     'mi_NZ': 'Maori',
98     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
99     'sk_SK': 'Slovak_Slovakia',
100     'sl_SI': 'Slovenian_Slovenia',
101     #should find more specific locales for spanish countries,
102     #but better than nothing
103     'es_AR': 'Spanish_Spain',
104     'es_BO': 'Spanish_Spain',
105     'es_CL': 'Spanish_Spain',
106     'es_CO': 'Spanish_Spain',
107     'es_CR': 'Spanish_Spain',
108     'es_DO': 'Spanish_Spain',
109     'es_EC': 'Spanish_Spain',
110     'es_ES': 'Spanish_Spain',
111     'es_GT': 'Spanish_Spain',
112     'es_HN': 'Spanish_Spain',
113     'es_MX': 'Spanish_Spain',
114     'es_NI': 'Spanish_Spain',
115     'es_PA': 'Spanish_Spain',
116     'es_PE': 'Spanish_Spain',
117     'es_PR': 'Spanish_Spain',
118     'es_PY': 'Spanish_Spain',
119     'es_SV': 'Spanish_Spain',
120     'es_UY': 'Spanish_Spain',
121     'es_VE': 'Spanish_Spain',
122     'sv_SE': 'Swedish_Sweden',
123     'ta_IN': 'English_Australia',
124     'th_TH': 'Thai_Thailand',
125     'mi_NZ': 'Maori',
126     'tr_TR': 'Turkish_Turkey',
127     'uk_UA': 'Ukrainian_Ukraine',
128     'vi_VN': 'Vietnamese_Viet Nam',
129     'tlh_TLH': 'Klingon',
130
131 }
132
133
134 class UNIX_LINE_TERMINATOR(csv.excel):
135     lineterminator = '\n'
136
137 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
138
139 #
140 # Warning: better use self.pool.get('ir.translation')._get_source if you can
141 #
142 def translate(cr, name, source_type, lang, source=None):
143     if source and name:
144         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))
145     elif name:
146         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
147     elif source:
148         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
149     res_trans = cr.fetchone()
150     res = res_trans and res_trans[0] or False
151     return res
152
153 logger = logging.getLogger('translate')
154
155 class GettextAlias(object):
156
157     def _get_db(self):
158         # find current DB based on thread/worker db name (see netsvc)
159         db_name = getattr(threading.currentThread(), 'dbname', None)
160         if db_name:
161             return pooler.get_db_only(db_name)
162
163     def _get_cr(self, frame):
164         is_new_cr = False
165         cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
166         if not cr:
167             s = frame.f_locals.get('self', {})
168             cr = getattr(s, 'cr', None)
169         if not cr:
170             db = self._get_db()
171             if db:
172                 cr = db.cursor()
173                 is_new_cr = True
174         return cr, is_new_cr
175
176     def _get_lang(self, frame):
177         lang = None
178         ctx = frame.f_locals.get('context')
179         if not ctx:
180             kwargs = frame.f_locals.get('kwargs')
181             if kwargs is None:
182                 args = frame.f_locals.get('args')
183                 if args and isinstance(args, (list, tuple)) \
184                         and isinstance(args[-1], dict):
185                     ctx = args[-1]
186             elif isinstance(kwargs, dict):
187                 ctx = kwargs.get('context')
188         if ctx:
189             lang = ctx.get('lang')
190         if not lang:
191             s = frame.f_locals.get('self', {})
192             c = getattr(s, 'localcontext', None)
193             if c:
194                 lang = c.get('lang')
195         return lang
196
197     def __call__(self, source):
198         res = source
199         cr = None
200         is_new_cr = False
201         try:
202             frame = inspect.currentframe()
203             if frame is None:
204                 return source
205             frame = frame.f_back
206             if not frame:
207                 return source
208             lang = self._get_lang(frame)
209             if lang:
210                 cr, is_new_cr = self._get_cr(frame)
211                 if cr:
212                     # Try to use ir.translation to benefit from global cache if possible
213                     pool = pooler.get_pool(cr.dbname)
214                     res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
215                 else:
216                     logger.debug('no context cursor detected, skipping translation for "%r"', source)
217             else:
218                 logger.debug('no translation language detected, skipping translation for "%r" ', source)
219         except Exception:
220             logger.debug('translation went wrong for "%r", skipped', source)
221                 # if so, double-check the root/base translations filenames
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     # Also scan these non-addon paths
753     for bin_path in ['osv', 'report' ]:
754         path_list.append(os.path.join(tools.config['root_path'], bin_path))
755
756     logger.debug("Scanning modules at paths: ", path_list)
757
758     mod_paths = []
759     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
760     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
761     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
762     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
763
764     def export_code_terms_from_file(fname, path, root, terms_type):
765         fabsolutepath = join(root, fname)
766         frelativepath = fabsolutepath[len(path):]
767         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
768         is_mod_installed = module in installed_modules
769         if (('all' in modules) or (module in modules)) and is_mod_installed:
770             logger.debug("Scanning code of %s at module: %s", frelativepath, module)
771             code_string = tools.file_open(fabsolutepath, subdir='').read()
772             if module in installed_modules:
773                 frelativepath = str("addons" + frelativepath)
774             ite = re_dquotes.finditer(code_string)
775             code_offset = 0
776             code_line = 1
777             for i in ite:
778                 src = i.group(1)
779                 if src.startswith('""'):
780                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
781                     src = src[2:-2]
782                 else:
783                     src = join_dquotes.sub(r'\1', src)
784                 # try to count the lines from the last pos to our place:
785                 code_line += code_string[code_offset:i.start(1)].count('\n')
786                 # now, since we did a binary read of a python source file, we
787                 # have to expand pythonic escapes like the interpreter does.
788                 src = src.decode('string_escape')
789                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
790                 code_line += i.group(1).count('\n')
791                 code_offset = i.end() # we have counted newlines up to the match end
792
793             ite = re_quotes.finditer(code_string)
794             code_offset = 0 #reset counters
795             code_line = 1
796             for i in ite:
797                 src = i.group(1)
798                 if src.startswith("''"):
799                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
800                     src = src[2:-2]
801                 else:
802                     src = join_quotes.sub(r'\1', src)
803                 code_line += code_string[code_offset:i.start(1)].count('\n')
804                 src = src.decode('string_escape')
805                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
806                 code_line += i.group(1).count('\n')
807                 code_offset = i.end() # we have counted newlines up to the match end
808
809     for path in path_list:
810         logger.debug("Scanning files of modules at %s", path)
811         for root, dummy, files in tools.osutil.walksymlinks(path):
812             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
813                 export_code_terms_from_file(fname, path, root, 'code')
814             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
815                 export_code_terms_from_file(fname, path, root, 'report')
816
817
818     out = [["module","type","name","res_id","src","value"]] # header
819     _to_translate.sort()
820     # translate strings marked as to be translated
821     for module, source, name, id, type in _to_translate:
822         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
823         out.append([module, type, name, id, source, encode(trans) or ''])
824
825     cr.close()
826     return out
827
828 def trans_load(db_name, filename, lang, verbose=True, context=None):
829     logger = logging.getLogger('i18n')
830     try:
831         fileobj = open(filename,'r')
832         logger.info("loading %s", filename)
833         fileformat = os.path.splitext(filename)[-1][1:].lower()
834         r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
835         fileobj.close()
836         return r
837     except IOError:
838         if verbose:
839             logger.error("couldn't read translation file %s", filename)
840         return None
841
842 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
843     logger = logging.getLogger('i18n')
844     if verbose:
845         logger.info('loading translation file for language %s', lang)
846     if context is None:
847         context = {}
848     pool = pooler.get_pool(db_name)
849     lang_obj = pool.get('res.lang')
850     trans_obj = pool.get('ir.translation')
851     model_data_obj = pool.get('ir.model.data')
852     iso_lang = tools.get_iso_codes(lang)
853     try:
854         uid = 1
855         cr = pooler.get_db(db_name).cursor()
856         ids = lang_obj.search(cr, uid, [('code','=', lang)])
857
858         if not ids:
859             # lets create the language with locale information
860             fail = True
861             for ln in get_locales(lang):
862                 try:
863                     locale.setlocale(locale.LC_ALL, str(ln))
864                     fail = False
865                     break
866                 except locale.Error:
867                     continue
868             if fail:
869                 lc = locale.getdefaultlocale()[0]
870                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
871                 logger.warning(msg, lang, lc)
872
873             if not lang_name:
874                 lang_name = tools.get_languages().get(lang, lang)
875
876             def fix_xa0(s):
877                 if s == '\xa0':
878                     return '\xc2\xa0'
879                 return s
880
881             lang_info = {
882                 'code': lang,
883                 'iso_code': iso_lang,
884                 'name': lang_name,
885                 'translatable': 1,
886                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
887                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
888                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
889                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
890             }
891
892             try:
893                 lang_obj.create(cr, uid, lang_info)
894             finally:
895                 resetlocale()
896
897
898         # now, the serious things: we read the language file
899         fileobj.seek(0)
900         if fileformat == 'csv':
901             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
902             # read the first line of the file (it contains columns titles)
903             for row in reader:
904                 f = row
905                 break
906         elif fileformat == 'po':
907             reader = TinyPoFile(fileobj)
908             f = ['type', 'name', 'res_id', 'src', 'value']
909         else:
910             logger.error('Bad file format: %s', fileformat)
911             raise Exception(_('Bad file format'))
912
913         # read the rest of the file
914         line = 1
915         for row in reader:
916             line += 1
917             # skip empty rows and rows where the translation field (=last fiefd) is empty
918             #if (not row) or (not row[-1]):
919             #    continue
920
921             # dictionary which holds values for this line of the csv file
922             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
923             #  'src': ..., 'value': ...}
924             dic = {'lang': lang}
925             for i in range(len(f)):
926                 if f[i] in ('module',):
927                     continue
928                 dic[f[i]] = row[i]
929
930             try:
931                 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
932             except:
933                 model_data_ids = model_data_obj.search(cr, uid, [
934                     ('model', '=', dic['name'].split(',')[0]),
935                     ('module', '=', dic['res_id'].split('.', 1)[0]),
936                     ('name', '=', dic['res_id'].split('.', 1)[1]),
937                     ])
938                 if model_data_ids:
939                     dic['res_id'] = model_data_obj.browse(cr, uid,
940                             model_data_ids[0]).res_id
941                 else:
942                     dic['res_id'] = False
943
944             args = [
945                 ('lang', '=', lang),
946                 ('type', '=', dic['type']),
947                 ('name', '=', dic['name']),
948                 ('src', '=', dic['src']),
949             ]
950             if dic['type'] == 'model':
951                 args.append(('res_id', '=', dic['res_id']))
952             ids = trans_obj.search(cr, uid, args)
953             if ids:
954                 if context.get('overwrite'):
955                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
956             else:
957                 trans_obj.create(cr, uid, dic)
958             cr.commit()
959         cr.close()
960         if verbose:
961             logger.info("translation file loaded succesfully")
962     except IOError:
963         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
964         logger.exception("couldn't read translation file %s", filename)
965
966 def get_locales(lang=None):
967     if lang is None:
968         lang = locale.getdefaultlocale()[0]
969
970     if os.name == 'nt':
971         lang = _LOCALE2WIN32.get(lang, lang)
972
973     def process(enc):
974         ln = locale._build_localename((lang, enc))
975         yield ln
976         nln = locale.normalize(ln)
977         if nln != ln:
978             yield nln
979
980     for x in process('utf8'): yield x
981
982     prefenc = locale.getpreferredencoding()
983     if prefenc:
984         for x in process(prefenc): yield x
985
986         prefenc = {
987             'latin1': 'latin9',
988             'iso-8859-1': 'iso8859-15',
989             'cp1252': '1252',
990         }.get(prefenc.lower())
991         if prefenc:
992             for x in process(prefenc): yield x
993
994     yield lang
995
996
997
998 def resetlocale():
999     # locale.resetlocale is bugged with some locales.
1000     for ln in get_locales():
1001         try:
1002             return locale.setlocale(locale.LC_ALL, ln)
1003         except locale.Error:
1004             continue
1005
1006 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1007