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