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
47 _logger = logging.getLogger(__name__)
50 'af_ZA': 'Afrikaans_South Africa',
51 'sq_AL': 'Albanian_Albania',
52 'ar_SA': 'Arabic_Saudi Arabia',
53 'eu_ES': 'Basque_Spain',
54 'be_BY': 'Belarusian_Belarus',
55 'bs_BA': 'Serbian (Latin)',
56 'bg_BG': 'Bulgarian_Bulgaria',
57 'ca_ES': 'Catalan_Spain',
58 'hr_HR': 'Croatian_Croatia',
59 'zh_CN': 'Chinese_China',
60 'zh_TW': 'Chinese_Taiwan',
61 'cs_CZ': 'Czech_Czech Republic',
62 'da_DK': 'Danish_Denmark',
63 'nl_NL': 'Dutch_Netherlands',
64 'et_EE': 'Estonian_Estonia',
65 'fa_IR': 'Farsi_Iran',
66 'ph_PH': 'Filipino_Philippines',
67 'fi_FI': 'Finnish_Finland',
68 'fr_FR': 'French_France',
69 'fr_BE': 'French_France',
70 'fr_CH': 'French_France',
71 'fr_CA': 'French_France',
72 'ga': 'Scottish Gaelic',
73 'gl_ES': 'Galician_Spain',
74 'ka_GE': 'Georgian_Georgia',
75 'de_DE': 'German_Germany',
76 'el_GR': 'Greek_Greece',
77 'gu': 'Gujarati_India',
78 'he_IL': 'Hebrew_Israel',
80 'hu': 'Hungarian_Hungary',
81 'is_IS': 'Icelandic_Iceland',
82 'id_ID': 'Indonesian_indonesia',
83 'it_IT': 'Italian_Italy',
84 'ja_JP': 'Japanese_Japan',
87 'ko_KR': 'Korean_Korea',
89 'lt_LT': 'Lithuanian_Lithuania',
90 'lat': 'Latvian_Latvia',
91 'ml_IN': 'Malayalam_India',
92 'id_ID': 'Indonesian_indonesia',
94 'mn': 'Cyrillic_Mongolian',
95 'no_NO': 'Norwegian_Norway',
96 'nn_NO': 'Norwegian-Nynorsk_Norway',
97 'pl': 'Polish_Poland',
98 'pt_PT': 'Portuguese_Portugal',
99 'pt_BR': 'Portuguese_Brazil',
100 'ro_RO': 'Romanian_Romania',
101 'ru_RU': 'Russian_Russia',
103 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
104 'sk_SK': 'Slovak_Slovakia',
105 'sl_SI': 'Slovenian_Slovenia',
106 #should find more specific locales for spanish countries,
107 #but better than nothing
108 'es_AR': 'Spanish_Spain',
109 'es_BO': 'Spanish_Spain',
110 'es_CL': 'Spanish_Spain',
111 'es_CO': 'Spanish_Spain',
112 'es_CR': 'Spanish_Spain',
113 'es_DO': 'Spanish_Spain',
114 'es_EC': 'Spanish_Spain',
115 'es_ES': 'Spanish_Spain',
116 'es_GT': 'Spanish_Spain',
117 'es_HN': 'Spanish_Spain',
118 'es_MX': 'Spanish_Spain',
119 'es_NI': 'Spanish_Spain',
120 'es_PA': 'Spanish_Spain',
121 'es_PE': 'Spanish_Spain',
122 'es_PR': 'Spanish_Spain',
123 'es_PY': 'Spanish_Spain',
124 'es_SV': 'Spanish_Spain',
125 'es_UY': 'Spanish_Spain',
126 'es_VE': 'Spanish_Spain',
127 'sv_SE': 'Swedish_Sweden',
128 'ta_IN': 'English_Australia',
129 'th_TH': 'Thai_Thailand',
131 'tr_TR': 'Turkish_Turkey',
132 'uk_UA': 'Ukrainian_Ukraine',
133 'vi_VN': 'Vietnamese_Viet Nam',
134 'tlh_TLH': 'Klingon',
139 class UNIX_LINE_TERMINATOR(csv.excel):
140 lineterminator = '\n'
142 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
145 # Warning: better use self.pool.get('ir.translation')._get_source if you can
147 def translate(cr, name, source_type, lang, source=None):
149 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))
151 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
153 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
154 res_trans = cr.fetchone()
155 res = res_trans and res_trans[0] or False
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):
255 def warn(self, msg, *args):
256 _logger.warning(msg, *args)
260 self.lines = self._get_lines()
261 self.lines_count = len(self.lines);
267 def _get_lines(self):
268 lines = self.buffer.readlines()
269 # remove the BOM (Byte Order Mark):
271 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
273 lines.append('') # ensure that the file ends with at least an empty line
277 return (self.lines_count - len(self.lines))
280 type = name = res_id = source = trad = None
283 type, name, res_id, source, trad = self.tnrs.pop(0)
291 if 0 == len(self.lines):
292 raise StopIteration()
293 line = self.lines.pop(0).strip()
294 while line.startswith('#'):
295 if line.startswith('#~ '):
297 if line.startswith('#:'):
298 for lpart in line[2:].strip().split(' '):
299 trans_info = lpart.strip().split(':',2)
300 if trans_info and len(trans_info) == 2:
301 # looks like the translation type is missing, which is not
302 # unexpected because it is not a GetText standard. Default: 'code'
303 trans_info[:0] = ['code']
304 if trans_info and len(trans_info) == 3:
305 tmp_tnrs.append(trans_info)
306 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
308 line = self.lines.pop(0).strip()
310 # allow empty lines between comments and msgid
311 line = self.lines.pop(0).strip()
312 if line.startswith('#~ '):
313 while line.startswith('#~ ') or not line.strip():
314 if 0 == len(self.lines):
315 raise StopIteration()
316 line = self.lines.pop(0)
317 # This has been a deprecated entry, don't return anything
320 if not line.startswith('msgid'):
321 raise Exception("malformed file: bad line: %s" % line)
322 source = unquote(line[6:])
323 line = self.lines.pop(0).strip()
324 if not source and self.first:
325 # if the source is "" and it's the first msgid, it's the special
326 # msgstr with the informations about the traduction and the
327 # traductor; we skip it
330 line = self.lines.pop(0).strip()
333 while not line.startswith('msgstr'):
335 raise Exception('malformed file at %d'% self.cur_line())
336 source += unquote(line)
337 line = self.lines.pop(0).strip()
339 trad = unquote(line[7:])
340 line = self.lines.pop(0).strip()
342 trad += unquote(line)
343 line = self.lines.pop(0).strip()
345 if tmp_tnrs and not fuzzy:
346 type, name, res_id = tmp_tnrs.pop(0)
347 for t, n, r in tmp_tnrs:
348 self.tnrs.append((t, n, r, source, trad))
354 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
355 self.cur_line(), source[:30])
357 return type, name, res_id, source, trad
359 def write_infos(self, modules):
360 import openerp.release as release
361 self.buffer.write("# Translation of %(project)s.\n" \
362 "# This file contains the translation of the following modules:\n" \
367 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
368 '''"Report-Msgid-Bugs-To: \\n"\n''' \
369 '''"POT-Creation-Date: %(now)s\\n"\n''' \
370 '''"PO-Revision-Date: %(now)s\\n"\n''' \
371 '''"Last-Translator: <>\\n"\n''' \
372 '''"Language-Team: \\n"\n''' \
373 '''"MIME-Version: 1.0\\n"\n''' \
374 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
375 '''"Content-Transfer-Encoding: \\n"\n''' \
376 '''"Plural-Forms: \\n"\n''' \
379 % { 'project': release.description,
380 'version': release.version,
381 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
382 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
386 def write(self, modules, tnrs, source, trad):
388 plurial = len(modules) > 1 and 's' or ''
389 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
393 for typy, name, res_id in tnrs:
394 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
399 # only strings in python code are python formated
400 self.buffer.write("#, python-format\n")
402 if not isinstance(trad, unicode):
403 trad = unicode(trad, 'utf8')
404 if not isinstance(source, unicode):
405 source = unicode(source, 'utf8')
409 % (quote(source), quote(trad))
410 self.buffer.write(msg.encode('utf8'))
413 # Methods to export the translation file
415 def trans_export(lang, modules, buffer, format, cr):
417 def _process(format, modules, rows, buffer, lang, newlang):
419 writer=csv.writer(buffer, 'UNIX')
424 writer = TinyPoFile(buffer)
425 writer.write_infos(modules)
427 # we now group the translations by source. That means one translation per source.
429 for module, type, name, res_id, src, trad in rows:
430 row = grouped_rows.setdefault(src, {})
431 row.setdefault('modules', set()).add(module)
432 if ('translation' not in row) or (not row['translation']):
433 row['translation'] = trad
434 row.setdefault('tnrs', []).append((type, name, res_id))
436 for src, row in grouped_rows.items():
437 writer.write(row['modules'], row['tnrs'], src, row['translation'])
439 elif format == 'tgz':
444 # first row is the "header", as in csv, it will be popped
445 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
446 rows_by_module[module].append(row)
448 tmpdir = tempfile.mkdtemp()
449 for mod, modrows in rows_by_module.items():
450 tmpmoddir = join(tmpdir, mod, 'i18n')
451 os.makedirs(tmpmoddir)
452 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
453 buf = file(join(tmpmoddir, pofilename), 'w')
454 _process('po', [mod], modrows, buf, lang, newlang)
457 tar = tarfile.open(fileobj=buffer, mode='w|gz')
462 raise Exception(_('Unrecognized extension: must be one of '
463 '.csv, .po, or .tgz (received .%s).' % format))
465 newlang = not bool(lang)
468 trans = trans_generate(lang, modules, cr)
469 if newlang and format!='csv':
472 modules = set([t[0] for t in trans[1:]])
473 _process(format, modules, trans, buffer, lang, newlang)
476 def trans_parse_xsl(de):
481 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
483 l = m.text.strip().replace('\n',' ')
485 res.append(l.encode("utf8"))
486 res.extend(trans_parse_xsl(n))
489 def trans_parse_rml(de):
493 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
495 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
496 for s in string_list:
498 res.append(s.encode("utf8"))
499 res.extend(trans_parse_rml(n))
502 def trans_parse_view(de):
504 if de.tag == 'attribute' and de.get("name") == 'string':
506 res.append(de.text.encode("utf8"))
508 res.append(de.get('string').encode("utf8"))
510 res.append(de.get('help').encode("utf8"))
512 res.append(de.get('sum').encode("utf8"))
513 if de.get("confirm"):
514 res.append(de.get('confirm').encode("utf8"))
516 res.extend(trans_parse_view(n))
519 # tests whether an object is in a list of modules
520 def in_modules(object_name, modules):
529 module = object_name.split('.')[0]
530 module = module_dict.get(module, module)
531 return module in modules
533 def trans_generate(lang, modules, cr):
536 pool = pooler.get_pool(dbname)
537 trans_obj = pool.get('ir.translation')
538 model_data_obj = pool.get('ir.model.data')
540 l = pool.models.items()
543 query = 'SELECT name, model, res_id, module' \
544 ' FROM ir_model_data'
546 query_models = """SELECT m.id, m.model, imd.module
547 FROM ir_model AS m, ir_model_data AS imd
548 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
550 if 'all_installed' in modules:
551 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
552 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
554 if 'all' not in modules:
555 query += ' WHERE module IN %s'
556 query_models += ' AND imd.module in %s'
557 query_param = (tuple(modules),)
558 query += ' ORDER BY module, model, name'
559 query_models += ' ORDER BY module, model'
561 cr.execute(query, query_param)
564 def push_translation(module, type, name, id, source):
565 tuple = (module, source, name, id, type)
566 if source and tuple not in _to_translate:
567 _to_translate.append(tuple)
570 if isinstance(s, unicode):
571 return s.encode('utf8')
574 for (xml_name,model,res_id,module) in cr.fetchall():
575 module = encode(module)
576 model = encode(model)
577 xml_name = "%s.%s" % (module, encode(xml_name))
579 if not pool.get(model):
580 _logger.error("Unable to find object %r", model)
583 exists = pool.get(model).exists(cr, uid, res_id)
585 _logger.warning("Unable to find object %r with id %d", model, res_id)
587 obj = pool.get(model).browse(cr, uid, res_id)
589 if model=='ir.ui.view':
590 d = etree.XML(encode(obj.arch))
591 for t in trans_parse_view(d):
592 push_translation(module, 'view', encode(obj.model), 0, t)
593 elif model=='ir.actions.wizard':
594 service_name = 'wizard.'+encode(obj.wiz_name)
595 import openerp.netsvc as netsvc
596 if netsvc.Service._services.get(service_name):
597 obj2 = netsvc.Service._services[service_name]
598 for state_name, state_def in obj2.states.iteritems():
599 if 'result' in state_def:
600 result = state_def['result']
601 if result['type'] != 'form':
603 name = "%s,%s" % (encode(obj.wiz_name), state_name)
606 'string': ('wizard_field', lambda s: [encode(s)]),
607 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
608 'help': ('help', lambda s: [encode(s)]),
612 if not result.has_key('fields'):
613 _logger.warning("res has no fields: %r", result)
615 for field_name, field_def in result['fields'].iteritems():
616 res_name = name + ',' + field_name
618 for fn in def_params:
620 transtype, modifier = def_params[fn]
621 for val in modifier(field_def[fn]):
622 push_translation(module, transtype, res_name, 0, val)
625 arch = result['arch']
626 if arch and not isinstance(arch, UpdateableStr):
628 for t in trans_parse_view(d):
629 push_translation(module, 'wizard_view', name, 0, t)
631 # export button labels
632 for but_args in result['state']:
633 button_name = but_args[0]
634 button_label = but_args[1]
635 res_name = name + ',' + button_name
636 push_translation(module, 'wizard_button', res_name, 0, button_label)
638 elif model=='ir.model.fields':
640 field_name = encode(obj.name)
641 except AttributeError, exc:
642 _logger.error("name error in %s: %s", xml_name, str(exc))
644 objmodel = pool.get(obj.model)
645 if not objmodel or not field_name in objmodel._columns:
647 field_def = objmodel._columns[field_name]
649 name = "%s,%s" % (encode(obj.model), field_name)
650 push_translation(module, 'field', name, 0, encode(field_def.string))
653 push_translation(module, 'help', name, 0, encode(field_def.help))
655 if field_def.translate:
656 ids = objmodel.search(cr, uid, [])
657 obj_values = objmodel.read(cr, uid, ids, [field_name])
658 for obj_value in obj_values:
659 res_id = obj_value['id']
660 if obj.name in ('ir.model', 'ir.ui.menu'):
662 model_data_ids = model_data_obj.search(cr, uid, [
663 ('model', '=', model),
664 ('res_id', '=', res_id),
666 if not model_data_ids:
667 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
669 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
670 for dummy, val in field_def.selection:
671 push_translation(module, 'selection', name, 0, encode(val))
673 elif model=='ir.actions.report.xml':
674 name = encode(obj.report_name)
677 fname = obj.report_rml
678 parse_func = trans_parse_rml
679 report_type = "report"
681 fname = obj.report_xsl
682 parse_func = trans_parse_xsl
684 if fname and obj.report_type in ('pdf', 'xsl'):
686 report_file = misc.file_open(fname)
688 d = etree.parse(report_file)
689 for t in parse_func(d.iter()):
690 push_translation(module, report_type, name, 0, t)
693 except (IOError, etree.XMLSyntaxError):
694 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
696 for field_name,field_def in obj._table._columns.items():
697 if field_def.translate:
698 name = model + "," + field_name
700 trad = getattr(obj, field_name) or ''
703 push_translation(module, 'model', name, xml_name, encode(trad))
705 # End of data for ir.model.data query results
707 cr.execute(query_models, query_param)
709 def push_constraint_msg(module, term_type, model, msg):
710 # Check presence of __call__ directly instead of using
711 # callable() because it will be deprecated as of Python 3.0
712 if not hasattr(msg, '__call__'):
713 push_translation(module, term_type, model, 0, encode(msg))
715 for (model_id, model, module) in cr.fetchall():
716 module = encode(module)
717 model = encode(model)
719 model_obj = pool.get(model)
722 _logger.error("Unable to find object %r", model)
725 for constraint in getattr(model_obj, '_constraints', []):
726 push_constraint_msg(module, 'constraint', model, constraint[1])
728 for constraint in getattr(model_obj, '_sql_constraints', []):
729 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
731 # parse source code for _() calls
732 def get_module_from_path(path, mod_paths=None):
734 # First, construct a list of possible paths
735 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
736 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
739 mod_paths.append(adp)
740 if not os.path.isabs(adp):
741 mod_paths.append(adp)
742 elif adp.startswith(def_path):
743 mod_paths.append(adp[len(def_path)+1:])
745 if path.startswith(mp) and (os.path.dirname(path) != mp):
746 path = path[len(mp)+1:]
747 return path.split(os.path.sep)[0]
748 return 'base' # files that are not in a module are considered as being in 'base' module
750 modobj = pool.get('ir.module.module')
751 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
752 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
754 root_path = os.path.join(config.config['root_path'], 'addons')
756 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
757 if root_path in apaths:
760 path_list = [root_path,] + apaths
762 # Also scan these non-addon paths
763 for bin_path in ['osv', 'report' ]:
764 path_list.append(os.path.join(config.config['root_path'], bin_path))
766 _logger.debug("Scanning modules at paths: ", path_list)
769 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
770 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
771 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
772 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
774 def export_code_terms_from_file(fname, path, root, terms_type):
775 fabsolutepath = join(root, fname)
776 frelativepath = fabsolutepath[len(path):]
777 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
778 is_mod_installed = module in installed_modules
779 if (('all' in modules) or (module in modules)) and is_mod_installed:
780 _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
781 src_file = misc.file_open(fabsolutepath, subdir='')
783 code_string = src_file.read()
786 if module in installed_modules:
787 frelativepath = str("addons" + frelativepath)
788 ite = re_dquotes.finditer(code_string)
793 if src.startswith('""'):
794 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
797 src = join_dquotes.sub(r'\1', src)
798 # try to count the lines from the last pos to our place:
799 code_line += code_string[code_offset:i.start(1)].count('\n')
800 # now, since we did a binary read of a python source file, we
801 # have to expand pythonic escapes like the interpreter does.
802 src = src.decode('string_escape')
803 push_translation(module, terms_type, frelativepath, code_line, encode(src))
804 code_line += i.group(1).count('\n')
805 code_offset = i.end() # we have counted newlines up to the match end
807 ite = re_quotes.finditer(code_string)
808 code_offset = 0 #reset counters
812 if src.startswith("''"):
813 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
816 src = join_quotes.sub(r'\1', src)
817 code_line += code_string[code_offset:i.start(1)].count('\n')
818 src = src.decode('string_escape')
819 push_translation(module, terms_type, frelativepath, code_line, encode(src))
820 code_line += i.group(1).count('\n')
821 code_offset = i.end() # we have counted newlines up to the match end
823 for path in path_list:
824 _logger.debug("Scanning files of modules at %s", path)
825 for root, dummy, files in osutil.walksymlinks(path):
826 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
827 export_code_terms_from_file(fname, path, root, 'code')
828 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
829 export_code_terms_from_file(fname, path, root, 'report')
832 out = [["module","type","name","res_id","src","value"]] # header
834 # translate strings marked as to be translated
835 for module, source, name, id, type in _to_translate:
836 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
837 out.append([module, type, name, id, source, encode(trans) or ''])
841 def trans_load(cr, filename, lang, verbose=True, context=None):
843 fileobj = misc.file_open(filename)
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."""
857 _logger.info('loading translation file for language %s', lang)
861 pool = pooler.get_pool(db_name)
862 lang_obj = pool.get('res.lang')
863 trans_obj = pool.get('ir.translation')
864 iso_lang = misc.get_iso_codes(lang)
867 ids = lang_obj.search(cr, uid, [('code','=', lang)])
870 # lets create the language with locale information
871 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
874 # now, the serious things: we read the language file
876 if fileformat == 'csv':
877 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
878 # read the first line of the file (it contains columns titles)
882 elif fileformat == 'po':
883 reader = TinyPoFile(fileobj)
884 f = ['type', 'name', 'res_id', 'src', 'value']
886 _logger.error('Bad file format: %s', fileformat)
887 raise Exception(_('Bad file format'))
889 # read the rest of the file
891 irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
895 # skip empty rows and rows where the translation field (=last fiefd) is empty
896 #if (not row) or (not row[-1]):
899 # dictionary which holds values for this line of the csv file
900 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
901 # 'src': ..., 'value': ...}
904 for i in range(len(f)):
905 if f[i] in ('module',):
909 # This would skip terms that fail to specify a res_id
910 if not dic.get('res_id', False):
913 res_id = dic.pop('res_id')
914 if res_id and isinstance(res_id, (int, long)) \
915 or (isinstance(res_id, basestring) and res_id.isdigit()):
916 dic['res_id'] = int(res_id)
919 tmodel = dic['name'].split(',')[0]
921 tmodule, tname = res_id.split('.', 1)
925 dic['imd_model'] = tmodel
926 dic['imd_module'] = tmodule
927 dic['imd_name'] = tname
931 _logger.warning("Could not decode resource for %s, please fix the po file.",
932 dic['res_id'], exc_info=True)
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 get_locales(lang=None):
946 lang = locale.getdefaultlocale()[0]
949 lang = _LOCALE2WIN32.get(lang, lang)
952 ln = locale._build_localename((lang, enc))
954 nln = locale.normalize(ln)
958 for x in process('utf8'): yield x
960 prefenc = locale.getpreferredencoding()
962 for x in process(prefenc): yield x
966 'iso-8859-1': 'iso8859-15',
968 }.get(prefenc.lower())
970 for x in process(prefenc): yield x
977 # locale.resetlocale is bugged with some locales.
978 for ln in get_locales():
980 return locale.setlocale(locale.LC_ALL, ln)
984 def load_language(cr, lang):
985 """Loads a translation terms for a language.
986 Used mainly to automate language loading at db initialization.
988 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
991 pool = pooler.get_pool(cr.dbname)
992 language_installer = pool.get('base.language.install')
994 oid = language_installer.create(cr, uid, {'lang': lang})
995 language_installer.lang_install(cr, uid, [oid], context=None)
997 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: