# HG changeset patch # User cmlenz # Date 1257886446 0 # Node ID 878c4313c7d5e1b7fa2d38c1778c24d143b90456 # Parent c1d317c0df68e12623a7dab957bf28418908c9e3 Merged advanced-i18n branch back into trunk. diff --git a/genshi/filters/__init__.py b/genshi/filters/__init__.py --- a/genshi/filters/__init__.py +++ b/genshi/filters/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2007-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py --- a/genshi/filters/i18n.py +++ b/genshi/filters/i18n.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2007-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -11,71 +11,454 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://genshi.edgewall.org/log/. -"""Utilities for internationalization and localization of templates. +"""Directives and utilities for internationalization and localization of +templates. :since: version 0.4 +:note: Directives support added since version 0.6 """ from gettext import NullTranslations +import os import re from types import FunctionType from genshi.core import Attrs, Namespace, QName, START, END, TEXT, START_NS, \ - END_NS, XML_NAMESPACE, _ensure + END_NS, XML_NAMESPACE, _ensure, StreamEventKind from genshi.template.eval import _ast from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives -from genshi.template.directives import Directive +from genshi.template.directives import Directive, StripDirective from genshi.template.markup import MarkupTemplate, EXEC __all__ = ['Translator', 'extract'] __docformat__ = 'restructuredtext en' + I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n') +MSGBUF = StreamEventKind('MSGBUF') +SUB_START = StreamEventKind('SUB_START') +SUB_END = StreamEventKind('SUB_END') + -class CommentDirective(Directive): +class I18NDirective(Directive): + """Simple interface for i18n directives to support messages extraction.""" - __slots__ = [] - - @classmethod - def attach(cls, template, stream, value, namespaces, pos): - return None, stream + def __call__(self, stream, directives, ctxt, **vars): + return _apply_directives(stream, directives, ctxt, vars) -class MsgDirective(Directive): +class ExtractableI18NDirective(I18NDirective): + """Simple interface for directives to support messages extraction.""" + def extract(self, stream, comment_stack): + raise NotImplementedError + + +class CommentDirective(I18NDirective): + """Implementation of the ``i18n:comment`` template directive which adds + translation comments. + + >>> tmpl = MarkupTemplate(''' + ...
Foo
+ ... ''') + >>> translator = Translator() + >>> translator.setup(tmpl) + >>> list(translator.extract(tmpl.stream)) + [(2, None, u'Foo', [u'As in Foo Bar'])] + """ + __slots__ = ['comment'] + + def __init__(self, value, template, hints=None, namespaces=None, + lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.comment = value + + +class MsgDirective(ExtractableI18NDirective): + r"""Implementation of the ``i18n:msg`` directive which marks inner content + as translatable. Consider the following examples: + + >>> tmpl = MarkupTemplate(''' + ...Foo
+ ...Bar
+ ...Foo bar!
+ ... ''') + + >>> translator = Translator() + >>> translator.setup(tmpl) + >>> list(translator.extract(tmpl.stream)) + [(2, None, u'[1:Foo]\n [2:Bar]', []), (6, None, u'Foo [1:bar]!', [])] + >>> print tmpl.generate().render() + +Foo
+Bar
Foo bar!
+ + + >>> tmpl = MarkupTemplate(''' + ...First Name: ${fname}
+ ...Last Name: ${lname}
+ ...Foo bar!
+ ... ''') + >>> translator.setup(tmpl) + >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE + [(2, None, u'[1:First Name: %(fname)s]\n [2:Last Name: %(lname)s]', []), + (6, None, u'Foo [1:bar]!', [])] + + >>> tmpl = MarkupTemplate(''' + ...First Name: ${fname}
+ ...Last Name: ${lname}
+ ...Foo bar!
+ ... ''') + >>> translator.setup(tmpl) + >>> print tmpl.generate(fname='John', lname='Doe').render() + +First Name: John
+Last Name: Doe
Foo bar!
+ + + Starting and ending white-space is stripped of to make it simpler for + translators. Stripping it is not that important since it's on the html + source, the rendered output will remain the same. + """ __slots__ = ['params'] def __init__(self, value, template, hints=None, namespaces=None, lineno=-1, offset=-1): Directive.__init__(self, None, template, namespaces, lineno, offset) - self.params = [name.strip() for name in value.split(',')] + self.params = [param.strip() for param in value.split(',') if param] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('params', '').strip() + return super(MsgDirective, cls).attach(template, stream, value.strip(), + namespaces, pos) def __call__(self, stream, directives, ctxt, **vars): - msgbuf = MessageBuffer(self.params) + gettext = ctxt.get('_i18n.gettext') + dgettext = ctxt.get('_i18n.dgettext') + if ctxt.get('_i18n.domain'): + assert callable(dgettext), "No domain gettext function passed" + gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg) + + def _generate(): + msgbuf = MessageBuffer(self) + previous = stream.next() + if previous[0] is START: + yield previous + else: + msgbuf.append(*previous) + previous = stream.next() + for kind, data, pos in stream: + msgbuf.append(*previous) + previous = kind, data, pos + if previous[0] is not END: + msgbuf.append(*previous) + previous = None + for event in msgbuf.translate(gettext(msgbuf.format())): + yield event + if previous: + yield previous + + return _apply_directives(_generate(), directives, ctxt, vars) + + def extract(self, stream, comment_stack): + msgbuf = MessageBuffer(self) stream = iter(stream) - yield stream.next() # the outer start tag previous = stream.next() + if previous[0] is START: + previous = stream.next() for event in stream: msgbuf.append(*previous) previous = event + if previous[0] is not END: + msgbuf.append(*previous) - gettext = ctxt.get('_i18n.gettext') - for event in msgbuf.translate(gettext(msgbuf.format())): + yield None, msgbuf.format(), comment_stack[-1:] + + +class ChooseBranchDirective(I18NDirective): + __slots__ = ['params'] + + def __call__(self, stream, directives, ctxt, **vars): + self.params = ctxt.get('_i18n.choose.params', [])[:] + msgbuf = MessageBuffer(self) + + stream = iter(_apply_directives(stream, directives, ctxt, vars)) + yield stream.next() # the outer start tag + previous = stream.next() + for kind, data, pos in stream: + msgbuf.append(*previous) + previous = kind, data, pos + yield MSGBUF, (), -1 # the place holder for msgbuf output + yield previous # the outer end tag + ctxt['_i18n.choose.%s' % type(self).__name__] = msgbuf + + + def extract(self, stream, comment_stack, msgbuf): + stream = iter(stream) + previous = stream.next() + if previous[0] is START: + previous = stream.next() + for event in stream: + msgbuf.append(*previous) + previous = event + if previous[0] is not END: + msgbuf.append(*previous) + return msgbuf + + +class SingularDirective(ChooseBranchDirective): + """Implementation of the ``i18n:singular`` directive to be used with the + ``i18n:choose`` directive.""" + + +class PluralDirective(ChooseBranchDirective): + """Implementation of the ``i18n:plural`` directive to be used with the + ``i18n:choose`` directive.""" + + +class ChooseDirective(ExtractableI18NDirective): + """Implementation of the ``i18n:choose`` directive which provides plural + internationalisation of strings. + + This directive requires at least one parameter, the one which evaluates to + an integer which will allow to choose the plural/singular form. If you also + have expressions inside the singular and plural version of the string you + also need to pass a name for those parameters. Consider the following + examples: + + >>> tmpl = MarkupTemplate('''\ + + ...There is $num coin
+ ...There are $num coins
+ ...There is $num coin
+ ...There are $num coins
+ ...There is 1 coin
+There are 2 coins
+There is $num coin
+ ...There are $num coins
+ ...Bar
+ ...FooBar
+ ...Bar
+ ...Bar
+ ...Bar
+ ...Bar
+ ... ''') + + >>> translations = DummyTranslations({'Bar': 'Voh'}) + >>> translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'}) + >>> translations.add_domain('bar', {'Bar': 'bar_Bar'}) + >>> translator = Translator(translations) + >>> translator.setup(tmpl) + + >>> print tmpl.generate().render() + +Voh
+BarFoo
+foo_Bar
+bar_Bar
+Voh
+Voh
+ + """ + __slots__ = ['domain'] + + def __init__(self, value, template, hints=None, namespaces=None, + lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.domain = value and value.strip() or '__DEFAULT__' + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('name') + return super(DomainDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + ctxt.push({'_i18n.domain': self.domain}) + for event in _apply_directives(stream, directives, ctxt, vars): yield event - - yield previous # the outer end tag + ctxt.pop() class Translator(DirectiveFactory): """Can extract and translate localizable strings from markup streams and templates. - For example, assume the followng template: + For example, assume the following template: - >>> from genshi.template import MarkupTemplate - >>> >>> tmpl = MarkupTemplate(''' ... ...Hallo, Hans
- + Note that elements defining ``xml:lang`` attributes that do not contain variable expressions are ignored by this filter. That can be used to exclude specific parts of a template from being extracted and translated. """ directives = [ + ('domain', DomainDirective), ('comment', CommentDirective), - ('msg', MsgDirective) + ('msg', MsgDirective), + ('choose', ChooseDirective), + ('singular', SingularDirective), + ('plural', PluralDirective) ] IGNORE_TAGS = frozenset([ QName('script'), QName('http://www.w3.org/1999/xhtml}script'), QName('style'), QName('http://www.w3.org/1999/xhtml}style') ]) - INCLUDE_ATTRS = frozenset(['abbr', 'alt', 'label', 'prompt', 'standby', - 'summary', 'title']) + INCLUDE_ATTRS = frozenset([ + 'abbr', 'alt', 'label', 'prompt', 'standby', 'summary', 'title' + ]) NAMESPACE = I18N_NAMESPACE def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS, @@ -145,7 +532,7 @@ :param extract_text: whether the content of text nodes should be extracted, or only text in explicit ``gettext`` function calls - + :note: Changed in 0.6: the `translate` parameter can now be either a ``gettext``-style function, or an object compatible with the ``NullTransalations`` or ``GNUTranslations`` interface @@ -177,15 +564,36 @@ if type(self.translate) is FunctionType: gettext = self.translate + if ctxt: + ctxt['_i18n.gettext'] = gettext else: gettext = self.translate.ugettext - if ctxt: - ctxt['_i18n.gettext'] = gettext + try: + dgettext = self.translate.dugettext + except AttributeError: + dgettext = lambda x, y: gettext(y) + ngettext = self.translate.ungettext + try: + dngettext = self.translate.dungettext + except AttributeError: + dngettext = lambda d, s, p, n: ngettext(s, p, n) + + if ctxt: + ctxt['_i18n.gettext'] = gettext + ctxt['_i18n.ugettext'] = gettext + ctxt['_i18n.dgettext'] = dgettext + ctxt['_i18n.ngettext'] = ngettext + ctxt['_i18n.ungettext'] = ngettext + ctxt['_i18n.dngettext'] = dngettext extract_text = self.extract_text if not extract_text: search_text = False + if ctxt and ctxt.get('_i18n.domain'): + old_gettext = gettext + gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg) + for kind, data, pos in stream: # skip chunks that should not be localized @@ -208,14 +616,15 @@ new_attrs = [] changed = False + for name, value in attrs: newval = value if extract_text and isinstance(value, basestring): if name in include_attrs: newval = gettext(value) else: - newval = list(self(_ensure(value), ctxt, - search_text=False) + newval = list( + self(_ensure(value), ctxt, search_text=False) ) if newval != value: value = newval @@ -234,14 +643,28 @@ elif kind is SUB: directives, substream = data - # If this is an i18n:msg directive, no need to translate text + current_domain = None + for idx, directive in enumerate(directives): + # Organize directives to make everything work + if isinstance(directive, DomainDirective): + # Grab current domain and update context + current_domain = directive.domain + ctxt.push({'_i18n.domain': current_domain}) + # Put domain directive as the first one in order to + # update context before any other directives evaluation + directives.insert(0, directives.pop(idx)) + + # If this is an i18n directive, no need to translate text # nodes here - is_msg = filter(None, [isinstance(d, MsgDirective) - for d in directives]) + is_i18n_directive = filter(None, + [isinstance(d, ExtractableI18NDirective) + for d in directives]) substream = list(self(substream, ctxt, - search_text=not is_msg)) + search_text=not is_i18n_directive)) yield kind, (directives, substream), pos + if current_domain: + ctxt.pop() else: yield kind, data, pos @@ -249,7 +672,7 @@ 'ugettext', 'ungettext') def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS, - search_text=True, msgbuf=None): + search_text=True, msgbuf=None, comment_stack=None): """Extract localizable strings from the given template stream. For every string found, this function yields a ``(lineno, function, @@ -264,8 +687,6 @@ * ``comments`` is a list of comments related to the message, extracted from ``i18n:comment`` attributes found in the markup - >>> from genshi.template import MarkupTemplate - >>> >>> tmpl = MarkupTemplate(''' ... ...${ngettext("You have %d item", "You have %d items", num)}
... ... ''', filename='example.html') - >>> >>> for line, func, msg, comments in Translator().extract(tmpl.stream): ... print "%d, %r, %r" % (line, func, msg) 3, None, u'Example' @@ -295,18 +715,19 @@ :note: Changed in 0.4.1: For a function with multiple string arguments (such as ``ngettext``), a single item with a tuple of strings is yielded, instead an item for each string argument. - :note: Changed in 0.6: The returned tuples now include a 4th element, - which is a list of comments for the translator + :note: Changed in 0.6: The returned tuples now include a fourth + element, which is a list of comments for the translator. """ if not self.extract_text: search_text = False + if comment_stack is None: + comment_stack = [] skip = 0 - i18n_comment = I18N_NAMESPACE['comment'] - i18n_msg = I18N_NAMESPACE['msg'] + + # Un-comment bellow to extract messages without adding directives xml_lang = XML_NAMESPACE['lang'] for kind, data, pos in stream: - if skip: if kind is START: skip += 1 @@ -326,6 +747,7 @@ if name in self.include_attrs: text = value.strip() if text: + # XXX: Do we need to grab i18n:comment from comment_stack ??? yield pos[1], None, text, [] else: for lineno, funcname, text, comments in self.extract( @@ -335,20 +757,12 @@ if msgbuf: msgbuf.append(kind, data, pos) - else: - msg_params = attrs.get(i18n_msg) - if msg_params is not None: - if type(msg_params) is list: # event tuple - msg_params = msg_params[0][1] - msgbuf = MessageBuffer( - msg_params, attrs.get(i18n_comment), pos[1] - ) elif not skip and search_text and kind is TEXT: if not msgbuf: text = data.strip() if text and filter(None, [ch.isalpha() for ch in text]): - yield pos[1], None, text, [] + yield pos[1], None, text, comment_stack[-1:] else: msgbuf.append(kind, data, pos) @@ -356,7 +770,7 @@ msgbuf.append(kind, data, pos) if not msgbuf.depth: yield msgbuf.lineno, None, msgbuf.format(), \ - filter(None, [msgbuf.comment]) + filter(None, [msgbuf.comment]) msgbuf = None elif kind is EXPR or kind is EXEC: @@ -364,15 +778,73 @@ msgbuf.append(kind, data, pos) for funcname, strings in extract_from_code(data, gettext_functions): + # XXX: Do we need to grab i18n:comment from comment_stack ??? yield pos[1], funcname, strings, [] elif kind is SUB: - subkind, substream = data - messages = self.extract(substream, gettext_functions, - search_text=search_text and not skip, - msgbuf=msgbuf) - for lineno, funcname, text, comments in messages: - yield lineno, funcname, text, comments + directives, substream = data + in_comment = False + + for idx, directive in enumerate(directives): + # Do a first loop to see if there's a comment directive + # If there is update context and pop it from directives + if isinstance(directive, CommentDirective): + in_comment = True + comment_stack.append(directive.comment) + if len(directives) == 1: + # in case we're in the presence of something like: + #Foo
+ messages = self.extract( + substream, gettext_functions, + search_text=search_text and not skip, + msgbuf=msgbuf, comment_stack=comment_stack) + for lineno, funcname, text, comments in messages: + yield lineno, funcname, text, comments + directives.pop(idx) + elif not isinstance(directive, I18NDirective): + # Remove all other non i18n directives from the process + directives.pop(idx) + + if not directives and not in_comment: + # Extract content if there's no directives because + # strip was pop'ed and not because comment was pop'ed. + # Extraction in this case has been taken care of. + messages = self.extract( + substream, gettext_functions, + search_text=search_text and not skip, msgbuf=msgbuf) + for lineno, funcname, text, comments in messages: + yield lineno, funcname, text, comments + + for directive in directives: + if isinstance(directive, ExtractableI18NDirective): + messages = directive.extract(substream, comment_stack) + for funcname, text, comments in messages: + yield pos[1], funcname, text, comments + else: + messages = self.extract( + substream, gettext_functions, + search_text=search_text and not skip, msgbuf=msgbuf) + for lineno, funcname, text, comments in messages: + yield lineno, funcname, text, comments + + if in_comment: + comment_stack.pop() + + def get_directive_index(self, dir_cls): + total = len(self._dir_order) + if dir_cls in self._dir_order: + return self._dir_order.index(dir_cls) - total + return total + + def setup(self, template): + """Convenience function to register the `Translator` filter and the + related directives with the given template. + + :param template: a `Template` instance + """ + template.filters.insert(0, self) + if hasattr(template, 'add_directives'): + template.add_directives(Translator.NAMESPACE, self) class MessageBuffer(object): @@ -381,7 +853,7 @@ :since: version 0.5 """ - def __init__(self, params=u'', comment=None, lineno=-1): + def __init__(self, directive=None): """Initialize the message buffer. :param params: comma-separated list of parameter names @@ -389,17 +861,17 @@ :param lineno: the line number on which the first stream event belonging to the message was found """ - if isinstance(params, basestring): - params = [name.strip() for name in params.split(',')] - self.params = params - self.comment = comment - self.lineno = lineno + # params list needs to be copied so that directives can be evaluated + # more than once + self.orig_params = self.params = directive.params[:] + self.directive = directive self.string = [] self.events = {} self.values = {} self.depth = 1 self.order = 1 self.stack = [0] + self.subdirectives = {} def append(self, kind, data, pos): """Append a stream event to the buffer. @@ -408,19 +880,48 @@ :param data: the event data :param pos: the position of the event in the source """ - if kind is TEXT: + if kind is SUB: + # The order needs to be +1 because a new START kind event will + # happen and we we need to wrap those events into our custom kind(s) + order = self.stack[-1] + 1 + subdirectives, substream = data + # Store the directives that should be applied after translation + self.subdirectives.setdefault(order, []).extend(subdirectives) + self.events.setdefault(order, []).append((SUB_START, None, pos)) + for skind, sdata, spos in substream: + self.append(skind, sdata, spos) + self.events.setdefault(order, []).append((SUB_END, None, pos)) + elif kind is TEXT: + if '[' in data or ']' in data: + # Quote [ and ] if it ain't us adding it, ie, if the user is + # using those chars in his templates, escape them + data = data.replace('[', '\[').replace(']', '\]') self.string.append(data) - self.events.setdefault(self.stack[-1], []).append(None) + self.events.setdefault(self.stack[-1], []).append((kind, data, pos)) elif kind is EXPR: - param = self.params.pop(0) + if self.params: + param = self.params.pop(0) + else: + params = ', '.join(['"%s"' % p for p in self.orig_params if p]) + if params: + params = "(%s)" % params + raise IndexError("%d parameters%s given to 'i18n:%s' but " + "%d or more expressions used in '%s', line %s" + % (len(self.orig_params), params, + self.directive.tagname, + len(self.orig_params)+1, + os.path.basename(pos[0] or + 'In Memmory Template'), + pos[1])) self.string.append('%%(%s)s' % param) - self.events.setdefault(self.stack[-1], []).append(None) + self.events.setdefault(self.stack[-1], []).append((kind, data, pos)) self.values[param] = (kind, data, pos) else: - if kind is START: + if kind is START: self.string.append(u'[%d:' % self.order) - self.events.setdefault(self.order, []).append((kind, data, pos)) self.stack.append(self.order) + self.events.setdefault(self.stack[-1], + []).append((kind, data, pos)) self.depth += 1 self.order += 1 elif kind is END: @@ -442,41 +943,107 @@ :param string: the translated message string """ + substream = None + + def yield_parts(string): + for idx, part in enumerate(regex.split(string)): + if idx % 2: + yield self.values[part] + elif part: + yield (TEXT, + part.replace('\[', '[').replace('\]', ']'), + (None, -1, -1) + ) + parts = parse_msg(string) + parts_counter = {} for order, string in parts: - events = self.events[order] - while events: - event = events.pop(0) - if event: - yield event + parts_counter.setdefault(order, []).append(None) + + while parts: + order, string = parts.pop(0) + if len(parts_counter[order]) == 1: + events = self.events[order] + else: + events = [self.events[order].pop(0)] + parts_counter[order].pop() + + for event in events: + if event[0] is SUB_START: + substream = [] + elif event[0] is SUB_END: + # Yield a substream which might have directives to be + # applied to it (after translation events) + yield SUB, (self.subdirectives[order], substream), event[2] + substream = None + elif event[0] is TEXT: + if string: + for part in yield_parts(string): + if substream is not None: + substream.append(part) + else: + yield part + # String handled, reset it + string = None + elif event[0] is START: + if substream is not None: + substream.append(event) + else: + yield event + if string: + for part in yield_parts(string): + if substream is not None: + substream.append(part) + else: + yield part + # String handled, reset it + string = None + elif event[0] is END: + if string: + for part in yield_parts(string): + if substream is not None: + substream.append(part) + else: + yield part + # String handled, reset it + string = None + if substream is not None: + substream.append(event) + else: + yield event + elif event[0] is EXPR: + # These are handled on the strings itself + continue else: - if not string: - break - for idx, part in enumerate(regex.split(string)): - if idx % 2: - yield self.values[part] - elif part: - yield TEXT, part, (None, -1, -1) - if not self.events[order] or not self.events[order][0]: - break + if string: + for part in yield_parts(string): + if substream is not None: + substream.append(part) + else: + yield part + # String handled, reset it + string = None + if substream is not None: + substream.append(event) + else: + yield event - -def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')): +def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?>> parse_msg("See [1:Help].") [(0, 'See '), (1, 'Help'), (0, '.')] - + >>> parse_msg("See [1:our [2:Help] page] for details.") [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')] - + >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].") [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')] - + >>> parse_msg("[1:] Bilder pro Seite anzeigen.") [(1, ''), (0, ' Bilder pro Seite anzeigen.')] - + :param string: the translated message string :return: a list of ``(order, string)`` tuples :rtype: `list` @@ -510,11 +1077,10 @@ """Extract strings from Python bytecode. >>> from genshi.template.eval import Expression - >>> expr = Expression('_("Hello")') >>> 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, Translator.GETTEXT_FUNCTIONS)) @@ -591,6 +1157,9 @@ tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None), encoding=encoding) + translator = Translator(None, ignore_tags, include_attrs, extract_text) + if hasattr(tmpl, 'add_directives'): + tmpl.add_directives(Translator.NAMESPACE, translator) for message in translator.extract(tmpl.stream, gettext_functions=keywords): yield message 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 @@ -18,16 +18,26 @@ import unittest from genshi.core import Attrs -from genshi.template import MarkupTemplate +from genshi.template import MarkupTemplate, Context from genshi.filters.i18n import Translator, extract from genshi.input import HTML class DummyTranslations(NullTranslations): + _domains = {} - def __init__(self, catalog): + def __init__(self, catalog=()): NullTranslations.__init__(self) - self._catalog = catalog + self._catalog = catalog or {} + self.plural = lambda n: n != 1 + + def add_domain(self, domain, catalog): + translation = DummyTranslations(catalog) + translation.add_fallback(self) + self._domains[domain] = translation + + def _domain_call(self, func, domain, *args, **kwargs): + return getattr(self._domains.get(domain, self), func)(*args, **kwargs) def ugettext(self, message): missing = object() @@ -38,6 +48,23 @@ return unicode(message) return tmsg + def dugettext(self, domain, message): + return self._domain_call('ugettext', domain, message) + + def ungettext(self, msgid1, msgid2, n): + try: + return self._catalog[(msgid1, self.plural(n))] + except KeyError: + if self._fallback: + return self._fallback.ngettext(msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def dungettext(self, domain, singular, plural, numeral): + return self._domain_call('ungettext', domain, singular, plural, numeral) + class TranslatorTestCase(unittest.TestCase): @@ -162,6 +189,7 @@