[IMP] tools.translate: unused import
[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 locale
27 import os
28 import openerp.sql_db as sql_db
29 import re
30 import logging
31 import tarfile
32 import tempfile
33 import threading
34 from babel.messages import extract
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 SKIPPED_ELEMENT_TYPES
43 import osutil
44 import openerp
45 from openerp import SUPERUSER_ID
46
47 _logger = logging.getLogger(__name__)
48
49 # used to notify web client that these translations should be loaded in the UI
50 WEB_TRANSLATION_COMMENT = "openerp-web"
51
52 _LOCALE2WIN32 = {
53     'af_ZA': 'Afrikaans_South Africa',
54     'sq_AL': 'Albanian_Albania',
55     'ar_SA': 'Arabic_Saudi Arabia',
56     'eu_ES': 'Basque_Spain',
57     'be_BY': 'Belarusian_Belarus',
58     'bs_BA': 'Serbian (Latin)',
59     'bg_BG': 'Bulgarian_Bulgaria',
60     'ca_ES': 'Catalan_Spain',
61     'hr_HR': 'Croatian_Croatia',
62     'zh_CN': 'Chinese_China',
63     'zh_TW': 'Chinese_Taiwan',
64     'cs_CZ': 'Czech_Czech Republic',
65     'da_DK': 'Danish_Denmark',
66     'nl_NL': 'Dutch_Netherlands',
67     'et_EE': 'Estonian_Estonia',
68     'fa_IR': 'Farsi_Iran',
69     'ph_PH': 'Filipino_Philippines',
70     'fi_FI': 'Finnish_Finland',
71     'fr_FR': 'French_France',
72     'fr_BE': 'French_France',
73     'fr_CH': 'French_France',
74     'fr_CA': 'French_France',
75     'ga': 'Scottish Gaelic',
76     'gl_ES': 'Galician_Spain',
77     'ka_GE': 'Georgian_Georgia',
78     'de_DE': 'German_Germany',
79     'el_GR': 'Greek_Greece',
80     'gu': 'Gujarati_India',
81     'he_IL': 'Hebrew_Israel',
82     'hi_IN': 'Hindi',
83     'hu': 'Hungarian_Hungary',
84     'is_IS': 'Icelandic_Iceland',
85     'id_ID': 'Indonesian_indonesia',
86     'it_IT': 'Italian_Italy',
87     'ja_JP': 'Japanese_Japan',
88     'kn_IN': 'Kannada',
89     'km_KH': 'Khmer',
90     'ko_KR': 'Korean_Korea',
91     'lo_LA': 'Lao_Laos',
92     'lt_LT': 'Lithuanian_Lithuania',
93     'lat': 'Latvian_Latvia',
94     'ml_IN': 'Malayalam_India',
95     'mi_NZ': 'Maori',
96     'mn': 'Cyrillic_Mongolian',
97     'no_NO': 'Norwegian_Norway',
98     'nn_NO': 'Norwegian-Nynorsk_Norway',
99     'pl': 'Polish_Poland',
100     'pt_PT': 'Portuguese_Portugal',
101     'pt_BR': 'Portuguese_Brazil',
102     'ro_RO': 'Romanian_Romania',
103     'ru_RU': 'Russian_Russia',
104     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
105     'sk_SK': 'Slovak_Slovakia',
106     'sl_SI': 'Slovenian_Slovenia',
107     #should find more specific locales for spanish countries,
108     #but better than nothing
109     'es_AR': 'Spanish_Spain',
110     'es_BO': 'Spanish_Spain',
111     'es_CL': 'Spanish_Spain',
112     'es_CO': 'Spanish_Spain',
113     'es_CR': 'Spanish_Spain',
114     'es_DO': 'Spanish_Spain',
115     'es_EC': 'Spanish_Spain',
116     'es_ES': 'Spanish_Spain',
117     'es_GT': 'Spanish_Spain',
118     'es_HN': 'Spanish_Spain',
119     'es_MX': 'Spanish_Spain',
120     'es_NI': 'Spanish_Spain',
121     'es_PA': 'Spanish_Spain',
122     'es_PE': 'Spanish_Spain',
123     'es_PR': 'Spanish_Spain',
124     'es_PY': 'Spanish_Spain',
125     'es_SV': 'Spanish_Spain',
126     'es_UY': 'Spanish_Spain',
127     'es_VE': 'Spanish_Spain',
128     'sv_SE': 'Swedish_Sweden',
129     'ta_IN': 'English_Australia',
130     'th_TH': 'Thai_Thailand',
131     '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         # try, in order: cr, cursor, self.env.cr, self.cr
168         if 'cr' in frame.f_locals:
169             return frame.f_locals['cr'], False
170         if 'cursor' in frame.f_locals:
171             return frame.f_locals['cursor'], False
172         s = frame.f_locals.get('self')
173         if hasattr(s, 'env'):
174             return s.env.cr, False
175         if hasattr(s, 'cr'):
176             return s.cr, False
177         if allow_create:
178             # create a new cursor
179             db = self._get_db()
180             if db is not None:
181                 return db.cursor(), True
182         return None, False
183
184     def _get_uid(self, frame):
185         # try, in order: uid, user, self.env.uid
186         if 'uid' in frame.f_locals:
187             return frame.f_locals['uid']
188         if 'user' in frame.f_locals:
189             return int(frame.f_locals['user'])      # user may be a record
190         s = frame.f_locals.get('self')
191         return s.env.uid
192
193     def _get_lang(self, frame):
194         # try, in order: context.get('lang'), kwargs['context'].get('lang'),
195         # self.env.lang, self.localcontext.get('lang')
196         if 'context' in frame.f_locals:
197             return frame.f_locals['context'].get('lang')
198         kwargs = frame.f_locals.get('kwargs', {})
199         if 'context' in kwargs:
200             return kwargs['context'].get('lang')
201         s = frame.f_locals.get('self')
202         if hasattr(s, 'env'):
203             return s.env.lang
204         if hasattr(s, 'localcontext'):
205             return s.localcontext.get('lang')
206         # Last resort: attempt to guess the language of the user
207         # Pitfall: some operations are performed in sudo mode, and we
208         #          don't know the originial uid, so the language may
209         #          be wrong when the admin language differs.
210         pool = getattr(s, 'pool', None)
211         (cr, dummy) = self._get_cr(frame, allow_create=False)
212         uid = self._get_uid(frame)
213         if pool and cr and uid:
214             return pool['res.users'].context_get(cr, uid)['lang']
215         return None
216
217     def __call__(self, source):
218         res = source
219         cr = None
220         is_new_cr = False
221         try:
222             frame = inspect.currentframe()
223             if frame is None:
224                 return source
225             frame = frame.f_back
226             if not frame:
227                 return source
228             lang = self._get_lang(frame)
229             if lang:
230                 cr, is_new_cr = self._get_cr(frame)
231                 if cr:
232                     # Try to use ir.translation to benefit from global cache if possible
233                     registry = openerp.registry(cr.dbname)
234                     res = registry['ir.translation']._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
235                 else:
236                     _logger.debug('no context cursor detected, skipping translation for "%r"', source)
237             else:
238                 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
239         except Exception:
240             _logger.debug('translation went wrong for "%r", skipped', source)
241                 # if so, double-check the root/base translations filenames
242         finally:
243             if cr and is_new_cr:
244                 cr.close()
245         return res
246
247 _ = GettextAlias()
248
249
250 def quote(s):
251     """Returns quoted PO term string, with special PO characters escaped"""
252     assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
253     return '"%s"' % s.replace('\\','\\\\') \
254                      .replace('"','\\"') \
255                      .replace('\n', '\\n"\n"')
256
257 re_escaped_char = re.compile(r"(\\.)")
258 re_escaped_replacements = {'n': '\n', }
259
260 def _sub_replacement(match_obj):
261     return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
262
263 def unquote(str):
264     """Returns unquoted PO term string, with special PO characters unescaped"""
265     return re_escaped_char.sub(_sub_replacement, str[1:-1])
266
267 # class to handle po files
268 class TinyPoFile(object):
269     def __init__(self, buffer):
270         self.buffer = buffer
271
272     def warn(self, msg, *args):
273         _logger.warning(msg, *args)
274
275     def __iter__(self):
276         self.buffer.seek(0)
277         self.lines = self._get_lines()
278         self.lines_count = len(self.lines)
279
280         self.first = True
281         self.extra_lines= []
282         return self
283
284     def _get_lines(self):
285         lines = self.buffer.readlines()
286         # remove the BOM (Byte Order Mark):
287         if len(lines):
288             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
289
290         lines.append('') # ensure that the file ends with at least an empty line
291         return lines
292
293     def cur_line(self):
294         return self.lines_count - len(self.lines)
295
296     def next(self):
297         trans_type = name = res_id = source = trad = None
298         if self.extra_lines:
299             trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0)
300             if not res_id:
301                 res_id = '0'
302         else:
303             comments = []
304             targets = []
305             line = None
306             fuzzy = False
307             while not line:
308                 if 0 == len(self.lines):
309                     raise StopIteration()
310                 line = self.lines.pop(0).strip()
311             while line.startswith('#'):
312                 if line.startswith('#~ '):
313                     break
314                 if line.startswith('#.'):
315                     line = line[2:].strip()
316                     if not line.startswith('module:'):
317                         comments.append(line)
318                 elif line.startswith('#:'):
319                     for lpart in line[2:].strip().split(' '):
320                         trans_info = lpart.strip().split(':',2)
321                         if trans_info and len(trans_info) == 2:
322                             # looks like the translation trans_type is missing, which is not
323                             # unexpected because it is not a GetText standard. Default: 'code'
324                             trans_info[:0] = ['code']
325                         if trans_info and len(trans_info) == 3:
326                             # this is a ref line holding the destination info (model, field, record)
327                             targets.append(trans_info)
328                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
329                     fuzzy = True
330                 line = self.lines.pop(0).strip()
331             while not line:
332                 # allow empty lines between comments and msgid
333                 line = self.lines.pop(0).strip()
334             if line.startswith('#~ '):
335                 while line.startswith('#~ ') or not line.strip():
336                     if 0 == len(self.lines):
337                         raise StopIteration()
338                     line = self.lines.pop(0)
339                 # This has been a deprecated entry, don't return anything
340                 return self.next()
341
342             if not line.startswith('msgid'):
343                 raise Exception("malformed file: bad line: %s" % line)
344             source = unquote(line[6:])
345             line = self.lines.pop(0).strip()
346             if not source and self.first:
347                 # if the source is "" and it's the first msgid, it's the special
348                 # msgstr with the informations about the traduction and the
349                 # traductor; we skip it
350                 self.extra_lines = []
351                 while line:
352                     line = self.lines.pop(0).strip()
353                 return self.next()
354
355             while not line.startswith('msgstr'):
356                 if not line:
357                     raise Exception('malformed file at %d'% self.cur_line())
358                 source += unquote(line)
359                 line = self.lines.pop(0).strip()
360
361             trad = unquote(line[7:])
362             line = self.lines.pop(0).strip()
363             while line:
364                 trad += unquote(line)
365                 line = self.lines.pop(0).strip()
366
367             if targets and not fuzzy:
368                 trans_type, name, res_id = targets.pop(0)
369                 for t, n, r in targets:
370                     if t == trans_type == 'code': continue
371                     self.extra_lines.append((t, n, r, source, trad, comments))
372
373         self.first = False
374
375         if name is None:
376             if not fuzzy:
377                 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
378                         self.cur_line(), source[:30])
379             return self.next()
380         return trans_type, name, res_id, source, trad, '\n'.join(comments)
381
382     def write_infos(self, modules):
383         import openerp.release as release
384         self.buffer.write("# Translation of %(project)s.\n" \
385                           "# This file contains the translation of the following modules:\n" \
386                           "%(modules)s" \
387                           "#\n" \
388                           "msgid \"\"\n" \
389                           "msgstr \"\"\n" \
390                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
391                           '''"Report-Msgid-Bugs-To: \\n"\n''' \
392                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
393                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
394                           '''"Last-Translator: <>\\n"\n''' \
395                           '''"Language-Team: \\n"\n'''   \
396                           '''"MIME-Version: 1.0\\n"\n''' \
397                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
398                           '''"Content-Transfer-Encoding: \\n"\n'''       \
399                           '''"Plural-Forms: \\n"\n'''    \
400                           "\n"
401
402                           % { 'project': release.description,
403                               'version': release.version,
404                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
405                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
406                             }
407                           )
408
409     def write(self, modules, tnrs, source, trad, comments=None):
410
411         plurial = len(modules) > 1 and 's' or ''
412         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
413
414         if comments:
415             self.buffer.write(''.join(('#. %s\n' % c for c in comments)))
416
417         code = False
418         for typy, name, res_id in tnrs:
419             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
420             if typy == 'code':
421                 code = True
422
423         if code:
424             # only strings in python code are python formated
425             self.buffer.write("#, python-format\n")
426
427         if not isinstance(trad, unicode):
428             trad = unicode(trad, 'utf8')
429         if not isinstance(source, unicode):
430             source = unicode(source, 'utf8')
431
432         msg = "msgid %s\n"      \
433               "msgstr %s\n\n"   \
434                   % (quote(source), quote(trad))
435         self.buffer.write(msg.encode('utf8'))
436
437
438 # Methods to export the translation file
439
440 def trans_export(lang, modules, buffer, format, cr):
441
442     def _process(format, modules, rows, buffer, lang):
443         if format == 'csv':
444             writer = csv.writer(buffer, 'UNIX')
445             # write header first
446             writer.writerow(("module","type","name","res_id","src","value"))
447             for module, type, name, res_id, src, trad, comments in rows:
448                 # Comments are ignored by the CSV writer
449                 writer.writerow((module, type, name, res_id, src, trad))
450         elif format == 'po':
451             writer = TinyPoFile(buffer)
452             writer.write_infos(modules)
453
454             # we now group the translations by source. That means one translation per source.
455             grouped_rows = {}
456             for module, type, name, res_id, src, trad, comments in rows:
457                 row = grouped_rows.setdefault(src, {})
458                 row.setdefault('modules', set()).add(module)
459                 if not row.get('translation') and trad != src:
460                     row['translation'] = trad
461                 row.setdefault('tnrs', []).append((type, name, res_id))
462                 row.setdefault('comments', set()).update(comments)
463
464             for src, row in sorted(grouped_rows.items()):
465                 if not lang:
466                     # translation template, so no translation value
467                     row['translation'] = ''
468                 elif not row.get('translation'):
469                     row['translation'] = src
470                 writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments'])
471
472         elif format == 'tgz':
473             rows_by_module = {}
474             for row in rows:
475                 module = row[0]
476                 rows_by_module.setdefault(module, []).append(row)
477             tmpdir = tempfile.mkdtemp()
478             for mod, modrows in rows_by_module.items():
479                 tmpmoddir = join(tmpdir, mod, 'i18n')
480                 os.makedirs(tmpmoddir)
481                 pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '')
482                 buf = file(join(tmpmoddir, pofilename), 'w')
483                 _process('po', [mod], modrows, buf, lang)
484                 buf.close()
485
486             tar = tarfile.open(fileobj=buffer, mode='w|gz')
487             tar.add(tmpdir, '')
488             tar.close()
489
490         else:
491             raise Exception(_('Unrecognized extension: must be one of '
492                 '.csv, .po, or .tgz (received .%s).' % format))
493
494     trans_lang = lang
495     if not trans_lang and format == 'csv':
496         # CSV files are meant for translators and they need a starting point,
497         # so we at least put the original term in the translation column
498         trans_lang = 'en_US'
499     translations = trans_generate(lang, modules, cr)
500     modules = set([t[0] for t in translations[1:]])
501     _process(format, modules, translations, buffer, lang)
502     del translations
503
504 def trans_parse_xsl(de):
505     return list(set(trans_parse_xsl_aux(de, False)))
506
507 def trans_parse_xsl_aux(de, t):
508     res = []
509
510     for n in de:
511         t = t or n.get("t")
512         if t:
513                 if isinstance(n, SKIPPED_ELEMENT_TYPES) or n.tag.startswith('{http://www.w3.org/1999/XSL/Transform}'):
514                     continue
515                 if n.text:
516                     l = n.text.strip().replace('\n',' ')
517                     if len(l):
518                         res.append(l.encode("utf8"))
519                 if n.tail:
520                     l = n.tail.strip().replace('\n',' ')
521                     if len(l):
522                         res.append(l.encode("utf8"))
523         res.extend(trans_parse_xsl_aux(n, t))
524     return res
525
526 def trans_parse_rml(de):
527     res = []
528     for n in de:
529         for m in n:
530             if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
531                 continue
532             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
533             for s in string_list:
534                 if s:
535                     res.append(s.encode("utf8"))
536         res.extend(trans_parse_rml(n))
537     return res
538
539 def trans_parse_view(de):
540     res = []
541     if not isinstance(de, SKIPPED_ELEMENT_TYPES) and de.text and de.text.strip():
542         res.append(de.text.strip().encode("utf8"))
543     if de.tail and de.tail.strip():
544         res.append(de.tail.strip().encode("utf8"))
545     if de.tag == 'attribute' and de.get("name") == 'string':
546         if de.text:
547             res.append(de.text.encode("utf8"))
548     if de.get("string"):
549         res.append(de.get('string').encode("utf8"))
550     if de.get("help"):
551         res.append(de.get('help').encode("utf8"))
552     if de.get("sum"):
553         res.append(de.get('sum').encode("utf8"))
554     if de.get("confirm"):
555         res.append(de.get('confirm').encode("utf8"))
556     if de.get("placeholder"):
557         res.append(de.get('placeholder').encode("utf8"))
558     for n in de:
559         res.extend(trans_parse_view(n))
560     return res
561
562 # tests whether an object is in a list of modules
563 def in_modules(object_name, modules):
564     if 'all' in modules:
565         return True
566
567     module_dict = {
568         'ir': 'base',
569         'res': 'base',
570         'workflow': 'base',
571     }
572     module = object_name.split('.')[0]
573     module = module_dict.get(module, module)
574     return module in modules
575
576
577 def babel_extract_qweb(fileobj, keywords, comment_tags, options):
578     """Babel message extractor for qweb template files.
579     :param fileobj: the file-like object the messages should be extracted from
580     :param keywords: a list of keywords (i.e. function names) that should
581                      be recognized as translation functions
582     :param comment_tags: a list of translator tags to search for and
583                          include in the results
584     :param options: a dictionary of additional options (optional)
585     :return: an iterator over ``(lineno, funcname, message, comments)``
586              tuples
587     :rtype: ``iterator``
588     """
589     result = []
590     def handle_text(text, lineno):
591         text = (text or "").strip()
592         if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
593             result.append((lineno, None, text, []))
594
595     # not using elementTree.iterparse because we need to skip sub-trees in case
596     # the ancestor element had a reason to be skipped
597     def iter_elements(current_element):
598         for el in current_element:
599             if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
600             if "t-js" not in el.attrib and \
601                     not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
602                     not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
603                 handle_text(el.text, el.sourceline)
604                 for att in ('title', 'alt', 'label', 'placeholder'):
605                     if att in el.attrib:
606                         handle_text(el.attrib[att], el.sourceline)
607                 iter_elements(el)
608             handle_text(el.tail, el.sourceline)
609
610     tree = etree.parse(fileobj)
611     iter_elements(tree.getroot())
612
613     return result
614
615
616 def trans_generate(lang, modules, cr):
617     dbname = cr.dbname
618
619     registry = openerp.registry(dbname)
620     trans_obj = registry.get('ir.translation')
621     model_data_obj = registry.get('ir.model.data')
622     uid = 1
623     l = registry.models.items()
624     l.sort()
625
626     query = 'SELECT name, model, res_id, module'    \
627             '  FROM ir_model_data'
628
629     query_models = """SELECT m.id, m.model, imd.module
630             FROM ir_model AS m, ir_model_data AS imd
631             WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
632
633     if 'all_installed' in modules:
634         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
635         query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
636     query_param = None
637     if 'all' not in modules:
638         query += ' WHERE module IN %s'
639         query_models += ' AND imd.module in %s'
640         query_param = (tuple(modules),)
641     query += ' ORDER BY module, model, name'
642     query_models += ' ORDER BY module, model'
643
644     cr.execute(query, query_param)
645
646     _to_translate = []
647     def push_translation(module, type, name, id, source, comments=None):
648         tuple = (module, source, name, id, type, comments or [])
649         # empty and one-letter terms are ignored, they probably are not meant to be
650         # translated, and would be very hard to translate anyway.
651         if not source or len(source.strip()) <= 1:
652             _logger.debug("Ignoring empty or 1-letter source term: %r", tuple)
653             return
654         if tuple not in _to_translate:
655             _to_translate.append(tuple)
656
657     def encode(s):
658         if isinstance(s, unicode):
659             return s.encode('utf8')
660         return s
661
662     for (xml_name,model,res_id,module) in cr.fetchall():
663         module = encode(module)
664         model = encode(model)
665         xml_name = "%s.%s" % (module, encode(xml_name))
666
667         if model not in registry:
668             _logger.error("Unable to find object %r", model)
669             continue
670
671         exists = registry[model].exists(cr, uid, res_id)
672         if not exists:
673             _logger.warning("Unable to find object %r with id %d", model, res_id)
674             continue
675         obj = registry[model].browse(cr, uid, res_id)
676
677         if model=='ir.ui.view':
678             d = etree.XML(encode(obj.arch))
679             for t in trans_parse_view(d):
680                 push_translation(module, 'view', encode(obj.model), 0, t)
681         elif model=='ir.actions.wizard':
682             pass # TODO Can model really be 'ir.actions.wizard' ?
683
684         elif model=='ir.model.fields':
685             try:
686                 field_name = encode(obj.name)
687             except AttributeError, exc:
688                 _logger.error("name error in %s: %s", xml_name, str(exc))
689                 continue
690             objmodel = registry.get(obj.model)
691             if objmodel is None or field_name not in objmodel._columns:
692                 continue
693             field_def = objmodel._columns[field_name]
694
695             name = "%s,%s" % (encode(obj.model), field_name)
696             push_translation(module, 'field', name, 0, encode(field_def.string))
697
698             if field_def.help:
699                 push_translation(module, 'help', name, 0, encode(field_def.help))
700
701             if field_def.translate:
702                 ids = objmodel.search(cr, uid, [])
703                 obj_values = objmodel.read(cr, uid, ids, [field_name])
704                 for obj_value in obj_values:
705                     res_id = obj_value['id']
706                     if obj.name in ('ir.model', 'ir.ui.menu'):
707                         res_id = 0
708                     model_data_ids = model_data_obj.search(cr, uid, [
709                         ('model', '=', model),
710                         ('res_id', '=', res_id),
711                         ])
712                     if not model_data_ids:
713                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
714
715             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
716                 for dummy, val in field_def.selection:
717                     push_translation(module, 'selection', name, 0, encode(val))
718
719         elif model=='ir.actions.report.xml':
720             name = encode(obj.report_name)
721             fname = ""
722             if obj.report_rml:
723                 fname = obj.report_rml
724                 parse_func = trans_parse_rml
725                 report_type = "report"
726             elif obj.report_xsl:
727                 fname = obj.report_xsl
728                 parse_func = trans_parse_xsl
729                 report_type = "xsl"
730             if fname and obj.report_type in ('pdf', 'xsl'):
731                 try:
732                     report_file = misc.file_open(fname)
733                     try:
734                         d = etree.parse(report_file)
735                         for t in parse_func(d.iter()):
736                             push_translation(module, report_type, name, 0, t)
737                     finally:
738                         report_file.close()
739                 except (IOError, etree.XMLSyntaxError):
740                     _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
741
742         for field_name, field_def in obj._columns.items():
743             if field_def.translate:
744                 name = model + "," + field_name
745                 try:
746                     trad = getattr(obj, field_name) or ''
747                 except:
748                     trad = ''
749                 push_translation(module, 'model', name, xml_name, encode(trad))
750
751         # End of data for ir.model.data query results
752
753     cr.execute(query_models, query_param)
754
755     def push_constraint_msg(module, term_type, model, msg):
756         if not hasattr(msg, '__call__'):
757             push_translation(encode(module), term_type, encode(model), 0, encode(msg))
758
759     def push_local_constraints(module, model, cons_type='sql_constraints'):
760         """Climb up the class hierarchy and ignore inherited constraints
761            from other modules"""
762         term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
763         msg_pos = 2 if cons_type == 'sql_constraints' else 1
764         for cls in model.__class__.__mro__:
765             if getattr(cls, '_module', None) != module:
766                 continue
767             constraints = getattr(cls, '_local_' + cons_type, [])
768             for constraint in constraints:
769                 push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
770             
771     for (_, model, module) in cr.fetchall():
772         if model not in registry:
773             _logger.error("Unable to find object %r", model)
774             continue
775
776         model_obj = registry[model]
777
778         if model_obj._constraints:
779             push_local_constraints(module, model_obj, 'constraints')
780
781         if model_obj._sql_constraints:
782             push_local_constraints(module, model_obj, 'sql_constraints')
783
784
785     modobj = registry['ir.module.module']
786     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
787     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
788
789     path_list = list(openerp.modules.module.ad_paths)
790     # Also scan these non-addon paths
791     for bin_path in ['osv', 'report' ]:
792         path_list.append(os.path.join(config.config['root_path'], bin_path))
793
794     _logger.debug("Scanning modules at paths: ", path_list)
795
796     mod_paths = list(path_list)
797
798     def get_module_from_path(path):
799         for mp in mod_paths:
800             if path.startswith(mp) and (os.path.dirname(path) != mp):
801                 path = path[len(mp)+1:]
802                 return path.split(os.path.sep)[0]
803         return 'base'   # files that are not in a module are considered as being in 'base' module
804
805     def verified_module_filepaths(fname, path, root):
806         fabsolutepath = join(root, fname)
807         frelativepath = fabsolutepath[len(path):]
808         display_path = "addons%s" % frelativepath
809         module = get_module_from_path(fabsolutepath)
810         if ('all' in modules or module in modules) and module in installed_modules:
811             return module, fabsolutepath, frelativepath, display_path
812         return None, None, None, None
813
814     def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code',
815                                extra_comments=None, extract_keywords={'_': None}):
816         module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root)
817         extra_comments = extra_comments or []
818         if module:
819             src_file = open(fabsolutepath, 'r')
820             try:
821                 for extracted in extract.extract(extract_method, src_file,
822                                                  keywords=extract_keywords):
823                     # Babel 0.9.6 yields lineno, message, comments
824                     # Babel 1.3 yields lineno, message, comments, context
825                     lineno, message, comments = extracted[:3] 
826                     push_translation(module, trans_type, display_path, lineno,
827                                      encode(message), comments + extra_comments)
828             except Exception:
829                 _logger.exception("Failed to extract terms from %s", fabsolutepath)
830             finally:
831                 src_file.close()
832
833     for path in path_list:
834         _logger.debug("Scanning files of modules at %s", path)
835         for root, dummy, files in osutil.walksymlinks(path):
836             for fname in fnmatch.filter(files, '*.py'):
837                 babel_extract_terms(fname, path, root)
838             # mako provides a babel extractor: http://docs.makotemplates.org/en/latest/usage.html#babel
839             for fname in fnmatch.filter(files, '*.mako'):
840                 babel_extract_terms(fname, path, root, 'mako', trans_type='report')
841             # Javascript source files in the static/src/js directory, rest is ignored (libs)
842             if fnmatch.fnmatch(root, '*/static/src/js*'):
843                 for fname in fnmatch.filter(files, '*.js'):
844                     babel_extract_terms(fname, path, root, 'javascript',
845                                         extra_comments=[WEB_TRANSLATION_COMMENT],
846                                         extract_keywords={'_t': None, '_lt': None})
847             # QWeb template files
848             if fnmatch.fnmatch(root, '*/static/src/xml*'):
849                 for fname in fnmatch.filter(files, '*.xml'):
850                     babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb',
851                                         extra_comments=[WEB_TRANSLATION_COMMENT])
852
853     out = []
854     _to_translate.sort()
855     # translate strings marked as to be translated
856     for module, source, name, id, type, comments in _to_translate:
857         trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source)
858         out.append([module, type, name, id, source, encode(trans) or '', comments])
859     return out
860
861 def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None):
862     try:
863         fileobj = misc.file_open(filename)
864         _logger.info("loading %s", filename)
865         fileformat = os.path.splitext(filename)[-1][1:].lower()
866         result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
867         fileobj.close()
868         return result
869     except IOError:
870         if verbose:
871             _logger.error("couldn't read translation file %s", filename)
872         return None
873
874 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
875     """Populates the ir_translation table."""
876     if verbose:
877         _logger.info('loading translation file for language %s', lang)
878     if context is None:
879         context = {}
880     db_name = cr.dbname
881     registry = openerp.registry(db_name)
882     lang_obj = registry.get('res.lang')
883     trans_obj = registry.get('ir.translation')
884     iso_lang = misc.get_iso_codes(lang)
885     try:
886         ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
887
888         if not ids:
889             # lets create the language with locale information
890             lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
891
892
893         # now, the serious things: we read the language file
894         fileobj.seek(0)
895         if fileformat == 'csv':
896             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
897             # read the first line of the file (it contains columns titles)
898             for row in reader:
899                 f = row
900                 break
901         elif fileformat == 'po':
902             reader = TinyPoFile(fileobj)
903             f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
904         else:
905             _logger.error('Bad file format: %s', fileformat)
906             raise Exception(_('Bad file format'))
907
908         # read the rest of the file
909         line = 1
910         irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
911
912         for row in reader:
913             line += 1
914             # skip empty rows and rows where the translation field (=last fiefd) is empty
915             #if (not row) or (not row[-1]):
916             #    continue
917
918             # dictionary which holds values for this line of the csv file
919             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
920             #  'src': ..., 'value': ..., 'module':...}
921             dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
922             dic['lang'] = lang
923             for i, field in enumerate(f):
924                 dic[field] = row[i]
925
926             # This would skip terms that fail to specify a res_id
927             if not dic.get('res_id'):
928                 continue
929
930             res_id = dic.pop('res_id')
931             if res_id and isinstance(res_id, (int, long)) \
932                 or (isinstance(res_id, basestring) and res_id.isdigit()):
933                     dic['res_id'] = int(res_id)
934                     dic['module'] = module_name
935             else:
936                 tmodel = dic['name'].split(',')[0]
937                 if '.' in res_id:
938                     tmodule, tname = res_id.split('.', 1)
939                 else:
940                     tmodule = False
941                     tname = res_id
942                 dic['imd_model'] = tmodel
943                 dic['imd_name'] =  tname
944                 dic['module'] = tmodule
945                 dic['res_id'] = None
946
947             irt_cursor.push(dic)
948
949         irt_cursor.finish()
950         trans_obj.clear_caches()
951         if verbose:
952             _logger.info("translation file loaded succesfully")
953     except IOError:
954         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
955         _logger.exception("couldn't read translation file %s", filename)
956
957 def get_locales(lang=None):
958     if lang is None:
959         lang = locale.getdefaultlocale()[0]
960
961     if os.name == 'nt':
962         lang = _LOCALE2WIN32.get(lang, lang)
963
964     def process(enc):
965         ln = locale._build_localename((lang, enc))
966         yield ln
967         nln = locale.normalize(ln)
968         if nln != ln:
969             yield nln
970
971     for x in process('utf8'): yield x
972
973     prefenc = locale.getpreferredencoding()
974     if prefenc:
975         for x in process(prefenc): yield x
976
977         prefenc = {
978             'latin1': 'latin9',
979             'iso-8859-1': 'iso8859-15',
980             'cp1252': '1252',
981         }.get(prefenc.lower())
982         if prefenc:
983             for x in process(prefenc): yield x
984
985     yield lang
986
987
988
989 def resetlocale():
990     # locale.resetlocale is bugged with some locales.
991     for ln in get_locales():
992         try:
993             return locale.setlocale(locale.LC_ALL, ln)
994         except locale.Error:
995             continue
996
997 def load_language(cr, lang):
998     """Loads a translation terms for a language.
999     Used mainly to automate language loading at db initialization.
1000
1001     :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1002     :type lang: str
1003     """
1004     registry = openerp.registry(cr.dbname)
1005     language_installer = registry['base.language.install']
1006     oid = language_installer.create(cr, SUPERUSER_ID, {'lang': lang})
1007     language_installer.lang_install(cr, SUPERUSER_ID, [oid], context=None)
1008
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1010