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 import babel.messages.pofile
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, 1, 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 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.text and de.text.strip():
506 res.append(de.text.strip().encode("utf8"))
507 if de.tail and de.tail.strip():
508 res.append(de.tail.strip().encode("utf8"))
509 if de.tag == 'attribute' and de.get("name") == 'string':
511 res.append(de.text.encode("utf8"))
513 res.append(de.get('string').encode("utf8"))
515 res.append(de.get('help').encode("utf8"))
517 res.append(de.get('sum').encode("utf8"))
518 if de.get("confirm"):
519 res.append(de.get('confirm').encode("utf8"))
521 res.extend(trans_parse_view(n))
524 # tests whether an object is in a list of modules
525 def in_modules(object_name, modules):
534 module = object_name.split('.')[0]
535 module = module_dict.get(module, module)
536 return module in modules
538 def trans_generate(lang, modules, cr):
541 pool = pooler.get_pool(dbname)
542 trans_obj = pool.get('ir.translation')
543 model_data_obj = pool.get('ir.model.data')
545 l = pool.models.items()
548 query = 'SELECT name, model, res_id, module' \
549 ' FROM ir_model_data'
551 query_models = """SELECT m.id, m.model, imd.module
552 FROM ir_model AS m, ir_model_data AS imd
553 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
555 if 'all_installed' in modules:
556 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
557 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
559 if 'all' not in modules:
560 query += ' WHERE module IN %s'
561 query_models += ' AND imd.module in %s'
562 query_param = (tuple(modules),)
563 query += ' ORDER BY module, model, name'
564 query_models += ' ORDER BY module, model'
566 cr.execute(query, query_param)
569 def push_translation(module, type, name, id, source):
570 tuple = (module, source, name, id, type)
571 if source and tuple not in _to_translate:
572 _to_translate.append(tuple)
575 if isinstance(s, unicode):
576 return s.encode('utf8')
579 for (xml_name,model,res_id,module) in cr.fetchall():
580 module = encode(module)
581 model = encode(model)
582 xml_name = "%s.%s" % (module, encode(xml_name))
584 if not pool.get(model):
585 _logger.error("Unable to find object %r", model)
588 exists = pool.get(model).exists(cr, uid, res_id)
590 _logger.warning("Unable to find object %r with id %d", model, res_id)
592 obj = pool.get(model).browse(cr, uid, res_id)
594 if model=='ir.ui.view':
595 d = etree.XML(encode(obj.arch))
596 for t in trans_parse_view(d):
597 push_translation(module, 'view', encode(obj.model), 0, t)
598 elif model=='ir.actions.wizard':
599 service_name = 'wizard.'+encode(obj.wiz_name)
600 import openerp.netsvc as netsvc
601 if netsvc.Service._services.get(service_name):
602 obj2 = netsvc.Service._services[service_name]
603 for state_name, state_def in obj2.states.iteritems():
604 if 'result' in state_def:
605 result = state_def['result']
606 if result['type'] != 'form':
608 name = "%s,%s" % (encode(obj.wiz_name), state_name)
611 'string': ('wizard_field', lambda s: [encode(s)]),
612 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
613 'help': ('help', lambda s: [encode(s)]),
617 if not result.has_key('fields'):
618 _logger.warning("res has no fields: %r", result)
620 for field_name, field_def in result['fields'].iteritems():
621 res_name = name + ',' + field_name
623 for fn in def_params:
625 transtype, modifier = def_params[fn]
626 for val in modifier(field_def[fn]):
627 push_translation(module, transtype, res_name, 0, val)
630 arch = result['arch']
631 if arch and not isinstance(arch, UpdateableStr):
633 for t in trans_parse_view(d):
634 push_translation(module, 'wizard_view', name, 0, t)
636 # export button labels
637 for but_args in result['state']:
638 button_name = but_args[0]
639 button_label = but_args[1]
640 res_name = name + ',' + button_name
641 push_translation(module, 'wizard_button', res_name, 0, button_label)
643 elif model=='ir.model.fields':
645 field_name = encode(obj.name)
646 except AttributeError, exc:
647 _logger.error("name error in %s: %s", xml_name, str(exc))
649 objmodel = pool.get(obj.model)
650 if not objmodel or not field_name in objmodel._columns:
652 field_def = objmodel._columns[field_name]
654 name = "%s,%s" % (encode(obj.model), field_name)
655 push_translation(module, 'field', name, 0, encode(field_def.string))
658 push_translation(module, 'help', name, 0, encode(field_def.help))
660 if field_def.translate:
661 ids = objmodel.search(cr, uid, [])
662 obj_values = objmodel.read(cr, uid, ids, [field_name])
663 for obj_value in obj_values:
664 res_id = obj_value['id']
665 if obj.name in ('ir.model', 'ir.ui.menu'):
667 model_data_ids = model_data_obj.search(cr, uid, [
668 ('model', '=', model),
669 ('res_id', '=', res_id),
671 if not model_data_ids:
672 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
674 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
675 for dummy, val in field_def.selection:
676 push_translation(module, 'selection', name, 0, encode(val))
678 elif model=='ir.actions.report.xml':
679 name = encode(obj.report_name)
682 fname = obj.report_rml
683 parse_func = trans_parse_rml
684 report_type = "report"
686 fname = obj.report_xsl
687 parse_func = trans_parse_xsl
689 if fname and obj.report_type in ('pdf', 'xsl'):
691 report_file = misc.file_open(fname)
693 d = etree.parse(report_file)
694 for t in parse_func(d.iter()):
695 push_translation(module, report_type, name, 0, t)
698 except (IOError, etree.XMLSyntaxError):
699 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
701 for field_name,field_def in obj._table._columns.items():
702 if field_def.translate:
703 name = model + "," + field_name
705 trad = getattr(obj, field_name) or ''
708 push_translation(module, 'model', name, xml_name, encode(trad))
710 # End of data for ir.model.data query results
712 cr.execute(query_models, query_param)
714 def push_constraint_msg(module, term_type, model, msg):
715 # Check presence of __call__ directly instead of using
716 # callable() because it will be deprecated as of Python 3.0
717 if not hasattr(msg, '__call__'):
718 push_translation(module, term_type, model, 0, encode(msg))
720 for (model_id, model, module) in cr.fetchall():
721 module = encode(module)
722 model = encode(model)
724 model_obj = pool.get(model)
727 _logger.error("Unable to find object %r", model)
730 for constraint in getattr(model_obj, '_constraints', []):
731 push_constraint_msg(module, 'constraint', model, constraint[1])
733 for constraint in getattr(model_obj, '_sql_constraints', []):
734 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
736 # parse source code for _() calls
737 def get_module_from_path(path, mod_paths=None):
739 # First, construct a list of possible paths
740 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
741 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
744 mod_paths.append(adp)
745 if not os.path.isabs(adp):
746 mod_paths.append(adp)
747 elif adp.startswith(def_path):
748 mod_paths.append(adp[len(def_path)+1:])
750 if path.startswith(mp) and (os.path.dirname(path) != mp):
751 path = path[len(mp)+1:]
752 return path.split(os.path.sep)[0]
753 return 'base' # files that are not in a module are considered as being in 'base' module
755 modobj = pool.get('ir.module.module')
756 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
757 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
759 root_path = os.path.join(config.config['root_path'], 'addons')
761 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
762 if root_path in apaths:
765 path_list = [root_path,] + apaths
767 # Also scan these non-addon paths
768 for bin_path in ['osv', 'report' ]:
769 path_list.append(os.path.join(config.config['root_path'], bin_path))
771 _logger.debug("Scanning modules at paths: ", path_list)
774 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
775 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
776 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
777 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
779 def export_code_terms_from_file(fname, path, root, terms_type):
780 fabsolutepath = join(root, fname)
781 frelativepath = fabsolutepath[len(path):]
782 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
783 is_mod_installed = module in installed_modules
784 if (('all' in modules) or (module in modules)) and is_mod_installed:
785 _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
786 src_file = misc.file_open(fabsolutepath, subdir='')
788 code_string = src_file.read()
791 if module in installed_modules:
792 frelativepath = str("addons" + frelativepath)
793 ite = re_dquotes.finditer(code_string)
798 if src.startswith('""'):
799 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
802 src = join_dquotes.sub(r'\1', src)
803 # try to count the lines from the last pos to our place:
804 code_line += code_string[code_offset:i.start(1)].count('\n')
805 # now, since we did a binary read of a python source file, we
806 # have to expand pythonic escapes like the interpreter does.
807 src = src.decode('string_escape')
808 push_translation(module, terms_type, frelativepath, code_line, encode(src))
809 code_line += i.group(1).count('\n')
810 code_offset = i.end() # we have counted newlines up to the match end
812 ite = re_quotes.finditer(code_string)
813 code_offset = 0 #reset counters
817 if src.startswith("''"):
818 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
821 src = join_quotes.sub(r'\1', src)
822 code_line += code_string[code_offset:i.start(1)].count('\n')
823 src = src.decode('string_escape')
824 push_translation(module, terms_type, frelativepath, code_line, encode(src))
825 code_line += i.group(1).count('\n')
826 code_offset = i.end() # we have counted newlines up to the match end
828 for path in path_list:
829 _logger.debug("Scanning files of modules at %s", path)
830 for root, dummy, files in osutil.walksymlinks(path):
831 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
832 export_code_terms_from_file(fname, path, root, 'code')
833 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
834 export_code_terms_from_file(fname, path, root, 'report')
837 out = [["module","type","name","res_id","src","value"]] # header
839 # translate strings marked as to be translated
840 for module, source, name, id, type in _to_translate:
841 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
842 out.append([module, type, name, id, source, encode(trans) or ''])
846 def trans_load(cr, filename, lang, verbose=True, flag=None, module_name=None, context=None):
848 fileobj = misc.file_open(filename)
849 pool = pooler.get_pool(cr.dbname)
850 traslation_obj = pool.get('ir.translation')
851 _logger.info("loading %s", filename)
852 if flag == 'web' and module_name == 'web':
854 trans_ids = traslation_obj.search(cr, 1, [('module','=', module_name),('lang','=',lang)])
855 for trans in traslation_obj.browse(cr, 1, trans_ids, context=context):
856 transl.append({'id': trans.src, 'string': trans.value})
859 fileformat = os.path.splitext(filename)[-1][1:].lower()
860 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context)
865 _logger.error("couldn't read translation file %s", filename)
868 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None):
869 """Populates the ir_translation table."""
871 _logger.info('loading translation file for language %s', lang)
875 pool = pooler.get_pool(db_name)
876 lang_obj = pool.get('res.lang')
877 trans_obj = pool.get('ir.translation')
878 iso_lang = misc.get_iso_codes(lang)
881 ids = lang_obj.search(cr, uid, [('code','=', lang)])
884 # lets create the language with locale information
885 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
888 # now, the serious things: we read the language file
890 if fileformat == 'csv':
891 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
892 # read the first line of the file (it contains columns titles)
896 elif fileformat == 'po':
897 reader = TinyPoFile(fileobj)
898 f = ['type', 'name', 'res_id', 'src', 'value', 'module']
900 _logger.error('Bad file format: %s', fileformat)
901 raise Exception(_('Bad file format'))
903 # read the rest of the file
905 irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
909 # skip empty rows and rows where the translation field (=last fiefd) is empty
910 #if (not row) or (not row[-1]):
913 # dictionary which holds values for this line of the csv file
914 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
915 # 'src': ..., 'value': ..., 'module':...}
918 for i in range(len(f)):
919 if f[i] in ('module',):
923 # This would skip terms that fail to specify a res_id
924 if not dic.get('res_id', False):
927 res_id = dic.pop('res_id')
928 if res_id and isinstance(res_id, (int, long)) \
929 or (isinstance(res_id, basestring) and res_id.isdigit()):
930 dic['res_id'] = int(res_id)
931 dic['module'] = module_name
934 tmodel = dic['name'].split(',')[0]
936 tmodule, tname = res_id.split('.', 1)
940 dic['imd_model'] = tmodel
941 dic['module'] = tmodule
942 dic['imd_name'] = tname
945 _logger.warning("Could not decode resource for %s, please fix the po file.",
946 dic['res_id'], exc_info=True)
953 _logger.info("translation file loaded succesfully")
955 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
956 _logger.exception("couldn't read translation file %s", filename)
958 def get_locales(lang=None):
960 lang = locale.getdefaultlocale()[0]
963 lang = _LOCALE2WIN32.get(lang, lang)
966 ln = locale._build_localename((lang, enc))
968 nln = locale.normalize(ln)
972 for x in process('utf8'): yield x
974 prefenc = locale.getpreferredencoding()
976 for x in process(prefenc): yield x
980 'iso-8859-1': 'iso8859-15',
982 }.get(prefenc.lower())
984 for x in process(prefenc): yield x
991 # locale.resetlocale is bugged with some locales.
992 for ln in get_locales():
994 return locale.setlocale(locale.LC_ALL, ln)
998 def load_language(cr, lang):
999 """Loads a translation terms for a language.
1000 Used mainly to automate language loading at db initialization.
1002 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1005 pool = pooler.get_pool(cr.dbname)
1006 language_installer = pool.get('base.language.install')
1008 oid = language_installer.create(cr, uid, {'lang': lang})
1009 language_installer.lang_install(cr, uid, [oid], context=None)
1011 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: