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 if ' ' in line[2:].strip():
300 for lpart in line[2:].strip().split(' '):
301 tmp_tnrs.append(lpart.strip().split(':',2))
303 tmp_tnrs.append( line[2:].strip().split(':',2) )
304 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
306 line = self.lines.pop(0).strip()
308 # allow empty lines between comments and msgid
309 line = self.lines.pop(0).strip()
310 if line.startswith('#~ '):
311 while line.startswith('#~ ') or not line.strip():
312 if 0 == len(self.lines):
313 raise StopIteration()
314 line = self.lines.pop(0)
315 # This has been a deprecated entry, don't return anything
318 if not line.startswith('msgid'):
319 raise Exception("malformed file: bad line: %s" % line)
320 source = unquote(line[6:])
321 line = self.lines.pop(0).strip()
322 if not source and self.first:
323 # if the source is "" and it's the first msgid, it's the special
324 # msgstr with the informations about the traduction and the
325 # traductor; we skip it
328 line = self.lines.pop(0).strip()
331 while not line.startswith('msgstr'):
333 raise Exception('malformed file at %d'% self.cur_line())
334 source += unquote(line)
335 line = self.lines.pop(0).strip()
337 trad = unquote(line[7:])
338 line = self.lines.pop(0).strip()
340 trad += unquote(line)
341 line = self.lines.pop(0).strip()
343 if tmp_tnrs and not fuzzy:
344 type, name, res_id = tmp_tnrs.pop(0)
345 for t, n, r in tmp_tnrs:
346 self.tnrs.append((t, n, r, source, trad))
352 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
353 self.cur_line(), source[:30])
355 return type, name, res_id, source, trad
357 def write_infos(self, modules):
358 import openerp.release as release
359 self.buffer.write("# Translation of %(project)s.\n" \
360 "# This file contains the translation of the following modules:\n" \
365 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
366 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
367 '''"POT-Creation-Date: %(now)s\\n"\n''' \
368 '''"PO-Revision-Date: %(now)s\\n"\n''' \
369 '''"Last-Translator: <>\\n"\n''' \
370 '''"Language-Team: \\n"\n''' \
371 '''"MIME-Version: 1.0\\n"\n''' \
372 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
373 '''"Content-Transfer-Encoding: \\n"\n''' \
374 '''"Plural-Forms: \\n"\n''' \
377 % { 'project': release.description,
378 'version': release.version,
379 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
380 'bugmail': release.support_email,
381 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
385 def write(self, modules, tnrs, source, trad):
387 plurial = len(modules) > 1 and 's' or ''
388 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
392 for typy, name, res_id in tnrs:
393 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
398 # only strings in python code are python formated
399 self.buffer.write("#, python-format\n")
401 if not isinstance(trad, unicode):
402 trad = unicode(trad, 'utf8')
403 if not isinstance(source, unicode):
404 source = unicode(source, 'utf8')
408 % (quote(source), quote(trad))
409 self.buffer.write(msg.encode('utf8'))
412 # Methods to export the translation file
414 def trans_export(lang, modules, buffer, format, cr):
416 def _process(format, modules, rows, buffer, lang, newlang):
418 writer=csv.writer(buffer, 'UNIX')
423 writer = TinyPoFile(buffer)
424 writer.write_infos(modules)
426 # we now group the translations by source. That means one translation per source.
428 for module, type, name, res_id, src, trad in rows:
429 row = grouped_rows.setdefault(src, {})
430 row.setdefault('modules', set()).add(module)
431 if ('translation' not in row) or (not row['translation']):
432 row['translation'] = trad
433 row.setdefault('tnrs', []).append((type, name, res_id))
435 for src, row in grouped_rows.items():
436 writer.write(row['modules'], row['tnrs'], src, row['translation'])
438 elif format == 'tgz':
443 # first row is the "header", as in csv, it will be popped
444 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
445 rows_by_module[module].append(row)
447 tmpdir = tempfile.mkdtemp()
448 for mod, modrows in rows_by_module.items():
449 tmpmoddir = join(tmpdir, mod, 'i18n')
450 os.makedirs(tmpmoddir)
451 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
452 buf = file(join(tmpmoddir, pofilename), 'w')
453 _process('po', [mod], modrows, buf, lang, newlang)
456 tar = tarfile.open(fileobj=buffer, mode='w|gz')
461 raise Exception(_('Unrecognized extension: must be one of '
462 '.csv, .po, or .tgz (received .%s).' % format))
464 newlang = not bool(lang)
467 trans = trans_generate(lang, modules, cr)
468 if newlang and format!='csv':
471 modules = set([t[0] for t in trans[1:]])
472 _process(format, modules, trans, buffer, lang, newlang)
475 def trans_parse_xsl(de):
480 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
482 l = m.text.strip().replace('\n',' ')
484 res.append(l.encode("utf8"))
485 res.extend(trans_parse_xsl(n))
488 def trans_parse_rml(de):
492 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
494 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
495 for s in string_list:
497 res.append(s.encode("utf8"))
498 res.extend(trans_parse_rml(n))
501 def trans_parse_view(de):
503 if de.tag == 'attribute' and de.get("name") == 'string':
505 res.append(de.text.encode("utf8"))
507 res.append(de.get('string').encode("utf8"))
509 res.append(de.get('help').encode("utf8"))
511 res.append(de.get('sum').encode("utf8"))
512 if de.get("confirm"):
513 res.append(de.get('confirm').encode("utf8"))
515 res.extend(trans_parse_view(n))
518 # tests whether an object is in a list of modules
519 def in_modules(object_name, modules):
528 module = object_name.split('.')[0]
529 module = module_dict.get(module, module)
530 return module in modules
532 def trans_generate(lang, modules, cr):
533 logger = logging.getLogger('i18n')
536 pool = pooler.get_pool(dbname)
537 trans_obj = pool.get('ir.translation')
538 model_data_obj = pool.get('ir.model.data')
540 l = pool.models.items()
543 query = 'SELECT name, model, res_id, module' \
544 ' FROM ir_model_data'
546 query_models = """SELECT m.id, m.model, imd.module
547 FROM ir_model AS m, ir_model_data AS imd
548 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
550 if 'all_installed' in modules:
551 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
552 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
554 if 'all' not in modules:
555 query += ' WHERE module IN %s'
556 query_models += ' AND imd.module in %s'
557 query_param = (tuple(modules),)
558 query += ' ORDER BY module, model, name'
559 query_models += ' ORDER BY module, model'
561 cr.execute(query, query_param)
564 def push_translation(module, type, name, id, source):
565 tuple = (module, source, name, id, type)
566 if source and tuple not in _to_translate:
567 _to_translate.append(tuple)
570 if isinstance(s, unicode):
571 return s.encode('utf8')
574 for (xml_name,model,res_id,module) in cr.fetchall():
575 module = encode(module)
576 model = encode(model)
577 xml_name = "%s.%s" % (module, encode(xml_name))
579 if not pool.get(model):
580 logger.error("Unable to find object %r", model)
583 exists = pool.get(model).exists(cr, uid, res_id)
585 logger.warning("Unable to find object %r with id %d", model, res_id)
587 obj = pool.get(model).browse(cr, uid, res_id)
589 if model=='ir.ui.view':
590 d = etree.XML(encode(obj.arch))
591 for t in trans_parse_view(d):
592 push_translation(module, 'view', encode(obj.model), 0, t)
593 elif model=='ir.actions.wizard':
594 service_name = 'wizard.'+encode(obj.wiz_name)
595 import openerp.netsvc as netsvc
596 if netsvc.Service._services.get(service_name):
597 obj2 = netsvc.Service._services[service_name]
598 for state_name, state_def in obj2.states.iteritems():
599 if 'result' in state_def:
600 result = state_def['result']
601 if result['type'] != 'form':
603 name = "%s,%s" % (encode(obj.wiz_name), state_name)
606 'string': ('wizard_field', lambda s: [encode(s)]),
607 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
608 'help': ('help', lambda s: [encode(s)]),
612 if not result.has_key('fields'):
613 logger.warning("res has no fields: %r", result)
615 for field_name, field_def in result['fields'].iteritems():
616 res_name = name + ',' + field_name
618 for fn in def_params:
620 transtype, modifier = def_params[fn]
621 for val in modifier(field_def[fn]):
622 push_translation(module, transtype, res_name, 0, val)
625 arch = result['arch']
626 if arch and not isinstance(arch, UpdateableStr):
628 for t in trans_parse_view(d):
629 push_translation(module, 'wizard_view', name, 0, t)
631 # export button labels
632 for but_args in result['state']:
633 button_name = but_args[0]
634 button_label = but_args[1]
635 res_name = name + ',' + button_name
636 push_translation(module, 'wizard_button', res_name, 0, button_label)
638 elif model=='ir.model.fields':
640 field_name = encode(obj.name)
641 except AttributeError, exc:
642 logger.error("name error in %s: %s", xml_name, str(exc))
644 objmodel = pool.get(obj.model)
645 if not objmodel or not field_name in objmodel._columns:
647 field_def = objmodel._columns[field_name]
649 name = "%s,%s" % (encode(obj.model), field_name)
650 push_translation(module, 'field', name, 0, encode(field_def.string))
653 push_translation(module, 'help', name, 0, encode(field_def.help))
655 if field_def.translate:
656 ids = objmodel.search(cr, uid, [])
657 obj_values = objmodel.read(cr, uid, ids, [field_name])
658 for obj_value in obj_values:
659 res_id = obj_value['id']
660 if obj.name in ('ir.model', 'ir.ui.menu'):
662 model_data_ids = model_data_obj.search(cr, uid, [
663 ('model', '=', model),
664 ('res_id', '=', res_id),
666 if not model_data_ids:
667 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
669 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
670 for dummy, val in field_def.selection:
671 push_translation(module, 'selection', name, 0, encode(val))
673 elif model=='ir.actions.report.xml':
674 name = encode(obj.report_name)
677 fname = obj.report_rml
678 parse_func = trans_parse_rml
679 report_type = "report"
681 fname = obj.report_xsl
682 parse_func = trans_parse_xsl
684 if fname and obj.report_type in ('pdf', 'xsl'):
686 report_file = misc.file_open(fname)
688 d = etree.parse(report_file)
689 for t in parse_func(d.iter()):
690 push_translation(module, report_type, name, 0, t)
693 except (IOError, etree.XMLSyntaxError):
694 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
696 for field_name,field_def in obj._table._columns.items():
697 if field_def.translate:
698 name = model + "," + field_name
700 trad = getattr(obj, field_name) or ''
703 push_translation(module, 'model', name, xml_name, encode(trad))
705 # End of data for ir.model.data query results
707 cr.execute(query_models, query_param)
709 def push_constraint_msg(module, term_type, model, msg):
710 # Check presence of __call__ directly instead of using
711 # callable() because it will be deprecated as of Python 3.0
712 if not hasattr(msg, '__call__'):
713 push_translation(module, term_type, model, 0, encode(msg))
715 for (model_id, model, module) in cr.fetchall():
716 module = encode(module)
717 model = encode(model)
719 model_obj = pool.get(model)
722 logging.getLogger("i18n").error("Unable to find object %r", model)
725 for constraint in getattr(model_obj, '_constraints', []):
726 push_constraint_msg(module, 'constraint', model, constraint[1])
728 for constraint in getattr(model_obj, '_sql_constraints', []):
729 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
731 # parse source code for _() calls
732 def get_module_from_path(path, mod_paths=None):
734 # First, construct a list of possible paths
735 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
736 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
739 mod_paths.append(adp)
740 if not os.path.isabs(adp):
741 mod_paths.append(adp)
742 elif adp.startswith(def_path):
743 mod_paths.append(adp[len(def_path)+1:])
745 if path.startswith(mp) and (os.path.dirname(path) != mp):
746 path = path[len(mp)+1:]
747 return path.split(os.path.sep)[0]
748 return 'base' # files that are not in a module are considered as being in 'base' module
750 modobj = pool.get('ir.module.module')
751 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
752 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
754 root_path = os.path.join(config.config['root_path'], 'addons')
756 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
757 if root_path in apaths:
760 path_list = [root_path,] + apaths
762 # Also scan these non-addon paths
763 for bin_path in ['osv', 'report' ]:
764 path_list.append(os.path.join(config.config['root_path'], bin_path))
766 logger.debug("Scanning modules at paths: ", path_list)
769 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
770 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
771 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
772 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
774 def export_code_terms_from_file(fname, path, root, terms_type):
775 fabsolutepath = join(root, fname)
776 frelativepath = fabsolutepath[len(path):]
777 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
778 is_mod_installed = module in installed_modules
779 if (('all' in modules) or (module in modules)) and is_mod_installed:
780 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
781 src_file = misc.file_open(fabsolutepath, subdir='')
783 code_string = src_file.read()
786 if module in installed_modules:
787 frelativepath = str("addons" + frelativepath)
788 ite = re_dquotes.finditer(code_string)
793 if src.startswith('""'):
794 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
797 src = join_dquotes.sub(r'\1', src)
798 # try to count the lines from the last pos to our place:
799 code_line += code_string[code_offset:i.start(1)].count('\n')
800 # now, since we did a binary read of a python source file, we
801 # have to expand pythonic escapes like the interpreter does.
802 src = src.decode('string_escape')
803 push_translation(module, terms_type, frelativepath, code_line, encode(src))
804 code_line += i.group(1).count('\n')
805 code_offset = i.end() # we have counted newlines up to the match end
807 ite = re_quotes.finditer(code_string)
808 code_offset = 0 #reset counters
812 if src.startswith("''"):
813 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
816 src = join_quotes.sub(r'\1', src)
817 code_line += code_string[code_offset:i.start(1)].count('\n')
818 src = src.decode('string_escape')
819 push_translation(module, terms_type, frelativepath, code_line, encode(src))
820 code_line += i.group(1).count('\n')
821 code_offset = i.end() # we have counted newlines up to the match end
823 for path in path_list:
824 logger.debug("Scanning files of modules at %s", path)
825 for root, dummy, files in osutil.walksymlinks(path):
826 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
827 export_code_terms_from_file(fname, path, root, 'code')
828 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
829 export_code_terms_from_file(fname, path, root, 'report')
832 out = [["module","type","name","res_id","src","value"]] # header
834 # translate strings marked as to be translated
835 for module, source, name, id, type in _to_translate:
836 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
837 out.append([module, type, name, id, source, encode(trans) or ''])
841 def trans_load(cr, filename, lang, verbose=True, context=None):
842 logger = logging.getLogger('i18n')
844 fileobj = open(filename,'r')
845 logger.info("loading %s", filename)
846 fileformat = os.path.splitext(filename)[-1][1:].lower()
847 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
852 logger.error("couldn't read translation file %s", filename)
855 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
856 """Populates the ir_translation table. Fixing the res_ids so that they point
857 correctly to ir_model_data is done in a separate step, using the
858 'trans_update_res_ids' function below."""
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
897 # skip empty rows and rows where the translation field (=last fiefd) is empty
898 #if (not row) or (not row[-1]):
901 # dictionary which holds values for this line of the csv file
902 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
903 # 'src': ..., 'value': ...}
905 for i in range(len(f)):
906 if f[i] in ('module',):
911 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
912 dic['module'] = False
913 dic['xml_id'] = False
915 split_id = dic['res_id'].split('.', 1)
916 dic['module'] = split_id[0]
917 dic['xml_id'] = split_id[1]
918 dic['res_id'] = False
922 ('type', '=', dic['type']),
923 ('name', '=', dic['name']),
924 ('src', '=', dic['src']),
926 if dic['type'] == 'model':
927 if dic['res_id'] is False:
928 args.append(('module', '=', dic['module']))
929 args.append(('xml_id', '=', dic['xml_id']))
931 args.append(('res_id', '=', dic['res_id']))
932 ids = trans_obj.search(cr, uid, args)
934 if context.get('overwrite') and dic['value']:
935 trans_obj.write(cr, uid, ids, {'value': dic['value']})
937 trans_obj.create(cr, uid, dic)
939 logger.info("translation file loaded succesfully")
941 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
942 logger.exception("couldn't read translation file %s", filename)
944 def trans_update_res_ids(cr):
946 UPDATE ir_translation
947 SET res_id = COALESCE ((SELECT ir_model_data.res_id
949 WHERE ir_translation.module = ir_model_data.module
950 AND ir_translation.xml_id = ir_model_data.name), 0)
951 WHERE ir_translation.module is not null
952 AND ir_translation.xml_id is not null
953 AND ir_translation.res_id = 0;
956 def get_locales(lang=None):
958 lang = locale.getdefaultlocale()[0]
961 lang = _LOCALE2WIN32.get(lang, lang)
964 ln = locale._build_localename((lang, enc))
966 nln = locale.normalize(ln)
970 for x in process('utf8'): yield x
972 prefenc = locale.getpreferredencoding()
974 for x in process(prefenc): yield x
978 'iso-8859-1': 'iso8859-15',
980 }.get(prefenc.lower())
982 for x in process(prefenc): yield x
989 # locale.resetlocale is bugged with some locales.
990 for ln in get_locales():
992 return locale.setlocale(locale.LC_ALL, ln)
996 def load_language(cr, lang):
997 """Loads a translation terms for a language.
998 Used mainly to automate language loading at db initialization.
1000 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1003 pool = pooler.get_pool(cr.dbname)
1004 language_installer = pool.get('base.language.install')
1006 oid = language_installer.create(cr, uid, {'lang': lang})
1007 language_installer.lang_install(cr, uid, [oid], context=None)
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: