Edgewall Software

Ticket #129: i18n_directives_full.patch

File i18n_directives_full.patch, 74.8 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) 
     500        self.assertEqual("""<html> 
     501          <p>Voh</p> 
     502        </html>""", tmpl.generate().render()) 
     503         
     504    def test_translate_i18n_domain_with_msg_directives(self): 
     505        #"""translate with i18n:domain and nested i18n:msg directives """ 
     506 
     507        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     508            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     509          <div i18n:domain="foo"> 
     510            <p i18n:msg="">FooBar</p> 
     511            <p i18n:msg="">Bar</p> 
     512          </div> 
     513        </html>""") 
     514        translations = DummyTranslations({'Bar': 'Voh'}) 
     515        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) 
     516        translator = Translator(translations) 
     517        setup_i18n(tmpl, translator) 
     518        self.assertEqual("""<html> 
     519          <div> 
     520            <p>BarFoo</p> 
     521            <p>PT_Foo</p> 
     522          </div> 
     523        </html>""", tmpl.generate().render()) 
     524         
     525    def test_translate_i18n_domain_with_inline_directives(self): 
     526        #"""translate with inlined i18n:domain and i18n:msg directives""" 
     527        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     528            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     529          <p i18n:msg="" i18n:domain="foo">FooBar</p> 
     530        </html>""") 
     531        translations = DummyTranslations({'Bar': 'Voh'}) 
     532        translations.add_domain('foo', {'FooBar': 'BarFoo'}) 
     533        translator = Translator(translations) 
     534        setup_i18n(tmpl, translator) 
     535        self.assertEqual("""<html> 
     536          <p>BarFoo</p> 
     537        </html>""", tmpl.generate().render()) 
     538         
     539    def test_translate_i18n_domain_without_msg_directives(self): 
     540        #"""translate domain call without i18n:msg directives still uses current domain""" 
     541         
     542        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     543            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     544          <p i18n:msg="">Bar</p> 
     545          <div i18n:domain="foo"> 
     546            <p i18n:msg="">FooBar</p> 
     547            <p i18n:msg="">Bar</p>             
     548            <p>Bar</p> 
     549          </div>           
     550          <p>Bar</p> 
     551        </html>""") 
     552        translations = DummyTranslations({'Bar': 'Voh'}) 
     553        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) 
     554        translator = Translator(translations) 
     555        setup_i18n(tmpl, translator) 
     556        self.assertEqual("""<html> 
     557          <p>Voh</p> 
     558          <div> 
     559            <p>BarFoo</p> 
     560            <p>PT_Foo</p> 
     561            <p>PT_Foo</p> 
     562          </div> 
     563          <p>Voh</p> 
     564        </html>""", tmpl.generate().render()) 
     565         
     566    def test_translate_i18n_domain_as_directive_not_attribute(self): 
     567        #"""translate with domain as directive""" 
     568         
     569        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     570            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     571        <i18n:domain name="foo"> 
     572          <p i18n:msg="">FooBar</p> 
     573          <p i18n:msg="">Bar</p> 
     574          <p>Bar</p> 
     575        </i18n:domain> 
     576          <p>Bar</p> 
     577        </html>""") 
     578        translations = DummyTranslations({'Bar': 'Voh'}) 
     579        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'PT_Foo'}) 
     580        translator = Translator(translations) 
     581        setup_i18n(tmpl, translator) 
     582        self.assertEqual("""<html> 
     583          <p>BarFoo</p> 
     584          <p>PT_Foo</p> 
     585          <p>PT_Foo</p> 
     586          <p>Voh</p> 
     587        </html>""", tmpl.generate().render()) 
     588         
     589    def test_translate_i18n_domain_nested_directives(self): 
     590        #"""translate with nested i18n:domain directives""" 
     591         
     592        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     593            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     594          <p i18n:msg="">Bar</p> 
     595          <div i18n:domain="foo"> 
     596            <p i18n:msg="">FooBar</p> 
     597            <p i18n:domain="bar" i18n:msg="">Bar</p>             
     598            <p>Bar</p> 
     599          </div>           
     600          <p>Bar</p> 
     601        </html>""") 
     602        translations = DummyTranslations({'Bar': 'Voh'}) 
     603        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'}) 
     604        translations.add_domain('bar', {'Bar': 'bar_Bar'}) 
     605        translator = Translator(translations) 
     606        setup_i18n(tmpl, translator) 
     607        self.assertEqual("""<html> 
     608          <p>Voh</p> 
     609          <div> 
     610            <p>BarFoo</p> 
     611            <p>bar_Bar</p> 
     612            <p>foo_Bar</p> 
     613          </div> 
     614          <p>Voh</p> 
     615        </html>""", tmpl.generate().render()) 
     616         
     617    def test_translate_i18n_domain_with_empty_nested_domain_directive(self): 
     618        #"""translate with empty nested i18n:domain directive does not use dngettext""" 
     619         
     620        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     621            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     622          <p i18n:msg="">Bar</p> 
     623          <div i18n:domain="foo"> 
     624            <p i18n:msg="">FooBar</p> 
     625            <p i18n:domain="" i18n:msg="">Bar</p>             
     626            <p>Bar</p> 
     627          </div>           
     628          <p>Bar</p> 
     629        </html>""") 
     630        translations = DummyTranslations({'Bar': 'Voh'}) 
     631        translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'}) 
     632        translations.add_domain('bar', {'Bar': 'bar_Bar'}) 
     633        translator = Translator(translations) 
     634        setup_i18n(tmpl, translator) 
    466635        self.assertEqual("""<html> 
    467636          <p>Voh</p> 
     637          <div> 
     638            <p>BarFoo</p> 
     639            <p>Voh</p> 
     640            <p>foo_Bar</p> 
     641          </div> 
     642          <p>Voh</p> 
    468643        </html>""", tmpl.generate().render()) 
    469644 
     645    def test_translate_i18n_choose_as_attribute(self): 
     646        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     647            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     648          <div i18n:choose="one"> 
     649            <p i18n:singular="">FooBar</p> 
     650            <p i18n:plural="">FooBars</p> 
     651          </div> 
     652          <div i18n:choose="two"> 
     653            <p i18n:singular="">FooBar</p> 
     654            <p i18n:plural="">FooBars</p> 
     655          </div> 
     656        </html>""") 
     657        translations = DummyTranslations() 
     658        translator = Translator(translations) 
     659        setup_i18n(tmpl, translator) 
     660        self.assertEqual("""<html> 
     661          <div> 
     662            <p>FooBar</p> 
     663          </div> 
     664          <div> 
     665            <p>FooBars</p> 
     666          </div> 
     667        </html>""", tmpl.generate(one=1, two=2).render()) 
     668         
     669    def test_translate_i18n_choose_as_directive(self): 
     670        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     671            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     672        <i18n:choose numeral="two"> 
     673          <p i18n:singular="">FooBar</p> 
     674          <p i18n:plural="">FooBars</p> 
     675        </i18n:choose> 
     676        <i18n:choose numeral="one"> 
     677          <p i18n:singular="">FooBar</p> 
     678          <p i18n:plural="">FooBars</p> 
     679        </i18n:choose> 
     680        </html>""") 
     681        translations = DummyTranslations() 
     682        translator = Translator(translations) 
     683        setup_i18n(tmpl, translator) 
     684        self.assertEqual("""<html> 
     685          <p>FooBars</p> 
     686          <p>FooBar</p> 
     687        </html>""", tmpl.generate(one=1, two=2).render()) 
     688         
     689    def test_translate_i18n_choose_as_attribute_with_params(self): 
     690        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     691            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     692          <div i18n:choose="two; fname, lname"> 
     693            <p i18n:singular="">Foo $fname $lname</p> 
     694            <p i18n:plural="">Foos $fname $lname</p> 
     695          </div> 
     696        </html>""") 
     697        translations = DummyTranslations({ 
     698            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', 
     699            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', 
     700                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', 
     701                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', 
     702        }) 
     703        translator = Translator(translations) 
     704        setup_i18n(tmpl, translator) 
     705        self.assertEqual("""<html> 
     706          <div> 
     707            <p>Vohs John Doe</p> 
     708          </div> 
     709        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render()) 
     710         
     711    def test_translate_i18n_choose_as_attribute_with_params_and_domain_as_param(self): 
     712        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     713            xmlns:i18n="http://genshi.edgewall.org/i18n" 
     714            i18n:domain="foo"> 
     715          <div i18n:choose="two; fname, lname"> 
     716            <p i18n:singular="">Foo $fname $lname</p> 
     717            <p i18n:plural="">Foos $fname $lname</p> 
     718          </div> 
     719        </html>""") 
     720        translations = DummyTranslations() 
     721        translations.add_domain('foo', { 
     722            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', 
     723            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', 
     724                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', 
     725                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', 
     726        }) 
     727        translator = Translator(translations) 
     728        setup_i18n(tmpl, translator) 
     729        self.assertEqual("""<html> 
     730          <div> 
     731            <p>Vohs John Doe</p> 
     732          </div> 
     733        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render()) 
     734         
     735    def test_translate_i18n_choose_as_directive_with_params(self): 
     736        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     737            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     738        <i18n:choose numeral="two" params="fname, lname"> 
     739          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     740          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     741        </i18n:choose> 
     742        <i18n:choose numeral="one" params="fname, lname"> 
     743          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     744          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     745        </i18n:choose> 
     746        </html>""") 
     747        translations = DummyTranslations({ 
     748            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', 
     749            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', 
     750                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', 
     751                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', 
     752        }) 
     753        translator = Translator(translations) 
     754        setup_i18n(tmpl, translator) 
     755        self.assertEqual("""<html> 
     756          <p>Vohs John Doe</p> 
     757          <p>Voh John Doe</p> 
     758        </html>""", tmpl.generate(one=1, two=2, 
     759                                  fname='John', lname='Doe').render()) 
     760         
     761    def test_translate_i18n_choose_as_directive_with_params_and_domain_as_directive(self): 
     762        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     763            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     764        <i18n:domain name="foo"> 
     765        <i18n:choose numeral="two" params="fname, lname"> 
     766          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     767          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     768        </i18n:choose> 
     769        </i18n:domain> 
     770        <i18n:choose numeral="one" params="fname, lname"> 
     771          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     772          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     773        </i18n:choose> 
     774        </html>""") 
     775        translations = DummyTranslations() 
     776        translations.add_domain('foo', { 
     777            ('Foo %(fname)s %(lname)s', 0): 'Voh %(fname)s %(lname)s', 
     778            ('Foo %(fname)s %(lname)s', 1): 'Vohs %(fname)s %(lname)s', 
     779                 'Foo %(fname)s %(lname)s': 'Voh %(fname)s %(lname)s', 
     780                'Foos %(fname)s %(lname)s': 'Vohs %(fname)s %(lname)s', 
     781        }) 
     782        translator = Translator(translations) 
     783        setup_i18n(tmpl, translator) 
     784        self.assertEqual("""<html> 
     785          <p>Vohs John Doe</p> 
     786          <p>Foo John Doe</p> 
     787        </html>""", tmpl.generate(one=1, two=2, 
     788                                  fname='John', lname='Doe').render()) 
     789 
     790    def test_extract_i18n_choose_as_attribute(self): 
     791        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     792            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     793          <div i18n:choose="one"> 
     794            <p i18n:singular="">FooBar</p> 
     795            <p i18n:plural="">FooBars</p> 
     796          </div> 
     797          <div i18n:choose="two"> 
     798            <p i18n:singular="">FooBar</p> 
     799            <p i18n:plural="">FooBars</p> 
     800          </div> 
     801        </html>""") 
     802        translator = Translator() 
     803        tmpl.add_directives(Translator.NAMESPACE, translator) 
     804        messages = list(translator.extract(tmpl.stream)) 
     805        self.assertEqual(2, len(messages)) 
     806        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0]) 
     807        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1]) 
     808         
     809    def test_extract_i18n_choose_as_directive(self): 
     810        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     811            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     812        <i18n:choose numeral="two"> 
     813          <p i18n:singular="">FooBar</p> 
     814          <p i18n:plural="">FooBars</p> 
     815        </i18n:choose> 
     816        <i18n:choose numeral="one"> 
     817          <p i18n:singular="">FooBar</p> 
     818          <p i18n:plural="">FooBars</p> 
     819        </i18n:choose> 
     820        </html>""") 
     821        translator = Translator() 
     822        tmpl.add_directives(Translator.NAMESPACE, translator) 
     823        messages = list(translator.extract(tmpl.stream)) 
     824        self.assertEqual(2, len(messages)) 
     825        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0]) 
     826        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1]) 
     827         
     828    def test_extract_i18n_choose_as_attribute_with_params(self): 
     829        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     830            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     831          <div i18n:choose="two; fname, lname"> 
     832            <p i18n:singular="">Foo $fname $lname</p> 
     833            <p i18n:plural="">Foos $fname $lname</p> 
     834          </div> 
     835        </html>""") 
     836        translator = Translator() 
     837        tmpl.add_directives(Translator.NAMESPACE, translator) 
     838        messages = list(translator.extract(tmpl.stream)) 
     839        self.assertEqual(1, len(messages)) 
     840        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     841                                          u'Foos %(fname)s %(lname)s'), []), 
     842                         messages[0]) 
     843 
     844    def test_extract_i18n_choose_as_attribute_with_params_and_domain_as_param(self): 
     845        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     846            xmlns:i18n="http://genshi.edgewall.org/i18n" 
     847            i18n:domain="foo"> 
     848          <div i18n:choose="two; fname, lname"> 
     849            <p i18n:singular="">Foo $fname $lname</p> 
     850            <p i18n:plural="">Foos $fname $lname</p> 
     851          </div> 
     852        </html>""") 
     853        translator = Translator() 
     854        tmpl.add_directives(Translator.NAMESPACE, translator) 
     855        messages = list(translator.extract(tmpl.stream)) 
     856        self.assertEqual(1, len(messages)) 
     857        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     858                                          u'Foos %(fname)s %(lname)s'), []), 
     859                         messages[0]) 
     860 
     861    def test_extract_i18n_choose_as_directive_with_params(self): 
     862        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     863            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     864        <i18n:choose numeral="two" params="fname, lname"> 
     865          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     866          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     867        </i18n:choose> 
     868        <i18n:choose numeral="one" params="fname, lname"> 
     869          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     870          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     871        </i18n:choose> 
     872        </html>""") 
     873        translator = Translator() 
     874        tmpl.add_directives(Translator.NAMESPACE, translator) 
     875        messages = list(translator.extract(tmpl.stream)) 
     876        self.assertEqual(2, len(messages)) 
     877        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     878                                          u'Foos %(fname)s %(lname)s'), []), 
     879                         messages[0]) 
     880        self.assertEqual((7, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     881                                          u'Foos %(fname)s %(lname)s'), []), 
     882                         messages[1]) 
     883 
     884    def test_extract_i18n_choose_as_directive_with_params_and_domain_as_directive(self): 
     885        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     886            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     887        <i18n:domain name="foo"> 
     888        <i18n:choose numeral="two" params="fname, lname"> 
     889          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     890          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     891        </i18n:choose> 
     892        </i18n:domain> 
     893        <i18n:choose numeral="one" params="fname, lname"> 
     894          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     895          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     896        </i18n:choose> 
     897        </html>""") 
     898        translator = Translator() 
     899        tmpl.add_directives(Translator.NAMESPACE, translator) 
     900        messages = list(translator.extract(tmpl.stream)) 
     901        self.assertEqual(2, len(messages)) 
     902        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     903                                          u'Foos %(fname)s %(lname)s'), []), 
     904                         messages[0]) 
     905        self.assertEqual((9, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     906                                          u'Foos %(fname)s %(lname)s'), []), 
     907                         messages[1]) 
     908         
     909    def test_extract_i18n_choose_as_attribute_with_params_and_comment(self): 
     910        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     911            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     912          <div i18n:choose="two; fname, lname" i18n:comment="As in Foo Bar"> 
     913            <p i18n:singular="">Foo $fname $lname</p> 
     914            <p i18n:plural="">Foos $fname $lname</p> 
     915          </div> 
     916        </html>""") 
     917        translator = Translator() 
     918        tmpl.add_directives(Translator.NAMESPACE, translator) 
     919        messages = list(translator.extract(tmpl.stream)) 
     920        self.assertEqual(1, len(messages)) 
     921        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     922                                          u'Foos %(fname)s %(lname)s'), 
     923                          [u'As in Foo Bar']), 
     924                         messages[0]) 
     925         
     926    def test_extract_i18n_choose_as_directive_with_params_and_comment(self): 
     927        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 
     928            xmlns:i18n="http://genshi.edgewall.org/i18n"> 
     929        <i18n:choose numeral="two" params="fname, lname" i18n:comment="As in Foo Bar"> 
     930          <p i18n:singular="">Foo ${fname} ${lname}</p> 
     931          <p i18n:plural="">Foos ${fname} ${lname}</p> 
     932        </i18n:choose> 
     933        </html>""") 
     934        translator = Translator() 
     935        tmpl.add_directives(Translator.NAMESPACE, translator) 
     936        messages = list(translator.extract(tmpl.stream)) 
     937        self.assertEqual(1, len(messages)) 
     938        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s', 
     939                                          u'Foos %(fname)s %(lname)s'), 
     940                          [u'As in Foo Bar']), 
     941                         messages[0]) 
     942 
     943    def test_translate_i18n_domain_with_nested_inlcudes(self): 
     944        import os, shutil, tempfile 
     945        from genshi.template.loader import TemplateLoader 
     946        dirname = tempfile.mkdtemp(suffix='genshi_test') 
     947        try: 
     948            for idx in range(7): 
     949                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w') 
     950                try: 
     951                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude" 
     952                                         xmlns:py="http://genshi.edgewall.org/" 
     953                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip=""> 
     954                        <div>Included tmpl$idx</div> 
     955                        <p i18n:msg="idx">Bar $idx</p> 
     956                        <p i18n:domain="bar">Bar</p> 
     957                        <p i18n:msg="idx" i18n:domain="">Bar $idx</p> 
     958                        <p i18n:domain="" i18n:msg="idx">Bar $idx</p> 
     959                        <py:if test="idx &lt; 6"> 
     960                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/> 
     961                        </py:if> 
     962                    </html>""") 
     963                finally: 
     964                    file1.close() 
     965 
     966            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w') 
     967            try: 
     968                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude" 
     969                                     xmlns:py="http://genshi.edgewall.org/" 
     970                                     xmlns:i18n="http://genshi.edgewall.org/i18n" 
     971                                     i18n:domain="foo"> 
     972                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/> 
     973                </html>""") 
     974            finally: 
     975                file2.close() 
     976 
     977            def callback(template): 
     978                translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s'}) 
     979                translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'}) 
     980                translations.add_domain('bar', {'Bar': 'bar_Bar'}) 
     981                translator = Translator(translations) 
     982                setup_i18n(template, translator) 
     983            loader = TemplateLoader([dirname], callback=callback) 
     984            tmpl = loader.load('tmpl10.html') 
     985             
     986            self.assertEqual("""<html> 
     987                        <div>Included tmpl0</div> 
     988                        <p>foo_Bar 0</p> 
     989                        <p>bar_Bar</p> 
     990                        <p>Voh 0</p> 
     991                        <p>Voh 0</p> 
     992                        <div>Included tmpl1</div> 
     993                        <p>foo_Bar 1</p> 
     994                        <p>bar_Bar</p> 
     995                        <p>Voh 1</p> 
     996                        <p>Voh 1</p> 
     997                        <div>Included tmpl2</div> 
     998                        <p>foo_Bar 2</p> 
     999                        <p>bar_Bar</p> 
     1000                        <p>Voh 2</p> 
     1001                        <p>Voh 2</p> 
     1002                        <div>Included tmpl3</div> 
     1003                        <p>foo_Bar 3</p> 
     1004                        <p>bar_Bar</p> 
     1005                        <p>Voh 3</p> 
     1006                        <p>Voh 3</p> 
     1007                        <div>Included tmpl4</div> 
     1008                        <p>foo_Bar 4</p> 
     1009                        <p>bar_Bar</p> 
     1010                        <p>Voh 4</p> 
     1011                        <p>Voh 4</p> 
     1012                        <div>Included tmpl5</div> 
     1013                        <p>foo_Bar 5</p> 
     1014                        <p>bar_Bar</p> 
     1015                        <p>Voh 5</p> 
     1016                        <p>Voh 5</p> 
     1017                        <div>Included tmpl6</div> 
     1018                        <p>foo_Bar 6</p> 
     1019                        <p>bar_Bar</p> 
     1020                        <p>Voh 6</p> 
     1021                        <p>Voh 6</p> 
     1022                </html>""", tmpl.generate(idx=-1).render()) 
     1023        finally: 
     1024            shutil.rmtree(dirname) 
     1025             
     1026    def test_translate_i18n_domain_with_nested_inlcudes_with_translatable_attrs(self): 
     1027        import os, shutil, tempfile 
     1028        from genshi.template.loader import TemplateLoader 
     1029        dirname = tempfile.mkdtemp(suffix='genshi_test') 
     1030        try: 
     1031            for idx in range(4): 
     1032                file1 = open(os.path.join(dirname, 'tmpl%d.html' % idx), 'w') 
     1033                try: 
     1034                    file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude" 
     1035                                         xmlns:py="http://genshi.edgewall.org/" 
     1036                                         xmlns:i18n="http://genshi.edgewall.org/i18n" py:strip=""> 
     1037                        <div>Included tmpl$idx</div> 
     1038                        <p title="${dg('foo', 'Bar %(idx)s') % dict(idx=idx)}" i18n:msg="idx">Bar $idx</p> 
     1039                        <p title="Bar" i18n:domain="bar">Bar</p> 
     1040                        <p title="Bar" i18n:msg="idx" i18n:domain="">Bar $idx</p> 
     1041                        <p i18n:domain="" i18n:msg="idx" title="Bar">Bar $idx</p> 
     1042                        <py:if test="idx &lt; 3"> 
     1043                        <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/> 
     1044                        </py:if> 
     1045                    </html>""") 
     1046                finally: 
     1047                    file1.close() 
     1048 
     1049            file2 = open(os.path.join(dirname, 'tmpl10.html'), 'w') 
     1050            try: 
     1051                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude" 
     1052                                     xmlns:py="http://genshi.edgewall.org/" 
     1053                                     xmlns:i18n="http://genshi.edgewall.org/i18n" 
     1054                                     i18n:domain="foo"> 
     1055                  <xi:include href="tmpl${idx}.html" py:with="idx = idx+1"/> 
     1056                </html>""") 
     1057            finally: 
     1058                file2.close() 
     1059                 
     1060            translations = DummyTranslations({'Bar %(idx)s': 'Voh %(idx)s', 
     1061                                              'Bar': 'Voh'}) 
     1062            translations.add_domain('foo', {'Bar %(idx)s': 'foo_Bar %(idx)s'}) 
     1063            translations.add_domain('bar', {'Bar': 'bar_Bar'}) 
     1064            translator = Translator(translations) 
     1065             
     1066            def callback(template):                 
     1067                setup_i18n(template, translator) 
     1068            loader = TemplateLoader([dirname], callback=callback) 
     1069            tmpl = loader.load('tmpl10.html') 
     1070             
     1071            self.assertEqual("""<html> 
     1072                        <div>Included tmpl0</div> 
     1073                        <p title="foo_Bar 0">foo_Bar 0</p> 
     1074                        <p title="bar_Bar">bar_Bar</p> 
     1075                        <p title="Voh">Voh 0</p> 
     1076                        <p title="Voh">Voh 0</p> 
     1077                        <div>Included tmpl1</div> 
     1078                        <p title="foo_Bar 1">foo_Bar 1</p> 
     1079                        <p title="bar_Bar">bar_Bar</p> 
     1080                        <p title="Voh">Voh 1</p> 
     1081                        <p title="Voh">Voh 1</p> 
     1082                        <div>Included tmpl2</div> 
     1083                        <p title="foo_Bar 2">foo_Bar 2</p> 
     1084                        <p title="bar_Bar">bar_Bar</p> 
     1085                        <p title="Voh">Voh 2</p> 
     1086                        <p title="Voh">Voh 2</p> 
     1087                        <div>Included tmpl3</div> 
     1088                        <p title="foo_Bar 3">foo_Bar 3</p> 
     1089                        <p title="bar_Bar">bar_Bar</p> 
     1090                        <p title="Voh">Voh 3</p> 
     1091                        <p title="Voh">Voh 3</p> 
     1092                </html>""", tmpl.generate(idx=-1, 
     1093                                          dg=translations.dugettext).render()) 
     1094        finally: 
     1095            shutil.rmtree(dirname) 
     1096 
    4701097 
    4711098class ExtractTestCase(unittest.TestCase): 
    4721099 
  • genshi/filters/__init__.py

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

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