[FIX] handle the case of missing "#:" formated comment in po files
[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     modules = set([t[0] for t in trans[1:]])
353     _process(format, modules, trans, buffer, lang, newlang)
354     del trans
355
356
357 def trans_parse_xsl(de):
358     res = []
359     for n in de:
360         if n.get("t"):
361             for m in [j for j in n if j.text]:
362                 l = m.text.strip().replace('\n',' ')
363                 if len(l):
364                     res.append(l.encode("utf8"))
365         res.extend(trans_parse_xsl(n))
366     return res
367
368 def trans_parse_rml(de):
369     res = []
370     for n in de:
371         for m in [j for j in n if j.text]:
372             string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
373             for s in string_list:
374                 if s:
375                     res.append(s.encode("utf8"))
376         res.extend(trans_parse_rml(n))
377     return res
378
379 def trans_parse_view(de):
380     res = []
381     if de.get("string"):
382         res.append(de.get('string').encode("utf8"))
383     if de.get("sum"):
384         res.append(de.get('sum').encode("utf8"))
385     for n in de:
386         res.extend(trans_parse_view(n))
387     return res
388
389 # tests whether an object is in a list of modules
390 def in_modules(object_name, modules):
391     if 'all' in modules:
392         return True
393
394     module_dict = {
395         'ir': 'base',
396         'res': 'base',
397         'workflow': 'base',
398     }
399     module = object_name.split('.')[0]
400     module = module_dict.get(module, module)
401     return module in modules
402
403 def trans_generate(lang, modules, dbname=None):
404     logger = netsvc.Logger()
405     if not dbname:
406         dbname=tools.config['db_name']
407         if not modules:
408             modules = ['all']
409
410     pool = pooler.get_pool(dbname)
411     trans_obj = pool.get('ir.translation')
412     model_data_obj = pool.get('ir.model.data')
413     cr = pooler.get_db(dbname).cursor()
414     uid = 1
415     l = pool.obj_pool.items()
416     l.sort()
417
418     query = 'SELECT name, model, res_id, module'    \
419             '  FROM ir_model_data'
420     if not 'all' in modules:
421         query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
422     query += ' ORDER BY module, model, name'
423
424     query_param = not 'all' in modules and modules or None
425     cr.execute(query, query_param)
426
427     _to_translate = []
428     def push_translation(module, type, name, id, source):
429         tuple = (module, source, name, id, type)
430         if source and tuple not in _to_translate:
431             _to_translate.append(tuple)
432     
433     def encode(s):
434         if isinstance(s, unicode):
435             return s.encode('utf8')
436         return s
437
438     for (xml_name,model,res_id,module) in cr.fetchall():
439         module = encode(module)
440         model = encode(model)
441         xml_name = "%s.%s" % (module, encode(xml_name))
442
443         if not pool.get(model):
444             logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
445             continue
446         
447         exists = pool.get(model).exists(cr, uid, res_id)
448         if not exists:
449             logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
450             continue
451         obj = pool.get(model).browse(cr, uid, res_id)
452
453         if model=='ir.ui.view':
454             d = etree.XML(encode(obj.arch))
455             for t in trans_parse_view(d):
456                 push_translation(module, 'view', encode(obj.model), 0, t)
457         elif model=='ir.actions.wizard':
458             service_name = 'wizard.'+encode(obj.wiz_name)
459             if netsvc.SERVICES.get(service_name):
460                 obj2 = netsvc.SERVICES[service_name]
461                 for state_name, state_def in obj2.states.iteritems():
462                     if 'result' in state_def:
463                         result = state_def['result']
464                         if result['type'] != 'form':
465                             continue
466                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
467                         
468                         def_params = {
469                             'string': ('wizard_field', lambda s: [encode(s)]),
470                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
471                             'help': ('help', lambda s: [encode(s)]),
472                         }
473
474                         # export fields
475                         for field_name, field_def in result['fields'].iteritems():
476                             res_name = name + ',' + field_name
477                            
478                             for fn in def_params:
479                                 if fn in field_def:
480                                     transtype, modifier = def_params[fn]
481                                     for val in modifier(field_def[fn]):
482                                         push_translation(module, transtype, res_name, 0, val)
483
484                         # export arch
485                         arch = result['arch']
486                         if arch and not isinstance(arch, UpdateableStr):
487                             d = etree.XML(arch)
488                             for t in trans_parse_view(d):
489                                 push_translation(module, 'wizard_view', name, 0, t)
490
491                         # export button labels
492                         for but_args in result['state']:
493                             button_name = but_args[0]
494                             button_label = but_args[1]
495                             res_name = name + ',' + button_name
496                             push_translation(module, 'wizard_button', res_name, 0, button_label)
497
498         elif model=='ir.model.fields':
499             field_name = encode(obj.name)
500             objmodel = pool.get(obj.model)
501             if not objmodel or not field_name in objmodel._columns:
502                 continue
503             field_def = objmodel._columns[field_name]
504
505             name = "%s,%s" % (encode(obj.model), field_name)
506             push_translation(module, 'field', name, 0, encode(field_def.string))
507
508             if field_def.help:
509                 push_translation(module, 'help', name, 0, encode(field_def.help))
510
511             if field_def.translate:
512                 ids = objmodel.search(cr, uid, [])
513                 obj_values = objmodel.read(cr, uid, ids, [field_name])
514                 for obj_value in obj_values:
515                     res_id = obj_value['id']
516                     if obj.name in ('ir.model', 'ir.ui.menu'):
517                         res_id = 0
518                     model_data_ids = model_data_obj.search(cr, uid, [
519                         ('model', '=', model),
520                         ('res_id', '=', res_id),
521                         ])
522                     if not model_data_ids:
523                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
524
525             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
526                 for key, val in field_def.selection:
527                     push_translation(module, 'selection', name, 0, encode(val))
528
529         elif model=='ir.actions.report.xml':
530             name = encode(obj.report_name)
531             fname = ""
532             if obj.report_rml:
533                 fname = obj.report_rml
534                 parse_func = trans_parse_rml
535                 report_type = "rml"
536             elif obj.report_xsl:
537                 fname = obj.report_xsl
538                 parse_func = trans_parse_xsl
539                 report_type = "xsl"
540             try:
541                 xmlstr = tools.file_open(fname).read()
542                 d = etree.XML(xmlstr)
543                 for t in parse_func(d):
544                     push_translation(module, report_type, name, 0, t)
545             except IOError, etree.XMLSyntaxError:
546                 if fname:
547                     logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
548
549         for constraint in pool.get(model)._constraints:
550             msg = constraint[1]
551             push_translation(module, 'constraint', model, 0, encode(msg))
552
553         for field_name,field_def in pool.get(model)._columns.items():
554             if field_def.translate:
555                 name = model + "," + field_name
556                 trad = getattr(obj, field_name) or ''
557                 push_translation(module, 'model', name, xml_name, encode(trad))
558
559     # parse source code for _() calls
560     def get_module_from_path(path):
561         relative_addons_path = tools.config['addons_path'][len(tools.config['root_path'])+1:]
562         if path.startswith(relative_addons_path) and (os.path.dirname(path) != relative_addons_path):
563             path = path[len(relative_addons_path)+1:]
564             return path.split(os.path.sep)[0]
565         return 'base'   # files that are not in a module are considered as being in 'base' module
566
567     modobj = pool.get('ir.module.module')
568     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
569     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
570     
571     root_path = os.path.join(tools.config['root_path'], 'addons')
572
573     if root_path in tools.config['addons_path'] :
574         path_list = [root_path]
575     else :
576         path_list = [root_path,tools.config['addons_path']]
577
578     for path in path_list: 
579         for root, dirs, files in tools.osutil.walksymlinks(path):
580             for fname in fnmatch.filter(files, '*.py'):
581                 fabsolutepath = join(root, fname)
582                 frelativepath = fabsolutepath[len(path):]
583                 module = get_module_from_path(frelativepath)
584                 is_mod_installed = module in installed_modules
585                 if (('all' in modules) or (module in modules)) and is_mod_installed:
586                     code_string = tools.file_open(fabsolutepath, subdir='').read()
587                     iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
588                         code_string, re.S)
589                     
590                     if module in installed_modules : 
591                         frelativepath =str("addons"+frelativepath)
592                     for i in iter:
593                         push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
594
595
596     out = [["module","type","name","res_id","src","value"]] # header
597     _to_translate.sort()
598     # translate strings marked as to be translated
599     for module, source, name, id, type in _to_translate:
600         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
601         out.append([module, type, name, id, source, encode(trans) or ''])
602     
603     cr.close()
604     return out
605
606 def trans_load(db_name, filename, lang, strict=False, verbose=True):
607     logger = netsvc.Logger()
608     try:
609         fileobj = open(filename,'r')
610         fileformat = os.path.splitext(filename)[-1][1:].lower()
611         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
612         fileobj.close()
613         return r
614     except IOError:
615         if verbose:
616             logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,)) 
617         return None
618
619 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
620     logger = netsvc.Logger()
621     if verbose:
622         logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
623     pool = pooler.get_pool(db_name)
624     lang_obj = pool.get('res.lang')
625     trans_obj = pool.get('ir.translation')
626     model_data_obj = pool.get('ir.model.data')
627     try:
628         uid = 1
629         cr = pooler.get_db(db_name).cursor()
630         ids = lang_obj.search(cr, uid, [('code','=', lang)])
631         
632         if not ids:
633             # lets create the language with locale information
634             fail = True
635             for ln in get_locales(lang):
636                 try:
637                     locale.setlocale(locale.LC_ALL, str(ln))
638                     fail = False
639                     break
640                 except locale.Error:
641                     continue
642             if fail:
643                 lc = locale.getdefaultlocale()[0]
644                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
645                 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc)) 
646                 
647             if not lang_name:
648                 lang_name = tools.get_languages().get(lang, lang)
649             
650             lang_info = {
651                 'code': lang,
652                 'name': lang_name,
653                 'translatable': 1,
654                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
655                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
656                 'decimal_point' : str(locale.localeconv()['decimal_point']).replace('\xa0', '\xc2\xa0'),
657                 'thousands_sep' : str(locale.localeconv()['thousands_sep']).replace('\xa0', '\xc2\xa0'),
658             }
659             
660             try: 
661                 lang_obj.create(cr, uid, lang_info)
662             finally:
663                 resetlocale()
664
665
666         # now, the serious things: we read the language file
667         fileobj.seek(0)
668         if fileformat == 'csv':
669             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
670             # read the first line of the file (it contains columns titles)
671             for row in reader:
672                 f = row
673                 break
674         elif fileformat == 'po':
675             reader = TinyPoFile(fileobj)
676             f = ['type', 'name', 'res_id', 'src', 'value']
677         else:
678             raise Exception(_('Bad file format'))
679
680         # read the rest of the file
681         line = 1
682         for row in reader:
683             line += 1
684             # skip empty rows and rows where the translation field (=last fiefd) is empty
685             #if (not row) or (not row[-1]):
686             #    continue
687
688             # dictionary which holds values for this line of the csv file
689             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
690             #  'src': ..., 'value': ...}
691             dic = {'lang': lang}
692             for i in range(len(f)):
693                 if f[i] in ('module',):
694                     continue
695                 dic[f[i]] = row[i]
696
697             try:
698                 dic['res_id'] = int(dic['res_id'])
699             except:
700                 model_data_ids = model_data_obj.search(cr, uid, [
701                     ('model', '=', dic['name'].split(',')[0]),
702                     ('module', '=', dic['res_id'].split('.', 1)[0]),
703                     ('name', '=', dic['res_id'].split('.', 1)[1]),
704                     ])
705                 if model_data_ids:
706                     dic['res_id'] = model_data_obj.browse(cr, uid,
707                             model_data_ids[0]).res_id
708                 else:
709                     dic['res_id'] = False
710
711             if dic['type'] == 'model' and not strict:
712                 (model, field) = dic['name'].split(',')
713
714                 # get the ids of the resources of this model which share
715                 # the same source
716                 obj = pool.get(model)
717                 if obj:
718                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
719
720                     # if the resource id (res_id) is in that list, use it,
721                     # otherwise use the whole list
722                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
723                     for id in ids:
724                         dic['res_id'] = id
725                         ids = trans_obj.search(cr, uid, [
726                             ('lang', '=', lang),
727                             ('type', '=', dic['type']),
728                             ('name', '=', dic['name']),
729                             ('src', '=', dic['src']),
730                             ('res_id', '=', dic['res_id'])
731                         ])
732                         if ids:
733                             trans_obj.write(cr, uid, ids, {'value': dic['value']})
734                         else:
735                             trans_obj.create(cr, uid, dic)
736             else:
737                 ids = trans_obj.search(cr, uid, [
738                     ('lang', '=', lang),
739                     ('type', '=', dic['type']),
740                     ('name', '=', dic['name']),
741                     ('src', '=', dic['src'])
742                 ])
743                 if ids:
744                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
745                 else:
746                     trans_obj.create(cr, uid, dic)
747             cr.commit()
748         cr.close()
749         if verbose:
750             logger.notifyChannel("i18n", netsvc.LOG_INFO,
751                     "translation file loaded succesfully")
752     except IOError:
753         filename = '[lang: %s][format: %s]' % (lang or 'new', fileformat)
754         logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
755
756 def get_locales(lang=None):
757     if lang is None:
758         lang = locale.getdefaultlocale()[0]
759     
760     if os.name == 'nt':
761         lang = _LOCALE2WIN32.get(lang, lang)
762     
763     def process(enc):
764         ln = locale._build_localename((lang, enc))
765         yield ln
766         nln = locale.normalize(ln)
767         if nln != ln:
768             yield nln
769
770     for x in process('utf8'): yield x
771
772     prefenc = locale.getpreferredencoding()
773     if prefenc:
774         for x in process(prefenc): yield x
775         
776         prefenc = {
777             'latin1': 'latin9', 
778             'iso-8859-1': 'iso8859-15',
779             'cp1252': '1252',
780         }.get(prefenc.lower())
781         if prefenc:
782             for x in process(prefenc): yield x
783
784     yield lang
785
786
787
788 def resetlocale():
789     # locale.resetlocale is bugged with some locales. 
790     for ln in get_locales():
791         try:
792             return locale.setlocale(locale.LC_ALL, ln)
793         except locale.Error:
794             continue
795
796 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
797