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