Ticket #129: i18n_directives_full.patch
| File i18n_directives_full.patch, 74.8 KB (added by palgarvio, 15 years ago) |
|---|
-
genshi/filters/tests/i18n.py
13 13 14 14 from datetime import datetime 15 15 import doctest 16 from gettext import NullTranslations 16 from gettext import NullTranslations, c2py 17 17 from StringIO import StringIO 18 18 import unittest 19 19 20 20 from genshi.core import Attrs 21 21 from genshi.template import MarkupTemplate 22 from genshi.filters.i18n import Translator, extract 22 from genshi.filters.i18n import Translator, extract, setup_i18n 23 23 from genshi.input import HTML 24 24 25 25 26 26 class DummyTranslations(NullTranslations): 27 _domains = {} 27 28 28 def __init__(self, catalog ):29 def __init__(self, catalog=()): 29 30 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) 31 41 32 42 def ugettext(self, message): 33 43 missing = object() … … 37 47 return self._fallback.ugettext(message) 38 48 return unicode(message) 39 49 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) 40 67 41 68 42 69 class TranslatorTestCase(unittest.TestCase): … … 162 189 </p> 163 190 </html>""") 164 191 translator = Translator() 192 tmpl.add_directives(Translator.NAMESPACE, translator) 165 193 messages = list(translator.extract(tmpl.stream)) 166 194 self.assertEqual(1, len(messages)) 167 195 self.assertEqual('Please see [1:Help] for details.', messages[0][2]) … … 175 203 </html>""") 176 204 gettext = lambda s: u"Für Details siehe bitte [1:Hilfe]." 177 205 translator = Translator(gettext) 178 tmpl.filters.insert(0, translator) 179 tmpl.add_directives(Translator.NAMESPACE, translator) 206 setup_i18n(tmpl, translator) 180 207 self.assertEqual("""<html> 181 208 <p>Für Details siehe bitte <a href="help.html">Hilfe</a>.</p> 182 209 </html>""", tmpl.generate().render()) … … 189 216 </p> 190 217 </html>""") 191 218 translator = Translator() 219 tmpl.add_directives(Translator.NAMESPACE, translator) 192 220 messages = list(translator.extract(tmpl.stream)) 193 221 self.assertEqual(1, len(messages)) 194 222 self.assertEqual('Please see [1:[2:Help] page] for details.', … … 203 231 </html>""") 204 232 gettext = lambda s: u"Für Details siehe bitte [1:[2:Hilfeseite]]." 205 233 translator = Translator(gettext) 206 tmpl.filters.insert(0, translator) 207 tmpl.add_directives(Translator.NAMESPACE, translator) 234 setup_i18n(tmpl, translator) 208 235 self.assertEqual("""<html> 209 236 <p>Für Details siehe bitte <a href="help.html"><em>Hilfeseite</em></a>.</p> 210 237 </html>""", tmpl.generate().render()) … … 217 244 </p> 218 245 </html>""") 219 246 translator = Translator() 247 tmpl.add_directives(Translator.NAMESPACE, translator) 220 248 messages = list(translator.extract(tmpl.stream)) 221 249 self.assertEqual(1, len(messages)) 222 250 self.assertEqual('Show me [1:] entries per page.', messages[0][2]) … … 230 258 </html>""") 231 259 gettext = lambda s: u"[1:] Einträge pro Seite anzeigen." 232 260 translator = Translator(gettext) 233 tmpl.filters.insert(0, translator) 234 tmpl.add_directives(Translator.NAMESPACE, translator) 261 setup_i18n(tmpl, translator) 235 262 self.assertEqual("""<html> 236 263 <p><input type="text" name="num"/> Einträge pro Seite anzeigen.</p> 237 264 </html>""", tmpl.generate().render()) … … 244 271 </p> 245 272 </html>""") 246 273 translator = Translator() 274 tmpl.add_directives(Translator.NAMESPACE, translator) 247 275 messages = list(translator.extract(tmpl.stream)) 248 276 self.assertEqual(1, len(messages)) 249 277 self.assertEqual('Please see [1:Help] for [2:details].', messages[0][2]) … … 257 285 </html>""") 258 286 gettext = lambda s: u"Für [2:Details] siehe bitte [1:Hilfe]." 259 287 translator = Translator(gettext) 260 tmpl.filters.insert(0, translator) 261 tmpl.add_directives(Translator.NAMESPACE, translator) 288 setup_i18n(tmpl, translator) 262 289 self.assertEqual("""<html> 263 290 <p>Für <em>Details</em> siehe bitte <a href="help.html">Hilfe</a>.</p> 264 291 </html>""", tmpl.generate().render()) … … 271 298 </p> 272 299 </html>""") 273 300 translator = Translator() 301 tmpl.add_directives(Translator.NAMESPACE, translator) 274 302 messages = list(translator.extract(tmpl.stream)) 275 303 self.assertEqual(1, len(messages)) 276 304 self.assertEqual('Show me [1:] entries per page, starting at page [2:].', … … 285 313 </html>""") 286 314 gettext = lambda s: u"[1:] Einträge pro Seite, beginnend auf Seite [2:]." 287 315 translator = Translator(gettext) 288 tmpl.filters.insert(0, translator) 289 tmpl.add_directives(Translator.NAMESPACE, translator) 316 setup_i18n(tmpl, translator) 290 317 self.assertEqual("""<html> 291 318 <p><input type="text" name="num"/> Eintr\xc3\xa4ge pro Seite, beginnend auf Seite <input type="text" name="num"/>.</p> 292 319 </html>""", tmpl.generate().render()) … … 299 326 </p> 300 327 </html>""") 301 328 translator = Translator() 329 tmpl.add_directives(Translator.NAMESPACE, translator) 302 330 messages = list(translator.extract(tmpl.stream)) 303 331 self.assertEqual(1, len(messages)) 304 332 self.assertEqual('Hello, %(name)s!', messages[0][2]) … … 312 340 </html>""") 313 341 gettext = lambda s: u"Hallo, %(name)s!" 314 342 translator = Translator(gettext) 315 tmpl.filters.insert(0, translator) 316 tmpl.add_directives(Translator.NAMESPACE, translator) 343 setup_i18n(tmpl, translator) 317 344 self.assertEqual("""<html> 318 345 <p>Hallo, Jim!</p> 319 346 </html>""", tmpl.generate(user=dict(name='Jim')).render()) … … 327 354 </html>""") 328 355 gettext = lambda s: u"%(name)s, sei gegrüßt!" 329 356 translator = Translator(gettext) 330 tmpl.filters.insert(0, translator) 331 tmpl.add_directives(Translator.NAMESPACE, translator) 357 setup_i18n(tmpl, translator) 332 358 self.assertEqual("""<html> 333 359 <p>Jim, sei gegrüßt!</p> 334 360 </html>""", tmpl.generate(user=dict(name='Jim')).render()) … … 342 368 </html>""") 343 369 gettext = lambda s: u"Sei gegrüßt, [1:Alter]!" 344 370 translator = Translator(gettext) 345 tmpl.filters.insert(0, translator) 346 tmpl.add_directives(Translator.NAMESPACE, translator) 371 setup_i18n(tmpl, translator) 347 372 self.assertEqual("""<html> 348 373 <p>Sei gegrüßt, <a href="#42">Alter</a>!</p> 349 374 </html>""", tmpl.generate(anchor='42').render()) … … 356 381 </p> 357 382 </html>""") 358 383 translator = Translator() 384 tmpl.add_directives(Translator.NAMESPACE, translator) 359 385 messages = list(translator.extract(tmpl.stream)) 360 386 self.assertEqual(1, len(messages)) 361 387 self.assertEqual('Posted by %(name)s at %(time)s', messages[0][2]) … … 369 395 </html>""") 370 396 gettext = lambda s: u"%(name)s schrieb dies um %(time)s" 371 397 translator = Translator(gettext) 372 tmpl.filters.insert(0, translator) 373 tmpl.add_directives(Translator.NAMESPACE, translator) 398 setup_i18n(tmpl, translator) 374 399 entry = { 375 400 'author': 'Jim', 376 401 'time': datetime(2008, 4, 1, 14, 30) … … 387 412 </p> 388 413 </html>""") 389 414 translator = Translator() 415 tmpl.add_directives(Translator.NAMESPACE, translator) 390 416 messages = list(translator.extract(tmpl.stream)) 391 417 self.assertEqual(1, len(messages)) 392 418 self.assertEqual('Show me [1:] entries per page.', messages[0][2]) 393 419 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()) 407 433 408 434 def test_extract_i18n_msg_with_comment(self): 409 435 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 410 436 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"> 411 446 <p i18n:msg="" i18n:comment="As in foo bar">Foo</p> 412 447 </html>""") 413 448 translator = Translator() 449 tmpl.add_directives(Translator.NAMESPACE, translator) 414 450 messages = list(translator.extract(tmpl.stream)) 415 451 self.assertEqual(1, len(messages)) 416 452 self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0]) … … 422 458 </html>""") 423 459 gettext = lambda s: u"Voh" 424 460 translator = Translator(gettext) 425 tmpl.filters.insert(0, translator) 426 tmpl.add_directives(Translator.NAMESPACE, translator) 461 setup_i18n(tmpl, translator) 427 462 self.assertEqual("""<html> 428 463 <p>Voh</p> 429 464 </html>""", tmpl.generate().render()) … … 461 496 <p i18n:msg="" i18n:comment="As in foo bar">Foo</p> 462 497 </html>""") 463 498 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) 466 635 self.assertEqual("""<html> 467 636 <p>Voh</p> 637 <div> 638 <p>BarFoo</p> 639 <p>Voh</p> 640 <p>foo_Bar</p> 641 </div> 642 <p>Voh</p> 468 643 </html>""", tmpl.generate().render()) 469 644 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 < 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 < 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 470 1097 471 1098 class ExtractTestCase(unittest.TestCase): 472 1099 -
genshi/filters/__init__.py
14 14 """Implementation of a number of stream filters.""" 15 15 16 16 from genshi.filters.html import HTMLFormFiller, HTMLSanitizer 17 from genshi.filters.i18n import Translator 17 from genshi.filters.i18n import Translator, setup_i18n 18 18 from genshi.filters.transform import Transformer 19 19 20 20 __docformat__ = 'restructuredtext en' -
genshi/filters/i18n.py
11 11 # individuals. For the exact contribution history, see the revision 12 12 # history and logs, available at http://genshi.edgewall.org/log/. 13 13 14 """Utilities for internationalization and localization of templates. 14 """Directives and utilities for internationalization and localization of 15 templates. 15 16 16 17 :since: version 0.4 18 :note: Directives support added since version 0.6 17 19 """ 18 20 19 21 from compiler import ast 20 22 from gettext import NullTranslations 23 import os 21 24 import re 22 25 from types import FunctionType 23 26 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.core import Attrs, Namespace, QName, START, END, TEXT, \ 28 XML_NAMESPACE, _ensure 29 from genshi.template.base import Context, DirectiveFactory, EXPR, SUB, \ 30 _apply_directives 27 31 from genshi.template.directives import Directive 28 32 from genshi.template.markup import MarkupTemplate, EXEC 29 33 … … 32 36 33 37 I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n') 34 38 39 class DirectiveExtract(object): 40 """Simple interface for directives to support messages extraction""" 35 41 36 class CommentDirective(Directive): 42 def extract(self, stream, ctxt): 43 raise NotImplementedError 37 44 38 __slots__ = [] 45 class 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 """ 39 63 40 @classmethod 41 def attach(cls, template, stream, value, namespaces, pos): 42 return None, stream 64 __slots__ = ['comment'] 43 65 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 44 70 45 class MsgDirective(Directive): 71 class 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 """ 46 131 47 132 __slots__ = ['params'] 48 133 49 134 def __init__(self, value, template, hints=None, namespaces=None, 50 135 lineno=-1, offset=-1): 51 136 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) 53 145 54 146 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 55 154 msgbuf = MessageBuffer(self.params) 56 155 57 156 stream = iter(stream) 58 157 yield stream.next() # the outer start tag 59 158 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 63 184 64 gettext = ctxt.get('_i18n.gettext')65 185 for event in msgbuf.translate(gettext(msgbuf.format())): 66 186 yield event 67 187 68 188 yield previous # the outer end tag 69 189 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 210 class 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 244 class SingularDirective(InnerChooseDirective): 245 """Implementation of the ``i18n:singular`` directive to be used with the 246 ``i18n:choose`` directive.""" 247 248 249 class PluralDirective(InnerChooseDirective): 250 """Implementation of the ``i18n:plural`` directive to be used with the 251 ``i18n:choose`` directive.""" 252 253 254 class 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 443 class 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 70 527 71 528 class Translator(DirectiveFactory): 72 529 """Can extract and translate localizable strings from markup streams and 73 530 templates. 74 531 75 For example, assume the follow ng template:532 For example, assume the following template: 76 533 77 534 >>> from genshi.template import MarkupTemplate 78 >>> 535 >>> 79 536 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 80 537 ... <head> 81 538 ... <title>Example</title> … … 94 551 ... 'Example': 'Beispiel', 95 552 ... 'Hello, %(name)s': 'Hallo, %(name)s' 96 553 ... }[string] 97 >>> 554 >>> 98 555 >>> translator = Translator(pseudo_gettext) 99 556 100 557 Next, the translator needs to be prepended to any already defined filters … … 115 572 <p>Hallo, Hans</p> 116 573 </body> 117 574 </html> 118 575 119 576 Note that elements defining ``xml:lang`` attributes that do not contain 120 577 variable expressions are ignored by this filter. That can be used to 121 578 exclude specific parts of a template from being extracted and translated. 122 579 """ 123 580 124 581 directives = [ 582 ('domain', DomainDirective), 125 583 ('comment', CommentDirective), 126 ('msg', MsgDirective) 584 ('msg', MsgDirective), 585 ('choose', ChooseDirective), 586 ('singular', SingularDirective), 587 ('plural', PluralDirective) 127 588 ] 128 589 129 590 IGNORE_TAGS = frozenset([ 130 591 QName('script'), QName('http://www.w3.org/1999/xhtml}script'), 131 592 QName('style'), QName('http://www.w3.org/1999/xhtml}style') 132 593 ]) 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 ]) 135 597 NAMESPACE = I18N_NAMESPACE 136 598 137 599 def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS, … … 145 607 :param extract_text: whether the content of text nodes should be 146 608 extracted, or only text in explicit ``gettext`` 147 609 function calls 148 610 149 611 :note: Changed in 0.6: the `translate` parameter can now be either 150 612 a ``gettext``-style function, or an object compatible with the 151 613 ``NullTransalations`` or ``GNUTranslations`` interface … … 177 639 178 640 if type(self.translate) is FunctionType: 179 641 gettext = self.translate 642 if ctxt: 643 ctxt['_i18n.gettext'] = gettext 180 644 else: 181 645 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 184 663 185 664 extract_text = self.extract_text 186 665 if not extract_text: 187 666 search_text = False 188 667 668 if ctxt and ctxt.get('_i18n.domain'): 669 old_gettext = gettext 670 gettext = lambda x: dgettext(ctxt.get('_i18n.domain'), x) 671 189 672 for kind, data, pos in stream: 190 673 191 674 # skip chunks that should not be localized … … 208 691 209 692 new_attrs = [] 210 693 changed = False 694 211 695 for name, value in attrs: 212 696 newval = value 213 697 if extract_text and isinstance(value, basestring): 214 698 if name in include_attrs: 215 699 newval = gettext(value) 216 700 else: 217 newval = list( self(_ensure(value), ctxt,218 se arch_text=False)701 newval = list( 702 self(_ensure(value), ctxt, search_text=False) 219 703 ) 220 704 if newval != value: 221 705 value = newval … … 234 718 235 719 elif kind is SUB: 236 720 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 238 729 # 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]) 241 733 substream = list(self(substream, ctxt, 242 search_text=not is_ msg))734 search_text=not is_i18n_directive)) 243 735 yield kind, (directives, substream), pos 244 736 737 if current_domain: 738 ctxt.pop() 245 739 else: 246 740 yield kind, data, pos 247 741 … … 249 743 'ugettext', 'ungettext') 250 744 251 745 def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS, 252 search_text=True, msgbuf=None ):746 search_text=True, msgbuf=None, ctxt=Context()): 253 747 """Extract localizable strings from the given template stream. 254 748 255 749 For every string found, this function yields a ``(lineno, function, … … 265 759 from ``i18n:comment`` attributes found in the markup 266 760 267 761 >>> from genshi.template import MarkupTemplate 268 >>> 762 >>> 269 763 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 270 764 ... <head> 271 765 ... <title>Example</title> … … 276 770 ... <p>${ngettext("You have %d item", "You have %d items", num)}</p> 277 771 ... </body> 278 772 ... </html>''', filename='example.html') 279 >>> 773 >>> 280 774 >>> for line, func, msg, comments in Translator().extract(tmpl.stream): 281 775 ... print "%d, %r, %r" % (line, func, msg) 282 776 3, None, u'Example' … … 291 785 functions 292 786 :param search_text: whether the content of text nodes should be 293 787 extracted (used internally) 788 :param ctxt: the current extraction context (used internaly) 294 789 295 790 :note: Changed in 0.4.1: For a function with multiple string arguments 296 791 (such as ``ngettext``), a single item with a tuple of strings is 297 792 yielded, instead an item for each string argument. 298 793 :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. 300 797 """ 301 798 if not self.extract_text: 302 799 search_text = False 303 800 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'] 306 805 xml_lang = XML_NAMESPACE['lang'] 307 806 308 807 for kind, data, pos in stream: 309 310 808 if skip: 311 809 if kind is START: 312 810 skip += 1 … … 335 833 336 834 if msgbuf: 337 835 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 # ) 346 847 347 848 elif not skip and search_text and kind is TEXT: 348 849 if not msgbuf: 349 850 text = data.strip() 350 851 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')]) 352 854 else: 353 855 msgbuf.append(kind, data, pos) 354 856 … … 356 858 msgbuf.append(kind, data, pos) 357 859 if not msgbuf.depth: 358 860 yield msgbuf.lineno, None, msgbuf.format(), \ 359 filter(None, [msgbuf.comment])861 filter(None, [msgbuf.comment]) 360 862 msgbuf = None 361 863 362 864 elif kind is EXPR or kind is EXEC: … … 367 869 yield pos[1], funcname, strings, [] 368 870 369 871 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 376 873 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() 377 905 378 906 class MessageBuffer(object): 379 907 """Helper class for managing internationalized mixed content. … … 408 936 :param data: the event data 409 937 :param pos: the position of the event in the source 410 938 """ 939 if kind is SUB: 940 # py:attrs for example 941 for skind, sdata, spos in data[1]: 942 self.append(skind, sdata, spos) 411 943 if kind is TEXT: 412 944 self.string.append(data) 413 945 self.events.setdefault(self.stack[-1], []).append(None) … … 464 996 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')): 465 997 """Parse a translated message using Genshi mixed content message 466 998 formatting. 467 999 468 1000 >>> parse_msg("See [1:Help].") 469 1001 [(0, 'See '), (1, 'Help'), (0, '.')] 470 1002 471 1003 >>> parse_msg("See [1:our [2:Help] page] for details.") 472 1004 [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')] 473 1005 474 1006 >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].") 475 1007 [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')] 476 1008 477 1009 >>> parse_msg("[1:] Bilder pro Seite anzeigen.") 478 1010 [(1, ''), (0, ' Bilder pro Seite anzeigen.')] 479 1011 480 1012 :param string: the translated message string 481 1013 :return: a list of ``(order, string)`` tuples 482 1014 :rtype: `list` … … 514 1046 >>> expr = Expression('_("Hello")') 515 1047 >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) 516 1048 [('_', u'Hello')] 517 1049 518 1050 >>> expr = Expression('ngettext("You have %(num)s item", ' 519 1051 ... '"You have %(num)s items", num)') 520 1052 >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) … … 584 1116 585 1117 tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None), 586 1118 encoding=encoding) 1119 587 1120 translator = Translator(None, ignore_tags, include_attrs, extract_text) 1121 if hasattr(tmpl, 'add_directives'): 1122 tmpl.add_directives(Translator.NAMESPACE, translator) 588 1123 for message in translator.extract(tmpl.stream, gettext_functions=keywords): 589 1124 yield message 1125 1126 def 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)
