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, allow_create=True):
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)
172 if not cr and allow_create:
179 def _get_uid(self, frame):
180 return frame.f_locals.get('uid') or frame.f_locals.get('user')
182 def _get_lang(self, frame):
184 ctx = frame.f_locals.get('context')
186 kwargs = frame.f_locals.get('kwargs')
188 args = frame.f_locals.get('args')
189 if args and isinstance(args, (list, tuple)) \
190 and isinstance(args[-1], dict):
192 elif isinstance(kwargs, dict):
193 ctx = kwargs.get('context')
195 lang = ctx.get('lang')
196 s = frame.f_locals.get('self', {})
198 c = getattr(s, 'localcontext', None)
202 # Last resort: attempt to guess the language of the user
203 # Pitfall: some operations are performed in sudo mode, and we
204 # don't know the originial uid, so the language may
205 # be wrong when the admin language differs.
206 pool = getattr(s, 'pool', None)
207 (cr, dummy) = self._get_cr(frame, allow_create=False)
208 uid = self._get_uid(frame)
209 if pool and cr and uid:
210 lang = pool.get('res.users').context_get(cr, uid)['lang']
213 def __call__(self, source):
218 frame = inspect.currentframe()
224 lang = self._get_lang(frame)
226 cr, is_new_cr = self._get_cr(frame)
228 # Try to use ir.translation to benefit from global cache if possible
229 pool = pooler.get_pool(cr.dbname)
230 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
232 _logger.debug('no context cursor detected, skipping translation for "%r"', source)
234 _logger.debug('no translation language detected, skipping translation for "%r" ', source)
236 _logger.debug('translation went wrong for "%r", skipped', source)
237 # if so, double-check the root/base translations filenames
247 """Returns quoted PO term string, with special PO characters escaped"""
248 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
249 return '"%s"' % s.replace('\\','\\\\') \
250 .replace('"','\\"') \
251 .replace('\n', '\\n"\n"')
253 re_escaped_char = re.compile(r"(\\.)")
254 re_escaped_replacements = {'n': '\n', }
256 def _sub_replacement(match_obj):
257 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
260 """Returns unquoted PO term string, with special PO characters unescaped"""
261 return re_escaped_char.sub(_sub_replacement, str[1:-1])
263 # class to handle po files
264 class TinyPoFile(object):
265 def __init__(self, buffer):
268 def warn(self, msg, *args):
269 _logger.warning(msg, *args)
273 self.lines = self._get_lines()
274 self.lines_count = len(self.lines);
280 def _get_lines(self):
281 lines = self.buffer.readlines()
282 # remove the BOM (Byte Order Mark):
284 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
286 lines.append('') # ensure that the file ends with at least an empty line
290 return (self.lines_count - len(self.lines))
293 type = name = res_id = source = trad = None
296 type, name, res_id, source, trad = self.tnrs.pop(0)
304 if 0 == len(self.lines):
305 raise StopIteration()
306 line = self.lines.pop(0).strip()
307 while line.startswith('#'):
308 if line.startswith('#~ '):
310 if line.startswith('#:'):
311 for lpart in line[2:].strip().split(' '):
312 trans_info = lpart.strip().split(':',2)
313 if trans_info and len(trans_info) == 2:
314 # looks like the translation type is missing, which is not
315 # unexpected because it is not a GetText standard. Default: 'code'
316 trans_info[:0] = ['code']
317 if trans_info and len(trans_info) == 3:
318 tmp_tnrs.append(trans_info)
319 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
321 line = self.lines.pop(0).strip()
323 # allow empty lines between comments and msgid
324 line = self.lines.pop(0).strip()
325 if line.startswith('#~ '):
326 while line.startswith('#~ ') or not line.strip():
327 if 0 == len(self.lines):
328 raise StopIteration()
329 line = self.lines.pop(0)
330 # This has been a deprecated entry, don't return anything
333 if not line.startswith('msgid'):
334 raise Exception("malformed file: bad line: %s" % line)
335 source = unquote(line[6:])
336 line = self.lines.pop(0).strip()
337 if not source and self.first:
338 # if the source is "" and it's the first msgid, it's the special
339 # msgstr with the informations about the traduction and the
340 # traductor; we skip it
343 line = self.lines.pop(0).strip()
346 while not line.startswith('msgstr'):
348 raise Exception('malformed file at %d'% self.cur_line())
349 source += unquote(line)
350 line = self.lines.pop(0).strip()
352 trad = unquote(line[7:])
353 line = self.lines.pop(0).strip()
355 trad += unquote(line)
356 line = self.lines.pop(0).strip()
358 if tmp_tnrs and not fuzzy:
359 type, name, res_id = tmp_tnrs.pop(0)
360 for t, n, r in tmp_tnrs:
361 self.tnrs.append((t, n, r, source, trad))
367 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
368 self.cur_line(), source[:30])
370 return type, name, res_id, source, trad
372 def write_infos(self, modules):
373 import openerp.release as release
374 self.buffer.write("# Translation of %(project)s.\n" \
375 "# This file contains the translation of the following modules:\n" \
380 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
381 '''"Report-Msgid-Bugs-To: \\n"\n''' \
382 '''"POT-Creation-Date: %(now)s\\n"\n''' \
383 '''"PO-Revision-Date: %(now)s\\n"\n''' \
384 '''"Last-Translator: <>\\n"\n''' \
385 '''"Language-Team: \\n"\n''' \
386 '''"MIME-Version: 1.0\\n"\n''' \
387 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
388 '''"Content-Transfer-Encoding: \\n"\n''' \
389 '''"Plural-Forms: \\n"\n''' \
392 % { 'project': release.description,
393 'version': release.version,
394 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
395 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M')+"+0000",
399 def write(self, modules, tnrs, source, trad):
401 plurial = len(modules) > 1 and 's' or ''
402 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
406 for typy, name, res_id in tnrs:
407 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
412 # only strings in python code are python formated
413 self.buffer.write("#, python-format\n")
415 if not isinstance(trad, unicode):
416 trad = unicode(trad, 'utf8')
417 if not isinstance(source, unicode):
418 source = unicode(source, 'utf8')
422 % (quote(source), quote(trad))
423 self.buffer.write(msg.encode('utf8'))
426 # Methods to export the translation file
428 def trans_export(lang, modules, buffer, format, cr):
430 def _process(format, modules, rows, buffer, lang, newlang):
432 writer=csv.writer(buffer, 'UNIX')
437 writer = TinyPoFile(buffer)
438 writer.write_infos(modules)
440 # we now group the translations by source. That means one translation per source.
442 for module, type, name, res_id, src, trad in rows:
443 row = grouped_rows.setdefault(src, {})
444 row.setdefault('modules', set()).add(module)
445 if ('translation' not in row) or (not row['translation']):
446 row['translation'] = trad
447 row.setdefault('tnrs', []).append((type, name, res_id))
449 for src, row in grouped_rows.items():
450 writer.write(row['modules'], row['tnrs'], src, row['translation'])
452 elif format == 'tgz':
457 # first row is the "header", as in csv, it will be popped
458 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
459 rows_by_module[module].append(row)
461 tmpdir = tempfile.mkdtemp()
462 for mod, modrows in rows_by_module.items():
463 tmpmoddir = join(tmpdir, mod, 'i18n')
464 os.makedirs(tmpmoddir)
465 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
466 buf = file(join(tmpmoddir, pofilename), 'w')
467 _process('po', [mod], modrows, buf, lang, newlang)
470 tar = tarfile.open(fileobj=buffer, mode='w|gz')
475 raise Exception(_('Unrecognized extension: must be one of '
476 '.csv, .po, or .tgz (received .%s).' % format))
478 newlang = not bool(lang)
481 trans = trans_generate(lang, modules, cr)
482 if newlang and format!='csv':
485 modules = set([t[0] for t in trans[1:]])
486 _process(format, modules, trans, buffer, lang, newlang)
489 def trans_parse_xsl(de):
494 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
496 l = m.text.strip().replace('\n',' ')
498 res.append(l.encode("utf8"))
499 res.extend(trans_parse_xsl(n))
502 def trans_parse_rml(de):
506 if isinstance(m, SKIPPED_ELEMENT_TYPES) or not m.text:
508 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
509 for s in string_list:
511 res.append(s.encode("utf8"))
512 res.extend(trans_parse_rml(n))
515 def trans_parse_view(de):
517 if de.tag == 'attribute' and de.get("name") == 'string':
519 res.append(de.text.encode("utf8"))
521 res.append(de.get('string').encode("utf8"))
523 res.append(de.get('help').encode("utf8"))
525 res.append(de.get('sum').encode("utf8"))
526 if de.get("confirm"):
527 res.append(de.get('confirm').encode("utf8"))
529 res.extend(trans_parse_view(n))
532 # tests whether an object is in a list of modules
533 def in_modules(object_name, modules):
542 module = object_name.split('.')[0]
543 module = module_dict.get(module, module)
544 return module in modules
546 def trans_generate(lang, modules, cr):
549 pool = pooler.get_pool(dbname)
550 trans_obj = pool.get('ir.translation')
551 model_data_obj = pool.get('ir.model.data')
553 l = pool.models.items()
556 query = 'SELECT name, model, res_id, module' \
557 ' FROM ir_model_data'
559 query_models = """SELECT m.id, m.model, imd.module
560 FROM ir_model AS m, ir_model_data AS imd
561 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
563 if 'all_installed' in modules:
564 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
565 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
567 if 'all' not in modules:
568 query += ' WHERE module IN %s'
569 query_models += ' AND imd.module in %s'
570 query_param = (tuple(modules),)
571 query += ' ORDER BY module, model, name'
572 query_models += ' ORDER BY module, model'
574 cr.execute(query, query_param)
577 def push_translation(module, type, name, id, source):
578 tuple = (module, source, name, id, type)
579 if source and tuple not in _to_translate:
580 _to_translate.append(tuple)
583 if isinstance(s, unicode):
584 return s.encode('utf8')
587 for (xml_name,model,res_id,module) in cr.fetchall():
588 module = encode(module)
589 model = encode(model)
590 xml_name = "%s.%s" % (module, encode(xml_name))
592 if not pool.get(model):
593 _logger.error("Unable to find object %r", model)
596 exists = pool.get(model).exists(cr, uid, res_id)
598 _logger.warning("Unable to find object %r with id %d", model, res_id)
600 obj = pool.get(model).browse(cr, uid, res_id)
602 if model=='ir.ui.view':
603 d = etree.XML(encode(obj.arch))
604 for t in trans_parse_view(d):
605 push_translation(module, 'view', encode(obj.model), 0, t)
606 elif model=='ir.actions.wizard':
607 service_name = 'wizard.'+encode(obj.wiz_name)
608 import openerp.netsvc as netsvc
609 if netsvc.Service._services.get(service_name):
610 obj2 = netsvc.Service._services[service_name]
611 for state_name, state_def in obj2.states.iteritems():
612 if 'result' in state_def:
613 result = state_def['result']
614 if result['type'] != 'form':
616 name = "%s,%s" % (encode(obj.wiz_name), state_name)
619 'string': ('wizard_field', lambda s: [encode(s)]),
620 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
621 'help': ('help', lambda s: [encode(s)]),
625 if not result.has_key('fields'):
626 _logger.warning("res has no fields: %r", result)
628 for field_name, field_def in result['fields'].iteritems():
629 res_name = name + ',' + field_name
631 for fn in def_params:
633 transtype, modifier = def_params[fn]
634 for val in modifier(field_def[fn]):
635 push_translation(module, transtype, res_name, 0, val)
638 arch = result['arch']
639 if arch and not isinstance(arch, UpdateableStr):
641 for t in trans_parse_view(d):
642 push_translation(module, 'wizard_view', name, 0, t)
644 # export button labels
645 for but_args in result['state']:
646 button_name = but_args[0]
647 button_label = but_args[1]
648 res_name = name + ',' + button_name
649 push_translation(module, 'wizard_button', res_name, 0, button_label)
651 elif model=='ir.model.fields':
653 field_name = encode(obj.name)
654 except AttributeError, exc:
655 _logger.error("name error in %s: %s", xml_name, str(exc))
657 objmodel = pool.get(obj.model)
658 if not objmodel or not field_name in objmodel._columns:
660 field_def = objmodel._columns[field_name]
662 name = "%s,%s" % (encode(obj.model), field_name)
663 push_translation(module, 'field', name, 0, encode(field_def.string))
666 push_translation(module, 'help', name, 0, encode(field_def.help))
668 if field_def.translate:
669 ids = objmodel.search(cr, uid, [])
670 obj_values = objmodel.read(cr, uid, ids, [field_name])
671 for obj_value in obj_values:
672 res_id = obj_value['id']
673 if obj.name in ('ir.model', 'ir.ui.menu'):
675 model_data_ids = model_data_obj.search(cr, uid, [
676 ('model', '=', model),
677 ('res_id', '=', res_id),
679 if not model_data_ids:
680 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
682 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
683 for dummy, val in field_def.selection:
684 push_translation(module, 'selection', name, 0, encode(val))
686 elif model=='ir.actions.report.xml':
687 name = encode(obj.report_name)
690 fname = obj.report_rml
691 parse_func = trans_parse_rml
692 report_type = "report"
694 fname = obj.report_xsl
695 parse_func = trans_parse_xsl
697 if fname and obj.report_type in ('pdf', 'xsl'):
699 report_file = misc.file_open(fname)
701 d = etree.parse(report_file)
702 for t in parse_func(d.iter()):
703 push_translation(module, report_type, name, 0, t)
706 except (IOError, etree.XMLSyntaxError):
707 _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
709 for field_name,field_def in obj._table._columns.items():
710 if field_def.translate:
711 name = model + "," + field_name
713 trad = getattr(obj, field_name) or ''
716 push_translation(module, 'model', name, xml_name, encode(trad))
718 # End of data for ir.model.data query results
720 cr.execute(query_models, query_param)
722 def push_constraint_msg(module, term_type, model, msg):
723 # Check presence of __call__ directly instead of using
724 # callable() because it will be deprecated as of Python 3.0
725 if not hasattr(msg, '__call__'):
726 push_translation(module, term_type, model, 0, encode(msg))
728 for (model_id, model, module) in cr.fetchall():
729 module = encode(module)
730 model = encode(model)
732 model_obj = pool.get(model)
735 _logger.error("Unable to find object %r", model)
738 for constraint in getattr(model_obj, '_constraints', []):
739 push_constraint_msg(module, 'constraint', model, constraint[1])
741 for constraint in getattr(model_obj, '_sql_constraints', []):
742 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
744 # parse source code for _() calls
745 def get_module_from_path(path, mod_paths=None):
747 # First, construct a list of possible paths
748 def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base)
749 ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
752 mod_paths.append(adp)
753 if not os.path.isabs(adp):
754 mod_paths.append(adp)
755 elif adp.startswith(def_path):
756 mod_paths.append(adp[len(def_path)+1:])
758 if path.startswith(mp) and (os.path.dirname(path) != mp):
759 path = path[len(mp)+1:]
760 return path.split(os.path.sep)[0]
761 return 'base' # files that are not in a module are considered as being in 'base' module
763 modobj = pool.get('ir.module.module')
764 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
765 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
767 root_path = os.path.join(config.config['root_path'], 'addons')
769 apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
770 if root_path in apaths:
773 path_list = [root_path,] + apaths
775 # Also scan these non-addon paths
776 for bin_path in ['osv', 'report' ]:
777 path_list.append(os.path.join(config.config['root_path'], bin_path))
779 _logger.debug("Scanning modules at paths: ", path_list)
782 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
783 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
784 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
785 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
787 def export_code_terms_from_file(fname, path, root, terms_type):
788 fabsolutepath = join(root, fname)
789 frelativepath = fabsolutepath[len(path):]
790 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
791 is_mod_installed = module in installed_modules
792 if (('all' in modules) or (module in modules)) and is_mod_installed:
793 _logger.debug("Scanning code of %s at module: %s", frelativepath, module)
794 src_file = misc.file_open(fabsolutepath, subdir='')
796 code_string = src_file.read()
799 if module in installed_modules:
800 frelativepath = str("addons" + frelativepath)
801 ite = re_dquotes.finditer(code_string)
806 if src.startswith('""'):
807 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
810 src = join_dquotes.sub(r'\1', src)
811 # try to count the lines from the last pos to our place:
812 code_line += code_string[code_offset:i.start(1)].count('\n')
813 # now, since we did a binary read of a python source file, we
814 # have to expand pythonic escapes like the interpreter does.
815 src = src.decode('string_escape')
816 push_translation(module, terms_type, frelativepath, code_line, encode(src))
817 code_line += i.group(1).count('\n')
818 code_offset = i.end() # we have counted newlines up to the match end
820 ite = re_quotes.finditer(code_string)
821 code_offset = 0 #reset counters
825 if src.startswith("''"):
826 assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
829 src = join_quotes.sub(r'\1', src)
830 code_line += code_string[code_offset:i.start(1)].count('\n')
831 src = src.decode('string_escape')
832 push_translation(module, terms_type, frelativepath, code_line, encode(src))
833 code_line += i.group(1).count('\n')
834 code_offset = i.end() # we have counted newlines up to the match end
836 for path in path_list:
837 _logger.debug("Scanning files of modules at %s", path)
838 for root, dummy, files in osutil.walksymlinks(path):
839 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
840 export_code_terms_from_file(fname, path, root, 'code')
841 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
842 export_code_terms_from_file(fname, path, root, 'report')
845 out = [["module","type","name","res_id","src","value"]] # header
847 # translate strings marked as to be translated
848 for module, source, name, id, type in _to_translate:
849 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
850 out.append([module, type, name, id, source, encode(trans) or ''])
854 def trans_load(cr, filename, lang, verbose=True, context=None):
856 fileobj = misc.file_open(filename)
857 _logger.info("loading %s", filename)
858 fileformat = os.path.splitext(filename)[-1][1:].lower()
859 r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context)
864 _logger.error("couldn't read translation file %s", filename)
867 def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
868 """Populates the ir_translation table."""
870 _logger.info('loading translation file for language %s', lang)
874 pool = pooler.get_pool(db_name)
875 lang_obj = pool.get('res.lang')
876 trans_obj = pool.get('ir.translation')
877 iso_lang = misc.get_iso_codes(lang)
880 ids = lang_obj.search(cr, uid, [('code','=', lang)])
883 # lets create the language with locale information
884 lang_obj.load_lang(cr, 1, lang=lang, lang_name=lang_name)
887 # now, the serious things: we read the language file
889 if fileformat == 'csv':
890 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
891 # read the first line of the file (it contains columns titles)
895 elif fileformat == 'po':
896 reader = TinyPoFile(fileobj)
897 f = ['type', 'name', 'res_id', 'src', 'value']
899 _logger.error('Bad file format: %s', fileformat)
900 raise Exception(_('Bad file format'))
902 # read the rest of the file
904 irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context)
908 # skip empty rows and rows where the translation field (=last fiefd) is empty
909 #if (not row) or (not row[-1]):
912 # dictionary which holds values for this line of the csv file
913 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
914 # 'src': ..., 'value': ...}
917 for i in range(len(f)):
918 if f[i] in ('module',):
922 # This would skip terms that fail to specify a res_id
923 if not dic.get('res_id', False):
926 res_id = dic.pop('res_id')
927 if res_id and isinstance(res_id, (int, long)) \
928 or (isinstance(res_id, basestring) and res_id.isdigit()):
929 dic['res_id'] = int(res_id)
932 tmodel = dic['name'].split(',')[0]
934 tmodule, tname = res_id.split('.', 1)
938 dic['imd_model'] = tmodel
939 dic['imd_module'] = tmodule
940 dic['imd_name'] = tname
944 _logger.warning("Could not decode resource for %s, please fix the po file.",
945 dic['res_id'], exc_info=True)
952 _logger.info("translation file loaded succesfully")
954 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
955 _logger.exception("couldn't read translation file %s", filename)
957 def get_locales(lang=None):
959 lang = locale.getdefaultlocale()[0]
962 lang = _LOCALE2WIN32.get(lang, lang)
965 ln = locale._build_localename((lang, enc))
967 nln = locale.normalize(ln)
971 for x in process('utf8'): yield x
973 prefenc = locale.getpreferredencoding()
975 for x in process(prefenc): yield x
979 'iso-8859-1': 'iso8859-15',
981 }.get(prefenc.lower())
983 for x in process(prefenc): yield x
990 # locale.resetlocale is bugged with some locales.
991 for ln in get_locales():
993 return locale.setlocale(locale.LC_ALL, ln)
997 def load_language(cr, lang):
998 """Loads a translation terms for a language.
999 Used mainly to automate language loading at db initialization.
1001 :param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
1004 pool = pooler.get_pool(cr.dbname)
1005 language_installer = pool.get('base.language.install')
1007 oid = language_installer.create(cr, uid, {'lang': lang})
1008 language_installer.lang_install(cr, uid, [oid], context=None)
1010 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: