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