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