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