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