Index: genshi/filters/tests/i18n.py
===================================================================
--- genshi/filters/tests/i18n.py	(revision 946)
+++ genshi/filters/tests/i18n.py	(working copy)
@@ -13,21 +13,31 @@
 
 from datetime import datetime
 import doctest
-from gettext import NullTranslations
+from gettext import NullTranslations, c2py
 from StringIO import StringIO
 import unittest
 
 from genshi.core import Attrs
 from genshi.template import MarkupTemplate
-from genshi.filters.i18n import Translator, extract
+from genshi.filters.i18n import Translator, extract, setup_i18n
 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 = c2py('(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()
@@ -37,6 +47,23 @@
                 return self._fallback.ugettext(message)
             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)
+        setup_i18n(tmpl, translator)
         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,8 +231,7 @@
         </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)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Für Details siehe bitte <a href="help.html"><em>Hilfeseite</em></a>.</p>
         </html>""", tmpl.generate().render())
@@ -217,6 +244,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 +258,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)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p><input type="text" name="num"/> Einträge pro Seite anzeigen.</p>
         </html>""", tmpl.generate().render())
@@ -244,6 +271,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 +285,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)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Für <em>Details</em> siehe bitte <a href="help.html">Hilfe</a>.</p>
         </html>""", tmpl.generate().render())
@@ -271,6 +298,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 +313,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)
+        setup_i18n(tmpl, translator)
         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 +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('Hello, %(name)s!', messages[0][2])
@@ -312,8 +340,7 @@
         </html>""")
         gettext = lambda s: u"Hallo, %(name)s!"
         translator = Translator(gettext)
-        tmpl.filters.insert(0, translator)
-        tmpl.add_directives(Translator.NAMESPACE, translator)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Hallo, Jim!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -327,8 +354,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)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Jim, sei gegrüßt!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -342,8 +368,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)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Sei gegrüßt, <a href="#42">Alter</a>!</p>
         </html>""", tmpl.generate(anchor='42').render())
@@ -356,6 +381,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 +395,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)
+        setup_i18n(tmpl, translator)
         entry = {
             'author': 'Jim',
             'time': datetime(2008, 4, 1, 14, 30)
@@ -387,30 +412,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)
+        setup_i18n(tmpl, translator)
+        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 +458,7 @@
         </html>""")
         gettext = lambda s: u"Voh"
         translator = Translator(gettext)
-        tmpl.filters.insert(0, translator)
-        tmpl.add_directives(Translator.NAMESPACE, translator)
+        setup_i18n(tmpl, translator)
         self.assertEqual("""<html>
           <p>Voh</p>
         </html>""", tmpl.generate().render())
@@ -461,12 +496,604 @@
           <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)
+        setup_i18n(tmpl, translator)
+        self.assertEqual("""<html>
+          <p>Voh</p>
+        </html>""", tmpl.generate().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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
         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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+        setup_i18n(tmpl, translator)
+        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)
+                setup_i18n(template, translator)
+            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: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):                
+                setup_i18n(template, translator)
+            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>
+                        <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>
+                        <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>
+                        <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>
+                </html>""", tmpl.generate(idx=-1,
+                                          dg=translations.dugettext).render())
+        finally:
+            shutil.rmtree(dirname)
+
 
 class ExtractTestCase(unittest.TestCase):
 
Index: genshi/filters/__init__.py
===================================================================
--- genshi/filters/__init__.py	(revision 946)
+++ genshi/filters/__init__.py	(working copy)
@@ -14,7 +14,7 @@
 """Implementation of a number of stream filters."""
 
 from genshi.filters.html import HTMLFormFiller, HTMLSanitizer
-from genshi.filters.i18n import Translator
+from genshi.filters.i18n import Translator, setup_i18n
 from genshi.filters.transform import Transformer
 
 __docformat__ = 'restructuredtext en'
Index: genshi/filters/i18n.py
===================================================================
--- genshi/filters/i18n.py	(revision 946)
+++ genshi/filters/i18n.py	(working copy)
@@ -11,19 +11,23 @@
 # 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 compiler import ast
 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
-from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
+from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
+                        XML_NAMESPACE, _ensure
+from genshi.template.base import Context, DirectiveFactory, EXPR, SUB, \
+                                 _apply_directives
 from genshi.template.directives import Directive
 from genshi.template.markup import MarkupTemplate, EXEC
 
@@ -32,50 +36,503 @@
 
 I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n')
 
+class DirectiveExtract(object):
+    """Simple interface for directives to support messages extraction"""
 
-class CommentDirective(Directive):
+    def extract(self, stream, ctxt):
+        raise NotImplementedError
 
-    __slots__ = []
+class CommentDirective(Directive):
+    """Implementation of the ``i18n:comment`` template directive which adds
+    translation comments.
+    
+    >>> from genshi.filters.i18n import Translator, setup_i18n
+    >>> from genshi.template import MarkupTemplate
+    >>>
+    >>> translator = Translator()
+    >>>
+    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+    ...   <p i18n:comment="As in Foo Bar">Foo</p>
+    ... </html>''')
+    >>>
+    >>> setup_i18n(tmpl, translator)
+    >>> list(translator.extract(tmpl.stream))
+    [(2, None, u'Foo', [u'As in Foo Bar'])]
+    >>>
+    """
 
-    @classmethod
-    def attach(cls, template, stream, value, namespaces, pos):
-        return None, stream
+    __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(Directive):
+class MsgDirective(Directive, DirectiveExtract):
+    r"""Implementation of the ``i18n:msg`` directive which marks inner content
+    as translatable. Consider the following examples:
+    
+    >>> from genshi.filters.i18n import Translator, setup_i18n
+    >>> from genshi.template import MarkupTemplate
+    >>>
+    >>> translator = Translator()
+    >>>
+    >>> 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>''')
+    >>>
+    >>> setup_i18n(tmpl, translator)
+    >>>
+    >>> 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>''')
+    >>> setup_i18n(tmpl, translator)
+    >>> 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>''')
+    >>> setup_i18n(tmpl, translator)
+    >>> print tmpl.generate(fname='John', lname='Doe').render()
+    <html>
+      <div><p>First Name: John
+        <p>Last Name: Doe</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):
+
+        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)
+
         msgbuf = MessageBuffer(self.params)
 
         stream = iter(stream)
         yield stream.next() # the outer start tag
         previous = stream.next()
-        for event in stream:
-            msgbuf.append(*previous)
-            previous = event
+        for kind, data, pos in stream:
+            if kind is SUB:
+                # py:attrs for example
+                subdirectives, substream = data
+                for skind, sdata, spos in _apply_directives(substream,
+                                                            subdirectives,
+                                                            ctxt):
+                    try:
+                        msgbuf.append(*previous)
+                        previous = skind, sdata, spos
+                    except IndexError:
+                        raise IndexError("Not enough parameters passed to '%s' "
+                                         "on '%s', line number %s: %s" %
+                                         (type(self).__name__,
+                                          os.path.basename(spos[0]), spos[1],
+                                          self.params))
+            try:
+                msgbuf.append(*previous)
+            except IndexError:
+                raise IndexError("Not enough parameters passed to '%s' on '%s',"
+                                 " line number %s: %s" %
+                                 (type(self).__name__,
+                                  os.path.basename(previous[2][0]),
+                                  previous[2][1], self.params), previous[1])
+            previous = kind, data, pos
 
-        gettext = ctxt.get('_i18n.gettext')
         for event in msgbuf.translate(gettext(msgbuf.format())):
             yield event
 
         yield previous # the outer end tag
 
+    def extract(self, stream, ctxt):
+
+        msgbuf = MessageBuffer(self.params)
+
+        stream = iter(stream)
+        stream.next() # the outer start tag
+        previous = stream.next()
+        for event in stream:
+            try:
+                msgbuf.append(*previous)
+            except IndexError:
+                raise IndexError("Not enough parameters passed to '%s' on '%s',"
+                                 " line number %s: %s" %
+                                 (type(self).__name__,
+                                  os.path.basename(previous[2][0]),
+                                  previous[2][1], self.params))
+            previous = event
+
+        yield None, msgbuf.format(), filter(None, [ctxt.get('_i18n.comment')])
+
+class InnerChooseDirective(Directive):
+    __slots__ = []
+
+    def __call__(self, stream, directives, ctxt, **vars):
+
+        msgbuf = MessageBuffer(ctxt.get('_i18n.choose.params', [])[:])
+
+        stream = iter(stream)
+        yield stream.next() # the outer start tag
+        previous = stream.next()
+#        if previous[0] is TEXT and not previous[1].strip():
+#            yield previous  # white space and newlines
+        for kind, data, pos in stream:
+
+            msgbuf.append(*previous)
+            previous = kind, data, pos
+#            if event[0] is TEXT and not event[1].strip():
+#                yield event # white space and newlines
+        yield None, None, None # the place holder for msgbuf output
+        yield previous # the outer end tag
+        ctxt['_i18n.choose.%s' % type(self).__name__] = msgbuf
+
+
+    def extract(self, stream, ctxt, msgbuf):
+
+        stream = iter(stream)
+        stream.next() # the outer start tag
+        previous = stream.next()
+        for event in stream:
+            msgbuf.append(*previous)
+            previous = event
+        return msgbuf
+
+
+class SingularDirective(InnerChooseDirective):
+    """Implementation of the ``i18n:singular`` directive to be used with the
+    ``i18n:choose`` directive."""
+
+
+class PluralDirective(InnerChooseDirective):
+    """Implementation of the ``i18n:plural`` directive to be used with the
+    ``i18n:choose`` directive."""
+
+
+class ChooseDirective(Directive, DirectiveExtract):
+    """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:
+    
+    >>> from genshi.filters.i18n import Translator, setup_i18n
+    >>> from genshi.template import MarkupTemplate
+    >>>
+    >>> translator = Translator()
+    >>>
+    >>> 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>''')
+    >>> setup_i18n(tmpl, translator)
+    >>> 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>''')
+    >>> setup_i18n(tmpl, translator)
+    >>> 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>''')
+    >>> setup_i18n(tmpl, translator)
+    >>> 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))
+                    new_stream.append((None, None, None)) # 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))
+                    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 not kind and not data and not pos:
+                for skind, sdata, spos in singular_stream:
+                    if not skind and not sdata and not spos:
+                        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, ctxt):
+
+        stream = iter(stream)
+        previous = stream.next()
+        if previous is START:
+            stream.next()
+
+        singular_msgbuf = MessageBuffer(self.params[:])
+        plural_msgbuf = MessageBuffer(self.params[:])
+
+        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, ctxt,
+                                                               singular_msgbuf)
+                    elif isinstance(subdirective, PluralDirective):
+                        plural_msgbuf = subdirective.extract(substream, ctxt,
+                                                             plural_msgbuf)
+                    else:
+                        try:
+                            singular_msgbuf.append(kind, event, pos)
+                            plural_msgbuf.append(kind, event, pos)
+                        except IndexError:
+                            raise IndexError("Not enough parameters passed to "
+                                             "'%s' on '%s', line number %s: "
+                                             "%s" % (type(self).__name__,
+                                                     os.path.basename(pos[0]),
+                                                     pos[1], self.params))
+            else:
+                try:
+                    singular_msgbuf.append(kind, event, pos)
+                    plural_msgbuf.append(kind, event, pos)
+                except IndexError:
+                    raise IndexError("Not enough parameters passed to '%s' on "
+                                     "'%s', line number %s: %s" %
+                                     (type(self).__name__,
+                                      os.path.basename(pos[0]), pos[1],
+                                      self.params))
+
+        yield 'ngettext', \
+            (singular_msgbuf.format(), plural_msgbuf.format()), \
+            filter(None, [ctxt.get('_i18n.comment')])
+
+class DomainDirective(Directive):
+    """Implementation of the ``i18n:domain`` directive which allows choosing
+    another i18n domain(catalog) to translate from.
+    
+    >>> from gettext import NullTranslations
+    >>> from genshi.filters.i18n import Translator, setup_i18n
+    >>> from genshi.template.markup import MarkupTemplate
+    >>>
+    >>> class DummyTranslations(NullTranslations):
+    ...     _domains = {}
+    ...     def __init__(self, catalog):
+    ...         NullTranslations.__init__(self)
+    ...         self._catalog = catalog
+    ...     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()
+    ...         tmsg = self._catalog.get(message, missing)
+    ...         if tmsg is missing:
+    ...             if self._fallback:
+    ...                 return self._fallback.ugettext(message)
+    ...             return unicode(message)
+    ...         return tmsg
+    ...     def dugettext(self, domain, message):
+    ...         return self._domain_call('ugettext', domain, message)
+    ...
+    >>>
+    >>> 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)
+    >>> setup_i18n(tmpl, translator)
+    >>>
+    >>> 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
+
+    @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):
+            yield event
+        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 +551,7 @@
     ...         '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 +572,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 +607,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 +639,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 x: dgettext(ctxt.get('_i18n.domain'), x)
+
         for kind, data, pos in stream:
 
             # skip chunks that should not be localized
@@ -208,14 +691,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 +718,24 @@
 
             elif kind is SUB:
                 directives, substream = data
-                # If this is an i18n:msg directive, no need to translate text
+                # Is there a DomainDirective defined ?
+                current_domain = [d.domain for d in directives if
+                                  isinstance(d, DomainDirective)]
+                if current_domain:
+                    # Domain defined, lets update the context with it
+                    ctxt.push({'_i18n.domain': current_domain[0]})
+
+                # 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, DirectiveExtract)
+                                            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 +743,7 @@
                          'ugettext', 'ungettext')
 
     def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
-                search_text=True, msgbuf=None):
+                search_text=True, msgbuf=None, ctxt=Context()):
         """Extract localizable strings from the given template stream.
         
         For every string found, this function yields a ``(lineno, function,
@@ -265,7 +759,7 @@
            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 +770,7 @@
         ...     <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'
@@ -291,22 +785,26 @@
                                   functions
         :param search_text: whether the content of text nodes should be
                             extracted (used internally)
+        :param ctxt: the current extraction context (used internaly)
         
         :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
+               which is a list of comments for the translator. Added an ``ctxt``
+               argument which is used to pass arround the current extraction
+               context.
         """
         if not self.extract_text:
             search_text = False
         skip = 0
-        i18n_comment = I18N_NAMESPACE['comment']
-        i18n_msg = I18N_NAMESPACE['msg']
+
+        # Un-comment bellow to extract messages without adding directives
+#        i18n_comment = I18N_NAMESPACE['comment']
+#        i18n_msg = I18N_NAMESPACE['msg']
         xml_lang = XML_NAMESPACE['lang']
 
         for kind, data, pos in stream:
-
             if skip:
                 if kind is START:
                     skip += 1
@@ -335,20 +833,24 @@
 
                 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]
-                        )
+                # Un-comment bellow to extract messages without adding
+                # directives
+#                else:
+#                    msg_params = attrs.get(i18n_msg)
+#                    if msg_params is not None:
+#                        print kind, data, pos
+#                        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, \
+                                    filter(None, [ctxt.get('_i18n.comment')])
                 else:
                     msgbuf.append(kind, data, pos)
 
@@ -356,7 +858,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:
@@ -367,13 +869,39 @@
                     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
 
+                comment = None
+                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):
+                        comment = directive.comment
+                        ctxt.push({'_i18n.comment': 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, ctxt=ctxt)
+                            for lineno, funcname, text, comments in messages:
+                                yield lineno, funcname, text, comments
+                        directives.pop(idx)
+
+                for directive in directives:
+                    if isinstance(directive, DirectiveExtract):
+                        messages = directive.extract(substream, ctxt)
+                        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 comment:
+                    ctxt.pop()
 
 class MessageBuffer(object):
     """Helper class for managing internationalized mixed content.
@@ -408,6 +936,10 @@
         :param data: the event data
         :param pos: the position of the event in the source
         """
+        if kind is SUB:
+            # py:attrs for example
+            for skind, sdata, spos in data[1]:
+                self.append(skind, sdata, spos)
         if kind is TEXT:
             self.string.append(data)
             self.events.setdefault(self.stack[-1], []).append(None)
@@ -464,19 +996,19 @@
 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`
@@ -514,7 +1046,7 @@
     >>> 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))
@@ -584,6 +1116,20 @@
 
     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
+
+def setup_i18n(template, translator):
+    """Convinience function to setup both the i18n filter and the i18n
+    directives.
+    
+    :param template: an instance of a genshi template
+    :param translator: an instance of ``Translator``
+    """
+    template.filters.insert(0, translator)
+    if hasattr(template, 'add_directives'):
+        template.add_directives(Translator.NAMESPACE, translator)
