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 ##############################################################################
35 from os.path import join
37 from datetime import datetime
38 from lxml import etree
42 from tools.misc import UpdateableStr
45 'af_ZA': 'Afrikaans_South Africa',
46 'sq_AL': 'Albanian_Albania',
47 'ar_SA': 'Arabic_Saudi Arabia',
48 'eu_ES': 'Basque_Spain',
49 'be_BY': 'Belarusian_Belarus',
50 'bs_BA': 'Serbian (Latin)',
51 'bg_BG': 'Bulgarian_Bulgaria',
52 'ca_ES': 'Catalan_Spain',
53 'hr_HR': 'Croatian_Croatia',
54 'zh_CN': 'Chinese_China',
55 'zh_TW': 'Chinese_Taiwan',
56 'cs_CZ': 'Czech_Czech Republic',
57 'da_DK': 'Danish_Denmark',
58 'nl_NL': 'Dutch_Netherlands',
59 'et_EE': 'Estonian_Estonia',
60 'fa_IR': 'Farsi_Iran',
61 'ph_PH': 'Filipino_Philippines',
62 'fi_FI': 'Finnish_Finland',
63 'fr_FR': 'French_France',
64 'fr_BE': 'French_France',
65 'fr_CH': 'French_France',
66 'fr_CA': 'French_France',
67 'ga': 'Scottish Gaelic',
68 'gl_ES': 'Galician_Spain',
69 'ka_GE': 'Georgian_Georgia',
70 'de_DE': 'German_Germany',
71 'el_GR': 'Greek_Greece',
72 'gu': 'Gujarati_India',
73 'he_IL': 'Hebrew_Israel',
75 'hu': 'Hungarian_Hungary',
76 'is_IS': 'Icelandic_Iceland',
77 'id_ID': 'Indonesian_indonesia',
78 'it_IT': 'Italian_Italy',
79 'ja_JP': 'Japanese_Japan',
82 'ko_KR': 'Korean_Korea',
84 'lt_LT': 'Lithuanian_Lithuania',
85 'lat': 'Latvian_Latvia',
86 'ml_IN': 'Malayalam_India',
87 'id_ID': 'Indonesian_indonesia',
89 'mn': 'Cyrillic_Mongolian',
90 'no_NO': 'Norwegian_Norway',
91 'nn_NO': 'Norwegian-Nynorsk_Norway',
92 'pl': 'Polish_Poland',
93 'pt_PT': 'Portuguese_Portugal',
94 'pt_BR': 'Portuguese_Brazil',
95 'ro_RO': 'Romanian_Romania',
96 'ru_RU': 'Russian_Russia',
98 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
99 'sk_SK': 'Slovak_Slovakia',
100 'sl_SI': 'Slovenian_Slovenia',
101 #should find more specific locales for spanish countries,
102 #but better than nothing
103 'es_AR': 'Spanish_Spain',
104 'es_BO': 'Spanish_Spain',
105 'es_CL': 'Spanish_Spain',
106 'es_CO': 'Spanish_Spain',
107 'es_CR': 'Spanish_Spain',
108 'es_DO': 'Spanish_Spain',
109 'es_EC': 'Spanish_Spain',
110 'es_ES': 'Spanish_Spain',
111 'es_GT': 'Spanish_Spain',
112 'es_HN': 'Spanish_Spain',
113 'es_MX': 'Spanish_Spain',
114 'es_NI': 'Spanish_Spain',
115 'es_PA': 'Spanish_Spain',
116 'es_PE': 'Spanish_Spain',
117 'es_PR': 'Spanish_Spain',
118 'es_PY': 'Spanish_Spain',
119 'es_SV': 'Spanish_Spain',
120 'es_UY': 'Spanish_Spain',
121 'es_VE': 'Spanish_Spain',
122 'sv_SE': 'Swedish_Sweden',
123 'ta_IN': 'English_Australia',
124 'th_TH': 'Thai_Thailand',
126 'tr_TR': 'Turkish_Turkey',
127 'uk_UA': 'Ukrainian_Ukraine',
128 'vi_VN': 'Vietnamese_Viet Nam',
129 'tlh_TLH': 'Klingon',
134 class UNIX_LINE_TERMINATOR(csv.excel):
135 lineterminator = '\n'
137 csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
140 # Warning: better use self.pool.get('ir.translation')._get_source if you can
142 def translate(cr, name, source_type, lang, source=None):
144 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))
146 cr.execute('select value from ir_translation where lang=%s and type=%s and name=%s', (lang, source_type, str(name)))
148 cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, source_type, source))
149 res_trans = cr.fetchone()
150 res = res_trans and res_trans[0] or False
153 logger = logging.getLogger('translate')
155 class GettextAlias(object):
158 # find current DB based on thread/worker db name (see netsvc)
159 db_name = getattr(threading.currentThread(), 'dbname', None)
161 return pooler.get_db_only(db_name)
163 def _get_cr(self, frame):
165 cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
167 s = frame.f_locals.get('self', {})
168 cr = getattr(s, 'cr', None)
176 def _get_lang(self, frame):
178 ctx = frame.f_locals.get('context')
180 kwargs = frame.f_locals.get('kwargs')
182 args = frame.f_locals.get('args')
183 if args and isinstance(args, (list, tuple)) \
184 and isinstance(args[-1], dict):
186 elif isinstance(kwargs, dict):
187 ctx = kwargs.get('context')
189 lang = ctx.get('lang')
191 s = frame.f_locals.get('self', {})
192 c = getattr(s, 'localcontext', None)
197 def __call__(self, source):
202 frame = inspect.currentframe()
208 lang = self._get_lang(frame)
210 cr, is_new_cr = self._get_cr(frame)
212 # Try to use ir.translation to benefit from global cache if possible
213 pool = pooler.get_pool(cr.dbname)
214 res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
216 logger.debug('no context cursor detected, skipping translation for "%r"', source)
218 logger.debug('no translation language detected, skipping translation for "%r" ', source)
220 logger.debug('translation went wrong for "%r", skipped', source)
221 # if so, double-check the root/base translations filenames
231 """Returns quoted PO term string, with special PO characters escaped"""
232 assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
233 return '"%s"' % s.replace('\\','\\\\') \
234 .replace('"','\\"') \
235 .replace('\n', '\\n"\n"')
237 re_escaped_char = re.compile(r"(\\.)")
238 re_escaped_replacements = {'n': '\n', }
240 def _sub_replacement(match_obj):
241 return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
244 """Returns unquoted PO term string, with special PO characters unescaped"""
245 return re_escaped_char.sub(_sub_replacement, str[1:-1])
247 # class to handle po files
248 class TinyPoFile(object):
249 def __init__(self, buffer):
250 self.logger = logging.getLogger('i18n')
253 def warn(self, msg, *args):
254 self.logger.warning(msg, *args)
258 self.lines = self._get_lines()
259 self.lines_count = len(self.lines);
265 def _get_lines(self):
266 lines = self.buffer.readlines()
267 # remove the BOM (Byte Order Mark):
269 lines[0] = unicode(lines[0], 'utf8').lstrip(unicode( codecs.BOM_UTF8, "utf8"))
271 lines.append('') # ensure that the file ends with at least an empty line
275 return (self.lines_count - len(self.lines))
278 type = name = res_id = source = trad = None
281 type, name, res_id, source, trad = self.tnrs.pop(0)
289 if 0 == len(self.lines):
290 raise StopIteration()
291 line = self.lines.pop(0).strip()
292 while line.startswith('#'):
293 if line.startswith('#~ '):
295 if line.startswith('#:'):
296 if ' ' in line[2:].strip():
297 for lpart in line[2:].strip().split(' '):
298 tmp_tnrs.append(lpart.strip().split(':',2))
300 tmp_tnrs.append( line[2:].strip().split(':',2) )
301 elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
303 line = self.lines.pop(0).strip()
305 # allow empty lines between comments and msgid
306 line = self.lines.pop(0).strip()
307 if line.startswith('#~ '):
308 while line.startswith('#~ ') or not line.strip():
309 if 0 == len(self.lines):
310 raise StopIteration()
311 line = self.lines.pop(0)
312 # This has been a deprecated entry, don't return anything
315 if not line.startswith('msgid'):
316 raise Exception("malformed file: bad line: %s" % line)
317 source = unquote(line[6:])
318 line = self.lines.pop(0).strip()
319 if not source and self.first:
320 # if the source is "" and it's the first msgid, it's the special
321 # msgstr with the informations about the traduction and the
322 # traductor; we skip it
325 line = self.lines.pop(0).strip()
328 while not line.startswith('msgstr'):
330 raise Exception('malformed file at %d'% self.cur_line())
331 source += unquote(line)
332 line = self.lines.pop(0).strip()
334 trad = unquote(line[7:])
335 line = self.lines.pop(0).strip()
337 trad += unquote(line)
338 line = self.lines.pop(0).strip()
340 if tmp_tnrs and not fuzzy:
341 type, name, res_id = tmp_tnrs.pop(0)
342 for t, n, r in tmp_tnrs:
343 self.tnrs.append((t, n, r, source, trad))
349 self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s',
350 self.cur_line(), source[:30])
352 return type, name, res_id, source, trad
354 def write_infos(self, modules):
356 self.buffer.write("# Translation of %(project)s.\n" \
357 "# This file contains the translation of the following modules:\n" \
362 '''"Project-Id-Version: %(project)s %(version)s\\n"\n''' \
363 '''"Report-Msgid-Bugs-To: %(bugmail)s\\n"\n''' \
364 '''"POT-Creation-Date: %(now)s\\n"\n''' \
365 '''"PO-Revision-Date: %(now)s\\n"\n''' \
366 '''"Last-Translator: <>\\n"\n''' \
367 '''"Language-Team: \\n"\n''' \
368 '''"MIME-Version: 1.0\\n"\n''' \
369 '''"Content-Type: text/plain; charset=UTF-8\\n"\n''' \
370 '''"Content-Transfer-Encoding: \\n"\n''' \
371 '''"Plural-Forms: \\n"\n''' \
374 % { 'project': release.description,
375 'version': release.version,
376 'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
377 'bugmail': release.support_email,
378 'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
382 def write(self, modules, tnrs, source, trad):
384 plurial = len(modules) > 1 and 's' or ''
385 self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
389 for typy, name, res_id in tnrs:
390 self.buffer.write("#: %s:%s:%s\n" % (typy, name, res_id))
395 # only strings in python code are python formated
396 self.buffer.write("#, python-format\n")
398 if not isinstance(trad, unicode):
399 trad = unicode(trad, 'utf8')
400 if not isinstance(source, unicode):
401 source = unicode(source, 'utf8')
405 % (quote(source), quote(trad))
406 self.buffer.write(msg.encode('utf8'))
409 # Methods to export the translation file
411 def trans_export(lang, modules, buffer, format, dbname=None):
413 def _process(format, modules, rows, buffer, lang, newlang):
415 writer=csv.writer(buffer, 'UNIX')
420 writer = tools.TinyPoFile(buffer)
421 writer.write_infos(modules)
423 # we now group the translations by source. That means one translation per source.
425 for module, type, name, res_id, src, trad in rows:
426 row = grouped_rows.setdefault(src, {})
427 row.setdefault('modules', set()).add(module)
428 if ('translation' not in row) or (not row['translation']):
429 row['translation'] = trad
430 row.setdefault('tnrs', []).append((type, name, res_id))
432 for src, row in grouped_rows.items():
433 writer.write(row['modules'], row['tnrs'], src, row['translation'])
435 elif format == 'tgz':
440 # first row is the "header", as in csv, it will be popped
441 rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
442 rows_by_module[module].append(row)
444 tmpdir = tempfile.mkdtemp()
445 for mod, modrows in rows_by_module.items():
446 tmpmoddir = join(tmpdir, mod, 'i18n')
447 os.makedirs(tmpmoddir)
448 pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '')
449 buf = file(join(tmpmoddir, pofilename), 'w')
450 _process('po', [mod], modrows, buf, lang, newlang)
453 tar = tarfile.open(fileobj=buffer, mode='w|gz')
458 raise Exception(_('Bad file format'))
460 newlang = not bool(lang)
463 trans = trans_generate(lang, modules, dbname)
464 if newlang and format!='csv':
467 modules = set([t[0] for t in trans[1:]])
468 _process(format, modules, trans, buffer, lang, newlang)
472 def trans_parse_xsl(de):
476 for m in [j for j in n if j.text]:
477 l = m.text.strip().replace('\n',' ')
479 res.append(l.encode("utf8"))
480 res.extend(trans_parse_xsl(n))
483 def trans_parse_rml(de):
486 for m in [j for j in n if j.text]:
487 string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
488 for s in string_list:
490 res.append(s.encode("utf8"))
491 res.extend(trans_parse_rml(n))
494 def trans_parse_view(de):
496 if de.tag == 'attribute' and de.get("name") == 'string':
498 res.append(de.text.encode("utf8"))
500 res.append(de.get('string').encode("utf8"))
502 res.append(de.get('sum').encode("utf8"))
503 if de.get("confirm"):
504 res.append(de.get('confirm').encode("utf8"))
506 res.extend(trans_parse_view(n))
509 # tests whether an object is in a list of modules
510 def in_modules(object_name, modules):
519 module = object_name.split('.')[0]
520 module = module_dict.get(module, module)
521 return module in modules
523 def trans_generate(lang, modules, dbname=None):
524 logger = logging.getLogger('i18n')
526 dbname=tools.config['db_name']
530 pool = pooler.get_pool(dbname)
531 trans_obj = pool.get('ir.translation')
532 model_data_obj = pool.get('ir.model.data')
533 cr = pooler.get_db(dbname).cursor()
535 l = pool.obj_pool.items()
538 query = 'SELECT name, model, res_id, module' \
539 ' FROM ir_model_data'
541 query_models = """SELECT m.id, m.model, imd.module
542 FROM ir_model AS m, ir_model_data AS imd
543 WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
545 if 'all_installed' in modules:
546 query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
547 query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
549 if 'all' not in modules:
550 query += ' WHERE module IN %s'
551 query_models += ' AND imd.module in %s'
552 query_param = (tuple(modules),)
553 query += ' ORDER BY module, model, name'
554 query_models += ' ORDER BY module, model'
556 cr.execute(query, query_param)
559 def push_translation(module, type, name, id, source):
560 tuple = (module, source, name, id, type)
561 if source and tuple not in _to_translate:
562 _to_translate.append(tuple)
565 if isinstance(s, unicode):
566 return s.encode('utf8')
569 for (xml_name,model,res_id,module) in cr.fetchall():
570 module = encode(module)
571 model = encode(model)
572 xml_name = "%s.%s" % (module, encode(xml_name))
574 if not pool.get(model):
575 logger.error("Unable to find object %r", model)
578 exists = pool.get(model).exists(cr, uid, res_id)
580 logger.warning("Unable to find object %r with id %d", model, res_id)
582 obj = pool.get(model).browse(cr, uid, res_id)
584 if model=='ir.ui.view':
585 d = etree.XML(encode(obj.arch))
586 for t in trans_parse_view(d):
587 push_translation(module, 'view', encode(obj.model), 0, t)
588 elif model=='ir.actions.wizard':
589 service_name = 'wizard.'+encode(obj.wiz_name)
590 if netsvc.Service._services.get(service_name):
591 obj2 = netsvc.Service._services[service_name]
592 for state_name, state_def in obj2.states.iteritems():
593 if 'result' in state_def:
594 result = state_def['result']
595 if result['type'] != 'form':
597 name = "%s,%s" % (encode(obj.wiz_name), state_name)
600 'string': ('wizard_field', lambda s: [encode(s)]),
601 'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
602 'help': ('help', lambda s: [encode(s)]),
606 if not result.has_key('fields'):
607 logger.warning("res has no fields: %r", result)
609 for field_name, field_def in result['fields'].iteritems():
610 res_name = name + ',' + field_name
612 for fn in def_params:
614 transtype, modifier = def_params[fn]
615 for val in modifier(field_def[fn]):
616 push_translation(module, transtype, res_name, 0, val)
619 arch = result['arch']
620 if arch and not isinstance(arch, UpdateableStr):
622 for t in trans_parse_view(d):
623 push_translation(module, 'wizard_view', name, 0, t)
625 # export button labels
626 for but_args in result['state']:
627 button_name = but_args[0]
628 button_label = but_args[1]
629 res_name = name + ',' + button_name
630 push_translation(module, 'wizard_button', res_name, 0, button_label)
632 elif model=='ir.model.fields':
634 field_name = encode(obj.name)
635 except AttributeError, exc:
636 logger.error("name error in %s: %s", xml_name, str(exc))
638 objmodel = pool.get(obj.model)
639 if not objmodel or not field_name in objmodel._columns:
641 field_def = objmodel._columns[field_name]
643 name = "%s,%s" % (encode(obj.model), field_name)
644 push_translation(module, 'field', name, 0, encode(field_def.string))
647 push_translation(module, 'help', name, 0, encode(field_def.help))
649 if field_def.translate:
650 ids = objmodel.search(cr, uid, [])
651 obj_values = objmodel.read(cr, uid, ids, [field_name])
652 for obj_value in obj_values:
653 res_id = obj_value['id']
654 if obj.name in ('ir.model', 'ir.ui.menu'):
656 model_data_ids = model_data_obj.search(cr, uid, [
657 ('model', '=', model),
658 ('res_id', '=', res_id),
660 if not model_data_ids:
661 push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
663 if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
664 for dummy, val in field_def.selection:
665 push_translation(module, 'selection', name, 0, encode(val))
667 elif model=='ir.actions.report.xml':
668 name = encode(obj.report_name)
671 fname = obj.report_rml
672 parse_func = trans_parse_rml
673 report_type = "report"
675 fname = obj.report_xsl
676 parse_func = trans_parse_xsl
678 if fname and obj.report_type in ('pdf', 'xsl'):
680 d = etree.parse(tools.file_open(fname))
681 for t in parse_func(d.iter()):
682 push_translation(module, report_type, name, 0, t)
683 except (IOError, etree.XMLSyntaxError):
684 logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
686 for field_name,field_def in obj._table._columns.items():
687 if field_def.translate:
688 name = model + "," + field_name
690 trad = getattr(obj, field_name) or ''
693 push_translation(module, 'model', name, xml_name, encode(trad))
695 # End of data for ir.model.data query results
697 cr.execute(query_models, query_param)
699 def push_constraint_msg(module, term_type, model, msg):
700 # Check presence of __call__ directly instead of using
701 # callable() because it will be deprecated as of Python 3.0
702 if not hasattr(msg, '__call__'):
703 push_translation(module, term_type, model, 0, encode(msg))
705 for (model_id, model, module) in cr.fetchall():
706 module = encode(module)
707 model = encode(model)
709 model_obj = pool.get(model)
712 logging.getLogger("i18n").error("Unable to find object %r", model)
715 for constraint in getattr(model_obj, '_constraints', []):
716 push_constraint_msg(module, 'constraint', model, constraint[1])
718 for constraint in getattr(model_obj, '_sql_constraints', []):
719 push_constraint_msg(module, 'sql_constraint', model, constraint[2])
721 # parse source code for _() calls
722 def get_module_from_path(path, mod_paths=None):
724 # First, construct a list of possible paths
725 def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
726 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
729 mod_paths.append(adp)
730 if not os.path.isabs(adp):
731 mod_paths.append(adp)
732 elif adp.startswith(def_path):
733 mod_paths.append(adp[len(def_path)+1:])
735 if path.startswith(mp) and (os.path.dirname(path) != mp):
736 path = path[len(mp)+1:]
737 return path.split(os.path.sep)[0]
738 return 'base' # files that are not in a module are considered as being in 'base' module
740 modobj = pool.get('ir.module.module')
741 installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
742 installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
744 root_path = os.path.join(tools.config['root_path'], 'addons')
746 apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
747 if root_path in apaths:
750 path_list = [root_path,] + apaths
752 # Also scan these non-addon paths
753 for bin_path in ['osv', 'report' ]:
754 path_list.append(os.path.join(tools.config['root_path'], bin_path))
756 logger.debug("Scanning modules at paths: ", path_list)
759 join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
760 join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
761 re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
762 re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
764 def export_code_terms_from_file(fname, path, root, terms_type):
765 fabsolutepath = join(root, fname)
766 frelativepath = fabsolutepath[len(path):]
767 module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
768 is_mod_installed = module in installed_modules
769 if (('all' in modules) or (module in modules)) and is_mod_installed:
770 logger.debug("Scanning code of %s at module: %s", frelativepath, module)
771 code_string = tools.file_open(fabsolutepath, subdir='').read()
772 if module in installed_modules:
773 frelativepath = str("addons" + frelativepath)
774 ite = re_dquotes.finditer(code_string)
779 if src.startswith('""'):
780 assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
783 src = join_dquotes.sub(r'\1', src)
784 # try to count the lines from the last pos to our place:
785 code_line += code_string[code_offset:i.start(1)].count('\n')
786 # now, since we did a binary read of a python source file, we
787 # have to expand pythonic escapes like the interpreter does.
788 src = src.decode('string_escape')
789 push_translation(module, terms_type, frelativepath, code_line, encode(src))
790 code_line += i.group(1).count('\n')
791 code_offset = i.end() # we have counted newlines up to the match end
793 ite = re_quotes.finditer(code_string)
794 code_offset = 0 #reset counters
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_quotes.sub(r'\1', src)
803 code_line += code_string[code_offset:i.start(1)].count('\n')
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 for path in path_list:
810 logger.debug("Scanning files of modules at %s", path)
811 for root, dummy, files in tools.osutil.walksymlinks(path):
812 for fname in itertools.chain(fnmatch.filter(files, '*.py')):
813 export_code_terms_from_file(fname, path, root, 'code')
814 for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
815 export_code_terms_from_file(fname, path, root, 'report')
818 out = [["module","type","name","res_id","src","value"]] # header
820 # translate strings marked as to be translated
821 for module, source, name, id, type in _to_translate:
822 trans = trans_obj._get_source(cr, uid, name, type, lang, source)
823 out.append([module, type, name, id, source, encode(trans) or ''])
828 def trans_load(db_name, filename, lang, verbose=True, context=None):
829 logger = logging.getLogger('i18n')
831 fileobj = open(filename,'r')
832 logger.info("loading %s", filename)
833 fileformat = os.path.splitext(filename)[-1][1:].lower()
834 r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
839 logger.error("couldn't read translation file %s", filename)
842 def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
843 logger = logging.getLogger('i18n')
845 logger.info('loading translation file for language %s', lang)
848 pool = pooler.get_pool(db_name)
849 lang_obj = pool.get('res.lang')
850 trans_obj = pool.get('ir.translation')
851 model_data_obj = pool.get('ir.model.data')
852 iso_lang = tools.get_iso_codes(lang)
855 cr = pooler.get_db(db_name).cursor()
856 ids = lang_obj.search(cr, uid, [('code','=', lang)])
859 # lets create the language with locale information
861 for ln in get_locales(lang):
863 locale.setlocale(locale.LC_ALL, str(ln))
869 lc = locale.getdefaultlocale()[0]
870 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
871 logger.warning(msg, lang, lc)
874 lang_name = tools.get_languages().get(lang, lang)
883 'iso_code': iso_lang,
886 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
887 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
888 'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
889 'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
893 lang_obj.create(cr, uid, lang_info)
898 # now, the serious things: we read the language file
900 if fileformat == 'csv':
901 reader = csv.reader(fileobj, quotechar='"', delimiter=',')
902 # read the first line of the file (it contains columns titles)
906 elif fileformat == 'po':
907 reader = TinyPoFile(fileobj)
908 f = ['type', 'name', 'res_id', 'src', 'value']
910 logger.error('Bad file format: %s', fileformat)
911 raise Exception(_('Bad file format'))
913 # read the rest of the file
917 # skip empty rows and rows where the translation field (=last fiefd) is empty
918 #if (not row) or (not row[-1]):
921 # dictionary which holds values for this line of the csv file
922 # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ...,
923 # 'src': ..., 'value': ...}
925 for i in range(len(f)):
926 if f[i] in ('module',):
931 dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
933 model_data_ids = model_data_obj.search(cr, uid, [
934 ('model', '=', dic['name'].split(',')[0]),
935 ('module', '=', dic['res_id'].split('.', 1)[0]),
936 ('name', '=', dic['res_id'].split('.', 1)[1]),
939 dic['res_id'] = model_data_obj.browse(cr, uid,
940 model_data_ids[0]).res_id
942 dic['res_id'] = False
946 ('type', '=', dic['type']),
947 ('name', '=', dic['name']),
948 ('src', '=', dic['src']),
950 if dic['type'] == 'model':
951 args.append(('res_id', '=', dic['res_id']))
952 ids = trans_obj.search(cr, uid, args)
954 if context.get('overwrite'):
955 trans_obj.write(cr, uid, ids, {'value': dic['value']})
957 trans_obj.create(cr, uid, dic)
961 logger.info("translation file loaded succesfully")
963 filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
964 logger.exception("couldn't read translation file %s", filename)
966 def get_locales(lang=None):
968 lang = locale.getdefaultlocale()[0]
971 lang = _LOCALE2WIN32.get(lang, lang)
974 ln = locale._build_localename((lang, enc))
976 nln = locale.normalize(ln)
980 for x in process('utf8'): yield x
982 prefenc = locale.getpreferredencoding()
984 for x in process(prefenc): yield x
988 'iso-8859-1': 'iso8859-15',
990 }.get(prefenc.lower())
992 for x in process(prefenc): yield x
999 # locale.resetlocale is bugged with some locales.
1000 for ln in get_locales():
1002 return locale.setlocale(locale.LC_ALL, ln)
1003 except locale.Error:
1006 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: