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