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
48 'af_ZA': 'Afrikaans_South Africa',
49 'sq_AL': 'Albanian_Albania',
50 'ar_SA': 'Arabic_Saudi Arabia',
51 'eu_ES': 'Basque_Spain',
52 'be_BY': 'Belarusian_Belarus',
53 'bs_BA': 'Serbian (Latin)',
54 'bg_BG': 'Bulgarian_Bulgaria',
55 'ca_ES': 'Catalan_Spain',
56 'hr_HR': 'Croatian_Croatia',
57 'zh_CN': 'Chinese_China',
58 'zh_TW': 'Chinese_Taiwan',
59 'cs_CZ': 'Czech_Czech Republic',
60 'da_DK': 'Danish_Denmark',
61 'nl_NL': 'Dutch_Netherlands',
62 'et_EE': 'Estonian_Estonia',
63 'fa_IR': 'Farsi_Iran',
64 'ph_PH': 'Filipino_Philippines',
65 'fi_FI': 'Finnish_Finland',
66 'fr_FR': 'French_France',
67 'fr_BE': 'French_France',
68 'fr_CH': 'French_France',
69 'fr_CA': 'French_France',
70 'ga': 'Scottish Gaelic',
71 'gl_ES': 'Galician_Spain',
72 'ka_GE': 'Georgian_Georgia',
73 'de_DE': 'German_Germany',
74 'el_GR': 'Greek_Greece',
75 'gu': 'Gujarati_India',
76 'he_IL': 'Hebrew_Israel',
78 'hu': 'Hungarian_Hungary',
79 'is_IS': 'Icelandic_Iceland',
80 'id_ID': 'Indonesian_indonesia',
81 'it_IT': 'Italian_Italy',
82 'ja_JP': 'Japanese_Japan',
85 'ko_KR': 'Korean_Korea',
87 'lt_LT': 'Lithuanian_Lithuania',
88 'lat': 'Latvian_Latvia',
89 'ml_IN': 'Malayalam_India',
90 'id_ID': 'Indonesian_indonesia',
92 'mn': 'Cyrillic_Mongolian',
93 'no_NO': 'Norwegian_Norway',
94 'nn_NO': 'Norwegian-Nynorsk_Norway',
95 'pl': 'Polish_Poland',
96 'pt_PT': 'Portuguese_Portugal',
97 'pt_BR': 'Portuguese_Brazil',
98 'ro_RO': 'Romanian_Romania',
99 'ru_RU': 'Russian_Russia',
101 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
102 'sk_SK': 'Slovak_Slovakia',
103 'sl_SI': 'Slovenian_Slovenia',
104 #should find more specific locales for spanish countries,
105 #but better than nothing
106 'es_AR': 'Spanish_Spain',
107 'es_BO': 'Spanish_Spain',
108 'es_CL': 'Spanish_Spain',
109 'es_CO': 'Spanish_Spain',
110 'es_CR': 'Spanish_Spain',
111 'es_DO': 'Spanish_Spain',
112 'es_EC': 'Spanish_Spain',
113 'es_ES': 'Spanish_Spain',
114 'es_GT': 'Spanish_Spain',
115 'es_HN': 'Spanish_Spain',
116 'es_MX': 'Spanish_Spain',
117 'es_NI': 'Spanish_Spain',
118 'es_PA': 'Spanish_Spain',
119 'es_PE': 'Spanish_Spain',
120 'es_PR': 'Spanish_Spain',
121 'es_PY': 'Spanish_Spain',
122 'es_SV': 'Spanish_Spain',
123 'es_UY': 'Spanish_Spain',
124 'es_VE': 'Spanish_Spain',
125 'sv_SE': 'Swedish_Sweden',
126 'ta_IN': 'English_Australia',
127 'th_TH': 'Thai_Thailand',
129 'tr_TR': 'Turkish_Turkey',
130 'uk_UA': 'Ukrainian_Ukraine',
131 'vi_VN': 'Vietnamese_Viet Nam',
132 'tlh_TLH': 'Klingon',
137 class UNIX_LINE_TERMINATOR(csv.excel):
138 lineterminator = '\n'
140 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
143 # Warning: better use self.pool.get('ir.translation')._get_source if you can
145 def translate(cr, name, source_type, lang, source=None):
147 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))
149 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
151 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
152 res_trans = cr.fetchone()
153 res = res_trans and res_trans[0] or False
156 logger = logging.getLogger('translate')
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):
253 self.logger = logging.getLogger('i18n')
256 def warn(self, msg, *args):
257 self.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 type = name = res_id = source = trad = None
284 type, name, res_id, source, trad = self.tnrs.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 for lpart in line[2:].strip().split(' '):
300 trans_info = lpart.strip().split(':',2)
301 if trans_info and len(trans_info) == 2:
302 # looks like the translation type is missing, which is not
303 # unexpected because it is not a GetText standard. Default: 'code'
304 trans_info[:0] = ['code']
305 if trans_info and len(trans_info) == 3:
306 tmp_tnrs.append(trans_info)
307 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
309 line = self.lines.pop(0).strip()
311 # allow empty lines between comments and msgid
312 line = self.lines.pop(0).strip()
313 if line.startswith('#~ '):
314 while line.startswith('#~ ') or not line.strip():
315 if 0 == len(self.lines):
316 raise StopIteration()
317 line = self.lines.pop(0)
318 # This has been a deprecated entry, don't return anything
321 if not line.startswith('msgid'):
322 raise Exception("malformed file: bad line: %s" % line)
323 source = unquote(line[6:])
324 line = self.lines.pop(0).strip()
325 if not source and self.first:
326 # if the source is "" and it's the first msgid, it's the special
327 # msgstr with the informations about the traduction and the
328 # traductor; we skip it
331 line = self.lines.pop(0).strip()
334 while not line.startswith('msgstr'):
336 raise Exception('malformed file at %d'% self.cur_line())
337 source += unquote(line)
338 line = self.lines.pop(0).strip()
340 trad = unquote(line[7:])
341 line = self.lines.pop(0).strip()
343 trad += unquote(line)
344 line = self.lines.pop(0).strip()
346 if tmp_tnrs and not fuzzy:
347 type, name, res_id = tmp_tnrs.pop(0)
348 for t, n, r in tmp_tnrs:
349 self.tnrs.append((t, n, r, source, trad))
355 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
356 self.cur_line(), source[:30])
358 return type, name, res_id, source, trad
360 def write_infos(self, modules):
361 import openerp.release as release
362 self.buffer.write("# Translation of %(project)s.\n" \
363 "# This file contains the translation of the following modules:\n" \
368 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
369 '''"Report-Msgid-Bugs-To: \\n"\n''' \
370 '''"POT-Creation-Date: %(now)s\\n"\n''' \
371 '''"PO-Revision-Date: %(now)s\\n"\n''' \
372 '''"Last-Translator: <>\\n"\n''' \
373 '''"Language-Team: \\n"\n''' \
374 '''"MIME-Version: 1.0\\n"\n''' \
375 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
376 '''"Content-Transfer-Encoding: \\n"\n''' \
377 '''"Plural-Forms: \\n"\n''' \
380 % { 'project': release.description,
381 'version': release.version,
382 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
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 = 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(_('Unrecognized extension: must be one of '
464 '.csv, .po, or .tgz (received .%s).' % format))
466 newlang = not bool(lang)
469 trans = trans_generate(lang, modules, cr)
470 if newlang and format!='csv':
473 modules = set([t[0] for t in trans[1:]])
474 _process(format, modules, trans, buffer, lang, newlang)
477 def trans_parse_xsl(de):
482 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
484 l = m.text.strip().replace('\n',' ')
486 res.append(l.encode("utf8"))
487 res.extend(trans_parse_xsl(n))
490 def trans_parse_rml(de):
494 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
496 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
497 for s in string_list:
499 res.append(s.encode("utf8"))
500 res.extend(trans_parse_rml(n))
503 def trans_parse_view(de):
505 if de.tag == 'attribute' and de.get("name") == 'string':
507 res.append(de.text.encode("utf8"))
509 res.append(de.get('string').encode("utf8"))
511 res.append(de.get('help').encode("utf8"))
513 res.append(de.get('sum').encode("utf8"))
514 if de.get("confirm"):
515 res.append(de.get('confirm').encode("utf8"))
517 res.extend(trans_parse_view(n))
520 # tests whether an object is in a list of modules
521 def in_modules(object_name, modules):
530 module = object_name.split('.')[0]
531 module = module_dict.get(module, module)
532 return module in modules
534 def trans_generate(lang, modules, cr):
535 logger = logging.getLogger('i18n')
538 pool = pooler.get_pool(dbname)
539 trans_obj = pool.get('ir.translation')
540 model_data_obj = pool.get('ir.model.data')
542 l = pool.models.items()
545 query = 'SELECT name, model, res_id, module' \
546 ' FROM ir_model_data'
548 query_models = """SELECT m.id, m.model, imd.module
549 FROM ir_model AS m, ir_model_data AS imd
550 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
552 if 'all_installed' in modules:
553 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
554 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
556 if 'all' not in modules:
557 query += ' WHERE module IN %s'
558 query_models += ' AND imd.module in %s'
559 query_param = (tuple(modules),)
560 query += ' ORDER BY module, model, name'
561 query_models += ' ORDER BY module, model'
563 cr.execute(query, query_param)
566 def push_translation(module, type, name, id, source):
567 tuple = (module, source, name, id, type)
568 if source and tuple not in _to_translate:
569 _to_translate.append(tuple)
572 if isinstance(s, unicode):
573 return s.encode('utf8')
576 for (xml_name,model,res_id,module) in cr.fetchall():
577 module = encode(module)
578 model = encode(model)
579 xml_name = "%s.%s" % (module, encode(xml_name))
581 if not pool.get(model):
582 logger.error("Unable to find object %r", model)
585 exists = pool.get(model).exists(cr, uid, res_id)
587 logger.warning("Unable to find object %r with id %d", model, res_id)
589 obj = pool.get(model).browse(cr, uid, res_id)
591 if model=='ir.ui.view':
592 d = etree.XML(encode(obj.arch))
593 for t in trans_parse_view(d):
594 push_translation(module, 'view', encode(obj.model), 0, t)
595 elif model=='ir.actions.wizard':
596 service_name = 'wizard.'+encode(obj.wiz_name)
597 import openerp.netsvc as netsvc
598 if netsvc.Service._services.get(service_name):
599 obj2 = netsvc.Service._services[service_name]
600 for state_name, state_def in obj2.states.iteritems():
601 if 'result' in state_def:
602 result = state_def['result']
603 if result['type'] != 'form':
605 name = "%s,%s" % (encode(obj.wiz_name), state_name)
608 'string': ('wizard_field', lambda s: [encode(s)]),
609 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
610 'help': ('help', lambda s: [encode(s)]),
614 if not result.has_key('fields'):
615 logger.warning("res has no fields: %r", result)
617 for field_name, field_def in result['fields'].iteritems():
618 res_name = name + ',' + field_name
620 for fn in def_params:
622 transtype, modifier = def_params[fn]
623 for val in modifier(field_def[fn]):
624 push_translation(module, transtype, res_name, 0, val)
627 arch = result['arch']
628 if arch and not isinstance(arch, UpdateableStr):
630 for t in trans_parse_view(d):
631 push_translation(module, 'wizard_view', name, 0, t)
633 # export button labels
634 for but_args in result['state']:
635 button_name = but_args[0]
636 button_label = but_args[1]
637 res_name = name + ',' + button_name
638 push_translation(module, 'wizard_button', res_name, 0, button_label)
640 elif model=='ir.model.fields':
642 field_name = encode(obj.name)
643 except AttributeError, exc:
644 logger.error("name error in %s: %s", xml_name, str(exc))
646 objmodel = pool.get(obj.model)
647 if not objmodel or not field_name in objmodel._columns:
649 field_def = objmodel._columns[field_name]
651 name = "%s,%s" % (encode(obj.model), field_name)
652 push_translation(module, 'field', name, 0, encode(field_def.string))
655 push_translation(module, 'help', name, 0, encode(field_def.help))
657 if field_def.translate:
658 ids = objmodel.search(cr, uid, [])
659 obj_values = objmodel.read(cr, uid, ids, [field_name])
660 for obj_value in obj_values:
661 res_id = obj_value['id']
662 if obj.name in ('ir.model', 'ir.ui.menu'):
664 model_data_ids = model_data_obj.search(cr, uid, [
665 ('model', '=', model),
666 ('res_id', '=', res_id),
668 if not model_data_ids:
669 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
671 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
672 for dummy, val in field_def.selection:
673 push_translation(module, 'selection', name, 0, encode(val))
675 elif model=='ir.actions.report.xml':
676 name = encode(obj.report_name)
679 fname = obj.report_rml
680 parse_func = trans_parse_rml
681 report_type = "report"
683 fname = obj.report_xsl
684 parse_func = trans_parse_xsl
686 if fname and obj.report_type in ('pdf', 'xsl'):
688 report_file = misc.file_open(fname)
690 d = etree.parse(report_file)
691 for t in parse_func(d.iter()):
692 push_translation(module, report_type, name, 0, t)
695 except (IOError, etree.XMLSyntaxError):
696 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
698 for field_name,field_def in obj._table._columns.items():
699 if field_def.translate:
700 name = model + "," + field_name
702 trad = getattr(obj, field_name) or ''
705 push_translation(module, 'model', name, xml_name, encode(trad))
707 # End of data for ir.model.data query results
709 cr.execute(query_models, query_param)
711 def push_constraint_msg(module, term_type, model, msg):
712 # Check presence of __call__ directly instead of using
713 # callable() because it will be deprecated as of Python 3.0
714 if not hasattr(msg, '__call__'):
715 push_translation(module, term_type, model, 0, encode(msg))
717 for (model_id, model, module) in cr.fetchall():
718 module = encode(module)
719 model = encode(model)
721 model_obj = pool.get(model)
724 logging.getLogger("i18n").error("Unable to find object %r", model)
727 for constraint in getattr(model_obj, '_constraints', []):
728 push_constraint_msg(module, 'constraint', model, constraint[1])
730 for constraint in getattr(model_obj, '_sql_constraints', []):
731 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
733 # parse source code for _() calls
734 def get_module_from_path(path, mod_paths=None):
736 # First, construct a list of possible paths
737 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
738 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
741 mod_paths.append(adp)
742 if not os.path.isabs(adp):
743 mod_paths.append(adp)
744 elif adp.startswith(def_path):
745 mod_paths.append(adp[len(def_path)+1:])
747 if path.startswith(mp) and (os.path.dirname(path) != mp):
748 path = path[len(mp)+1:]
749 return path.split(os.path.sep)[0]
750 return 'base' # files that are not in a module are considered as being in 'base' module
752 modobj = pool.get('ir.module.module')
753 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
754 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
756 root_path = os.path.join(config.config['root_path'], 'addons')
758 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
759 if root_path in apaths:
762 path_list = [root_path,] + apaths
764 # Also scan these non-addon paths
765 for bin_path in ['osv', 'report' ]:
766 path_list.append(os.path.join(config.config['root_path'], bin_path))
768 logger.debug("Scanning modules at paths: ", path_list)
771 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
772 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
773 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
774 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
776 def export_code_terms_from_file(fname, path, root, terms_type):
777 fabsolutepath = join(root, fname)
778 frelativepath = fabsolutepath[len(path):]
779 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
780 is_mod_installed = module in installed_modules
781 if (('all' in modules) or (module in modules)) and is_mod_installed:
782 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
783 src_file = misc.file_open(fabsolutepath, subdir='')
785 code_string = src_file.read()
788 if module in installed_modules:
789 frelativepath = str("addons" + frelativepath)
790 ite = re_dquotes.finditer(code_string)
795 if src.startswith('""'):
796 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
799 src = join_dquotes.sub(r'\1', src)
800 # try to count the lines from the last pos to our place:
801 code_line += code_string[code_offset:i.start(1)].count('\n')
802 # now, since we did a binary read of a python source file, we
803 # have to expand pythonic escapes like the interpreter does.
804 src = src.decode('string_escape')
805 push_translation(module, terms_type, frelativepath, code_line, encode(src))
806 code_line += i.group(1).count('\n')
807 code_offset = i.end() # we have counted newlines up to the match end
809 ite = re_quotes.finditer(code_string)
810 code_offset = 0 #reset counters
814 if src.startswith("''"):
815 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
818 src = join_quotes.sub(r'\1', src)
819 code_line += code_string[code_offset:i.start(1)].count('\n')
820 src = src.decode('string_escape')
821 push_translation(module, terms_type, frelativepath, code_line, encode(src))
822 code_line += i.group(1).count('\n')
823 code_offset = i.end() # we have counted newlines up to the match end
825 for path in path_list:
826 logger.debug("Scanning files of modules at %s", path)
827 for root, dummy, files in osutil.walksymlinks(path):
828 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
829 export_code_terms_from_file(fname, path, root, 'code')
830 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
831 export_code_terms_from_file(fname, path, root, 'report')
834 out = [["module","type","name","res_id","src","value"]] # header
836 # translate strings marked as to be translated
837 for module, source, name, id, type in _to_translate:
838 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
839 out.append([module, type, name, id, source, encode(trans) or ''])
843 def trans_load(cr, filename, lang, verbose=True, context=None):
844 logger = logging.getLogger('i18n')
846 fileobj = misc.file_open(filename)
847 logger.info("loading %s", filename)
848 fileformat = os.path.splitext(filename)[-1][1:].lower()
849 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
854 logger.error("couldn't read translation file %s", filename)
857 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
858 """Populates the ir_translation table."""
859 logger = logging.getLogger('i18n')
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: