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, 6 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):