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