changeset 849:e43633b320db

Merged advanced-i18n branch back into trunk.
author cmlenz
date Tue, 10 Nov 2009 20:54:06 +0000
parents 6c66e274198d
children 47297fd93363
files genshi/filters/__init__.py genshi/filters/i18n.py genshi/filters/tests/i18n.py
diffstat 3 files changed, 1718 insertions(+), 136 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <p i18n:comment="As in Foo Bar">Foo</p>
+    ... </html>''')
+    >>> 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('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <div i18n:msg="">
+    ...     <p>Foo</p>
+    ...     <p>Bar</p>
+    ...   </div>
+    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
+    ... </html>''')
+    
+    >>> 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()
+    <html>
+      <div><p>Foo</p>
+        <p>Bar</p></div>
+      <p>Foo <em>bar</em>!</p>
+    </html>
+
+    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <div i18n:msg="fname, lname">
+    ...     <p>First Name: ${fname}</p>
+    ...     <p>Last Name: ${lname}</p>
+    ...   </div>
+    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
+    ... </html>''')
+    >>> 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('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <div i18n:msg="fname, lname">
+    ...     <p>First Name: ${fname}</p>
+    ...     <p>Last Name: ${lname}</p>
+    ...   </div>
+    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
+    ... </html>''')
+    >>> translator.setup(tmpl)
+    >>> print tmpl.generate(fname='John', lname='Doe').render()
+    <html>
+      <div><p>First Name: John</p>
+        <p>Last Name: Doe</p></div>
+      <p>Foo <em>bar</em>!</p>
+    </html>
+
+    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('''\
+        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <div i18n:choose="num; num">
+    ...     <p i18n:singular="">There is $num coin</p>
+    ...     <p i18n:plural="">There are $num coins</p>
+    ...   </div>
+    ... </html>''')
+    >>> 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('''\
+        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <div i18n:choose="num; num">
+    ...     <p i18n:singular="">There is $num coin</p>
+    ...     <p i18n:plural="">There are $num coins</p>
+    ...   </div>
+    ... </html>''')
+    >>> translator.setup(tmpl)
+    >>> print tmpl.generate(num=1).render()
+    <html>
+      <div>
+        <p>There is 1 coin</p>
+      </div>
+    </html>
+    >>> print tmpl.generate(num=2).render()
+    <html>
+      <div>
+        <p>There are 2 coins</p>
+      </div>
+    </html>
+
+    When used as a directive and not as an attribute:
+
+    >>> tmpl = MarkupTemplate('''\
+        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <i18n:choose numeral="num" params="num">
+    ...     <p i18n:singular="">There is $num coin</p>
+    ...     <p i18n:plural="">There are $num coins</p>
+    ...   </i18n:choose>
+    ... </html>''')
+    >>> 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('''\
+        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <p i18n:msg="">Bar</p>
+    ...   <div i18n:domain="foo">
+    ...     <p i18n:msg="">FooBar</p>
+    ...     <p>Bar</p>
+    ...     <p i18n:domain="bar" i18n:msg="">Bar</p>
+    ...     <p i18n:domain="">Bar</p>
+    ...   </div>
+    ...   <p>Bar</p>
+    ... </html>''')
+
+    >>> 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()
+    <html>
+      <p>Voh</p>
+      <div>
+        <p>BarFoo</p>
+        <p>foo_Bar</p>
+        <p>bar_Bar</p>
+        <p>Voh</p>
+      </div>
+      <p>Voh</p>
+    </html>
+    """
+    __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('''<html xmlns:py="http://genshi.edgewall.org/">
     ...   <head>
     ...     <title>Example</title>
@@ -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 @@
         <p>Hallo, Hans</p>
       </body>
     </html>
-
+    
     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('''<html xmlns:py="http://genshi.edgewall.org/">
         ...   <head>
         ...     <title>Example</title>
@@ -276,7 +697,6 @@
         ...     <p>${ngettext("You have %d item", "You have %d items", num)}</p>
         ...   </body>
         ... </html>''', 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:
+                            # <p i18n:comment="foo">Foo</p>
+                            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 a translated message using Genshi mixed content message
     formatting.
-
+    
     >>> 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
--- 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 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p>Für Details siehe bitte <a href="help.html">Hilfe</a>.</p>
         </html>""", tmpl.generate().render())
@@ -189,6 +216,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p>Für Details siehe bitte <a href="help.html"><em>Hilfeseite</em></a>.</p>
         </html>""", tmpl.generate().render())
 
+    def test_extract_i18n_msg_label_with_nested_input(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:msg="">
+            <label><input type="text" size="3" name="daysback" value="30" /> days back</label>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:msg="">
+            <label><input type="text" size="3" name="daysback" value="30" /> foo bar</label>
+          </div>
+        </html>""")
+        gettext = lambda s: "[1:[2:] foo bar]"
+        translator = Translator(gettext)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <div><label><input type="text" size="3" name="daysback" value="30"/> foo bar</label></div>
+        </html>""", tmpl.generate().render())
+
     def test_extract_i18n_msg_empty(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
@@ -217,6 +272,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p><input type="text" name="num"/> Einträge pro Seite anzeigen.</p>
         </html>""", tmpl.generate().render())
@@ -244,6 +299,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p>Für <em>Details</em> siehe bitte <a href="help.html">Hilfe</a>.</p>
         </html>""", tmpl.generate().render())
@@ -271,6 +326,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p><input type="text" name="num"/> Eintr\xc3\xa4ge pro Seite, beginnend auf Seite <input type="text" name="num"/>.</p>
         </html>""", tmpl.generate().render())
@@ -299,6 +354,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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("""<html>
           <p>Hallo, Jim!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -327,8 +382,7 @@
         </html>""")
         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("""<html>
           <p>Jim, sei gegrüßt!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -342,8 +396,7 @@
         </html>""")
         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("""<html>
           <p>Sei gegrüßt, <a href="#42">Alter</a>!</p>
         </html>""", tmpl.generate(anchor='42').render())
@@ -356,6 +409,7 @@
           </p>
         </html>""")
         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 @@
         </html>""")
         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 @@
           </p>
         </html>""")
         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("""<html xmlns:py="http://genshi.edgewall.org/"
-#            xmlns:i18n="http://genshi.edgewall.org/i18n">
-#          <p i18n:msg="">
-#            Show me <input type="text" name="num" py:attrs="{'value': x}" /> entries per page.
-#          </p>
-#        </html>""")
-#        gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
-#        tmpl.filters.insert(0, Translator(gettext))
-#        self.assertEqual("""<html>
-#          <p><input type="text" name="num" value="x"/> Einträge pro Seite anzeigen.</p>
-#        </html>""", tmpl.generate().render())
+    def test_translate_i18n_msg_with_directive(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="">
+            Show me <input type="text" name="num" py:attrs="{'value': 'x'}" /> entries per page.
+          </p>
+        </html>""")
+        gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
+        translator = Translator(gettext)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p><input type="text" name="num" value="x"/> Einträge pro Seite anzeigen.</p>
+        </html>""", tmpl.generate().render())
 
     def test_extract_i18n_msg_with_comment(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:comment="As in foo bar" i18n:msg="">Foo</p>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
           <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
         </html>""")
         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 @@
         </html>""")
         gettext = lambda s: u"Voh"
         translator = Translator(gettext)
-        tmpl.filters.insert(0, translator)
-        tmpl.add_directives(Translator.NAMESPACE, translator)
+        translator.setup(tmpl)
         self.assertEqual("""<html>
           <p>Voh</p>
         </html>""", tmpl.generate().render())
@@ -461,12 +524,908 @@
           <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
         </html>""")
         translator = Translator(DummyTranslations({'Foo': 'Voh'}))
-        tmpl.filters.insert(0, translator)
-        tmpl.add_directives(Translator.NAMESPACE, translator)
+        translator.setup(tmpl)
         self.assertEqual("""<html>
           <p>Voh</p>
         </html>""", tmpl.generate().render())
 
+    def test_translate_i18n_msg_and_py_strip_directives(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" py:strip="">Foo</p>
+          <p py:strip="" i18n:msg="">Foo</p>
+        </html>""")
+        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          Voh
+          Voh
+        </html>""", tmpl.generate().render())
+
+    def test_i18n_msg_ticket_300_extract(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:msg params="date, author">
+            Changed ${ '10/12/2008' } ago by ${ 'me, the author' }
+          </i18n:msg>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:msg params="date, author">
+            Changed ${ date } ago by ${ author }
+          </i18n:msg>
+        </html>""")
+        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("""<html>
+          Modificado à um dia por Pedro
+        </html>""", tmpl.generate(date='um dia', author="Pedro").render())
+
+
+    def test_i18n_msg_ticket_251_extract(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg=""><tt><b>Translation[&nbsp;0&nbsp;]</b>: <em>One coin</em></tt></p>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg=""><tt><b>Translation[&nbsp;0&nbsp;]</b>: <em>One coin</em></tt></p>
+        </html>""")
+        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("""<html>
+          <p><tt><b>Trandução[ 0 ]</b>: <em>Uma moeda</em></tt></p>
+        </html>""", tmpl.generate().render())
+
+    def test_extract_i18n_msg_with_other_directives_nested(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" py:with="q = quote_plus(message[:80])">Before you do that, though, please first try
+            <strong><a href="${trac.homepage}search?ticket=yes&amp;noquickjump=1&amp;q=$q">searching</a>
+            for similar issues</strong>, as it is quite likely that this problem
+            has been reported before. For questions about installation
+            and configuration of Trac, please try the
+            <a href="${trac.homepage}wiki/MailingList">mailing list</a>
+            instead of filing a ticket.
+          </p>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="">Before you do that, though, please first try
+            <strong><a href="${trac.homepage}search?ticket=yes&amp;noquickjump=1&amp;q=q">searching</a>
+            for similar issues</strong>, as it is quite likely that this problem
+            has been reported before. For questions about installation
+            and configuration of Trac, please try the
+            <a href="${trac.homepage}wiki/MailingList">mailing list</a>
+            instead of filing a ticket.
+          </p>
+        </html>""")
+        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("""<html>
+          <p>Antes de o fazer, porém,
+            <strong>por favor tente <a href="http://trac.edgewall.org/search?ticket=yes&amp;noquickjump=1&amp;q=q">procurar</a>
+            por problemas semelhantes</strong>, 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
+            <a href="http://trac.edgewall.org/wiki/MailingList">mailing list</a>
+            em vez de criar um assunto.</p>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p py:if="not editable" class="hint" i18n:msg="">
+            <strong>Note:</strong> This repository is defined in
+            <code><a href="${ 'href.wiki(TracIni)' }">trac.ini</a></code>
+            and cannot be edited on this page.
+          </p>
+        </html>""")
+        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("""<html>
+          <p class="hint"><strong>Nota:</strong> Este repositório está definido em
+           <code><a href="href.wiki(TracIni)">trac.ini</a></code>
+            e não pode ser editado nesta página.</p>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:domain="foo">
+            <p i18n:msg="">FooBar</p>
+            <p i18n:msg="">Bar</p>
+          </div>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <div>
+            <p>BarFoo</p>
+            <p>PT_Foo</p>
+          </div>
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_domain_with_inline_directives(self):
+        #"""translate with inlined i18n:domain and i18n:msg directives"""
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" i18n:domain="foo">FooBar</p>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p>BarFoo</p>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="">Bar</p>
+          <div i18n:domain="foo">
+            <p i18n:msg="">FooBar</p>
+            <p i18n:msg="">Bar</p>
+            <p>Bar</p>
+          </div>
+          <p>Bar</p>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p>Voh</p>
+          <div>
+            <p>BarFoo</p>
+            <p>PT_Foo</p>
+            <p>PT_Foo</p>
+          </div>
+          <p>Voh</p>
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_domain_as_directive_not_attribute(self):
+        #"""translate with domain as directive"""
+
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:domain name="foo">
+          <p i18n:msg="">FooBar</p>
+          <p i18n:msg="">Bar</p>
+          <p>Bar</p>
+        </i18n:domain>
+          <p>Bar</p>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p>BarFoo</p>
+          <p>PT_Foo</p>
+          <p>PT_Foo</p>
+          <p>Voh</p>
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_domain_nested_directives(self):
+        #"""translate with nested i18n:domain directives"""
+
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="">Bar</p>
+          <div i18n:domain="foo">
+            <p i18n:msg="">FooBar</p>
+            <p i18n:domain="bar" i18n:msg="">Bar</p>
+            <p>Bar</p>
+          </div>
+          <p>Bar</p>
+        </html>""")
+        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("""<html>
+          <p>Voh</p>
+          <div>
+            <p>BarFoo</p>
+            <p>bar_Bar</p>
+            <p>foo_Bar</p>
+          </div>
+          <p>Voh</p>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="">Bar</p>
+          <div i18n:domain="foo">
+            <p i18n:msg="">FooBar</p>
+            <p i18n:domain="" i18n:msg="">Bar</p>
+            <p>Bar</p>
+          </div>
+          <p>Bar</p>
+        </html>""")
+        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("""<html>
+          <p>Voh</p>
+          <div>
+            <p>BarFoo</p>
+            <p>Voh</p>
+            <p>foo_Bar</p>
+          </div>
+          <p>Voh</p>
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_choose_as_attribute(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="one">
+            <p i18n:singular="">FooBar</p>
+            <p i18n:plural="">FooBars</p>
+          </div>
+          <div i18n:choose="two">
+            <p i18n:singular="">FooBar</p>
+            <p i18n:plural="">FooBars</p>
+          </div>
+        </html>""")
+        translations = DummyTranslations()
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <div>
+            <p>FooBar</p>
+          </div>
+          <div>
+            <p>FooBars</p>
+          </div>
+        </html>""", tmpl.generate(one=1, two=2).render())
+
+    def test_translate_i18n_choose_as_directive(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:choose numeral="two">
+          <p i18n:singular="">FooBar</p>
+          <p i18n:plural="">FooBars</p>
+        </i18n:choose>
+        <i18n:choose numeral="one">
+          <p i18n:singular="">FooBar</p>
+          <p i18n:plural="">FooBars</p>
+        </i18n:choose>
+        </html>""")
+        translations = DummyTranslations()
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p>FooBars</p>
+          <p>FooBar</p>
+        </html>""", tmpl.generate(one=1, two=2).render())
+
+    def test_translate_i18n_choose_as_attribute_with_params(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html>
+          <div>
+            <p>Vohs John Doe</p>
+          </div>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n"
+            i18n:domain="foo">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html>
+          <div>
+            <p>Vohs John Doe</p>
+          </div>
+        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
+
+    def test_translate_i18n_choose_as_directive_with_params(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:choose numeral="two" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        <i18n:choose numeral="one" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html>
+          <p>Vohs John Doe</p>
+          <p>Voh John Doe</p>
+        </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:domain name="foo">
+        <i18n:choose numeral="two" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </i18n:domain>
+        <i18n:choose numeral="one" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html>
+          <p>Vohs John Doe</p>
+          <p>Foo John Doe</p>
+        </html>""", tmpl.generate(one=1, two=2,
+                                  fname='John', lname='Doe').render())
+
+    def test_extract_i18n_choose_as_attribute(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="one">
+            <p i18n:singular="">FooBar</p>
+            <p i18n:plural="">FooBars</p>
+          </div>
+          <div i18n:choose="two">
+            <p i18n:singular="">FooBar</p>
+            <p i18n:plural="">FooBars</p>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:choose numeral="two">
+          <p i18n:singular="">FooBar</p>
+          <p i18n:plural="">FooBars</p>
+        </i18n:choose>
+        <i18n:choose numeral="one">
+          <p i18n:singular="">FooBar</p>
+          <p i18n:plural="">FooBars</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n"
+            i18n:domain="foo">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:choose numeral="two" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        <i18n:choose numeral="one" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:domain name="foo">
+        <i18n:choose numeral="two" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </i18n:domain>
+        <i18n:choose numeral="one" params="fname, lname">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="two; fname, lname" i18n:comment="As in Foo Bar">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+        <i18n:choose numeral="two" params="fname, lname" i18n:comment="As in Foo Bar">
+          <p i18n:singular="">Foo ${fname} ${lname}</p>
+          <p i18n:plural="">Foos ${fname} ${lname}</p>
+        </i18n:choose>
+        </html>""")
+        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("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                         xmlns:py="http://genshi.edgewall.org/"
+                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
+                        <div>Included tmpl$idx</div>
+                        <p i18n:msg="idx">Bar $idx</p>
+                        <p i18n:domain="bar">Bar</p>
+                        <p i18n:msg="idx" i18n:domain="">Bar $idx</p>
+                        <p i18n:domain="" i18n:msg="idx">Bar $idx</p>
+                        <py:if test="idx &lt; 6">
+                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
+                        </py:if>
+                    </html>""")
+                finally:
+                    file1.close()
+
+            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
+            try:
+                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                     xmlns:py="http://genshi.edgewall.org/"
+                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
+                                     i18n:domain="foo">
+                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
+                </html>""")
+            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("""<html>
+                        <div>Included tmpl0</div>
+                        <p>foo_Bar 0</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 0</p>
+                        <p>Voh 0</p>
+                        <div>Included tmpl1</div>
+                        <p>foo_Bar 1</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 1</p>
+                        <p>Voh 1</p>
+                        <div>Included tmpl2</div>
+                        <p>foo_Bar 2</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 2</p>
+                        <p>Voh 2</p>
+                        <div>Included tmpl3</div>
+                        <p>foo_Bar 3</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 3</p>
+                        <p>Voh 3</p>
+                        <div>Included tmpl4</div>
+                        <p>foo_Bar 4</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 4</p>
+                        <p>Voh 4</p>
+                        <div>Included tmpl5</div>
+                        <p>foo_Bar 5</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 5</p>
+                        <p>Voh 5</p>
+                        <div>Included tmpl6</div>
+                        <p>foo_Bar 6</p>
+                        <p>bar_Bar</p>
+                        <p>Voh 6</p>
+                        <p>Voh 6</p>
+                </html>""", 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("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                         xmlns:py="http://genshi.edgewall.org/"
+                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
+                        <div>Included tmpl$idx</div>
+                        <p title="${dg('foo', 'Bar %(idx)s') % dict(idx=idx)}" i18n:msg="idx">Bar $idx</p>
+                        <p title="Bar" i18n:domain="bar">Bar</p>
+                        <p title="Bar" i18n:msg="idx" i18n:domain="">Bar $idx</p>
+                        <p i18n:msg="idx" i18n:domain="" title="Bar">Bar $idx</p>
+                        <p i18n:domain="" i18n:msg="idx" title="Bar">Bar $idx</p>
+                        <py:if test="idx &lt; 3">
+                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
+                        </py:if>
+                    </html>""")
+                finally:
+                    file1.close()
+
+            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
+            try:
+                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                     xmlns:py="http://genshi.edgewall.org/"
+                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
+                                     i18n:domain="foo">
+                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
+                </html>""")
+            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("""<html>
+                        <div>Included tmpl0</div>
+                        <p title="foo_Bar 0">foo_Bar 0</p>
+                        <p title="bar_Bar">bar_Bar</p>
+                        <p title="Voh">Voh 0</p>
+                        <p title="Voh">Voh 0</p>
+                        <p title="Voh">Voh 0</p>
+                        <div>Included tmpl1</div>
+                        <p title="foo_Bar 1">foo_Bar 1</p>
+                        <p title="bar_Bar">bar_Bar</p>
+                        <p title="Voh">Voh 1</p>
+                        <p title="Voh">Voh 1</p>
+                        <p title="Voh">Voh 1</p>
+                        <div>Included tmpl2</div>
+                        <p title="foo_Bar 2">foo_Bar 2</p>
+                        <p title="bar_Bar">bar_Bar</p>
+                        <p title="Voh">Voh 2</p>
+                        <p title="Voh">Voh 2</p>
+                        <p title="Voh">Voh 2</p>
+                        <div>Included tmpl3</div>
+                        <p title="foo_Bar 3">foo_Bar 3</p>
+                        <p title="bar_Bar">bar_Bar</p>
+                        <p title="Voh">Voh 3</p>
+                        <p title="Voh">Voh 3</p>
+                        <p title="Voh">Voh 3</p>
+                </html>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" i18n:comment="As in foo bar" py:strip="">Foo</p>
+          <p py:strip="" i18n:msg="" i18n:comment="As in foo bar">Foo</p>
+        </html>""")
+        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          Voh
+          Voh
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_choose_and_py_strip(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html>
+          <div>
+            <p>Vohs John Doe</p>
+          </div>
+        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
+
+    def test_translate_i18n_choose_and_domain_and_py_strip(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n"
+            i18n:domain="foo">
+          <div i18n:choose="two; fname, lname">
+            <p i18n:singular="">Foo $fname $lname</p>
+            <p i18n:plural="">Foos $fname $lname</p>
+          </div>
+        </html>""")
+        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("""<html>
+          <div>
+            <p>Vohs John Doe</p>
+          </div>
+        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
+
+    def test_extract_i18n_msg_with_py_strip(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" py:strip="">
+            Please see <a href="help.html">Help</a> for details.
+          </p>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" py:strip="" i18n:comment="Foo">
+            Please see <a href="help.html">Help</a> for details.
+          </p>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <div i18n:choose="one" py:strip="">
+            <p i18n:singular="" py:strip="">FooBar</p>
+            <p i18n:plural="" py:strip="">FooBars</p>
+          </div>
+        </html>""")
+        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("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n" i18n:domain="foo">
+          <p i18n:msg="">FooBar</p>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""<html>
+          <p>BarFoo</p>
+        </html>""", tmpl.generate().render())
+
+    def test_translate_i18n_domain_with_inline_directive_on_START_NS_with_py_strip(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n"
+            i18n:domain="foo" py:strip="">
+          <p i18n:msg="">FooBar</p>
+        </html>""")
+        translations = DummyTranslations({'Bar': 'Voh'})
+        translations.add_domain('foo', {'FooBar': 'BarFoo'})
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual("""
+          <p>BarFoo</p>
+        """, 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 @@
         </html>""")
         self.assertEqual([], list(extract(buf, ['_'], [], {})))
 
+    def test_extract_py_def_directive_with_py_strip(self):
+        # Failed extraction from Trac
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" py:strip="">
+    <py:def function="diff_options_fields(diff)">
+    <label for="style">View differences</label>
+    <select id="style" name="style">
+      <option selected="${diff.style == 'inline' or None}"
+              value="inline">inline</option>
+      <option selected="${diff.style == 'sidebyside' or None}"
+              value="sidebyside">side by side</option>
+    </select>
+    <div class="field">
+      Show <input type="text" name="contextlines" id="contextlines" size="2"
+                  maxlength="3" value="${diff.options.contextlines &lt; 0 and 'all' or diff.options.contextlines}" />
+      <label for="contextlines">lines around each change</label>
+    </div>
+    <fieldset id="ignore" py:with="options = diff.options">
+      <legend>Ignore:</legend>
+      <div class="field">
+        <input type="checkbox" id="ignoreblanklines" name="ignoreblanklines"
+               checked="${options.ignoreblanklines or None}" />
+        <label for="ignoreblanklines">Blank lines</label>
+      </div>
+      <div class="field">
+        <input type="checkbox" id="ignorecase" name="ignorecase"
+               checked="${options.ignorecase or None}" />
+        <label for="ignorecase">Case changes</label>
+      </div>
+      <div class="field">
+        <input type="checkbox" id="ignorewhitespace" name="ignorewhitespace"
+               checked="${options.ignorewhitespace or None}" />
+        <label for="ignorewhitespace">White space changes</label>
+      </div>
+    </fieldset>
+    <div class="buttons">
+      <input type="submit" name="update" value="${_('Update')}" />
+    </div>
+  </py:def></html>""")
+        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()
Copyright (C) 2012-2017 Edgewall Software