# HG changeset patch # User cmlenz # Date 1184344307 0 # Node ID aa8e85a4085eb2c27451c72fb8bee6f66b9adc25 # Parent b3b07279b86c10e3fe8ee0e2335dfdfa4e33fa8b * The I18n extractor now handles gettext function calls that use non-string parameters as well as keyword arguments. * Expression and Suite objects now retain the AST information so that it can be used by the I18n message extraction. * The AST transformation no longer modifies the AST in place, but instead returns a transformed copy of the AST node. diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py --- a/genshi/filters/i18n.py +++ b/genshi/filters/i18n.py @@ -13,12 +13,12 @@ """Utilities for internationalization and localization of templates.""" +from compiler import ast try: frozenset except NameError: from sets import ImmutableSet as frozenset from gettext import gettext -from opcode import opmap import re from genshi.core import Attrs, Namespace, QName, START, END, TEXT, START_NS, \ @@ -29,11 +29,6 @@ __all__ = ['Translator', 'extract'] __docformat__ = 'restructuredtext en' -_LOAD_NAME = chr(opmap['LOAD_NAME']) -_LOAD_CONST = chr(opmap['LOAD_CONST']) -_CALL_FUNCTION = chr(opmap['CALL_FUNCTION']) -_BINARY_ADD = chr(opmap['BINARY_ADD']) - I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n') @@ -246,7 +241,7 @@ 3, None, u'Example' 6, None, u'Example' 7, '_', u'Hello, %(name)s' - 8, 'ngettext', (u'You have %d item', u'You have %d items') + 8, 'ngettext', (u'You have %d item', u'You have %d items', None) :param stream: the event stream to extract strings from; can be a regular stream or a template stream @@ -312,7 +307,7 @@ msgbuf = None elif kind is EXPR or kind is EXEC: - for funcname, strings in extract_from_code(data.code, + for funcname, strings in extract_from_code(data, gettext_functions): yield pos[1], funcname, strings @@ -379,43 +374,38 @@ >>> from genshi.template.eval import Expression >>> expr = Expression('_("Hello")') - >>> list(extract_from_code(expr.code, Translator.GETTEXT_FUNCTIONS)) + >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) [('_', u'Hello')] >>> expr = Expression('ngettext("You have %(num)s item", ' ... '"You have %(num)s items", num)') - >>> list(extract_from_code(expr.code, Translator.GETTEXT_FUNCTIONS)) - [('ngettext', (u'You have %(num)s item', u'You have %(num)s items'))] + >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) + [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))] - :param code: the code object + :param code: the `Code` object + :type code: `genshi.template.eval.Code` :param gettext_functions: a sequence of function names """ - consts = dict([(n, chr(i) + '\x00') for i, n in enumerate(code.co_consts)]) - gettext_locs = [consts[n] for n in gettext_functions if n in consts] - ops = [ - _LOAD_CONST, '(', '|'.join(gettext_locs), ')', - _CALL_FUNCTION, '.\x00', - '((?:', _BINARY_ADD, '|', _LOAD_CONST, '.\x00)+)' - ] - for loc, opcodes in re.findall(''.join(ops), code.co_code): - funcname = code.co_consts[ord(loc[0])] - strings = [] - opcodes = iter(opcodes) - for opcode in opcodes: - if opcode == _BINARY_ADD: - arg = strings.pop() - strings[-1] += arg + def _walk(node): + if isinstance(node, ast.CallFunc) and isinstance(node.node, ast.Name) \ + and node.node.name in gettext_functions: + strings = [] + for arg in node.args: + if isinstance(arg, ast.Const) \ + and isinstance(arg.value, basestring): + strings.append(unicode(arg.value)) + elif not isinstance(arg, ast.Keyword): + strings.append(None) + if len(strings) == 1: + strings = strings[0] else: - arg = code.co_consts[ord(opcodes.next())] - opcodes.next() # skip second byte - if not isinstance(arg, basestring): - break - strings.append(unicode(arg)) - if len(strings) == 1: - strings = strings[0] + strings = tuple(strings) + yield node.node.name, strings else: - strings = tuple(strings) - yield funcname, strings + for child in node.getChildNodes(): + for funcname, strings in _walk(child): + yield funcname, strings + return _walk(code.ast) def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')): """Parse a message using Genshi compound message formatting. diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py --- a/genshi/filters/tests/i18n.py +++ b/genshi/filters/tests/i18n.py @@ -28,7 +28,8 @@ translator = Translator() messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) - self.assertEqual((2, 'ngettext', (u'Singular', u'Plural')), messages[0]) + self.assertEqual((2, 'ngettext', (u'Singular', u'Plural', None)), + messages[0]) def test_extract_included_attribute_text(self): tmpl = MarkupTemplate(""" @@ -263,7 +264,8 @@ (3, None, u'Example', []), (6, None, u'Example', []), (7, '_', u'Hello, %(name)s', []), - (8, 'ngettext', (u'You have %d item', u'You have %d items'), []), + (8, 'ngettext', (u'You have %d item', u'You have %d items', None), + []), ], results) def test_text_template_extraction(self): @@ -281,10 +283,28 @@ })) self.assertEqual([ (1, '_', u'Dear %(name)s', []), - (3, 'ngettext', (u'Your item:', u'Your items'), []), + (3, 'ngettext', (u'Your item:', u'Your items', None), []), (7, None, u'All the best,\n Foobar', []) ], results) + def test_extraction_with_keyword_arg(self): + buf = StringIO(""" + ${gettext('Foobar', foo='bar')} + """) + results = list(extract(buf, ['gettext'], [], {})) + self.assertEqual([ + (2, 'gettext', (u'Foobar'), []), + ], results) + + def test_extraction_with_nonstring_arg(self): + buf = StringIO(""" + ${dgettext(curdomain, 'Foobar')} + """) + results = list(extract(buf, ['dgettext'], [], {})) + self.assertEqual([ + (2, 'dgettext', (None, u'Foobar'), []), + ], results) + def test_extraction_inside_ignored_tags(self): buf = StringIO("""