[FIX] Gengo - Update modoel ir_translation for gengo and make it working. Works with...
[odoo/odoo.git] / openerp / addons / base / ir / ir_translation.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 logging
23
24 from openerp import tools
25 import openerp.modules
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28
29 _logger = logging.getLogger(__name__)
30
31 TRANSLATION_TYPE = [
32     ('field', 'Field'),
33     ('model', 'Object'),
34     ('rml', 'RML  (deprecated - use Report)'), # Pending deprecation - to be replaced by report!
35     ('report', 'Report/Template'),
36     ('selection', 'Selection'),
37     ('view', 'View'),
38     ('wizard_button', 'Wizard Button'),
39     ('wizard_field', 'Wizard Field'),
40     ('wizard_view', 'Wizard View'),
41     ('xsl', 'XSL'),
42     ('help', 'Help'),
43     ('code', 'Code'),
44     ('constraint', 'Constraint'),
45     ('sql_constraint', 'SQL Constraint')
46 ]
47
48 class ir_translation_import_cursor(object):
49     """Temporary cursor for optimizing mass insert into ir.translation
50
51     Open it (attached to a sql cursor), feed it with translation data and
52     finish() it in order to insert multiple translations in a batch.
53     """
54     _table_name = 'tmp_ir_translation_import'
55
56     def __init__(self, cr, uid, parent, context):
57         """ Initializer
58
59         Store some values, and also create a temporary SQL table to accept
60         the data.
61         @param parent an instance of ir.translation ORM model
62         """
63         self._cr = cr
64         self._uid = uid
65         self._context = context
66         self._overwrite = context.get('overwrite', False)
67         self._debug = False
68         self._parent_table = parent._table
69
70         # Note that Postgres will NOT inherit the constraints or indexes
71         # of ir_translation, so this copy will be much faster.
72         cr.execute('''CREATE TEMP TABLE %s(
73             imd_model VARCHAR(64),
74             imd_name VARCHAR(128)
75             ) INHERITS (%s) ''' % (self._table_name, self._parent_table))
76
77     def push(self, trans_dict):
78         """Feed a translation, as a dictionary, into the cursor
79         """
80         params = dict(trans_dict, state="translated" if trans_dict['value'] else "to_translate")
81         self._cr.execute("""INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
82                             VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
83                                     %%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s)""" % self._table_name,
84                          params)
85
86     def finish(self):
87         """ Transfer the data from the temp table to ir.translation
88         """
89         cr = self._cr
90         if self._debug:
91             cr.execute("SELECT count(*) FROM %s" % self._table_name)
92             c = cr.fetchone()[0]
93             _logger.debug("ir.translation.cursor: We have %d entries to process", c)
94
95         # Step 1: resolve ir.model.data references to res_ids
96         cr.execute("""UPDATE %s AS ti
97             SET res_id = imd.res_id
98             FROM ir_model_data AS imd
99             WHERE ti.res_id IS NULL
100                 AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
101
102                 AND ti.module = imd.module AND ti.imd_name = imd.name
103                 AND ti.imd_model = imd.model; """ % self._table_name)
104
105         if self._debug:
106             cr.execute("SELECT module, imd_model, imd_name FROM %s " \
107                 "WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
108             for row in cr.fetchall():
109                 _logger.debug("ir.translation.cursor: missing res_id for %s. %s/%s ", *row)
110
111         # Records w/o res_id must _not_ be inserted into our db, because they are
112         # referencing non-existent data.
113         cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % \
114             self._table_name)
115
116         find_expr = "irt.lang = ti.lang AND irt.type = ti.type " \
117                     " AND irt.name = ti.name AND irt.src = ti.src " \
118                     " AND (ti.type != 'model' OR ti.res_id = irt.res_id) "
119
120         # Step 2: update existing (matching) translations
121         if self._overwrite:
122             cr.execute("""UPDATE ONLY %s AS irt
123                 SET value = ti.value,
124                 state = 'translated'
125                 FROM %s AS ti
126                 WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
127                 """ % (self._parent_table, self._table_name, find_expr))
128
129         # Step 3: insert new translations
130         cr.execute("""INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
131             SELECT name, lang, res_id, src, type, value, module, state, comments
132               FROM %s AS ti
133               WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
134               """ % (self._parent_table, self._table_name, self._parent_table, find_expr))
135
136         if self._debug:
137             cr.execute('SELECT COUNT(*) FROM ONLY %s' % self._parent_table)
138             c1 = cr.fetchone()[0]
139             cr.execute('SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s' % \
140                 (self._parent_table, self._table_name, find_expr))
141             c = cr.fetchone()[0]
142             _logger.debug("ir.translation.cursor:  %d entries now in ir.translation, %d common entries with tmp", c1, c)
143
144         # Step 4: cleanup
145         cr.execute("DROP TABLE %s" % self._table_name)
146         return True
147
148 class ir_translation(osv.osv):
149     _name = "ir.translation"
150     _log_access = False
151
152     def _get_language(self, cr, uid, context):
153         lang_model = self.pool.get('res.lang')
154         lang_ids = lang_model.search(cr, uid, [('translatable', '=', True)], context=context)
155         lang_data = lang_model.read(cr, uid, lang_ids, ['code', 'name'], context=context)
156         return [(d['code'], d['name']) for d in lang_data]
157
158     def _get_src(self, cr, uid, ids, name, arg, context=None):
159         ''' Get source name for the translation. If object type is model then
160         return the value store in db. Otherwise return value store in src field
161         '''
162         if context is None:
163             context = {}
164         res = dict.fromkeys(ids, False)
165         for record in self.browse(cr, uid, ids, context=context):
166             if record.type != 'model':
167                 res[record.id] = record.src
168             else:
169                 model_name, field = record.name.split(',')
170                 model = self.pool.get(model_name)
171                 if model and model.exists(cr, uid, record.res_id, context=context):
172                     # Pass context without lang, need to read real stored field, not translation
173                     context_no_lang = dict(context, lang=None)
174                     result = model.read(cr, uid, record.res_id, [field], context=context_no_lang)
175                     res[record.id] = result[field] if result else False
176         return res
177
178     def _set_src(self, cr, uid, id, name, value, args, context=None):
179         ''' When changing source term of a translation, change its value in db for
180         the associated object, and the src field
181         '''
182         if context is None:
183             context = {}
184         record = self.browse(cr, uid, id, context=context)
185         if  record.type == 'model':
186             model_name, field = record.name.split(',')
187             model = self.pool.get(model_name)
188             #We need to take the context without the language information, because we want to write on the
189             #value store in db and not on the one associate with current language.
190             #Also not removing lang from context trigger an error when lang is different
191             context_wo_lang = context.copy()
192             context_wo_lang.pop('lang', None)
193             model.write(cr, uid, record.res_id, {field: value}, context=context_wo_lang)
194         return self.write(cr, uid, id, {'src': value}, context=context)
195
196     _columns = {
197         'name': fields.char('Translated field', required=True),
198         'res_id': fields.integer('Record ID', select=True),
199         'lang': fields.selection(_get_language, string='Language'),
200         'type': fields.selection(TRANSLATION_TYPE, string='Type', select=True),
201         'src': fields.text('Old source'),
202         'source': fields.function(_get_src, fnct_inv=_set_src, type='text', string='Source'),
203         'value': fields.text('Translation Value'),
204         'module': fields.char('Module', help="Module this term belongs to", select=True),
205
206         'state': fields.selection(
207             [('to_translate','To Translate'),
208              ('inprogress','Translation in Progress'),
209              ('translated','Translated')],
210             string="Status",
211             help="Automatically set to let administators find new terms that might need to be translated"),
212
213         # aka gettext extracted-comments - we use them to flag openerp-web translation
214         # cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
215         'comments': fields.text('Translation comments', select=True),
216     }
217
218     _defaults = {
219         'state': 'to_translate',
220     }
221
222     _sql_constraints = [ ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
223         'Language code of translation item must be among known languages' ), ]
224
225     def _auto_init(self, cr, context=None):
226         super(ir_translation, self)._auto_init(cr, context)
227
228         # FIXME: there is a size limit on btree indexed values so we can't index src column with normal btree.
229         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_ltns',))
230         if cr.fetchone():
231             #temporarily removed: cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)')
232             cr.execute('DROP INDEX ir_translation_ltns')
233             cr.commit()
234         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_lts',))
235         if cr.fetchone():
236             #temporarily removed: cr.execute('CREATE INDEX ir_translation_lts ON ir_translation (lang, type, src)')
237             cr.execute('DROP INDEX ir_translation_lts')
238             cr.commit()
239
240         # add separate hash index on src (no size limit on values), as postgres 8.1+ is able to combine separate indexes
241         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_src_hash_idx',))
242         if not cr.fetchone():
243             cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation using hash (src)')
244
245         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_ltn',))
246         if not cr.fetchone():
247             cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
248             cr.commit()
249
250     def _check_selection_field_value(self, cr, uid, field, value, context=None):
251         if field == 'lang':
252             return
253         return super(ir_translation, self)._check_selection_field_value(cr, uid, field, value, context=context)
254
255     @tools.ormcache_multi(skiparg=3, multi=6)
256     def _get_ids(self, cr, uid, name, tt, lang, ids):
257         translations = dict.fromkeys(ids, False)
258         if ids:
259             cr.execute('select res_id,value '
260                     'from ir_translation '
261                     'where lang=%s '
262                         'and type=%s '
263                         'and name=%s '
264                         'and res_id IN %s',
265                     (lang,tt,name,tuple(ids)))
266             for res_id, value in cr.fetchall():
267                 translations[res_id] = value
268         return translations
269
270     def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
271         self._get_ids.clear_cache(self)
272         self._get_source.clear_cache(self)
273
274         cr.execute('delete from ir_translation '
275                 'where lang=%s '
276                     'and type=%s '
277                     'and name=%s '
278                     'and res_id IN %s',
279                 (lang,tt,name,tuple(ids),))
280         for id in ids:
281             self.create(cr, uid, {
282                 'lang':lang,
283                 'type':tt,
284                 'name':name,
285                 'res_id':id,
286                 'value':value,
287                 'src':src,
288                 })
289         return len(ids)
290
291     def _get_source_query(self, cr, uid, name, types, lang, source, res_id):
292         if source:
293             query = """SELECT value
294                        FROM ir_translation
295                        WHERE lang=%s
296                         AND type in %s
297                         AND src=%s"""
298             params = (lang or '', types, tools.ustr(source))
299             if res_id:
300                 query += "AND res_id=%s"
301                 params += (res_id,)
302             if name:
303                 query += " AND name=%s"
304                 params += (tools.ustr(name),)
305         else:
306             query = """SELECT value
307                        FROM ir_translation
308                        WHERE lang=%s
309                         AND type in %s
310                         AND name=%s"""
311
312             params = (lang or '', types, tools.ustr(name))
313         
314         return (query, params)
315
316     @tools.ormcache(skiparg=3)
317     def _get_source(self, cr, uid, name, types, lang, source=None, res_id=None):
318         """
319         Returns the translation for the given combination of name, type, language
320         and source. All values passed to this method should be unicode (not byte strings),
321         especially ``source``.
322
323         :param name: identification of the term to translate, such as field name (optional if source is passed)
324         :param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
325         :param lang: language code of the desired translation
326         :param source: optional source term to translate (should be unicode)
327         :param res_id: optional resource id to translate (if used, ``source`` should be set)
328         :rtype: unicode
329         :return: the request translation, or an empty unicode string if no translation was
330                  found and `source` was not passed
331         """
332         # FIXME: should assert that `source` is unicode and fix all callers to always pass unicode
333         # so we can remove the string encoding/decoding.
334         if not lang:
335             return tools.ustr(source or '')
336         if isinstance(types, basestring):
337             types = (types,)
338         
339         query, params = self._get_source_query(cr, uid, name, types, lang, source, res_id)
340         
341         cr.execute(query, params)
342         res = cr.fetchone()
343         trad = res and res[0] or u''
344         if source and not trad:
345             return tools.ustr(source)
346         return trad
347
348     def create(self, cr, uid, vals, context=None):
349         if context is None:
350             context = {}
351         ids = super(ir_translation, self).create(cr, uid, vals, context=context)
352         self._get_source.clear_cache(self)
353         self._get_ids.clear_cache(self)
354         self.pool['ir.ui.view'].clear_cache()
355         return ids
356
357     def write(self, cursor, user, ids, vals, context=None):
358         if context is None:
359             context = {}
360         if isinstance(ids, (int, long)):
361             ids = [ids]
362         if vals.get('src') or ('value' in vals and not(vals.get('value'))):
363             vals.update({'state':'to_translate'})
364         if vals.get('value'):
365             vals.update({'state':'translated'})
366         result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
367         self._get_source.clear_cache(self)
368         self._get_ids.clear_cache(self)
369         self.pool['ir.ui.view'].clear_cache()
370         return result
371
372     def unlink(self, cursor, user, ids, context=None):
373         if context is None:
374             context = {}
375         if isinstance(ids, (int, long)):
376             ids = [ids]
377
378         self._get_source.clear_cache(self)
379         self._get_ids.clear_cache(self)
380         result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
381         return result
382
383     def translate_fields(self, cr, uid, model, id, field=None, context=None):
384         trans_model = self.pool[model]
385         domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
386         langs_ids = self.pool.get('res.lang').search(cr, uid, [('code', '!=', 'en_US')], context=context)
387         if not langs_ids:
388             raise osv.except_osv(_('Error'), _("Translation features are unavailable until you install an extra OpenERP translation."))
389         langs = [lg.code for lg in self.pool.get('res.lang').browse(cr, uid, langs_ids, context=context)]
390         main_lang = 'en_US'
391         translatable_fields = []
392         for f, info in trans_model._all_columns.items():
393             if info.column.translate:
394                 if info.parent_model:
395                     parent_id = trans_model.read(cr, uid, [id], [info.parent_column], context=context)[0][info.parent_column][0]
396                     translatable_fields.append({ 'name': f, 'id': parent_id, 'model': info.parent_model })
397                     domain.insert(0, '|')
398                     domain.extend(['&', ('res_id', '=', parent_id), ('name', '=', "%s,%s" % (info.parent_model, f))])
399                 else:
400                     translatable_fields.append({ 'name': f, 'id': id, 'model': model })
401         if len(langs):
402             fields = [f.get('name') for f in translatable_fields]
403             record = trans_model.read(cr, uid, [id], fields, context={ 'lang': main_lang })[0]
404             for lg in langs:
405                 for f in translatable_fields:
406                     # Check if record exists, else create it (at once)
407                     sql = """INSERT INTO ir_translation (lang, src, name, type, res_id, value)
408                         SELECT %s, %s, %s, 'model', %s, %s WHERE NOT EXISTS
409                         (SELECT 1 FROM ir_translation WHERE lang=%s AND name=%s AND res_id=%s AND type='model');
410                         UPDATE ir_translation SET src = %s WHERE lang=%s AND name=%s AND res_id=%s AND type='model';
411                         """
412                     src = record[f['name']] or None
413                     name = "%s,%s" % (f['model'], f['name'])
414                     cr.execute(sql, (lg, src , name, f['id'], src, lg, name, f['id'], src, lg, name, id))
415
416         action = {
417             'name': 'Translate',
418             'res_model': 'ir.translation',
419             'type': 'ir.actions.act_window',
420             'view_type': 'form',
421             'view_mode': 'tree,form',
422             'domain': domain,
423         }
424         if field:
425             info = trans_model._all_columns[field]
426             action['context'] = {
427                 'search_default_name': "%s,%s" % (info.parent_model or model, field)
428             }
429         return action
430
431     def _get_import_cursor(self, cr, uid, context=None):
432         """ Return a cursor-like object for fast inserting translations
433         """
434         return ir_translation_import_cursor(cr, uid, self, context=context)
435
436     def load_module_terms(self, cr, modules, langs, context=None):
437         context = dict(context or {}) # local copy
438         for module_name in modules:
439             modpath = openerp.modules.get_module_path(module_name)
440             if not modpath:
441                 continue
442             for lang in langs:
443                 lang_code = tools.get_iso_codes(lang)
444                 base_lang_code = None
445                 if '_' in lang_code:
446                     base_lang_code = lang_code.split('_')[0]
447
448                 # Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po)
449                 if base_lang_code:
450                     base_trans_file = openerp.modules.get_module_resource(module_name, 'i18n', base_lang_code + '.po')
451                     if base_trans_file:
452                         _logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
453                         tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
454                         context['overwrite'] = True # make sure the requested translation will override the base terms later
455
456                     # i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
457                     base_trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
458                     if base_trans_extra_file:
459                         _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
460                         tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
461                         context['overwrite'] = True # make sure the requested translation will override the base terms later
462
463                 # Step 2: then load the main translation file, possibly overriding the terms coming from the base language
464                 trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
465                 if trans_file:
466                     _logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
467                     tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
468                 elif lang_code != 'en_US':
469                     _logger.warning('module %s: no translation for language %s', module_name, lang_code)
470
471                 trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
472                 if trans_extra_file:
473                     _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
474                     tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
475         return True
476
477
478 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: