translate: fix usage of TinyPoFile.warn() [Bug 692845]
[odoo/odoo.git] / bin / tools / translate.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import codecs
23 import csv
24 import fnmatch
25 import inspect
26 import itertools
27 import locale
28 import os
29 import pooler
30 import re
31 import logging
32 import tarfile
33 import tempfile
34 import threading
35 from os.path import join
36 import logging
37
38 from datetime import datetime
39 from lxml import etree
40
41 import tools, pooler
42 import netsvc
43 from tools.misc import UpdateableStr
44
45 _LOCALE2WIN32 = {
46     'af_ZA': 'Afrikaans_South Africa',
47     'sq_AL': 'Albanian_Albania',
48     'ar_SA': 'Arabic_Saudi Arabia',
49     'eu_ES': 'Basque_Spain',
50     'be_BY': 'Belarusian_Belarus',
51     'bs_BA': 'Serbian (Latin)',
52     'bg_BG': 'Bulgarian_Bulgaria',
53     'ca_ES': 'Catalan_Spain',
54     'hr_HR': 'Croatian_Croatia',
55     'zh_CN': 'Chinese_China',
56     'zh_TW': 'Chinese_Taiwan',
57     'cs_CZ': 'Czech_Czech Republic',
58     'da_DK': 'Danish_Denmark',
59     'nl_NL': 'Dutch_Netherlands',
60     'et_EE': 'Estonian_Estonia',
61     'fa_IR': 'Farsi_Iran',
62     'ph_PH': 'Filipino_Philippines',
63     'fi_FI': 'Finnish_Finland',
64     'fr_FR': 'French_France',
65     'fr_BE': 'French_France',
66     'fr_CH': 'French_France',
67     'fr_CA': 'French_France',
68     'ga': 'Scottish Gaelic',
69     'gl_ES': 'Galician_Spain',
70     'ka_GE': 'Georgian_Georgia',
71     'de_DE': 'German_Germany',
72     'el_GR': 'Greek_Greece',
73     'gu': 'Gujarati_India',
74     'he_IL': 'Hebrew_Israel',
75     'hi_IN': 'Hindi',
76     'hu': 'Hungarian_Hungary',
77     'is_IS': 'Icelandic_Iceland',
78     'id_ID': 'Indonesian_indonesia',
79     'it_IT': 'Italian_Italy',
80     'ja_JP': 'Japanese_Japan',
81     'kn_IN': 'Kannada',
82     'km_KH': 'Khmer',
83     'ko_KR': 'Korean_Korea',
84     'lo_LA': 'Lao_Laos',
85     'lt_LT': 'Lithuanian_Lithuania',
86     'lat': 'Latvian_Latvia',
87     'ml_IN': 'Malayalam_India',
88     'id_ID': 'Indonesian_indonesia',
89     'mi_NZ': 'Maori',
90     'mn': 'Cyrillic_Mongolian',
91     'no_NO': 'Norwegian_Norway',
92     'nn_NO': 'Norwegian-Nynorsk_Norway',
93     'pl': 'Polish_Poland',
94     'pt_PT': 'Portuguese_Portugal',
95     'pt_BR': 'Portuguese_Brazil',
96     'ro_RO': 'Romanian_Romania',
97     'ru_RU': 'Russian_Russia',
98     'mi_NZ': 'Maori',
99     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
100     'sk_SK': 'Slovak_Slovakia',
101     'sl_SI': 'Slovenian_Slovenia',
102     #should find more specific locales for spanish countries,
103     #but better than nothing
104     'es_AR': 'Spanish_Spain',
105     'es_BO': 'Spanish_Spain',
106     'es_CL': 'Spanish_Spain',
107     'es_CO': 'Spanish_Spain',
108     'es_CR': 'Spanish_Spain',
109     'es_DO': 'Spanish_Spain',
110     'es_EC': 'Spanish_Spain',
111     'es_ES': 'Spanish_Spain',
112     'es_GT': 'Spanish_Spain',
113     'es_HN': 'Spanish_Spain',
114     'es_MX': 'Spanish_Spain',
115     'es_NI': 'Spanish_Spain',
116     'es_PA': 'Spanish_Spain',
117     'es_PE': 'Spanish_Spain',
118     'es_PR': 'Spanish_Spain',
119     'es_PY': 'Spanish_Spain',
120     'es_SV': 'Spanish_Spain',
121     'es_UY': 'Spanish_Spain',
122     'es_VE': 'Spanish_Spain',
123     'sv_SE': 'Swedish_Sweden',
124     'ta_IN': 'English_Australia',
125     'th_TH': 'Thai_Thailand',
126     'mi_NZ': 'Maori',
127     'tr_TR': 'Turkish_Turkey',
128     'uk_UA': 'Ukrainian_Ukraine',
129     'vi_VN': 'Vietnamese_Viet Nam',
130     'tlh_TLH': 'Klingon',
131
132 }
133
134
135 class UNIX_LINE_TERMINATOR(csv.excel):
136     lineterminator = '\n'
137
138 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
139
140 #
141 # Warning: better use self.pool.get('ir.translation')._get_source if you can
142 #
143 def translate(cr, name, source_type, lang, source=None):
144     if source and name:
145         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s and src=%s', (lang, source_type, str(name), source))
146     elif name:
147         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
148     elif source:
149         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
150     res_trans = cr.fetchone()
151     res = res_trans and res_trans[0] or False
152     return res
153
154 logger = logging.getLogger('translate')
155
156 class GettextAlias(object):
157
158     def _get_db(self):
159         # find current DB based on thread/worker db name (see netsvc)
160         db_name = getattr(threading.currentThread(), 'dbname', None)
161         if db_name:
162             dbname = getattr(threading.currentThread(), 'dbname')
163             return pooler.get_db_only(dbname)
164
165     def _get_cr(self, frame):
166         is_new_cr = False
167         cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
168         if not cr:
169             s = frame.f_locals.get('self', {})
170             cr = getattr(s, 'cr', None)
171         if not cr:
172             db = self._get_db()
173             if db:
174                 cr = db.cursor()
175                 is_new_cr = True
176         return cr, is_new_cr
177
178     def _get_lang(self, frame):
179         lang = None
180         ctx = frame.f_locals.get('context')
181         if not ctx:
182             kwargs = frame.f_locals.get('kwargs')
183             if kwargs is None:
184                 args = frame.f_locals.get('args')
185                 if args and isinstance(args, (list, tuple)) \
186                         and isinstance(args[-1], dict):
187                     ctx = args[-1]
188             elif isinstance(kwargs, dict):
189                 ctx = kwargs.get('context')
190         if ctx:
191             lang = ctx.get('lang')
192         if not lang:
193             s = frame.f_locals.get('self', {})
194             c = getattr(s, 'localcontext', None)
195             if c:
196                 lang = c.get('lang')
197         return lang
198
199     def __call__(self, source):
200         res = source
201         cr = None
202         is_new_cr = False
203         try:
204             frame = inspect.currentframe()
205             if frame is None:
206                 return source
207             frame = frame.f_back
208             if not frame:
209                 return source
210             lang = self._get_lang(frame)
211             if lang:
212                 cr, is_new_cr = self._get_cr(frame)
213                 if cr:
214                     # Try to use ir.translation to benefit from global cache if possible
215                     pool = pooler.get_pool(cr.dbname)
216                     res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
217                 else:
218                     logger.debug('no context cursor detected, skipping translation for "%r"', source)
219             else:
220                 logger.debug('no translation language detected, skipping translation for "%r" ', source)
221         except Exception:
222             logger.debug('translation went wrong for "%r", skipped', source)
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:%S')+"+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, dbname=None):
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, dbname)
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
473 def trans_parse_xsl(de):
474     res = []
475     for n in de:
476         if n.get("t"):
477             for m in [j for j in n if j.text]:
478                 l = m.text.strip().replace('\n',' ')
479                 if len(l):
480                     res.append(l.encode("utf8"))
481         res.extend(trans_parse_xsl(n))
482     return res
483
484 def trans_parse_rml(de):
485     res = []
486     for n in de:
487         for m in [j for j in n if j.text]:
488             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
489             for s in string_list:
490                 if s:
491                     res.append(s.encode("utf8"))
492         res.extend(trans_parse_rml(n))
493     return res
494
495 def trans_parse_view(de):
496     res = []
497     if de.tag == 'attribute' and de.get("name") == 'string':
498         if de.text:
499             res.append(de.text.encode("utf8"))
500     if de.get("string"):
501         res.append(de.get('string').encode("utf8"))
502     if de.get("sum"):
503         res.append(de.get('sum').encode("utf8"))
504     if de.get("confirm"):
505         res.append(de.get('confirm').encode("utf8"))
506     for n in de:
507         res.extend(trans_parse_view(n))
508     return res
509
510 # tests whether an object is in a list of modules
511 def in_modules(object_name, modules):
512     if 'all' in modules:
513         return True
514
515     module_dict = {
516         'ir': 'base',
517         'res': 'base',
518         'workflow': 'base',
519     }
520     module = object_name.split('.')[0]
521     module = module_dict.get(module, module)
522     return module in modules
523
524 def trans_generate(lang, modules, dbname=None):
525     logger = logging.getLogger('i18n')
526     if not dbname:
527         dbname=tools.config['db_name']
528         if not modules:
529             modules = ['all']
530
531     pool = pooler.get_pool(dbname)
532     trans_obj = pool.get('ir.translation')
533     model_data_obj = pool.get('ir.model.data')
534     cr = pooler.get_db(dbname).cursor()
535     uid = 1
536     l = pool.obj_pool.items()
537     l.sort()
538
539     query = 'SELECT name, model, res_id, module'    \
540             '  FROM ir_model_data'
541             
542     query_models = """SELECT m.id, m.model, imd.module 
543             FROM ir_model AS m, ir_model_data AS imd 
544             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
545
546     if 'all_installed' in modules:
547         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
548         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
549     query_param = None
550     if 'all' not in modules:
551         query += ' WHERE module IN %s'
552         query_models += ' AND imd.module in %s'
553         query_param = (tuple(modules),)
554     query += ' ORDER BY module, model, name'
555     query_models += ' ORDER BY module, model'
556
557     cr.execute(query, query_param)
558
559     _to_translate = []
560     def push_translation(module, type, name, id, source):
561         tuple = (module, source, name, id, type)
562         if source and tuple not in _to_translate:
563             _to_translate.append(tuple)
564
565     def encode(s):
566         if isinstance(s, unicode):
567             return s.encode('utf8')
568         return s
569
570     for (xml_name,model,res_id,module) in cr.fetchall():
571         module = encode(module)
572         model = encode(model)
573         xml_name = "%s.%s" % (module, encode(xml_name))
574
575         if not pool.get(model):
576             logger.error("Unable to find object %r", model)
577             continue
578
579         exists = pool.get(model).exists(cr, uid, res_id)
580         if not exists:
581             logger.warning("Unable to find object %r with id %d", model, res_id)
582             continue
583         obj = pool.get(model).browse(cr, uid, res_id)
584
585         if model=='ir.ui.view':
586             d = etree.XML(encode(obj.arch))
587             for t in trans_parse_view(d):
588                 push_translation(module, 'view', encode(obj.model), 0, t)
589         elif model=='ir.actions.wizard':
590             service_name = 'wizard.'+encode(obj.wiz_name)
591             if netsvc.Service._services.get(service_name):
592                 obj2 = netsvc.Service._services[service_name]
593                 for state_name, state_def in obj2.states.iteritems():
594                     if 'result' in state_def:
595                         result = state_def['result']
596                         if result['type'] != 'form':
597                             continue
598                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
599
600                         def_params = {
601                             'string': ('wizard_field', lambda s: [encode(s)]),
602                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
603                             'help': ('help', lambda s: [encode(s)]),
604                         }
605
606                         # export fields
607                         if not result.has_key('fields'):
608                             logger.warning("res has no fields: %r", result)
609                             continue
610                         for field_name, field_def in result['fields'].iteritems():
611                             res_name = name + ',' + field_name
612
613                             for fn in def_params:
614                                 if fn in field_def:
615                                     transtype, modifier = def_params[fn]
616                                     for val in modifier(field_def[fn]):
617                                         push_translation(module, transtype, res_name, 0, val)
618
619                         # export arch
620                         arch = result['arch']
621                         if arch and not isinstance(arch, UpdateableStr):
622                             d = etree.XML(arch)
623                             for t in trans_parse_view(d):
624                                 push_translation(module, 'wizard_view', name, 0, t)
625
626                         # export button labels
627                         for but_args in result['state']:
628                             button_name = but_args[0]
629                             button_label = but_args[1]
630                             res_name = name + ',' + button_name
631                             push_translation(module, 'wizard_button', res_name, 0, button_label)
632
633         elif model=='ir.model.fields':
634             try:
635                 field_name = encode(obj.name)
636             except AttributeError, exc:
637                 logger.error("name error in %s: %s", xml_name, str(exc))
638                 continue
639             objmodel = pool.get(obj.model)
640             if not objmodel or not field_name in objmodel._columns:
641                 continue
642             field_def = objmodel._columns[field_name]
643
644             name = "%s,%s" % (encode(obj.model), field_name)
645             push_translation(module, 'field', name, 0, encode(field_def.string))
646
647             if field_def.help:
648                 push_translation(module, 'help', name, 0, encode(field_def.help))
649
650             if field_def.translate:
651                 ids = objmodel.search(cr, uid, [])
652                 obj_values = objmodel.read(cr, uid, ids, [field_name])
653                 for obj_value in obj_values:
654                     res_id = obj_value['id']
655                     if obj.name in ('ir.model', 'ir.ui.menu'):
656                         res_id = 0
657                     model_data_ids = model_data_obj.search(cr, uid, [
658                         ('model', '=', model),
659                         ('res_id', '=', res_id),
660                         ])
661                     if not model_data_ids:
662                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
663
664             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
665                 for dummy, val in field_def.selection:
666                     push_translation(module, 'selection', name, 0, encode(val))
667
668         elif model=='ir.actions.report.xml':
669             name = encode(obj.report_name)
670             fname = ""
671             if obj.report_rml:
672                 fname = obj.report_rml
673                 parse_func = trans_parse_rml
674                 report_type = "report"
675             elif obj.report_xsl:
676                 fname = obj.report_xsl
677                 parse_func = trans_parse_xsl
678                 report_type = "xsl"
679             if fname and obj.report_type in ('pdf', 'xsl'):
680                 try:
681                     d = etree.parse(tools.file_open(fname))
682                     for t in parse_func(d.iter()):
683                         push_translation(module, report_type, name, 0, t)
684                 except (IOError, etree.XMLSyntaxError):
685                     logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
686
687         for field_name,field_def in obj._table._columns.items():
688             if field_def.translate:
689                 name = model + "," + field_name
690                 try:
691                     trad = getattr(obj, field_name) or ''
692                 except:
693                     trad = ''
694                 push_translation(module, 'model', name, xml_name, encode(trad))
695
696         # End of data for ir.model.data query results
697
698     cr.execute(query_models, query_param)
699
700     def push_constraint_msg(module, term_type, model, msg):
701         # Check presence of __call__ directly instead of using
702         # callable() because it will be deprecated as of Python 3.0
703         if not hasattr(msg, '__call__'):
704             push_translation(module, term_type, model, 0, encode(msg))
705
706     for (model_id, model, module) in cr.fetchall():
707         module = encode(module)
708         model = encode(model)
709
710         model_obj = pool.get(model)
711
712         if not model_obj:
713             logging.getLogger("i18n").error("Unable to find object %r", model)
714             continue
715
716         for constraint in getattr(model_obj, '_constraints', []):
717             push_constraint_msg(module, 'constraint', model, constraint[1])
718
719         for constraint in getattr(model_obj, '_sql_constraints', []):
720             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
721
722     # parse source code for _() calls
723     def get_module_from_path(path, mod_paths=None):
724         if not mod_paths:
725             # First, construct a list of possible paths
726             def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
727             ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
728             mod_paths=[def_path]
729             for adp in ad_paths:
730                 mod_paths.append(adp)
731                 if not os.path.isabs(adp):
732                     mod_paths.append(adp)
733                 elif adp.startswith(def_path):
734                     mod_paths.append(adp[len(def_path)+1:])
735         for mp in mod_paths:
736             if path.startswith(mp) and (os.path.dirname(path) != mp):
737                 path = path[len(mp)+1:]
738                 return path.split(os.path.sep)[0]
739         return 'base'   # files that are not in a module are considered as being in 'base' module
740
741     modobj = pool.get('ir.module.module')
742     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
743     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
744
745     root_path = os.path.join(tools.config['root_path'], 'addons')
746
747     apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
748     if root_path in apaths:
749         path_list = apaths
750     else :
751         path_list = [root_path,] + apaths
752
753     logger.debug("Scanning modules at paths: ", path_list)
754
755     mod_paths = []
756     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
757     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
758     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
759     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
760
761     def export_code_terms_from_file(fname, path, root, terms_type):
762         fabsolutepath = join(root, fname)
763         frelativepath = fabsolutepath[len(path):]
764         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
765         is_mod_installed = module in installed_modules
766         if (('all' in modules) or (module in modules)) and is_mod_installed:
767             logger.debug("Scanning code of %s at module: %s", frelativepath, module)
768             code_string = tools.file_open(fabsolutepath, subdir='').read()
769             if module in installed_modules:
770                 frelativepath = str("addons" + frelativepath)
771             ite = re_dquotes.finditer(code_string)
772             for i in ite:
773                 src = i.group(1)
774                 if src.startswith('""'):
775                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
776                     src = src[2:-2]
777                 else:
778                     src = join_dquotes.sub(r'\1', src)
779                 # now, since we did a binary read of a python source file, we
780                 # have to expand pythonic escapes like the interpreter does.
781                 src = src.decode('string_escape')
782                 push_translation(module, terms_type, frelativepath, 0, encode(src))
783             ite = re_quotes.finditer(code_string)
784             for i in ite:
785                 src = i.group(1)
786                 if src.startswith("''"):
787                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
788                     src = src[2:-2]
789                 else:
790                     src = join_quotes.sub(r'\1', src)
791                 src = src.decode('string_escape')
792                 push_translation(module, terms_type, frelativepath, 0, encode(src))
793
794     for path in path_list:
795         logger.debug("Scanning files of modules at %s", path)
796         for root, dummy, files in tools.osutil.walksymlinks(path):
797             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
798                 export_code_terms_from_file(fname, path, root, 'code')
799             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
800                 export_code_terms_from_file(fname, path, root, 'report')
801
802
803     out = [["module","type","name","res_id","src","value"]] # header
804     _to_translate.sort()
805     # translate strings marked as to be translated
806     for module, source, name, id, type in _to_translate:
807         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
808         out.append([module, type, name, id, source, encode(trans) or ''])
809
810     cr.close()
811     return out
812
813 def trans_load(db_name, filename, lang, verbose=True, context=None):
814     logger = logging.getLogger('i18n')
815     try:
816         fileobj = open(filename,'r')
817         logger.info("loading %s", filename)
818         fileformat = os.path.splitext(filename)[-1][1:].lower()
819         r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
820         fileobj.close()
821         return r
822     except IOError:
823         if verbose:
824             logger.error("couldn't read translation file %s", filename)
825         return None
826
827 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
828     logger = logging.getLogger('i18n')
829     if verbose:
830         logger.info('loading translation file for language %s', lang)
831     if context is None:
832         context = {}
833     pool = pooler.get_pool(db_name)
834     lang_obj = pool.get('res.lang')
835     trans_obj = pool.get('ir.translation')
836     model_data_obj = pool.get('ir.model.data')
837     iso_lang = tools.get_iso_codes(lang)
838     try:
839         uid = 1
840         cr = pooler.get_db(db_name).cursor()
841         ids = lang_obj.search(cr, uid, [('code','=', lang)])
842
843         if not ids:
844             # lets create the language with locale information
845             fail = True
846             for ln in get_locales(lang):
847                 try:
848                     locale.setlocale(locale.LC_ALL, str(ln))
849                     fail = False
850                     break
851                 except locale.Error:
852                     continue
853             if fail:
854                 lc = locale.getdefaultlocale()[0]
855                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
856                 logger.warning(msg, lang, lc)
857
858             if not lang_name:
859                 lang_name = tools.get_languages().get(lang, lang)
860
861             def fix_xa0(s):
862                 if s == '\xa0':
863                     return '\xc2\xa0'
864                 return s
865
866             lang_info = {
867                 'code': lang,
868                 'iso_code': iso_lang,
869                 'name': lang_name,
870                 'translatable': 1,
871                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
872                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
873                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
874                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
875             }
876
877             try:
878                 lang_obj.create(cr, uid, lang_info)
879             finally:
880                 resetlocale()
881
882
883         # now, the serious things: we read the language file
884         fileobj.seek(0)
885         if fileformat == 'csv':
886             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
887             # read the first line of the file (it contains columns titles)
888             for row in reader:
889                 f = row
890                 break
891         elif fileformat == 'po':
892             reader = TinyPoFile(fileobj)
893             f = ['type', 'name', 'res_id', 'src', 'value']
894         else:
895             logger.error('Bad file format: %s', fileformat)
896             raise Exception(_('Bad file format'))
897
898         # read the rest of the file
899         line = 1
900         for row in reader:
901             line += 1
902             # skip empty rows and rows where the translation field (=last fiefd) is empty
903             #if (not row) or (not row[-1]):
904             #    continue
905
906             # dictionary which holds values for this line of the csv file
907             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
908             #  'src': ..., 'value': ...}
909             dic = {'lang': lang}
910             for i in range(len(f)):
911                 if f[i] in ('module',):
912                     continue
913                 dic[f[i]] = row[i]
914
915             try:
916                 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
917             except:
918                 model_data_ids = model_data_obj.search(cr, uid, [
919                     ('model', '=', dic['name'].split(',')[0]),
920                     ('module', '=', dic['res_id'].split('.', 1)[0]),
921                     ('name', '=', dic['res_id'].split('.', 1)[1]),
922                     ])
923                 if model_data_ids:
924                     dic['res_id'] = model_data_obj.browse(cr, uid,
925                             model_data_ids[0]).res_id
926                 else:
927                     dic['res_id'] = False
928
929             args = [
930                 ('lang', '=', lang),
931                 ('type', '=', dic['type']),
932                 ('name', '=', dic['name']),
933                 ('src', '=', dic['src']),
934             ]
935             if dic['type'] == 'model':
936                 args.append(('res_id', '=', dic['res_id']))
937             ids = trans_obj.search(cr, uid, args)
938             if ids:
939                 if context.get('overwrite'):
940                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
941             else:
942                 trans_obj.create(cr, uid, dic)
943             cr.commit()
944         cr.close()
945         if verbose:
946             logger.info("translation file loaded succesfully")
947     except IOError:
948         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
949         logger.exception("couldn't read translation file %s", filename)
950
951 def get_locales(lang=None):
952     if lang is None:
953         lang = locale.getdefaultlocale()[0]
954
955     if os.name == 'nt':
956         lang = _LOCALE2WIN32.get(lang, lang)
957
958     def process(enc):
959         ln = locale._build_localename((lang, enc))
960         yield ln
961         nln = locale.normalize(ln)
962         if nln != ln:
963             yield nln
964
965     for x in process('utf8'): yield x
966
967     prefenc = locale.getpreferredencoding()
968     if prefenc:
969         for x in process(prefenc): yield x
970
971         prefenc = {
972             'latin1': 'latin9',
973             'iso-8859-1': 'iso8859-15',
974             'cp1252': '1252',
975         }.get(prefenc.lower())
976         if prefenc:
977             for x in process(prefenc): yield x
978
979     yield lang
980
981
982
983 def resetlocale():
984     # locale.resetlocale is bugged with some locales.
985     for ln in get_locales():
986         try:
987             return locale.setlocale(locale.LC_ALL, ln)
988         except locale.Error:
989             continue
990
991 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
992