[IMP] Translations of static terms in views
[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.text and de.text.strip():
505         res.append(de.text.strip().encode("utf8"))
506     if de.tail and de.tail.strip():
507         res.append(de.tail.strip().encode("utf8"))
508     if de.tag == 'attribute' and de.get("name") == 'string':
509         if de.text:
510             res.append(de.text.encode("utf8"))
511     if de.get("string"):
512         res.append(de.get('string').encode("utf8"))
513     if de.get("help"):
514         res.append(de.get('help').encode("utf8"))
515     if de.get("sum"):
516         res.append(de.get('sum').encode("utf8"))
517     if de.get("confirm"):
518         res.append(de.get('confirm').encode("utf8"))
519     for n in de:
520         res.extend(trans_parse_view(n))
521     return res
522
523 # tests whether an object is in a list of modules
524 def in_modules(object_name, modules):
525     if 'all' in modules:
526         return True
527
528     module_dict = {
529         'ir': 'base',
530         'res': 'base',
531         'workflow': 'base',
532     }
533     module = object_name.split('.')[0]
534     module = module_dict.get(module, module)
535     return module in modules
536
537 def trans_generate(lang, modules, cr):
538     dbname = cr.dbname
539
540     pool = pooler.get_pool(dbname)
541     trans_obj = pool.get('ir.translation')
542     model_data_obj = pool.get('ir.model.data')
543     uid = 1
544     l = pool.models.items()
545     l.sort()
546
547     query = 'SELECT name, model, res_id, module'    \
548             '  FROM ir_model_data'
549
550     query_models = """SELECT m.id, m.model, imd.module
551             FROM ir_model AS m, ir_model_data AS imd
552             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
553
554     if 'all_installed' in modules:
555         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
556         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
557     query_param = None
558     if 'all' not in modules:
559         query += ' WHERE module IN %s'
560         query_models += ' AND imd.module in %s'
561         query_param = (tuple(modules),)
562     query += ' ORDER BY module, model, name'
563     query_models += ' ORDER BY module, model'
564
565     cr.execute(query, query_param)
566
567     _to_translate = []
568     def push_translation(module, type, name, id, source):
569         tuple = (module, source, name, id, type)
570         if source and tuple not in _to_translate:
571             _to_translate.append(tuple)
572
573     def encode(s):
574         if isinstance(s, unicode):
575             return s.encode('utf8')
576         return s
577
578     for (xml_name,model,res_id,module) in cr.fetchall():
579         module = encode(module)
580         model = encode(model)
581         xml_name = "%s.%s" % (module, encode(xml_name))
582
583         if not pool.get(model):
584             _logger.error("Unable to find object %r", model)
585             continue
586
587         exists = pool.get(model).exists(cr, uid, res_id)
588         if not exists:
589             _logger.warning("Unable to find object %r with id %d", model, res_id)
590             continue
591         obj = pool.get(model).browse(cr, uid, res_id)
592
593         if model=='ir.ui.view':
594             d = etree.XML(encode(obj.arch))
595             for t in trans_parse_view(d):
596                 push_translation(module, 'view', encode(obj.model), 0, t)
597         elif model=='ir.actions.wizard':
598             service_name = 'wizard.'+encode(obj.wiz_name)
599             import openerp.netsvc as netsvc
600             if netsvc.Service._services.get(service_name):
601                 obj2 = netsvc.Service._services[service_name]
602                 for state_name, state_def in obj2.states.iteritems():
603                     if 'result' in state_def:
604                         result = state_def['result']
605                         if result['type'] != 'form':
606                             continue
607                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
608
609                         def_params = {
610                             'string': ('wizard_field', lambda s: [encode(s)]),
611                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
612                             'help': ('help', lambda s: [encode(s)]),
613                         }
614
615                         # export fields
616                         if not result.has_key('fields'):
617                             _logger.warning("res has no fields: %r", result)
618                             continue
619                         for field_name, field_def in result['fields'].iteritems():
620                             res_name = name + ',' + field_name
621
622                             for fn in def_params:
623                                 if fn in field_def:
624                                     transtype, modifier = def_params[fn]
625                                     for val in modifier(field_def[fn]):
626                                         push_translation(module, transtype, res_name, 0, val)
627
628                         # export arch
629                         arch = result['arch']
630                         if arch and not isinstance(arch, UpdateableStr):
631                             d = etree.XML(arch)
632                             for t in trans_parse_view(d):
633                                 push_translation(module, 'wizard_view', name, 0, t)
634
635                         # export button labels
636                         for but_args in result['state']:
637                             button_name = but_args[0]
638                             button_label = but_args[1]
639                             res_name = name + ',' + button_name
640                             push_translation(module, 'wizard_button', res_name, 0, button_label)
641
642         elif model=='ir.model.fields':
643             try:
644                 field_name = encode(obj.name)
645             except AttributeError, exc:
646                 _logger.error("name error in %s: %s", xml_name, str(exc))
647                 continue
648             objmodel = pool.get(obj.model)
649             if not objmodel or not field_name in objmodel._columns:
650                 continue
651             field_def = objmodel._columns[field_name]
652
653             name = "%s,%s" % (encode(obj.model), field_name)
654             push_translation(module, 'field', name, 0, encode(field_def.string))
655
656             if field_def.help:
657                 push_translation(module, 'help', name, 0, encode(field_def.help))
658
659             if field_def.translate:
660                 ids = objmodel.search(cr, uid, [])
661                 obj_values = objmodel.read(cr, uid, ids, [field_name])
662                 for obj_value in obj_values:
663                     res_id = obj_value['id']
664                     if obj.name in ('ir.model', 'ir.ui.menu'):
665                         res_id = 0
666                     model_data_ids = model_data_obj.search(cr, uid, [
667                         ('model', '=', model),
668                         ('res_id', '=', res_id),
669                         ])
670                     if not model_data_ids:
671                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
672
673             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
674                 for dummy, val in field_def.selection:
675                     push_translation(module, 'selection', name, 0, encode(val))
676
677         elif model=='ir.actions.report.xml':
678             name = encode(obj.report_name)
679             fname = ""
680             if obj.report_rml:
681                 fname = obj.report_rml
682                 parse_func = trans_parse_rml
683                 report_type = "report"
684             elif obj.report_xsl:
685                 fname = obj.report_xsl
686                 parse_func = trans_parse_xsl
687                 report_type = "xsl"
688             if fname and obj.report_type in ('pdf', 'xsl'):
689                 try:
690                     report_file = misc.file_open(fname)
691                     try:
692                         d = etree.parse(report_file)
693                         for t in parse_func(d.iter()):
694                             push_translation(module, report_type, name, 0, t)
695                     finally:
696                         report_file.close()
697                 except (IOError, etree.XMLSyntaxError):
698                     _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
699
700         for field_name,field_def in obj._table._columns.items():
701             if field_def.translate:
702                 name = model + "," + field_name
703                 try:
704                     trad = getattr(obj, field_name) or ''
705                 except:
706                     trad = ''
707                 push_translation(module, 'model', name, xml_name, encode(trad))
708
709         # End of data for ir.model.data query results
710
711     cr.execute(query_models, query_param)
712
713     def push_constraint_msg(module, term_type, model, msg):
714         # Check presence of __call__ directly instead of using
715         # callable() because it will be deprecated as of Python 3.0
716         if not hasattr(msg, '__call__'):
717             push_translation(module, term_type, model, 0, encode(msg))
718
719     for (model_id, model, module) in cr.fetchall():
720         module = encode(module)
721         model = encode(model)
722
723         model_obj = pool.get(model)
724
725         if not model_obj:
726             _logger.error("Unable to find object %r", model)
727             continue
728
729         for constraint in getattr(model_obj, '_constraints', []):
730             push_constraint_msg(module, 'constraint', model, constraint[1])
731
732         for constraint in getattr(model_obj, '_sql_constraints', []):
733             push_constraint_msg(module, 'sql_constraint', model, constraint[2])
734
735     # parse source code for _() calls
736     def get_module_from_path(path, mod_paths=None):
737         if not mod_paths:
738             # First, construct a list of possible paths
739             def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))     # default addons path (base)
740             ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
741             mod_paths=[def_path]
742             for adp in ad_paths:
743                 mod_paths.append(adp)
744                 if not os.path.isabs(adp):
745                     mod_paths.append(adp)
746                 elif adp.startswith(def_path):
747                     mod_paths.append(adp[len(def_path)+1:])
748         for mp in mod_paths:
749             if path.startswith(mp) and (os.path.dirname(path) != mp):
750                 path = path[len(mp)+1:]
751                 return path.split(os.path.sep)[0]
752         return 'base'   # files that are not in a module are considered as being in 'base' module
753
754     modobj = pool.get('ir.module.module')
755     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
756     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
757
758     root_path = os.path.join(config.config['root_path'], 'addons')
759
760     apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
761     if root_path in apaths:
762         path_list = apaths
763     else :
764         path_list = [root_path,] + apaths
765
766     # Also scan these non-addon paths
767     for bin_path in ['osv', 'report' ]:
768         path_list.append(os.path.join(config.config['root_path'], bin_path))
769
770     _logger.debug("Scanning modules at paths: ", path_list)
771
772     mod_paths = []
773     join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
774     join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
775     re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
776     re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
777
778     def export_code_terms_from_file(fname, path, root, terms_type):
779         fabsolutepath = join(root, fname)
780         frelativepath = fabsolutepath[len(path):]
781         module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
782         is_mod_installed = module in installed_modules
783         if (('all' in modules) or (module in modules)) and is_mod_installed:
784             _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
785             src_file = misc.file_open(fabsolutepath, subdir='')
786             try:
787                 code_string = src_file.read()
788             finally:
789                 src_file.close()
790             if module in installed_modules:
791                 frelativepath = str("addons" + frelativepath)
792             ite = re_dquotes.finditer(code_string)
793             code_offset = 0
794             code_line = 1
795             for i in ite:
796                 src = i.group(1)
797                 if src.startswith('""'):
798                     assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
799                     src = src[2:-2]
800                 else:
801                     src = join_dquotes.sub(r'\1', src)
802                 # try to count the lines from the last pos to our place:
803                 code_line += code_string[code_offset:i.start(1)].count('\n')
804                 # now, since we did a binary read of a python source file, we
805                 # have to expand pythonic escapes like the interpreter does.
806                 src = src.decode('string_escape')
807                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
808                 code_line += i.group(1).count('\n')
809                 code_offset = i.end() # we have counted newlines up to the match end
810
811             ite = re_quotes.finditer(code_string)
812             code_offset = 0 #reset counters
813             code_line = 1
814             for i in ite:
815                 src = i.group(1)
816                 if src.startswith("''"):
817                     assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
818                     src = src[2:-2]
819                 else:
820                     src = join_quotes.sub(r'\1', src)
821                 code_line += code_string[code_offset:i.start(1)].count('\n')
822                 src = src.decode('string_escape')
823                 push_translation(module, terms_type, frelativepath, code_line, encode(src))
824                 code_line += i.group(1).count('\n')
825                 code_offset = i.end() # we have counted newlines up to the match end
826
827     for path in path_list:
828         _logger.debug("Scanning files of modules at %s", path)
829         for root, dummy, files in osutil.walksymlinks(path):
830             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
831                 export_code_terms_from_file(fname, path, root, 'code')
832             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
833                 export_code_terms_from_file(fname, path, root, 'report')
834
835
836     out = [["module","type","name","res_id","src","value"]] # header
837     _to_translate.sort()
838     # translate strings marked as to be translated
839     for module, source, name, id, type in _to_translate:
840         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
841         out.append([module, type, name, id, source, encode(trans) or ''])
842
843     return out
844
845 def trans_load(cr, filename, lang, verbose=True, context=None):
846     try:
847         fileobj = misc.file_open(filename)
848         _logger.info("loading %s", filename)
849         fileformat = os.path.splitext(filename)[-1][1:].lower()
850         r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
851         fileobj.close()
852         return r
853     except IOError:
854         if verbose:
855             _logger.error("couldn't read translation file %s", filename)
856         return None
857
858 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
859     """Populates the ir_translation table."""
860     if verbose:
861         _logger.info('loading translation file for language %s', lang)
862     if context is None:
863         context = {}
864     db_name = cr.dbname
865     pool = pooler.get_pool(db_name)
866     lang_obj = pool.get('res.lang')
867     trans_obj = pool.get('ir.translation')
868     iso_lang = misc.get_iso_codes(lang)
869     try:
870         uid = 1
871         ids = lang_obj.search(cr, uid, [('code','=', lang)])
872
873         if not ids:
874             # lets create the language with locale information
875             lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
876
877
878         # now, the serious things: we read the language file
879         fileobj.seek(0)
880         if fileformat == 'csv':
881             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
882             # read the first line of the file (it contains columns titles)
883             for row in reader:
884                 f = row
885                 break
886         elif fileformat == 'po':
887             reader = TinyPoFile(fileobj)
888             f = ['type', 'name', 'res_id', 'src', 'value']
889         else:
890             _logger.error('Bad file format: %s', fileformat)
891             raise Exception(_('Bad file format'))
892
893         # read the rest of the file
894         line = 1
895         irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
896
897         for row in reader:
898             line += 1
899             # skip empty rows and rows where the translation field (=last fiefd) is empty
900             #if (not row) or (not row[-1]):
901             #    continue
902
903             # dictionary which holds values for this line of the csv file
904             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
905             #  'src': ..., 'value': ...}
906             dic = {'lang': lang}
907             dic_module = False
908             for i in range(len(f)):
909                 if f[i] in ('module',):
910                     continue
911                 dic[f[i]] = row[i]
912
913             # This would skip terms that fail to specify a res_id
914             if not dic.get('res_id', False):
915                 continue
916
917             res_id = dic.pop('res_id')
918             if res_id and isinstance(res_id, (int, long)) \
919                 or (isinstance(res_id, basestring) and res_id.isdigit()):
920                     dic['res_id'] = int(res_id)
921             else:
922                 try:
923                     tmodel = dic['name'].split(',')[0]
924                     if '.' in res_id:
925                         tmodule, tname = res_id.split('.', 1)
926                     else:
927                         tmodule = dic_module
928                         tname = res_id
929                     dic['imd_model'] = tmodel
930                     dic['imd_module'] = tmodule
931                     dic['imd_name'] =  tname
932
933                     dic['res_id'] = None
934                 except Exception:
935                     _logger.warning("Could not decode resource for %s, please fix the po file.",
936                                     dic['res_id'], exc_info=True)
937                     dic['res_id'] = None
938
939             irt_cursor.push(dic)
940
941         irt_cursor.finish()
942         if verbose:
943             _logger.info("translation file loaded succesfully")
944     except IOError:
945         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
946         _logger.exception("couldn't read translation file %s", filename)
947
948 def get_locales(lang=None):
949     if lang is None:
950         lang = locale.getdefaultlocale()[0]
951
952     if os.name == 'nt':
953         lang = _LOCALE2WIN32.get(lang, lang)
954
955     def process(enc):
956         ln = locale._build_localename((lang, enc))
957         yield ln
958         nln = locale.normalize(ln)
959         if nln != ln:
960             yield nln
961
962     for x in process('utf8'): yield x
963
964     prefenc = locale.getpreferredencoding()
965     if prefenc:
966         for x in process(prefenc): yield x
967
968         prefenc = {
969             'latin1': 'latin9',
970             'iso-8859-1': 'iso8859-15',
971             'cp1252': '1252',
972         }.get(prefenc.lower())
973         if prefenc:
974             for x in process(prefenc): yield x
975
976     yield lang
977
978
979
980 def resetlocale():
981     # locale.resetlocale is bugged with some locales.
982     for ln in get_locales():
983         try:
984             return locale.setlocale(locale.LC_ALL, ln)
985         except locale.Error:
986             continue
987
988 def load_language(cr, lang):
989     """Loads a translation terms for a language.
990     Used mainly to automate language loading at db initialization.
991
992     :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
993     :type lang: str
994     """
995     pool = pooler.get_pool(cr.dbname)
996     language_installer = pool.get('base.language.install')
997     uid = 1
998     oid = language_installer.create(cr, uid, {'lang': lang})
999     language_installer.lang_install(cr, uid, [oid], context=None)
1000
1001 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1002