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