Fixed xml dom problem in export 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>). 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 osv, 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     'tlh_TLH': 'Klingon',
105     
106 }
107
108
109 class UNIX_LINE_TERMINATOR(csv.excel):
110     lineterminator = '\n'
111
112 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
113
114 #
115 # Warning: better use self.pool.get('ir.translation')._get_source if you can
116 #
117 def translate(cr, name, source_type, lang, source=None):
118     if source and name:
119         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))
120     elif name:
121         cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
122     elif source:
123         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
124     res_trans = cr.fetchone()
125     res = res_trans and res_trans[0] or False
126     return res
127
128 class GettextAlias(object):
129     def __call__(self, source):
130         try:
131             frame = inspect.stack()[1][0]
132         except:
133             return source
134
135         cr = frame.f_locals.get('cr')
136         lang = (frame.f_locals.get('context') or {}).get('lang', False)
137         if not (lang and cr):
138             return source
139
140         cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, 'code', source))
141         res_trans = cr.fetchone()
142         return res_trans and res_trans[0] or source
143 _ = GettextAlias()
144
145
146 # class to handle po files
147 class TinyPoFile(object):
148     def __init__(self, buffer):
149         self.buffer = buffer
150
151     def __iter__(self):
152         self.buffer.seek(0)
153         self.lines = self._get_lines()
154
155         self.first = True
156         self.tnrs= []
157         return self
158
159     def _get_lines(self):
160         lines = self.buffer.readlines()
161         # remove the BOM (Byte Order Mark):
162         if len(lines):
163             lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
164
165         lines.append('') # ensure that the file ends with at least an empty line
166         return lines
167
168     def next(self):
169         def unquote(str):
170             return str[1:-1].replace("\\n", "\n")   \
171                             .replace("\\\\ ", "\\ ") \
172                             .replace('\\"', '"')
173
174         type = name = res_id = source = trad = None
175
176         if self.tnrs:
177             type, name, res_id, source, trad = self.tnrs.pop(0)
178         else:
179             tmp_tnrs = []
180             line = None
181             while (not line):
182                 if 0 == len(self.lines):
183                     raise StopIteration()
184                 line = self.lines.pop(0).strip()
185                 if line.startswith('#:'): 
186                     tmp_tnrs.append( line[2:].strip().split(':') )
187                 if line.startswith('#'):
188                     line = None
189
190             if not line.startswith('msgid'):
191                 raise Exception("malformed file: bad line: %s" % line)
192             source = unquote(line[6:])
193             line = self.lines.pop(0).strip()
194             if not source and self.first:
195                 # if the source is "" and it's the first msgid, it's the special
196                 # msgstr with the informations about the traduction and the
197                 # traductor; we skip it
198                 self.tnrs = []
199                 while line:
200                     line = self.lines.pop(0).strip()
201                 return self.next()
202
203             while not line.startswith('msgstr'):
204                 if not line:
205                     raise Exception('malformed file')
206                 source += unquote(line)
207                 line = self.lines.pop(0).strip()
208
209             trad = unquote(line[7:])
210             line = self.lines.pop(0).strip()
211             while line:
212                 trad += unquote(line)
213                 line = self.lines.pop(0).strip()
214
215             if tmp_tnrs:
216                 type, name, res_id = tmp_tnrs.pop(0)
217                 for t, n, r in tmp_tnrs:
218                     self.tnrs.append((t, n, r, source, trad))
219
220         self.first = False
221         return type, name, res_id, source, trad
222
223     def write_infos(self, modules):
224         import release
225         self.buffer.write("# Translation of %(project)s.\n" \
226                           "# This file contains the translation of the following modules:\n" \
227                           "%(modules)s" \
228                           "#\n" \
229                           "msgid \"\"\n" \
230                           "msgstr \"\"\n" \
231                           '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
232                           '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
233                           '''"POT-Creation-Date: %(now)s\\n"\n'''        \
234                           '''"PO-Revision-Date: %(now)s\\n"\n'''         \
235                           '''"Last-Translator: <>\\n"\n''' \
236                           '''"Language-Team: \\n"\n'''   \
237                           '''"MIME-Version: 1.0\\n"\n''' \
238                           '''"Content-Type: text/plain; charset=UTF-8\\n"\n'''   \
239                           '''"Content-Transfer-Encoding: \\n"\n'''       \
240                           '''"Plural-Forms: \\n"\n'''    \
241                           "\n"
242
243                           % { 'project': release.description,
244                               'version': release.version,
245                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
246                               'bugmail': release.support_email,
247                               'now': mxdt.ISO.strUTC(mxdt.ISO.DateTime.utc()),
248                             }
249                           )
250
251     def write(self, modules, tnrs, source, trad):
252         def quote(s):
253             return '"%s"' % s.replace('"','\\"') \
254                             .replace('\\ ','\\\\ ') \
255                             .replace('\n', '\\n"\n"')
256
257         plurial = len(modules) > 1 and 's' or ''
258         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
259
260
261         code = False
262         for typy, name, res_id in tnrs:
263             self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
264             if typy == 'code':
265                 code = True
266
267         if code:
268             # only strings in python code are python formated
269             self.buffer.write("#, python-format\n")
270
271         if not isinstance(trad, unicode):
272             trad = unicode(trad, 'utf8')
273         if not isinstance(source, unicode):
274             source = unicode(source, 'utf8')
275
276         msg = "msgid %s\n"      \
277               "msgstr %s\n\n"   \
278                   % (quote(source), quote(trad))
279         self.buffer.write(msg.encode('utf8'))
280
281
282 # Methods to export the translation file
283
284 def trans_export(lang, modules, buffer, format, dbname=None):
285
286     def _process(format, modules, rows, buffer, lang, newlang):
287         if format == 'csv':
288             writer=csv.writer(buffer, 'UNIX')
289             for row in rows:
290                 writer.writerow(row)
291         elif format == 'po':
292             rows.pop(0)
293             writer = tools.TinyPoFile(buffer)
294             writer.write_infos(modules)
295
296             # we now group the translations by source. That means one translation per source.
297             grouped_rows = {}
298             for module, type, name, res_id, src, trad in rows:
299                 row = grouped_rows.setdefault(src, {})
300                 row.setdefault('modules', set()).add(module)
301                 if ('translation' not in row) or (not row['translation']):
302                     row['translation'] = trad
303                 row.setdefault('tnrs', []).append((type, name, res_id))
304
305             for src, row in grouped_rows.items():
306                 writer.write(row['modules'], row['tnrs'], src, row['translation'])
307
308         elif format == 'tgz':
309             rows.pop(0)
310             rows_by_module = {}
311             for row in rows:
312                 module = row[0]
313                 rows_by_module.setdefault(module, []).append(row)
314
315             tmpdir = tempfile.mkdtemp()
316             for mod, modrows in rows_by_module.items():
317                 tmpmoddir = join(tmpdir, mod, 'i18n')
318                 os.makedirs(tmpmoddir)
319                 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
320                 buf = file(join(tmpmoddir, pofilename), 'w')
321                 _process('po', [mod], modrows, buf, lang, newlang)
322                 buf.close()
323
324             tar = tarfile.open(fileobj=buffer, mode='w|gz')
325             tar.add(tmpdir, '')
326             tar.close()
327
328         else:
329             raise Exception(_('Bad file format'))
330
331     newlang = not bool(lang)
332     if newlang:
333         lang = 'en_US'
334     trans = trans_generate(lang, modules, dbname)
335     modules = set([t[0] for t in trans[1:]])
336     _process(format, modules, trans, buffer, lang, newlang)
337     del trans
338
339
340 def trans_parse_xsl(de):
341     res = []
342     for n in [i for i in de.getchildren()]:
343         if n.get("t"):
344             for m in [j for j in n.getchildren() if j.text]:
345                 l = m.text.strip().replace('\n',' ')
346                 if len(l):
347                     res.append(l.encode("utf8"))
348         res.extend(trans_parse_xsl(n))
349     return res
350
351 def trans_parse_rml(de):
352     res = []
353     for n in [i for i in de.getchildren()]:
354         for m in [j for j in n.getchildren() if j.text]:
355             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
356             for s in string_list:
357                 if s:
358                     res.append(s.encode("utf8"))
359         res.extend(trans_parse_rml(n))
360     return res
361
362 def trans_parse_view(de):
363     res = []
364     if de.get("string"):
365         s = de.get('string')
366         if s:
367             res.append(s.encode("utf8"))
368     if de.get("sum"):
369         s = de.get('sum')
370         if s:
371             res.append(s.encode("utf8"))
372     for n in [i for i in de.getchildren()]:
373         res.extend(trans_parse_view(n))
374     return res
375
376 # tests whether an object is in a list of modules
377 def in_modules(object_name, modules):
378     if 'all' in modules:
379         return True
380
381     module_dict = {
382         'ir': 'base',
383         'res': 'base',
384         'workflow': 'base',
385     }
386     module = object_name.split('.')[0]
387     module = module_dict.get(module, module)
388     return module in modules
389
390 def trans_generate(lang, modules, dbname=None):
391     logger = netsvc.Logger()
392     if not dbname:
393         dbname=tools.config['db_name']
394         if not modules:
395             modules = ['all']
396
397     pool = pooler.get_pool(dbname)
398     trans_obj = pool.get('ir.translation')
399     model_data_obj = pool.get('ir.model.data')
400     cr = pooler.get_db(dbname).cursor()
401     uid = 1
402     l = pool.obj_pool.items()
403     l.sort()
404
405     query = 'SELECT name, model, res_id, module'    \
406             '  FROM ir_model_data'
407     if not 'all' in modules:
408         query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
409     query += ' ORDER BY module, model, name'
410
411     query_param = not 'all' in modules and modules or None
412     cr.execute(query, query_param)
413
414     _to_translate = []
415     def push_translation(module, type, name, id, source):
416         tuple = (module, source, name, id, type)
417         if source and tuple not in _to_translate:
418             _to_translate.append(tuple)
419     
420     def encode(s):
421         if isinstance(s, unicode):
422             return s.encode('utf8')
423         return s
424
425     for (xml_name,model,res_id,module) in cr.fetchall():
426         module = encode(module)
427         model = encode(model)
428         xml_name = "%s.%s" % (module, encode(xml_name))
429
430         if not pool.get(model):
431             logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
432             continue
433         
434         exists = pool.get(model).exists(cr, uid, res_id)
435         if not exists:
436             logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
437             continue
438         obj = pool.get(model).browse(cr, uid, res_id)
439
440         if model=='ir.ui.view':
441             d = etree.XML(encode(obj.arch))
442             for t in trans_parse_view(d):
443                 push_translation(module, 'view', encode(obj.model), 0, t)
444         elif model=='ir.actions.wizard':
445             service_name = 'wizard.'+encode(obj.wiz_name)
446             if netsvc.SERVICES.get(service_name):
447                 obj2 = netsvc.SERVICES[service_name]
448                 for state_name, state_def in obj2.states.iteritems():
449                     if 'result' in state_def:
450                         result = state_def['result']
451                         if result['type'] != 'form':
452                             continue
453                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
454                         
455                         def_params = {
456                             'string': ('wizard_field', lambda s: [encode(s)]),
457                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
458                             'help': ('help', lambda s: [encode(s)]),
459                         }
460
461                         # export fields
462                         for field_name, field_def in result['fields'].iteritems():
463                             res_name = name + ',' + field_name
464                            
465                             for fn in def_params:
466                                 if fn in field_def:
467                                     transtype, modifier = def_params[fn]
468                                     for val in modifier(field_def[fn]):
469                                         push_translation(module, transtype, res_name, 0, val)
470
471                         # export arch
472                         arch = result['arch']
473                         if arch and not isinstance(arch, UpdateableStr):
474                             d = etree.XML(arch)
475                             for t in trans_parse_view(d):
476                                 push_translation(module, 'wizard_view', name, 0, t)
477
478                         # export button labels
479                         for but_args in result['state']:
480                             button_name = but_args[0]
481                             button_label = but_args[1]
482                             res_name = name + ',' + button_name
483                             push_translation(module, 'wizard_button', res_name, 0, button_label)
484
485         elif model=='ir.model.fields':
486             field_name = encode(obj.name)
487             objmodel = pool.get(obj.model)
488             if not objmodel or not field_name in objmodel._columns:
489                 continue
490             field_def = objmodel._columns[field_name]
491
492             name = "%s,%s" % (encode(obj.model), field_name)
493             push_translation(module, 'field', name, 0, encode(field_def.string))
494
495             if field_def.help:
496                 push_translation(module, 'help', name, 0, encode(field_def.help))
497
498             if field_def.translate:
499                 ids = objmodel.search(cr, uid, [])
500                 obj_values = objmodel.read(cr, uid, ids, [field_name])
501                 for obj_value in obj_values:
502                     res_id = obj_value['id']
503                     if obj.name in ('ir.model', 'ir.ui.menu'):
504                         res_id = 0
505                     model_data_ids = model_data_obj.search(cr, uid, [
506                         ('model', '=', model),
507                         ('res_id', '=', res_id),
508                         ])
509                     if not model_data_ids:
510                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
511
512             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
513                 for key, val in field_def.selection:
514                     push_translation(module, 'selection', name, 0, encode(val))
515
516         elif model=='ir.actions.report.xml':
517             name = encode(obj.report_name)
518             fname = ""
519             if obj.report_rml:
520                 fname = obj.report_rml
521                 parse_func = trans_parse_rml
522                 report_type = "rml"
523             elif obj.report_xsl:
524                 fname = obj.report_xsl
525                 parse_func = trans_parse_xsl
526                 report_type = "xsl"
527             try:
528                 xmlstr = tools.file_open(fname).read()
529                 d = etree.XML(xmlstr)
530                 for t in parse_func(d):
531                     push_translation(module, report_type, name, 0, t)
532             except IOError, etree.XMLSyntaxError:
533                 if fname:
534                     logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
535
536         for constraint in pool.get(model)._constraints:
537             msg = constraint[1]
538             push_translation(module, 'constraint', model, 0, encode(msg))
539
540         for field_name,field_def in pool.get(model)._columns.items():
541             if field_def.translate:
542                 name = model + "," + field_name
543                 trad = getattr(obj, field_name) or ''
544                 push_translation(module, 'model', name, xml_name, encode(trad))
545
546     # parse source code for _() calls
547     def get_module_from_path(path):
548         relative_addons_path = tools.config['addons_path'][len(tools.config['root_path'])+1:]
549         if path.startswith(relative_addons_path) and (os.path.dirname(path) != relative_addons_path):
550             path = path[len(relative_addons_path)+1:]
551             return path.split(os.path.sep)[0]
552         return 'base'   # files that are not in a module are considered as being in 'base' module
553
554     modobj = pool.get('ir.module.module')
555     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
556     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
557     
558     root_path = os.path.join(tools.config['root_path'], 'addons')
559
560     if root_path in tools.config['addons_path'] :
561         path_list = [root_path]
562     else :
563         path_list = [root_path,tools.config['addons_path']]
564
565     for path in path_list: 
566         for root, dirs, files in tools.osutil.walksymlinks(path):
567             for fname in fnmatch.filter(files, '*.py'):
568                 fabsolutepath = join(root, fname)
569                 frelativepath = fabsolutepath[len(path):]
570                 module = get_module_from_path(frelativepath)
571                 is_mod_installed = module in installed_modules
572                 if (('all' in modules) or (module in modules)) and is_mod_installed:
573                     code_string = tools.file_open(fabsolutepath, subdir='').read()
574                     iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
575                         code_string, re.S)
576                     
577                     if module in installed_modules : 
578                         frelativepath =str("addons"+frelativepath)
579                     for i in iter:
580                         push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
581
582
583     out = [["module","type","name","res_id","src","value"]] # header
584     _to_translate.sort()
585     # translate strings marked as to be translated
586     for module, source, name, id, type in _to_translate:
587         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
588         out.append([module, type, name, id, source, encode(trans) or ''])
589     
590     cr.close()
591     return out
592
593 def trans_load(db_name, filename, lang, strict=False, verbose=True):
594     logger = netsvc.Logger()
595     try:
596         fileobj = open(filename,'r')
597         fileformat = os.path.splitext(filename)[-1][1:].lower()
598         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
599         fileobj.close()
600         return r
601     except IOError:
602         if verbose:
603             logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,)) 
604         return None
605
606 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
607     logger = netsvc.Logger()
608     if verbose:
609         logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
610     pool = pooler.get_pool(db_name)
611     lang_obj = pool.get('res.lang')
612     trans_obj = pool.get('ir.translation')
613     model_data_obj = pool.get('ir.model.data')
614     try:
615         uid = 1
616         cr = pooler.get_db(db_name).cursor()
617         ids = lang_obj.search(cr, uid, [('code','=', lang)])
618         
619         if not ids:
620             # lets create the language with locale information
621             fail = True
622             for ln in get_locales(lang):
623                 try:
624                     locale.setlocale(locale.LC_ALL, str(ln))
625                     fail = False
626                     break
627                 except locale.Error:
628                     continue
629             if fail:
630                 lc = locale.getdefaultlocale()[0]
631                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
632                 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc)) 
633                 
634             if not lang_name:
635                 lang_name = tools.get_languages().get(lang, lang)
636             
637             lang_info = {
638                 'code': lang,
639                 'name': lang_name,
640                 'translatable': 1,
641                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
642                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
643                 'decimal_point' : str(locale.localeconv()['decimal_point']).replace('\xa0', '\xc2\xa0'),
644                 'thousands_sep' : str(locale.localeconv()['thousands_sep']).replace('\xa0', '\xc2\xa0'),
645             }
646             
647             try: 
648                 lang_obj.create(cr, uid, lang_info)
649             finally:
650                 resetlocale()
651
652
653         # now, the serious things: we read the language file
654         fileobj.seek(0)
655         if fileformat == 'csv':
656             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
657             # read the first line of the file (it contains columns titles)
658             for row in reader:
659                 f = row
660                 break
661         elif fileformat == 'po':
662             reader = TinyPoFile(fileobj)
663             f = ['type', 'name', 'res_id', 'src', 'value']
664         else:
665             raise Exception(_('Bad file format'))
666
667         # read the rest of the file
668         line = 1
669         for row in reader:
670             line += 1
671             # skip empty rows and rows where the translation field (=last fiefd) is empty
672             #if (not row) or (not row[-1]):
673             #    continue
674
675             # dictionary which holds values for this line of the csv file
676             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
677             #  'src': ..., 'value': ...}
678             dic = {'lang': lang}
679             for i in range(len(f)):
680                 if f[i] in ('module',):
681                     continue
682                 dic[f[i]] = row[i]
683
684             try:
685                 dic['res_id'] = int(dic['res_id'])
686             except:
687                 model_data_ids = model_data_obj.search(cr, uid, [
688                     ('model', '=', dic['name'].split(',')[0]),
689                     ('module', '=', dic['res_id'].split('.', 1)[0]),
690                     ('name', '=', dic['res_id'].split('.', 1)[1]),
691                     ])
692                 if model_data_ids:
693                     dic['res_id'] = model_data_obj.browse(cr, uid,
694                             model_data_ids[0]).res_id
695                 else:
696                     dic['res_id'] = False
697
698             if dic['type'] == 'model' and not strict:
699                 (model, field) = dic['name'].split(',')
700
701                 # get the ids of the resources of this model which share
702                 # the same source
703                 obj = pool.get(model)
704                 if obj:
705                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
706
707                     # if the resource id (res_id) is in that list, use it,
708                     # otherwise use the whole list
709                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
710                     for id in ids:
711                         dic['res_id'] = id
712                         ids = trans_obj.search(cr, uid, [
713                             ('lang', '=', lang),
714                             ('type', '=', dic['type']),
715                             ('name', '=', dic['name']),
716                             ('src', '=', dic['src']),
717                             ('res_id', '=', dic['res_id'])
718                         ])
719                         if ids:
720                             trans_obj.write(cr, uid, ids, {'value': dic['value']})
721                         else:
722                             trans_obj.create(cr, uid, dic)
723             else:
724                 ids = trans_obj.search(cr, uid, [
725                     ('lang', '=', lang),
726                     ('type', '=', dic['type']),
727                     ('name', '=', dic['name']),
728                     ('src', '=', dic['src'])
729                 ])
730                 if ids:
731                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
732                 else:
733                     trans_obj.create(cr, uid, dic)
734             cr.commit()
735         cr.close()
736         if verbose:
737             logger.notifyChannel("i18n", netsvc.LOG_INFO,
738                     "translation file loaded succesfully")
739     except IOError:
740         filename = '[lang: %s][format: %s]' % (lang or 'new', fileformat)
741         logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
742
743 def get_locales(lang=None):
744     if lang is None:
745         lang = locale.getdefaultlocale()[0]
746     
747     if os.name == 'nt':
748         lang = _LOCALE2WIN32.get(lang, lang)
749     
750     def process(enc):
751         ln = locale._build_localename((lang, enc))
752         yield ln
753         nln = locale.normalize(ln)
754         if nln != ln:
755             yield nln
756
757     for x in process('utf8'): yield x
758
759     prefenc = locale.getpreferredencoding()
760     if prefenc:
761         for x in process(prefenc): yield x
762         
763         prefenc = {
764             'latin1': 'latin9', 
765             'iso-8859-1': 'iso8859-15',
766             'cp1252': '1252',
767         }.get(prefenc.lower())
768         if prefenc:
769             for x in process(prefenc): yield x
770
771     yield lang
772
773
774
775 def resetlocale():
776     # locale.resetlocale is bugged with some locales. 
777     for ln in get_locales():
778         try:
779             return locale.setlocale(locale.LC_ALL, ln)
780         except locale.Error:
781             continue
782
783 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
784