[IMP] SQL injection Refactored
[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     query_param = None
450     if 'all' not in modules:
451         query += ' WHERE module IN %s'
452         query_param = (tuple(modules),)
453     query += ' ORDER BY module, model, name'
454
455     cr.execute(query, query_param)
456
457     _to_translate = []
458     def push_translation(module, type, name, id, source):
459         tuple = (module, source, name, id, type)
460         if source and tuple not in _to_translate:
461             _to_translate.append(tuple)
462
463     def encode(s):
464         if isinstance(s, unicode):
465             return s.encode('utf8')
466         return s
467
468     for (xml_name,model,res_id,module) in cr.fetchall():
469         module = encode(module)
470         model = encode(model)
471         xml_name = "%s.%s" % (module, encode(xml_name))
472
473         if not pool.get(model):
474             logger.notifyChannel("db", netsvc.LOG_ERROR, "Unable to find object %r" % (model,))
475             continue
476
477         exists = pool.get(model).exists(cr, uid, res_id)
478         if not exists:
479             logger.notifyChannel("db", netsvc.LOG_WARNING, "Unable to find object %r with id %d" % (model, res_id))
480             continue
481         obj = pool.get(model).browse(cr, uid, res_id)
482
483         if model=='ir.ui.view':
484             d = etree.XML(encode(obj.arch))
485             for t in trans_parse_view(d):
486                 push_translation(module, 'view', encode(obj.model), 0, t)
487         elif model=='ir.actions.wizard':
488             service_name = 'wizard.'+encode(obj.wiz_name)
489             if netsvc.Service._services.get(service_name):
490                 obj2 = netsvc.Service._services[service_name]
491                 for state_name, state_def in obj2.states.iteritems():
492                     if 'result' in state_def:
493                         result = state_def['result']
494                         if result['type'] != 'form':
495                             continue
496                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
497
498                         def_params = {
499                             'string': ('wizard_field', lambda s: [encode(s)]),
500                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
501                             'help': ('help', lambda s: [encode(s)]),
502                         }
503
504                         # export fields
505                         if not result.has_key('fields'):
506                             logger.notifyChannel("db",netsvc.LOG_WARNING,"res has no fields: %r" % result)
507                             continue
508                         for field_name, field_def in result['fields'].iteritems():
509                             res_name = name + ',' + field_name
510
511                             for fn in def_params:
512                                 if fn in field_def:
513                                     transtype, modifier = def_params[fn]
514                                     for val in modifier(field_def[fn]):
515                                         push_translation(module, transtype, res_name, 0, val)
516
517                         # export arch
518                         arch = result['arch']
519                         if arch and not isinstance(arch, UpdateableStr):
520                             d = etree.XML(arch)
521                             for t in trans_parse_view(d):
522                                 push_translation(module, 'wizard_view', name, 0, t)
523
524                         # export button labels
525                         for but_args in result['state']:
526                             button_name = but_args[0]
527                             button_label = but_args[1]
528                             res_name = name + ',' + button_name
529                             push_translation(module, 'wizard_button', res_name, 0, button_label)
530
531         elif model=='ir.model.fields':
532             try:
533                 field_name = encode(obj.name)
534             except AttributeError, exc:
535                 logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
536                 continue
537             objmodel = pool.get(obj.model)
538             if not objmodel or not field_name in objmodel._columns:
539                 continue
540             field_def = objmodel._columns[field_name]
541
542             name = "%s,%s" % (encode(obj.model), field_name)
543             push_translation(module, 'field', name, 0, encode(field_def.string))
544
545             if field_def.help:
546                 push_translation(module, 'help', name, 0, encode(field_def.help))
547
548             if field_def.translate:
549                 ids = objmodel.search(cr, uid, [])
550                 obj_values = objmodel.read(cr, uid, ids, [field_name])
551                 for obj_value in obj_values:
552                     res_id = obj_value['id']
553                     if obj.name in ('ir.model', 'ir.ui.menu'):
554                         res_id = 0
555                     model_data_ids = model_data_obj.search(cr, uid, [
556                         ('model', '=', model),
557                         ('res_id', '=', res_id),
558                         ])
559                     if not model_data_ids:
560                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
561
562             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
563                 for key, val in field_def.selection:
564                     push_translation(module, 'selection', name, 0, encode(val))
565
566         elif model=='ir.actions.report.xml':
567             name = encode(obj.report_name)
568             fname = ""
569             if obj.report_rml:
570                 fname = obj.report_rml
571                 parse_func = trans_parse_rml
572                 report_type = "rml"
573             elif obj.report_xsl:
574                 fname = obj.report_xsl
575                 parse_func = trans_parse_xsl
576                 report_type = "xsl"
577             try:
578                 xmlstr = tools.file_open(fname).read()
579                 d = etree.XML(xmlstr)
580                 for t in parse_func(d):
581                     push_translation(module, report_type, name, 0, t)
582             except IOError, etree.XMLSyntaxError:
583                 if fname:
584                     logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
585
586         for constraint in pool.get(model)._constraints:
587             msg = constraint[1]
588             push_translation(module, 'constraint', model, 0, encode(msg))
589
590         for field_name,field_def in pool.get(model)._columns.items():
591             if field_def.translate:
592                 name = model + "," + field_name
593                 try:
594                     trad = getattr(obj, field_name) or ''
595                 except:
596                     trad = ''
597                 push_translation(module, 'model', name, xml_name, encode(trad))
598
599     # parse source code for _() calls
600     def get_module_from_path(path, mod_paths=None):
601 #        if not mod_paths:
602 ##             First, construct a list of possible paths
603 #            def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
604 #            ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
605 #            mod_paths=[def_path]
606 #            for adp in ad_paths:
607 #                mod_paths.append(adp)
608 #                if not adp.startswith('/'):
609 #                    mod_paths.append(os.path.join(def_path,adp))
610 #                elif adp.startswith(def_path):
611 #                    mod_paths.append(adp[len(def_path)+1:])
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         path_dir = os.path.dirname(path[1:])
617         if path_dir:
618             if os.path.exists(os.path.join(tools.config['addons_path'],path[1:])):
619                 return path.split(os.path.sep)[1]
620             else:
621                 root_addons = os.path.join(tools.config['root_path'], 'addons')
622                 if os.path.exists(os.path.join(root_addons,path[1:])):
623                     return path.split(os.path.sep)[1]
624         return 'base'   # files that are not in a module are considered as being in 'base' module
625
626     modobj = pool.get('ir.module.module')
627     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
628     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
629
630     root_path = os.path.join(tools.config['root_path'], 'addons')
631
632     if root_path in tools.config['addons_path'] :
633         path_list = [root_path]
634     else :
635         path_list = [root_path,tools.config['addons_path']]
636
637     for path in path_list:
638         for root, dirs, files in tools.osutil.walksymlinks(path):
639             for fname in fnmatch.filter(files, '*.py'):
640                 fabsolutepath = join(root, fname)
641                 frelativepath = fabsolutepath[len(path):]
642                 module = get_module_from_path(fabsolutepath)
643                 is_mod_installed = module in installed_modules
644                 if (('all' in modules) or (module in modules)) and is_mod_installed:
645                     code_string = tools.file_open(fabsolutepath, subdir='').read()
646                     iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
647                         code_string, re.S)
648
649                     if module in installed_modules :
650                         frelativepath =str("addons"+frelativepath)
651                     for i in iter:
652                         push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
653
654
655     out = [["module","type","name","res_id","src","value"]] # header
656     _to_translate.sort()
657     # translate strings marked as to be translated
658     for module, source, name, id, type in _to_translate:
659         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
660         out.append([module, type, name, id, source, encode(trans) or ''])
661
662     cr.close()
663     return out
664
665 def trans_load(db_name, filename, lang, strict=False, verbose=True):
666     logger = netsvc.Logger()
667     try:
668         fileobj = open(filename,'r')
669         fileformat = os.path.splitext(filename)[-1][1:].lower()
670         r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
671         fileobj.close()
672         return r
673     except IOError:
674         if verbose:
675             logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
676         return None
677
678 def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
679     logger = netsvc.Logger()
680     if verbose:
681         logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
682     pool = pooler.get_pool(db_name)
683     lang_obj = pool.get('res.lang')
684     trans_obj = pool.get('ir.translation')
685     model_data_obj = pool.get('ir.model.data')
686     iso_lang = tools.get_iso_codes(lang)
687     try:
688         uid = 1
689         cr = pooler.get_db(db_name).cursor()
690         ids = lang_obj.search(cr, uid, [('code','=', lang)])
691
692         if not ids:
693             # lets create the language with locale information
694             fail = True
695             for ln in get_locales(lang):
696                 try:
697                     locale.setlocale(locale.LC_ALL, str(ln))
698                     fail = False
699                     break
700                 except locale.Error:
701                     continue
702             if fail:
703                 lc = locale.getdefaultlocale()[0]
704                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
705                 logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc))
706
707             if not lang_name:
708                 lang_name = tools.get_languages().get(lang, lang)
709
710             def fix_xa0(s):
711                 if s == '\xa0':
712                     return '\xc2\xa0'
713                 return s
714
715             lang_info = {
716                 'code': lang,
717                 'iso_code': iso_lang,
718                 'name': lang_name,
719                 'translatable': 1,
720                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
721                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
722                 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
723                 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
724             }
725
726             try:
727                 lang_obj.create(cr, uid, lang_info)
728             finally:
729                 resetlocale()
730
731
732         # now, the serious things: we read the language file
733         fileobj.seek(0)
734         if fileformat == 'csv':
735             reader = csv.reader(fileobj, quotechar='"', delimiter=',')
736             # read the first line of the file (it contains columns titles)
737             for row in reader:
738                 f = row
739                 break
740         elif fileformat == 'po':
741             reader = TinyPoFile(fileobj)
742             f = ['type', 'name', 'res_id', 'src', 'value']
743         else:
744             raise Exception(_('Bad file format'))
745
746         # read the rest of the file
747         line = 1
748         for row in reader:
749             line += 1
750             # skip empty rows and rows where the translation field (=last fiefd) is empty
751             #if (not row) or (not row[-1]):
752             #    continue
753
754             # dictionary which holds values for this line of the csv file
755             # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
756             #  'src': ..., 'value': ...}
757             dic = {'lang': lang}
758             for i in range(len(f)):
759                 if f[i] in ('module',):
760                     continue
761                 dic[f[i]] = row[i]
762
763             try:
764                 dic['res_id'] = int(dic['res_id'])
765             except:
766                 model_data_ids = model_data_obj.search(cr, uid, [
767                     ('model', '=', dic['name'].split(',')[0]),
768                     ('module', '=', dic['res_id'].split('.', 1)[0]),
769                     ('name', '=', dic['res_id'].split('.', 1)[1]),
770                     ])
771                 if model_data_ids:
772                     dic['res_id'] = model_data_obj.browse(cr, uid,
773                             model_data_ids[0]).res_id
774                 else:
775                     dic['res_id'] = False
776
777             if dic['type'] == 'model' and not strict:
778                 (model, field) = dic['name'].split(',')
779
780                 # get the ids of the resources of this model which share
781                 # the same source
782                 obj = pool.get(model)
783                 if obj:
784                     if field not in obj.fields_get_keys(cr, uid):
785                         continue
786                     ids = obj.search(cr, uid, [(field, '=', dic['src'])])
787
788                     # if the resource id (res_id) is in that list, use it,
789                     # otherwise use the whole list
790                     if not ids:
791                         ids = []
792                     ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
793                     for id in ids:
794                         dic['res_id'] = id
795                         ids = trans_obj.search(cr, uid, [
796                             ('lang', '=', lang),
797                             ('type', '=', dic['type']),
798                             ('name', '=', dic['name']),
799                             ('src', '=', dic['src']),
800                             ('res_id', '=', dic['res_id'])
801                         ])
802                         if ids:
803                             trans_obj.write(cr, uid, ids, {'value': dic['value']})
804                         else:
805                             trans_obj.create(cr, uid, dic)
806             else:
807                 ids = trans_obj.search(cr, uid, [
808                     ('lang', '=', lang),
809                     ('type', '=', dic['type']),
810                     ('name', '=', dic['name']),
811                     ('src', '=', dic['src'])
812                 ])
813                 if ids:
814                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
815                 else:
816                     trans_obj.create(cr, uid, dic)
817             cr.commit()
818         cr.close()
819         if verbose:
820             logger.notifyChannel("i18n", netsvc.LOG_INFO,
821                     "translation file loaded succesfully")
822     except IOError:
823         filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
824         logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
825
826 def get_locales(lang=None):
827     if lang is None:
828         lang = locale.getdefaultlocale()[0]
829
830     if os.name == 'nt':
831         lang = _LOCALE2WIN32.get(lang, lang)
832
833     def process(enc):
834         ln = locale._build_localename((lang, enc))
835         yield ln
836         nln = locale.normalize(ln)
837         if nln != ln:
838             yield nln
839
840     for x in process('utf8'): yield x
841
842     prefenc = locale.getpreferredencoding()
843     if prefenc:
844         for x in process(prefenc): yield x
845
846         prefenc = {
847             'latin1': 'latin9',
848             'iso-8859-1': 'iso8859-15',
849             'cp1252': '1252',
850         }.get(prefenc.lower())
851         if prefenc:
852             for x in process(prefenc): yield x
853
854     yield lang
855
856
857
858 def resetlocale():
859     # locale.resetlocale is bugged with some locales.
860     for ln in get_locales():
861         try:
862             return locale.setlocale(locale.LC_ALL, ln)
863         except locale.Error:
864             continue
865
866 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
867