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, allow_create=True):
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)
171 if not cr and allow_create:
178 def _get_uid(self, frame):
179 return frame.f_locals.get('uid') or frame.f_locals.get('user')
181 def _get_lang(self, frame):
183 ctx = frame.f_locals.get('context')
185 kwargs = frame.f_locals.get('kwargs')
187 args = frame.f_locals.get('args')
188 if args and isinstance(args, (list, tuple)) \
189 and isinstance(args[-1], dict):
191 elif isinstance(kwargs, dict):
192 ctx = kwargs.get('context')
194 lang = ctx.get('lang')
195 s = frame.f_locals.get('self', {})
197 c = getattr(s, 'localcontext', None)
201 # Last resort: attempt to guess the language of the user
202 # Pitfall: some operations are performed in sudo mode, and we
203 # don't know the originial uid, so the language may
204 # be wrong when the admin language differs.
205 pool = getattr(s, 'pool', None)
206 (cr, dummy) = self._get_cr(frame, allow_create=False)
207 uid = self._get_uid(frame)
208 if pool and cr and uid:
209 lang = pool.get('res.users').context_get(cr, uid)['lang']
212 def __call__(self, source):
217 frame = inspect.currentframe()
223 lang = self._get_lang(frame)
225 cr, is_new_cr = self._get_cr(frame)
227 # Try to use ir.translation to benefit from global cache if possible
228 pool = pooler.get_pool(cr.dbname)
229 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
231 logger.debug('no context cursor detected, skipping translation for "%r"', source)
233 logger.debug('no translation language detected, skipping translation for "%r" ', source)
235 logger.debug('translation went wrong for "%r", skipped', source)
236 # if so, double-check the root/base translations filenames
246 """Returns quoted PO term string, with special PO characters escaped"""
247 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
248 return '"%s"' % s.replace('\\','\\\\') \
249 .replace('"','\\"') \
250 .replace('\n', '\\n"\n"')
252 re_escaped_char = re.compile(r"(\\.)")
253 re_escaped_replacements = {'n': '\n', }
255 def _sub_replacement(match_obj):
256 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
259 """Returns unquoted PO term string, with special PO characters unescaped"""
260 return re_escaped_char.sub(_sub_replacement, str[1:-1])
262 # class to handle po files
263 class TinyPoFile(object):
264 def __init__(self, buffer):
265 self.logger = logging.getLogger('i18n')
268 def warn(self, msg, *args):
269 self.logger.warning(msg, *args)
273 self.lines = self._get_lines()
274 self.lines_count = len(self.lines);
280 def _get_lines(self):
281 lines = self.buffer.readlines()
282 # remove the BOM (Byte Order Mark):
284 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
286 lines.append('') # ensure that the file ends with at least an empty line
290 return (self.lines_count - len(self.lines))
293 type = name = res_id = source = trad = None
296 type, name, res_id, source, trad = self.tnrs.pop(0)
304 if 0 == len(self.lines):
305 raise StopIteration()
306 line = self.lines.pop(0).strip()
307 while line.startswith('#'):
308 if line.startswith('#~ '):
310 if line.startswith('#:'):
311 for lpart in line[2:].strip().split(' '):
312 trans_info = lpart.strip().split(':',2)
313 if trans_info and len(trans_info) == 2:
314 # looks like the translation type is missing, which is not
315 # unexpected because it is not a GetText standard. Default: 'code'
316 trans_info[:0] = ['code']
317 if trans_info and len(trans_info) == 3:
318 tmp_tnrs.append(trans_info)
319 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
321 line = self.lines.pop(0).strip()
323 # allow empty lines between comments and msgid
324 line = self.lines.pop(0).strip()
325 if line.startswith('#~ '):
326 while line.startswith('#~ ') or not line.strip():
327 if 0 == len(self.lines):
328 raise StopIteration()
329 line = self.lines.pop(0)
330 # This has been a deprecated entry, don't return anything
333 if not line.startswith('msgid'):
334 raise Exception("malformed file: bad line: %s" % line)
335 source = unquote(line[6:])
336 line = self.lines.pop(0).strip()
337 if not source and self.first:
338 # if the source is "" and it's the first msgid, it's the special
339 # msgstr with the informations about the traduction and the
340 # traductor; we skip it
343 line = self.lines.pop(0).strip()
346 while not line.startswith('msgstr'):
348 raise Exception('malformed file at %d'% self.cur_line())
349 source += unquote(line)
350 line = self.lines.pop(0).strip()
352 trad = unquote(line[7:])
353 line = self.lines.pop(0).strip()
355 trad += unquote(line)
356 line = self.lines.pop(0).strip()
358 if tmp_tnrs and not fuzzy:
359 type, name, res_id = tmp_tnrs.pop(0)
360 for t, n, r in tmp_tnrs:
361 self.tnrs.append((t, n, r, source, trad))
367 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
368 self.cur_line(), source[:30])
370 return type, name, res_id, source, trad
372 def write_infos(self, modules):
374 self.buffer.write("# Translation of %(project)s.\n" \
375 "# This file contains the translation of the following modules:\n" \
380 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
381 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
382 '''"POT-Creation-Date: %(now)s\\n"\n''' \
383 '''"PO-Revision-Date: %(now)s\\n"\n''' \
384 '''"Last-Translator: <>\\n"\n''' \
385 '''"Language-Team: \\n"\n''' \
386 '''"MIME-Version: 1.0\\n"\n''' \
387 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
388 '''"Content-Transfer-Encoding: \\n"\n''' \
389 '''"Plural-Forms: \\n"\n''' \
392 % { 'project': release.description,
393 'version': release.version,
394 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
395 'bugmail': release.support_email,
396 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
400 def write(self, modules, tnrs, source, trad):
402 plurial = len(modules) > 1 and 's' or ''
403 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
407 for typy, name, res_id in tnrs:
408 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
413 # only strings in python code are python formated
414 self.buffer.write("#, python-format\n")
416 if not isinstance(trad, unicode):
417 trad = unicode(trad, 'utf8')
418 if not isinstance(source, unicode):
419 source = unicode(source, 'utf8')
423 % (quote(source), quote(trad))
424 self.buffer.write(msg.encode('utf8'))
427 # Methods to export the translation file
429 def trans_export(lang, modules, buffer, format, cr):
431 def _process(format, modules, rows, buffer, lang, newlang):
433 writer=csv.writer(buffer, 'UNIX')
438 writer = tools.TinyPoFile(buffer)
439 writer.write_infos(modules)
441 # we now group the translations by source. That means one translation per source.
443 for module, type, name, res_id, src, trad in rows:
444 row = grouped_rows.setdefault(src, {})
445 row.setdefault('modules', set()).add(module)
446 if ('translation' not in row) or (not row['translation']):
447 row['translation'] = trad
448 row.setdefault('tnrs', []).append((type, name, res_id))
450 for src, row in grouped_rows.items():
451 writer.write(row['modules'], row['tnrs'], src, row['translation'])
453 elif format == 'tgz':
458 # first row is the "header", as in csv, it will be popped
459 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
460 rows_by_module[module].append(row)
462 tmpdir = tempfile.mkdtemp()
463 for mod, modrows in rows_by_module.items():
464 tmpmoddir = join(tmpdir, mod, 'i18n')
465 os.makedirs(tmpmoddir)
466 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
467 buf = file(join(tmpmoddir, pofilename), 'w')
468 _process('po', [mod], modrows, buf, lang, newlang)
471 tar = tarfile.open(fileobj=buffer, mode='w|gz')
476 raise Exception(_('Bad file format'))
478 newlang = not bool(lang)
481 trans = trans_generate(lang, modules, cr)
482 if newlang and format!='csv':
485 modules = set([t[0] for t in trans[1:]])
486 _process(format, modules, trans, buffer, lang, newlang)
489 def trans_parse_xsl(de):
494 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
496 l = m.text.strip().replace('\n',' ')
498 res.append(l.encode("utf8"))
499 res.extend(trans_parse_xsl(n))
502 def trans_parse_rml(de):
506 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
508 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
509 for s in string_list:
511 res.append(s.encode("utf8"))
512 res.extend(trans_parse_rml(n))
515 def trans_parse_view(de):
517 if de.tag == 'attribute' and de.get("name") == 'string':
519 res.append(de.text.encode("utf8"))
521 res.append(de.get('string').encode("utf8"))
523 res.append(de.get('sum').encode("utf8"))
524 if de.get("confirm"):
525 res.append(de.get('confirm').encode("utf8"))
527 res.extend(trans_parse_view(n))
530 # tests whether an object is in a list of modules
531 def in_modules(object_name, modules):
540 module = object_name.split('.')[0]
541 module = module_dict.get(module, module)
542 return module in modules
544 def trans_generate(lang, modules, cr):
545 logger = logging.getLogger('i18n')
548 pool = pooler.get_pool(dbname)
549 trans_obj = pool.get('ir.translation')
550 model_data_obj = pool.get('ir.model.data')
552 l = pool.obj_pool.items()
555 query = 'SELECT name, model, res_id, module' \
556 ' FROM ir_model_data'
558 query_models = """SELECT m.id, m.model, imd.module
559 FROM ir_model AS m, ir_model_data AS imd
560 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
562 if 'all_installed' in modules:
563 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
564 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
566 if 'all' not in modules:
567 query += ' WHERE module IN %s'
568 query_models += ' AND imd.module in %s'
569 query_param = (tuple(modules),)
570 query += ' ORDER BY module, model, name'
571 query_models += ' ORDER BY module, model'
573 cr.execute(query, query_param)
576 def push_translation(module, type, name, id, source):
577 tuple = (module, source, name, id, type)
578 if source and tuple not in _to_translate:
579 _to_translate.append(tuple)
582 if isinstance(s, unicode):
583 return s.encode('utf8')
586 for (xml_name,model,res_id,module) in cr.fetchall():
587 module = encode(module)
588 model = encode(model)
589 xml_name = "%s.%s" % (module, encode(xml_name))
591 if not pool.get(model):
592 logger.error("Unable to find object %r", model)
595 exists = pool.get(model).exists(cr, uid, res_id)
597 logger.warning("Unable to find object %r with id %d", model, res_id)
599 obj = pool.get(model).browse(cr, uid, res_id)
601 if model=='ir.ui.view':
602 d = etree.XML(encode(obj.arch))
603 for t in trans_parse_view(d):
604 push_translation(module, 'view', encode(obj.model), 0, t)
605 elif model=='ir.actions.wizard':
606 service_name = 'wizard.'+encode(obj.wiz_name)
607 if netsvc.Service._services.get(service_name):
608 obj2 = netsvc.Service._services[service_name]
609 for state_name, state_def in obj2.states.iteritems():
610 if 'result' in state_def:
611 result = state_def['result']
612 if result['type'] != 'form':
614 name = "%s,%s" % (encode(obj.wiz_name), state_name)
617 'string': ('wizard_field', lambda s: [encode(s)]),
618 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
619 'help': ('help', lambda s: [encode(s)]),
623 if not result.has_key('fields'):
624 logger.warning("res has no fields: %r", result)
626 for field_name, field_def in result['fields'].iteritems():
627 res_name = name + ',' + field_name
629 for fn in def_params:
631 transtype, modifier = def_params[fn]
632 for val in modifier(field_def[fn]):
633 push_translation(module, transtype, res_name, 0, val)
636 arch = result['arch']
637 if arch and not isinstance(arch, UpdateableStr):
639 for t in trans_parse_view(d):
640 push_translation(module, 'wizard_view', name, 0, t)
642 # export button labels
643 for but_args in result['state']:
644 button_name = but_args[0]
645 button_label = but_args[1]
646 res_name = name + ',' + button_name
647 push_translation(module, 'wizard_button', res_name, 0, button_label)
649 elif model=='ir.model.fields':
651 field_name = encode(obj.name)
652 except AttributeError, exc:
653 logger.error("name error in %s: %s", xml_name, str(exc))
655 objmodel = pool.get(obj.model)
656 if not objmodel or not field_name in objmodel._columns:
658 field_def = objmodel._columns[field_name]
660 name = "%s,%s" % (encode(obj.model), field_name)
661 push_translation(module, 'field', name, 0, encode(field_def.string))
664 push_translation(module, 'help', name, 0, encode(field_def.help))
666 if field_def.translate:
667 ids = objmodel.search(cr, uid, [])
668 obj_values = objmodel.read(cr, uid, ids, [field_name])
669 for obj_value in obj_values:
670 res_id = obj_value['id']
671 if obj.name in ('ir.model', 'ir.ui.menu'):
673 model_data_ids = model_data_obj.search(cr, uid, [
674 ('model', '=', model),
675 ('res_id', '=', res_id),
677 if not model_data_ids:
678 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
680 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
681 for dummy, val in field_def.selection:
682 push_translation(module, 'selection', name, 0, encode(val))
684 elif model=='ir.actions.report.xml':
685 name = encode(obj.report_name)
688 fname = obj.report_rml
689 parse_func = trans_parse_rml
690 report_type = "report"
692 fname = obj.report_xsl
693 parse_func = trans_parse_xsl
695 if fname and obj.report_type in ('pdf', 'xsl'):
697 report_file = tools.file_open(fname)
699 d = etree.parse(report_file)
700 for t in parse_func(d.iter()):
701 push_translation(module, report_type, name, 0, t)
704 except (IOError, etree.XMLSyntaxError):
705 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
707 for field_name,field_def in obj._table._columns.items():
708 if field_def.translate:
709 name = model + "," + field_name
711 trad = getattr(obj, field_name) or ''
714 push_translation(module, 'model', name, xml_name, encode(trad))
716 # End of data for ir.model.data query results
718 cr.execute(query_models, query_param)
720 def push_constraint_msg(module, term_type, model, msg):
721 # Check presence of __call__ directly instead of using
722 # callable() because it will be deprecated as of Python 3.0
723 if not hasattr(msg, '__call__'):
724 push_translation(module, term_type, model, 0, encode(msg))
726 for (model_id, model, module) in cr.fetchall():
727 module = encode(module)
728 model = encode(model)
730 model_obj = pool.get(model)
733 logging.getLogger("i18n").error("Unable to find object %r", model)
736 for constraint in getattr(model_obj, '_constraints', []):
737 push_constraint_msg(module, 'constraint', model, constraint[1])
739 for constraint in getattr(model_obj, '_sql_constraints', []):
740 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
742 # parse source code for _() calls
743 def get_module_from_path(path, mod_paths=None):
745 # First, construct a list of possible paths
746 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
747 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
750 mod_paths.append(adp)
751 if not os.path.isabs(adp):
752 mod_paths.append(adp)
753 elif adp.startswith(def_path):
754 mod_paths.append(adp[len(def_path)+1:])
756 if path.startswith(mp) and (os.path.dirname(path) != mp):
757 path = path[len(mp)+1:]
758 return path.split(os.path.sep)[0]
759 return 'base' # files that are not in a module are considered as being in 'base' module
761 modobj = pool.get('ir.module.module')
762 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
763 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
765 root_path = os.path.join(tools.config['root_path'], 'addons')
767 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
768 if root_path in apaths:
771 path_list = [root_path,] + apaths
773 # Also scan these non-addon paths
774 for bin_path in ['osv', 'report' ]:
775 path_list.append(os.path.join(tools.config['root_path'], bin_path))
777 logger.debug("Scanning modules at paths: ", path_list)
780 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
781 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
782 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
783 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
785 def export_code_terms_from_file(fname, path, root, terms_type):
786 fabsolutepath = join(root, fname)
787 frelativepath = fabsolutepath[len(path):]
788 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
789 is_mod_installed = module in installed_modules
790 if (('all' in modules) or (module in modules)) and is_mod_installed:
791 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
792 src_file = tools.file_open(fabsolutepath, subdir='')
794 code_string = src_file.read()
797 if module in installed_modules:
798 frelativepath = str("addons" + frelativepath)
799 ite = re_dquotes.finditer(code_string)
804 if src.startswith('""'):
805 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
808 src = join_dquotes.sub(r'\1', src)
809 # try to count the lines from the last pos to our place:
810 code_line += code_string[code_offset:i.start(1)].count('\n')
811 # now, since we did a binary read of a python source file, we
812 # have to expand pythonic escapes like the interpreter does.
813 src = src.decode('string_escape')
814 push_translation(module, terms_type, frelativepath, code_line, encode(src))
815 code_line += i.group(1).count('\n')
816 code_offset = i.end() # we have counted newlines up to the match end
818 ite = re_quotes.finditer(code_string)
819 code_offset = 0 #reset counters
823 if src.startswith("''"):
824 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
827 src = join_quotes.sub(r'\1', src)
828 code_line += code_string[code_offset:i.start(1)].count('\n')
829 src = src.decode('string_escape')
830 push_translation(module, terms_type, frelativepath, code_line, encode(src))
831 code_line += i.group(1).count('\n')
832 code_offset = i.end() # we have counted newlines up to the match end
834 for path in path_list:
835 logger.debug("Scanning files of modules at %s", path)
836 for root, dummy, files in tools.osutil.walksymlinks(path):
837 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
838 export_code_terms_from_file(fname, path, root, 'code')
839 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
840 export_code_terms_from_file(fname, path, root, 'report')
843 out = [["module","type","name","res_id","src","value"]] # header
845 # translate strings marked as to be translated
846 for module, source, name, id, type in _to_translate:
847 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
848 out.append([module, type, name, id, source, encode(trans) or ''])
852 def trans_load(cr, filename, lang, verbose=True, context=None):
853 logger = logging.getLogger('i18n')
855 fileobj = open(filename,'r')
856 logger.info("loading %s", filename)
857 fileformat = os.path.splitext(filename)[-1][1:].lower()
858 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
863 logger.error("couldn't read translation file %s", filename)
866 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
867 """Populates the ir_translation table. Fixing the res_ids so that they point
868 correctly to ir_model_data is done in a separate step, using the
869 'trans_update_res_ids' function below."""
870 logger = logging.getLogger('i18n')
872 logger.info('loading translation file for language %s', lang)
876 pool = pooler.get_pool(db_name)
877 lang_obj = pool.get('res.lang')
878 trans_obj = pool.get('ir.translation')
879 model_data_obj = pool.get('ir.model.data')
880 iso_lang = tools.get_iso_codes(lang)
883 ids = lang_obj.search(cr, uid, [('code','=', lang)])
886 # lets create the language with locale information
887 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
890 # now, the serious things: we read the language file
892 if fileformat == 'csv':
893 #Setting the limit of data while loading a CSV
894 csv.field_size_limit(sys.maxint)
895 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
896 # read the first line of the file (it contains columns titles)
900 elif fileformat == 'po':
901 reader = TinyPoFile(fileobj)
902 f = ['type', 'name', 'res_id', 'src', 'value']
904 logger.error('Bad file format: %s', fileformat)
905 raise Exception(_('Bad file format'))
907 # read the rest of the file
911 # skip empty rows and rows where the translation field (=last fiefd) is empty
912 #if (not row) or (not row[-1]):
915 # dictionary which holds values for this line of the csv file
916 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
917 # 'src': ..., 'value': ...}
919 for i in range(len(f)):
920 if f[i] in ('module',):
925 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
926 dic['module'] = False
927 dic['xml_id'] = False
929 split_id = dic['res_id'].split('.', 1)
930 dic['module'] = split_id[0]
931 dic['xml_id'] = split_id[1]
932 dic['res_id'] = False
936 ('type', '=', dic['type']),
937 ('name', '=', dic['name']),
939 if dic['type'] == 'model':
940 if dic['res_id'] is False:
941 args.append(('module', '=', dic['module']))
942 args.append(('xml_id', '=', dic['xml_id']))
944 args.append(('res_id', '=', dic['res_id']))
946 args.append(('src', '=', dic['src']))
948 ids = trans_obj.search(cr, uid, args)
950 if context.get('overwrite') and dic['value']:
951 trans_obj.write(cr, uid, ids, {'value': dic['value']})
953 trans_obj.create(cr, uid, dic)
955 logger.info("translation file loaded succesfully")
957 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
958 logger.exception("couldn't read translation file %s", filename)
960 def trans_update_res_ids(cr):
962 UPDATE ir_translation
963 SET res_id = COALESCE ((SELECT ir_model_data.res_id
965 WHERE ir_translation.module = ir_model_data.module
966 AND ir_translation.xml_id = ir_model_data.name), 0)
967 WHERE ir_translation.module is not null
968 AND ir_translation.xml_id is not null
969 AND ir_translation.res_id = 0;
972 def get_locales(lang=None):
974 lang = locale.getdefaultlocale()[0]
977 lang = _LOCALE2WIN32.get(lang, lang)
980 ln = locale._build_localename((lang, enc))
982 nln = locale.normalize(ln)
986 for x in process('utf8'): yield x
988 prefenc = locale.getpreferredencoding()
990 for x in process(prefenc): yield x
994 'iso-8859-1': 'iso8859-15',
996 }.get(prefenc.lower())
998 for x in process(prefenc): yield x
1005 # locale.resetlocale is bugged with some locales.
1006 for ln in get_locales():
1008 return locale.setlocale(locale.LC_ALL, ln)
1009 except locale.Error:
1012 def load_language(cr, lang):
1013 """Loads a translation terms for a language.
1014 Used mainly to automate language loading at db initialization.
1016 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1019 pool = pooler.get_pool(cr.dbname)
1020 language_installer = pool.get('base.language.install')
1022 oid = language_installer.create(cr, uid, {'lang': lang})
1023 language_installer.lang_install(cr, uid, [oid], context=None)
1025 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: