[REF] Replace the user id 1 by openerp.SUPERUSER_ID
[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.tnrs= []
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         type = name = res_id = source = trad = None
282
283         if self.tnrs:
284             type, name, res_id, source, trad = self.tnrs.pop(0)
285             if not res_id:
286                 res_id = '0'
287         else:
288             tmp_tnrs = []
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                     for lpart in line[2:].strip().split(' '):
300                         trans_info = lpart.strip().split(':',2)
301                         if trans_info and len(trans_info) == 2:
302                             # looks like the translation type is missing, which is not
303                             # unexpected because it is not a GetText standard. Default: 'code'
304                             trans_info[:0] = ['code']
305                         if trans_info and len(trans_info) == 3:
306                             tmp_tnrs.append(trans_info)
307                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
308                     fuzzy = True
309                 line = self.lines.pop(0).strip()
310             while not line:
311                 # allow empty lines between comments and msgid
312                 line = self.lines.pop(0).strip()
313             if line.startswith('#~ '):
314                 while line.startswith('#~ ') or not line.strip():
315                     if 0 == len(self.lines):
316                         raise StopIteration()
317                     line = self.lines.pop(0)
318                 # This has been a deprecated entry, don't return anything
319                 return self.next()
320
321             if not line.startswith('msgid'):
322                 raise Exception("malformed file: bad line: %s" % line)
323             source = unquote(line[6:])
324             line = self.lines.pop(0).strip()
325             if not source and self.first:
326                 # if the source is "" and it's the first msgid, it's the special
327                 # msgstr with the informations about the traduction and the
328                 # traductor; we skip it
329                 self.tnrs = []
330                 while line:
331                     line = self.lines.pop(0).strip()
332                 return self.next()
333
334             while not line.startswith('msgstr'):
335                 if not line:
336                     raise Exception('malformed file at %d'% self.cur_line())
337                 source += unquote(line)
338                 line = self.lines.pop(0).strip()
339
340             trad = unquote(line[7:])
341             line = self.lines.pop(0).strip()
342             while line:
343                 trad += unquote(line)
344                 line = self.lines.pop(0).strip()
345
346             if tmp_tnrs and not fuzzy:
347                 type, name, res_id = tmp_tnrs.pop(0)
348                 for t, n, r in tmp_tnrs:
349                     self.tnrs.append((t, n, r, source, trad))
350
351         self.first = False
352
353         if name is None:
354             if not fuzzy:
355                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
356                         self.cur_line(), source[:30])
357             return self.next()
358         return type, name, res_id, source, trad
359
360     def write_infos(self, modules):
361         import openerp.release as release
362         self.buffer.write("# Translation of %(project)s.\n" \
363                           "# This file contains the translation of the following modules:\n" \
364                           "%(modules)s" \
365                           "#\n" \
366                           "msgid \"\"\n" \
367                           "msgstr \"\"\n" \
368                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
369                           '''"Report-Msgid-Bugs-To: \\n"\n''' \
370                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
371                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
372                           '''"Last-Translator: <>\\n"\n''' \
373                           '''"Language-Team: \\n"\n'''   \
374                           '''"MIME-Version: 1.0\\n"\n''' \
375                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
376                           '''"Content-Transfer-Encoding: \\n"\n'''       \
377                           '''"Plural-Forms: \\n"\n'''    \
378                           "\n"
379
380                           % { 'project': release.description,
381                               'version': release.version,
382                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
383                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
384                             }
385                           )
386
387     def write(self, modules, tnrs, source, trad):
388
389         plurial = len(modules) > 1 and 's' or ''
390         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
391
392
393         code = False
394         for typy, name, res_id in tnrs:
395             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
396             if typy == 'code':
397                 code = True
398
399         if code:
400             # only strings in python code are python formated
401             self.buffer.write("#, python-format\n")
402
403         if not isinstance(trad, unicode):
404             trad = unicode(trad, 'utf8')
405         if not isinstance(source, unicode):
406             source = unicode(source, 'utf8')
407
408         msg = "msgid %s\n"      \
409               "msgstr %s\n\n"   \
410                   % (quote(source), quote(trad))
411         self.buffer.write(msg.encode('utf8'))
412
413
414 # Methods to export the translation file
415
416 def trans_export(lang, modules, buffer, format, cr):
417
418     def _process(format, modules, rows, buffer, lang, newlang):
419         if format == 'csv':
420             writer=csv.writer(buffer, 'UNIX')
421             for row in rows:
422                 writer.writerow(row)
423         elif format == 'po':
424             rows.pop(0)
425             writer = TinyPoFile(buffer)
426             writer.write_infos(modules)
427
428             # we now group the translations by source. That means one translation per source.
429             grouped_rows = {}
430             for module, type, name, res_id, src, trad in rows:
431                 row = grouped_rows.setdefault(src, {})
432                 row.setdefault('modules', set()).add(module)
433                 if ('translation' not in row) or (not row['translation']):
434                     row['translation'] = trad
435                 row.setdefault('tnrs', []).append((type, name, res_id))
436
437             for src, row in grouped_rows.items():
438                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
439
440         elif format == 'tgz':
441             rows.pop(0)
442             rows_by_module = {}
443             for row in rows:
444                 module = row[0]
445                 # first row is the "header", as in csv, it will be popped
446                 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
447                 rows_by_module[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(_('Unrecognized extension: must be one of '
464                 '.csv, .po, or .tgz (received .%s).' % format))
465
466     newlang = not bool(lang)
467     if newlang:
468         lang = 'en_US'
469     trans = trans_generate(lang, modules, cr)
470     if newlang and format!='csv':
471         for trx in trans:
472             trx[-1] = ''
473     modules = set([t[0] for t in trans[1:]])
474     _process(format, modules, trans, buffer, lang, newlang)
475     del trans
476
477 def trans_parse_xsl(de):
478     res = []
479     for n in de:
480         if n.get("t"):
481             for m in n:
482                 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
483                     continue
484                 l = m.text.strip().replace('\n',' ')
485                 if len(l):
486                     res.append(l.encode("utf8"))
487         res.extend(trans_parse_xsl(n))
488     return res
489
490 def trans_parse_rml(de):
491     res = []
492     for n in de:
493         for m in n:
494             if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
495                 continue
496             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
497             for s in string_list:
498                 if s:
499                     res.append(s.encode("utf8"))
500         res.extend(trans_parse_rml(n))
501     return res
502
503 def trans_parse_view(de):
504     res = []
505     if de.text and de.text.strip():
506         res.append(de.text.strip().encode("utf8"))
507     if de.tail and de.tail.strip():
508         res.append(de.tail.strip().encode("utf8"))
509     if de.tag == 'attribute' and de.get("name") == 'string':
510         if de.text:
511             res.append(de.text.encode("utf8"))
512     if de.get("string"):
513         res.append(de.get('string').encode("utf8"))
514     if de.get("help"):
515         res.append(de.get('help').encode("utf8"))
516     if de.get("sum"):
517         res.append(de.get('sum').encode("utf8"))
518     if de.get("confirm"):
519         res.append(de.get('confirm').encode("utf8"))
520     for n in de:
521         res.extend(trans_parse_view(n))
522     return res
523
524 # tests whether an object is in a list of modules
525 def in_modules(object_name, modules):
526     if 'all' in modules:
527         return True
528
529     module_dict = {
530         'ir': 'base',
531         'res': 'base',
532         'workflow': 'base',
533     }
534     module = object_name.split('.')[0]
535     module = module_dict.get(module, module)
536     return module in modules
537
538 def trans_generate(lang, modules, cr):
539     dbname = cr.dbname
540
541     pool = pooler.get_pool(dbname)
542     trans_obj = pool.get('ir.translation')
543     model_data_obj = pool.get('ir.model.data')
544     uid = 1
545     l = pool.models.items()
546     l.sort()
547
548     query = 'SELECT name, model, res_id, module'    \
549             '  FROM ir_model_data'
550
551     query_models = """SELECT m.id, m.model, imd.module
552             FROM ir_model AS m, ir_model_data AS imd
553             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
554
555     if 'all_installed' in modules:
556         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
557         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
558     query_param = None
559     if 'all' not in modules:
560         query += ' WHERE module IN %s'
561         query_models += ' AND imd.module in %s'
562         query_param = (tuple(modules),)
563     query += ' ORDER BY module, model, name'
564     query_models += ' ORDER BY module, model'
565
566     cr.execute(query, query_param)
567
568     _to_translate = []
569     def push_translation(module, type, name, id, source):
570         tuple = (module, source, name, id, type)
571         if source and tuple not in _to_translate:
572             _to_translate.append(tuple)
573
574     def encode(s):
575         if isinstance(s, unicode):
576             return s.encode('utf8')
577         return s
578
579     for (xml_name,model,res_id,module) in cr.fetchall():
580         module = encode(module)
581         model = encode(model)
582         xml_name = "%s.%s" % (module, encode(xml_name))
583
584         if not pool.get(model):
585             _logger.error("Unable to find object %r", model)
586             continue
587
588         exists = pool.get(model).exists(cr, uid, res_id)
589         if not exists:
590             _logger.warning("Unable to find object %r with id %d", model, res_id)
591             continue
592         obj = pool.get(model).browse(cr, uid, res_id)
593
594         if model=='ir.ui.view':
595             d = etree.XML(encode(obj.arch))
596             for t in trans_parse_view(d):
597                 push_translation(module, 'view', encode(obj.model), 0, t)
598         elif model=='ir.actions.wizard':
599             service_name = 'wizard.'+encode(obj.wiz_name)
600             import openerp.netsvc as netsvc
601             if netsvc.Service._services.get(service_name):
602                 obj2 = netsvc.Service._services[service_name]
603                 for state_name, state_def in obj2.states.iteritems():
604                     if 'result' in state_def:
605                         result = state_def['result']
606                         if result['type'] != 'form':
607                             continue
608                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
609
610                         def_params = {
611                             'string': ('wizard_field', lambda s: [encode(s)]),
612                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
613                             'help': ('help', lambda s: [encode(s)]),
614                         }
615
616                         # export fields
617                         if not result.has_key('fields'):
618                             _logger.warning("res has no fields: %r", result)
619                             continue
620                         for field_name, field_def in result['fields'].iteritems():
621                             res_name = name + ',' + field_name
622
623                             for fn in def_params:
624                                 if fn in field_def:
625                                     transtype, modifier = def_params[fn]
626                                     for val in modifier(field_def[fn]):
627                                         push_translation(module, transtype, res_name, 0, val)
628
629                         # export arch
630                         arch = result['arch']
631                         if arch and not isinstance(arch, UpdateableStr):
632                             d = etree.XML(arch)
633                             for t in trans_parse_view(d):
634                                 push_translation(module, 'wizard_view', name, 0, t)
635
636                         # export button labels
637                         for but_args in result['state']:
638                             button_name = but_args[0]
639                             button_label = but_args[1]
640                             res_name = name + ',' + button_name
641                             push_translation(module, 'wizard_button', res_name, 0, button_label)
642
643         elif model=='ir.model.fields':
644             try:
645                 field_name = encode(obj.name)
646             except AttributeError, exc:
647                 _logger.error("name error in %s: %s", xml_name, str(exc))
648                 continue
649             objmodel = pool.get(obj.model)
650             if not objmodel or not field_name in objmodel._columns:
651                 continue
652             field_def = objmodel._columns[field_name]
653
654             name = "%s,%s" % (encode(obj.model), field_name)
655             push_translation(module, 'field', name, 0, encode(field_def.string))
656
657             if field_def.help:
658                 push_translation(module, 'help', name, 0, encode(field_def.help))
659
660             if field_def.translate:
661                 ids = objmodel.search(cr, uid, [])
662                 obj_values = objmodel.read(cr, uid, ids, [field_name])
663                 for obj_value in obj_values:
664                     res_id = obj_value['id']
665                     if obj.name in ('ir.model', 'ir.ui.menu'):
666                         res_id = 0
667                     model_data_ids = model_data_obj.search(cr, uid, [
668                         ('model', '=', model),
669                         ('res_id', '=', res_id),
670                         ])
671                     if not model_data_ids:
672                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
673
674             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
675                 for dummy, val in field_def.selection:
676                     push_translation(module, 'selection', name, 0, encode(val))
677
678         elif model=='ir.actions.report.xml':
679             name = encode(obj.report_name)
680             fname = ""
681             if obj.report_rml:
682                 fname = obj.report_rml
683                 parse_func = trans_parse_rml
684                 report_type = "report"
685             elif obj.report_xsl:
686                 fname = obj.report_xsl
687                 parse_func = trans_parse_xsl
688                 report_type = "xsl"
689             if fname and obj.report_type in ('pdf', 'xsl'):
690                 try:
691                     report_file = misc.file_open(fname)
692                     try:
693                         d = etree.parse(report_file)
694                         for t in parse_func(d.iter()):
695                             push_translation(module, report_type, name, 0, t)
696                     finally:
697                         report_file.close()
698                 except (IOError, etree.XMLSyntaxError):
699                     _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
700
701         for field_name,field_def in obj._table._columns.items():
702             if field_def.translate:
703                 name = model + "," + field_name
704                 try:
705                     trad = getattr(obj, field_name) or ''
706                 except:
707                     trad = ''
708                 push_translation(module, 'model', name, xml_name, encode(trad))
709
710         # End of data for ir.model.data query results
711
712     cr.execute(query_models, query_param)
713
714     def push_constraint_msg(module, term_type, model, msg):
715         # Check presence of __call__ directly instead of using
716         # callable() because it will be deprecated as of Python 3.0
717         if not hasattr(msg, '__call__'):
718             push_translation(module, term_type, model, 0, encode(msg))
719
720     for (model_id, model, module) in cr.fetchall():
721         module = encode(module)
722         model = encode(model)
723
724         model_obj = pool.get(model)
725
726         if not model_obj:
727             _logger.error("Unable to find object %r", model)
728             continue
729
730         for constraint in getattr(model_obj, '_constraints', []):
731             push_constraint_msg(module, 'constraint', model, constraint[1])
732
733         for constraint in getattr(model_obj, '_sql_constraints', []):
734             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
735
736     # parse source code for _() calls
737     def get_module_from_path(path, mod_paths=None):
738         if not mod_paths:
739             # First, construct a list of possible paths
740             def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))     # default addons path (base)
741             ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
742             mod_paths=[def_path]
743             for adp in ad_paths:
744                 mod_paths.append(adp)
745                 if not os.path.isabs(adp):
746                     mod_paths.append(adp)
747                 elif adp.startswith(def_path):
748                     mod_paths.append(adp[len(def_path)+1:])
749         for mp in mod_paths:
750             if path.startswith(mp) and (os.path.dirname(path) != mp):
751                 path = path[len(mp)+1:]
752                 return path.split(os.path.sep)[0]
753         return 'base'   # files that are not in a module are considered as being in 'base' module
754
755     modobj = pool.get('ir.module.module')
756     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
757     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
758
759     root_path = os.path.join(config.config['root_path'], 'addons')
760
761     apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
762     if root_path in apaths:
763         path_list = apaths
764     else :
765         path_list = [root_path,] + apaths
766
767     # Also scan these non-addon paths
768     for bin_path in ['osv', 'report' ]:
769         path_list.append(os.path.join(config.config['root_path'], bin_path))
770
771     _logger.debug("Scanning modules at paths: ", path_list)
772
773     mod_paths = []
774     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
775     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
776     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
777     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
778
779     def export_code_terms_from_file(fname, path, root, terms_type):
780         fabsolutepath = join(root, fname)
781         frelativepath = fabsolutepath[len(path):]
782         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
783         is_mod_installed = module in installed_modules
784         if (('all' in modules) or (module in modules)) and is_mod_installed:
785             _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
786             src_file = misc.file_open(fabsolutepath, subdir='')
787             try:
788                 code_string = src_file.read()
789             finally:
790                 src_file.close()
791             if module in installed_modules:
792                 frelativepath = str("addons" + frelativepath)
793             ite = re_dquotes.finditer(code_string)
794             code_offset = 0
795             code_line = 1
796             for i in ite:
797                 src = i.group(1)
798                 if src.startswith('""'):
799                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
800                     src = src[2:-2]
801                 else:
802                     src = join_dquotes.sub(r'\1', src)
803                 # try to count the lines from the last pos to our place:
804                 code_line += code_string[code_offset:i.start(1)].count('\n')
805                 # now, since we did a binary read of a python source file, we
806                 # have to expand pythonic escapes like the interpreter does.
807                 src = src.decode('string_escape')
808                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
809                 code_line += i.group(1).count('\n')
810                 code_offset = i.end() # we have counted newlines up to the match end
811
812             ite = re_quotes.finditer(code_string)
813             code_offset = 0 #reset counters
814             code_line = 1
815             for i in ite:
816                 src = i.group(1)
817                 if src.startswith("''"):
818                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
819                     src = src[2:-2]
820                 else:
821                     src = join_quotes.sub(r'\1', src)
822                 code_line += code_string[code_offset:i.start(1)].count('\n')
823                 src = src.decode('string_escape')
824                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
825                 code_line += i.group(1).count('\n')
826                 code_offset = i.end() # we have counted newlines up to the match end
827
828     for path in path_list:
829         _logger.debug("Scanning files of modules at %s", path)
830         for root, dummy, files in osutil.walksymlinks(path):
831             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
832                 export_code_terms_from_file(fname, path, root, 'code')
833             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
834                 export_code_terms_from_file(fname, path, root, 'report')
835
836
837     out = [["module","type","name","res_id","src","value"]] # header
838     _to_translate.sort()
839     # translate strings marked as to be translated
840     for module, source, name, id, type in _to_translate:
841         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
842         out.append([module, type, name, id, source, encode(trans) or ''])
843
844     return out
845
846 def trans_load(cr, filename, lang, verbose=True, context=None):
847     try:
848         fileobj = misc.file_open(filename)
849         _logger.info("loading %s", filename)
850         fileformat = os.path.splitext(filename)[-1][1:].lower()
851         r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
852         fileobj.close()
853         return r
854     except IOError:
855         if verbose:
856             _logger.error("couldn't read translation file %s", filename)
857         return None
858
859 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
860     """Populates the ir_translation table."""
861     if verbose:
862         _logger.info('loading translation file for language %s', lang)
863     if context is None:
864         context = {}
865     db_name = cr.dbname
866     pool = pooler.get_pool(db_name)
867     lang_obj = pool.get('res.lang')
868     trans_obj = pool.get('ir.translation')
869     iso_lang = misc.get_iso_codes(lang)
870     try:
871         uid = 1
872         ids = lang_obj.search(cr, uid, [('code','=', lang)])
873
874         if not ids:
875             # lets create the language with locale information
876             lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
877
878
879         # now, the serious things: we read the language file
880         fileobj.seek(0)
881         if fileformat == 'csv':
882             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
883             # read the first line of the file (it contains columns titles)
884             for row in reader:
885                 f = row
886                 break
887         elif fileformat == 'po':
888             reader = TinyPoFile(fileobj)
889             f = ['type', 'name', 'res_id', 'src', 'value']
890         else:
891             _logger.error('Bad file format: %s', fileformat)
892             raise Exception(_('Bad file format'))
893
894         # read the rest of the file
895         line = 1
896         irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
897
898         for row in reader:
899             line += 1
900             # skip empty rows and rows where the translation field (=last fiefd) is empty
901             #if (not row) or (not row[-1]):
902             #    continue
903
904             # dictionary which holds values for this line of the csv file
905             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
906             #  'src': ..., 'value': ...}
907             dic = {'lang': lang}
908             dic_module = False
909             for i in range(len(f)):
910                 if f[i] in ('module',):
911                     continue
912                 dic[f[i]] = row[i]
913
914             # This would skip terms that fail to specify a res_id
915             if not dic.get('res_id', False):
916                 continue
917
918             res_id = dic.pop('res_id')
919             if res_id and isinstance(res_id, (int, long)) \
920                 or (isinstance(res_id, basestring) and res_id.isdigit()):
921                     dic['res_id'] = int(res_id)
922             else:
923                 try:
924                     tmodel = dic['name'].split(',')[0]
925                     if '.' in res_id:
926                         tmodule, tname = res_id.split('.', 1)
927                     else:
928                         tmodule = dic_module
929                         tname = res_id
930                     dic['imd_model'] = tmodel
931                     dic['imd_module'] = tmodule
932                     dic['imd_name'] =  tname
933
934                     dic['res_id'] = None
935                 except Exception:
936                     _logger.warning("Could not decode resource for %s, please fix the po file.",
937                                     dic['res_id'], exc_info=True)
938                     dic['res_id'] = None
939
940             irt_cursor.push(dic)
941
942         irt_cursor.finish()
943         if verbose:
944             _logger.info("translation file loaded succesfully")
945     except IOError:
946         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
947         _logger.exception("couldn't read translation file %s", filename)
948
949 def get_locales(lang=None):
950     if lang is None:
951         lang = locale.getdefaultlocale()[0]
952
953     if os.name == 'nt':
954         lang = _LOCALE2WIN32.get(lang, lang)
955
956     def process(enc):
957         ln = locale._build_localename((lang, enc))
958         yield ln
959         nln = locale.normalize(ln)
960         if nln != ln:
961             yield nln
962
963     for x in process('utf8'): yield x
964
965     prefenc = locale.getpreferredencoding()
966     if prefenc:
967         for x in process(prefenc): yield x
968
969         prefenc = {
970             'latin1': 'latin9',
971             'iso-8859-1': 'iso8859-15',
972             'cp1252': '1252',
973         }.get(prefenc.lower())
974         if prefenc:
975             for x in process(prefenc): yield x
976
977     yield lang
978
979
980
981 def resetlocale():
982     # locale.resetlocale is bugged with some locales.
983     for ln in get_locales():
984         try:
985             return locale.setlocale(locale.LC_ALL, ln)
986         except locale.Error:
987             continue
988
989 def load_language(cr, lang):
990     """Loads a translation terms for a language.
991     Used mainly to automate language loading at db initialization.
992
993     :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
994     :type lang: str
995     """
996     pool = pooler.get_pool(cr.dbname)
997     language_installer = pool.get('base.language.install')
998     uid = 1
999     oid = language_installer.create(cr, uid, {'lang': lang})
1000     language_installer.lang_install(cr, uid, [oid], context=None)
1001
1002 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1003