rml2pdf: fix logging of exceptions.
[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 re
30 import logging
31 import tarfile
32 import tempfile
33 from os.path import join
34 import logging
35
36 from datetime import datetime
37 from lxml import etree
38
39 import tools, pooler
40 import netsvc
41 from tools.misc import UpdateableStr
42
43 _LOCALE2WIN32 = {
44     'af_ZA': 'Afrikaans_South Africa',
45     'sq_AL': 'Albanian_Albania',
46     'ar_SA': 'Arabic_Saudi Arabia',
47     'eu_ES': 'Basque_Spain',
48     'be_BY': 'Belarusian_Belarus',
49     'bs_BA': 'Serbian (Latin)',
50     'bg_BG': 'Bulgarian_Bulgaria',
51     'ca_ES': 'Catalan_Spain',
52     'hr_HR': 'Croatian_Croatia',
53     'zh_CN': 'Chinese_China',
54     'zh_TW': 'Chinese_Taiwan',
55     'cs_CZ': 'Czech_Czech Republic',
56     'da_DK': 'Danish_Denmark',
57     'nl_NL': 'Dutch_Netherlands',
58     'et_EE': 'Estonian_Estonia',
59     'fa_IR': 'Farsi_Iran',
60     'ph_PH': 'Filipino_Philippines',
61     'fi_FI': 'Finnish_Finland',
62     'fr_FR': 'French_France',
63     'fr_BE': 'French_France',
64     'fr_CH': 'French_France',
65     'fr_CA': 'French_France',
66     'ga': 'Scottish Gaelic',
67     'gl_ES': 'Galician_Spain',
68     'ka_GE': 'Georgian_Georgia',
69     'de_DE': 'German_Germany',
70     'el_GR': 'Greek_Greece',
71     'gu': 'Gujarati_India',
72     'he_IL': 'Hebrew_Israel',
73     'hi_IN': 'Hindi',
74     'hu': 'Hungarian_Hungary',
75     'is_IS': 'Icelandic_Iceland',
76     'id_ID': 'Indonesian_indonesia',
77     'it_IT': 'Italian_Italy',
78     'ja_JP': 'Japanese_Japan',
79     'kn_IN': 'Kannada',
80     'km_KH': 'Khmer',
81     'ko_KR': 'Korean_Korea',
82     'lo_LA': 'Lao_Laos',
83     'lt_LT': 'Lithuanian_Lithuania',
84     'lat': 'Latvian_Latvia',
85     'ml_IN': 'Malayalam_India',
86     'id_ID': 'Indonesian_indonesia',
87     'mi_NZ': 'Maori',
88     'mn': 'Cyrillic_Mongolian',
89     'no_NO': 'Norwegian_Norway',
90     'nn_NO': 'Norwegian-Nynorsk_Norway',
91     'pl': 'Polish_Poland',
92     'pt_PT': 'Portuguese_Portugal',
93     'pt_BR': 'Portuguese_Brazil',
94     'ro_RO': 'Romanian_Romania',
95     'ru_RU': 'Russian_Russia',
96     'mi_NZ': 'Maori',
97     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
98     'sk_SK': 'Slovak_Slovakia',
99     'sl_SI': 'Slovenian_Slovenia',
100     #should find more specific locales for spanish countries,
101     #but better than nothing
102     'es_AR': 'Spanish_Spain',
103     'es_BO': 'Spanish_Spain',
104     'es_CL': 'Spanish_Spain',
105     'es_CO': 'Spanish_Spain',
106     'es_CR': 'Spanish_Spain',
107     'es_DO': 'Spanish_Spain',
108     'es_EC': 'Spanish_Spain',
109     'es_ES': 'Spanish_Spain',
110     'es_GT': 'Spanish_Spain',
111     'es_HN': 'Spanish_Spain',
112     'es_MX': 'Spanish_Spain',
113     'es_NI': 'Spanish_Spain',
114     'es_PA': 'Spanish_Spain',
115     'es_PE': 'Spanish_Spain',
116     'es_PR': 'Spanish_Spain',
117     'es_PY': 'Spanish_Spain',
118     'es_SV': 'Spanish_Spain',
119     'es_UY': 'Spanish_Spain',
120     'es_VE': 'Spanish_Spain',
121     'sv_SE': 'Swedish_Sweden',
122     'ta_IN': 'English_Australia',
123     'th_TH': 'Thai_Thailand',
124     'mi_NZ': 'Maori',
125     'tr_TR': 'Turkish_Turkey',
126     'uk_UA': 'Ukrainian_Ukraine',
127     'vi_VN': 'Vietnamese_Viet Nam',
128     'tlh_TLH': 'Klingon',
129
130 }
131
132
133 class UNIX_LINE_TERMINATOR(csv.excel):
134     lineterminator = '\n'
135
136 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
137
138 #
139 # Warning: better use self.pool.get('ir.translation')._get_source if you can
140 #
141 def translate(cr, name, source_type, lang, source=None):
142     if source and name:
143         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))
144     elif name:
145         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
146     elif source:
147         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
148     res_trans = cr.fetchone()
149     res = res_trans and res_trans[0] or False
150     return res
151
152 logger = logging.getLogger('translate')
153
154 class GettextAlias(object):
155
156     def _get_cr(self, frame):
157         is_new_cr = False
158         cr = frame.f_locals.get('cr')
159         if not cr:
160             s = frame.f_locals.get('self', {})
161             cr = getattr(s, 'cr', False)
162             if not cr:
163                 if frame.f_globals.get('pooler', False):
164                     # TODO: we should probably get rid of the 'is_new_cr' case: no cr in locals -> no translation for you
165                     dbs = frame.f_globals['pooler'].pool_dic.keys()
166                     if len(dbs) == 1:
167                         cr = pooler.get_db(dbs[0]).cursor()
168                         is_new_cr = True
169         return cr, is_new_cr
170     
171     def _get_lang(self, frame):
172         lang = frame.f_locals.get('context', {}).get('lang', False)
173         if not lang:
174             args = frame.f_locals.get('args', False)
175             if args:
176                 lang = args[-1].get('lang', False)
177             if not lang:
178                 s = frame.f_locals.get('self', {})
179                 c = getattr(s, 'localcontext', {})
180                 lang = c.get('lang', False)
181         return lang
182     
183     def __call__(self, source):
184         is_new_cr = False
185         res = source
186         try:
187             frame = inspect.stack()[1][0]
188             cr, is_new_cr = self._get_cr(frame)
189             lang = self._get_lang(frame)
190             if lang and cr:
191                 cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
192                 res_trans = cr.fetchone()
193                 res = res_trans and res_trans[0] or source
194         except Exception:
195             logger.debug('translation went wrong for string %s', repr(source))
196         finally:
197             if is_new_cr:
198                 cr.close()
199         return res
200
201 _ = GettextAlias()
202
203
204 def quote(s):
205     """Returns quoted PO term string, with special PO characters escaped"""
206     assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines"
207     return '"%s"' % s.replace('\\','\\\\') \
208                      .replace('"','\\"') \
209                      .replace('\n', '\\n"\n"')
210
211 re_escaped_char = re.compile(r"(\\.)")
212 re_escaped_replacements = {'n': '\n', }
213
214 def _sub_replacement(match_obj):
215     return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
216
217 def unquote(str):
218     """Returns unquoted PO term string, with special PO characters unescaped"""
219     return re_escaped_char.sub(_sub_replacement, str[1:-1])
220
221 # class to handle po files
222 class TinyPoFile(object):
223     def __init__(self, buffer):
224         self.logger = logging.getLogger('i18n')
225         self.buffer = buffer
226
227     def warn(self, msg):
228         self.logger.warning(msg)
229
230     def __iter__(self):
231         self.buffer.seek(0)
232         self.lines = self._get_lines()
233         self.lines_count = len(self.lines);
234
235         self.first = True
236         self.tnrs= []
237         return self
238
239     def _get_lines(self):
240         lines = self.buffer.readlines()
241         # remove the BOM (Byte Order Mark):
242         if len(lines):
243             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
244
245         lines.append('') # ensure that the file ends with at least an empty line
246         return lines
247
248     def cur_line(self):
249         return (self.lines_count - len(self.lines))
250
251     def next(self):
252         type = name = res_id = source = trad = None
253
254         if self.tnrs:
255             type, name, res_id, source, trad = self.tnrs.pop(0)
256         else:
257             tmp_tnrs = []
258             line = None
259             fuzzy = False
260             while (not line):
261                 if 0 == len(self.lines):
262                     raise StopIteration()
263                 line = self.lines.pop(0).strip()
264             while line.startswith('#'):
265                 if line.startswith('#~ '):
266                     break
267                 if line.startswith('#:'):
268                     if ' ' in line[2:].strip():
269                         for lpart in line[2:].strip().split(' '):
270                             tmp_tnrs.append(lpart.strip().split(':',2))
271                     else:
272                         tmp_tnrs.append( line[2:].strip().split(':',2) )
273                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
274                     fuzzy = True
275                 line = self.lines.pop(0).strip()
276             while not line:
277                 # allow empty lines between comments and msgid
278                 line = self.lines.pop(0).strip()
279             if line.startswith('#~ '):
280                 while line.startswith('#~ ') or not line.strip():
281                     if 0 == len(self.lines):
282                         raise StopIteration()
283                     line = self.lines.pop(0)
284                 # This has been a deprecated entry, don't return anything
285                 return self.next()
286
287             if not line.startswith('msgid'):
288                 raise Exception("malformed file: bad line: %s" % line)
289             source = unquote(line[6:])
290             line = self.lines.pop(0).strip()
291             if not source and self.first:
292                 # if the source is "" and it's the first msgid, it's the special
293                 # msgstr with the informations about the traduction and the
294                 # traductor; we skip it
295                 self.tnrs = []
296                 while line:
297                     line = self.lines.pop(0).strip()
298                 return self.next()
299
300             while not line.startswith('msgstr'):
301                 if not line:
302                     raise Exception('malformed file at %d'% self.cur_line())
303                 source += unquote(line)
304                 line = self.lines.pop(0).strip()
305
306             trad = unquote(line[7:])
307             line = self.lines.pop(0).strip()
308             while line:
309                 trad += unquote(line)
310                 line = self.lines.pop(0).strip()
311
312             if tmp_tnrs and not fuzzy:
313                 type, name, res_id = tmp_tnrs.pop(0)
314                 for t, n, r in tmp_tnrs:
315                     self.tnrs.append((t, n, r, source, trad))
316
317         self.first = False
318
319         if name is None:
320             if not fuzzy:
321                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', 
322                         self.cur_line(), source[:30])
323             return self.next()
324         return type, name, res_id, source, trad
325
326     def write_infos(self, modules):
327         import release
328         self.buffer.write("# Translation of %(project)s.\n" \
329                           "# This file contains the translation of the following modules:\n" \
330                           "%(modules)s" \
331                           "#\n" \
332                           "msgid \"\"\n" \
333                           "msgstr \"\"\n" \
334                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
335                           '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
336                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
337                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
338                           '''"Last-Translator: <>\\n"\n''' \
339                           '''"Language-Team: \\n"\n'''   \
340                           '''"MIME-Version: 1.0\\n"\n''' \
341                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
342                           '''"Content-Transfer-Encoding: \\n"\n'''       \
343                           '''"Plural-Forms: \\n"\n'''    \
344                           "\n"
345
346                           % { 'project': release.description,
347                               'version': release.version,
348                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
349                               'bugmail': release.support_email,
350                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
351                             }
352                           )
353
354     def write(self, modules, tnrs, source, trad):
355
356         plurial = len(modules) > 1 and 's' or ''
357         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
358
359
360         code = False
361         for typy, name, res_id in tnrs:
362             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
363             if typy == 'code':
364                 code = True
365
366         if code:
367             # only strings in python code are python formated
368             self.buffer.write("#, python-format\n")
369
370         if not isinstance(trad, unicode):
371             trad = unicode(trad, 'utf8')
372         if not isinstance(source, unicode):
373             source = unicode(source, 'utf8')
374
375         msg = "msgid %s\n"      \
376               "msgstr %s\n\n"   \
377                   % (quote(source), quote(trad))
378         self.buffer.write(msg.encode('utf8'))
379
380
381 # Methods to export the translation file
382
383 def trans_export(lang, modules, buffer, format, dbname=None):
384
385     def _process(format, modules, rows, buffer, lang, newlang):
386         if format == 'csv':
387             writer=csv.writer(buffer, 'UNIX')
388             for row in rows:
389                 writer.writerow(row)
390         elif format == 'po':
391             rows.pop(0)
392             writer = tools.TinyPoFile(buffer)
393             writer.write_infos(modules)
394
395             # we now group the translations by source. That means one translation per source.
396             grouped_rows = {}
397             for module, type, name, res_id, src, trad in rows:
398                 row = grouped_rows.setdefault(src, {})
399                 row.setdefault('modules', set()).add(module)
400                 if ('translation' not in row) or (not row['translation']):
401                     row['translation'] = trad
402                 row.setdefault('tnrs', []).append((type, name, res_id))
403
404             for src, row in grouped_rows.items():
405                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
406
407         elif format == 'tgz':
408             rows.pop(0)
409             rows_by_module = {}
410             for row in rows:
411                 module = row[0]
412                 rows_by_module.setdefault(module, []).append(row)
413
414             tmpdir = tempfile.mkdtemp()
415             for mod, modrows in rows_by_module.items():
416                 tmpmoddir = join(tmpdir, mod, 'i18n')
417                 os.makedirs(tmpmoddir)
418                 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
419                 buf = file(join(tmpmoddir, pofilename), 'w')
420                 _process('po', [mod], modrows, buf, lang, newlang)
421                 buf.close()
422
423             tar = tarfile.open(fileobj=buffer, mode='w|gz')
424             tar.add(tmpdir, '')
425             tar.close()
426
427         else:
428             raise Exception(_('Bad file format'))
429
430     newlang = not bool(lang)
431     if newlang:
432         lang = 'en_US'
433     trans = trans_generate(lang, modules, dbname)
434     if newlang and format!='csv':
435         for trx in trans:
436             trx[-1] = ''
437     modules = set([t[0] for t in trans[1:]])
438     _process(format, modules, trans, buffer, lang, newlang)
439     del trans
440
441
442 def trans_parse_xsl(de):
443     res = []
444     for n in de:
445         if n.get("t"):
446             for m in [j for j in n if j.text]:
447                 l = m.text.strip().replace('\n',' ')
448                 if len(l):
449                     res.append(l.encode("utf8"))
450         res.extend(trans_parse_xsl(n))
451     return res
452
453 def trans_parse_rml(de):
454     res = []
455     for n in de:
456         for m in [j for j in n if j.text]:
457             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
458             for s in string_list:
459                 if s:
460                     res.append(s.encode("utf8"))
461         res.extend(trans_parse_rml(n))
462     return res
463
464 def trans_parse_view(de):
465     res = []
466     if de.get("string"):
467         res.append(de.get('string').encode("utf8"))
468     if de.get("sum"):
469         res.append(de.get('sum').encode("utf8"))
470     for n in de:
471         res.extend(trans_parse_view(n))
472     return res
473
474 # tests whether an object is in a list of modules
475 def in_modules(object_name, modules):
476     if 'all' in modules:
477         return True
478
479     module_dict = {
480         'ir': 'base',
481         'res': 'base',
482         'workflow': 'base',
483     }
484     module = object_name.split('.')[0]
485     module = module_dict.get(module, module)
486     return module in modules
487
488 def trans_generate(lang, modules, dbname=None):
489     logger = logging.getLogger('i18n')
490     if not dbname:
491         dbname=tools.config['db_name']
492         if not modules:
493             modules = ['all']
494
495     pool = pooler.get_pool(dbname)
496     trans_obj = pool.get('ir.translation')
497     model_data_obj = pool.get('ir.model.data')
498     cr = pooler.get_db(dbname).cursor()
499     uid = 1
500     l = pool.obj_pool.items()
501     l.sort()
502
503     query = 'SELECT name, model, res_id, module'    \
504             '  FROM ir_model_data'
505             
506     query_models = """SELECT m.id, m.model, imd.module 
507             FROM ir_model AS m, ir_model_data AS imd 
508             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
509
510     if 'all_installed' in modules:
511         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
512         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
513     query_param = None
514     if 'all' not in modules:
515         query += ' WHERE module IN %s'
516         query_models += ' AND imd.module in %s'
517         query_param = (tuple(modules),)
518     query += ' ORDER BY module, model, name'
519     query_models += ' ORDER BY module, model'
520
521     cr.execute(query, query_param)
522
523     _to_translate = []
524     def push_translation(module, type, name, id, source):
525         tuple = (module, source, name, id, type)
526         if source and tuple not in _to_translate:
527             _to_translate.append(tuple)
528
529     def encode(s):
530         if isinstance(s, unicode):
531             return s.encode('utf8')
532         return s
533
534     for (xml_name,model,res_id,module) in cr.fetchall():
535         module = encode(module)
536         model = encode(model)
537         xml_name = "%s.%s" % (module, encode(xml_name))
538
539         if not pool.get(model):
540             logger.error("Unable to find object %r", model)
541             continue
542
543         exists = pool.get(model).exists(cr, uid, res_id)
544         if not exists:
545             logger.warning("Unable to find object %r with id %d", model, res_id)
546             continue
547         obj = pool.get(model).browse(cr, uid, res_id)
548
549         if model=='ir.ui.view':
550             d = etree.XML(encode(obj.arch))
551             for t in trans_parse_view(d):
552                 push_translation(module, 'view', encode(obj.model), 0, t)
553         elif model=='ir.actions.wizard':
554             service_name = 'wizard.'+encode(obj.wiz_name)
555             if netsvc.Service._services.get(service_name):
556                 obj2 = netsvc.Service._services[service_name]
557                 for state_name, state_def in obj2.states.iteritems():
558                     if 'result' in state_def:
559                         result = state_def['result']
560                         if result['type'] != 'form':
561                             continue
562                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
563
564                         def_params = {
565                             'string': ('wizard_field', lambda s: [encode(s)]),
566                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
567                             'help': ('help', lambda s: [encode(s)]),
568                         }
569
570                         # export fields
571                         if not result.has_key('fields'):
572                             logger.warning("res has no fields: %r", result)
573                             continue
574                         for field_name, field_def in result['fields'].iteritems():
575                             res_name = name + ',' + field_name
576
577                             for fn in def_params:
578                                 if fn in field_def:
579                                     transtype, modifier = def_params[fn]
580                                     for val in modifier(field_def[fn]):
581                                         push_translation(module, transtype, res_name, 0, val)
582
583                         # export arch
584                         arch = result['arch']
585                         if arch and not isinstance(arch, UpdateableStr):
586                             d = etree.XML(arch)
587                             for t in trans_parse_view(d):
588                                 push_translation(module, 'wizard_view', name, 0, t)
589
590                         # export button labels
591                         for but_args in result['state']:
592                             button_name = but_args[0]
593                             button_label = but_args[1]
594                             res_name = name + ',' + button_name
595                             push_translation(module, 'wizard_button', res_name, 0, button_label)
596
597         elif model=='ir.model.fields':
598             try:
599                 field_name = encode(obj.name)
600             except AttributeError, exc:
601                 logger.error("name error in %s: %s", xml_name, str(exc))
602                 continue
603             objmodel = pool.get(obj.model)
604             if not objmodel or not field_name in objmodel._columns:
605                 continue
606             field_def = objmodel._columns[field_name]
607
608             name = "%s,%s" % (encode(obj.model), field_name)
609             push_translation(module, 'field', name, 0, encode(field_def.string))
610
611             if field_def.help:
612                 push_translation(module, 'help', name, 0, encode(field_def.help))
613
614             if field_def.translate:
615                 ids = objmodel.search(cr, uid, [])
616                 obj_values = objmodel.read(cr, uid, ids, [field_name])
617                 for obj_value in obj_values:
618                     res_id = obj_value['id']
619                     if obj.name in ('ir.model', 'ir.ui.menu'):
620                         res_id = 0
621                     model_data_ids = model_data_obj.search(cr, uid, [
622                         ('model', '=', model),
623                         ('res_id', '=', res_id),
624                         ])
625                     if not model_data_ids:
626                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
627
628             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
629                 for dummy, val in field_def.selection:
630                     push_translation(module, 'selection', name, 0, encode(val))
631
632         elif model=='ir.actions.report.xml':
633             name = encode(obj.report_name)
634             fname = ""
635             if obj.report_rml:
636                 fname = obj.report_rml
637                 parse_func = trans_parse_rml
638                 report_type = "report"
639             elif obj.report_xsl:
640                 fname = obj.report_xsl
641                 parse_func = trans_parse_xsl
642                 report_type = "xsl"
643             if fname and obj.report_type in ('pdf', 'xsl'):
644                 try:
645                     d = etree.parse(tools.file_open(fname))
646                     for t in parse_func(d.iter()):
647                         push_translation(module, report_type, name, 0, t)
648                 except (IOError, etree.XMLSyntaxError):
649                     logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
650
651         for field_name,field_def in obj._table._columns.items():
652             if field_def.translate:
653                 name = model + "," + field_name
654                 try:
655                     trad = getattr(obj, field_name) or ''
656                 except:
657                     trad = ''
658                 push_translation(module, 'model', name, xml_name, encode(trad))
659
660         # End of data for ir.model.data query results
661
662     cr.execute(query_models, query_param)
663
664     def push_constraint_msg(module, term_type, model, msg):
665         # Check presence of __call__ directly instead of using
666         # callable() because it will be deprecated as of Python 3.0
667         if not hasattr(msg, '__call__'):
668             push_translation(module, term_type, model, 0, encode(msg))
669
670     for (model_id, model, module) in cr.fetchall():
671         module = encode(module)
672         model = encode(model)
673
674         model_obj = pool.get(model)
675
676         if not model_obj:
677             logging.getLogger("i18n").error("Unable to find object %r", model)
678             continue
679
680         for constraint in getattr(model_obj, '_constraints', []):
681             push_constraint_msg(module, 'constraint', model, constraint[1])
682
683         for constraint in getattr(model_obj, '_sql_constraints', []):
684             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
685
686     # parse source code for _() calls
687     def get_module_from_path(path, mod_paths=None):
688         if not mod_paths:
689             # First, construct a list of possible paths
690             def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
691             ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
692             mod_paths=[def_path]
693             for adp in ad_paths:
694                 mod_paths.append(adp)
695                 if not os.path.isabs(adp):
696                     mod_paths.append(adp)
697                 elif adp.startswith(def_path):
698                     mod_paths.append(adp[len(def_path)+1:])
699         for mp in mod_paths:
700             if path.startswith(mp) and (os.path.dirname(path) != mp):
701                 path = path[len(mp)+1:]
702                 return path.split(os.path.sep)[0]
703         return 'base'   # files that are not in a module are considered as being in 'base' module
704
705     modobj = pool.get('ir.module.module')
706     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
707     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
708
709     root_path = os.path.join(tools.config['root_path'], 'addons')
710
711     apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
712     if root_path in apaths:
713         path_list = apaths
714     else :
715         path_list = [root_path,] + apaths
716
717     logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning modules at paths: %s" % (' '.join(path_list),))
718
719     mod_paths = []
720     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
721     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
722     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
723     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
724
725     def export_code_terms_from_file(fname, path, root, terms_type):
726         fabsolutepath = join(root, fname)
727         frelativepath = fabsolutepath[len(path):]
728         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
729         is_mod_installed = module in installed_modules
730         if (('all' in modules) or (module in modules)) and is_mod_installed:
731             logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning code of %s at module: %s" % (frelativepath, module))
732             code_string = tools.file_open(fabsolutepath, subdir='').read()
733             if module in installed_modules:
734                 frelativepath = str("addons" + frelativepath)
735             ite = re_dquotes.finditer(code_string)
736             for i in ite:
737                 src = i.group(1)
738                 if src.startswith('""'):
739                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
740                     src = src[2:-2]
741                 else:
742                     src = join_dquotes.sub(r'\1', src)
743                 # now, since we did a binary read of a python source file, we
744                 # have to expand pythonic escapes like the interpreter does.
745                 src = src.decode('string_escape')
746                 push_translation(module, terms_type, frelativepath, 0, encode(src))
747             ite = re_quotes.finditer(code_string)
748             for i in ite:
749                 src = i.group(1)
750                 if src.startswith("''"):
751                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
752                     src = src[2:-2]
753                 else:
754                     src = join_quotes.sub(r'\1', src)
755                 src = src.decode('string_escape')
756                 push_translation(module, terms_type, frelativepath, 0, encode(src))
757
758     for path in path_list:
759         logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning files of modules at %s" % path)
760         for root, dummy, files in tools.osutil.walksymlinks(path):
761             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
762                 export_code_terms_from_file(fname, path, root, 'code')
763             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
764                 export_code_terms_from_file(fname, path, root, 'report')
765
766
767     out = [["module","type","name","res_id","src","value"]] # header
768     _to_translate.sort()
769     # translate strings marked as to be translated
770     for module, source, name, id, type in _to_translate:
771         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
772         out.append([module, type, name, id, source, encode(trans) or ''])
773
774     cr.close()
775     return out
776
777 def trans_load(db_name, filename, lang, strict=False, verbose=True, context={}):
778     logger = logging.getLogger('i18n')
779     try:
780         fileobj = open(filename,'r')
781         logger.info("loading %s", filename)
782         fileformat = os.path.splitext(filename)[-1][1:].lower()
783         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
784         fileobj.close()
785         return r
786     except IOError:
787         if verbose:
788             logger.error("couldn't read translation file %s", filename)
789         return None
790
791 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context={}):
792     logger = logging.getLogger('i18n')
793     if verbose:
794         logger.info('loading translation file for language %s', lang)
795     pool = pooler.get_pool(db_name)
796     lang_obj = pool.get('res.lang')
797     trans_obj = pool.get('ir.translation')
798     model_data_obj = pool.get('ir.model.data')
799     iso_lang = tools.get_iso_codes(lang)
800     try:
801         uid = 1
802         cr = pooler.get_db(db_name).cursor()
803         ids = lang_obj.search(cr, uid, [('code','=', lang)])
804
805         if not ids:
806             # lets create the language with locale information
807             fail = True
808             for ln in get_locales(lang):
809                 try:
810                     locale.setlocale(locale.LC_ALL, str(ln))
811                     fail = False
812                     break
813                 except locale.Error:
814                     continue
815             if fail:
816                 lc = locale.getdefaultlocale()[0]
817                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
818                 logger.warning(msg, lang, lc)
819
820             if not lang_name:
821                 lang_name = tools.get_languages().get(lang, lang)
822
823             def fix_xa0(s):
824                 if s == '\xa0':
825                     return '\xc2\xa0'
826                 return s
827
828             lang_info = {
829                 'code': lang,
830                 'iso_code': iso_lang,
831                 'name': lang_name,
832                 'translatable': 1,
833                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
834                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
835                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
836                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
837             }
838
839             try:
840                 lang_obj.create(cr, uid, lang_info)
841             finally:
842                 resetlocale()
843
844
845         # now, the serious things: we read the language file
846         fileobj.seek(0)
847         if fileformat == 'csv':
848             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
849             # read the first line of the file (it contains columns titles)
850             for row in reader:
851                 f = row
852                 break
853         elif fileformat == 'po':
854             reader = TinyPoFile(fileobj)
855             f = ['type', 'name', 'res_id', 'src', 'value']
856         else:
857             logger.error('Bad file format: %s', fileformat)
858             raise Exception(_('Bad file format'))
859
860         # read the rest of the file
861         line = 1
862         for row in reader:
863             line += 1
864             # skip empty rows and rows where the translation field (=last fiefd) is empty
865             #if (not row) or (not row[-1]):
866             #    continue
867
868             # dictionary which holds values for this line of the csv file
869             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
870             #  'src': ..., 'value': ...}
871             dic = {'lang': lang}
872             for i in range(len(f)):
873                 if f[i] in ('module',):
874                     continue
875                 dic[f[i]] = row[i]
876
877             try:
878                 dic['res_id'] = int(dic['res_id'])
879             except:
880                 model_data_ids = model_data_obj.search(cr, uid, [
881                     ('model', '=', dic['name'].split(',')[0]),
882                     ('module', '=', dic['res_id'].split('.', 1)[0]),
883                     ('name', '=', dic['res_id'].split('.', 1)[1]),
884                     ])
885                 if model_data_ids:
886                     dic['res_id'] = model_data_obj.browse(cr, uid,
887                             model_data_ids[0]).res_id
888                 else:
889                     dic['res_id'] = False
890
891             if dic['type'] == 'model' and not strict:
892                 (model, field) = dic['name'].split(',')
893
894                 # get the ids of the resources of this model which share
895                 # the same source
896                 obj = pool.get(model)
897                 if obj:
898                     if field not in obj.fields_get_keys(cr, uid):
899                         continue
900                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
901
902                     # if the resource id (res_id) is in that list, use it,
903                     # otherwise use the whole list
904                     if not ids:
905                         ids = []
906                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
907                     for id in ids:
908                         dic['res_id'] = id
909                         ids = trans_obj.search(cr, uid, [
910                             ('lang', '=', lang),
911                             ('type', '=', dic['type']),
912                             ('name', '=', dic['name']),
913                             ('src', '=', dic['src']),
914                             ('res_id', '=', dic['res_id'])
915                         ])
916                         if ids:
917                             if context.get('overwrite', False):
918                                 trans_obj.write(cr, uid, ids, {'value': dic['value']})
919                         else:
920                             trans_obj.create(cr, uid, dic)
921             else:
922                 ids = trans_obj.search(cr, uid, [
923                     ('lang', '=', lang),
924                     ('type', '=', dic['type']),
925                     ('name', '=', dic['name']),
926                     ('src', '=', dic['src'])
927                 ])
928                 if ids:
929                     if context.get('overwrite', False):
930                         trans_obj.write(cr, uid, ids, {'value': dic['value']})
931                 else:
932                     trans_obj.create(cr, uid, dic)
933             cr.commit()
934         cr.close()
935         if verbose:
936             logger.info("translation file loaded succesfully")
937     except IOError:
938         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
939         logger.exception("couldn't read translation file %s", filename)
940
941 def get_locales(lang=None):
942     if lang is None:
943         lang = locale.getdefaultlocale()[0]
944
945     if os.name == 'nt':
946         lang = _LOCALE2WIN32.get(lang, lang)
947
948     def process(enc):
949         ln = locale._build_localename((lang, enc))
950         yield ln
951         nln = locale.normalize(ln)
952         if nln != ln:
953             yield nln
954
955     for x in process('utf8'): yield x
956
957     prefenc = locale.getpreferredencoding()
958     if prefenc:
959         for x in process(prefenc): yield x
960
961         prefenc = {
962             'latin1': 'latin9',
963             'iso-8859-1': 'iso8859-15',
964             'cp1252': '1252',
965         }.get(prefenc.lower())
966         if prefenc:
967             for x in process(prefenc): yield x
968
969     yield lang
970
971
972
973 def resetlocale():
974     # locale.resetlocale is bugged with some locales.
975     for ln in get_locales():
976         try:
977             return locale.setlocale(locale.LC_ALL, ln)
978         except locale.Error:
979             continue
980
981 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
982