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