# HG changeset patch # User cmlenz # Date 1257886446 0 # Node ID e43633b320dbac10c9cfaad0ff503bf11db01182 # Parent 6c66e274198ddf2273e2066612980a19d671d7fe 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

+ ...
+ ... ''') + >>> translator = Translator() + >>> translator.setup(tmpl) + >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE + [(2, 'ngettext', (u'There is %(num)s coin', + u'There are %(num)s coins'), [])] + + >>> tmpl = MarkupTemplate('''\ + + ...
+ ...

There is $num coin

+ ...

There are $num coins

+ ...
+ ... ''') + >>> translator.setup(tmpl) + >>> print tmpl.generate(num=1).render() + +
+

There is 1 coin

+
+ + >>> print tmpl.generate(num=2).render() + +
+

There are 2 coins

+
+ + + When used as a directive and not as an attribute: + + >>> tmpl = MarkupTemplate('''\ + + ... + ...

There is $num coin

+ ...

There are $num coins

+ ...
+ ... ''') + >>> translator.setup(tmpl) + >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE + [(2, 'ngettext', (u'There is %(num)s coin', + u'There are %(num)s coins'), [])] + """ + __slots__ = ['numeral', 'params'] + + def __init__(self, value, template, hints=None, namespaces=None, + lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + params = [v.strip() for v in value.split(';')] + self.numeral = self._parse_expr(params.pop(0), template, lineno, offset) + self.params = params and [name.strip() for name in + params[0].split(',') if name] or [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + numeral = value.get('numeral', '').strip() + assert numeral is not '', "at least pass the numeral param" + params = [v.strip() for v in value.get('params', '').split(',')] + value = '%s; ' % numeral + ', '.join(params) + return super(ChooseDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + ctxt.push({'_i18n.choose.params': self.params, + '_i18n.choose.SingularDirective': None, + '_i18n.choose.PluralDirective': None}) + + new_stream = [] + singular_stream = None + singular_msgbuf = None + plural_stream = None + plural_msgbuf = None + + ngettext = ctxt.get('_i18n.ungettext') + assert callable(ngettext), "No ngettext function available" + dngettext = ctxt.get('_i18n.dngettext') + if not dngettext: + dngettext = lambda d, s, p, n: ngettext(s, p, n) + + for kind, event, pos in stream: + if kind is SUB: + subdirectives, substream = event + if isinstance(subdirectives[0], + SingularDirective) and not singular_stream: + # Apply directives to update context + singular_stream = list(_apply_directives(substream, + subdirectives, + ctxt, vars)) + new_stream.append((MSGBUF, (), ('', -1))) # msgbuf place holder + singular_msgbuf = ctxt.get('_i18n.choose.SingularDirective') + elif isinstance(subdirectives[0], + PluralDirective) and not plural_stream: + # Apply directives to update context + plural_stream = list(_apply_directives(substream, + subdirectives, + ctxt, vars)) + plural_msgbuf = ctxt.get('_i18n.choose.PluralDirective') + else: + new_stream.append((kind, event, pos)) + else: + new_stream.append((kind, event, pos)) + + if ctxt.get('_i18n.domain'): + ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'), + s, p, n) + + for kind, data, pos in new_stream: + if kind is MSGBUF: + for skind, sdata, spos in singular_stream: + if skind is MSGBUF: + translation = ngettext(singular_msgbuf.format(), + plural_msgbuf.format(), + self.numeral.evaluate(ctxt)) + for event in singular_msgbuf.translate(translation): + yield event + else: + yield skind, sdata, spos + else: + yield kind, data, pos + + ctxt.pop() + + def extract(self, stream, comment_stack): + stream = iter(stream) + previous = stream.next() + if previous is START: + stream.next() + + singular_msgbuf = MessageBuffer(self) + plural_msgbuf = MessageBuffer(self) + + for kind, event, pos in stream: + if kind is SUB: + subdirectives, substream = event + for subdirective in subdirectives: + if isinstance(subdirective, SingularDirective): + singular_msgbuf = subdirective.extract(substream, comment_stack, + singular_msgbuf) + elif isinstance(subdirective, PluralDirective): + plural_msgbuf = subdirective.extract(substream, comment_stack, + plural_msgbuf) + elif not isinstance(subdirective, StripDirective): + singular_msgbuf.append(kind, event, pos) + plural_msgbuf.append(kind, event, pos) + else: + singular_msgbuf.append(kind, event, pos) + plural_msgbuf.append(kind, event, pos) + + yield 'ngettext', \ + (singular_msgbuf.format(), plural_msgbuf.format()), \ + comment_stack[-1:] + + +class DomainDirective(I18NDirective): + """Implementation of the ``i18n:domain`` directive which allows choosing + another i18n domain(catalog) to translate from. + + >>> from genshi.filters.tests.i18n import DummyTranslations + >>> tmpl = MarkupTemplate('''\ + + ...

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(''' ... ... Example @@ -94,7 +477,6 @@ ... 'Example': 'Beispiel', ... 'Hello, %(name)s': 'Hallo, %(name)s' ... }[string] - >>> >>> translator = Translator(pseudo_gettext) Next, the translator needs to be prepended to any already defined filters @@ -115,23 +497,28 @@

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(''' ... ... Example @@ -276,7 +697,6 @@ ...

${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 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Please see [1:Help] for details.', messages[0][2]) @@ -175,8 +203,7 @@ """) gettext = lambda s: u"Für Details siehe bitte [1:Hilfe]." translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Für Details siehe bitte Hilfe.

""", tmpl.generate().render()) @@ -189,6 +216,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Please see [1:[2:Help] page] for details.', @@ -203,12 +231,39 @@ """) gettext = lambda s: u"Für Details siehe bitte [1:[2:Hilfeseite]]." translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Für Details siehe bitte Hilfeseite.

""", tmpl.generate().render()) + def test_extract_i18n_msg_label_with_nested_input(self): + tmpl = MarkupTemplate(""" +
+ +
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual('[1:[2:] days back]', + messages[0][2]) + + def test_translate_i18n_msg_label_with_nested_input(self): + tmpl = MarkupTemplate(""" +
+ +
+ """) + gettext = lambda s: "[1:[2:] foo bar]" + translator = Translator(gettext) + translator.setup(tmpl) + self.assertEqual(""" +
+ """, tmpl.generate().render()) + def test_extract_i18n_msg_empty(self): tmpl = MarkupTemplate(""" @@ -217,6 +272,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Show me [1:] entries per page.', messages[0][2]) @@ -230,8 +286,7 @@ """) gettext = lambda s: u"[1:] Einträge pro Seite anzeigen." translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Einträge pro Seite anzeigen.

""", tmpl.generate().render()) @@ -244,6 +299,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Please see [1:Help] for [2:details].', messages[0][2]) @@ -257,8 +313,7 @@ """) gettext = lambda s: u"Für [2:Details] siehe bitte [1:Hilfe]." translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Für Details siehe bitte Hilfe.

""", tmpl.generate().render()) @@ -271,6 +326,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Show me [1:] entries per page, starting at page [2:].', @@ -285,8 +341,7 @@ """) gettext = lambda s: u"[1:] Einträge pro Seite, beginnend auf Seite [2:]." translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Eintr\xc3\xa4ge pro Seite, beginnend auf Seite .

""", tmpl.generate().render()) @@ -299,6 +354,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Hello, %(name)s!', messages[0][2]) @@ -312,8 +368,7 @@ """) gettext = lambda s: u"Hallo, %(name)s!" translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Hallo, Jim!

""", tmpl.generate(user=dict(name='Jim')).render()) @@ -327,8 +382,7 @@ """) gettext = lambda s: u"%(name)s, sei gegrüßt!" translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Jim, sei gegrüßt!

""", tmpl.generate(user=dict(name='Jim')).render()) @@ -342,8 +396,7 @@ """) gettext = lambda s: u"Sei gegrüßt, [1:Alter]!" translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Sei gegrüßt, Alter!

""", tmpl.generate(anchor='42').render()) @@ -356,6 +409,7 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Posted by %(name)s at %(time)s', messages[0][2]) @@ -369,8 +423,7 @@ """) gettext = lambda s: u"%(name)s schrieb dies um %(time)s" translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) entry = { 'author': 'Jim', 'time': datetime(2008, 4, 1, 14, 30) @@ -387,30 +440,41 @@

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual('Show me [1:] entries per page.', messages[0][2]) - # FIXME: this currently fails :-/ -# def test_translate_i18n_msg_with_directive(self): -# tmpl = MarkupTemplate(""" -#

-# Show me entries per page. -#

-# """) -# gettext = lambda s: u"[1:] Einträge pro Seite anzeigen." -# tmpl.filters.insert(0, Translator(gettext)) -# self.assertEqual(""" -#

Einträge pro Seite anzeigen.

-# """, tmpl.generate().render()) + def test_translate_i18n_msg_with_directive(self): + tmpl = MarkupTemplate(""" +

+ Show me entries per page. +

+ """) + gettext = lambda s: u"[1:] Einträge pro Seite anzeigen." + translator = Translator(gettext) + translator.setup(tmpl) + self.assertEqual(""" +

Einträge pro Seite anzeigen.

+ """, tmpl.generate().render()) def test_extract_i18n_msg_with_comment(self): tmpl = MarkupTemplate(""" +

Foo

+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0]) + tmpl = MarkupTemplate("""

Foo

""") translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) messages = list(translator.extract(tmpl.stream)) self.assertEqual(1, len(messages)) self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0]) @@ -422,8 +486,7 @@ """) gettext = lambda s: u"Voh" translator = Translator(gettext) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Voh

""", tmpl.generate().render()) @@ -461,12 +524,908 @@

Foo

""") translator = Translator(DummyTranslations({'Foo': 'Voh'})) - tmpl.filters.insert(0, translator) - tmpl.add_directives(Translator.NAMESPACE, translator) + translator.setup(tmpl) self.assertEqual("""

Voh

""", tmpl.generate().render()) + def test_translate_i18n_msg_and_py_strip_directives(self): + tmpl = MarkupTemplate(""" +

Foo

+

Foo

+ """) + translator = Translator(DummyTranslations({'Foo': 'Voh'})) + translator.setup(tmpl) + self.assertEqual(""" + Voh + Voh + """, tmpl.generate().render()) + + def test_i18n_msg_ticket_300_extract(self): + tmpl = MarkupTemplate(""" + + Changed ${ '10/12/2008' } ago by ${ 'me, the author' } + + """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual( + (3, None, u'Changed %(date)s ago by %(author)s', []), messages[0] + ) + + def test_i18n_msg_ticket_300_translate(self): + tmpl = MarkupTemplate(""" + + Changed ${ date } ago by ${ author } + + """) + translations = DummyTranslations({ + u'Changed %(date)s ago by %(author)s': u'Modificado à %(date)s por %(author)s' + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" + Modificado à um dia por Pedro + """, tmpl.generate(date='um dia', author="Pedro").render()) + + + def test_i18n_msg_ticket_251_extract(self): + tmpl = MarkupTemplate(""" +

Translation[ 0 ]: One coin

+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual( + (3, None, u'[1:[2:Translation\\[\xa00\xa0\\]]: [3:One coin]]', []), messages[0] + ) + + def test_i18n_msg_ticket_251_translate(self): + tmpl = MarkupTemplate(""" +

Translation[ 0 ]: One coin

+ """) + translations = DummyTranslations({ + u'[1:[2:Translation\\[\xa00\xa0\\]]: [3:One coin]]': + u'[1:[2:Trandução\\[\xa00\xa0\\]]: [3:Uma moeda]]' + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

Trandução[ 0 ]: Uma moeda

+ """, tmpl.generate().render()) + + def test_extract_i18n_msg_with_other_directives_nested(self): + tmpl = MarkupTemplate(""" +

Before you do that, though, please first try + searching + for similar issues, as it is quite likely that this problem + has been reported before. For questions about installation + and configuration of Trac, please try the + mailing list + instead of filing a ticket. +

+ """) + translator = Translator() + translator.setup(tmpl) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual( + u'Before you do that, though, please first try\n ' + u'[1:[2:searching]\n for similar issues], as it is ' + u'quite likely that this problem\n has been reported ' + u'before. For questions about installation\n and ' + u'configuration of Trac, please try the\n ' + u'[3:mailing list]\n instead of filing a ticket.', + messages[0][2] + ) + + def test_translate_i18n_msg_with_other_directives_nested(self): + tmpl = MarkupTemplate(""" +

Before you do that, though, please first try + searching + for similar issues, as it is quite likely that this problem + has been reported before. For questions about installation + and configuration of Trac, please try the + mailing list + instead of filing a ticket. +

+ """) + translations = DummyTranslations({ + u'Before you do that, though, please first try\n ' + u'[1:[2:searching]\n for similar issues], as it is ' + u'quite likely that this problem\n has been reported ' + u'before. For questions about installation\n and ' + u'configuration of Trac, please try the\n ' + u'[3:mailing list]\n instead of filing a ticket.': + u'Antes de o fazer, porém,\n ' + u'[1:por favor tente [2:procurar]\n por problemas semelhantes], uma vez que ' + u'é muito provável que este problema\n já tenha sido reportado ' + u'anteriormente. Para questões relativas à instalação\n e ' + u'configuração do Trac, por favor tente a\n ' + u'[3:mailing list]\n em vez de criar um assunto.' + }) + translator = Translator(translations) + translator.setup(tmpl) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + ctx = Context() + ctx.push({'trac': {'homepage': 'http://trac.edgewall.org/'}}) + self.assertEqual(""" +

Antes de o fazer, porém, + por favor tente procurar + por problemas semelhantes, uma vez que é muito provável que este problema + já tenha sido reportado anteriormente. Para questões relativas à instalação + e configuração do Trac, por favor tente a + mailing list + em vez de criar um assunto.

+ """, tmpl.generate(ctx).render()) + + def test_i18n_msg_with_other_nested_directives_with_reordered_content(self): + # See: http://genshi.edgewall.org/ticket/300#comment:10 + tmpl = MarkupTemplate(""" +

+ Note: This repository is defined in + trac.ini + and cannot be edited on this page. +

+ """) + translations = DummyTranslations({ + u'[1:Note:] This repository is defined in\n ' + u'[2:[3:trac.ini]]\n and cannot be edited on this page.': + u'[1:Nota:] Este repositório está definido em \n ' + u'[2:[3:trac.ini]]\n e não pode ser editado nesta página.', + }) + translator = Translator(translations) + translator.setup(tmpl) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual( + u'[1:Note:] This repository is defined in\n ' + u'[2:[3:trac.ini]]\n and cannot be edited on this page.', + messages[0][2] + ) + self.assertEqual(""" +

Nota: Este repositório está definido em + trac.ini + e não pode ser editado nesta página.

+ """, tmpl.generate(editable=False).render()) + + def test_translate_i18n_domain_with_msg_directives(self): + #"""translate with i18n:domain and nested i18n:msg directives """ + + tmpl = MarkupTemplate(""" +
+

FooBar

+

Bar

+
+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

BarFoo

+

PT_Foo

+
+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_with_inline_directives(self): + #"""translate with inlined i18n:domain and i18n:msg directives""" + tmpl = MarkupTemplate(""" +

FooBar

+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

BarFoo

+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_without_msg_directives(self): + #"""translate domain call without i18n:msg directives still uses current domain""" + + tmpl = MarkupTemplate(""" +

Bar

+
+

FooBar

+

Bar

+

Bar

+
+

Bar

+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

Voh

+
+

BarFoo

+

PT_Foo

+

PT_Foo

+
+

Voh

+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_as_directive_not_attribute(self): + #"""translate with domain as directive""" + + tmpl = MarkupTemplate(""" + +

FooBar

+

Bar

+

Bar

+
+

Bar

+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

BarFoo

+

PT_Foo

+

PT_Foo

+

Voh

+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_nested_directives(self): + #"""translate with nested i18n:domain directives""" + + tmpl = MarkupTemplate(""" +

Bar

+
+

FooBar

+

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) + self.assertEqual(""" +

Voh

+
+

BarFoo

+

bar_Bar

+

foo_Bar

+
+

Voh

+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_with_empty_nested_domain_directive(self): + #"""translate with empty nested i18n:domain directive does not use dngettext""" + + tmpl = MarkupTemplate(""" +

Bar

+
+

FooBar

+

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) + self.assertEqual(""" +

Voh

+
+

BarFoo

+

Voh

+

foo_Bar

+
+

Voh

+ """, tmpl.generate().render()) + + def test_translate_i18n_choose_as_attribute(self): + tmpl = MarkupTemplate(""" +
+

FooBar

+

FooBars

+
+
+

FooBar

+

FooBars

+
+ """) + translations = DummyTranslations() + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

FooBar

+
+
+

FooBars

+
+ """, tmpl.generate(one=1, two=2).render()) + + def test_translate_i18n_choose_as_directive(self): + tmpl = MarkupTemplate(""" + +

FooBar

+

FooBars

+
+ +

FooBar

+

FooBars

+
+ """) + translations = DummyTranslations() + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

FooBars

+

FooBar

+ """, tmpl.generate(one=1, two=2).render()) + + def test_translate_i18n_choose_as_attribute_with_params(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translations = DummyTranslations({ + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

Vohs John Doe

+
+ """, tmpl.generate(two=2, fname='John', lname='Doe').render()) + + def test_translate_i18n_choose_as_attribute_with_params_and_domain_as_param(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translations = DummyTranslations() + translations.add_domain('foo', { + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

Vohs John Doe

+
+ """, tmpl.generate(two=2, fname='John', lname='Doe').render()) + + def test_translate_i18n_choose_as_directive_with_params(self): + tmpl = MarkupTemplate(""" + +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ """) + translations = DummyTranslations({ + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

Vohs John Doe

+

Voh John Doe

+ """, tmpl.generate(one=1, two=2, + fname='John', lname='Doe').render()) + + def test_translate_i18n_choose_as_directive_with_params_and_domain_as_directive(self): + tmpl = MarkupTemplate(""" + + +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+
+ +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ """) + translations = DummyTranslations() + translations.add_domain('foo', { + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

Vohs John Doe

+

Foo John Doe

+ """, tmpl.generate(one=1, two=2, + fname='John', lname='Doe').render()) + + def test_extract_i18n_choose_as_attribute(self): + tmpl = MarkupTemplate(""" +
+

FooBar

+

FooBars

+
+
+

FooBar

+

FooBars

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(2, len(messages)) + self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0]) + self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1]) + + def test_extract_i18n_choose_as_directive(self): + tmpl = MarkupTemplate(""" + +

FooBar

+

FooBars

+
+ +

FooBar

+

FooBars

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(2, len(messages)) + self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0]) + self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1]) + + def test_extract_i18n_choose_as_attribute_with_params(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[0]) + + def test_extract_i18n_choose_as_attribute_with_params_and_domain_as_param(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[0]) + + def test_extract_i18n_choose_as_directive_with_params(self): + tmpl = MarkupTemplate(""" + +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(2, len(messages)) + self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[0]) + self.assertEqual((7, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[1]) + + def test_extract_i18n_choose_as_directive_with_params_and_domain_as_directive(self): + tmpl = MarkupTemplate(""" + + +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+
+ +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(2, len(messages)) + self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[0]) + self.assertEqual((9, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), []), + messages[1]) + + def test_extract_i18n_choose_as_attribute_with_params_and_comment(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), + [u'As in Foo Bar']), + messages[0]) + + def test_extract_i18n_choose_as_directive_with_params_and_comment(self): + tmpl = MarkupTemplate(""" + +

Foo ${fname} ${lname}

+

Foos ${fname} ${lname}

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', + u'Foos %(fname)s %(lname)s'), + [u'As in Foo Bar']), + messages[0]) + + def test_translate_i18n_domain_with_nested_inlcudes(self): + import os, shutil, tempfile + from genshi.template.loader import TemplateLoader + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + for idx in range(7): + file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w') + try: + file1.write(""" +
Included tmpl$idx
+

Bar $idx

+

Bar

+

Bar $idx

+

Bar $idx

+ + + + """) + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w') + try: + file2.write(""" + + """) + finally: + file2.close() + + def callback(template): + translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s'}) + translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'}) + translations.add_domain('bar', {'Bar': 'bar_Bar'}) + translator = Translator(translations) + translator.setup(template) + loader = TemplateLoader([dirname], callback=callback) + tmpl = loader.load('tmpl10.html') + + self.assertEqual(""" +
Included tmpl0
+

foo_Bar 0

+

bar_Bar

+

Voh 0

+

Voh 0

+
Included tmpl1
+

foo_Bar 1

+

bar_Bar

+

Voh 1

+

Voh 1

+
Included tmpl2
+

foo_Bar 2

+

bar_Bar

+

Voh 2

+

Voh 2

+
Included tmpl3
+

foo_Bar 3

+

bar_Bar

+

Voh 3

+

Voh 3

+
Included tmpl4
+

foo_Bar 4

+

bar_Bar

+

Voh 4

+

Voh 4

+
Included tmpl5
+

foo_Bar 5

+

bar_Bar

+

Voh 5

+

Voh 5

+
Included tmpl6
+

foo_Bar 6

+

bar_Bar

+

Voh 6

+

Voh 6

+ """, tmpl.generate(idx=-1).render()) + finally: + shutil.rmtree(dirname) + + def test_translate_i18n_domain_with_nested_inlcudes_with_translatable_attrs(self): + import os, shutil, tempfile + from genshi.template.loader import TemplateLoader + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + for idx in range(4): + file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w') + try: + file1.write(""" +
Included tmpl$idx
+

Bar $idx

+

Bar

+

Bar $idx

+

Bar $idx

+

Bar $idx

+ + + + """) + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w') + try: + file2.write(""" + + """) + finally: + file2.close() + + translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s', + 'Bar': 'Voh'}) + translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'}) + translations.add_domain('bar', {'Bar': 'bar_Bar'}) + translator = Translator(translations) + + def callback(template): + translator.setup(template) + loader = TemplateLoader([dirname], callback=callback) + tmpl = loader.load('tmpl10.html') + + self.assertEqual(""" +
Included tmpl0
+

foo_Bar 0

+

bar_Bar

+

Voh 0

+

Voh 0

+

Voh 0

+
Included tmpl1
+

foo_Bar 1

+

bar_Bar

+

Voh 1

+

Voh 1

+

Voh 1

+
Included tmpl2
+

foo_Bar 2

+

bar_Bar

+

Voh 2

+

Voh 2

+

Voh 2

+
Included tmpl3
+

foo_Bar 3

+

bar_Bar

+

Voh 3

+

Voh 3

+

Voh 3

+ """, tmpl.generate(idx=-1, + dg=translations.dugettext).render()) + finally: + shutil.rmtree(dirname) + + def test_translate_i18n_msg_and_comment_with_py_strip_directives(self): + tmpl = MarkupTemplate(""" +

Foo

+

Foo

+ """) + translator = Translator(DummyTranslations({'Foo': 'Voh'})) + translator.setup(tmpl) + self.assertEqual(""" + Voh + Voh + """, tmpl.generate().render()) + + def test_translate_i18n_choose_and_py_strip(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translations = DummyTranslations({ + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

Vohs John Doe

+
+ """, tmpl.generate(two=2, fname='John', lname='Doe').render()) + + def test_translate_i18n_choose_and_domain_and_py_strip(self): + tmpl = MarkupTemplate(""" +
+

Foo $fname $lname

+

Foos $fname $lname

+
+ """) + translations = DummyTranslations() + translations.add_domain('foo', { + ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', + ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', + 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', + 'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', + }) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +
+

Vohs John Doe

+
+ """, tmpl.generate(two=2, fname='John', lname='Doe').render()) + + def test_extract_i18n_msg_with_py_strip(self): + tmpl = MarkupTemplate(""" +

+ Please see Help for details. +

+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, None, u'Please see [1:Help] for details.', []), + messages[0]) + + def test_extract_i18n_msg_with_py_strip_and_comment(self): + tmpl = MarkupTemplate(""" +

+ Please see Help for details. +

+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, None, u'Please see [1:Help] for details.', + ['Foo']), messages[0]) + + def test_extract_i18n_choose_as_attribute_and_py_strip(self): + tmpl = MarkupTemplate(""" +
+

FooBar

+

FooBars

+
+ """) + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0]) + + def test_translate_i18n_domain_with_inline_directive_on_START_NS(self): + tmpl = MarkupTemplate(""" +

FooBar

+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

BarFoo

+ """, tmpl.generate().render()) + + def test_translate_i18n_domain_with_inline_directive_on_START_NS_with_py_strip(self): + tmpl = MarkupTemplate(""" +

FooBar

+ """) + translations = DummyTranslations({'Bar': 'Voh'}) + translations.add_domain('foo', {'FooBar': 'BarFoo'}) + translator = Translator(translations) + translator.setup(tmpl) + self.assertEqual(""" +

BarFoo

+ """, tmpl.generate().render()) class ExtractTestCase(unittest.TestCase): @@ -504,12 +1463,12 @@ def test_text_template_extraction(self): buf = StringIO("""${_("Dear %(name)s") % {'name': name}}, - + ${ngettext("Your item:", "Your items", len(items))} #for item in items * $item #end - + All the best, Foobar""") results = list(extract(buf, ['_', 'ngettext'], [], { @@ -563,6 +1522,60 @@ """) self.assertEqual([], list(extract(buf, ['_'], [], {}))) + def test_extract_py_def_directive_with_py_strip(self): + # Failed extraction from Trac + tmpl = MarkupTemplate(""" + + + +
+ Show + +
+
+ Ignore: +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
""") + translator = Translator() + tmpl.add_directives(Translator.NAMESPACE, translator) + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(10, len(messages)) + self.assertEqual([ + (3, None, u'View differences', []), + (6, None, u'inline', []), + (8, None, u'side by side', []), + (10, None, u'Show', []), + (13, None, u'lines around each change', []), + (16, None, u'Ignore:', []), + (20, None, u'Blank lines', []), + (25, None, u'Case changes',[]), + (30, None, u'White space changes', []), + (34, '_', u'Update', [])], messages) + def suite(): suite = unittest.TestSuite()