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