1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
29 import openerp.pooler as pooler
30 import openerp.sql_db as sql_db
36 from os.path import join
38 from datetime import datetime
39 from lxml import etree
43 from misc import UpdateableStr
44 from misc import SKIPPED_ELEMENT_TYPES
48 'af_ZA': 'Afrikaans_South Africa',
49 'sq_AL': 'Albanian_Albania',
50 'ar_SA': 'Arabic_Saudi Arabia',
51 'eu_ES': 'Basque_Spain',
52 'be_BY': 'Belarusian_Belarus',
53 'bs_BA': 'Serbian (Latin)',
54 'bg_BG': 'Bulgarian_Bulgaria',
55 'ca_ES': 'Catalan_Spain',
56 'hr_HR': 'Croatian_Croatia',
57 'zh_CN': 'Chinese_China',
58 'zh_TW': 'Chinese_Taiwan',
59 'cs_CZ': 'Czech_Czech Republic',
60 'da_DK': 'Danish_Denmark',
61 'nl_NL': 'Dutch_Netherlands',
62 'et_EE': 'Estonian_Estonia',
63 'fa_IR': 'Farsi_Iran',
64 'ph_PH': 'Filipino_Philippines',
65 'fi_FI': 'Finnish_Finland',
66 'fr_FR': 'French_France',
67 'fr_BE': 'French_France',
68 'fr_CH': 'French_France',
69 'fr_CA': 'French_France',
70 'ga': 'Scottish Gaelic',
71 'gl_ES': 'Galician_Spain',
72 'ka_GE': 'Georgian_Georgia',
73 'de_DE': 'German_Germany',
74 'el_GR': 'Greek_Greece',
75 'gu': 'Gujarati_India',
76 'he_IL': 'Hebrew_Israel',
78 'hu': 'Hungarian_Hungary',
79 'is_IS': 'Icelandic_Iceland',
80 'id_ID': 'Indonesian_indonesia',
81 'it_IT': 'Italian_Italy',
82 'ja_JP': 'Japanese_Japan',
85 'ko_KR': 'Korean_Korea',
87 'lt_LT': 'Lithuanian_Lithuania',
88 'lat': 'Latvian_Latvia',
89 'ml_IN': 'Malayalam_India',
90 'id_ID': 'Indonesian_indonesia',
92 'mn': 'Cyrillic_Mongolian',
93 'no_NO': 'Norwegian_Norway',
94 'nn_NO': 'Norwegian-Nynorsk_Norway',
95 'pl': 'Polish_Poland',
96 'pt_PT': 'Portuguese_Portugal',
97 'pt_BR': 'Portuguese_Brazil',
98 'ro_RO': 'Romanian_Romania',
99 'ru_RU': 'Russian_Russia',
101 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
102 'sk_SK': 'Slovak_Slovakia',
103 'sl_SI': 'Slovenian_Slovenia',
104 #should find more specific locales for spanish countries,
105 #but better than nothing
106 'es_AR': 'Spanish_Spain',
107 'es_BO': 'Spanish_Spain',
108 'es_CL': 'Spanish_Spain',
109 'es_CO': 'Spanish_Spain',
110 'es_CR': 'Spanish_Spain',
111 'es_DO': 'Spanish_Spain',
112 'es_EC': 'Spanish_Spain',
113 'es_ES': 'Spanish_Spain',
114 'es_GT': 'Spanish_Spain',
115 'es_HN': 'Spanish_Spain',
116 'es_MX': 'Spanish_Spain',
117 'es_NI': 'Spanish_Spain',
118 'es_PA': 'Spanish_Spain',
119 'es_PE': 'Spanish_Spain',
120 'es_PR': 'Spanish_Spain',
121 'es_PY': 'Spanish_Spain',
122 'es_SV': 'Spanish_Spain',
123 'es_UY': 'Spanish_Spain',
124 'es_VE': 'Spanish_Spain',
125 'sv_SE': 'Swedish_Sweden',
126 'ta_IN': 'English_Australia',
127 'th_TH': 'Thai_Thailand',
129 'tr_TR': 'Turkish_Turkey',
130 'uk_UA': 'Ukrainian_Ukraine',
131 'vi_VN': 'Vietnamese_Viet Nam',
132 'tlh_TLH': 'Klingon',
137 class UNIX_LINE_TERMINATOR(csv.excel):
138 lineterminator = '\n'
140 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
143 # Warning: better use self.pool.get('ir.translation')._get_source if you can
145 def translate(cr, name, source_type, lang, source=None):
147 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))
149 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
151 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
152 res_trans = cr.fetchone()
153 res = res_trans and res_trans[0] or False
156 logger = logging.getLogger('translate')
158 class GettextAlias(object):
161 # find current DB based on thread/worker db name (see netsvc)
162 db_name = getattr(threading.currentThread(), 'dbname', None)
164 return sql_db.db_connect(db_name)
166 def _get_cr(self, frame):
168 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
170 s = frame.f_locals.get('self', {})
171 cr = getattr(s, 'cr', None)
179 def _get_lang(self, frame):
181 ctx = frame.f_locals.get('context')
183 kwargs = frame.f_locals.get('kwargs')
185 args = frame.f_locals.get('args')
186 if args and isinstance(args, (list, tuple)) \
187 and isinstance(args[-1], dict):
189 elif isinstance(kwargs, dict):
190 ctx = kwargs.get('context')
192 lang = ctx.get('lang')
194 s = frame.f_locals.get('self', {})
195 c = getattr(s, 'localcontext', None)
200 def __call__(self, source):
205 frame = inspect.currentframe()
211 lang = self._get_lang(frame)
213 cr, is_new_cr = self._get_cr(frame)
215 # Try to use ir.translation to benefit from global cache if possible
216 pool = pooler.get_pool(cr.dbname)
217 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
219 logger.debug('no context cursor detected, skipping translation for "%r"', source)
221 logger.debug('no translation language detected, skipping translation for "%r" ', source)
223 logger.debug('translation went wrong for "%r", skipped', source)
224 # if so, double-check the root/base translations filenames
234 """Returns quoted PO term string, with special PO characters escaped"""
235 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
236 return '"%s"' % s.replace('\\','\\\\') \
237 .replace('"','\\"') \
238 .replace('\n', '\\n"\n"')
240 re_escaped_char = re.compile(r"(\\.)")
241 re_escaped_replacements = {'n': '\n', }
243 def _sub_replacement(match_obj):
244 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
247 """Returns unquoted PO term string, with special PO characters unescaped"""
248 return re_escaped_char.sub(_sub_replacement, str[1:-1])
250 # class to handle po files
251 class TinyPoFile(object):
252 def __init__(self, buffer):
253 self.logger = logging.getLogger('i18n')
256 def warn(self, msg, *args):
257 self.logger.warning(msg, *args)
261 self.lines = self._get_lines()
262 self.lines_count = len(self.lines);
268 def _get_lines(self):
269 lines = self.buffer.readlines()
270 # remove the BOM (Byte Order Mark):
272 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
274 lines.append('') # ensure that the file ends with at least an empty line
278 return (self.lines_count - len(self.lines))
281 type = name = res_id = source = trad = None
284 type, name, res_id, source, trad = self.tnrs.pop(0)
292 if 0 == len(self.lines):
293 raise StopIteration()
294 line = self.lines.pop(0).strip()
295 while line.startswith('#'):
296 if line.startswith('#~ '):
298 if line.startswith('#:'):
299 if ' ' in line[2:].strip():
300 for lpart in line[2:].strip().split(' '):
301 tmp_tnrs.append(lpart.strip().split(':',2))
303 tmp_tnrs.append( line[2:].strip().split(':',2) )
304 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
306 line = self.lines.pop(0).strip()
308 # allow empty lines between comments and msgid
309 line = self.lines.pop(0).strip()
310 if line.startswith('#~ '):
311 while line.startswith('#~ ') or not line.strip():
312 if 0 == len(self.lines):
313 raise StopIteration()
314 line = self.lines.pop(0)
315 # This has been a deprecated entry, don't return anything
318 if not line.startswith('msgid'):
319 raise Exception("malformed file: bad line: %s" % line)
320 source = unquote(line[6:])
321 line = self.lines.pop(0).strip()
322 if not source and self.first:
323 # if the source is "" and it's the first msgid, it's the special
324 # msgstr with the informations about the traduction and the
325 # traductor; we skip it
328 line = self.lines.pop(0).strip()
331 while not line.startswith('msgstr'):
333 raise Exception('malformed file at %d'% self.cur_line())
334 source += unquote(line)
335 line = self.lines.pop(0).strip()
337 trad = unquote(line[7:])
338 line = self.lines.pop(0).strip()
340 trad += unquote(line)
341 line = self.lines.pop(0).strip()
343 if tmp_tnrs and not fuzzy:
344 type, name, res_id = tmp_tnrs.pop(0)
345 for t, n, r in tmp_tnrs:
346 self.tnrs.append((t, n, r, source, trad))
352 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
353 self.cur_line(), source[:30])
355 return type, name, res_id, source, trad
357 def write_infos(self, modules):
358 import openerp.release as release
359 self.buffer.write("# Translation of %(project)s.\n" \
360 "# This file contains the translation of the following modules:\n" \
365 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
366 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
367 '''"POT-Creation-Date: %(now)s\\n"\n''' \
368 '''"PO-Revision-Date: %(now)s\\n"\n''' \
369 '''"Last-Translator: <>\\n"\n''' \
370 '''"Language-Team: \\n"\n''' \
371 '''"MIME-Version: 1.0\\n"\n''' \
372 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
373 '''"Content-Transfer-Encoding: \\n"\n''' \
374 '''"Plural-Forms: \\n"\n''' \
377 % { 'project': release.description,
378 'version': release.version,
379 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
380 'bugmail': release.support_email,
381 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
385 def write(self, modules, tnrs, source, trad):
387 plurial = len(modules) > 1 and 's' or ''
388 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
392 for typy, name, res_id in tnrs:
393 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
398 # only strings in python code are python formated
399 self.buffer.write("#, python-format\n")
401 if not isinstance(trad, unicode):
402 trad = unicode(trad, 'utf8')
403 if not isinstance(source, unicode):
404 source = unicode(source, 'utf8')
408 % (quote(source), quote(trad))
409 self.buffer.write(msg.encode('utf8'))
412 # Methods to export the translation file
414 def trans_export(lang, modules, buffer, format, cr):
416 def _process(format, modules, rows, buffer, lang, newlang):
418 writer=csv.writer(buffer, 'UNIX')
423 writer = TinyPoFile(buffer)
424 writer.write_infos(modules)
426 # we now group the translations by source. That means one translation per source.
428 for module, type, name, res_id, src, trad in rows:
429 row = grouped_rows.setdefault(src, {})
430 row.setdefault('modules', set()).add(module)
431 if ('translation' not in row) or (not row['translation']):
432 row['translation'] = trad
433 row.setdefault('tnrs', []).append((type, name, res_id))
435 for src, row in grouped_rows.items():
436 writer.write(row['modules'], row['tnrs'], src, row['translation'])
438 elif format == 'tgz':
443 # first row is the "header", as in csv, it will be popped
444 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
445 rows_by_module[module].append(row)
447 tmpdir = tempfile.mkdtemp()
448 for mod, modrows in rows_by_module.items():
449 tmpmoddir = join(tmpdir, mod, 'i18n')
450 os.makedirs(tmpmoddir)
451 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
452 buf = file(join(tmpmoddir, pofilename), 'w')
453 _process('po', [mod], modrows, buf, lang, newlang)
456 tar = tarfile.open(fileobj=buffer, mode='w|gz')
461 raise Exception(_('Bad file format'))
463 newlang = not bool(lang)
466 trans = trans_generate(lang, modules, cr)
467 if newlang and format!='csv':
470 modules = set([t[0] for t in trans[1:]])
471 _process(format, modules, trans, buffer, lang, newlang)
474 def trans_parse_xsl(de):
479 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
481 l = m.text.strip().replace('\n',' ')
483 res.append(l.encode("utf8"))
484 res.extend(trans_parse_xsl(n))
487 def trans_parse_rml(de):
491 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
493 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
494 for s in string_list:
496 res.append(s.encode("utf8"))
497 res.extend(trans_parse_rml(n))
500 def trans_parse_view(de):
502 if de.tag == 'attribute' and de.get("name") == 'string':
504 res.append(de.text.encode("utf8"))
506 res.append(de.get('string').encode("utf8"))
508 res.append(de.get('help').encode("utf8"))
510 res.append(de.get('sum').encode("utf8"))
511 if de.get("confirm"):
512 res.append(de.get('confirm').encode("utf8"))
514 res.extend(trans_parse_view(n))
517 # tests whether an object is in a list of modules
518 def in_modules(object_name, modules):
527 module = object_name.split('.')[0]
528 module = module_dict.get(module, module)
529 return module in modules
531 def trans_generate(lang, modules, cr):
532 logger = logging.getLogger('i18n')
535 pool = pooler.get_pool(dbname)
536 trans_obj = pool.get('ir.translation')
537 model_data_obj = pool.get('ir.model.data')
539 l = pool.obj_pool.items()
542 query = 'SELECT name, model, res_id, module' \
543 ' FROM ir_model_data'
545 query_models = """SELECT m.id, m.model, imd.module
546 FROM ir_model AS m, ir_model_data AS imd
547 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
549 if 'all_installed' in modules:
550 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
551 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
553 if 'all' not in modules:
554 query += ' WHERE module IN %s'
555 query_models += ' AND imd.module in %s'
556 query_param = (tuple(modules),)
557 query += ' ORDER BY module, model, name'
558 query_models += ' ORDER BY module, model'
560 cr.execute(query, query_param)
563 def push_translation(module, type, name, id, source):
564 tuple = (module, source, name, id, type)
565 if source and tuple not in _to_translate:
566 _to_translate.append(tuple)
569 if isinstance(s, unicode):
570 return s.encode('utf8')
573 for (xml_name,model,res_id,module) in cr.fetchall():
574 module = encode(module)
575 model = encode(model)
576 xml_name = "%s.%s" % (module, encode(xml_name))
578 if not pool.get(model):
579 logger.error("Unable to find object %r", model)
582 exists = pool.get(model).exists(cr, uid, res_id)
584 logger.warning("Unable to find object %r with id %d", model, res_id)
586 obj = pool.get(model).browse(cr, uid, res_id)
588 if model=='ir.ui.view':
589 d = etree.XML(encode(obj.arch))
590 for t in trans_parse_view(d):
591 push_translation(module, 'view', encode(obj.model), 0, t)
592 elif model=='ir.actions.wizard':
593 service_name = 'wizard.'+encode(obj.wiz_name)
594 import openerp.netsvc as netsvc
595 if netsvc.Service._services.get(service_name):
596 obj2 = netsvc.Service._services[service_name]
597 for state_name, state_def in obj2.states.iteritems():
598 if 'result' in state_def:
599 result = state_def['result']
600 if result['type'] != 'form':
602 name = "%s,%s" % (encode(obj.wiz_name), state_name)
605 'string': ('wizard_field', lambda s: [encode(s)]),
606 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
607 'help': ('help', lambda s: [encode(s)]),
611 if not result.has_key('fields'):
612 logger.warning("res has no fields: %r", result)
614 for field_name, field_def in result['fields'].iteritems():
615 res_name = name + ',' + field_name
617 for fn in def_params:
619 transtype, modifier = def_params[fn]
620 for val in modifier(field_def[fn]):
621 push_translation(module, transtype, res_name, 0, val)
624 arch = result['arch']
625 if arch and not isinstance(arch, UpdateableStr):
627 for t in trans_parse_view(d):
628 push_translation(module, 'wizard_view', name, 0, t)
630 # export button labels
631 for but_args in result['state']:
632 button_name = but_args[0]
633 button_label = but_args[1]
634 res_name = name + ',' + button_name
635 push_translation(module, 'wizard_button', res_name, 0, button_label)
637 elif model=='ir.model.fields':
639 field_name = encode(obj.name)
640 except AttributeError, exc:
641 logger.error("name error in %s: %s", xml_name, str(exc))
643 objmodel = pool.get(obj.model)
644 if not objmodel or not field_name in objmodel._columns:
646 field_def = objmodel._columns[field_name]
648 name = "%s,%s" % (encode(obj.model), field_name)
649 push_translation(module, 'field', name, 0, encode(field_def.string))
652 push_translation(module, 'help', name, 0, encode(field_def.help))
654 if field_def.translate:
655 ids = objmodel.search(cr, uid, [])
656 obj_values = objmodel.read(cr, uid, ids, [field_name])
657 for obj_value in obj_values:
658 res_id = obj_value['id']
659 if obj.name in ('ir.model', 'ir.ui.menu'):
661 model_data_ids = model_data_obj.search(cr, uid, [
662 ('model', '=', model),
663 ('res_id', '=', res_id),
665 if not model_data_ids:
666 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
668 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
669 for dummy, val in field_def.selection:
670 push_translation(module, 'selection', name, 0, encode(val))
672 elif model=='ir.actions.report.xml':
673 name = encode(obj.report_name)
676 fname = obj.report_rml
677 parse_func = trans_parse_rml
678 report_type = "report"
680 fname = obj.report_xsl
681 parse_func = trans_parse_xsl
683 if fname and obj.report_type in ('pdf', 'xsl'):
685 report_file = misc.file_open(fname)
687 d = etree.parse(report_file)
688 for t in parse_func(d.iter()):
689 push_translation(module, report_type, name, 0, t)
692 except (IOError, etree.XMLSyntaxError):
693 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
695 for field_name,field_def in obj._table._columns.items():
696 if field_def.translate:
697 name = model + "," + field_name
699 trad = getattr(obj, field_name) or ''
702 push_translation(module, 'model', name, xml_name, encode(trad))
704 # End of data for ir.model.data query results
706 cr.execute(query_models, query_param)
708 def push_constraint_msg(module, term_type, model, msg):
709 # Check presence of __call__ directly instead of using
710 # callable() because it will be deprecated as of Python 3.0
711 if not hasattr(msg, '__call__'):
712 push_translation(module, term_type, model, 0, encode(msg))
714 for (model_id, model, module) in cr.fetchall():
715 module = encode(module)
716 model = encode(model)
718 model_obj = pool.get(model)
721 logging.getLogger("i18n").error("Unable to find object %r", model)
724 for constraint in getattr(model_obj, '_constraints', []):
725 push_constraint_msg(module, 'constraint', model, constraint[1])
727 for constraint in getattr(model_obj, '_sql_constraints', []):
728 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
730 # parse source code for _() calls
731 def get_module_from_path(path, mod_paths=None):
733 # First, construct a list of possible paths
734 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
735 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
738 mod_paths.append(adp)
739 if not os.path.isabs(adp):
740 mod_paths.append(adp)
741 elif adp.startswith(def_path):
742 mod_paths.append(adp[len(def_path)+1:])
744 if path.startswith(mp) and (os.path.dirname(path) != mp):
745 path = path[len(mp)+1:]
746 return path.split(os.path.sep)[0]
747 return 'base' # files that are not in a module are considered as being in 'base' module
749 modobj = pool.get('ir.module.module')
750 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
751 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
753 root_path = os.path.join(config.config['root_path'], 'addons')
755 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
756 if root_path in apaths:
759 path_list = [root_path,] + apaths
761 # Also scan these non-addon paths
762 for bin_path in ['osv', 'report' ]:
763 path_list.append(os.path.join(config.config['root_path'], bin_path))
765 logger.debug("Scanning modules at paths: ", path_list)
768 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
769 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
770 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
771 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
773 def export_code_terms_from_file(fname, path, root, terms_type):
774 fabsolutepath = join(root, fname)
775 frelativepath = fabsolutepath[len(path):]
776 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
777 is_mod_installed = module in installed_modules
778 if (('all' in modules) or (module in modules)) and is_mod_installed:
779 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
780 src_file = misc.file_open(fabsolutepath, subdir='')
782 code_string = src_file.read()
785 if module in installed_modules:
786 frelativepath = str("addons" + frelativepath)
787 ite = re_dquotes.finditer(code_string)
792 if src.startswith('""'):
793 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
796 src = join_dquotes.sub(r'\1', src)
797 # try to count the lines from the last pos to our place:
798 code_line += code_string[code_offset:i.start(1)].count('\n')
799 # now, since we did a binary read of a python source file, we
800 # have to expand pythonic escapes like the interpreter does.
801 src = src.decode('string_escape')
802 push_translation(module, terms_type, frelativepath, code_line, encode(src))
803 code_line += i.group(1).count('\n')
804 code_offset = i.end() # we have counted newlines up to the match end
806 ite = re_quotes.finditer(code_string)
807 code_offset = 0 #reset counters
811 if src.startswith("''"):
812 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
815 src = join_quotes.sub(r'\1', src)
816 code_line += code_string[code_offset:i.start(1)].count('\n')
817 src = src.decode('string_escape')
818 push_translation(module, terms_type, frelativepath, code_line, encode(src))
819 code_line += i.group(1).count('\n')
820 code_offset = i.end() # we have counted newlines up to the match end
822 for path in path_list:
823 logger.debug("Scanning files of modules at %s", path)
824 for root, dummy, files in osutil.walksymlinks(path):
825 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
826 export_code_terms_from_file(fname, path, root, 'code')
827 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
828 export_code_terms_from_file(fname, path, root, 'report')
831 out = [["module","type","name","res_id","src","value"]] # header
833 # translate strings marked as to be translated
834 for module, source, name, id, type in _to_translate:
835 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
836 out.append([module, type, name, id, source, encode(trans) or ''])
840 def trans_load(cr, filename, lang, verbose=True, context=None):
841 logger = logging.getLogger('i18n')
843 fileobj = open(filename,'r')
844 logger.info("loading %s", filename)
845 fileformat = os.path.splitext(filename)[-1][1:].lower()
846 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
851 logger.error("couldn't read translation file %s", filename)
854 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
855 """Populates the ir_translation table. Fixing the res_ids so that they point
856 correctly to ir_model_data is done in a separate step, using the
857 'trans_update_res_ids' function below."""
858 logger = logging.getLogger('i18n')
860 logger.info('loading translation file for language %s', lang)
864 pool = pooler.get_pool(db_name)
865 lang_obj = pool.get('res.lang')
866 trans_obj = pool.get('ir.translation')
867 model_data_obj = pool.get('ir.model.data')
868 iso_lang = misc.get_iso_codes(lang)
871 ids = lang_obj.search(cr, uid, [('code','=', lang)])
874 # lets create the language with locale information
875 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
878 # now, the serious things: we read the language file
880 if fileformat == 'csv':
881 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
882 # read the first line of the file (it contains columns titles)
886 elif fileformat == 'po':
887 reader = TinyPoFile(fileobj)
888 f = ['type', 'name', 'res_id', 'src', 'value']
890 logger.error('Bad file format: %s', fileformat)
891 raise Exception(_('Bad file format'))
893 # read the rest of the file
897 # skip empty rows and rows where the translation field (=last fiefd) is empty
898 #if (not row) or (not row[-1]):
901 # dictionary which holds values for this line of the csv file
902 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
903 # 'src': ..., 'value': ...}
905 for i in range(len(f)):
906 if f[i] in ('module',):
911 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
912 dic['module'] = False
913 dic['xml_id'] = False
915 split_id = dic['res_id'].split('.', 1)
916 dic['module'] = split_id[0]
917 dic['xml_id'] = split_id[1]
918 dic['res_id'] = False
922 ('type', '=', dic['type']),
923 ('name', '=', dic['name']),
924 ('src', '=', dic['src']),
926 if dic['type'] == 'model':
927 if dic['res_id'] is False:
928 args.append(('module', '=', dic['module']))
929 args.append(('xml_id', '=', dic['xml_id']))
931 args.append(('res_id', '=', dic['res_id']))
932 ids = trans_obj.search(cr, uid, args)
934 if context.get('overwrite') and dic['value']:
935 trans_obj.write(cr, uid, ids, {'value': dic['value']})
937 trans_obj.create(cr, uid, dic)
939 logger.info("translation file loaded succesfully")
941 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
942 logger.exception("couldn't read translation file %s", filename)
944 def trans_update_res_ids(cr):
946 UPDATE ir_translation
947 SET res_id = COALESCE ((SELECT ir_model_data.res_id
949 WHERE ir_translation.module = ir_model_data.module
950 AND ir_translation.xml_id = ir_model_data.name), 0)
951 WHERE ir_translation.module is not null
952 AND ir_translation.xml_id is not null
953 AND ir_translation.res_id = 0;
956 def get_locales(lang=None):
958 lang = locale.getdefaultlocale()[0]
961 lang = _LOCALE2WIN32.get(lang, lang)
964 ln = locale._build_localename((lang, enc))
966 nln = locale.normalize(ln)
970 for x in process('utf8'): yield x
972 prefenc = locale.getpreferredencoding()
974 for x in process(prefenc): yield x
978 'iso-8859-1': 'iso8859-15',
980 }.get(prefenc.lower())
982 for x in process(prefenc): yield x
989 # locale.resetlocale is bugged with some locales.
990 for ln in get_locales():
992 return locale.setlocale(locale.LC_ALL, ln)
996 def load_language(cr, lang):
997 """Loads a translation terms for a language.
998 Used mainly to automate language loading at db initialization.
1000 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1003 pool = pooler.get_pool(cr.dbname)
1004 language_installer = pool.get('base.language.install')
1006 oid = language_installer.create(cr, uid, {'lang': lang})
1007 language_installer.lang_install(cr, uid, [oid], context=None)
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: