Edgewall Software

Ticket #129: i18n_directives_full.patch

File i18n_directives_full.patch, 74.8 KB (added by palgarvio, 7 years ago)
  • genshi/filters/tests/i18n.py

     
    1313
    1414from datetime import datetime
    1515import doctest
    16 from gettext import NullTranslations
     16from gettext import NullTranslations, c2py
    1717from StringIO import StringIO
    1818import unittest
    1919
    2020from genshi.core import Attrs
    2121from genshi.template import MarkupTemplate
    22 from genshi.filters.i18n import Translator, extract
     22from genshi.filters.i18n import Translator, extract, setup_i18n
    2323from genshi.input import HTML
    2424
    2525
    2626class DummyTranslations(NullTranslations):
     27    _domains = {}
    2728
    28     def __init__(self, catalog):
     29    def __init__(self, catalog=()):
    2930        NullTranslations.__init__(self)
    30         self._catalog = catalog
     31        self._catalog = catalog or {}
     32        self.plural = c2py('(n != 1)')
     33       
     34    def add_domain(self, domain, catalog):
     35        translation = DummyTranslations(catalog)
     36        translation.add_fallback(self)
     37        self._domains[domain] = translation
     38       
     39    def _domain_call(self, func, domain, *args, **kwargs):
     40        return getattr(self._domains.get(domain, self), func)(*args, **kwargs)
    3141
    3242    def ugettext(self, message):
    3343        missing = object()
     
    3747                return self._fallback.ugettext(message)
    3848            return unicode(message)
    3949        return tmsg
     50   
     51    def dugettext(self, domain, message):
     52        return self._domain_call('ugettext', domain, message)
     53   
     54    def ungettext(self, msgid1, msgid2, n):
     55        try:
     56            return self._catalog[(msgid1, self.plural(n))]
     57        except KeyError:
     58            if self._fallback:
     59                return self._fallback.ngettext(msgid1, msgid2, n)
     60            if n == 1:
     61                return msgid1
     62            else:
     63                return msgid2
     64           
     65    def dungettext(self, domain, singular, plural, numeral):
     66        return self._domain_call('ungettext', domain, singular, plural, numeral)
    4067
    4168
    4269class TranslatorTestCase(unittest.TestCase):
     
    162189          </p>
    163190        </html>""")
    164191        translator = Translator()
     192        tmpl.add_directives(Translator.NAMESPACE, translator)
    165193        messages = list(translator.extract(tmpl.stream))
    166194        self.assertEqual(1, len(messages))
    167195        self.assertEqual('Please see [1:Help] for details.', messages[0][2])
     
    175203        </html>""")
    176204        gettext = lambda s: u"Für Details siehe bitte [1:Hilfe]."
    177205        translator = Translator(gettext)
    178         tmpl.filters.insert(0, translator)
    179         tmpl.add_directives(Translator.NAMESPACE, translator)
     206        setup_i18n(tmpl, translator)
    180207        self.assertEqual("""<html>
    181208          <p>Für Details siehe bitte <a href="help.html">Hilfe</a>.</p>
    182209        </html>""", tmpl.generate().render())
     
    189216          </p>
    190217        </html>""")
    191218        translator = Translator()
     219        tmpl.add_directives(Translator.NAMESPACE, translator)
    192220        messages = list(translator.extract(tmpl.stream))
    193221        self.assertEqual(1, len(messages))
    194222        self.assertEqual('Please see [1:[2:Help] page] for details.',
     
    203231        </html>""")
    204232        gettext = lambda s: u"Für Details siehe bitte [1:[2:Hilfeseite]]."
    205233        translator = Translator(gettext)
    206         tmpl.filters.insert(0, translator)
    207         tmpl.add_directives(Translator.NAMESPACE, translator)
     234        setup_i18n(tmpl, translator)
    208235        self.assertEqual("""<html>
    209236          <p>Für Details siehe bitte <a href="help.html"><em>Hilfeseite</em></a>.</p>
    210237        </html>""", tmpl.generate().render())
     
    217244          </p>
    218245        </html>""")
    219246        translator = Translator()
     247        tmpl.add_directives(Translator.NAMESPACE, translator)
    220248        messages = list(translator.extract(tmpl.stream))
    221249        self.assertEqual(1, len(messages))
    222250        self.assertEqual('Show me [1:] entries per page.', messages[0][2])
     
    230258        </html>""")
    231259        gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
    232260        translator = Translator(gettext)
    233         tmpl.filters.insert(0, translator)
    234         tmpl.add_directives(Translator.NAMESPACE, translator)
     261        setup_i18n(tmpl, translator)
    235262        self.assertEqual("""<html>
    236263          <p><input type="text" name="num"/> Einträge pro Seite anzeigen.</p>
    237264        </html>""", tmpl.generate().render())
     
    244271          </p>
    245272        </html>""")
    246273        translator = Translator()
     274        tmpl.add_directives(Translator.NAMESPACE, translator)
    247275        messages = list(translator.extract(tmpl.stream))
    248276        self.assertEqual(1, len(messages))
    249277        self.assertEqual('Please see [1:Help] for [2:details].', messages[0][2])
     
    257285        </html>""")
    258286        gettext = lambda s: u"Für [2:Details] siehe bitte [1:Hilfe]."
    259287        translator = Translator(gettext)
    260         tmpl.filters.insert(0, translator)
    261         tmpl.add_directives(Translator.NAMESPACE, translator)
     288        setup_i18n(tmpl, translator)
    262289        self.assertEqual("""<html>
    263290          <p>Für <em>Details</em> siehe bitte <a href="help.html">Hilfe</a>.</p>
    264291        </html>""", tmpl.generate().render())
     
    271298          </p>
    272299        </html>""")
    273300        translator = Translator()
     301        tmpl.add_directives(Translator.NAMESPACE, translator)
    274302        messages = list(translator.extract(tmpl.stream))
    275303        self.assertEqual(1, len(messages))
    276304        self.assertEqual('Show me [1:] entries per page, starting at page [2:].',
     
    285313        </html>""")
    286314        gettext = lambda s: u"[1:] Einträge pro Seite, beginnend auf Seite [2:]."
    287315        translator = Translator(gettext)
    288         tmpl.filters.insert(0, translator)
    289         tmpl.add_directives(Translator.NAMESPACE, translator)
     316        setup_i18n(tmpl, translator)
    290317        self.assertEqual("""<html>
    291318          <p><input type="text" name="num"/> Eintr\xc3\xa4ge pro Seite, beginnend auf Seite <input type="text" name="num"/>.</p>
    292319        </html>""", tmpl.generate().render())
     
    299326          </p>
    300327        </html>""")
    301328        translator = Translator()
     329        tmpl.add_directives(Translator.NAMESPACE, translator)
    302330        messages = list(translator.extract(tmpl.stream))
    303331        self.assertEqual(1, len(messages))
    304332        self.assertEqual('Hello, %(name)s!', messages[0][2])
     
    312340        </html>""")
    313341        gettext = lambda s: u"Hallo, %(name)s!"
    314342        translator = Translator(gettext)
    315         tmpl.filters.insert(0, translator)
    316         tmpl.add_directives(Translator.NAMESPACE, translator)
     343        setup_i18n(tmpl, translator)
    317344        self.assertEqual("""<html>
    318345          <p>Hallo, Jim!</p>
    319346        </html>""", tmpl.generate(user=dict(name='Jim')).render())
     
    327354        </html>""")
    328355        gettext = lambda s: u"%(name)s, sei gegrüßt!"
    329356        translator = Translator(gettext)
    330         tmpl.filters.insert(0, translator)
    331         tmpl.add_directives(Translator.NAMESPACE, translator)
     357        setup_i18n(tmpl, translator)
    332358        self.assertEqual("""<html>
    333359          <p>Jim, sei gegrüßt!</p>
    334360        </html>""", tmpl.generate(user=dict(name='Jim')).render())
     
    342368        </html>""")
    343369        gettext = lambda s: u"Sei gegrüßt, [1:Alter]!"
    344370        translator = Translator(gettext)
    345         tmpl.filters.insert(0, translator)
    346         tmpl.add_directives(Translator.NAMESPACE, translator)
     371        setup_i18n(tmpl, translator)
    347372        self.assertEqual("""<html>
    348373          <p>Sei gegrüßt, <a href="#42">Alter</a>!</p>
    349374        </html>""", tmpl.generate(anchor='42').render())
     
    356381          </p>
    357382        </html>""")
    358383        translator = Translator()
     384        tmpl.add_directives(Translator.NAMESPACE, translator)
    359385        messages = list(translator.extract(tmpl.stream))
    360386        self.assertEqual(1, len(messages))
    361387        self.assertEqual('Posted by %(name)s at %(time)s', messages[0][2])
     
    369395        </html>""")
    370396        gettext = lambda s: u"%(name)s schrieb dies um %(time)s"
    371397        translator = Translator(gettext)
    372         tmpl.filters.insert(0, translator)
    373         tmpl.add_directives(Translator.NAMESPACE, translator)
     398        setup_i18n(tmpl, translator)
    374399        entry = {
    375400            'author': 'Jim',
    376401            'time': datetime(2008, 4, 1, 14, 30)
     
    387412          </p>
    388413        </html>""")
    389414        translator = Translator()
     415        tmpl.add_directives(Translator.NAMESPACE, translator)
    390416        messages = list(translator.extract(tmpl.stream))
    391417        self.assertEqual(1, len(messages))
    392418        self.assertEqual('Show me [1:] entries per page.', messages[0][2])
    393419
    394     # FIXME: this currently fails :-/
    395 #    def test_translate_i18n_msg_with_directive(self):
    396 #        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
    397 #            xmlns:i18n="http://genshi.edgewall.org/i18n">
    398 #          <p i18n:msg="">
    399 #            Show me <input type="text" name="num" py:attrs="{'value': x}" /> entries per page.
    400 #          </p>
    401 #        </html>""")
    402 #        gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
    403 #        tmpl.filters.insert(0, Translator(gettext))
    404 #        self.assertEqual("""<html>
    405 #          <p><input type="text" name="num" value="x"/> Einträge pro Seite anzeigen.</p>
    406 #        </html>""", tmpl.generate().render())
     420    def test_translate_i18n_msg_with_directive(self):
     421        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     422            xmlns:i18n="http://genshi.edgewall.org/i18n">
     423          <p i18n:msg="">
     424            Show me <input type="text" name="num" py:attrs="{'value': 'x'}" /> entries per page.
     425          </p>
     426        </html>""")
     427        gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
     428        translator = Translator(gettext)
     429        setup_i18n(tmpl, translator)
     430        self.assertEqual("""<html>
     431          <p><input type="text" name="num" value="x"/> Einträge pro Seite anzeigen.</p>
     432        </html>""", tmpl.generate().render())
    407433
    408434    def test_extract_i18n_msg_with_comment(self):
    409435        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
    410436            xmlns:i18n="http://genshi.edgewall.org/i18n">
     437          <p i18n:comment="As in foo bar" i18n:msg="">Foo</p>
     438        </html>""")
     439        translator = Translator()
     440        tmpl.add_directives(Translator.NAMESPACE, translator)
     441        messages = list(translator.extract(tmpl.stream))
     442        self.assertEqual(1, len(messages))
     443        self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0])
     444        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     445            xmlns:i18n="http://genshi.edgewall.org/i18n">
    411446          <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
    412447        </html>""")
    413448        translator = Translator()
     449        tmpl.add_directives(Translator.NAMESPACE, translator)
    414450        messages = list(translator.extract(tmpl.stream))
    415451        self.assertEqual(1, len(messages))
    416452        self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0])
     
    422458        </html>""")
    423459        gettext = lambda s: u"Voh"
    424460        translator = Translator(gettext)
    425         tmpl.filters.insert(0, translator)
    426         tmpl.add_directives(Translator.NAMESPACE, translator)
     461        setup_i18n(tmpl, translator)
    427462        self.assertEqual("""<html>
    428463          <p>Voh</p>
    429464        </html>""", tmpl.generate().render())
     
    461496          <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
    462497        </html>""")
    463498        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
    464         tmpl.filters.insert(0, translator)
    465         tmpl.add_directives(Translator.NAMESPACE, translator)
     499        setup_i18n(tmpl, translator)
     500        self.assertEqual("""<html>
     501          <p>Voh</p>
     502        </html>""", tmpl.generate().render())
     503       
     504    def test_translate_i18n_domain_with_msg_directives(self):
     505        #"""translate with i18n:domain and nested i18n:msg directives """
     506
     507        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     508            xmlns:i18n="http://genshi.edgewall.org/i18n">
     509          <div i18n:domain="foo">
     510            <p i18n:msg="">FooBar</p>
     511            <p i18n:msg="">Bar</p>
     512          </div>
     513        </html>""")
     514        translations = DummyTranslations({'Bar': 'Voh'})
     515        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     516        translator = Translator(translations)
     517        setup_i18n(tmpl, translator)
     518        self.assertEqual("""<html>
     519          <div>
     520            <p>BarFoo</p>
     521            <p>PT_Foo</p>
     522          </div>
     523        </html>""", tmpl.generate().render())
     524       
     525    def test_translate_i18n_domain_with_inline_directives(self):
     526        #"""translate with inlined i18n:domain and i18n:msg directives"""
     527        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     528            xmlns:i18n="http://genshi.edgewall.org/i18n">
     529          <p i18n:msg="" i18n:domain="foo">FooBar</p>
     530        </html>""")
     531        translations = DummyTranslations({'Bar': 'Voh'})
     532        translations.add_domain('foo', {'FooBar': 'BarFoo'})
     533        translator = Translator(translations)
     534        setup_i18n(tmpl, translator)
     535        self.assertEqual("""<html>
     536          <p>BarFoo</p>
     537        </html>""", tmpl.generate().render())
     538       
     539    def test_translate_i18n_domain_without_msg_directives(self):
     540        #"""translate domain call without i18n:msg directives still uses current domain"""
     541       
     542        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     543            xmlns:i18n="http://genshi.edgewall.org/i18n">
     544          <p i18n:msg="">Bar</p>
     545          <div i18n:domain="foo">
     546            <p i18n:msg="">FooBar</p>
     547            <p i18n:msg="">Bar</p>           
     548            <p>Bar</p>
     549          </div>         
     550          <p>Bar</p>
     551        </html>""")
     552        translations = DummyTranslations({'Bar': 'Voh'})
     553        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     554        translator = Translator(translations)
     555        setup_i18n(tmpl, translator)
     556        self.assertEqual("""<html>
     557          <p>Voh</p>
     558          <div>
     559            <p>BarFoo</p>
     560            <p>PT_Foo</p>
     561            <p>PT_Foo</p>
     562          </div>
     563          <p>Voh</p>
     564        </html>""", tmpl.generate().render())
     565       
     566    def test_translate_i18n_domain_as_directive_not_attribute(self):
     567        #"""translate with domain as directive"""
     568       
     569        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     570            xmlns:i18n="http://genshi.edgewall.org/i18n">
     571        <i18n:domain name="foo">
     572          <p i18n:msg="">FooBar</p>
     573          <p i18n:msg="">Bar</p>
     574          <p>Bar</p>
     575        </i18n:domain>
     576          <p>Bar</p>
     577        </html>""")
     578        translations = DummyTranslations({'Bar': 'Voh'})
     579        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     580        translator = Translator(translations)
     581        setup_i18n(tmpl, translator)
     582        self.assertEqual("""<html>
     583          <p>BarFoo</p>
     584          <p>PT_Foo</p>
     585          <p>PT_Foo</p>
     586          <p>Voh</p>
     587        </html>""", tmpl.generate().render())
     588       
     589    def test_translate_i18n_domain_nested_directives(self):
     590        #"""translate with nested i18n:domain directives"""
     591       
     592        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     593            xmlns:i18n="http://genshi.edgewall.org/i18n">
     594          <p i18n:msg="">Bar</p>
     595          <div i18n:domain="foo">
     596            <p i18n:msg="">FooBar</p>
     597            <p i18n:domain="bar" i18n:msg="">Bar</p>           
     598            <p>Bar</p>
     599          </div>         
     600          <p>Bar</p>
     601        </html>""")
     602        translations = DummyTranslations({'Bar': 'Voh'})
     603        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     604        translations.add_domain('bar', {'Bar': 'bar_Bar'})
     605        translator = Translator(translations)
     606        setup_i18n(tmpl, translator)
     607        self.assertEqual("""<html>
     608          <p>Voh</p>
     609          <div>
     610            <p>BarFoo</p>
     611            <p>bar_Bar</p>
     612            <p>foo_Bar</p>
     613          </div>
     614          <p>Voh</p>
     615        </html>""", tmpl.generate().render())
     616       
     617    def test_translate_i18n_domain_with_empty_nested_domain_directive(self):
     618        #"""translate with empty nested i18n:domain directive does not use dngettext"""
     619       
     620        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     621            xmlns:i18n="http://genshi.edgewall.org/i18n">
     622          <p i18n:msg="">Bar</p>
     623          <div i18n:domain="foo">
     624            <p i18n:msg="">FooBar</p>
     625            <p i18n:domain="" i18n:msg="">Bar</p>           
     626            <p>Bar</p>
     627          </div>         
     628          <p>Bar</p>
     629        </html>""")
     630        translations = DummyTranslations({'Bar': 'Voh'})
     631        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     632        translations.add_domain('bar', {'Bar': 'bar_Bar'})
     633        translator = Translator(translations)
     634        setup_i18n(tmpl, translator)
    466635        self.assertEqual("""<html>
    467636          <p>Voh</p>
     637          <div>
     638            <p>BarFoo</p>
     639            <p>Voh</p>
     640            <p>foo_Bar</p>
     641          </div>
     642          <p>Voh</p>
    468643        </html>""", tmpl.generate().render())
    469644
     645    def test_translate_i18n_choose_as_attribute(self):
     646        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     647            xmlns:i18n="http://genshi.edgewall.org/i18n">
     648          <div i18n:choose="one">
     649            <p i18n:singular="">FooBar</p>
     650            <p i18n:plural="">FooBars</p>
     651          </div>
     652          <div i18n:choose="two">
     653            <p i18n:singular="">FooBar</p>
     654            <p i18n:plural="">FooBars</p>
     655          </div>
     656        </html>""")
     657        translations = DummyTranslations()
     658        translator = Translator(translations)
     659        setup_i18n(tmpl, translator)
     660        self.assertEqual("""<html>
     661          <div>
     662            <p>FooBar</p>
     663          </div>
     664          <div>
     665            <p>FooBars</p>
     666          </div>
     667        </html>""", tmpl.generate(one=1, two=2).render())
     668       
     669    def test_translate_i18n_choose_as_directive(self):
     670        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     671            xmlns:i18n="http://genshi.edgewall.org/i18n">
     672        <i18n:choose numeral="two">
     673          <p i18n:singular="">FooBar</p>
     674          <p i18n:plural="">FooBars</p>
     675        </i18n:choose>
     676        <i18n:choose numeral="one">
     677          <p i18n:singular="">FooBar</p>
     678          <p i18n:plural="">FooBars</p>
     679        </i18n:choose>
     680        </html>""")
     681        translations = DummyTranslations()
     682        translator = Translator(translations)
     683        setup_i18n(tmpl, translator)
     684        self.assertEqual("""<html>
     685          <p>FooBars</p>
     686          <p>FooBar</p>
     687        </html>""", tmpl.generate(one=1, two=2).render())
     688       
     689    def test_translate_i18n_choose_as_attribute_with_params(self):
     690        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     691            xmlns:i18n="http://genshi.edgewall.org/i18n">
     692          <div i18n:choose="two; fname, lname">
     693            <p i18n:singular="">Foo $fname $lname</p>
     694            <p i18n:plural="">Foos $fname $lname</p>
     695          </div>
     696        </html>""")
     697        translations = DummyTranslations({
     698            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     699            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     700                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     701                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     702        })
     703        translator = Translator(translations)
     704        setup_i18n(tmpl, translator)
     705        self.assertEqual("""<html>
     706          <div>
     707            <p>Vohs John Doe</p>
     708          </div>
     709        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     710       
     711    def test_translate_i18n_choose_as_attribute_with_params_and_domain_as_param(self):
     712        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     713            xmlns:i18n="http://genshi.edgewall.org/i18n"
     714            i18n:domain="foo">
     715          <div i18n:choose="two; fname, lname">
     716            <p i18n:singular="">Foo $fname $lname</p>
     717            <p i18n:plural="">Foos $fname $lname</p>
     718          </div>
     719        </html>""")
     720        translations = DummyTranslations()
     721        translations.add_domain('foo', {
     722            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     723            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     724                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     725                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     726        })
     727        translator = Translator(translations)
     728        setup_i18n(tmpl, translator)
     729        self.assertEqual("""<html>
     730          <div>
     731            <p>Vohs John Doe</p>
     732          </div>
     733        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     734       
     735    def test_translate_i18n_choose_as_directive_with_params(self):
     736        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     737            xmlns:i18n="http://genshi.edgewall.org/i18n">
     738        <i18n:choose numeral="two" params="fname, lname">
     739          <p i18n:singular="">Foo ${fname} ${lname}</p>
     740          <p i18n:plural="">Foos ${fname} ${lname}</p>
     741        </i18n:choose>
     742        <i18n:choose numeral="one" params="fname, lname">
     743          <p i18n:singular="">Foo ${fname} ${lname}</p>
     744          <p i18n:plural="">Foos ${fname} ${lname}</p>
     745        </i18n:choose>
     746        </html>""")
     747        translations = DummyTranslations({
     748            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     749            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     750                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     751                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     752        })
     753        translator = Translator(translations)
     754        setup_i18n(tmpl, translator)
     755        self.assertEqual("""<html>
     756          <p>Vohs John Doe</p>
     757          <p>Voh John Doe</p>
     758        </html>""", tmpl.generate(one=1, two=2,
     759                                  fname='John', lname='Doe').render())
     760       
     761    def test_translate_i18n_choose_as_directive_with_params_and_domain_as_directive(self):
     762        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     763            xmlns:i18n="http://genshi.edgewall.org/i18n">
     764        <i18n:domain name="foo">
     765        <i18n:choose numeral="two" params="fname, lname">
     766          <p i18n:singular="">Foo ${fname} ${lname}</p>
     767          <p i18n:plural="">Foos ${fname} ${lname}</p>
     768        </i18n:choose>
     769        </i18n:domain>
     770        <i18n:choose numeral="one" params="fname, lname">
     771          <p i18n:singular="">Foo ${fname} ${lname}</p>
     772          <p i18n:plural="">Foos ${fname} ${lname}</p>
     773        </i18n:choose>
     774        </html>""")
     775        translations = DummyTranslations()
     776        translations.add_domain('foo', {
     777            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     778            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     779                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     780                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     781        })
     782        translator = Translator(translations)
     783        setup_i18n(tmpl, translator)
     784        self.assertEqual("""<html>
     785          <p>Vohs John Doe</p>
     786          <p>Foo John Doe</p>
     787        </html>""", tmpl.generate(one=1, two=2,
     788                                  fname='John', lname='Doe').render())
     789
     790    def test_extract_i18n_choose_as_attribute(self):
     791        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     792            xmlns:i18n="http://genshi.edgewall.org/i18n">
     793          <div i18n:choose="one">
     794            <p i18n:singular="">FooBar</p>
     795            <p i18n:plural="">FooBars</p>
     796          </div>
     797          <div i18n:choose="two">
     798            <p i18n:singular="">FooBar</p>
     799            <p i18n:plural="">FooBars</p>
     800          </div>
     801        </html>""")
     802        translator = Translator()
     803        tmpl.add_directives(Translator.NAMESPACE, translator)
     804        messages = list(translator.extract(tmpl.stream))
     805        self.assertEqual(2, len(messages))
     806        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     807        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     808       
     809    def test_extract_i18n_choose_as_directive(self):
     810        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     811            xmlns:i18n="http://genshi.edgewall.org/i18n">
     812        <i18n:choose numeral="two">
     813          <p i18n:singular="">FooBar</p>
     814          <p i18n:plural="">FooBars</p>
     815        </i18n:choose>
     816        <i18n:choose numeral="one">
     817          <p i18n:singular="">FooBar</p>
     818          <p i18n:plural="">FooBars</p>
     819        </i18n:choose>
     820        </html>""")
     821        translator = Translator()
     822        tmpl.add_directives(Translator.NAMESPACE, translator)
     823        messages = list(translator.extract(tmpl.stream))
     824        self.assertEqual(2, len(messages))
     825        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     826        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     827       
     828    def test_extract_i18n_choose_as_attribute_with_params(self):
     829        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     830            xmlns:i18n="http://genshi.edgewall.org/i18n">
     831          <div i18n:choose="two; fname, lname">
     832            <p i18n:singular="">Foo $fname $lname</p>
     833            <p i18n:plural="">Foos $fname $lname</p>
     834          </div>
     835        </html>""")
     836        translator = Translator()
     837        tmpl.add_directives(Translator.NAMESPACE, translator)
     838        messages = list(translator.extract(tmpl.stream))
     839        self.assertEqual(1, len(messages))
     840        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     841                                          u'Foos %(fname)s %(lname)s'), []),
     842                         messages[0])
     843
     844    def test_extract_i18n_choose_as_attribute_with_params_and_domain_as_param(self):
     845        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     846            xmlns:i18n="http://genshi.edgewall.org/i18n"
     847            i18n:domain="foo">
     848          <div i18n:choose="two; fname, lname">
     849            <p i18n:singular="">Foo $fname $lname</p>
     850            <p i18n:plural="">Foos $fname $lname</p>
     851          </div>
     852        </html>""")
     853        translator = Translator()
     854        tmpl.add_directives(Translator.NAMESPACE, translator)
     855        messages = list(translator.extract(tmpl.stream))
     856        self.assertEqual(1, len(messages))
     857        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
     858                                          u'Foos %(fname)s %(lname)s'), []),
     859                         messages[0])
     860
     861    def test_extract_i18n_choose_as_directive_with_params(self):
     862        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     863            xmlns:i18n="http://genshi.edgewall.org/i18n">
     864        <i18n:choose numeral="two" params="fname, lname">
     865          <p i18n:singular="">Foo ${fname} ${lname}</p>
     866          <p i18n:plural="">Foos ${fname} ${lname}</p>
     867        </i18n:choose>
     868        <i18n:choose numeral="one" params="fname, lname">
     869          <p i18n:singular="">Foo ${fname} ${lname}</p>
     870          <p i18n:plural="">Foos ${fname} ${lname}</p>
     871        </i18n:choose>
     872        </html>""")
     873        translator = Translator()
     874        tmpl.add_directives(Translator.NAMESPACE, translator)
     875        messages = list(translator.extract(tmpl.stream))
     876        self.assertEqual(2, len(messages))
     877        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     878                                          u'Foos %(fname)s %(lname)s'), []),
     879                         messages[0])
     880        self.assertEqual((7, 'ngettext', (u'Foo %(fname)s %(lname)s',
     881                                          u'Foos %(fname)s %(lname)s'), []),
     882                         messages[1])
     883
     884    def test_extract_i18n_choose_as_directive_with_params_and_domain_as_directive(self):
     885        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     886            xmlns:i18n="http://genshi.edgewall.org/i18n">
     887        <i18n:domain name="foo">
     888        <i18n:choose numeral="two" params="fname, lname">
     889          <p i18n:singular="">Foo ${fname} ${lname}</p>
     890          <p i18n:plural="">Foos ${fname} ${lname}</p>
     891        </i18n:choose>
     892        </i18n:domain>
     893        <i18n:choose numeral="one" params="fname, lname">
     894          <p i18n:singular="">Foo ${fname} ${lname}</p>
     895          <p i18n:plural="">Foos ${fname} ${lname}</p>
     896        </i18n:choose>
     897        </html>""")
     898        translator = Translator()
     899        tmpl.add_directives(Translator.NAMESPACE, translator)
     900        messages = list(translator.extract(tmpl.stream))
     901        self.assertEqual(2, len(messages))
     902        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
     903                                          u'Foos %(fname)s %(lname)s'), []),
     904                         messages[0])
     905        self.assertEqual((9, 'ngettext', (u'Foo %(fname)s %(lname)s',
     906                                          u'Foos %(fname)s %(lname)s'), []),
     907                         messages[1])
     908       
     909    def test_extract_i18n_choose_as_attribute_with_params_and_comment(self):
     910        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     911            xmlns:i18n="http://genshi.edgewall.org/i18n">
     912          <div i18n:choose="two; fname, lname" i18n:comment="As in Foo Bar">
     913            <p i18n:singular="">Foo $fname $lname</p>
     914            <p i18n:plural="">Foos $fname $lname</p>
     915          </div>
     916        </html>""")
     917        translator = Translator()
     918        tmpl.add_directives(Translator.NAMESPACE, translator)
     919        messages = list(translator.extract(tmpl.stream))
     920        self.assertEqual(1, len(messages))
     921        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     922                                          u'Foos %(fname)s %(lname)s'),
     923                          [u'As in Foo Bar']),
     924                         messages[0])
     925       
     926    def test_extract_i18n_choose_as_directive_with_params_and_comment(self):
     927        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     928            xmlns:i18n="http://genshi.edgewall.org/i18n">
     929        <i18n:choose numeral="two" params="fname, lname" i18n:comment="As in Foo Bar">
     930          <p i18n:singular="">Foo ${fname} ${lname}</p>
     931          <p i18n:plural="">Foos ${fname} ${lname}</p>
     932        </i18n:choose>
     933        </html>""")
     934        translator = Translator()
     935        tmpl.add_directives(Translator.NAMESPACE, translator)
     936        messages = list(translator.extract(tmpl.stream))
     937        self.assertEqual(1, len(messages))
     938        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     939                                          u'Foos %(fname)s %(lname)s'),
     940                          [u'As in Foo Bar']),
     941                         messages[0])
     942
     943    def test_translate_i18n_domain_with_nested_inlcudes(self):
     944        import os, shutil, tempfile
     945        from genshi.template.loader import TemplateLoader
     946        dirname = tempfile.mkdtemp(suffix='genshi_test')
     947        try:
     948            for idx in range(7):
     949                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w')
     950                try:
     951                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     952                                         xmlns:py="http://genshi.edgewall.org/"
     953                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
     954                        <div>Included tmpl$idx</div>
     955                        <p i18n:msg="idx">Bar $idx</p>
     956                        <p i18n:domain="bar">Bar</p>
     957                        <p i18n:msg="idx" i18n:domain="">Bar $idx</p>
     958                        <p i18n:domain="" i18n:msg="idx">Bar $idx</p>
     959                        <py:if test="idx &lt; 6">
     960                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     961                        </py:if>
     962                    </html>""")
     963                finally:
     964                    file1.close()
     965
     966            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
     967            try:
     968                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     969                                     xmlns:py="http://genshi.edgewall.org/"
     970                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
     971                                     i18n:domain="foo">
     972                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     973                </html>""")
     974            finally:
     975                file2.close()
     976
     977            def callback(template):
     978                translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s'})
     979                translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'})
     980                translations.add_domain('bar', {'Bar': 'bar_Bar'})
     981                translator = Translator(translations)
     982                setup_i18n(template, translator)
     983            loader = TemplateLoader([dirname], callback=callback)
     984            tmpl = loader.load('tmpl10.html')
     985           
     986            self.assertEqual("""<html>
     987                        <div>Included tmpl0</div>
     988                        <p>foo_Bar 0</p>
     989                        <p>bar_Bar</p>
     990                        <p>Voh 0</p>
     991                        <p>Voh 0</p>
     992                        <div>Included tmpl1</div>
     993                        <p>foo_Bar 1</p>
     994                        <p>bar_Bar</p>
     995                        <p>Voh 1</p>
     996                        <p>Voh 1</p>
     997                        <div>Included tmpl2</div>
     998                        <p>foo_Bar 2</p>
     999                        <p>bar_Bar</p>
     1000                        <p>Voh 2</p>
     1001                        <p>Voh 2</p>
     1002                        <div>Included tmpl3</div>
     1003                        <p>foo_Bar 3</p>
     1004                        <p>bar_Bar</p>
     1005                        <p>Voh 3</p>
     1006                        <p>Voh 3</p>
     1007                        <div>Included tmpl4</div>
     1008                        <p>foo_Bar 4</p>
     1009                        <p>bar_Bar</p>
     1010                        <p>Voh 4</p>
     1011                        <p>Voh 4</p>
     1012                        <div>Included tmpl5</div>
     1013                        <p>foo_Bar 5</p>
     1014                        <p>bar_Bar</p>
     1015                        <p>Voh 5</p>
     1016                        <p>Voh 5</p>
     1017                        <div>Included tmpl6</div>
     1018                        <p>foo_Bar 6</p>
     1019                        <p>bar_Bar</p>
     1020                        <p>Voh 6</p>
     1021                        <p>Voh 6</p>
     1022                </html>""", tmpl.generate(idx=-1).render())
     1023        finally:
     1024            shutil.rmtree(dirname)
     1025           
     1026    def test_translate_i18n_domain_with_nested_inlcudes_with_translatable_attrs(self):
     1027        import os, shutil, tempfile
     1028        from genshi.template.loader import TemplateLoader
     1029        dirname = tempfile.mkdtemp(suffix='genshi_test')
     1030        try:
     1031            for idx in range(4):
     1032                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w')
     1033                try:
     1034                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     1035                                         xmlns:py="http://genshi.edgewall.org/"
     1036                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
     1037                        <div>Included tmpl$idx</div>
     1038                        <p title="${dg('foo', 'Bar %(idx)s') % dict(idx=idx)}" i18n:msg="idx">Bar $idx</p>
     1039                        <p title="Bar" i18n:domain="bar">Bar</p>
     1040                        <p title="Bar" i18n:msg="idx" i18n:domain="">Bar $idx</p>
     1041                        <p i18n:domain="" i18n:msg="idx" title="Bar">Bar $idx</p>
     1042                        <py:if test="idx &lt; 3">
     1043                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     1044                        </py:if>
     1045                    </html>""")
     1046                finally:
     1047                    file1.close()
     1048
     1049            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
     1050            try:
     1051                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     1052                                     xmlns:py="http://genshi.edgewall.org/"
     1053                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
     1054                                     i18n:domain="foo">
     1055                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     1056                </html>""")
     1057            finally:
     1058                file2.close()
     1059               
     1060            translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s',
     1061                                              'Bar': 'Voh'})
     1062            translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'})
     1063            translations.add_domain('bar', {'Bar': 'bar_Bar'})
     1064            translator = Translator(translations)
     1065           
     1066            def callback(template):               
     1067                setup_i18n(template, translator)
     1068            loader = TemplateLoader([dirname], callback=callback)
     1069            tmpl = loader.load('tmpl10.html')
     1070           
     1071            self.assertEqual("""<html>
     1072                        <div>Included tmpl0</div>
     1073                        <p title="foo_Bar 0">foo_Bar 0</p>
     1074                        <p title="bar_Bar">bar_Bar</p>
     1075                        <p title="Voh">Voh 0</p>
     1076                        <p title="Voh">Voh 0</p>
     1077                        <div>Included tmpl1</div>
     1078                        <p title="foo_Bar 1">foo_Bar 1</p>
     1079                        <p title="bar_Bar">bar_Bar</p>
     1080                        <p title="Voh">Voh 1</p>
     1081                        <p title="Voh">Voh 1</p>
     1082                        <div>Included tmpl2</div>
     1083                        <p title="foo_Bar 2">foo_Bar 2</p>
     1084                        <p title="bar_Bar">bar_Bar</p>
     1085                        <p title="Voh">Voh 2</p>
     1086                        <p title="Voh">Voh 2</p>
     1087                        <div>Included tmpl3</div>
     1088                        <p title="foo_Bar 3">foo_Bar 3</p>
     1089                        <p title="bar_Bar">bar_Bar</p>
     1090                        <p title="Voh">Voh 3</p>
     1091                        <p title="Voh">Voh 3</p>
     1092                </html>""", tmpl.generate(idx=-1,
     1093                                          dg=translations.dugettext).render())
     1094        finally:
     1095            shutil.rmtree(dirname)
     1096
    4701097
    4711098class ExtractTestCase(unittest.TestCase):
    4721099
  • genshi/filters/__init__.py

     
    1414"""Implementation of a number of stream filters."""
    1515
    1616from genshi.filters.html import HTMLFormFiller, HTMLSanitizer
    17 from genshi.filters.i18n import Translator
     17from genshi.filters.i18n import Translator, setup_i18n
    1818from genshi.filters.transform import Transformer
    1919
    2020__docformat__ = 'restructuredtext en'
  • genshi/filters/i18n.py

     
    1111# individuals. For the exact contribution history, see the revision
    1212# history and logs, available at http://genshi.edgewall.org/log/.
    1313
    14 """Utilities for internationalization and localization of templates.
     14"""Directives and utilities for internationalization and localization of
     15templates.
    1516
    1617:since: version 0.4
     18:note: Directives support added since version 0.6
    1719"""
    1820
    1921from compiler import ast
    2022from gettext import NullTranslations
     23import os
    2124import re
    2225from types import FunctionType
    2326
    24 from genshi.core import Attrs, Namespace, QName, START, END, TEXT, START_NS, \
    25                         END_NS, XML_NAMESPACE, _ensure
    26 from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
     27from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
     28                        XML_NAMESPACE, _ensure
     29from genshi.template.base import Context, DirectiveFactory, EXPR, SUB, \
     30                                 _apply_directives
    2731from genshi.template.directives import Directive
    2832from genshi.template.markup import MarkupTemplate, EXEC
    2933
     
    3236
    3337I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n')
    3438
     39class DirectiveExtract(object):
     40    """Simple interface for directives to support messages extraction"""
    3541
    36 class CommentDirective(Directive):
     42    def extract(self, stream, ctxt):
     43        raise NotImplementedError
    3744
    38     __slots__ = []
     45class CommentDirective(Directive):
     46    """Implementation of the ``i18n:comment`` template directive which adds
     47    translation comments.
     48   
     49    >>> from genshi.filters.i18n import Translator, setup_i18n
     50    >>> from genshi.template import MarkupTemplate
     51    >>>
     52    >>> translator = Translator()
     53    >>>
     54    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     55    ...   <p i18n:comment="As in Foo Bar">Foo</p>
     56    ... </html>''')
     57    >>>
     58    >>> setup_i18n(tmpl, translator)
     59    >>> list(translator.extract(tmpl.stream))
     60    [(2, None, u'Foo', [u'As in Foo Bar'])]
     61    >>>
     62    """
    3963
    40     @classmethod
    41     def attach(cls, template, stream, value, namespaces, pos):
    42         return None, stream
     64    __slots__ = ['comment']
    4365
     66    def __init__(self, value, template, hints=None, namespaces=None,
     67                 lineno=-1, offset=-1):
     68        Directive.__init__(self, None, template, namespaces, lineno, offset)
     69        self.comment = value
    4470
    45 class MsgDirective(Directive):
     71class MsgDirective(Directive, DirectiveExtract):
     72    r"""Implementation of the ``i18n:msg`` directive which marks inner content
     73    as translatable. Consider the following examples:
     74   
     75    >>> from genshi.filters.i18n import Translator, setup_i18n
     76    >>> from genshi.template import MarkupTemplate
     77    >>>
     78    >>> translator = Translator()
     79    >>>
     80    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     81    ...   <div i18n:msg="">
     82    ...     <p>Foo</p>
     83    ...     <p>Bar</p>
     84    ...   </div>
     85    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     86    ... </html>''')
     87    >>>
     88    >>> setup_i18n(tmpl, translator)
     89    >>>
     90    >>> list(translator.extract(tmpl.stream))
     91    [(2, None, u'[1:Foo]\n    [2:Bar]', []), (6, None, u'Foo [1:bar]!', [])]
     92    >>> print tmpl.generate().render()
     93    <html>
     94      <div><p>Foo</p>
     95        <p>Bar</p></div>
     96      <p>Foo <em>bar</em>!</p>
     97    </html>
     98    >>>
     99    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     100    ...   <div i18n:msg="fname, lname">
     101    ...     <p>First Name: ${fname}</p>
     102    ...     <p>Last Name: ${lname}</p>
     103    ...   </div>
     104    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     105    ... </html>''')
     106    >>> setup_i18n(tmpl, translator)
     107    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     108    [(2, None, u'[1:First Name: %(fname)s]\n    [2:Last Name: %(lname)s]', []),
     109    (6, None, u'Foo [1:bar]!', [])]
     110    >>>
     111    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     112    ...   <div i18n:msg="fname, lname">
     113    ...     <p>First Name: ${fname}</p>
     114    ...     <p>Last Name: ${lname}</p>
     115    ...   </div>
     116    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     117    ... </html>''')
     118    >>> setup_i18n(tmpl, translator)
     119    >>> print tmpl.generate(fname='John', lname='Doe').render()
     120    <html>
     121      <div><p>First Name: John
     122        <p>Last Name: Doe</div>
     123      <p>Foo <em>bar</em>!</p>
     124    </html>
     125    >>>
     126   
     127    Starting and ending white-space is stripped of to make it simpler for
     128    translators. Stripping it is not that important since it's on the html
     129    source, the rendered output will remain the same.
     130    """
    46131
    47132    __slots__ = ['params']
    48133
    49134    def __init__(self, value, template, hints=None, namespaces=None,
    50135                 lineno=-1, offset=-1):
    51136        Directive.__init__(self, None, template, namespaces, lineno, offset)
    52         self.params = [name.strip() for name in value.split(',')]
     137        self.params = [param.strip() for param in value.split(',') if param]
     138
     139    @classmethod
     140    def attach(cls, template, stream, value, namespaces, pos):
     141        if type(value) is dict:
     142            value = value.get('params', '').strip()
     143        return super(MsgDirective, cls).attach(template, stream, value.strip(),
     144                                               namespaces, pos)
    53145
    54146    def __call__(self, stream, directives, ctxt, **vars):
     147
     148        gettext = ctxt.get('_i18n.gettext')
     149        dgettext = ctxt.get('_i18n.dgettext')
     150        if ctxt.get('_i18n.domain'):
     151            assert callable(dgettext), "No domain gettext function passed"
     152            gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
     153
    55154        msgbuf = MessageBuffer(self.params)
    56155
    57156        stream = iter(stream)
    58157        yield stream.next() # the outer start tag
    59158        previous = stream.next()
    60         for event in stream:
    61             msgbuf.append(*previous)
    62             previous = event
     159        for kind, data, pos in stream:
     160            if kind is SUB:
     161                # py:attrs for example
     162                subdirectives, substream = data
     163                for skind, sdata, spos in _apply_directives(substream,
     164                                                            subdirectives,
     165                                                            ctxt):
     166                    try:
     167                        msgbuf.append(*previous)
     168                        previous = skind, sdata, spos
     169                    except IndexError:
     170                        raise IndexError("Not enough parameters passed to '%s' "
     171                                         "on '%s', line number %s: %s" %
     172                                         (type(self).__name__,
     173                                          os.path.basename(spos[0]), spos[1],
     174                                          self.params))
     175            try:
     176                msgbuf.append(*previous)
     177            except IndexError:
     178                raise IndexError("Not enough parameters passed to '%s' on '%s',"
     179                                 " line number %s: %s" %
     180                                 (type(self).__name__,
     181                                  os.path.basename(previous[2][0]),
     182                                  previous[2][1], self.params), previous[1])
     183            previous = kind, data, pos
    63184
    64         gettext = ctxt.get('_i18n.gettext')
    65185        for event in msgbuf.translate(gettext(msgbuf.format())):
    66186            yield event
    67187
    68188        yield previous # the outer end tag
    69189
     190    def extract(self, stream, ctxt):
     191
     192        msgbuf = MessageBuffer(self.params)
     193
     194        stream = iter(stream)
     195        stream.next() # the outer start tag
     196        previous = stream.next()
     197        for event in stream:
     198            try:
     199                msgbuf.append(*previous)
     200            except IndexError:
     201                raise IndexError("Not enough parameters passed to '%s' on '%s',"
     202                                 " line number %s: %s" %
     203                                 (type(self).__name__,
     204                                  os.path.basename(previous[2][0]),
     205                                  previous[2][1], self.params))
     206            previous = event
     207
     208        yield None, msgbuf.format(), filter(None, [ctxt.get('_i18n.comment')])
     209
     210class InnerChooseDirective(Directive):
     211    __slots__ = []
     212
     213    def __call__(self, stream, directives, ctxt, **vars):
     214
     215        msgbuf = MessageBuffer(ctxt.get('_i18n.choose.params', [])[:])
     216
     217        stream = iter(stream)
     218        yield stream.next() # the outer start tag
     219        previous = stream.next()
     220#        if previous[0] is TEXT and not previous[1].strip():
     221#            yield previous  # white space and newlines
     222        for kind, data, pos in stream:
     223
     224            msgbuf.append(*previous)
     225            previous = kind, data, pos
     226#            if event[0] is TEXT and not event[1].strip():
     227#                yield event # white space and newlines
     228        yield None, None, None # the place holder for msgbuf output
     229        yield previous # the outer end tag
     230        ctxt['_i18n.choose.%s' % type(self).__name__] = msgbuf
     231
     232
     233    def extract(self, stream, ctxt, msgbuf):
     234
     235        stream = iter(stream)
     236        stream.next() # the outer start tag
     237        previous = stream.next()
     238        for event in stream:
     239            msgbuf.append(*previous)
     240            previous = event
     241        return msgbuf
     242
     243
     244class SingularDirective(InnerChooseDirective):
     245    """Implementation of the ``i18n:singular`` directive to be used with the
     246    ``i18n:choose`` directive."""
     247
     248
     249class PluralDirective(InnerChooseDirective):
     250    """Implementation of the ``i18n:plural`` directive to be used with the
     251    ``i18n:choose`` directive."""
     252
     253
     254class ChooseDirective(Directive, DirectiveExtract):
     255    """Implementation of the ``i18n:choose`` directive which provides plural
     256    internationalisation of strings.
     257   
     258    This directive requires at least one parameter, the one which evaluates to
     259    an integer which will allow to choose the plural/singular form. If you also
     260    have expressions inside the singular and plural version of the string you
     261    also need to pass a name for those parameters. Consider the following
     262    examples:
     263   
     264    >>> from genshi.filters.i18n import Translator, setup_i18n
     265    >>> from genshi.template import MarkupTemplate
     266    >>>
     267    >>> translator = Translator()
     268    >>>
     269    >>> tmpl = MarkupTemplate('''\
     270        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     271    ...   <div i18n:choose="num; num">
     272    ...     <p i18n:singular="">There is $num coin</p>
     273    ...     <p i18n:plural="">There are $num coins</p>
     274    ...   </div>
     275    ... </html>''')
     276    >>> setup_i18n(tmpl, translator)
     277    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     278    [(2, 'ngettext', (u'There is %(num)s coin',
     279                      u'There are %(num)s coins'), [])]
     280    >>>
     281    >>> tmpl = MarkupTemplate('''\
     282        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     283    ...   <div i18n:choose="num; num">
     284    ...     <p i18n:singular="">There is $num coin</p>
     285    ...     <p i18n:plural="">There are $num coins</p>
     286    ...   </div>
     287    ... </html>''')
     288    >>> setup_i18n(tmpl, translator)
     289    >>> print tmpl.generate(num=1).render()
     290    <html>
     291      <div>
     292        <p>There is 1 coin</p>
     293      </div>
     294    </html>
     295    >>> print tmpl.generate(num=2).render()
     296    <html>
     297      <div>
     298        <p>There are 2 coins</p>
     299      </div>
     300    </html>
     301    >>>
     302   
     303    When used as a directive and not as an attribute:
     304    >>> tmpl = MarkupTemplate('''\
     305        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     306    ...   <i18n:choose numeral="num" params="num">
     307    ...     <p i18n:singular="">There is $num coin</p>
     308    ...     <p i18n:plural="">There are $num coins</p>
     309    ...   </i18n:choose>
     310    ... </html>''')
     311    >>> setup_i18n(tmpl, translator)
     312    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     313    [(2, 'ngettext', (u'There is %(num)s coin',
     314                      u'There are %(num)s coins'), [])]
     315    >>>
     316    """
     317
     318    __slots__ = ['numeral', 'params']
     319
     320    def __init__(self, value, template, hints=None, namespaces=None,
     321                 lineno=-1, offset=-1):
     322        Directive.__init__(self, None, template, namespaces, lineno, offset)
     323        params = [v.strip() for v in value.split(';')]
     324        self.numeral = self._parse_expr(params.pop(0), template, lineno, offset)
     325        self.params = params and [name.strip() for name in
     326                                  params[0].split(',') if name] or []
     327
     328    @classmethod
     329    def attach(cls, template, stream, value, namespaces, pos):
     330        if type(value) is dict:
     331            numeral = value.get('numeral', '').strip()
     332            assert numeral is not '', "at least pass the numeral param"
     333            params = [v.strip() for v in value.get('params', '').split(',')]
     334            value = '%s; ' % numeral + ', '.join(params)
     335        return super(ChooseDirective, cls).attach(template, stream, value,
     336                                                  namespaces, pos)
     337
     338    def __call__(self, stream, directives, ctxt, **vars):
     339
     340        ctxt.push({'_i18n.choose.params': self.params,
     341                   '_i18n.choose.SingularDirective': None,
     342                   '_i18n.choose.PluralDirective': None})
     343
     344        new_stream = []
     345        singular_stream = None
     346        singular_msgbuf = None
     347        plural_stream = None
     348        plural_msgbuf = None
     349
     350        ngettext = ctxt.get('_i18n.ungettext')
     351        assert callable(ngettext), "No ngettext function available"
     352        dngettext = ctxt.get('_i18n.dngettext')
     353        if not dngettext:
     354            dngettext = lambda d, s, p, n: ngettext(s, p, n)
     355
     356        for kind, event, pos in stream:
     357            if kind is SUB:
     358                subdirectives, substream = event
     359                if isinstance(subdirectives[0],
     360                              SingularDirective) and not singular_stream:
     361                    # Apply directives to update context
     362                    singular_stream = list(_apply_directives(substream,
     363                                                             subdirectives,
     364                                                             ctxt))
     365                    new_stream.append((None, None, None)) # msgbuf place holder
     366                    singular_msgbuf = ctxt.get('_i18n.choose.SingularDirective')
     367                elif isinstance(subdirectives[0],
     368                                PluralDirective) and not plural_stream:
     369                    # Apply directives to update context
     370                    plural_stream = list(_apply_directives(substream,
     371                                                           subdirectives, ctxt))
     372                    plural_msgbuf = ctxt.get('_i18n.choose.PluralDirective')
     373                else:
     374                    new_stream.append((kind, event, pos))
     375            else:
     376                new_stream.append((kind, event, pos))
     377
     378        if ctxt.get('_i18n.domain'):
     379            ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'),
     380                                                 s, p, n)
     381
     382        for kind, data, pos in new_stream:
     383            if not kind and not data and not pos:
     384                for skind, sdata, spos in singular_stream:
     385                    if not skind and not sdata and not spos:
     386                        translation = ngettext(singular_msgbuf.format(),
     387                                               plural_msgbuf.format(),
     388                                               self.numeral.evaluate(ctxt))
     389                        for event in singular_msgbuf.translate(translation):
     390                            yield event
     391                    else:
     392                        yield skind, sdata, spos
     393            else:
     394                yield kind, data, pos
     395
     396        ctxt.pop()
     397
     398    def extract(self, stream, ctxt):
     399
     400        stream = iter(stream)
     401        previous = stream.next()
     402        if previous is START:
     403            stream.next()
     404
     405        singular_msgbuf = MessageBuffer(self.params[:])
     406        plural_msgbuf = MessageBuffer(self.params[:])
     407
     408        for kind, event, pos in stream:
     409            if kind is SUB:
     410                subdirectives, substream = event
     411                for subdirective in subdirectives:
     412                    if isinstance(subdirective, SingularDirective):
     413                        singular_msgbuf = subdirective.extract(substream, ctxt,
     414                                                               singular_msgbuf)
     415                    elif isinstance(subdirective, PluralDirective):
     416                        plural_msgbuf = subdirective.extract(substream, ctxt,
     417                                                             plural_msgbuf)
     418                    else:
     419                        try:
     420                            singular_msgbuf.append(kind, event, pos)
     421                            plural_msgbuf.append(kind, event, pos)
     422                        except IndexError:
     423                            raise IndexError("Not enough parameters passed to "
     424                                             "'%s' on '%s', line number %s: "
     425                                             "%s" % (type(self).__name__,
     426                                                     os.path.basename(pos[0]),
     427                                                     pos[1], self.params))
     428            else:
     429                try:
     430                    singular_msgbuf.append(kind, event, pos)
     431                    plural_msgbuf.append(kind, event, pos)
     432                except IndexError:
     433                    raise IndexError("Not enough parameters passed to '%s' on "
     434                                     "'%s', line number %s: %s" %
     435                                     (type(self).__name__,
     436                                      os.path.basename(pos[0]), pos[1],
     437                                      self.params))
     438
     439        yield 'ngettext', \
     440            (singular_msgbuf.format(), plural_msgbuf.format()), \
     441            filter(None, [ctxt.get('_i18n.comment')])
     442
     443class DomainDirective(Directive):
     444    """Implementation of the ``i18n:domain`` directive which allows choosing
     445    another i18n domain(catalog) to translate from.
     446   
     447    >>> from gettext import NullTranslations
     448    >>> from genshi.filters.i18n import Translator, setup_i18n
     449    >>> from genshi.template.markup import MarkupTemplate
     450    >>>
     451    >>> class DummyTranslations(NullTranslations):
     452    ...     _domains = {}
     453    ...     def __init__(self, catalog):
     454    ...         NullTranslations.__init__(self)
     455    ...         self._catalog = catalog
     456    ...     def add_domain(self, domain, catalog):
     457    ...         translation = DummyTranslations(catalog)
     458    ...         translation.add_fallback(self)
     459    ...         self._domains[domain] = translation
     460    ...     def _domain_call(self, func, domain, *args, **kwargs):
     461    ...         return getattr(self._domains.get(domain, self), func)(*args,
     462    ...                                                               **kwargs)
     463    ...     def ugettext(self, message):
     464    ...         missing = object()
     465    ...         tmsg = self._catalog.get(message, missing)
     466    ...         if tmsg is missing:
     467    ...             if self._fallback:
     468    ...                 return self._fallback.ugettext(message)
     469    ...             return unicode(message)
     470    ...         return tmsg
     471    ...     def dugettext(self, domain, message):
     472    ...         return self._domain_call('ugettext', domain, message)
     473    ...
     474    >>>
     475    >>> tmpl = MarkupTemplate('''\
     476        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     477    ...   <p i18n:msg="">Bar</p>
     478    ...   <div i18n:domain="foo">
     479    ...     <p i18n:msg="">FooBar</p>
     480    ...     <p>Bar</p>
     481    ...     <p i18n:domain="bar" i18n:msg="">Bar</p>
     482    ...     <p i18n:domain="">Bar</p>
     483    ...   </div>
     484    ...   <p>Bar</p>
     485    ... </html>''')
     486    >>>
     487    >>> translations = DummyTranslations({'Bar': 'Voh'})
     488    >>> translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     489    >>> translations.add_domain('bar', {'Bar': 'bar_Bar'})
     490    >>> translator = Translator(translations)
     491    >>> setup_i18n(tmpl, translator)
     492    >>>
     493    >>> print tmpl.generate().render()
     494    <html>
     495      <p>Voh</p>
     496      <div>
     497        <p>BarFoo</p>
     498        <p>foo_Bar</p>
     499        <p>bar_Bar</p>
     500        <p>Voh</p>
     501      </div>
     502      <p>Voh</p>
     503    </html>
     504    >>>
     505    """
     506
     507    __slots__ = ['domain']
     508
     509    def __init__(self, value, template, hints=None, namespaces=None,
     510                 lineno=-1, offset=-1):
     511        Directive.__init__(self, None, template, namespaces, lineno, offset)
     512        self.domain = value
     513
     514    @classmethod
     515    def attach(cls, template, stream, value, namespaces, pos):
     516        if type(value) is dict:
     517            value = value.get('name')
     518        return super(DomainDirective, cls).attach(template, stream, value,
     519                                                  namespaces, pos)
     520
     521    def __call__(self, stream, directives, ctxt, **vars):
     522        ctxt.push({'_i18n.domain': self.domain})
     523        for event in _apply_directives(stream, directives, ctxt):
     524            yield event
     525        ctxt.pop()
     526
    70527
    71528class Translator(DirectiveFactory):
    72529    """Can extract and translate localizable strings from markup streams and
    73530    templates.
    74531   
    75     For example, assume the followng template:
     532    For example, assume the following template:
    76533   
    77534    >>> from genshi.template import MarkupTemplate
    78     >>> 
     535    >>>
    79536    >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    80537    ...   <head>
    81538    ...     <title>Example</title>
     
    94551    ...         'Example': 'Beispiel',
    95552    ...         'Hello, %(name)s': 'Hallo, %(name)s'
    96553    ...     }[string]
    97     >>> 
     554    >>>
    98555    >>> translator = Translator(pseudo_gettext)
    99556   
    100557    Next, the translator needs to be prepended to any already defined filters
     
    115572        <p>Hallo, Hans</p>
    116573      </body>
    117574    </html>
    118 
     575   
    119576    Note that elements defining ``xml:lang`` attributes that do not contain
    120577    variable expressions are ignored by this filter. That can be used to
    121578    exclude specific parts of a template from being extracted and translated.
    122579    """
    123580
    124581    directives = [
     582        ('domain', DomainDirective),
    125583        ('comment', CommentDirective),
    126         ('msg', MsgDirective)
     584        ('msg', MsgDirective),
     585        ('choose', ChooseDirective),
     586        ('singular', SingularDirective),
     587        ('plural', PluralDirective)
    127588    ]
    128589
    129590    IGNORE_TAGS = frozenset([
    130591        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
    131592        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
    132593    ])
    133     INCLUDE_ATTRS = frozenset(['abbr', 'alt', 'label', 'prompt', 'standby',
    134                                'summary', 'title'])
     594    INCLUDE_ATTRS = frozenset([
     595        'abbr', 'alt', 'label', 'prompt', 'standby', 'summary', 'title'
     596    ])
    135597    NAMESPACE = I18N_NAMESPACE
    136598
    137599    def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
     
    145607        :param extract_text: whether the content of text nodes should be
    146608                             extracted, or only text in explicit ``gettext``
    147609                             function calls
    148 
     610       
    149611        :note: Changed in 0.6: the `translate` parameter can now be either
    150612               a ``gettext``-style function, or an object compatible with the
    151613               ``NullTransalations`` or ``GNUTranslations`` interface
     
    177639
    178640        if type(self.translate) is FunctionType:
    179641            gettext = self.translate
     642            if ctxt:
     643                ctxt['_i18n.gettext'] = gettext
    180644        else:
    181645            gettext = self.translate.ugettext
    182         if ctxt:
    183             ctxt['_i18n.gettext'] = gettext
     646            try:
     647                dgettext = self.translate.dugettext
     648            except AttributeError:
     649                dgettext = lambda x, y: gettext(y)
     650            ngettext = self.translate.ungettext
     651            try:
     652                dngettext = self.translate.dungettext
     653            except AttributeError:
     654                dngettext = lambda d, s, p, n: ngettext(s, p, n)
     655
     656            if ctxt:
     657                ctxt['_i18n.gettext'] = gettext
     658                ctxt['_i18n.ugettext'] = gettext
     659                ctxt['_i18n.dgettext'] = dgettext
     660                ctxt['_i18n.ngettext'] = ngettext
     661                ctxt['_i18n.ungettext'] = ngettext
     662                ctxt['_i18n.dngettext'] = dngettext
    184663
    185664        extract_text = self.extract_text
    186665        if not extract_text:
    187666            search_text = False
    188667
     668        if ctxt and ctxt.get('_i18n.domain'):
     669            old_gettext = gettext
     670            gettext = lambda x: dgettext(ctxt.get('_i18n.domain'), x)
     671
    189672        for kind, data, pos in stream:
    190673
    191674            # skip chunks that should not be localized
     
    208691
    209692                new_attrs = []
    210693                changed = False
     694
    211695                for name, value in attrs:
    212696                    newval = value
    213697                    if extract_text and isinstance(value, basestring):
    214698                        if name in include_attrs:
    215699                            newval = gettext(value)
    216700                    else:
    217                         newval = list(self(_ensure(value), ctxt,
    218                             search_text=False)
     701                        newval = list(
     702                            self(_ensure(value), ctxt, search_text=False)
    219703                        )
    220704                    if newval != value:
    221705                        value = newval
     
    234718
    235719            elif kind is SUB:
    236720                directives, substream = data
    237                 # If this is an i18n:msg directive, no need to translate text
     721                # Is there a DomainDirective defined ?
     722                current_domain = [d.domain for d in directives if
     723                                  isinstance(d, DomainDirective)]
     724                if current_domain:
     725                    # Domain defined, lets update the context with it
     726                    ctxt.push({'_i18n.domain': current_domain[0]})
     727
     728                # If this is an i18n directive, no need to translate text
    238729                # nodes here
    239                 is_msg = filter(None, [isinstance(d, MsgDirective)
    240                                        for d in directives])
     730                is_i18n_directive = filter(None,
     731                                           [isinstance(d, DirectiveExtract)
     732                                            for d in directives])
    241733                substream = list(self(substream, ctxt,
    242                                       search_text=not is_msg))
     734                                      search_text=not is_i18n_directive))
    243735                yield kind, (directives, substream), pos
    244736
     737                if current_domain:
     738                    ctxt.pop()
    245739            else:
    246740                yield kind, data, pos
    247741
     
    249743                         'ugettext', 'ungettext')
    250744
    251745    def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
    252                 search_text=True, msgbuf=None):
     746                search_text=True, msgbuf=None, ctxt=Context()):
    253747        """Extract localizable strings from the given template stream.
    254748       
    255749        For every string found, this function yields a ``(lineno, function,
     
    265759           from ``i18n:comment`` attributes found in the markup
    266760       
    267761        >>> from genshi.template import MarkupTemplate
    268         >>> 
     762        >>>
    269763        >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    270764        ...   <head>
    271765        ...     <title>Example</title>
     
    276770        ...     <p>${ngettext("You have %d item", "You have %d items", num)}</p>
    277771        ...   </body>
    278772        ... </html>''', filename='example.html')
    279         >>> 
     773        >>>
    280774        >>> for line, func, msg, comments in Translator().extract(tmpl.stream):
    281775        ...    print "%d, %r, %r" % (line, func, msg)
    282776        3, None, u'Example'
     
    291785                                  functions
    292786        :param search_text: whether the content of text nodes should be
    293787                            extracted (used internally)
     788        :param ctxt: the current extraction context (used internaly)
    294789       
    295790        :note: Changed in 0.4.1: For a function with multiple string arguments
    296791               (such as ``ngettext``), a single item with a tuple of strings is
    297792               yielded, instead an item for each string argument.
    298793        :note: Changed in 0.6: The returned tuples now include a 4th element,
    299                which is a list of comments for the translator
     794               which is a list of comments for the translator. Added an ``ctxt``
     795               argument which is used to pass arround the current extraction
     796               context.
    300797        """
    301798        if not self.extract_text:
    302799            search_text = False
    303800        skip = 0
    304         i18n_comment = I18N_NAMESPACE['comment']
    305         i18n_msg = I18N_NAMESPACE['msg']
     801
     802        # Un-comment bellow to extract messages without adding directives
     803#        i18n_comment = I18N_NAMESPACE['comment']
     804#        i18n_msg = I18N_NAMESPACE['msg']
    306805        xml_lang = XML_NAMESPACE['lang']
    307806
    308807        for kind, data, pos in stream:
    309 
    310808            if skip:
    311809                if kind is START:
    312810                    skip += 1
     
    335833
    336834                if msgbuf:
    337835                    msgbuf.append(kind, data, pos)
    338                 else:
    339                     msg_params = attrs.get(i18n_msg)
    340                     if msg_params is not None:
    341                         if type(msg_params) is list: # event tuple
    342                             msg_params = msg_params[0][1]
    343                         msgbuf = MessageBuffer(
    344                             msg_params, attrs.get(i18n_comment), pos[1]
    345                         )
     836                # Un-comment bellow to extract messages without adding
     837                # directives
     838#                else:
     839#                    msg_params = attrs.get(i18n_msg)
     840#                    if msg_params is not None:
     841#                        print kind, data, pos
     842#                        if type(msg_params) is list: # event tuple
     843#                            msg_params = msg_params[0][1]
     844#                        msgbuf = MessageBuffer(
     845#                            msg_params, attrs.get(i18n_comment), pos[1]
     846#                        )
    346847
    347848            elif not skip and search_text and kind is TEXT:
    348849                if not msgbuf:
    349850                    text = data.strip()
    350851                    if text and filter(None, [ch.isalpha() for ch in text]):
    351                         yield pos[1], None, text, []
     852                        yield pos[1], None, text, \
     853                                    filter(None, [ctxt.get('_i18n.comment')])
    352854                else:
    353855                    msgbuf.append(kind, data, pos)
    354856
     
    356858                msgbuf.append(kind, data, pos)
    357859                if not msgbuf.depth:
    358860                    yield msgbuf.lineno, None, msgbuf.format(), \
    359                           filter(None, [msgbuf.comment])
     861                                                  filter(None, [msgbuf.comment])
    360862                    msgbuf = None
    361863
    362864            elif kind is EXPR or kind is EXEC:
     
    367869                    yield pos[1], funcname, strings, []
    368870
    369871            elif kind is SUB:
    370                 subkind, substream = data
    371                 messages = self.extract(substream, gettext_functions,
    372                                         search_text=search_text and not skip,
    373                                         msgbuf=msgbuf)
    374                 for lineno, funcname, text, comments in messages:
    375                     yield lineno, funcname, text, comments
     872                directives, substream = data
    376873
     874                comment = None
     875                for idx, directive in enumerate(directives):
     876                    # Do a first loop to see if there's a comment directive
     877                    # If there is update context and pop it from directives
     878                    if isinstance(directive, CommentDirective):
     879                        comment = directive.comment
     880                        ctxt.push({'_i18n.comment': comment})
     881                        if len(directives) == 1:
     882                            # in case we're in the presence of something like:
     883                            # <p i18n:comment="foo">Foo</p>
     884                            messages = self.extract(
     885                                substream, gettext_functions,
     886                                search_text=search_text and not skip,
     887                                msgbuf=msgbuf, ctxt=ctxt)
     888                            for lineno, funcname, text, comments in messages:
     889                                yield lineno, funcname, text, comments
     890                        directives.pop(idx)
     891
     892                for directive in directives:
     893                    if isinstance(directive, DirectiveExtract):
     894                        messages = directive.extract(substream, ctxt)
     895                        for funcname, text, comments in messages:
     896                            yield pos[1], funcname, text, comments
     897                    else:
     898                        messages = self.extract(
     899                            substream, gettext_functions,
     900                            search_text=search_text and not skip, msgbuf=msgbuf)
     901                        for lineno, funcname, text, comments in messages:
     902                            yield lineno, funcname, text, comments
     903                if comment:
     904                    ctxt.pop()
    377905
    378906class MessageBuffer(object):
    379907    """Helper class for managing internationalized mixed content.
     
    408936        :param data: the event data
    409937        :param pos: the position of the event in the source
    410938        """
     939        if kind is SUB:
     940            # py:attrs for example
     941            for skind, sdata, spos in data[1]:
     942                self.append(skind, sdata, spos)
    411943        if kind is TEXT:
    412944            self.string.append(data)
    413945            self.events.setdefault(self.stack[-1], []).append(None)
     
    464996def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')):
    465997    """Parse a translated message using Genshi mixed content message
    466998    formatting.
    467 
     999   
    4681000    >>> parse_msg("See [1:Help].")
    4691001    [(0, 'See '), (1, 'Help'), (0, '.')]
    470 
     1002   
    4711003    >>> parse_msg("See [1:our [2:Help] page] for details.")
    4721004    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
    473 
     1005   
    4741006    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
    4751007    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
    476 
     1008   
    4771009    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
    4781010    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
    479 
     1011   
    4801012    :param string: the translated message string
    4811013    :return: a list of ``(order, string)`` tuples
    4821014    :rtype: `list`
     
    5141046    >>> expr = Expression('_("Hello")')
    5151047    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    5161048    [('_', u'Hello')]
    517 
     1049   
    5181050    >>> expr = Expression('ngettext("You have %(num)s item", '
    5191051    ...                            '"You have %(num)s items", num)')
    5201052    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
     
    5841116
    5851117    tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
    5861118                          encoding=encoding)
     1119
    5871120    translator = Translator(None, ignore_tags, include_attrs, extract_text)
     1121    if hasattr(tmpl, 'add_directives'):
     1122        tmpl.add_directives(Translator.NAMESPACE, translator)
    5881123    for message in translator.extract(tmpl.stream, gettext_functions=keywords):
    5891124        yield message
     1125
     1126def setup_i18n(template, translator):
     1127    """Convinience function to setup both the i18n filter and the i18n
     1128    directives.
     1129   
     1130    :param template: an instance of a genshi template
     1131    :param translator: an instance of ``Translator``
     1132    """
     1133    template.filters.insert(0, translator)
     1134    if hasattr(template, 'add_directives'):
     1135        template.add_directives(Translator.NAMESPACE, translator)