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 ##############################################################################
36 from os.path import join
38 from datetime import datetime
39 from lxml import etree
43 from tools.misc import UpdateableStr
44 from tools.misc import SKIPPED_ELEMENT_TYPES
47 'af_ZA': 'Afrikaans_South Africa',
48 'sq_AL': 'Albanian_Albania',
49 'ar_SA': 'Arabic_Saudi Arabia',
50 'eu_ES': 'Basque_Spain',
51 'be_BY': 'Belarusian_Belarus',
52 'bs_BA': 'Serbian (Latin)',
53 'bg_BG': 'Bulgarian_Bulgaria',
54 'ca_ES': 'Catalan_Spain',
55 'hr_HR': 'Croatian_Croatia',
56 'zh_CN': 'Chinese_China',
57 'zh_TW': 'Chinese_Taiwan',
58 'cs_CZ': 'Czech_Czech Republic',
59 'da_DK': 'Danish_Denmark',
60 'nl_NL': 'Dutch_Netherlands',
61 'et_EE': 'Estonian_Estonia',
62 'fa_IR': 'Farsi_Iran',
63 'ph_PH': 'Filipino_Philippines',
64 'fi_FI': 'Finnish_Finland',
65 'fr_FR': 'French_France',
66 'fr_BE': 'French_France',
67 'fr_CH': 'French_France',
68 'fr_CA': 'French_France',
69 'ga': 'Scottish Gaelic',
70 'gl_ES': 'Galician_Spain',
71 'ka_GE': 'Georgian_Georgia',
72 'de_DE': 'German_Germany',
73 'el_GR': 'Greek_Greece',
74 'gu': 'Gujarati_India',
75 'he_IL': 'Hebrew_Israel',
77 'hu': 'Hungarian_Hungary',
78 'is_IS': 'Icelandic_Iceland',
79 'id_ID': 'Indonesian_indonesia',
80 'it_IT': 'Italian_Italy',
81 'ja_JP': 'Japanese_Japan',
84 'ko_KR': 'Korean_Korea',
86 'lt_LT': 'Lithuanian_Lithuania',
87 'lat': 'Latvian_Latvia',
88 'ml_IN': 'Malayalam_India',
89 'id_ID': 'Indonesian_indonesia',
91 'mn': 'Cyrillic_Mongolian',
92 'no_NO': 'Norwegian_Norway',
93 'nn_NO': 'Norwegian-Nynorsk_Norway',
94 'pl': 'Polish_Poland',
95 'pt_PT': 'Portuguese_Portugal',
96 'pt_BR': 'Portuguese_Brazil',
97 'ro_RO': 'Romanian_Romania',
98 'ru_RU': 'Russian_Russia',
100 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
101 'sk_SK': 'Slovak_Slovakia',
102 'sl_SI': 'Slovenian_Slovenia',
103 #should find more specific locales for spanish countries,
104 #but better than nothing
105 'es_AR': 'Spanish_Spain',
106 'es_BO': 'Spanish_Spain',
107 'es_CL': 'Spanish_Spain',
108 'es_CO': 'Spanish_Spain',
109 'es_CR': 'Spanish_Spain',
110 'es_DO': 'Spanish_Spain',
111 'es_EC': 'Spanish_Spain',
112 'es_ES': 'Spanish_Spain',
113 'es_GT': 'Spanish_Spain',
114 'es_HN': 'Spanish_Spain',
115 'es_MX': 'Spanish_Spain',
116 'es_NI': 'Spanish_Spain',
117 'es_PA': 'Spanish_Spain',
118 'es_PE': 'Spanish_Spain',
119 'es_PR': 'Spanish_Spain',
120 'es_PY': 'Spanish_Spain',
121 'es_SV': 'Spanish_Spain',
122 'es_UY': 'Spanish_Spain',
123 'es_VE': 'Spanish_Spain',
124 'sv_SE': 'Swedish_Sweden',
125 'ta_IN': 'English_Australia',
126 'th_TH': 'Thai_Thailand',
128 'tr_TR': 'Turkish_Turkey',
129 'uk_UA': 'Ukrainian_Ukraine',
130 'vi_VN': 'Vietnamese_Viet Nam',
131 'tlh_TLH': 'Klingon',
136 class UNIX_LINE_TERMINATOR(csv.excel):
137 lineterminator = '\n'
139 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
142 # Warning: better use self.pool.get('ir.translation')._get_source if you can
144 def translate(cr, name, source_type, lang, source=None):
146 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))
148 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
150 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
151 res_trans = cr.fetchone()
152 res = res_trans and res_trans[0] or False
155 logger = logging.getLogger('translate')
157 class GettextAlias(object):
160 # find current DB based on thread/worker db name (see netsvc)
161 db_name = getattr(threading.currentThread(), 'dbname', None)
163 return pooler.get_db_only(db_name)
165 def _get_cr(self, frame):
167 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
169 s = frame.f_locals.get('self', {})
170 cr = getattr(s, 'cr', None)
178 def _get_lang(self, frame):
180 ctx = frame.f_locals.get('context')
182 kwargs = frame.f_locals.get('kwargs')
184 args = frame.f_locals.get('args')
185 if args and isinstance(args, (list, tuple)) \
186 and isinstance(args[-1], dict):
188 elif isinstance(kwargs, dict):
189 ctx = kwargs.get('context')
191 lang = ctx.get('lang')
193 s = frame.f_locals.get('self', {})
194 c = getattr(s, 'localcontext', None)
199 def __call__(self, source):
204 frame = inspect.currentframe()
210 lang = self._get_lang(frame)
212 cr, is_new_cr = self._get_cr(frame)
214 # Try to use ir.translation to benefit from global cache if possible
215 pool = pooler.get_pool(cr.dbname)
216 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
218 logger.debug('no context cursor detected, skipping translation for "%r"', source)
220 logger.debug('no translation language detected, skipping translation for "%r" ', source)
222 logger.debug('translation went wrong for "%r", skipped', source)
223 # if so, double-check the root/base translations filenames
233 """Returns quoted PO term string, with special PO characters escaped"""
234 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
235 return '"%s"' % s.replace('\\','\\\\') \
236 .replace('"','\\"') \
237 .replace('\n', '\\n"\n"')
239 re_escaped_char = re.compile(r"(\\.)")
240 re_escaped_replacements = {'n': '\n', }
242 def _sub_replacement(match_obj):
243 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
246 """Returns unquoted PO term string, with special PO characters unescaped"""
247 return re_escaped_char.sub(_sub_replacement, str[1:-1])
249 # class to handle po files
250 class TinyPoFile(object):
251 def __init__(self, buffer):
252 self.logger = logging.getLogger('i18n')
255 def warn(self, msg, *args):
256 self.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):
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: %(bugmail)s\\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 'bugmail': release.support_email,
383 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
387 def write(self, modules, tnrs, source, trad):
389 plurial = len(modules) > 1 and 's' or ''
390 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
394 for typy, name, res_id in tnrs:
395 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
400 # only strings in python code are python formated
401 self.buffer.write("#, python-format\n")
403 if not isinstance(trad, unicode):
404 trad = unicode(trad, 'utf8')
405 if not isinstance(source, unicode):
406 source = unicode(source, 'utf8')
410 % (quote(source), quote(trad))
411 self.buffer.write(msg.encode('utf8'))
414 # Methods to export the translation file
416 def trans_export(lang, modules, buffer, format, cr):
418 def _process(format, modules, rows, buffer, lang, newlang):
420 writer=csv.writer(buffer, 'UNIX')
425 writer = tools.TinyPoFile(buffer)
426 writer.write_infos(modules)
428 # we now group the translations by source. That means one translation per source.
430 for module, type, name, res_id, src, trad in rows:
431 row = grouped_rows.setdefault(src, {})
432 row.setdefault('modules', set()).add(module)
433 if ('translation' not in row) or (not row['translation']):
434 row['translation'] = trad
435 row.setdefault('tnrs', []).append((type, name, res_id))
437 for src, row in grouped_rows.items():
438 writer.write(row['modules'], row['tnrs'], src, row['translation'])
440 elif format == 'tgz':
445 # first row is the "header", as in csv, it will be popped
446 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
447 rows_by_module[module].append(row)
449 tmpdir = tempfile.mkdtemp()
450 for mod, modrows in rows_by_module.items():
451 tmpmoddir = join(tmpdir, mod, 'i18n')
452 os.makedirs(tmpmoddir)
453 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
454 buf = file(join(tmpmoddir, pofilename), 'w')
455 _process('po', [mod], modrows, buf, lang, newlang)
458 tar = tarfile.open(fileobj=buffer, mode='w|gz')
463 raise Exception(_('Bad file 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('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 if netsvc.Service._services.get(service_name):
595 obj2 = netsvc.Service._services[service_name]
596 for state_name, state_def in obj2.states.iteritems():
597 if 'result' in state_def:
598 result = state_def['result']
599 if result['type'] != 'form':
601 name = "%s,%s" % (encode(obj.wiz_name), state_name)
604 'string': ('wizard_field', lambda s: [encode(s)]),
605 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
606 'help': ('help', lambda s: [encode(s)]),
610 if not result.has_key('fields'):
611 logger.warning("res has no fields: %r", result)
613 for field_name, field_def in result['fields'].iteritems():
614 res_name = name + ',' + field_name
616 for fn in def_params:
618 transtype, modifier = def_params[fn]
619 for val in modifier(field_def[fn]):
620 push_translation(module, transtype, res_name, 0, val)
623 arch = result['arch']
624 if arch and not isinstance(arch, UpdateableStr):
626 for t in trans_parse_view(d):
627 push_translation(module, 'wizard_view', name, 0, t)
629 # export button labels
630 for but_args in result['state']:
631 button_name = but_args[0]
632 button_label = but_args[1]
633 res_name = name + ',' + button_name
634 push_translation(module, 'wizard_button', res_name, 0, button_label)
636 elif model=='ir.model.fields':
638 field_name = encode(obj.name)
639 except AttributeError, exc:
640 logger.error("name error in %s: %s", xml_name, str(exc))
642 objmodel = pool.get(obj.model)
643 if not objmodel or not field_name in objmodel._columns:
645 field_def = objmodel._columns[field_name]
647 name = "%s,%s" % (encode(obj.model), field_name)
648 push_translation(module, 'field', name, 0, encode(field_def.string))
651 push_translation(module, 'help', name, 0, encode(field_def.help))
653 if field_def.translate:
654 ids = objmodel.search(cr, uid, [])
655 obj_values = objmodel.read(cr, uid, ids, [field_name])
656 for obj_value in obj_values:
657 res_id = obj_value['id']
658 if obj.name in ('ir.model', 'ir.ui.menu'):
660 model_data_ids = model_data_obj.search(cr, uid, [
661 ('model', '=', model),
662 ('res_id', '=', res_id),
664 if not model_data_ids:
665 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
667 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
668 for dummy, val in field_def.selection:
669 push_translation(module, 'selection', name, 0, encode(val))
671 elif model=='ir.actions.report.xml':
672 name = encode(obj.report_name)
675 fname = obj.report_rml
676 parse_func = trans_parse_rml
677 report_type = "report"
679 fname = obj.report_xsl
680 parse_func = trans_parse_xsl
682 if fname and obj.report_type in ('pdf', 'xsl'):
684 report_file = tools.file_open(fname)
686 d = etree.parse(report_file)
687 for t in parse_func(d.iter()):
688 push_translation(module, report_type, name, 0, t)
691 except (IOError, etree.XMLSyntaxError):
692 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
694 for field_name,field_def in obj._table._columns.items():
695 if field_def.translate:
696 name = model + "," + field_name
698 trad = getattr(obj, field_name) or ''
701 push_translation(module, 'model', name, xml_name, encode(trad))
703 # End of data for ir.model.data query results
705 cr.execute(query_models, query_param)
707 def push_constraint_msg(module, term_type, model, msg):
708 # Check presence of __call__ directly instead of using
709 # callable() because it will be deprecated as of Python 3.0
710 if not hasattr(msg, '__call__'):
711 push_translation(module, term_type, model, 0, encode(msg))
713 for (model_id, model, module) in cr.fetchall():
714 module = encode(module)
715 model = encode(model)
717 model_obj = pool.get(model)
720 logging.getLogger("i18n").error("Unable to find object %r", model)
723 for constraint in getattr(model_obj, '_constraints', []):
724 push_constraint_msg(module, 'constraint', model, constraint[1])
726 for constraint in getattr(model_obj, '_sql_constraints', []):
727 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
729 # parse source code for _() calls
730 def get_module_from_path(path, mod_paths=None):
732 # First, construct a list of possible paths
733 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
734 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
737 mod_paths.append(adp)
738 if not os.path.isabs(adp):
739 mod_paths.append(adp)
740 elif adp.startswith(def_path):
741 mod_paths.append(adp[len(def_path)+1:])
743 if path.startswith(mp) and (os.path.dirname(path) != mp):
744 path = path[len(mp)+1:]
745 return path.split(os.path.sep)[0]
746 return 'base' # files that are not in a module are considered as being in 'base' module
748 modobj = pool.get('ir.module.module')
749 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
750 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
752 root_path = os.path.join(tools.config['root_path'], 'addons')
754 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
755 if root_path in apaths:
758 path_list = [root_path,] + apaths
760 # Also scan these non-addon paths
761 for bin_path in ['osv', 'report' ]:
762 path_list.append(os.path.join(tools.config['root_path'], bin_path))
764 logger.debug("Scanning modules at paths: ", path_list)
767 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
768 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
769 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
770 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
772 def export_code_terms_from_file(fname, path, root, terms_type):
773 fabsolutepath = join(root, fname)
774 frelativepath = fabsolutepath[len(path):]
775 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
776 is_mod_installed = module in installed_modules
777 if (('all' in modules) or (module in modules)) and is_mod_installed:
778 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
779 src_file = tools.file_open(fabsolutepath, subdir='')
781 code_string = src_file.read()
784 if module in installed_modules:
785 frelativepath = str("addons" + frelativepath)
786 ite = re_dquotes.finditer(code_string)
791 if src.startswith('""'):
792 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
795 src = join_dquotes.sub(r'\1', src)
796 # try to count the lines from the last pos to our place:
797 code_line += code_string[code_offset:i.start(1)].count('\n')
798 # now, since we did a binary read of a python source file, we
799 # have to expand pythonic escapes like the interpreter does.
800 src = src.decode('string_escape')
801 push_translation(module, terms_type, frelativepath, code_line, encode(src))
802 code_line += i.group(1).count('\n')
803 code_offset = i.end() # we have counted newlines up to the match end
805 ite = re_quotes.finditer(code_string)
806 code_offset = 0 #reset counters
810 if src.startswith("''"):
811 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
814 src = join_quotes.sub(r'\1', src)
815 code_line += code_string[code_offset:i.start(1)].count('\n')
816 src = src.decode('string_escape')
817 push_translation(module, terms_type, frelativepath, code_line, encode(src))
818 code_line += i.group(1).count('\n')
819 code_offset = i.end() # we have counted newlines up to the match end
821 for path in path_list:
822 logger.debug("Scanning files of modules at %s", path)
823 for root, dummy, files in tools.osutil.walksymlinks(path):
824 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
825 export_code_terms_from_file(fname, path, root, 'code')
826 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
827 export_code_terms_from_file(fname, path, root, 'report')
830 out = [["module","type","name","res_id","src","value"]] # header
832 # translate strings marked as to be translated
833 for module, source, name, id, type in _to_translate:
834 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
835 out.append([module, type, name, id, source, encode(trans) or ''])
839 def trans_load(cr, filename, lang, verbose=True, context=None):
840 logger = logging.getLogger('i18n')
842 fileobj = open(filename,'r')
843 logger.info("loading %s", filename)
844 fileformat = os.path.splitext(filename)[-1][1:].lower()
845 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
850 logger.error("couldn't read translation file %s", filename)
853 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
854 """Populates the ir_translation table. Fixing the res_ids so that they point
855 correctly to ir_model_data is done in a separate step, using the
856 'trans_update_res_ids' function below."""
857 logger = logging.getLogger('i18n')
859 logger.info('loading translation file for language %s', lang)
863 pool = pooler.get_pool(db_name)
864 lang_obj = pool.get('res.lang')
865 trans_obj = pool.get('ir.translation')
866 model_data_obj = pool.get('ir.model.data')
867 iso_lang = tools.get_iso_codes(lang)
870 ids = lang_obj.search(cr, uid, [('code','=', lang)])
873 # lets create the language with locale information
874 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
877 # now, the serious things: we read the language file
879 if fileformat == 'csv':
880 #Setting the limit of data while loading a CSV
881 csv.field_size_limit(sys.maxint)
882 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
883 # read the first line of the file (it contains columns titles)
887 elif fileformat == 'po':
888 reader = TinyPoFile(fileobj)
889 f = ['type', 'name', 'res_id', 'src', 'value']
891 logger.error('Bad file format: %s', fileformat)
892 raise Exception(_('Bad file format'))
894 # read the rest of the file
898 # skip empty rows and rows where the translation field (=last fiefd) is empty
899 #if (not row) or (not row[-1]):
902 # dictionary which holds values for this line of the csv file
903 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
904 # 'src': ..., 'value': ...}
906 for i in range(len(f)):
907 if f[i] in ('module',):
912 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
913 dic['module'] = False
914 dic['xml_id'] = False
916 split_id = dic['res_id'].split('.', 1)
917 dic['module'] = split_id[0]
918 dic['xml_id'] = split_id[1]
919 dic['res_id'] = False
923 ('type', '=', dic['type']),
924 ('name', '=', dic['name']),
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']))
933 args.append(('src', '=', dic['src']))
935 ids = trans_obj.search(cr, uid, args)
937 if context.get('overwrite') and dic['value']:
938 trans_obj.write(cr, uid, ids, {'value': dic['value']})
940 trans_obj.create(cr, uid, dic)
942 logger.info("translation file loaded succesfully")
944 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
945 logger.exception("couldn't read translation file %s", filename)
947 def trans_update_res_ids(cr):
949 UPDATE ir_translation
950 SET res_id = COALESCE ((SELECT ir_model_data.res_id
952 WHERE ir_translation.module = ir_model_data.module
953 AND ir_translation.xml_id = ir_model_data.name), 0)
954 WHERE ir_translation.module is not null
955 AND ir_translation.xml_id is not null
956 AND ir_translation.res_id = 0;
959 def get_locales(lang=None):
961 lang = locale.getdefaultlocale()[0]
964 lang = _LOCALE2WIN32.get(lang, lang)
967 ln = locale._build_localename((lang, enc))
969 nln = locale.normalize(ln)
973 for x in process('utf8'): yield x
975 prefenc = locale.getpreferredencoding()
977 for x in process(prefenc): yield x
981 'iso-8859-1': 'iso8859-15',
983 }.get(prefenc.lower())
985 for x in process(prefenc): yield x
992 # locale.resetlocale is bugged with some locales.
993 for ln in get_locales():
995 return locale.setlocale(locale.LC_ALL, ln)
999 def load_language(cr, lang):
1000 """Loads a translation terms for a language.
1001 Used mainly to automate language loading at db initialization.
1003 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1006 pool = pooler.get_pool(cr.dbname)
1007 language_installer = pool.get('base.language.install')
1009 oid = language_installer.create(cr, uid, {'lang': lang})
1010 language_installer.lang_install(cr, uid, [oid], context=None)
1012 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: