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