[IMP] ir.translation,tools.translate._(): cleanup and speedup if translations via _()
[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_pool(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_and_pool(dbname)
164         return (None, None)
165
166     def _get_cr(self, frame):
167         new_cr = False
168         cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
169         if not cr:
170             s = frame.f_locals.get('self', {})
171             cr = getattr(s, 'cr', None)
172         if not cr:
173             db, _ = self._get_db_pool()
174             if db:
175                 cr = db.cursor()
176                 new_cr = True
177         return cr, new_cr
178
179     def _get_lang(self, frame):
180         lang = None
181         ctx = frame.f_locals.get('context')
182         if not ctx:
183             kwargs = frame.f_locals.get('kwargs')
184             if kwargs is None:
185                 args = frame.f_locals.get('args')
186                 if args and isinstance(args, (list, tuple)) \
187                         and isinstance(args[-1], dict):
188                     ctx = args[-1]
189             elif isinstance(kwargs, dict):
190                 ctx = kwargs.get('context')
191         if ctx:
192             lang = ctx.get('lang')
193         if not lang:
194             s = frame.f_locals.get('self', {})
195             c = getattr(s, 'localcontext', None)
196             if c:
197                 lang = c.get('lang')
198         return lang
199
200     def __call__(self, source):
201         res = source
202         cr = None
203         new_cr = False
204         try:
205             frame = inspect.currentframe()
206             if frame is None:
207                 return source
208             frame = frame.f_back
209             if not frame:
210                 return source
211             lang = self._get_lang(frame)
212             if lang:
213                 cr, new_cr = self._get_cr(frame)
214                 if cr:
215                     # Try to use ir.translation to benefit from global cache if possible
216                     _, pool = self._get_db_pool()
217                     if pool:
218                         res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
219                     else:
220                         cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
221                         res_trans = cr.fetchone()
222                         res = res_trans and res_trans[0] or source
223                 else:
224                     logger.debug('no context cursor detected, skipping translation for "%r"', source)
225             else:
226                 logger.debug('no translation language detected, skipping translation for "%r" ', source)
227         except Exception:
228             logger.debug('translation went wrong for "%r", skipped', source)
229         finally:
230             if cr and new_cr:
231                 cr.close()
232         return res
233
234 _ = GettextAlias()
235
236
237 def quote(s):
238     """Returns quoted PO term string, with special PO characters escaped"""
239     assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
240     return '"%s"' % s.replace('\\','\\\\') \
241                      .replace('"','\\"') \
242                      .replace('\n', '\\n"\n"')
243
244 re_escaped_char = re.compile(r"(\\.)")
245 re_escaped_replacements = {'n': '\n', }
246
247 def _sub_replacement(match_obj):
248     return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
249
250 def unquote(str):
251     """Returns unquoted PO term string, with special PO characters unescaped"""
252     return re_escaped_char.sub(_sub_replacement, str[1:-1])
253
254 # class to handle po files
255 class TinyPoFile(object):
256     def __init__(self, buffer):
257         self.logger = logging.getLogger('i18n')
258         self.buffer = buffer
259
260     def warn(self, msg):
261         self.logger.warning(msg)
262
263     def __iter__(self):
264         self.buffer.seek(0)
265         self.lines = self._get_lines()
266         self.lines_count = len(self.lines);
267
268         self.first = True
269         self.tnrs= []
270         return self
271
272     def _get_lines(self):
273         lines = self.buffer.readlines()
274         # remove the BOM (Byte Order Mark):
275         if len(lines):
276             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
277
278         lines.append('') # ensure that the file ends with at least an empty line
279         return lines
280
281     def cur_line(self):
282         return (self.lines_count - len(self.lines))
283
284     def next(self):
285         type = name = res_id = source = trad = None
286
287         if self.tnrs:
288             type, name, res_id, source, trad = self.tnrs.pop(0)
289             if not res_id:
290                 res_id = '0'
291         else:
292             tmp_tnrs = []
293             line = None
294             fuzzy = False
295             while (not line):
296                 if 0 == len(self.lines):
297                     raise StopIteration()
298                 line = self.lines.pop(0).strip()
299             while line.startswith('#'):
300                 if line.startswith('#~ '):
301                     break
302                 if line.startswith('#:'):
303                     if ' ' in line[2:].strip():
304                         for lpart in line[2:].strip().split(' '):
305                             tmp_tnrs.append(lpart.strip().split(':',2))
306                     else:
307                         tmp_tnrs.append( line[2:].strip().split(':',2) )
308                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
309                     fuzzy = True
310                 line = self.lines.pop(0).strip()
311             while not line:
312                 # allow empty lines between comments and msgid
313                 line = self.lines.pop(0).strip()
314             if line.startswith('#~ '):
315                 while line.startswith('#~ ') or not line.strip():
316                     if 0 == len(self.lines):
317                         raise StopIteration()
318                     line = self.lines.pop(0)
319                 # This has been a deprecated entry, don't return anything
320                 return self.next()
321
322             if not line.startswith('msgid'):
323                 raise Exception("malformed file: bad line: %s" % line)
324             source = unquote(line[6:])
325             line = self.lines.pop(0).strip()
326             if not source and self.first:
327                 # if the source is "" and it's the first msgid, it's the special
328                 # msgstr with the informations about the traduction and the
329                 # traductor; we skip it
330                 self.tnrs = []
331                 while line:
332                     line = self.lines.pop(0).strip()
333                 return self.next()
334
335             while not line.startswith('msgstr'):
336                 if not line:
337                     raise Exception('malformed file at %d'% self.cur_line())
338                 source += unquote(line)
339                 line = self.lines.pop(0).strip()
340
341             trad = unquote(line[7:])
342             line = self.lines.pop(0).strip()
343             while line:
344                 trad += unquote(line)
345                 line = self.lines.pop(0).strip()
346
347             if tmp_tnrs and not fuzzy:
348                 type, name, res_id = tmp_tnrs.pop(0)
349                 for t, n, r in tmp_tnrs:
350                     self.tnrs.append((t, n, r, source, trad))
351
352         self.first = False
353
354         if name is None:
355             if not fuzzy:
356                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', 
357                         self.cur_line(), source[:30])
358             return self.next()
359         return type, name, res_id, source, trad
360
361     def write_infos(self, modules):
362         import release
363         self.buffer.write("# Translation of %(project)s.\n" \
364                           "# This file contains the translation of the following modules:\n" \
365                           "%(modules)s" \
366                           "#\n" \
367                           "msgid \"\"\n" \
368                           "msgstr \"\"\n" \
369                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
370                           '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
371                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
372                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
373                           '''"Last-Translator: <>\\n"\n''' \
374                           '''"Language-Team: \\n"\n'''   \
375                           '''"MIME-Version: 1.0\\n"\n''' \
376                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
377                           '''"Content-Transfer-Encoding: \\n"\n'''       \
378                           '''"Plural-Forms: \\n"\n'''    \
379                           "\n"
380
381                           % { 'project': release.description,
382                               'version': release.version,
383                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
384                               'bugmail': release.support_email,
385                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
386                             }
387                           )
388
389     def write(self, modules, tnrs, source, trad):
390
391         plurial = len(modules) > 1 and 's' or ''
392         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
393
394
395         code = False
396         for typy, name, res_id in tnrs:
397             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
398             if typy == 'code':
399                 code = True
400
401         if code:
402             # only strings in python code are python formated
403             self.buffer.write("#, python-format\n")
404
405         if not isinstance(trad, unicode):
406             trad = unicode(trad, 'utf8')
407         if not isinstance(source, unicode):
408             source = unicode(source, 'utf8')
409
410         msg = "msgid %s\n"      \
411               "msgstr %s\n\n"   \
412                   % (quote(source), quote(trad))
413         self.buffer.write(msg.encode('utf8'))
414
415
416 # Methods to export the translation file
417
418 def trans_export(lang, modules, buffer, format, dbname=None):
419
420     def _process(format, modules, rows, buffer, lang, newlang):
421         if format == 'csv':
422             writer=csv.writer(buffer, 'UNIX')
423             for row in rows:
424                 writer.writerow(row)
425         elif format == 'po':
426             rows.pop(0)
427             writer = tools.TinyPoFile(buffer)
428             writer.write_infos(modules)
429
430             # we now group the translations by source. That means one translation per source.
431             grouped_rows = {}
432             for module, type, name, res_id, src, trad in rows:
433                 row = grouped_rows.setdefault(src, {})
434                 row.setdefault('modules', set()).add(module)
435                 if ('translation' not in row) or (not row['translation']):
436                     row['translation'] = trad
437                 row.setdefault('tnrs', []).append((type, name, res_id))
438
439             for src, row in grouped_rows.items():
440                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
441
442         elif format == 'tgz':
443             rows.pop(0)
444             rows_by_module = {}
445             for row in rows:
446                 module = row[0]
447                 rows_by_module.setdefault(module, []).append(row)
448
449             tmpdir = tempfile.mkdtemp()
450             for mod, modrows in rows_by_module.items():
451                 tmpmoddir = join(tmpdir, mod, 'i18n')
452                 os.makedirs(tmpmoddir)
453                 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
454                 buf = file(join(tmpmoddir, pofilename), 'w')
455                 _process('po', [mod], modrows, buf, lang, newlang)
456                 buf.close()
457
458             tar = tarfile.open(fileobj=buffer, mode='w|gz')
459             tar.add(tmpdir, '')
460             tar.close()
461
462         else:
463             raise Exception(_('Bad file format'))
464
465     newlang = not bool(lang)
466     if newlang:
467         lang = 'en_US'
468     trans = trans_generate(lang, modules, dbname)
469     if newlang and format!='csv':
470         for trx in trans:
471             trx[-1] = ''
472     modules = set([t[0] for t in trans[1:]])
473     _process(format, modules, trans, buffer, lang, newlang)
474     del trans
475
476
477 def trans_parse_xsl(de):
478     res = []
479     for n in de:
480         if n.get("t"):
481             for m in [j for j in n if j.text]:
482                 l = m.text.strip().replace('\n',' ')
483                 if len(l):
484                     res.append(l.encode("utf8"))
485         res.extend(trans_parse_xsl(n))
486     return res
487
488 def trans_parse_rml(de):
489     res = []
490     for n in de:
491         for m in [j for j in n if j.text]:
492             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
493             for s in string_list:
494                 if s:
495                     res.append(s.encode("utf8"))
496         res.extend(trans_parse_rml(n))
497     return res
498
499 def trans_parse_view(de):
500     res = []
501     if de.tag == 'attribute' and de.get("name") == 'string':
502         if de.text:
503             res.append(de.text.encode("utf8"))
504     if de.get("string"):
505         res.append(de.get('string').encode("utf8"))
506     if de.get("sum"):
507         res.append(de.get('sum').encode("utf8"))
508     if de.get("confirm"):
509         res.append(de.get('confirm').encode("utf8"))
510     for n in de:
511         res.extend(trans_parse_view(n))
512     return res
513
514 # tests whether an object is in a list of modules
515 def in_modules(object_name, modules):
516     if 'all' in modules:
517         return True
518
519     module_dict = {
520         'ir': 'base',
521         'res': 'base',
522         'workflow': 'base',
523     }
524     module = object_name.split('.')[0]
525     module = module_dict.get(module, module)
526     return module in modules
527
528 def trans_generate(lang, modules, dbname=None):
529     logger = logging.getLogger('i18n')
530     if not dbname:
531         dbname=tools.config['db_name']
532         if not modules:
533             modules = ['all']
534
535     pool = pooler.get_pool(dbname)
536     trans_obj = pool.get('ir.translation')
537     model_data_obj = pool.get('ir.model.data')
538     cr = pooler.get_db(dbname).cursor()
539     uid = 1
540     l = pool.obj_pool.items()
541     l.sort()
542
543     query = 'SELECT name, model, res_id, module'    \
544             '  FROM ir_model_data'
545             
546     query_models = """SELECT m.id, m.model, imd.module 
547             FROM ir_model AS m, ir_model_data AS imd 
548             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
549
550     if 'all_installed' in modules:
551         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
552         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
553     query_param = None
554     if 'all' not in modules:
555         query += ' WHERE module IN %s'
556         query_models += ' AND imd.module in %s'
557         query_param = (tuple(modules),)
558     query += ' ORDER BY module, model, name'
559     query_models += ' ORDER BY module, model'
560
561     cr.execute(query, query_param)
562
563     _to_translate = []
564     def push_translation(module, type, name, id, source):
565         tuple = (module, source, name, id, type)
566         if source and tuple not in _to_translate:
567             _to_translate.append(tuple)
568
569     def encode(s):
570         if isinstance(s, unicode):
571             return s.encode('utf8')
572         return s
573
574     for (xml_name,model,res_id,module) in cr.fetchall():
575         module = encode(module)
576         model = encode(model)
577         xml_name = "%s.%s" % (module, encode(xml_name))
578
579         if not pool.get(model):
580             logger.error("Unable to find object %r", model)
581             continue
582
583         exists = pool.get(model).exists(cr, uid, res_id)
584         if not exists:
585             logger.warning("Unable to find object %r with id %d", model, res_id)
586             continue
587         obj = pool.get(model).browse(cr, uid, res_id)
588
589         if model=='ir.ui.view':
590             d = etree.XML(encode(obj.arch))
591             for t in trans_parse_view(d):
592                 push_translation(module, 'view', encode(obj.model), 0, t)
593         elif model=='ir.actions.wizard':
594             service_name = 'wizard.'+encode(obj.wiz_name)
595             if netsvc.Service._services.get(service_name):
596                 obj2 = netsvc.Service._services[service_name]
597                 for state_name, state_def in obj2.states.iteritems():
598                     if 'result' in state_def:
599                         result = state_def['result']
600                         if result['type'] != 'form':
601                             continue
602                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
603
604                         def_params = {
605                             'string': ('wizard_field', lambda s: [encode(s)]),
606                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
607                             'help': ('help', lambda s: [encode(s)]),
608                         }
609
610                         # export fields
611                         if not result.has_key('fields'):
612                             logger.warning("res has no fields: %r", result)
613                             continue
614                         for field_name, field_def in result['fields'].iteritems():
615                             res_name = name + ',' + field_name
616
617                             for fn in def_params:
618                                 if fn in field_def:
619                                     transtype, modifier = def_params[fn]
620                                     for val in modifier(field_def[fn]):
621                                         push_translation(module, transtype, res_name, 0, val)
622
623                         # export arch
624                         arch = result['arch']
625                         if arch and not isinstance(arch, UpdateableStr):
626                             d = etree.XML(arch)
627                             for t in trans_parse_view(d):
628                                 push_translation(module, 'wizard_view', name, 0, t)
629
630                         # export button labels
631                         for but_args in result['state']:
632                             button_name = but_args[0]
633                             button_label = but_args[1]
634                             res_name = name + ',' + button_name
635                             push_translation(module, 'wizard_button', res_name, 0, button_label)
636
637         elif model=='ir.model.fields':
638             try:
639                 field_name = encode(obj.name)
640             except AttributeError, exc:
641                 logger.error("name error in %s: %s", xml_name, str(exc))
642                 continue
643             objmodel = pool.get(obj.model)
644             if not objmodel or not field_name in objmodel._columns:
645                 continue
646             field_def = objmodel._columns[field_name]
647
648             name = "%s,%s" % (encode(obj.model), field_name)
649             push_translation(module, 'field', name, 0, encode(field_def.string))
650
651             if field_def.help:
652                 push_translation(module, 'help', name, 0, encode(field_def.help))
653
654             if field_def.translate:
655                 ids = objmodel.search(cr, uid, [])
656                 obj_values = objmodel.read(cr, uid, ids, [field_name])
657                 for obj_value in obj_values:
658                     res_id = obj_value['id']
659                     if obj.name in ('ir.model', 'ir.ui.menu'):
660                         res_id = 0
661                     model_data_ids = model_data_obj.search(cr, uid, [
662                         ('model', '=', model),
663                         ('res_id', '=', res_id),
664                         ])
665                     if not model_data_ids:
666                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
667
668             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
669                 for dummy, val in field_def.selection:
670                     push_translation(module, 'selection', name, 0, encode(val))
671
672         elif model=='ir.actions.report.xml':
673             name = encode(obj.report_name)
674             fname = ""
675             if obj.report_rml:
676                 fname = obj.report_rml
677                 parse_func = trans_parse_rml
678                 report_type = "report"
679             elif obj.report_xsl:
680                 fname = obj.report_xsl
681                 parse_func = trans_parse_xsl
682                 report_type = "xsl"
683             if fname and obj.report_type in ('pdf', 'xsl'):
684                 try:
685                     d = etree.parse(tools.file_open(fname))
686                     for t in parse_func(d.iter()):
687                         push_translation(module, report_type, name, 0, t)
688                 except (IOError, etree.XMLSyntaxError):
689                     logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
690
691         for field_name,field_def in obj._table._columns.items():
692             if field_def.translate:
693                 name = model + "," + field_name
694                 try:
695                     trad = getattr(obj, field_name) or ''
696                 except:
697                     trad = ''
698                 push_translation(module, 'model', name, xml_name, encode(trad))
699
700         # End of data for ir.model.data query results
701
702     cr.execute(query_models, query_param)
703
704     def push_constraint_msg(module, term_type, model, msg):
705         # Check presence of __call__ directly instead of using
706         # callable() because it will be deprecated as of Python 3.0
707         if not hasattr(msg, '__call__'):
708             push_translation(module, term_type, model, 0, encode(msg))
709
710     for (model_id, model, module) in cr.fetchall():
711         module = encode(module)
712         model = encode(model)
713
714         model_obj = pool.get(model)
715
716         if not model_obj:
717             logging.getLogger("i18n").error("Unable to find object %r", model)
718             continue
719
720         for constraint in getattr(model_obj, '_constraints', []):
721             push_constraint_msg(module, 'constraint', model, constraint[1])
722
723         for constraint in getattr(model_obj, '_sql_constraints', []):
724             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
725
726     # parse source code for _() calls
727     def get_module_from_path(path, mod_paths=None):
728         if not mod_paths:
729             # First, construct a list of possible paths
730             def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
731             ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
732             mod_paths=[def_path]
733             for adp in ad_paths:
734                 mod_paths.append(adp)
735                 if not os.path.isabs(adp):
736                     mod_paths.append(adp)
737                 elif adp.startswith(def_path):
738                     mod_paths.append(adp[len(def_path)+1:])
739         for mp in mod_paths:
740             if path.startswith(mp) and (os.path.dirname(path) != mp):
741                 path = path[len(mp)+1:]
742                 return path.split(os.path.sep)[0]
743         return 'base'   # files that are not in a module are considered as being in 'base' module
744
745     modobj = pool.get('ir.module.module')
746     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
747     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
748
749     root_path = os.path.join(tools.config['root_path'], 'addons')
750
751     apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
752     if root_path in apaths:
753         path_list = apaths
754     else :
755         path_list = [root_path,] + apaths
756
757     logger.debug("Scanning modules at paths: ", path_list)
758
759     mod_paths = []
760     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
761     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
762     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
763     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
764
765     def export_code_terms_from_file(fname, path, root, terms_type):
766         fabsolutepath = join(root, fname)
767         frelativepath = fabsolutepath[len(path):]
768         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
769         is_mod_installed = module in installed_modules
770         if (('all' in modules) or (module in modules)) and is_mod_installed:
771             logger.debug("Scanning code of %s at module: %s", frelativepath, module)
772             code_string = tools.file_open(fabsolutepath, subdir='').read()
773             if module in installed_modules:
774                 frelativepath = str("addons" + frelativepath)
775             ite = re_dquotes.finditer(code_string)
776             for i in ite:
777                 src = i.group(1)
778                 if src.startswith('""'):
779                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
780                     src = src[2:-2]
781                 else:
782                     src = join_dquotes.sub(r'\1', src)
783                 # now, since we did a binary read of a python source file, we
784                 # have to expand pythonic escapes like the interpreter does.
785                 src = src.decode('string_escape')
786                 push_translation(module, terms_type, frelativepath, 0, encode(src))
787             ite = re_quotes.finditer(code_string)
788             for i in ite:
789                 src = i.group(1)
790                 if src.startswith("''"):
791                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
792                     src = src[2:-2]
793                 else:
794                     src = join_quotes.sub(r'\1', src)
795                 src = src.decode('string_escape')
796                 push_translation(module, terms_type, frelativepath, 0, encode(src))
797
798     for path in path_list:
799         logger.debug("Scanning files of modules at %s", path)
800         for root, dummy, files in tools.osutil.walksymlinks(path):
801             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
802                 export_code_terms_from_file(fname, path, root, 'code')
803             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
804                 export_code_terms_from_file(fname, path, root, 'report')
805
806
807     out = [["module","type","name","res_id","src","value"]] # header
808     _to_translate.sort()
809     # translate strings marked as to be translated
810     for module, source, name, id, type in _to_translate:
811         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
812         out.append([module, type, name, id, source, encode(trans) or ''])
813
814     cr.close()
815     return out
816
817 def trans_load(db_name, filename, lang, strict=False, verbose=True, context=None):
818     logger = logging.getLogger('i18n')
819     try:
820         fileobj = open(filename,'r')
821         logger.info("loading %s", filename)
822         fileformat = os.path.splitext(filename)[-1][1:].lower()
823         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
824         fileobj.close()
825         return r
826     except IOError:
827         if verbose:
828             logger.error("couldn't read translation file %s", filename)
829         return None
830
831 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context=None):
832     logger = logging.getLogger('i18n')
833     if verbose:
834         logger.info('loading translation file for language %s', lang)
835     if context is None:
836         context = {}
837     pool = pooler.get_pool(db_name)
838     lang_obj = pool.get('res.lang')
839     trans_obj = pool.get('ir.translation')
840     model_data_obj = pool.get('ir.model.data')
841     iso_lang = tools.get_iso_codes(lang)
842     try:
843         uid = 1
844         cr = pooler.get_db(db_name).cursor()
845         ids = lang_obj.search(cr, uid, [('code','=', lang)])
846
847         if not ids:
848             # lets create the language with locale information
849             fail = True
850             for ln in get_locales(lang):
851                 try:
852                     locale.setlocale(locale.LC_ALL, str(ln))
853                     fail = False
854                     break
855                 except locale.Error:
856                     continue
857             if fail:
858                 lc = locale.getdefaultlocale()[0]
859                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
860                 logger.warning(msg, lang, lc)
861
862             if not lang_name:
863                 lang_name = tools.get_languages().get(lang, lang)
864
865             def fix_xa0(s):
866                 if s == '\xa0':
867                     return '\xc2\xa0'
868                 return s
869
870             lang_info = {
871                 'code': lang,
872                 'iso_code': iso_lang,
873                 'name': lang_name,
874                 'translatable': 1,
875                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
876                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
877                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
878                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
879             }
880
881             try:
882                 lang_obj.create(cr, uid, lang_info)
883             finally:
884                 resetlocale()
885
886
887         # now, the serious things: we read the language file
888         fileobj.seek(0)
889         if fileformat == 'csv':
890             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
891             # read the first line of the file (it contains columns titles)
892             for row in reader:
893                 f = row
894                 break
895         elif fileformat == 'po':
896             reader = TinyPoFile(fileobj)
897             f = ['type', 'name', 'res_id', 'src', 'value']
898         else:
899             logger.error('Bad file format: %s', fileformat)
900             raise Exception(_('Bad file format'))
901
902         # read the rest of the file
903         line = 1
904         for row in reader:
905             line += 1
906             # skip empty rows and rows where the translation field (=last fiefd) is empty
907             #if (not row) or (not row[-1]):
908             #    continue
909
910             # dictionary which holds values for this line of the csv file
911             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
912             #  'src': ..., 'value': ...}
913             dic = {'lang': lang}
914             for i in range(len(f)):
915                 if f[i] in ('module',):
916                     continue
917                 dic[f[i]] = row[i]
918
919             try:
920                 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
921             except:
922                 model_data_ids = model_data_obj.search(cr, uid, [
923                     ('model', '=', dic['name'].split(',')[0]),
924                     ('module', '=', dic['res_id'].split('.', 1)[0]),
925                     ('name', '=', dic['res_id'].split('.', 1)[1]),
926                     ])
927                 if model_data_ids:
928                     dic['res_id'] = model_data_obj.browse(cr, uid,
929                             model_data_ids[0]).res_id
930                 else:
931                     dic['res_id'] = False
932
933             if dic['type'] == 'model' and not strict:
934                 (model, field) = dic['name'].split(',')
935
936                 # get the ids of the resources of this model which share
937                 # the same source
938                 obj = pool.get(model)
939                 if obj:
940                     if field not in obj.fields_get_keys(cr, uid):
941                         continue
942                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
943
944                     # if the resource id (res_id) is in that list, use it,
945                     # otherwise use the whole list
946                     if not ids:
947                         ids = []
948                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
949                     for id in ids:
950                         dic['res_id'] = id
951                         ids = trans_obj.search(cr, uid, [
952                             ('lang', '=', lang),
953                             ('type', '=', dic['type']),
954                             ('name', '=', dic['name']),
955                             ('src', '=', dic['src']),
956                             ('res_id', '=', dic['res_id'])
957                         ])
958                         if ids:
959                             if context.get('overwrite', False):
960                                 trans_obj.write(cr, uid, ids, {'value': dic['value']})
961                         else:
962                             trans_obj.create(cr, uid, dic)
963             else:
964                 ids = trans_obj.search(cr, uid, [
965                     ('lang', '=', lang),
966                     ('type', '=', dic['type']),
967                     ('name', '=', dic['name']),
968                     ('src', '=', dic['src'])
969                 ])
970                 if ids:
971                     if context.get('overwrite', False):
972                         trans_obj.write(cr, uid, ids, {'value': dic['value']})
973                 else:
974                     trans_obj.create(cr, uid, dic)
975             cr.commit()
976         cr.close()
977         if verbose:
978             logger.info("translation file loaded succesfully")
979     except IOError:
980         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
981         logger.exception("couldn't read translation file %s", filename)
982
983 def get_locales(lang=None):
984     if lang is None:
985         lang = locale.getdefaultlocale()[0]
986
987     if os.name == 'nt':
988         lang = _LOCALE2WIN32.get(lang, lang)
989
990     def process(enc):
991         ln = locale._build_localename((lang, enc))
992         yield ln
993         nln = locale.normalize(ln)
994         if nln != ln:
995             yield nln
996
997     for x in process('utf8'): yield x
998
999     prefenc = locale.getpreferredencoding()
1000     if prefenc:
1001         for x in process(prefenc): yield x
1002
1003         prefenc = {
1004             'latin1': 'latin9',
1005             'iso-8859-1': 'iso8859-15',
1006             'cp1252': '1252',
1007         }.get(prefenc.lower())
1008         if prefenc:
1009             for x in process(prefenc): yield x
1010
1011     yield lang
1012
1013
1014
1015 def resetlocale():
1016     # locale.resetlocale is bugged with some locales.
1017     for ln in get_locales():
1018         try:
1019             return locale.setlocale(locale.LC_ALL, ln)
1020         except locale.Error:
1021             continue
1022
1023 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1024