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.text and de.text.strip():
505 res.append(de.text.strip().encode("utf8"))
506 if de.tail and de.tail.strip():
507 res.append(de.tail.strip().encode("utf8"))
508 if de.tag == 'attribute' and de.get("name") == 'string':
510 res.append(de.text.encode("utf8"))
512 res.append(de.get('string').encode("utf8"))
514 res.append(de.get('help').encode("utf8"))
516 res.append(de.get('sum').encode("utf8"))
517 if de.get("confirm"):
518 res.append(de.get('confirm').encode("utf8"))
520 res.extend(trans_parse_view(n))
523 # tests whether an object is in a list of modules
524 def in_modules(object_name, modules):
533 module = object_name.split('.')[0]
534 module = module_dict.get(module, module)
535 return module in modules
537 def trans_generate(lang, modules, cr):
540 pool = pooler.get_pool(dbname)
541 trans_obj = pool.get('ir.translation')
542 model_data_obj = pool.get('ir.model.data')
544 l = pool.models.items()
547 query = 'SELECT name, model, res_id, module' \
548 ' FROM ir_model_data'
550 query_models = """SELECT m.id, m.model, imd.module
551 FROM ir_model AS m, ir_model_data AS imd
552 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
554 if 'all_installed' in modules:
555 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
556 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
558 if 'all' not in modules:
559 query += ' WHERE module IN %s'
560 query_models += ' AND imd.module in %s'
561 query_param = (tuple(modules),)
562 query += ' ORDER BY module, model, name'
563 query_models += ' ORDER BY module, model'
565 cr.execute(query, query_param)
568 def push_translation(module, type, name, id, source):
569 tuple = (module, source, name, id, type)
570 if source and tuple not in _to_translate:
571 _to_translate.append(tuple)
574 if isinstance(s, unicode):
575 return s.encode('utf8')
578 for (xml_name,model,res_id,module) in cr.fetchall():
579 module = encode(module)
580 model = encode(model)
581 xml_name = "%s.%s" % (module, encode(xml_name))
583 if not pool.get(model):
584 _logger.error("Unable to find object %r", model)
587 exists = pool.get(model).exists(cr, uid, res_id)
589 _logger.warning("Unable to find object %r with id %d", model, res_id)
591 obj = pool.get(model).browse(cr, uid, res_id)
593 if model=='ir.ui.view':
594 d = etree.XML(encode(obj.arch))
595 for t in trans_parse_view(d):
596 push_translation(module, 'view', encode(obj.model), 0, t)
597 elif model=='ir.actions.wizard':
598 service_name = 'wizard.'+encode(obj.wiz_name)
599 import openerp.netsvc as netsvc
600 if netsvc.Service._services.get(service_name):
601 obj2 = netsvc.Service._services[service_name]
602 for state_name, state_def in obj2.states.iteritems():
603 if 'result' in state_def:
604 result = state_def['result']
605 if result['type'] != 'form':
607 name = "%s,%s" % (encode(obj.wiz_name), state_name)
610 'string': ('wizard_field', lambda s: [encode(s)]),
611 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
612 'help': ('help', lambda s: [encode(s)]),
616 if not result.has_key('fields'):
617 _logger.warning("res has no fields: %r", result)
619 for field_name, field_def in result['fields'].iteritems():
620 res_name = name + ',' + field_name
622 for fn in def_params:
624 transtype, modifier = def_params[fn]
625 for val in modifier(field_def[fn]):
626 push_translation(module, transtype, res_name, 0, val)
629 arch = result['arch']
630 if arch and not isinstance(arch, UpdateableStr):
632 for t in trans_parse_view(d):
633 push_translation(module, 'wizard_view', name, 0, t)
635 # export button labels
636 for but_args in result['state']:
637 button_name = but_args[0]
638 button_label = but_args[1]
639 res_name = name + ',' + button_name
640 push_translation(module, 'wizard_button', res_name, 0, button_label)
642 elif model=='ir.model.fields':
644 field_name = encode(obj.name)
645 except AttributeError, exc:
646 _logger.error("name error in %s: %s", xml_name, str(exc))
648 objmodel = pool.get(obj.model)
649 if not objmodel or not field_name in objmodel._columns:
651 field_def = objmodel._columns[field_name]
653 name = "%s,%s" % (encode(obj.model), field_name)
654 push_translation(module, 'field', name, 0, encode(field_def.string))
657 push_translation(module, 'help', name, 0, encode(field_def.help))
659 if field_def.translate:
660 ids = objmodel.search(cr, uid, [])
661 obj_values = objmodel.read(cr, uid, ids, [field_name])
662 for obj_value in obj_values:
663 res_id = obj_value['id']
664 if obj.name in ('ir.model', 'ir.ui.menu'):
666 model_data_ids = model_data_obj.search(cr, uid, [
667 ('model', '=', model),
668 ('res_id', '=', res_id),
670 if not model_data_ids:
671 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
673 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
674 for dummy, val in field_def.selection:
675 push_translation(module, 'selection', name, 0, encode(val))
677 elif model=='ir.actions.report.xml':
678 name = encode(obj.report_name)
681 fname = obj.report_rml
682 parse_func = trans_parse_rml
683 report_type = "report"
685 fname = obj.report_xsl
686 parse_func = trans_parse_xsl
688 if fname and obj.report_type in ('pdf', 'xsl'):
690 report_file = misc.file_open(fname)
692 d = etree.parse(report_file)
693 for t in parse_func(d.iter()):
694 push_translation(module, report_type, name, 0, t)
697 except (IOError, etree.XMLSyntaxError):
698 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
700 for field_name,field_def in obj._table._columns.items():
701 if field_def.translate:
702 name = model + "," + field_name
704 trad = getattr(obj, field_name) or ''
707 push_translation(module, 'model', name, xml_name, encode(trad))
709 # End of data for ir.model.data query results
711 cr.execute(query_models, query_param)
713 def push_constraint_msg(module, term_type, model, msg):
714 # Check presence of __call__ directly instead of using
715 # callable() because it will be deprecated as of Python 3.0
716 if not hasattr(msg, '__call__'):
717 push_translation(module, term_type, model, 0, encode(msg))
719 for (model_id, model, module) in cr.fetchall():
720 module = encode(module)
721 model = encode(model)
723 model_obj = pool.get(model)
726 _logger.error("Unable to find object %r", model)
729 for constraint in getattr(model_obj, '_constraints', []):
730 push_constraint_msg(module, 'constraint', model, constraint[1])
732 for constraint in getattr(model_obj, '_sql_constraints', []):
733 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
735 # parse source code for _() calls
736 def get_module_from_path(path, mod_paths=None):
738 # First, construct a list of possible paths
739 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
740 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
743 mod_paths.append(adp)
744 if not os.path.isabs(adp):
745 mod_paths.append(adp)
746 elif adp.startswith(def_path):
747 mod_paths.append(adp[len(def_path)+1:])
749 if path.startswith(mp) and (os.path.dirname(path) != mp):
750 path = path[len(mp)+1:]
751 return path.split(os.path.sep)[0]
752 return 'base' # files that are not in a module are considered as being in 'base' module
754 modobj = pool.get('ir.module.module')
755 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
756 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
758 root_path = os.path.join(config.config['root_path'], 'addons')
760 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
761 if root_path in apaths:
764 path_list = [root_path,] + apaths
766 # Also scan these non-addon paths
767 for bin_path in ['osv', 'report' ]:
768 path_list.append(os.path.join(config.config['root_path'], bin_path))
770 _logger.debug("Scanning modules at paths: ", path_list)
773 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
774 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
775 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
776 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
778 def export_code_terms_from_file(fname, path, root, terms_type):
779 fabsolutepath = join(root, fname)
780 frelativepath = fabsolutepath[len(path):]
781 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
782 is_mod_installed = module in installed_modules
783 if (('all' in modules) or (module in modules)) and is_mod_installed:
784 _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
785 src_file = misc.file_open(fabsolutepath, subdir='')
787 code_string = src_file.read()
790 if module in installed_modules:
791 frelativepath = str("addons" + frelativepath)
792 ite = re_dquotes.finditer(code_string)
797 if src.startswith('""'):
798 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
801 src = join_dquotes.sub(r'\1', src)
802 # try to count the lines from the last pos to our place:
803 code_line += code_string[code_offset:i.start(1)].count('\n')
804 # now, since we did a binary read of a python source file, we
805 # have to expand pythonic escapes like the interpreter does.
806 src = src.decode('string_escape')
807 push_translation(module, terms_type, frelativepath, code_line, encode(src))
808 code_line += i.group(1).count('\n')
809 code_offset = i.end() # we have counted newlines up to the match end
811 ite = re_quotes.finditer(code_string)
812 code_offset = 0 #reset counters
816 if src.startswith("''"):
817 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
820 src = join_quotes.sub(r'\1', src)
821 code_line += code_string[code_offset:i.start(1)].count('\n')
822 src = src.decode('string_escape')
823 push_translation(module, terms_type, frelativepath, code_line, encode(src))
824 code_line += i.group(1).count('\n')
825 code_offset = i.end() # we have counted newlines up to the match end
827 for path in path_list:
828 _logger.debug("Scanning files of modules at %s", path)
829 for root, dummy, files in osutil.walksymlinks(path):
830 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
831 export_code_terms_from_file(fname, path, root, 'code')
832 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
833 export_code_terms_from_file(fname, path, root, 'report')
836 out = [["module","type","name","res_id","src","value"]] # header
838 # translate strings marked as to be translated
839 for module, source, name, id, type in _to_translate:
840 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
841 out.append([module, type, name, id, source, encode(trans) or ''])
845 def trans_load(cr, filename, lang, verbose=True, context=None):
847 fileobj = misc.file_open(filename)
848 _logger.info("loading %s", filename)
849 fileformat = os.path.splitext(filename)[-1][1:].lower()
850 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
855 _logger.error("couldn't read translation file %s", filename)
858 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
859 """Populates the ir_translation table."""
861 _logger.info('loading translation file for language %s', lang)
865 pool = pooler.get_pool(db_name)
866 lang_obj = pool.get('res.lang')
867 trans_obj = pool.get('ir.translation')
868 iso_lang = misc.get_iso_codes(lang)
871 ids = lang_obj.search(cr, uid, [('code','=', lang)])
874 # lets create the language with locale information
875 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
878 # now, the serious things: we read the language file
880 if fileformat == 'csv':
881 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
882 # read the first line of the file (it contains columns titles)
886 elif fileformat == 'po':
887 reader = TinyPoFile(fileobj)
888 f = ['type', 'name', 'res_id', 'src', 'value']
890 _logger.error('Bad file format: %s', fileformat)
891 raise Exception(_('Bad file format'))
893 # read the rest of the file
895 irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
899 # skip empty rows and rows where the translation field (=last fiefd) is empty
900 #if (not row) or (not row[-1]):
903 # dictionary which holds values for this line of the csv file
904 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
905 # 'src': ..., 'value': ...}
908 for i in range(len(f)):
909 if f[i] in ('module',):
913 # This would skip terms that fail to specify a res_id
914 if not dic.get('res_id', False):
917 res_id = dic.pop('res_id')
918 if res_id and isinstance(res_id, (int, long)) \
919 or (isinstance(res_id, basestring) and res_id.isdigit()):
920 dic['res_id'] = int(res_id)
923 tmodel = dic['name'].split(',')[0]
925 tmodule, tname = res_id.split('.', 1)
929 dic['imd_model'] = tmodel
930 dic['imd_module'] = tmodule
931 dic['imd_name'] = tname
935 _logger.warning("Could not decode resource for %s, please fix the po file.",
936 dic['res_id'], exc_info=True)
943 _logger.info("translation file loaded succesfully")
945 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
946 _logger.exception("couldn't read translation file %s", filename)
948 def get_locales(lang=None):
950 lang = locale.getdefaultlocale()[0]
953 lang = _LOCALE2WIN32.get(lang, lang)
956 ln = locale._build_localename((lang, enc))
958 nln = locale.normalize(ln)
962 for x in process('utf8'): yield x
964 prefenc = locale.getpreferredencoding()
966 for x in process(prefenc): yield x
970 'iso-8859-1': 'iso8859-15',
972 }.get(prefenc.lower())
974 for x in process(prefenc): yield x
981 # locale.resetlocale is bugged with some locales.
982 for ln in get_locales():
984 return locale.setlocale(locale.LC_ALL, ln)
988 def load_language(cr, lang):
989 """Loads a translation terms for a language.
990 Used mainly to automate language loading at db initialization.
992 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
995 pool = pooler.get_pool(cr.dbname)
996 language_installer = pool.get('base.language.install')
998 oid = language_installer.create(cr, uid, {'lang': lang})
999 language_installer.lang_install(cr, uid, [oid], context=None)
1001 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: