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
46 from openerp import SUPERUSER_ID
48 _logger = logging.getLogger(__name__)
51 'af_ZA': 'Afrikaans_South Africa',
52 'sq_AL': 'Albanian_Albania',
53 'ar_SA': 'Arabic_Saudi Arabia',
54 'eu_ES': 'Basque_Spain',
55 'be_BY': 'Belarusian_Belarus',
56 'bs_BA': 'Serbian (Latin)',
57 'bg_BG': 'Bulgarian_Bulgaria',
58 'ca_ES': 'Catalan_Spain',
59 'hr_HR': 'Croatian_Croatia',
60 'zh_CN': 'Chinese_China',
61 'zh_TW': 'Chinese_Taiwan',
62 'cs_CZ': 'Czech_Czech Republic',
63 'da_DK': 'Danish_Denmark',
64 'nl_NL': 'Dutch_Netherlands',
65 'et_EE': 'Estonian_Estonia',
66 'fa_IR': 'Farsi_Iran',
67 'ph_PH': 'Filipino_Philippines',
68 'fi_FI': 'Finnish_Finland',
69 'fr_FR': 'French_France',
70 'fr_BE': 'French_France',
71 'fr_CH': 'French_France',
72 'fr_CA': 'French_France',
73 'ga': 'Scottish Gaelic',
74 'gl_ES': 'Galician_Spain',
75 'ka_GE': 'Georgian_Georgia',
76 'de_DE': 'German_Germany',
77 'el_GR': 'Greek_Greece',
78 'gu': 'Gujarati_India',
79 'he_IL': 'Hebrew_Israel',
81 'hu': 'Hungarian_Hungary',
82 'is_IS': 'Icelandic_Iceland',
83 'id_ID': 'Indonesian_indonesia',
84 'it_IT': 'Italian_Italy',
85 'ja_JP': 'Japanese_Japan',
88 'ko_KR': 'Korean_Korea',
90 'lt_LT': 'Lithuanian_Lithuania',
91 'lat': 'Latvian_Latvia',
92 'ml_IN': 'Malayalam_India',
93 'id_ID': 'Indonesian_indonesia',
95 'mn': 'Cyrillic_Mongolian',
96 'no_NO': 'Norwegian_Norway',
97 'nn_NO': 'Norwegian-Nynorsk_Norway',
98 'pl': 'Polish_Poland',
99 'pt_PT': 'Portuguese_Portugal',
100 'pt_BR': 'Portuguese_Brazil',
101 'ro_RO': 'Romanian_Romania',
102 'ru_RU': 'Russian_Russia',
104 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
105 'sk_SK': 'Slovak_Slovakia',
106 'sl_SI': 'Slovenian_Slovenia',
107 #should find more specific locales for spanish countries,
108 #but better than nothing
109 'es_AR': 'Spanish_Spain',
110 'es_BO': 'Spanish_Spain',
111 'es_CL': 'Spanish_Spain',
112 'es_CO': 'Spanish_Spain',
113 'es_CR': 'Spanish_Spain',
114 'es_DO': 'Spanish_Spain',
115 'es_EC': 'Spanish_Spain',
116 'es_ES': 'Spanish_Spain',
117 'es_GT': 'Spanish_Spain',
118 'es_HN': 'Spanish_Spain',
119 'es_MX': 'Spanish_Spain',
120 'es_NI': 'Spanish_Spain',
121 'es_PA': 'Spanish_Spain',
122 'es_PE': 'Spanish_Spain',
123 'es_PR': 'Spanish_Spain',
124 'es_PY': 'Spanish_Spain',
125 'es_SV': 'Spanish_Spain',
126 'es_UY': 'Spanish_Spain',
127 'es_VE': 'Spanish_Spain',
128 'sv_SE': 'Swedish_Sweden',
129 'ta_IN': 'English_Australia',
130 'th_TH': 'Thai_Thailand',
132 'tr_TR': 'Turkish_Turkey',
133 'uk_UA': 'Ukrainian_Ukraine',
134 'vi_VN': 'Vietnamese_Viet Nam',
135 'tlh_TLH': 'Klingon',
140 class UNIX_LINE_TERMINATOR(csv.excel):
141 lineterminator = '\n'
143 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
146 # Warning: better use self.pool.get('ir.translation')._get_source if you can
148 def translate(cr, name, source_type, lang, source=None):
150 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))
152 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
154 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
155 res_trans = cr.fetchone()
156 res = res_trans and res_trans[0] or False
159 class GettextAlias(object):
162 # find current DB based on thread/worker db name (see netsvc)
163 db_name = getattr(threading.currentThread(), 'dbname', None)
165 return sql_db.db_connect(db_name)
167 def _get_cr(self, frame):
169 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
171 s = frame.f_locals.get('self', {})
172 cr = getattr(s, 'cr', None)
180 def _get_lang(self, frame):
182 ctx = frame.f_locals.get('context')
184 kwargs = frame.f_locals.get('kwargs')
186 args = frame.f_locals.get('args')
187 if args and isinstance(args, (list, tuple)) \
188 and isinstance(args[-1], dict):
190 elif isinstance(kwargs, dict):
191 ctx = kwargs.get('context')
193 lang = ctx.get('lang')
195 s = frame.f_locals.get('self', {})
196 c = getattr(s, 'localcontext', None)
201 def __call__(self, source):
206 frame = inspect.currentframe()
212 lang = self._get_lang(frame)
214 cr, is_new_cr = self._get_cr(frame)
216 # Try to use ir.translation to benefit from global cache if possible
217 pool = pooler.get_pool(cr.dbname)
218 res = pool.get('ir.translation')._get_source(cr, SUPERUSER_ID, None, ('code','sql_constraint'), lang, source)
220 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
222 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
224 _logger.debug('translation went wrong for "%r", skipped', source)
225 # if so, double-check the root/base translations filenames
235 """Returns quoted PO term string, with special PO characters escaped"""
236 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
237 return '"%s"' % s.replace('\\','\\\\') \
238 .replace('"','\\"') \
239 .replace('\n', '\\n"\n"')
241 re_escaped_char = re.compile(r"(\\.)")
242 re_escaped_replacements = {'n': '\n', }
244 def _sub_replacement(match_obj):
245 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
248 """Returns unquoted PO term string, with special PO characters unescaped"""
249 return re_escaped_char.sub(_sub_replacement, str[1:-1])
251 # class to handle po files
252 class TinyPoFile(object):
253 def __init__(self, buffer):
256 def warn(self, msg, *args):
257 _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 trans_type = name = res_id = source = trad = None
283 trans_type, name, res_id, source, trad, comments = self.extra_lines.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 line = line[2:].strip()
300 if not line.startswith('module:'):
301 comments.append(line)
302 elif line.startswith('#:'):
303 for lpart in line[2:].strip().split(' '):
304 trans_info = lpart.strip().split(':',2)
305 if trans_info and len(trans_info) == 2:
306 # looks like the translation trans_type is missing, which is not
307 # unexpected because it is not a GetText standard. Default: 'code'
308 trans_info[:0] = ['code']
309 if trans_info and len(trans_info) == 3:
310 # this is a ref line holding the destination info (model, field, record)
311 targets.append(trans_info)
312 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
314 line = self.lines.pop(0).strip()
316 # allow empty lines between comments and msgid
317 line = self.lines.pop(0).strip()
318 if line.startswith('#~ '):
319 while line.startswith('#~ ') or not line.strip():
320 if 0 == len(self.lines):
321 raise StopIteration()
322 line = self.lines.pop(0)
323 # This has been a deprecated entry, don't return anything
326 if not line.startswith('msgid'):
327 raise Exception("malformed file: bad line: %s" % line)
328 source = unquote(line[6:])
329 line = self.lines.pop(0).strip()
330 if not source and self.first:
331 # if the source is "" and it's the first msgid, it's the special
332 # msgstr with the informations about the traduction and the
333 # traductor; we skip it
334 self.extra_lines = []
336 line = self.lines.pop(0).strip()
339 while not line.startswith('msgstr'):
341 raise Exception('malformed file at %d'% self.cur_line())
342 source += unquote(line)
343 line = self.lines.pop(0).strip()
345 trad = unquote(line[7:])
346 line = self.lines.pop(0).strip()
348 trad += unquote(line)
349 line = self.lines.pop(0).strip()
351 if targets and not fuzzy:
352 trans_type, name, res_id = targets.pop(0)
353 for t, n, r in targets:
354 if t == trans_type == 'code': continue
355 self.extra_lines.append((t, n, r, source, trad, comments))
361 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
362 self.cur_line(), source[:30])
364 return trans_type, name, res_id, source, trad, '\n'.join(comments)
366 def write_infos(self, modules):
367 import openerp.release as release
368 self.buffer.write("# Translation of %(project)s.\n" \
369 "# This file contains the translation of the following modules:\n" \
374 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
375 '''"Report-Msgid-Bugs-To: \\n"\n''' \
376 '''"POT-Creation-Date: %(now)s\\n"\n''' \
377 '''"PO-Revision-Date: %(now)s\\n"\n''' \
378 '''"Last-Translator: <>\\n"\n''' \
379 '''"Language-Team: \\n"\n''' \
380 '''"MIME-Version: 1.0\\n"\n''' \
381 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
382 '''"Content-Transfer-Encoding: \\n"\n''' \
383 '''"Plural-Forms: \\n"\n''' \
386 % { 'project': release.description,
387 'version': release.version,
388 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
389 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
393 def write(self, modules, tnrs, source, trad):
395 plurial = len(modules) > 1 and 's' or ''
396 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
400 for typy, name, res_id in tnrs:
401 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
406 # only strings in python code are python formated
407 self.buffer.write("#, python-format\n")
409 if not isinstance(trad, unicode):
410 trad = unicode(trad, 'utf8')
411 if not isinstance(source, unicode):
412 source = unicode(source, 'utf8')
416 % (quote(source), quote(trad))
417 self.buffer.write(msg.encode('utf8'))
420 # Methods to export the translation file
422 def trans_export(lang, modules, buffer, format, cr):
424 def _process(format, modules, rows, buffer, lang, newlang):
426 writer=csv.writer(buffer, 'UNIX')
431 writer = TinyPoFile(buffer)
432 writer.write_infos(modules)
434 # we now group the translations by source. That means one translation per source.
436 for module, type, name, res_id, src, trad in rows:
437 row = grouped_rows.setdefault(src, {})
438 row.setdefault('modules', set()).add(module)
439 if ('translation' not in row) or (not row['translation']):
440 row['translation'] = trad
441 row.setdefault('tnrs', []).append((type, name, res_id))
443 for src, row in grouped_rows.items():
444 writer.write(row['modules'], row['tnrs'], src, row['translation'])
446 elif format == 'tgz':
451 # first row is the "header", as in csv, it will be popped
452 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
453 rows_by_module[module].append(row)
455 tmpdir = tempfile.mkdtemp()
456 for mod, modrows in rows_by_module.items():
457 tmpmoddir = join(tmpdir, mod, 'i18n')
458 os.makedirs(tmpmoddir)
459 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
460 buf = file(join(tmpmoddir, pofilename), 'w')
461 _process('po', [mod], modrows, buf, lang, newlang)
464 tar = tarfile.open(fileobj=buffer, mode='w|gz')
469 raise Exception(_('Unrecognized extension: must be one of '
470 '.csv, .po, or .tgz (received .%s).' % format))
472 newlang = not bool(lang)
475 trans = trans_generate(lang, modules, cr)
476 if newlang and format!='csv':
479 modules = set([t[0] for t in trans[1:]])
480 _process(format, modules, trans, buffer, lang, newlang)
483 def trans_parse_xsl(de):
488 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
490 l = m.text.strip().replace('\n',' ')
492 res.append(l.encode("utf8"))
493 res.extend(trans_parse_xsl(n))
496 def trans_parse_rml(de):
500 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
502 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
503 for s in string_list:
505 res.append(s.encode("utf8"))
506 res.extend(trans_parse_rml(n))
509 def trans_parse_view(de):
511 if de.text and de.text.strip():
512 res.append(de.text.strip().encode("utf8"))
513 if de.tail and de.tail.strip():
514 res.append(de.tail.strip().encode("utf8"))
515 if de.tag == 'attribute' and de.get("name") == 'string':
517 res.append(de.text.encode("utf8"))
519 res.append(de.get('string').encode("utf8"))
521 res.append(de.get('help').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):
547 pool = pooler.get_pool(dbname)
548 trans_obj = pool.get('ir.translation')
549 model_data_obj = pool.get('ir.model.data')
551 l = pool.models.items()
554 query = 'SELECT name, model, res_id, module' \
555 ' FROM ir_model_data'
557 query_models = """SELECT m.id, m.model, imd.module
558 FROM ir_model AS m, ir_model_data AS imd
559 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
561 if 'all_installed' in modules:
562 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
563 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
565 if 'all' not in modules:
566 query += ' WHERE module IN %s'
567 query_models += ' AND imd.module in %s'
568 query_param = (tuple(modules),)
569 query += ' ORDER BY module, model, name'
570 query_models += ' ORDER BY module, model'
572 cr.execute(query, query_param)
575 def push_translation(module, type, name, id, source):
576 tuple = (module, source, name, id, type)
577 if source and tuple not in _to_translate:
578 _to_translate.append(tuple)
581 if isinstance(s, unicode):
582 return s.encode('utf8')
585 for (xml_name,model,res_id,module) in cr.fetchall():
586 module = encode(module)
587 model = encode(model)
588 xml_name = "%s.%s" % (module, encode(xml_name))
590 if not pool.get(model):
591 _logger.error("Unable to find object %r", model)
594 exists = pool.get(model).exists(cr, uid, res_id)
596 _logger.warning("Unable to find object %r with id %d", model, res_id)
598 obj = pool.get(model).browse(cr, uid, res_id)
600 if model=='ir.ui.view':
601 d = etree.XML(encode(obj.arch))
602 for t in trans_parse_view(d):
603 push_translation(module, 'view', encode(obj.model), 0, t)
604 elif model=='ir.actions.wizard':
605 service_name = 'wizard.'+encode(obj.wiz_name)
606 import openerp.netsvc as netsvc
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 = misc.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 _logger.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(config.config['root_path'], 'addons')) # default addons path (base)
747 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.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(config.config['root_path'], 'addons')
767 apaths = map(os.path.abspath, map(str.strip, config.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(config.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 = misc.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 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, module_name=None, context=None):
854 fileobj = misc.file_open(filename)
855 _logger.info("loading %s", filename)
856 fileformat = os.path.splitext(filename)[-1][1:].lower()
857 result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
862 _logger.error("couldn't read translation file %s", filename)
865 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
866 """Populates the ir_translation table."""
868 _logger.info('loading translation file for language %s', lang)
872 pool = pooler.get_pool(db_name)
873 lang_obj = pool.get('res.lang')
874 trans_obj = pool.get('ir.translation')
875 iso_lang = misc.get_iso_codes(lang)
877 ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)])
880 # lets create the language with locale information
881 lang_obj.load_lang(cr, SUPERUSER_ID, lang=lang, lang_name=lang_name)
884 # now, the serious things: we read the language file
886 if fileformat == 'csv':
887 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
888 # read the first line of the file (it contains columns titles)
892 elif fileformat == 'po':
893 reader = TinyPoFile(fileobj)
894 f = ['type', 'name', 'res_id', 'src', 'value', 'comments']
896 _logger.error('Bad file format: %s', fileformat)
897 raise Exception(_('Bad file format'))
899 # read the rest of the file
901 irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context)
905 # skip empty rows and rows where the translation field (=last fiefd) is empty
906 #if (not row) or (not row[-1]):
909 # dictionary which holds values for this line of the csv file
910 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
911 # 'src': ..., 'value': ..., 'module':...}
912 dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments'))
914 for i, field in enumerate(f):
917 # This would skip terms that fail to specify a res_id
918 if not dic.get('res_id'):
921 res_id = dic.pop('res_id')
922 if res_id and isinstance(res_id, (int, long)) \
923 or (isinstance(res_id, basestring) and res_id.isdigit()):
924 dic['res_id'] = int(res_id)
925 dic['module'] = module_name
927 tmodel = dic['name'].split(',')[0]
929 tmodule, tname = res_id.split('.', 1)
933 dic['imd_model'] = tmodel
934 dic['imd_name'] = tname
935 dic['module'] = tmodule
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 get_locales(lang=None):
949 lang = locale.getdefaultlocale()[0]
952 lang = _LOCALE2WIN32.get(lang, lang)
955 ln = locale._build_localename((lang, enc))
957 nln = locale.normalize(ln)
961 for x in process('utf8'): yield x
963 prefenc = locale.getpreferredencoding()
965 for x in process(prefenc): yield x
969 'iso-8859-1': 'iso8859-15',
971 }.get(prefenc.lower())
973 for x in process(prefenc): yield x
980 # locale.resetlocale is bugged with some locales.
981 for ln in get_locales():
983 return locale.setlocale(locale.LC_ALL, ln)
987 def load_language(cr, lang):
988 """Loads a translation terms for a language.
989 Used mainly to automate language loading at db initialization.
991 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
994 pool = pooler.get_pool(cr.dbname)
995 language_installer = pool.get('base.language.install')
997 oid = language_installer.create(cr, uid, {'lang': lang})
998 language_installer.lang_install(cr, uid, [oid], context=None)
1000 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: