[FIX] Export & Synchronize Translations Corrected
[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 re
30 import tarfile
31 import tempfile
32 from os.path import join
33 import logging
34
35 from datetime import datetime
36 from lxml import etree
37
38 import tools, pooler
39 import netsvc
40 from tools.misc import UpdateableStr
41
42 _LOCALE2WIN32 = {
43     'af_ZA': 'Afrikaans_South Africa',
44     'sq_AL': 'Albanian_Albania',
45     'ar_SA': 'Arabic_Saudi Arabia',
46     'eu_ES': 'Basque_Spain',
47     'be_BY': 'Belarusian_Belarus',
48     'bs_BA': 'Serbian (Latin)',
49     'bg_BG': 'Bulgarian_Bulgaria',
50     'ca_ES': 'Catalan_Spain',
51     'hr_HR': 'Croatian_Croatia',
52     'zh_CN': 'Chinese_China',
53     'zh_TW': 'Chinese_Taiwan',
54     'cs_CZ': 'Czech_Czech Republic',
55     'da_DK': 'Danish_Denmark',
56     'nl_NL': 'Dutch_Netherlands',
57     'et_EE': 'Estonian_Estonia',
58     'fa_IR': 'Farsi_Iran',
59     'ph_PH': 'Filipino_Philippines',
60     'fi_FI': 'Finnish_Finland',
61     'fr_FR': 'French_France',
62     'fr_BE': 'French_France',
63     'fr_CH': 'French_France',
64     'fr_CA': 'French_France',
65     'ga': 'Scottish Gaelic',
66     'gl_ES': 'Galician_Spain',
67     'ka_GE': 'Georgian_Georgia',
68     'de_DE': 'German_Germany',
69     'el_GR': 'Greek_Greece',
70     'gu': 'Gujarati_India',
71     'he_IL': 'Hebrew_Israel',
72     'hi_IN': 'Hindi',
73     'hu': 'Hungarian_Hungary',
74     'is_IS': 'Icelandic_Iceland',
75     'id_ID': 'Indonesian_indonesia',
76     'it_IT': 'Italian_Italy',
77     'ja_JP': 'Japanese_Japan',
78     'kn_IN': 'Kannada',
79     'km_KH': 'Khmer',
80     'ko_KR': 'Korean_Korea',
81     'lo_LA': 'Lao_Laos',
82     'lt_LT': 'Lithuanian_Lithuania',
83     'lat': 'Latvian_Latvia',
84     'ml_IN': 'Malayalam_India',
85     'id_ID': 'Indonesian_indonesia',
86     'mi_NZ': 'Maori',
87     'mn': 'Cyrillic_Mongolian',
88     'no_NO': 'Norwegian_Norway',
89     'nn_NO': 'Norwegian-Nynorsk_Norway',
90     'pl': 'Polish_Poland',
91     'pt_PT': 'Portuguese_Portugal',
92     'pt_BR': 'Portuguese_Brazil',
93     'ro_RO': 'Romanian_Romania',
94     'ru_RU': 'Russian_Russia',
95     'mi_NZ': 'Maori',
96     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
97     'sk_SK': 'Slovak_Slovakia',
98     'sl_SI': 'Slovenian_Slovenia',
99     'es_ES': 'Spanish_Spain',
100     'sv_SE': 'Swedish_Sweden',
101     'ta_IN': 'English_Australia',
102     'th_TH': 'Thai_Thailand',
103     'mi_NZ': 'Maori',
104     'tr_TR': 'Turkish_Turkey',
105     'uk_UA': 'Ukrainian_Ukraine',
106     'vi_VN': 'Vietnamese_Viet Nam',
107     'tlh_TLH': 'Klingon',
108
109 }
110
111
112 class UNIX_LINE_TERMINATOR(csv.excel):
113     lineterminator = '\n'
114
115 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
116
117 #
118 # Warning: better use self.pool.get('ir.translation')._get_source if you can
119 #
120 def translate(cr, name, source_type, lang, source=None):
121     if source and name:
122         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))
123     elif name:
124         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
125     elif source:
126         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
127     res_trans = cr.fetchone()
128     res = res_trans and res_trans[0] or False
129     return res
130
131 logger = logging.getLogger('translate')
132
133 class GettextAlias(object):
134
135     def _get_cr(self, frame):
136         is_new_cr = False
137         cr = frame.f_locals.get('cr')
138         if not cr:
139             s = frame.f_locals.get('self', {})
140             cr = getattr(s, 'cr', False)
141             if not cr:
142                 if frame.f_globals.get('pooler', False):
143                     # TODO: we should probably get rid of the 'is_new_cr' case: no cr in locals -> no translation for you
144                     dbs = frame.f_globals['pooler'].pool_dic.keys()
145                     if len(dbs) == 1:
146                         cr = pooler.get_db(dbs[0]).cursor()
147                         is_new_cr = True
148         return cr, is_new_cr
149     
150     def _get_lang(self, frame):
151         lang = frame.f_locals.get('context', {}).get('lang', False)
152         if not lang:
153             args = frame.f_locals.get('args', False)
154             if args:
155                 lang = args[-1].get('lang', False)
156             if not lang:
157                 s = frame.f_locals.get('self', {})
158                 c = getattr(s, 'localcontext', {})
159                 lang = c.get('lang', False)
160         return lang
161     
162     def __call__(self, source):
163         is_new_cr = False
164         res = source
165         try:
166             frame = inspect.stack()[1][0]
167             cr, is_new_cr = self._get_cr(frame)
168             lang = self._get_lang(frame)
169             if lang and cr:
170                 cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
171                 res_trans = cr.fetchone()
172                 res = res_trans and res_trans[0] or source
173         except:
174             logger.warn('translation went wrong for string %s', repr(source))
175         finally:
176             if is_new_cr:
177                 cr.close()
178         return res
179
180 _ = GettextAlias()
181
182
183 # class to handle po files
184 class TinyPoFile(object):
185     def __init__(self, buffer):
186         self.logger = netsvc.Logger()
187         self.buffer = buffer
188
189     def warn(self, msg):
190         self.logger.notifyChannel("i18n", netsvc.LOG_WARNING, msg)
191
192     def __iter__(self):
193         self.buffer.seek(0)
194         self.lines = self._get_lines()
195         self.lines_count = len(self.lines);
196
197         self.first = True
198         self.tnrs= []
199         return self
200
201     def _get_lines(self):
202         lines = self.buffer.readlines()
203         # remove the BOM (Byte Order Mark):
204         if len(lines):
205             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
206
207         lines.append('') # ensure that the file ends with at least an empty line
208         return lines
209
210     def cur_line(self):
211         return (self.lines_count - len(self.lines))
212
213     def next(self):
214         def unquote(str):
215             return str[1:-1].replace("\\n", "\n")   \
216                             .replace("\\\\ ", "\\ ") \
217                             .replace('\\"', '"')
218
219         type = name = res_id = source = trad = None
220
221         if self.tnrs:
222             type, name, res_id, source, trad = self.tnrs.pop(0)
223         else:
224             tmp_tnrs = []
225             line = None
226             fuzzy = False
227             while (not line):
228                 if 0 == len(self.lines):
229                     raise StopIteration()
230                 line = self.lines.pop(0).strip()
231             while line.startswith('#'):
232                 if line.startswith('#~ '):
233                     break
234                 if line.startswith('#:'):
235                     if ' ' in line[2:].strip():
236                         for lpart in line[2:].strip().split(' '):
237                             tmp_tnrs.append(lpart.strip().split(':',2))
238                     else:
239                         tmp_tnrs.append( line[2:].strip().split(':',2) )
240                 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
241                     fuzzy = True
242                 line = self.lines.pop(0).strip()
243             while not line:
244                 # allow empty lines between comments and msgid
245                 line = self.lines.pop(0).strip()
246             if line.startswith('#~ '):
247                 while line.startswith('#~ ') or not line.strip():
248                     if 0 == len(self.lines):
249                         raise StopIteration()
250                     line = self.lines.pop(0)
251                 # This has been a deprecated entry, don't return anything
252                 return self.next()
253
254             if not line.startswith('msgid'):
255                 raise Exception("malformed file: bad line: %s" % line)
256             source = unquote(line[6:])
257             line = self.lines.pop(0).strip()
258             if not source and self.first:
259                 # if the source is "" and it's the first msgid, it's the special
260                 # msgstr with the informations about the traduction and the
261                 # traductor; we skip it
262                 self.tnrs = []
263                 while line:
264                     line = self.lines.pop(0).strip()
265                 return self.next()
266
267             while not line.startswith('msgstr'):
268                 if not line:
269                     raise Exception('malformed file at %d'% self.cur_line())
270                 source += unquote(line)
271                 line = self.lines.pop(0).strip()
272
273             trad = unquote(line[7:])
274             line = self.lines.pop(0).strip()
275             while line:
276                 trad += unquote(line)
277                 line = self.lines.pop(0).strip()
278
279             if tmp_tnrs and not fuzzy:
280                 type, name, res_id = tmp_tnrs.pop(0)
281                 for t, n, r in tmp_tnrs:
282                     self.tnrs.append((t, n, r, source, trad))
283
284         self.first = False
285
286         if name is None:
287             self.warn('Missing "#:" formated comment for the following source:\n\t%s' % (source,))
288             return self.next()
289         return type, name, res_id, source, trad
290
291     def write_infos(self, modules):
292         import release
293         self.buffer.write("# Translation of %(project)s.\n" \
294                           "# This file contains the translation of the following modules:\n" \
295                           "%(modules)s" \
296                           "#\n" \
297                           "msgid \"\"\n" \
298                           "msgstr \"\"\n" \
299                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
300                           '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
301                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
302                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
303                           '''"Last-Translator: <>\\n"\n''' \
304                           '''"Language-Team: \\n"\n'''   \
305                           '''"MIME-Version: 1.0\\n"\n''' \
306                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
307                           '''"Content-Transfer-Encoding: \\n"\n'''       \
308                           '''"Plural-Forms: \\n"\n'''    \
309                           "\n"
310
311                           % { 'project': release.description,
312                               'version': release.version,
313                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
314                               'bugmail': release.support_email,
315                               'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
316                             }
317                           )
318
319     def write(self, modules, tnrs, source, trad):
320         def quote(s):
321             return '"%s"' % s.replace('"','\\"') \
322                              .replace('\n', '\\n"\n"') \
323                              .replace(' \\ ',' \\\\ ')
324
325
326         plurial = len(modules) > 1 and 's' or ''
327         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
328
329
330         code = False
331         for typy, name, res_id in tnrs:
332             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
333             if typy == 'code':
334                 code = True
335
336         if code:
337             # only strings in python code are python formated
338             self.buffer.write("#, python-format\n")
339
340         if not isinstance(trad, unicode):
341             trad = unicode(trad, 'utf8')
342         if not isinstance(source, unicode):
343             source = unicode(source, 'utf8')
344
345         msg = "msgid %s\n"      \
346               "msgstr %s\n\n"   \
347                   % (quote(source), quote(trad))
348         self.buffer.write(msg.encode('utf8'))
349
350
351 # Methods to export the translation file
352
353 def trans_export(lang, modules, buffer, format, dbname=None):
354
355     def _process(format, modules, rows, buffer, lang, newlang):
356         if format == 'csv':
357             writer=csv.writer(buffer, 'UNIX')
358             for row in rows:
359                 writer.writerow(row)
360         elif format == 'po':
361             rows.pop(0)
362             writer = tools.TinyPoFile(buffer)
363             writer.write_infos(modules)
364
365             # we now group the translations by source. That means one translation per source.
366             grouped_rows = {}
367             for module, type, name, res_id, src, trad in rows:
368                 row = grouped_rows.setdefault(src, {})
369                 row.setdefault('modules', set()).add(module)
370                 if ('translation' not in row) or (not row['translation']):
371                     row['translation'] = trad
372                 row.setdefault('tnrs', []).append((type, name, res_id))
373
374             for src, row in grouped_rows.items():
375                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
376
377         elif format == 'tgz':
378             rows.pop(0)
379             rows_by_module = {}
380             for row in rows:
381                 module = row[0]
382                 rows_by_module.setdefault(module, []).append(row)
383
384             tmpdir = tempfile.mkdtemp()
385             for mod, modrows in rows_by_module.items():
386                 tmpmoddir = join(tmpdir, mod, 'i18n')
387                 os.makedirs(tmpmoddir)
388                 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
389                 buf = file(join(tmpmoddir, pofilename), 'w')
390                 _process('po', [mod], modrows, buf, lang, newlang)
391                 buf.close()
392
393             tar = tarfile.open(fileobj=buffer, mode='w|gz')
394             tar.add(tmpdir, '')
395             tar.close()
396
397         else:
398             raise Exception(_('Bad file format'))
399
400     newlang = not bool(lang)
401     if newlang:
402         lang = 'en_US'
403     trans = trans_generate(lang, modules, dbname)
404     if newlang and format!='csv':
405         for trx in trans:
406             trx[-1] = ''
407     modules = set([t[0] for t in trans[1:]])
408     _process(format, modules, trans, buffer, lang, newlang)
409     del trans
410
411
412 def trans_parse_xsl(de):
413     res = []
414     for n in de:
415         if n.get("t"):
416             for m in [j for j in n if j.text]:
417                 l = m.text.strip().replace('\n',' ')
418                 if len(l):
419                     res.append(l.encode("utf8"))
420         res.extend(trans_parse_xsl(n))
421     return res
422
423 def trans_parse_rml(de):
424     res = []
425     for n in de:
426         for m in [j for j in n if j.text]:
427             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
428             for s in string_list:
429                 if s:
430                     res.append(s.encode("utf8"))
431         res.extend(trans_parse_rml(n))
432     return res
433
434 def trans_parse_view(de):
435     res = []
436     if de.get("string"):
437         res.append(de.get('string').encode("utf8"))
438     if de.get("sum"):
439         res.append(de.get('sum').encode("utf8"))
440     for n in de:
441         res.extend(trans_parse_view(n))
442     return res
443
444 # tests whether an object is in a list of modules
445 def in_modules(object_name, modules):
446     if 'all' in modules:
447         return True
448
449     module_dict = {
450         'ir': 'base',
451         'res': 'base',
452         'workflow': 'base',
453     }
454     module = object_name.split('.')[0]
455     module = module_dict.get(module, module)
456     return module in modules
457
458 def trans_generate(lang, modules, dbname=None):
459     logger = netsvc.Logger()
460     if not dbname:
461         dbname=tools.config['db_name']
462         if not modules:
463             modules = ['all']
464
465     pool = pooler.get_pool(dbname)
466     trans_obj = pool.get('ir.translation')
467     model_data_obj = pool.get('ir.model.data')
468     cr = pooler.get_db(dbname).cursor()
469     uid = 1
470     l = pool.obj_pool.items()
471     l.sort()
472
473     query = 'SELECT name, model, res_id, module'    \
474             '  FROM ir_model_data'
475     if 'all_installed' in modules:
476         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
477     query_param = None
478     if 'all' not in modules:
479         query += ' WHERE module IN %s'
480         query_param = (tuple(modules),)
481     query += ' ORDER BY module, model, name'
482
483     cr.execute(query, query_param)
484
485     _to_translate = []
486     def push_translation(module, type, name, id, source):
487         tuple = (module, source, name, id, type)
488         if source and tuple not in _to_translate:
489             _to_translate.append(tuple)
490
491     def encode(s):
492         if isinstance(s, unicode):
493             return s.encode('utf8')
494         return s
495
496     for (xml_name,model,res_id,module) in cr.fetchall():
497         module = encode(module)
498         model = encode(model)
499         xml_name = "%s.%s" % (module, encode(xml_name))
500
501         if not pool.get(model):
502             logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
503             continue
504
505         exists = pool.get(model).exists(cr, uid, res_id)
506         if not exists:
507             logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
508             continue
509         obj = pool.get(model).browse(cr, uid, res_id)
510
511         if model=='ir.ui.view':
512             d = etree.XML(encode(obj.arch))
513             for t in trans_parse_view(d):
514                 push_translation(module, 'view', encode(obj.model), 0, t)
515         elif model=='ir.actions.wizard':
516             service_name = 'wizard.'+encode(obj.wiz_name)
517             if netsvc.Service._services.get(service_name):
518                 obj2 = netsvc.Service._services[service_name]
519                 for state_name, state_def in obj2.states.iteritems():
520                     if 'result' in state_def:
521                         result = state_def['result']
522                         if result['type'] != 'form':
523                             continue
524                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
525
526                         def_params = {
527                             'string': ('wizard_field', lambda s: [encode(s)]),
528                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
529                             'help': ('help', lambda s: [encode(s)]),
530                         }
531
532                         # export fields
533                         if not result.has_key('fields'):
534                             logger.notifyChannel("db",netsvc.LOG_WARNING,"res has no fields: %r" % result)
535                             continue
536                         for field_name, field_def in result['fields'].iteritems():
537                             res_name = name + ',' + field_name
538
539                             for fn in def_params:
540                                 if fn in field_def:
541                                     transtype, modifier = def_params[fn]
542                                     for val in modifier(field_def[fn]):
543                                         push_translation(module, transtype, res_name, 0, val)
544
545                         # export arch
546                         arch = result['arch']
547                         if arch and not isinstance(arch, UpdateableStr):
548                             d = etree.XML(arch)
549                             for t in trans_parse_view(d):
550                                 push_translation(module, 'wizard_view', name, 0, t)
551
552                         # export button labels
553                         for but_args in result['state']:
554                             button_name = but_args[0]
555                             button_label = but_args[1]
556                             res_name = name + ',' + button_name
557                             push_translation(module, 'wizard_button', res_name, 0, button_label)
558
559         elif model=='ir.model.fields':
560             try:
561                 field_name = encode(obj.name)
562             except AttributeError, exc:
563                 logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
564                 continue
565             objmodel = pool.get(obj.model)
566             if not objmodel or not field_name in objmodel._columns:
567                 continue
568             field_def = objmodel._columns[field_name]
569
570             name = "%s,%s" % (encode(obj.model), field_name)
571             push_translation(module, 'field', name, 0, encode(field_def.string))
572
573             if field_def.help:
574                 push_translation(module, 'help', name, 0, encode(field_def.help))
575
576             if field_def.translate:
577                 ids = objmodel.search(cr, uid, [])
578                 obj_values = objmodel.read(cr, uid, ids, [field_name])
579                 for obj_value in obj_values:
580                     res_id = obj_value['id']
581                     if obj.name in ('ir.model', 'ir.ui.menu'):
582                         res_id = 0
583                     model_data_ids = model_data_obj.search(cr, uid, [
584                         ('model', '=', model),
585                         ('res_id', '=', res_id),
586                         ])
587                     if not model_data_ids:
588                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
589
590             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
591                 for dummy, val in field_def.selection:
592                     push_translation(module, 'selection', name, 0, encode(val))
593
594         elif model=='ir.actions.report.xml':
595             name = encode(obj.report_name)
596             fname = ""
597             if obj.report_rml:
598                 fname = obj.report_rml
599                 parse_func = trans_parse_rml
600                 report_type = "report"
601             elif obj.report_xsl:
602                 fname = obj.report_xsl
603                 parse_func = trans_parse_xsl
604                 report_type = "xsl"
605             if fname and obj.report_type in ('pdf', 'xsl'):
606                 try:
607                     d = etree.parse(tools.file_open(fname))
608                     for t in parse_func(d.iter()):
609                         push_translation(module, report_type, name, 0, t)
610                 except (IOError, etree.XMLSyntaxError):
611                     logging.getLogger("i18n").exception("couldn't export translation for report %s %s %s", name, report_type, fname)
612
613         for constraint in pool.get(model)._constraints:
614             msg = constraint[1]
615             # Check presence of __call__ directly instead of using
616             # callable() because it will be deprecated as of Python 3.0
617             if not hasattr(msg, '__call__'):
618                 push_translation(module, 'constraint', model, 0, encode(msg))
619
620         for field_name,field_def in pool.get(model)._columns.items():
621             if field_def.translate:
622                 name = model + "," + field_name
623                 try:
624                     trad = getattr(obj, field_name) or ''
625                 except:
626                     trad = ''
627                 push_translation(module, 'model', name, xml_name, encode(trad))
628
629     # parse source code for _() calls
630     def get_module_from_path(path, mod_paths=None):
631 #        if not mod_paths:
632 ##             First, construct a list of possible paths
633 #            def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
634 #            ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
635 #            mod_paths=[def_path]
636 #            for adp in ad_paths:
637 #                mod_paths.append(adp)
638 #                if not adp.startswith('/'):
639 #                    mod_paths.append(os.path.join(def_path,adp))
640 #                elif adp.startswith(def_path):
641 #                    mod_paths.append(adp[len(def_path)+1:])
642 #        for mp in mod_paths:
643 #            if path.startswith(mp) and (os.path.dirname(path) != mp):
644 #                path = path[len(mp)+1:]
645 #                return path.split(os.path.sep)[0]
646         path_dir = os.path.dirname(path[1:])
647         if path_dir:
648             if os.path.exists(os.path.join(tools.config['addons_path'],path[1:])):
649                 return path.split(os.path.sep)[1]
650             else:
651                 root_addons = os.path.join(tools.config['root_path'], 'addons')
652                 if os.path.exists(os.path.join(root_addons,path[1:])):
653                     return path.split(os.path.sep)[1]
654         return 'base'   # files that are not in a module are considered as being in 'base' module
655
656     modobj = pool.get('ir.module.module')
657     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
658     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
659
660     root_path = os.path.join(tools.config['root_path'], 'addons')
661
662     if root_path in tools.config['addons_path'] :
663         path_list = [root_path]
664     else :
665         path_list = [root_path,tools.config['addons_path']]
666
667     def export_code_terms_from_file(fname, path, root, terms_type):
668         fabsolutepath = join(root, fname)
669         frelativepath = fabsolutepath[len(path):]
670         module = get_module_from_path(frelativepath)
671         is_mod_installed = module in installed_modules
672         if (('all' in modules) or (module in modules)) and is_mod_installed:
673             code_string = tools.file_open(fabsolutepath, subdir='').read()
674             iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)', code_string, re.S)
675             if module in installed_modules:
676                 frelativepath = str("addons" + frelativepath)
677             for i in iter:
678                 push_translation(module, terms_type, frelativepath, 0, encode(i.group(1)))
679
680     for path in path_list:
681         for root, dummy, files in tools.osutil.walksymlinks(path):
682             for fname in itertools.chain(fnmatch.filter(files, '*.py')):
683                 export_code_terms_from_file(fname, path, root, 'code')
684             for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
685                 export_code_terms_from_file(fname, path, root, 'report')
686
687
688     out = [["module","type","name","res_id","src","value"]] # header
689     _to_translate.sort()
690     # translate strings marked as to be translated
691     for module, source, name, id, type in _to_translate:
692         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
693         out.append([module, type, name, id, source, encode(trans) or ''])
694
695     cr.close()
696     return out
697
698 def trans_load(db_name, filename, lang, strict=False, verbose=True):
699     logger = netsvc.Logger()
700     try:
701         fileobj = open(filename,'r')
702         fileformat = os.path.splitext(filename)[-1][1:].lower()
703         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
704         fileobj.close()
705         return r
706     except IOError:
707         if verbose:
708             logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
709         return None
710
711 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
712     logger = netsvc.Logger()
713     if verbose:
714         logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
715     pool = pooler.get_pool(db_name)
716     lang_obj = pool.get('res.lang')
717     trans_obj = pool.get('ir.translation')
718     model_data_obj = pool.get('ir.model.data')
719     iso_lang = tools.get_iso_codes(lang)
720     try:
721         uid = 1
722         cr = pooler.get_db(db_name).cursor()
723         ids = lang_obj.search(cr, uid, [('code','=', lang)])
724
725         if not ids:
726             # lets create the language with locale information
727             fail = True
728             for ln in get_locales(lang):
729                 try:
730                     locale.setlocale(locale.LC_ALL, str(ln))
731                     fail = False
732                     break
733                 except locale.Error:
734                     continue
735             if fail:
736                 lc = locale.getdefaultlocale()[0]
737                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
738                 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc))
739
740             if not lang_name:
741                 lang_name = tools.get_languages().get(lang, lang)
742
743             def fix_xa0(s):
744                 if s == '\xa0':
745                     return '\xc2\xa0'
746                 return s
747
748             lang_info = {
749                 'code': lang,
750                 'iso_code': iso_lang,
751                 'name': lang_name,
752                 'translatable': 1,
753                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
754                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
755                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
756                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
757             }
758
759             try:
760                 lang_obj.create(cr, uid, lang_info)
761             finally:
762                 resetlocale()
763
764
765         # now, the serious things: we read the language file
766         fileobj.seek(0)
767         if fileformat == 'csv':
768             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
769             # read the first line of the file (it contains columns titles)
770             for row in reader:
771                 f = row
772                 break
773         elif fileformat == 'po':
774             reader = TinyPoFile(fileobj)
775             f = ['type', 'name', 'res_id', 'src', 'value']
776         else:
777             raise Exception(_('Bad file format'))
778
779         # read the rest of the file
780         line = 1
781         for row in reader:
782             line += 1
783             # skip empty rows and rows where the translation field (=last fiefd) is empty
784             #if (not row) or (not row[-1]):
785             #    continue
786
787             # dictionary which holds values for this line of the csv file
788             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
789             #  'src': ..., 'value': ...}
790             dic = {'lang': lang}
791             for i in range(len(f)):
792                 if f[i] in ('module',):
793                     continue
794                 dic[f[i]] = row[i]
795
796             try:
797                 dic['res_id'] = int(dic['res_id'])
798             except:
799                 model_data_ids = model_data_obj.search(cr, uid, [
800                     ('model', '=', dic['name'].split(',')[0]),
801                     ('module', '=', dic['res_id'].split('.', 1)[0]),
802                     ('name', '=', dic['res_id'].split('.', 1)[1]),
803                     ])
804                 if model_data_ids:
805                     dic['res_id'] = model_data_obj.browse(cr, uid,
806                             model_data_ids[0]).res_id
807                 else:
808                     dic['res_id'] = False
809
810             if dic['type'] == 'model' and not strict:
811                 (model, field) = dic['name'].split(',')
812
813                 # get the ids of the resources of this model which share
814                 # the same source
815                 obj = pool.get(model)
816                 if obj:
817                     if field not in obj.fields_get_keys(cr, uid):
818                         continue
819                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
820
821                     # if the resource id (res_id) is in that list, use it,
822                     # otherwise use the whole list
823                     if not ids:
824                         ids = []
825                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
826                     for id in ids:
827                         dic['res_id'] = id
828                         ids = trans_obj.search(cr, uid, [
829                             ('lang', '=', lang),
830                             ('type', '=', dic['type']),
831                             ('name', '=', dic['name']),
832                             ('src', '=', dic['src']),
833                             ('res_id', '=', dic['res_id'])
834                         ])
835                         if ids:
836                             trans_obj.write(cr, uid, ids, {'value': dic['value']})
837                         else:
838                             trans_obj.create(cr, uid, dic)
839             else:
840                 ids = trans_obj.search(cr, uid, [
841                     ('lang', '=', lang),
842                     ('type', '=', dic['type']),
843                     ('name', '=', dic['name']),
844                     ('src', '=', dic['src'])
845                 ])
846                 if ids:
847                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
848                 else:
849                     trans_obj.create(cr, uid, dic)
850             cr.commit()
851         cr.close()
852         if verbose:
853             logger.notifyChannel("i18n", netsvc.LOG_INFO,
854                     "translation file loaded succesfully")
855     except IOError:
856         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
857         logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
858
859 def get_locales(lang=None):
860     if lang is None:
861         lang = locale.getdefaultlocale()[0]
862
863     if os.name == 'nt':
864         lang = _LOCALE2WIN32.get(lang, lang)
865
866     def process(enc):
867         ln = locale._build_localename((lang, enc))
868         yield ln
869         nln = locale.normalize(ln)
870         if nln != ln:
871             yield nln
872
873     for x in process('utf8'): yield x
874
875     prefenc = locale.getpreferredencoding()
876     if prefenc:
877         for x in process(prefenc): yield x
878
879         prefenc = {
880             'latin1': 'latin9',
881             'iso-8859-1': 'iso8859-15',
882             'cp1252': '1252',
883         }.get(prefenc.lower())
884         if prefenc:
885             for x in process(prefenc): yield x
886
887     yield lang
888
889
890
891 def resetlocale():
892     # locale.resetlocale is bugged with some locales.
893     for ln in get_locales():
894         try:
895             return locale.setlocale(locale.LC_ALL, ln)
896         except locale.Error:
897             continue
898
899 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
900