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