Edgewall Software

Ticket #129: i18n_directives_full_with_py_strip.patch

File i18n_directives_full_with_py_strip.patch, 85.2 KB (added by palgarvio, 8 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)
    466500        self.assertEqual("""<html>
    467501          <p>Voh</p>
    468502        </html>""", tmpl.generate().render())
     503       
     504    def test_translate_mixing_py_and_i18n_directives(self):
     505        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     506            xmlns:i18n="http://genshi.edgewall.org/i18n">
     507          <p i18n:msg="" i18n:comment="As in foo bar" py:strip="">Foo</p>
     508          <p py:strip="" i18n:msg="" i18n:comment="As in foo bar">Foo</p>
     509        </html>""")
     510        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
     511        setup_i18n(tmpl, translator)
     512        self.assertEqual("""<html>
     513          Voh
     514          Voh
     515        </html>""", tmpl.generate().render())
     516       
     517    def test_translate_i18n_domain_with_msg_directives(self):
     518        #"""translate with i18n:domain and nested i18n:msg directives """
    469519
     520        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     521            xmlns:i18n="http://genshi.edgewall.org/i18n">
     522          <div i18n:domain="foo">
     523            <p i18n:msg="">FooBar</p>
     524            <p i18n:msg="">Bar</p>
     525          </div>
     526        </html>""")
     527        translations = DummyTranslations({'Bar': 'Voh'})
     528        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     529        translator = Translator(translations)
     530        setup_i18n(tmpl, translator)
     531        self.assertEqual("""<html>
     532          <div>
     533            <p>BarFoo</p>
     534            <p>PT_Foo</p>
     535          </div>
     536        </html>""", tmpl.generate().render())
     537       
     538    def test_translate_i18n_domain_with_inline_directives(self):
     539        #"""translate with inlined i18n:domain and i18n:msg directives"""
     540        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     541            xmlns:i18n="http://genshi.edgewall.org/i18n">
     542          <p i18n:msg="" i18n:domain="foo">FooBar</p>
     543        </html>""")
     544        translations = DummyTranslations({'Bar': 'Voh'})
     545        translations.add_domain('foo', {'FooBar': 'BarFoo'})
     546        translator = Translator(translations)
     547        setup_i18n(tmpl, translator)
     548        self.assertEqual("""<html>
     549          <p>BarFoo</p>
     550        </html>""", tmpl.generate().render())
     551       
     552    def test_translate_i18n_domain_without_msg_directives(self):
     553        #"""translate domain call without i18n:msg directives still uses current domain"""
     554       
     555        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     556            xmlns:i18n="http://genshi.edgewall.org/i18n">
     557          <p i18n:msg="">Bar</p>
     558          <div i18n:domain="foo">
     559            <p i18n:msg="">FooBar</p>
     560            <p i18n:msg="">Bar</p>           
     561            <p>Bar</p>
     562          </div>         
     563          <p>Bar</p>
     564        </html>""")
     565        translations = DummyTranslations({'Bar': 'Voh'})
     566        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     567        translator = Translator(translations)
     568        setup_i18n(tmpl, translator)
     569        self.assertEqual("""<html>
     570          <p>Voh</p>
     571          <div>
     572            <p>BarFoo</p>
     573            <p>PT_Foo</p>
     574            <p>PT_Foo</p>
     575          </div>
     576          <p>Voh</p>
     577        </html>""", tmpl.generate().render())
     578       
     579    def test_translate_i18n_domain_as_directive_not_attribute(self):
     580        #"""translate with domain as directive"""
     581       
     582        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     583            xmlns:i18n="http://genshi.edgewall.org/i18n">
     584        <i18n:domain name="foo">
     585          <p i18n:msg="">FooBar</p>
     586          <p i18n:msg="">Bar</p>
     587          <p>Bar</p>
     588        </i18n:domain>
     589          <p>Bar</p>
     590        </html>""")
     591        translations = DummyTranslations({'Bar': 'Voh'})
     592        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'})
     593        translator = Translator(translations)
     594        setup_i18n(tmpl, translator)
     595        self.assertEqual("""<html>
     596          <p>BarFoo</p>
     597          <p>PT_Foo</p>
     598          <p>PT_Foo</p>
     599          <p>Voh</p>
     600        </html>""", tmpl.generate().render())
     601       
     602    def test_translate_i18n_domain_nested_directives(self):
     603        #"""translate with nested i18n:domain directives"""
     604       
     605        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     606            xmlns:i18n="http://genshi.edgewall.org/i18n">
     607          <p i18n:msg="">Bar</p>
     608          <div i18n:domain="foo">
     609            <p i18n:msg="">FooBar</p>
     610            <p i18n:domain="bar" i18n:msg="">Bar</p>
     611            <p>Bar</p>
     612          </div>         
     613          <p>Bar</p>
     614        </html>""")
     615        translations = DummyTranslations({'Bar': 'Voh'})
     616        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     617        translations.add_domain('bar', {'Bar': 'bar_Bar'})
     618        translator = Translator(translations)
     619        setup_i18n(tmpl, translator)
     620        self.assertEqual("""<html>
     621          <p>Voh</p>
     622          <div>
     623            <p>BarFoo</p>
     624            <p>bar_Bar</p>
     625            <p>foo_Bar</p>
     626          </div>
     627          <p>Voh</p>
     628        </html>""", tmpl.generate().render())
     629       
     630    def test_translate_i18n_domain_with_empty_nested_domain_directive(self):
     631        #"""translate with empty nested i18n:domain directive does not use dngettext"""
     632       
     633        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     634            xmlns:i18n="http://genshi.edgewall.org/i18n">
     635          <p i18n:msg="">Bar</p>
     636          <div i18n:domain="foo">
     637            <p i18n:msg="">FooBar</p>
     638            <p i18n:domain="" i18n:msg="">Bar</p>
     639            <p>Bar</p>
     640          </div>         
     641          <p>Bar</p>
     642        </html>""")
     643        translations = DummyTranslations({'Bar': 'Voh'})
     644        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     645        translations.add_domain('bar', {'Bar': 'bar_Bar'})
     646        translator = Translator(translations)
     647        setup_i18n(tmpl, translator)
     648        self.assertEqual("""<html>
     649          <p>Voh</p>
     650          <div>
     651            <p>BarFoo</p>
     652            <p>Voh</p>
     653            <p>foo_Bar</p>
     654          </div>
     655          <p>Voh</p>
     656        </html>""", tmpl.generate().render())
     657
     658    def test_translate_i18n_choose_as_attribute(self):
     659        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     660            xmlns:i18n="http://genshi.edgewall.org/i18n">
     661          <div i18n:choose="one">
     662            <p i18n:singular="">FooBar</p>
     663            <p i18n:plural="">FooBars</p>
     664          </div>
     665          <div i18n:choose="two">
     666            <p i18n:singular="">FooBar</p>
     667            <p i18n:plural="">FooBars</p>
     668          </div>
     669        </html>""")
     670        translations = DummyTranslations()
     671        translator = Translator(translations)
     672        setup_i18n(tmpl, translator)
     673        self.assertEqual("""<html>
     674          <div>
     675            <p>FooBar</p>
     676          </div>
     677          <div>
     678            <p>FooBars</p>
     679          </div>
     680        </html>""", tmpl.generate(one=1, two=2).render())
     681       
     682    def test_translate_i18n_choose_as_directive(self):
     683        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     684            xmlns:i18n="http://genshi.edgewall.org/i18n">
     685        <i18n:choose numeral="two">
     686          <p i18n:singular="">FooBar</p>
     687          <p i18n:plural="">FooBars</p>
     688        </i18n:choose>
     689        <i18n:choose numeral="one">
     690          <p i18n:singular="">FooBar</p>
     691          <p i18n:plural="">FooBars</p>
     692        </i18n:choose>
     693        </html>""")
     694        translations = DummyTranslations()
     695        translator = Translator(translations)
     696        setup_i18n(tmpl, translator)
     697        self.assertEqual("""<html>
     698          <p>FooBars</p>
     699          <p>FooBar</p>
     700        </html>""", tmpl.generate(one=1, two=2).render())
     701       
     702    def test_translate_i18n_choose_as_attribute_with_params(self):
     703        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     704            xmlns:i18n="http://genshi.edgewall.org/i18n">
     705          <div i18n:choose="two; fname, lname">
     706            <p i18n:singular="">Foo $fname $lname</p>
     707            <p i18n:plural="">Foos $fname $lname</p>
     708          </div>
     709        </html>""")
     710        translations = DummyTranslations({
     711            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     712            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     713                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     714                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     715        })
     716        translator = Translator(translations)
     717        setup_i18n(tmpl, translator)
     718        self.assertEqual("""<html>
     719          <div>
     720            <p>Vohs John Doe</p>
     721          </div>
     722        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     723       
     724    def test_translate_i18n_choose_as_attribute_with_params_and_domain_as_param(self):
     725        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     726            xmlns:i18n="http://genshi.edgewall.org/i18n"
     727            i18n:domain="foo">
     728          <div i18n:choose="two; fname, lname">
     729            <p i18n:singular="">Foo $fname $lname</p>
     730            <p i18n:plural="">Foos $fname $lname</p>
     731          </div>
     732        </html>""")
     733        translations = DummyTranslations()
     734        translations.add_domain('foo', {
     735            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     736            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     737                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     738                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     739        })
     740        translator = Translator(translations)
     741        setup_i18n(tmpl, translator)
     742        self.assertEqual("""<html>
     743          <div>
     744            <p>Vohs John Doe</p>
     745          </div>
     746        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     747       
     748    def test_translate_i18n_choose_as_directive_with_params(self):
     749        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     750            xmlns:i18n="http://genshi.edgewall.org/i18n">
     751        <i18n:choose numeral="two" params="fname, lname">
     752          <p i18n:singular="">Foo ${fname} ${lname}</p>
     753          <p i18n:plural="">Foos ${fname} ${lname}</p>
     754        </i18n:choose>
     755        <i18n:choose numeral="one" params="fname, lname">
     756          <p i18n:singular="">Foo ${fname} ${lname}</p>
     757          <p i18n:plural="">Foos ${fname} ${lname}</p>
     758        </i18n:choose>
     759        </html>""")
     760        translations = DummyTranslations({
     761            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     762            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     763                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     764                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     765        })
     766        translator = Translator(translations)
     767        setup_i18n(tmpl, translator)
     768        self.assertEqual("""<html>
     769          <p>Vohs John Doe</p>
     770          <p>Voh John Doe</p>
     771        </html>""", tmpl.generate(one=1, two=2,
     772                                  fname='John', lname='Doe').render())
     773       
     774    def test_translate_i18n_choose_as_directive_with_params_and_domain_as_directive(self):
     775        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     776            xmlns:i18n="http://genshi.edgewall.org/i18n">
     777        <i18n:domain name="foo">
     778        <i18n:choose numeral="two" params="fname, lname">
     779          <p i18n:singular="">Foo ${fname} ${lname}</p>
     780          <p i18n:plural="">Foos ${fname} ${lname}</p>
     781        </i18n:choose>
     782        </i18n:domain>
     783        <i18n:choose numeral="one" params="fname, lname">
     784          <p i18n:singular="">Foo ${fname} ${lname}</p>
     785          <p i18n:plural="">Foos ${fname} ${lname}</p>
     786        </i18n:choose>
     787        </html>""")
     788        translations = DummyTranslations()
     789        translations.add_domain('foo', {
     790            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     791            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     792                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     793                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     794        })
     795        translator = Translator(translations)
     796        setup_i18n(tmpl, translator)
     797        self.assertEqual("""<html>
     798          <p>Vohs John Doe</p>
     799          <p>Foo John Doe</p>
     800        </html>""", tmpl.generate(one=1, two=2,
     801                                  fname='John', lname='Doe').render())
     802
     803    def test_extract_i18n_choose_as_attribute(self):
     804        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     805            xmlns:i18n="http://genshi.edgewall.org/i18n">
     806          <div i18n:choose="one">
     807            <p i18n:singular="">FooBar</p>
     808            <p i18n:plural="">FooBars</p>
     809          </div>
     810          <div i18n:choose="two">
     811            <p i18n:singular="">FooBar</p>
     812            <p i18n:plural="">FooBars</p>
     813          </div>
     814        </html>""")
     815        translator = Translator()
     816        tmpl.add_directives(Translator.NAMESPACE, translator)
     817        messages = list(translator.extract(tmpl.stream))
     818        self.assertEqual(2, len(messages))
     819        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     820        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     821       
     822    def test_extract_i18n_choose_as_directive(self):
     823        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     824            xmlns:i18n="http://genshi.edgewall.org/i18n">
     825        <i18n:choose numeral="two">
     826          <p i18n:singular="">FooBar</p>
     827          <p i18n:plural="">FooBars</p>
     828        </i18n:choose>
     829        <i18n:choose numeral="one">
     830          <p i18n:singular="">FooBar</p>
     831          <p i18n:plural="">FooBars</p>
     832        </i18n:choose>
     833        </html>""")
     834        translator = Translator()
     835        tmpl.add_directives(Translator.NAMESPACE, translator)
     836        messages = list(translator.extract(tmpl.stream))
     837        self.assertEqual(2, len(messages))
     838        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     839        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     840       
     841    def test_extract_i18n_choose_as_attribute_with_params(self):
     842        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     843            xmlns:i18n="http://genshi.edgewall.org/i18n">
     844          <div i18n:choose="two; fname, lname">
     845            <p i18n:singular="">Foo $fname $lname</p>
     846            <p i18n:plural="">Foos $fname $lname</p>
     847          </div>
     848        </html>""")
     849        translator = Translator()
     850        tmpl.add_directives(Translator.NAMESPACE, translator)
     851        messages = list(translator.extract(tmpl.stream))
     852        self.assertEqual(1, len(messages))
     853        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     854                                          u'Foos %(fname)s %(lname)s'), []),
     855                         messages[0])
     856
     857    def test_extract_i18n_choose_as_attribute_with_params_and_domain_as_param(self):
     858        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     859            xmlns:i18n="http://genshi.edgewall.org/i18n"
     860            i18n:domain="foo">
     861          <div i18n:choose="two; fname, lname">
     862            <p i18n:singular="">Foo $fname $lname</p>
     863            <p i18n:plural="">Foos $fname $lname</p>
     864          </div>
     865        </html>""")
     866        translator = Translator()
     867        tmpl.add_directives(Translator.NAMESPACE, translator)
     868        messages = list(translator.extract(tmpl.stream))
     869        self.assertEqual(1, len(messages))
     870        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
     871                                          u'Foos %(fname)s %(lname)s'), []),
     872                         messages[0])
     873
     874    def test_extract_i18n_choose_as_directive_with_params(self):
     875        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     876            xmlns:i18n="http://genshi.edgewall.org/i18n">
     877        <i18n:choose numeral="two" params="fname, lname">
     878          <p i18n:singular="">Foo ${fname} ${lname}</p>
     879          <p i18n:plural="">Foos ${fname} ${lname}</p>
     880        </i18n:choose>
     881        <i18n:choose numeral="one" params="fname, lname">
     882          <p i18n:singular="">Foo ${fname} ${lname}</p>
     883          <p i18n:plural="">Foos ${fname} ${lname}</p>
     884        </i18n:choose>
     885        </html>""")
     886        translator = Translator()
     887        tmpl.add_directives(Translator.NAMESPACE, translator)
     888        messages = list(translator.extract(tmpl.stream))
     889        self.assertEqual(2, len(messages))
     890        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     891                                          u'Foos %(fname)s %(lname)s'), []),
     892                         messages[0])
     893        self.assertEqual((7, 'ngettext', (u'Foo %(fname)s %(lname)s',
     894                                          u'Foos %(fname)s %(lname)s'), []),
     895                         messages[1])
     896
     897    def test_extract_i18n_choose_as_directive_with_params_and_domain_as_directive(self):
     898        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     899            xmlns:i18n="http://genshi.edgewall.org/i18n">
     900        <i18n:domain name="foo">
     901        <i18n:choose numeral="two" params="fname, lname">
     902          <p i18n:singular="">Foo ${fname} ${lname}</p>
     903          <p i18n:plural="">Foos ${fname} ${lname}</p>
     904        </i18n:choose>
     905        </i18n:domain>
     906        <i18n:choose numeral="one" params="fname, lname">
     907          <p i18n:singular="">Foo ${fname} ${lname}</p>
     908          <p i18n:plural="">Foos ${fname} ${lname}</p>
     909        </i18n:choose>
     910        </html>""")
     911        translator = Translator()
     912        tmpl.add_directives(Translator.NAMESPACE, translator)
     913        messages = list(translator.extract(tmpl.stream))
     914        self.assertEqual(2, len(messages))
     915        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
     916                                          u'Foos %(fname)s %(lname)s'), []),
     917                         messages[0])
     918        self.assertEqual((9, 'ngettext', (u'Foo %(fname)s %(lname)s',
     919                                          u'Foos %(fname)s %(lname)s'), []),
     920                         messages[1])
     921       
     922    def test_extract_i18n_choose_as_attribute_with_params_and_comment(self):
     923        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     924            xmlns:i18n="http://genshi.edgewall.org/i18n">
     925          <div i18n:choose="two; fname, lname" i18n:comment="As in Foo Bar">
     926            <p i18n:singular="">Foo $fname $lname</p>
     927            <p i18n:plural="">Foos $fname $lname</p>
     928          </div>
     929        </html>""")
     930        translator = Translator()
     931        tmpl.add_directives(Translator.NAMESPACE, translator)
     932        messages = list(translator.extract(tmpl.stream))
     933        self.assertEqual(1, len(messages))
     934        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     935                                          u'Foos %(fname)s %(lname)s'),
     936                          [u'As in Foo Bar']),
     937                         messages[0])
     938       
     939    def test_extract_i18n_choose_as_directive_with_params_and_comment(self):
     940        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     941            xmlns:i18n="http://genshi.edgewall.org/i18n">
     942        <i18n:choose numeral="two" params="fname, lname" i18n:comment="As in Foo Bar">
     943          <p i18n:singular="">Foo ${fname} ${lname}</p>
     944          <p i18n:plural="">Foos ${fname} ${lname}</p>
     945        </i18n:choose>
     946        </html>""")
     947        translator = Translator()
     948        tmpl.add_directives(Translator.NAMESPACE, translator)
     949        messages = list(translator.extract(tmpl.stream))
     950        self.assertEqual(1, len(messages))
     951        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
     952                                          u'Foos %(fname)s %(lname)s'),
     953                          [u'As in Foo Bar']),
     954                         messages[0])
     955
     956    def test_translate_i18n_domain_with_nested_inlcudes(self):
     957        import os, shutil, tempfile
     958        from genshi.template.loader import TemplateLoader
     959        dirname = tempfile.mkdtemp(suffix='genshi_test')
     960        try:
     961            for idx in range(7):
     962                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w')
     963                try:
     964                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     965                                         xmlns:py="http://genshi.edgewall.org/"
     966                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
     967                        <div>Included tmpl$idx</div>
     968                        <p i18n:msg="idx">Bar $idx</p>
     969                        <p i18n:domain="bar">Bar</p>
     970                        <p i18n:msg="idx" i18n:domain="">Bar $idx</p>
     971                        <p i18n:domain="" i18n:msg="idx">Bar $idx</p>
     972                        <py:if test="idx &lt; 6">
     973                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     974                        </py:if>
     975                    </html>""")
     976                finally:
     977                    file1.close()
     978
     979            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
     980            try:
     981                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     982                                     xmlns:py="http://genshi.edgewall.org/"
     983                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
     984                                     i18n:domain="foo">
     985                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     986                </html>""")
     987            finally:
     988                file2.close()
     989
     990            def callback(template):
     991                translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s'})
     992                translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'})
     993                translations.add_domain('bar', {'Bar': 'bar_Bar'})
     994                translator = Translator(translations)
     995                setup_i18n(template, translator)
     996            loader = TemplateLoader([dirname], callback=callback)
     997            tmpl = loader.load('tmpl10.html')
     998           
     999            self.assertEqual("""<html>
     1000                        <div>Included tmpl0</div>
     1001                        <p>foo_Bar 0</p>
     1002                        <p>bar_Bar</p>
     1003                        <p>Voh 0</p>
     1004                        <p>Voh 0</p>
     1005                        <div>Included tmpl1</div>
     1006                        <p>foo_Bar 1</p>
     1007                        <p>bar_Bar</p>
     1008                        <p>Voh 1</p>
     1009                        <p>Voh 1</p>
     1010                        <div>Included tmpl2</div>
     1011                        <p>foo_Bar 2</p>
     1012                        <p>bar_Bar</p>
     1013                        <p>Voh 2</p>
     1014                        <p>Voh 2</p>
     1015                        <div>Included tmpl3</div>
     1016                        <p>foo_Bar 3</p>
     1017                        <p>bar_Bar</p>
     1018                        <p>Voh 3</p>
     1019                        <p>Voh 3</p>
     1020                        <div>Included tmpl4</div>
     1021                        <p>foo_Bar 4</p>
     1022                        <p>bar_Bar</p>
     1023                        <p>Voh 4</p>
     1024                        <p>Voh 4</p>
     1025                        <div>Included tmpl5</div>
     1026                        <p>foo_Bar 5</p>
     1027                        <p>bar_Bar</p>
     1028                        <p>Voh 5</p>
     1029                        <p>Voh 5</p>
     1030                        <div>Included tmpl6</div>
     1031                        <p>foo_Bar 6</p>
     1032                        <p>bar_Bar</p>
     1033                        <p>Voh 6</p>
     1034                        <p>Voh 6</p>
     1035                </html>""", tmpl.generate(idx=-1).render())
     1036        finally:
     1037            shutil.rmtree(dirname)
     1038           
     1039    def test_translate_i18n_domain_with_nested_inlcudes_with_translatable_attrs(self):
     1040        import os, shutil, tempfile
     1041        from genshi.template.loader import TemplateLoader
     1042        dirname = tempfile.mkdtemp(suffix='genshi_test')
     1043        try:
     1044            for idx in range(4):
     1045                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w')
     1046                try:
     1047                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     1048                                         xmlns:py="http://genshi.edgewall.org/"
     1049                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip="">
     1050                        <div>Included tmpl$idx</div>
     1051                        <p title="${dg('foo', 'Bar %(idx)s') % dict(idx=idx)}" i18n:msg="idx">Bar $idx</p>
     1052                        <p title="Bar" i18n:domain="bar">Bar</p>
     1053                        <p title="Bar" i18n:msg="idx" i18n:domain="">Bar $idx</p>
     1054                        <p i18n:msg="idx" i18n:domain="" title="Bar">Bar $idx</p>
     1055                        <p i18n:domain="" i18n:msg="idx" title="Bar">Bar $idx</p>
     1056                        <py:if test="idx &lt; 3">
     1057                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     1058                        </py:if>
     1059                    </html>""")
     1060                finally:
     1061                    file1.close()
     1062
     1063            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w')
     1064            try:
     1065                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     1066                                     xmlns:py="http://genshi.edgewall.org/"
     1067                                     xmlns:i18n="http://genshi.edgewall.org/i18n"
     1068                                     i18n:domain="foo">
     1069                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/>
     1070                </html>""")
     1071            finally:
     1072                file2.close()
     1073               
     1074            translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s',
     1075                                              'Bar': 'Voh'})
     1076            translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'})
     1077            translations.add_domain('bar', {'Bar': 'bar_Bar'})
     1078            translator = Translator(translations)
     1079           
     1080            def callback(template):               
     1081                setup_i18n(template, translator)
     1082            loader = TemplateLoader([dirname], callback=callback)
     1083            tmpl = loader.load('tmpl10.html')
     1084           
     1085            self.assertEqual("""<html>
     1086                        <div>Included tmpl0</div>
     1087                        <p title="foo_Bar 0">foo_Bar 0</p>
     1088                        <p title="bar_Bar">bar_Bar</p>
     1089                        <p title="Voh">Voh 0</p>
     1090                        <p title="Voh">Voh 0</p>
     1091                        <p title="Voh">Voh 0</p>
     1092                        <div>Included tmpl1</div>
     1093                        <p title="foo_Bar 1">foo_Bar 1</p>
     1094                        <p title="bar_Bar">bar_Bar</p>
     1095                        <p title="Voh">Voh 1</p>
     1096                        <p title="Voh">Voh 1</p>
     1097                        <p title="Voh">Voh 1</p>
     1098                        <div>Included tmpl2</div>
     1099                        <p title="foo_Bar 2">foo_Bar 2</p>
     1100                        <p title="bar_Bar">bar_Bar</p>
     1101                        <p title="Voh">Voh 2</p>
     1102                        <p title="Voh">Voh 2</p>
     1103                        <p title="Voh">Voh 2</p>
     1104                        <div>Included tmpl3</div>
     1105                        <p title="foo_Bar 3">foo_Bar 3</p>
     1106                        <p title="bar_Bar">bar_Bar</p>
     1107                        <p title="Voh">Voh 3</p>
     1108                        <p title="Voh">Voh 3</p>
     1109                        <p title="Voh">Voh 3</p>
     1110                </html>""", tmpl.generate(idx=-1,
     1111                                          dg=translations.dugettext).render())
     1112        finally:
     1113            shutil.rmtree(dirname)
     1114           
     1115    def test_translate_with_i18n_msg_and_py_strip_directives(self):
     1116        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1117            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1118          <p i18n:msg="" i18n:comment="As in foo bar" py:strip="">Foo</p>
     1119          <p py:strip="" i18n:msg="" i18n:comment="As in foo bar">Foo</p>
     1120        </html>""")
     1121        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
     1122        setup_i18n(tmpl, translator)
     1123        self.assertEqual("""<html>
     1124          Voh
     1125          Voh
     1126        </html>""", tmpl.generate().render())
     1127       
     1128    def test_translate_i18n_choose_and_py_strip(self):
     1129        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1130            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1131          <div i18n:choose="two; fname, lname">
     1132            <p i18n:singular="">Foo $fname $lname</p>
     1133            <p i18n:plural="">Foos $fname $lname</p>
     1134          </div>
     1135        </html>""")
     1136        translations = DummyTranslations({
     1137            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     1138            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     1139                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     1140                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     1141        })
     1142        translator = Translator(translations)
     1143        setup_i18n(tmpl, translator)
     1144        self.assertEqual("""<html>
     1145          <div>
     1146            <p>Vohs John Doe</p>
     1147          </div>
     1148        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     1149       
     1150    def test_translate_i18n_choose_and_domain_and_py_strip(self):
     1151        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1152            xmlns:i18n="http://genshi.edgewall.org/i18n"
     1153            i18n:domain="foo">
     1154          <div i18n:choose="two; fname, lname">
     1155            <p i18n:singular="">Foo $fname $lname</p>
     1156            <p i18n:plural="">Foos $fname $lname</p>
     1157          </div>
     1158        </html>""")
     1159        translations = DummyTranslations()
     1160        translations.add_domain('foo', {
     1161            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s',
     1162            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s',
     1163                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s',
     1164                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s',
     1165        })
     1166        translator = Translator(translations)
     1167        setup_i18n(tmpl, translator)
     1168        self.assertEqual("""<html>
     1169          <div>
     1170            <p>Vohs John Doe</p>
     1171          </div>
     1172        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
     1173       
     1174    def test_extract_i18n_msg_with_py_strip(self):
     1175        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1176            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1177          <p i18n:msg="" py:strip="">
     1178            Please see <a href="help.html">Help</a> for details.
     1179          </p>
     1180        </html>""")
     1181        translator = Translator()
     1182        tmpl.add_directives(Translator.NAMESPACE, translator)
     1183        messages = list(translator.extract(tmpl.stream))
     1184        self.assertEqual(1, len(messages))
     1185        self.assertEqual((3, None, u'Please see [1:Help] for details.', []),
     1186                         messages[0])
     1187       
     1188    def test_extract_i18n_msg_with_py_strip_and_comment(self):
     1189        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1190            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1191          <p i18n:msg="" py:strip="" i18n:comment="Foo">
     1192            Please see <a href="help.html">Help</a> for details.
     1193          </p>
     1194        </html>""")
     1195        translator = Translator()
     1196        tmpl.add_directives(Translator.NAMESPACE, translator)
     1197        messages = list(translator.extract(tmpl.stream))
     1198        self.assertEqual(1, len(messages))
     1199        self.assertEqual((3, None, u'Please see [1:Help] for details.',
     1200                          ['Foo']), messages[0])
     1201
     1202    def test_extract_i18n_choose_as_attribute_and_py_strip(self):
     1203        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1204            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1205          <div i18n:choose="one" py:strip="">
     1206            <p i18n:singular="" py:strip="">FooBar</p>
     1207            <p i18n:plural="" py:strip="">FooBars</p>
     1208          </div>
     1209        </html>""")
     1210        translator = Translator()
     1211        tmpl.add_directives(Translator.NAMESPACE, translator)
     1212        messages = list(translator.extract(tmpl.stream))
     1213        self.assertEqual(1, len(messages))
     1214        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     1215       
     1216    def test_translate_i18n_domain_with_inline_directive_on_START_NS(self):
     1217        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1218            xmlns:i18n="http://genshi.edgewall.org/i18n" i18n:domain="foo">
     1219          <p i18n:msg="">FooBar</p>
     1220        </html>""")
     1221        translations = DummyTranslations({'Bar': 'Voh'})
     1222        translations.add_domain('foo', {'FooBar': 'BarFoo'})
     1223        translator = Translator(translations)
     1224        setup_i18n(tmpl, translator)
     1225        self.assertEqual("""<html>
     1226          <p>BarFoo</p>
     1227        </html>""", tmpl.generate().render())
     1228       
     1229    def test_translate_i18n_domain_with_inline_directive_on_START_NS_with_py_strip(self):
     1230        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1231            xmlns:i18n="http://genshi.edgewall.org/i18n"
     1232            i18n:domain="foo" py:strip="">
     1233          <p i18n:msg="">FooBar</p>
     1234        </html>""")
     1235        translations = DummyTranslations({'Bar': 'Voh'})
     1236        translations.add_domain('foo', {'FooBar': 'BarFoo'})
     1237        translator = Translator(translations)
     1238        setup_i18n(tmpl, translator)
     1239        self.assertEqual("""
     1240          <p>BarFoo</p>
     1241        """, tmpl.generate().render())
    4701242
    4711243class ExtractTestCase(unittest.TestCase):
    4721244
  • 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
    27 from genshi.template.directives import Directive
     27from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
     28                        XML_NAMESPACE, _ensure, StreamEventKind
     29from genshi.template.base import Context, DirectiveFactory, EXPR, SUB, \
     30                                 _apply_directives
     31from genshi.template.directives import Directive, StripDirective
    2832from genshi.template.markup import MarkupTemplate, EXEC
    2933
    3034__all__ = ['Translator', 'extract']
     
    3236
    3337I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n')
    3438
     39MSGBUF = StreamEventKind('MSGBUF')
     40
     41class DirectiveExtract(object):
     42    """Simple interface for directives to support messages extraction"""
     43
     44    def extract(self, stream, ctxt):
     45        raise NotImplementedError
    3546
    3647class CommentDirective(Directive):
     48    """Implementation of the ``i18n:comment`` template directive which adds
     49    translation comments.
     50   
     51    >>> from genshi.filters.i18n import Translator, setup_i18n
     52    >>> from genshi.template import MarkupTemplate
     53    >>>
     54    >>> translator = Translator()
     55    >>>
     56    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     57    ...   <p i18n:comment="As in Foo Bar">Foo</p>
     58    ... </html>''')
     59    >>>
     60    >>> setup_i18n(tmpl, translator)
     61    >>> list(translator.extract(tmpl.stream))
     62    [(2, None, u'Foo', [u'As in Foo Bar'])]
     63    >>>
     64    """
    3765
    38     __slots__ = []
     66    __slots__ = ['comment']
    3967
    40     @classmethod
    41     def attach(cls, template, stream, value, namespaces, pos):
    42         return None, stream
     68    def __init__(self, value, template, hints=None, namespaces=None,
     69                 lineno=-1, offset=-1):
     70        Directive.__init__(self, None, template, namespaces, lineno, offset)
     71        self.comment = value
    4372
     73    def __call__(self, stream, directives, ctxt, **vars):
     74        return stream
    4475
    45 class MsgDirective(Directive):
     76class MsgDirective(Directive, DirectiveExtract):
     77    r"""Implementation of the ``i18n:msg`` directive which marks inner content
     78    as translatable. Consider the following examples:
     79   
     80    >>> from genshi.filters.i18n import Translator, setup_i18n
     81    >>> from genshi.template import MarkupTemplate
     82    >>>
     83    >>> translator = Translator()
     84    >>>
     85    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     86    ...   <div i18n:msg="">
     87    ...     <p>Foo</p>
     88    ...     <p>Bar</p>
     89    ...   </div>
     90    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     91    ... </html>''')
     92    >>>
     93    >>> setup_i18n(tmpl, translator)
     94    >>>
     95    >>> list(translator.extract(tmpl.stream))
     96    [(2, None, u'[1:Foo]\n    [2:Bar]', []), (6, None, u'Foo [1:bar]!', [])]
     97    >>> print tmpl.generate().render()
     98    <html>
     99      <div><p>Foo</p>
     100        <p>Bar</p></div>
     101      <p>Foo <em>bar</em>!</p>
     102    </html>
     103    >>>
     104    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     105    ...   <div i18n:msg="fname, lname">
     106    ...     <p>First Name: ${fname}</p>
     107    ...     <p>Last Name: ${lname}</p>
     108    ...   </div>
     109    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     110    ... </html>''')
     111    >>> setup_i18n(tmpl, translator)
     112    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     113    [(2, None, u'[1:First Name: %(fname)s]\n    [2:Last Name: %(lname)s]', []),
     114    (6, None, u'Foo [1:bar]!', [])]
     115    >>>
     116    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     117    ...   <div i18n:msg="fname, lname">
     118    ...     <p>First Name: ${fname}</p>
     119    ...     <p>Last Name: ${lname}</p>
     120    ...   </div>
     121    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     122    ... </html>''')
     123    >>> setup_i18n(tmpl, translator)
     124    >>> print tmpl.generate(fname='John', lname='Doe').render()
     125    <html>
     126      <div><p>First Name: John
     127        <p>Last Name: Doe</div>
     128      <p>Foo <em>bar</em>!</p>
     129    </html>
     130    >>>
     131   
     132    Starting and ending white-space is stripped of to make it simpler for
     133    translators. Stripping it is not that important since it's on the html
     134    source, the rendered output will remain the same.
     135    """
    46136
    47137    __slots__ = ['params']
    48138
    49139    def __init__(self, value, template, hints=None, namespaces=None,
    50140                 lineno=-1, offset=-1):
    51141        Directive.__init__(self, None, template, namespaces, lineno, offset)
    52         self.params = [name.strip() for name in value.split(',')]
     142        self.params = [param.strip() for param in value.split(',') if param]
     143
     144    @classmethod
     145    def attach(cls, template, stream, value, namespaces, pos):
     146        if type(value) is dict:
     147            value = value.get('params', '').strip()
     148        return super(MsgDirective, cls).attach(template, stream, value.strip(),
     149                                               namespaces, pos)
    53150
    54151    def __call__(self, stream, directives, ctxt, **vars):
     152
     153        gettext = ctxt.get('_i18n.gettext')
     154        dgettext = ctxt.get('_i18n.dgettext')
     155        if ctxt.get('_i18n.domain'):
     156            assert callable(dgettext), "No domain gettext function passed"
     157            gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
     158
     159        msgbuf = MessageBuffer(self.params)
     160
     161        new_stream = []
     162        stream = iter(_apply_directives(stream, directives, ctxt))
     163        strip_directive = [d for d in directives if
     164                           isinstance(d, StripDirective)]
     165        new_stream.append(stream.next())
     166        previous = stream.next()
     167        for kind, data, pos in stream:
     168            if kind is SUB and not strip_directive:
     169                # py:attrs for example
     170                subdirectives, substream = data
     171                for skind, sdata, spos in _apply_directives(substream,
     172                                                            subdirectives,
     173                                                            ctxt):
     174                    try:
     175                        msgbuf.append(*previous)
     176                        previous = skind, sdata, spos
     177                    except IndexError:
     178                        raise IndexError("Not enough parameters passed to '%s' "
     179                                         "on '%s', line number %s: %s" %
     180                                         (type(self).__name__,
     181                                          os.path.basename(spos[0]), spos[1],
     182                                          self.params))
     183            try:
     184                msgbuf.append(*previous)
     185            except IndexError:
     186                raise IndexError("Not enough parameters passed to '%s' on '%s',"
     187                                 " line number %s: %s" %
     188                                 (type(self).__name__,
     189                                  os.path.basename(previous[2][0]),
     190                                  previous[2][1], self.params), previous[1])
     191
     192            previous = kind, data, pos
     193
     194        for event in msgbuf.translate(gettext(msgbuf.format())):
     195            new_stream.append(event)
     196        new_stream.append(previous)
     197        if strip_directive:
     198            return _apply_directives(new_stream, strip_directive, ctxt)
     199        return new_stream
     200
     201    def extract(self, stream, ctxt):
    55202        msgbuf = MessageBuffer(self.params)
    56203
    57204        stream = iter(stream)
     205        stream.next() # the outer start tag
     206        previous = stream.next()
     207        for event in stream:
     208            try:
     209                msgbuf.append(*previous)
     210            except IndexError:
     211                raise IndexError("Not enough parameters passed to '%s' on '%s',"
     212                                 " line number %s: %s" %
     213                                 (type(self).__name__,
     214                                  os.path.basename(previous[2][0]),
     215                                  previous[2][1], self.params))
     216            previous = event
     217
     218        yield None, msgbuf.format(), filter(None, [ctxt.get('_i18n.comment')])
     219
     220class InnerChooseDirective(Directive):
     221    __slots__ = []
     222
     223    def __call__(self, stream, directives, ctxt, **vars):
     224
     225        msgbuf = MessageBuffer(ctxt.get('_i18n.choose.params', [])[:])
     226
     227        stream = iter(_apply_directives(stream, directives, ctxt))
    58228        yield stream.next() # the outer start tag
    59229        previous = stream.next()
     230#        if previous[0] is TEXT and not previous[1].strip():
     231#            yield previous  # white space and newlines
     232        for kind, data, pos in stream:
     233
     234            msgbuf.append(*previous)
     235            previous = kind, data, pos
     236#            if event[0] is TEXT and not event[1].strip():
     237#                yield event # white space and newlines
     238        yield MSGBUF, (), -1 # the place holder for msgbuf output
     239        yield previous # the outer end tag
     240        ctxt['_i18n.choose.%s' % type(self).__name__] = msgbuf
     241
     242
     243    def extract(self, stream, ctxt, msgbuf):
     244
     245        stream = iter(stream)
     246        stream.next() # the outer start tag
     247        previous = stream.next()
    60248        for event in stream:
    61249            msgbuf.append(*previous)
    62250            previous = event
     251        return msgbuf
    63252
    64         gettext = ctxt.get('_i18n.gettext')
    65         for event in msgbuf.translate(gettext(msgbuf.format())):
    66             yield event
    67253
    68         yield previous # the outer end tag
     254class SingularDirective(InnerChooseDirective):
     255    """Implementation of the ``i18n:singular`` directive to be used with the
     256    ``i18n:choose`` directive."""
     257
     258
     259class PluralDirective(InnerChooseDirective):
     260    """Implementation of the ``i18n:plural`` directive to be used with the
     261    ``i18n:choose`` directive."""
     262
     263
     264class ChooseDirective(Directive, DirectiveExtract):
     265    """Implementation of the ``i18n:choose`` directive which provides plural
     266    internationalisation of strings.
     267   
     268    This directive requires at least one parameter, the one which evaluates to
     269    an integer which will allow to choose the plural/singular form. If you also
     270    have expressions inside the singular and plural version of the string you
     271    also need to pass a name for those parameters. Consider the following
     272    examples:
     273   
     274    >>> from genshi.filters.i18n import Translator, setup_i18n
     275    >>> from genshi.template import MarkupTemplate
     276    >>>
     277    >>> translator = Translator()
     278    >>>
     279    >>> tmpl = MarkupTemplate('''\
     280        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     281    ...   <div i18n:choose="num; num">
     282    ...     <p i18n:singular="">There is $num coin</p>
     283    ...     <p i18n:plural="">There are $num coins</p>
     284    ...   </div>
     285    ... </html>''')
     286    >>> setup_i18n(tmpl, translator)
     287    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     288    [(2, 'ngettext', (u'There is %(num)s coin',
     289                      u'There are %(num)s coins'), [])]
     290    >>>
     291    >>> tmpl = MarkupTemplate('''\
     292        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     293    ...   <div i18n:choose="num; num">
     294    ...     <p i18n:singular="">There is $num coin</p>
     295    ...     <p i18n:plural="">There are $num coins</p>
     296    ...   </div>
     297    ... </html>''')
     298    >>> setup_i18n(tmpl, translator)
     299    >>> print tmpl.generate(num=1).render()
     300    <html>
     301      <div>
     302        <p>There is 1 coin</p>
     303      </div>
     304    </html>
     305    >>> print tmpl.generate(num=2).render()
     306    <html>
     307      <div>
     308        <p>There are 2 coins</p>
     309      </div>
     310    </html>
     311    >>>
     312   
     313    When used as a directive and not as an attribute:
     314    >>> tmpl = MarkupTemplate('''\
     315        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     316    ...   <i18n:choose numeral="num" params="num">
     317    ...     <p i18n:singular="">There is $num coin</p>
     318    ...     <p i18n:plural="">There are $num coins</p>
     319    ...   </i18n:choose>
     320    ... </html>''')
     321    >>> setup_i18n(tmpl, translator)
     322    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     323    [(2, 'ngettext', (u'There is %(num)s coin',
     324                      u'There are %(num)s coins'), [])]
     325    >>>
     326    """
     327
     328    __slots__ = ['numeral', 'params']
     329
     330    def __init__(self, value, template, hints=None, namespaces=None,
     331                 lineno=-1, offset=-1):
     332        Directive.__init__(self, None, template, namespaces, lineno, offset)
     333        params = [v.strip() for v in value.split(';')]
     334        self.numeral = self._parse_expr(params.pop(0), template, lineno, offset)
     335        self.params = params and [name.strip() for name in
     336                                  params[0].split(',') if name] or []
     337
     338    @classmethod
     339    def attach(cls, template, stream, value, namespaces, pos):
     340        if type(value) is dict:
     341            numeral = value.get('numeral', '').strip()
     342            assert numeral is not '', "at least pass the numeral param"
     343            params = [v.strip() for v in value.get('params', '').split(',')]
     344            value = '%s; ' % numeral + ', '.join(params)
     345        return super(ChooseDirective, cls).attach(template, stream, value,
     346                                                  namespaces, pos)
     347
     348    def __call__(self, stream, directives, ctxt, **vars):
     349
     350        ctxt.push({'_i18n.choose.params': self.params,
     351                   '_i18n.choose.SingularDirective': None,
     352                   '_i18n.choose.PluralDirective': None})
     353
     354        new_stream = []
     355        singular_stream = None
     356        singular_msgbuf = None
     357        plural_stream = None
     358        plural_msgbuf = None
     359
     360        ngettext = ctxt.get('_i18n.ungettext')
     361        assert callable(ngettext), "No ngettext function available"
     362        dngettext = ctxt.get('_i18n.dngettext')
     363        if not dngettext:
     364            dngettext = lambda d, s, p, n: ngettext(s, p, n)
     365
     366        for kind, event, pos in stream:
     367            if kind is SUB:
     368                subdirectives, substream = event
     369                if isinstance(subdirectives[0],
     370                              SingularDirective) and not singular_stream:
     371                    # Apply directives to update context
     372                    singular_stream = list(_apply_directives(substream,
     373                                                             subdirectives,
     374                                                             ctxt))
     375                    new_stream.append((MSGBUF, (), -1)) # msgbuf place holder
     376                    singular_msgbuf = ctxt.get('_i18n.choose.SingularDirective')
     377                elif isinstance(subdirectives[0],
     378                                PluralDirective) and not plural_stream:
     379                    # Apply directives to update context
     380                    plural_stream = list(_apply_directives(substream,
     381                                                           subdirectives, ctxt))
     382                    plural_msgbuf = ctxt.get('_i18n.choose.PluralDirective')
     383                else:
     384                    new_stream.append((kind, event, pos))
     385            else:
     386                new_stream.append((kind, event, pos))
     387
     388        if ctxt.get('_i18n.domain'):
     389            ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'),
     390                                                 s, p, n)
     391
     392        for kind, data, pos in new_stream:
     393            if kind is MSGBUF:
     394                for skind, sdata, spos in singular_stream:
     395                    if skind is MSGBUF:
     396                        translation = ngettext(singular_msgbuf.format(),
     397                                               plural_msgbuf.format(),
     398                                               self.numeral.evaluate(ctxt))
     399                        for event in singular_msgbuf.translate(translation):
     400                            yield event
     401                    else:
     402                        yield skind, sdata, spos
     403            else:
     404                yield kind, data, pos
     405
     406        ctxt.pop()
     407
     408    def extract(self, stream, ctxt):
     409
     410        stream = iter(stream)
     411        previous = stream.next()
     412        if previous is START:
     413            stream.next()
     414
     415        singular_msgbuf = MessageBuffer(self.params[:])
     416        plural_msgbuf = MessageBuffer(self.params[:])
     417
     418        for kind, event, pos in stream:
     419            if kind is SUB:
     420                subdirectives, substream = event
     421                for subdirective in subdirectives:
     422                    if isinstance(subdirective, SingularDirective):
     423                        singular_msgbuf = subdirective.extract(substream, ctxt,
     424                                                               singular_msgbuf)
     425                    elif isinstance(subdirective, PluralDirective):
     426                        plural_msgbuf = subdirective.extract(substream, ctxt,
     427                                                             plural_msgbuf)
     428                    elif not isinstance(subdirective, StripDirective):
     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 "
     434                                             "'%s' on '%s', line number %s: "
     435                                             "%s" % (type(self).__name__,
     436                                                     os.path.basename(pos[0]),
     437                                                     pos[1], self.params))
     438            else:
     439                try:
     440                    singular_msgbuf.append(kind, event, pos)
     441                    plural_msgbuf.append(kind, event, pos)
     442                except IndexError:
     443                    raise IndexError("Not enough parameters passed to '%s' on "
     444                                     "'%s', line number %s: %s" %
     445                                     (type(self).__name__,
     446                                      os.path.basename(pos[0]), pos[1],
     447                                      self.params))
     448
     449        yield 'ngettext', \
     450            (singular_msgbuf.format(), plural_msgbuf.format()), \
     451            filter(None, [ctxt.get('_i18n.comment')])
     452
     453class DomainDirective(Directive):
     454    """Implementation of the ``i18n:domain`` directive which allows choosing
     455    another i18n domain(catalog) to translate from.
     456   
     457    >>> from gettext import NullTranslations
     458    >>> from genshi.filters.i18n import Translator, setup_i18n
     459    >>> from genshi.template.markup import MarkupTemplate
     460    >>>
     461    >>> class DummyTranslations(NullTranslations):
     462    ...     _domains = {}
     463    ...     def __init__(self, catalog):
     464    ...         NullTranslations.__init__(self)
     465    ...         self._catalog = catalog
     466    ...     def add_domain(self, domain, catalog):
     467    ...         translation = DummyTranslations(catalog)
     468    ...         translation.add_fallback(self)
     469    ...         self._domains[domain] = translation
     470    ...     def _domain_call(self, func, domain, *args, **kwargs):
     471    ...         return getattr(self._domains.get(domain, self), func)(*args,
     472    ...                                                               **kwargs)
     473    ...     def ugettext(self, message):
     474    ...         missing = object()
     475    ...         tmsg = self._catalog.get(message, missing)
     476    ...         if tmsg is missing:
     477    ...             if self._fallback:
     478    ...                 return self._fallback.ugettext(message)
     479    ...             return unicode(message)
     480    ...         return tmsg
     481    ...     def dugettext(self, domain, message):
     482    ...         return self._domain_call('ugettext', domain, message)
     483    ...
     484    >>>
     485    >>> tmpl = MarkupTemplate('''\
     486        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     487    ...   <p i18n:msg="">Bar</p>
     488    ...   <div i18n:domain="foo">
     489    ...     <p i18n:msg="">FooBar</p>
     490    ...     <p>Bar</p>
     491    ...     <p i18n:domain="bar" i18n:msg="">Bar</p>
     492    ...     <p i18n:domain="">Bar</p>
     493    ...   </div>
     494    ...   <p>Bar</p>
     495    ... </html>''')
     496    >>>
     497    >>> translations = DummyTranslations({'Bar': 'Voh'})
     498    >>> translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
     499    >>> translations.add_domain('bar', {'Bar': 'bar_Bar'})
     500    >>> translator = Translator(translations)
     501    >>> setup_i18n(tmpl, translator)
     502    >>>
     503    >>> print tmpl.generate().render()
     504    <html>
     505      <p>Voh</p>
     506      <div>
     507        <p>BarFoo</p>
     508        <p>foo_Bar</p>
     509        <p>bar_Bar</p>
     510        <p>Voh</p>
     511      </div>
     512      <p>Voh</p>
     513    </html>
     514    >>>
     515    """
     516
     517    __slots__ = ['domain']
     518
     519    def __init__(self, value, template, hints=None, namespaces=None,
     520                 lineno=-1, offset=-1):
     521        Directive.__init__(self, None, template, namespaces, lineno, offset)
     522        self.domain = value and value.strip() or '__DEFAULT__'
     523
     524    @classmethod
     525    def attach(cls, template, stream, value, namespaces, pos):
     526        if type(value) is dict:
     527            value = value.get('name')
     528        return super(DomainDirective, cls).attach(template, stream, value,
     529                                                  namespaces, pos)
     530
     531    def __call__(self, stream, directives, ctxt, **vars):
     532        ctxt.push({'_i18n.domain': self.domain})
     533        for event in _apply_directives(stream, directives, ctxt):
     534            yield event
     535        ctxt.pop()
    69536
    70537
    71538class Translator(DirectiveFactory):
    72539    """Can extract and translate localizable strings from markup streams and
    73540    templates.
    74541   
    75     For example, assume the followng template:
     542    For example, assume the following template:
    76543   
    77544    >>> from genshi.template import MarkupTemplate
    78     >>> 
     545    >>>
    79546    >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    80547    ...   <head>
    81548    ...     <title>Example</title>
     
    94561    ...         'Example': 'Beispiel',
    95562    ...         'Hello, %(name)s': 'Hallo, %(name)s'
    96563    ...     }[string]
    97     >>> 
     564    >>>
    98565    >>> translator = Translator(pseudo_gettext)
    99566   
    100567    Next, the translator needs to be prepended to any already defined filters
     
    115582        <p>Hallo, Hans</p>
    116583      </body>
    117584    </html>
    118 
     585   
    119586    Note that elements defining ``xml:lang`` attributes that do not contain
    120587    variable expressions are ignored by this filter. That can be used to
    121588    exclude specific parts of a template from being extracted and translated.
    122589    """
    123590
    124591    directives = [
     592        ('domain', DomainDirective),
    125593        ('comment', CommentDirective),
    126         ('msg', MsgDirective)
     594        ('msg', MsgDirective),
     595        ('choose', ChooseDirective),
     596        ('singular', SingularDirective),
     597        ('plural', PluralDirective)
    127598    ]
    128599
    129600    IGNORE_TAGS = frozenset([
    130601        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
    131602        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
    132603    ])
    133     INCLUDE_ATTRS = frozenset(['abbr', 'alt', 'label', 'prompt', 'standby',
    134                                'summary', 'title'])
     604    INCLUDE_ATTRS = frozenset([
     605        'abbr', 'alt', 'label', 'prompt', 'standby', 'summary', 'title'
     606    ])
    135607    NAMESPACE = I18N_NAMESPACE
    136608
    137609    def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
     
    145617        :param extract_text: whether the content of text nodes should be
    146618                             extracted, or only text in explicit ``gettext``
    147619                             function calls
    148 
     620       
    149621        :note: Changed in 0.6: the `translate` parameter can now be either
    150622               a ``gettext``-style function, or an object compatible with the
    151623               ``NullTransalations`` or ``GNUTranslations`` interface
     
    177649
    178650        if type(self.translate) is FunctionType:
    179651            gettext = self.translate
     652            if ctxt:
     653                ctxt['_i18n.gettext'] = gettext
    180654        else:
    181655            gettext = self.translate.ugettext
    182         if ctxt:
    183             ctxt['_i18n.gettext'] = gettext
     656            try:
     657                dgettext = self.translate.dugettext
     658            except AttributeError:
     659                dgettext = lambda x, y: gettext(y)
     660            ngettext = self.translate.ungettext
     661            try:
     662                dngettext = self.translate.dungettext
     663            except AttributeError:
     664                dngettext = lambda d, s, p, n: ngettext(s, p, n)
     665
     666            if ctxt:
     667                ctxt['_i18n.gettext'] = gettext
     668                ctxt['_i18n.ugettext'] = gettext
     669                ctxt['_i18n.dgettext'] = dgettext
     670                ctxt['_i18n.ngettext'] = ngettext
     671                ctxt['_i18n.ungettext'] = ngettext
     672                ctxt['_i18n.dngettext'] = dngettext
    184673
    185674        extract_text = self.extract_text
    186675        if not extract_text:
    187676            search_text = False
    188677
     678        if ctxt and ctxt.get('_i18n.domain'):
     679            old_gettext = gettext
     680            gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
     681
    189682        for kind, data, pos in stream:
    190683
    191684            # skip chunks that should not be localized
     
    208701
    209702                new_attrs = []
    210703                changed = False
     704
    211705                for name, value in attrs:
    212706                    newval = value
    213707                    if extract_text and isinstance(value, basestring):
    214708                        if name in include_attrs:
    215709                            newval = gettext(value)
    216710                    else:
    217                         newval = list(self(_ensure(value), ctxt,
    218                             search_text=False)
     711                        newval = list(
     712                            self(_ensure(value), ctxt, search_text=False)
    219713                        )
    220714                    if newval != value:
    221715                        value = newval
     
    234728
    235729            elif kind is SUB:
    236730                directives, substream = data
    237                 # If this is an i18n:msg directive, no need to translate text
     731                current_domain = None
     732                for idx, directive in enumerate(directives):
     733                    # Organize directives to make everything work
     734                    if isinstance(directive, StripDirective):
     735                        # Push stripping to last
     736                        directives.append(directives.pop(idx))
     737                    elif isinstance(directive, DomainDirective):
     738                        # Grab current domain and update context
     739                        current_domain = directive.domain
     740                        ctxt.push({'_i18n.domain': current_domain})
     741                        # Put domain directive as the first one in order to
     742                        # update context before any other directives evaluation
     743                        directives.insert(0, directives.pop(idx))
     744
     745                # If this is an i18n directive, no need to translate text
    238746                # nodes here
    239                 is_msg = filter(None, [isinstance(d, MsgDirective)
    240                                        for d in directives])
     747                is_i18n_directive = filter(None,
     748                                           [isinstance(d, DirectiveExtract)
     749                                            for d in directives])
    241750                substream = list(self(substream, ctxt,
    242                                       search_text=not is_msg))
     751                                      search_text=not is_i18n_directive))
    243752                yield kind, (directives, substream), pos
    244753
     754                if current_domain:
     755                    ctxt.pop()
    245756            else:
    246757                yield kind, data, pos
    247758
     
    249760                         'ugettext', 'ungettext')
    250761
    251762    def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
    252                 search_text=True, msgbuf=None):
     763                search_text=True, msgbuf=None, ctxt=Context()):
    253764        """Extract localizable strings from the given template stream.
    254765       
    255766        For every string found, this function yields a ``(lineno, function,
     
    265776           from ``i18n:comment`` attributes found in the markup
    266777       
    267778        >>> from genshi.template import MarkupTemplate
    268         >>> 
     779        >>>
    269780        >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    270781        ...   <head>
    271782        ...     <title>Example</title>
     
    276787        ...     <p>${ngettext("You have %d item", "You have %d items", num)}</p>
    277788        ...   </body>
    278789        ... </html>''', filename='example.html')
    279         >>> 
     790        >>>
    280791        >>> for line, func, msg, comments in Translator().extract(tmpl.stream):
    281792        ...    print "%d, %r, %r" % (line, func, msg)
    282793        3, None, u'Example'
     
    291802                                  functions
    292803        :param search_text: whether the content of text nodes should be
    293804                            extracted (used internally)
     805        :param ctxt: the current extraction context (used internaly)
    294806       
    295807        :note: Changed in 0.4.1: For a function with multiple string arguments
    296808               (such as ``ngettext``), a single item with a tuple of strings is
    297809               yielded, instead an item for each string argument.
    298810        :note: Changed in 0.6: The returned tuples now include a 4th element,
    299                which is a list of comments for the translator
     811               which is a list of comments for the translator. Added an ``ctxt``
     812               argument which is used to pass arround the current extraction
     813               context.
    300814        """
    301815        if not self.extract_text:
    302816            search_text = False
    303817        skip = 0
    304         i18n_comment = I18N_NAMESPACE['comment']
    305         i18n_msg = I18N_NAMESPACE['msg']
     818
     819        # Un-comment bellow to extract messages without adding directives
     820#        i18n_comment = I18N_NAMESPACE['comment']
     821#        i18n_msg = I18N_NAMESPACE['msg']
    306822        xml_lang = XML_NAMESPACE['lang']
    307823
    308824        for kind, data, pos in stream:
    309 
    310825            if skip:
    311826                if kind is START:
    312827                    skip += 1
     
    326841                        if name in self.include_attrs:
    327842                            text = value.strip()
    328843                            if text:
     844                                # XXX: Do we need to grab i18n:comment from ctxt ???
    329845                                yield pos[1], None, text, []
    330846                    else:
    331847                        for lineno, funcname, text, comments in self.extract(
     
    335851
    336852                if msgbuf:
    337853                    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                         )
     854                # Un-comment bellow to extract messages without adding
     855                # directives
     856#                else:
     857#                    msg_params = attrs.get(i18n_msg)
     858#                    if msg_params is not None:
     859#                        print kind, data, pos
     860#                        if type(msg_params) is list: # event tuple
     861#                            msg_params = msg_params[0][1]
     862#                        msgbuf = MessageBuffer(
     863#                            msg_params, attrs.get(i18n_comment), pos[1]
     864#                        )
    346865
    347866            elif not skip and search_text and kind is TEXT:
    348867                if not msgbuf:
    349868                    text = data.strip()
    350869                    if text and filter(None, [ch.isalpha() for ch in text]):
    351                         yield pos[1], None, text, []
     870                        yield pos[1], None, text, \
     871                                    filter(None, [ctxt.get('_i18n.comment')])
    352872                else:
    353873                    msgbuf.append(kind, data, pos)
    354874
     
    356876                msgbuf.append(kind, data, pos)
    357877                if not msgbuf.depth:
    358878                    yield msgbuf.lineno, None, msgbuf.format(), \
    359                           filter(None, [msgbuf.comment])
     879                                                  filter(None, [msgbuf.comment])
    360880                    msgbuf = None
    361881
    362882            elif kind is EXPR or kind is EXEC:
     
    364884                    msgbuf.append(kind, data, pos)
    365885                for funcname, strings in extract_from_code(data,
    366886                                                           gettext_functions):
     887                    # XXX: Do we need to grab i18n:comment from ctxt ???
    367888                    yield pos[1], funcname, strings, []
    368889
    369890            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
     891                directives, substream = data
    376892
     893                comment = None
     894                for idx, directive in enumerate(directives):
     895                    # Do a first loop to see if there's a comment directive
     896                    # If there is update context and pop it from directives
     897                    if isinstance(directive, CommentDirective):
     898                        comment = directive.comment
     899                        ctxt.push({'_i18n.comment': comment})
     900                        if len(directives) == 1:
     901                            # in case we're in the presence of something like:
     902                            # <p i18n:comment="foo">Foo</p>
     903                            messages = self.extract(
     904                                substream, gettext_functions,
     905                                search_text=search_text and not skip,
     906                                msgbuf=msgbuf, ctxt=ctxt)
     907                            for lineno, funcname, text, comments in messages:
     908                                yield lineno, funcname, text, comments
     909                        directives.pop(idx)
     910                    elif isinstance(directive, StripDirective):
     911                        # Previously we didn't evaluate py:strip directives
     912                        # in extraction, let's continue not to
     913                        directives.pop(idx)
     914
     915                for directive in directives:
     916                    if isinstance(directive, DirectiveExtract):
     917                        messages = directive.extract(substream, ctxt)
     918                        for funcname, text, comments in messages:
     919                            yield pos[1], funcname, text, comments
     920                    else:
     921                        messages = self.extract(
     922                            substream, gettext_functions,
     923                            search_text=search_text and not skip, msgbuf=msgbuf)
     924                        for lineno, funcname, text, comments in messages:
     925                            yield lineno, funcname, text, comments
     926                if comment:
     927                    ctxt.pop()
    377928
    378929class MessageBuffer(object):
    379930    """Helper class for managing internationalized mixed content.
     
    391942        """
    392943        if isinstance(params, basestring):
    393944            params = [name.strip() for name in params.split(',')]
    394         self.params = params
     945        # params list needs to be copied so that directives can be evaluated
     946        # more than once
     947        self.params = params[:]
    395948        self.comment = comment
    396949        self.lineno = lineno
    397950        self.string = []
     
    408961        :param data: the event data
    409962        :param pos: the position of the event in the source
    410963        """
     964        if kind is SUB:
     965            # py:attrs for example
     966            for skind, sdata, spos in data[1]:
     967                self.append(skind, sdata, spos)
    411968        if kind is TEXT:
    412969            self.string.append(data)
    413970            self.events.setdefault(self.stack[-1], []).append(None)
     
    4641021def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')):
    4651022    """Parse a translated message using Genshi mixed content message
    4661023    formatting.
    467 
     1024   
    4681025    >>> parse_msg("See [1:Help].")
    4691026    [(0, 'See '), (1, 'Help'), (0, '.')]
    470 
     1027   
    4711028    >>> parse_msg("See [1:our [2:Help] page] for details.")
    4721029    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
    473 
     1030   
    4741031    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
    4751032    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
    476 
     1033   
    4771034    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
    4781035    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
    479 
     1036   
    4801037    :param string: the translated message string
    4811038    :return: a list of ``(order, string)`` tuples
    4821039    :rtype: `list`
     
    5141071    >>> expr = Expression('_("Hello")')
    5151072    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    5161073    [('_', u'Hello')]
    517 
     1074   
    5181075    >>> expr = Expression('ngettext("You have %(num)s item", '
    5191076    ...                            '"You have %(num)s items", num)')
    5201077    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
     
    5841141
    5851142    tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
    5861143                          encoding=encoding)
     1144
    5871145    translator = Translator(None, ignore_tags, include_attrs, extract_text)
     1146    if hasattr(tmpl, 'add_directives'):
     1147        tmpl.add_directives(Translator.NAMESPACE, translator)
    5881148    for message in translator.extract(tmpl.stream, gettext_functions=keywords):
    5891149        yield message
     1150
     1151def setup_i18n(template, translator):
     1152    """Convinience function to setup both the i18n filter and the i18n
     1153    directives.
     1154   
     1155    :param template: an instance of a genshi template
     1156    :param translator: an instance of ``Translator``
     1157    """
     1158    template.filters.insert(0, translator)
     1159    if hasattr(template, 'add_directives'):
     1160        template.add_directives(Translator.NAMESPACE, translator)
  • genshi/template/directives.py

     
    530530
    531531    def __call__(self, stream, directives, ctxt, **vars):
    532532        def _generate():
    533             if _eval_expr(self.expr, ctxt, **vars):
     533            if not self.expr:
     534                for event in list(stream)[1:-1]:
     535                    yield event
     536            elif _eval_expr(self.expr, ctxt, **vars):
    534537                stream.next() # skip start tag
    535538                previous = stream.next()
    536539                for event in stream:
     
    539542            else:
    540543                for event in stream:
    541544                    yield event
    542         return _apply_directives(_generate(), directives, ctxt, **vars)
    543 
    544     def attach(cls, template, stream, value, namespaces, pos):
    545         if not value:
    546             return None, stream[1:-1]
    547         return super(StripDirective, cls).attach(template, stream, value,
    548                                                  namespaces, pos)
    549     attach = classmethod(attach)
     545        return _apply_directives(_generate(), directives, ctxt)
    550546
    551547
    552548class ChooseDirective(Directive):