Ticket #580: msgctxt.3.patch
| File msgctxt.3.patch, 43.2 KB (added by hodgestar, 10 years ago) |
|---|
-
doc/i18n.txt
9 9 localizable strings from templates, as well as a template filter and special 10 10 directives that can apply translations to templates as they get rendered. 11 11 12 This support is based on `gettext`_ message catalogs and the `gettext Python 12 This support is based on `gettext`_ message catalogs and the `gettext Python 13 13 module`_. The extraction process can be used from the API level, or through 14 14 the front-ends implemented by the `Babel`_ project, for which Genshi provides 15 15 a plugin. … … 39 39 However, this approach results in significant “character noise” in templates, 40 40 making them harder to read and preview. 41 41 42 The ``genshi.filters.Translator`` filter allows you to get rid of the 42 The ``genshi.filters.Translator`` filter allows you to get rid of the 43 43 explicit `gettext`_ function calls, so you can (often) just continue to write: 44 44 45 45 .. code-block:: genshi … … 54 54 corresponding ``gettext`` function in embedded Python expressions. 55 55 56 56 You can control which tags should be ignored by this process; for example, it 57 doesn't really make sense to translate the content of the HTML 57 doesn't really make sense to translate the content of the HTML 58 58 ``<script></script>`` element. Both ``<script>`` and ``<style>`` are excluded 59 59 by default. 60 60 61 Attribute values can also be automatically translated. The default is to 61 Attribute values can also be automatically translated. The default is to 62 62 consider the attributes ``abbr``, ``alt``, ``label``, ``prompt``, ``standby``, 63 63 ``summary``, and ``title``, which is a list that makes sense for HTML 64 64 documents. Of course, you can tell the translator to use a different set of … … 77 77 <p xml:lang="en">Hello, world!</p> 78 78 79 79 On the other hand, if the value of the ``xml:lang`` attribute contains a Python 80 expression, the element contents and attributes are still considered for 80 expression, the element contents and attributes are still considered for 81 81 automatic translation: 82 82 83 83 .. code-block:: genshi … … 337 337 </div> 338 338 339 339 340 ``i18n.ctxt`` 341 ------------- 342 343 Sometimes a source string can have two different meanings. Without resorting to 344 splitting these two occurrences into different domains, gettext provides a 345 means to specify a *context* for each translatable string. For instance, the 346 word "volunteer" can either mean the noun, one who volunteers, or the verb, 347 to volunteer. 348 349 The ``i18n:ctxt`` directive allows you to mark a scope with a particular 350 context. Here is a rather contrived example: 351 352 .. code-block:: genshi 353 354 <p>A <span i18n:ctxt="noun">volunteer</span> can really help their community. 355 Why don't you <span i18n:ctxt="verb">volunteer</span> some time today? 356 </p> 357 358 340 359 Extraction 341 360 ========== 342 361 343 362 The ``Translator`` class provides a class method called ``extract``, which is 344 a generator yielding all localizable strings found in a template or markup 363 a generator yielding all localizable strings found in a template or markup 345 364 stream. This includes both literal strings in text nodes and attribute values, 346 365 as well as strings in ``gettext()`` calls in embedded Python code. See the API 347 366 documentation for details on how to use this method directly. … … 351 370 ----------------- 352 371 353 372 This functionality is integrated with the message extraction framework provided 354 by the `Babel`_ project. Babel provides a command-line interface as well as 355 commands that can be used from ``setup.py`` scripts using `Setuptools`_ or 373 by the `Babel`_ project. Babel provides a command-line interface as well as 374 commands that can be used from ``setup.py`` scripts using `Setuptools`_ or 356 375 `Distutils`_. 357 376 358 377 .. _`setuptools`: http://peak.telecommunity.com/DevCenter/setuptools 359 378 .. _`distutils`: http://docs.python.org/dist/dist.html 360 379 361 The first thing you need to do to make Babel extract messages from Genshi 380 The first thing you need to do to make Babel extract messages from Genshi 362 381 templates is to let Babel know which files are Genshi templates. This is done 363 382 using a “mapping configuration”, which can be stored in a configuration file, 364 383 or specified directly in your ``setup.py``. … … 407 426 408 427 ``include_attrs`` 409 428 ----------------- 410 Comma-separated list of attribute names that should be considered to have 429 Comma-separated list of attribute names that should be considered to have 411 430 localizable values. Only used for markup templates. 412 431 413 432 ``ignore_tags`` 414 433 --------------- 415 Comma-separated list of tag names that should be ignored. Only used for markup 434 Comma-separated list of tag names that should be ignored. Only used for markup 416 435 templates. 417 436 418 437 ``extract_text`` 419 438 ---------------- 420 439 Whether text outside explicit ``gettext`` function calls should be extracted. 421 440 By default, any text nodes not inside ignored tags, and values of attribute in 422 the ``include_attrs`` list are extracted. If this option is disabled, only 441 the ``include_attrs`` list are extracted. If this option is disabled, only 423 442 strings in ``gettext`` function calls are extracted. 424 443 425 444 .. note:: If you disable this option, and do not make use of the … … 446 465 447 466 from genshi.filters import Translator 448 467 from genshi.template import MarkupTemplate 449 468 450 469 template = MarkupTemplate("...") 451 470 template.filters.insert(0, Translator(translations.ugettext)) 452 471 … … 457 476 458 477 from genshi.filters import Translator 459 478 from genshi.template import MarkupTemplate 460 479 461 480 template = MarkupTemplate("...") 462 481 translator = Translator(translations.ugettext) 463 482 translator.setup(template) … … 473 492 Related Considerations 474 493 ====================== 475 494 476 If you intend to produce an application that is fully prepared for an 495 If you intend to produce an application that is fully prepared for an 477 496 international audience, there are a couple of other things to keep in mind: 478 497 479 498 ------- … … 482 501 483 502 Use ``unicode`` internally, not encoded bytestrings. Only encode/decode where 484 503 data enters or exits the system. This means that your code works with characters 485 and not just with bytes, which is an important distinction for example when 504 and not just with bytes, which is an important distinction for example when 486 505 calculating the length of a piece of text. When you need to decode/encode, it's 487 506 probably a good idea to use UTF-8. 488 507 … … 490 509 Date and Time 491 510 ------------- 492 511 493 If your application uses datetime information that should be displayed to users 494 in different timezones, you should try to work with UTC (universal time) 495 internally. Do the conversion from and to "local time" when the data enters or 496 exits the system. Make use the Python `datetime`_ module and the third-party 512 If your application uses datetime information that should be displayed to users 513 in different timezones, you should try to work with UTC (universal time) 514 internally. Do the conversion from and to "local time" when the data enters or 515 exits the system. Make use the Python `datetime`_ module and the third-party 497 516 `pytz`_ package. 498 517 499 518 -------------------------- 500 519 Formatting and Locale Data 501 520 -------------------------- 502 521 503 Make sure you check out the functionality provided by the `Babel`_ project for 522 Make sure you check out the functionality provided by the `Babel`_ project for 504 523 things like number and date formatting, locale display strings, etc. 505 524 506 525 .. _`datetime`: http://docs.python.org/lib/module-datetime.html -
genshi/filters/i18n.py
22 22 any 23 23 except NameError: 24 24 from genshi.util import any 25 from functools import partial 25 26 from gettext import NullTranslations 26 27 import os 27 28 import re 28 29 from types import FunctionType 29 30 30 from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \ 31 XML_NAMESPACE, _ensure, StreamEventKind 31 from genshi.core import ( 32 Attrs, Namespace, QName, START, END, TEXT, 33 XML_NAMESPACE, _ensure, StreamEventKind) 32 34 from genshi.template.eval import _ast 33 35 from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives 34 36 from genshi.template.directives import Directive, StripDirective … … 60 62 """Simple interface for directives to support messages extraction.""" 61 63 62 64 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 63 search_text=True, comment_stack=None ):65 search_text=True, comment_stack=None, context_stack=None): 64 66 raise NotImplementedError 65 67 66 68 69 contexted = { 70 None: 'pgettext', 71 'gettext': 'pgettext', 72 'ngettext': 'pngettext', 73 'dgettext': 'dpgettext', 74 'dngettext': 'dnpgettext' 75 } 76 77 78 def contextify(line, func, msg, comment, context): 79 if context: 80 context = context[0] 81 func = contexted.get(func) 82 if func is None: 83 raise Exception("failure, bogus extraction method") 84 if isinstance(msg, tuple): 85 msg = (context, tuple[0], tuple[1]) 86 else: 87 msg = (context, msg) 88 return line, func, msg, comment 89 90 67 91 class CommentDirective(I18NDirective): 68 92 """Implementation of the ``i18n:comment`` template directive which adds 69 93 translation comments. 70 94 71 95 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 72 96 ... <p i18n:comment="As in Foo Bar">Foo</p> 73 97 ... </html>''') … … 87 111 class MsgDirective(ExtractableI18NDirective): 88 112 r"""Implementation of the ``i18n:msg`` directive which marks inner content 89 113 as translatable. Consider the following examples: 90 114 91 115 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 92 116 ... <div i18n:msg=""> 93 117 ... <p>Foo</p> … … 95 119 ... </div> 96 120 ... <p i18n:msg="">Foo <em>bar</em>!</p> 97 121 ... </html>''') 98 122 99 123 >>> translator = Translator() 100 124 >>> translator.setup(tmpl) 101 125 >>> list(translator.extract(tmpl.stream)) … … 155 179 156 180 def __call__(self, stream, directives, ctxt, **vars): 157 181 gettext = ctxt.get('_i18n.gettext') 158 if ctxt.get('_i18n.domain'): 182 if ctxt.get('_i18n.domain') and ctxt.get('_i18n.context'): 183 dpgettext = ctxt.get('_i18n.dpgettext') 184 assert hasattr(dpgettext, '__call__'), \ 185 'No domain/context gettext function passed' 186 gettext = lambda msg: dpgettext(ctxt.get('_i18n.domain'), 187 ctxt.get('_i18n.context'), 188 msg) 189 elif ctxt.get('_i18n.domain'): 159 190 dgettext = ctxt.get('_i18n.dgettext') 160 191 assert hasattr(dgettext, '__call__'), \ 161 192 'No domain gettext function passed' 162 193 gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg) 194 elif ctxt.get('_i18n.context'): 195 pgettext = ctxt.get('_i18n.pgettext') 196 assert hasattr(pgettext, '__call__'), \ 197 'No context gettext function passed' 198 gettext = lambda msg: pgettext(ctxt.get('_i18n.context'), msg) 163 199 164 200 def _generate(): 165 201 msgbuf = MessageBuffer(self) … … 183 219 return _apply_directives(_generate(), directives, ctxt, vars) 184 220 185 221 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 186 search_text=True, comment_stack=None ):222 search_text=True, comment_stack=None, context_stack=None): 187 223 msgbuf = MessageBuffer(self) 188 224 strip = False 189 225 … … 207 243 if not strip: 208 244 msgbuf.append(*previous) 209 245 210 yield self.lineno, None, msgbuf.format(), comment_stack[-1:] 246 yield contextify( 247 self.lineno, None, msgbuf.format(), comment_stack[-1:], context_stack[-1:]) 211 248 212 249 213 250 class ChooseBranchDirective(I18NDirective): … … 244 281 ctxt['_i18n.choose.%s' % self.tagname] = msgbuf 245 282 246 283 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 247 search_text=True, comment_stack=None, msgbuf=None): 284 search_text=True, comment_stack=None, context_stack=None, 285 msgbuf=None): 248 286 stream = iter(stream) 249 287 previous = stream.next() 250 288 … … 282 320 class ChooseDirective(ExtractableI18NDirective): 283 321 """Implementation of the ``i18n:choose`` directive which provides plural 284 322 internationalisation of strings. 285 323 286 324 This directive requires at least one parameter, the one which evaluates to 287 325 an integer which will allow to choose the plural/singular form. If you also 288 326 have expressions inside the singular and plural version of the string you 289 327 also need to pass a name for those parameters. Consider the following 290 328 examples: 291 329 292 330 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 293 331 ... <div i18n:choose="num; num"> 294 332 ... <p i18n:singular="">There is $num coin</p> … … 362 400 363 401 ngettext = ctxt.get('_i18n.ngettext') 364 402 assert hasattr(ngettext, '__call__'), 'No ngettext function available' 403 npgettext = ctxt.get('_i18n.npgettext') 404 if not npgettext: 405 npgettext = lambda c, s, p, n: ngettext(s, p, n) 365 406 dngettext = ctxt.get('_i18n.dngettext') 366 407 if not dngettext: 367 408 dngettext = lambda d, s, p, n: ngettext(s, p, n) 409 dnpgettext = ctxt.get('_i18n.dnpgettext') 410 if not dnpgettext: 411 dnpgettext = lambda d, c, s, p, n: dngettext(d, s, p, n) 368 412 369 413 new_stream = [] 370 414 singular_stream = None … … 395 439 else: 396 440 new_stream.append(event) 397 441 398 if ctxt.get('_i18n.domain'): 442 if ctxt.get('_i18n.context') and ctxt.get('_i18n.domain'): 443 ngettext = lambda s, p, n: dnpgettext(ctxt.get('_i18n.domain'), 444 ctxt.get('_i18n.context'), 445 s, p, n) 446 elif ctxt.get('_i18n.context'): 447 ngettext = lambda s, p, n: npgettext(ctxt.get('_i18n.context'), 448 s, p, n) 449 elif ctxt.get('_i18n.domain'): 399 450 ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'), 400 451 s, p, n) 401 452 … … 424 475 ctxt.pop() 425 476 426 477 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 427 search_text=True, comment_stack=None ):478 search_text=True, comment_stack=None, context_stack=None): 428 479 strip = False 429 480 stream = iter(stream) 430 481 previous = stream.next() … … 448 499 if isinstance(directive, SingularDirective): 449 500 for message in directive.extract(translator, 450 501 substream, gettext_functions, search_text, 451 comment_stack, msgbuf=singular_msgbuf):502 comment_stack, context_stack, msgbuf=singular_msgbuf): 452 503 yield message 453 504 elif isinstance(directive, PluralDirective): 454 505 for message in directive.extract(translator, 455 506 substream, gettext_functions, search_text, 456 comment_stack, msgbuf=plural_msgbuf):507 comment_stack, context_stack, msgbuf=plural_msgbuf): 457 508 yield message 458 509 elif not isinstance(directive, StripDirective): 459 510 singular_msgbuf.append(*previous) … … 472 523 singular_msgbuf.append(*previous) 473 524 plural_msgbuf.append(*previous) 474 525 475 yield self.lineno, 'ngettext', \526 yield contextify(self.lineno, 'ngettext', \ 476 527 (singular_msgbuf.format(), plural_msgbuf.format()), \ 477 comment_stack[-1:]528 comment_stack[-1:], context_stack[-1:]) 478 529 479 530 def _is_plural(self, numeral, ngettext): 480 531 # XXX: should we test which form was chosen like this!?!?!? … … 488 539 class DomainDirective(I18NDirective): 489 540 """Implementation of the ``i18n:domain`` directive which allows choosing 490 541 another i18n domain(catalog) to translate from. 491 542 492 543 >>> from genshi.filters.tests.i18n import DummyTranslations 493 544 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 494 545 ... <p i18n:msg="">Bar</p> … … 540 591 ctxt.pop() 541 592 542 593 594 class ContextDirective(I18NDirective): 595 __slots__ = ['context'] 596 597 def __init__(self, value, template=None, namespaces=None, lineno=-1, 598 offset=-1): 599 Directive.__init__(self, None, template, namespaces, lineno, offset) 600 self.context = value 601 602 @classmethod 603 def attach(cls, template, stream, value, namespaces, pos): 604 if type(value) is dict: 605 value = value.get('name') 606 return super(ContextDirective, cls).attach(template, stream, value, 607 namespaces, pos) 608 609 def __call__(self, stream, directives, ctxt, **vars): 610 ctxt.push({'_i18n.context': self.context}) 611 for event in _apply_directives(stream, directives, ctxt, vars): 612 yield event 613 ctxt.pop() 614 615 543 616 class Translator(DirectiveFactory): 544 617 """Can extract and translate localizable strings from markup streams and 545 618 templates. 546 619 547 620 For example, assume the following template: 548 621 549 622 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 550 623 ... <head> 551 624 ... <title>Example</title> … … 555 628 ... <p>${_("Hello, %(name)s") % dict(name=username)}</p> 556 629 ... </body> 557 630 ... </html>''', filename='example.html') 558 631 559 632 For demonstration, we define a dummy ``gettext``-style function with a 560 633 hard-coded translation table, and pass that to the `Translator` initializer: 561 634 562 635 >>> def pseudo_gettext(string): 563 636 ... return { 564 637 ... 'Example': 'Beispiel', 565 638 ... 'Hello, %(name)s': 'Hallo, %(name)s' 566 639 ... }[string] 567 640 >>> translator = Translator(pseudo_gettext) 568 641 569 642 Next, the translator needs to be prepended to any already defined filters 570 643 on the template: 571 644 572 645 >>> tmpl.filters.insert(0, translator) 573 646 574 647 When generating the template output, our hard-coded translations should be 575 648 applied as expected: 576 649 577 650 >>> print(tmpl.generate(username='Hans', _=pseudo_gettext)) 578 651 <html> 579 652 <head> … … 584 657 <p>Hallo, Hans</p> 585 658 </body> 586 659 </html> 587 660 588 661 Note that elements defining ``xml:lang`` attributes that do not contain 589 662 variable expressions are ignored by this filter. That can be used to 590 663 exclude specific parts of a template from being extracted and translated. … … 593 666 directives = [ 594 667 ('domain', DomainDirective), 595 668 ('comment', CommentDirective), 669 ('ctxt', ContextDirective), 596 670 ('msg', MsgDirective), 597 671 ('choose', ChooseDirective), 598 672 ('singular', SingularDirective), … … 611 685 def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS, 612 686 include_attrs=INCLUDE_ATTRS, extract_text=True): 613 687 """Initialize the translator. 614 688 615 689 :param translate: the translation function, for example ``gettext`` or 616 690 ``ugettext``. 617 691 :param ignore_tags: a set of tag names that should not be localized … … 619 693 :param extract_text: whether the content of text nodes should be 620 694 extracted, or only text in explicit ``gettext`` 621 695 function calls 622 696 623 697 :note: Changed in 0.6: the `translate` parameter can now be either 624 698 a ``gettext``-style function, or an object compatible with the 625 699 ``NullTransalations`` or ``GNUTranslations`` interface … … 632 706 def __call__(self, stream, ctxt=None, translate_text=True, 633 707 translate_attrs=True): 634 708 """Translate any localizable strings in the given stream. 635 709 636 710 This function shouldn't be called directly. Instead, an instance of 637 711 the `Translator` class should be registered as a filter with the 638 712 `Template` or the `TemplateLoader`, or applied as a regular stream 639 713 filter. If used as a template filter, it should be inserted in front of 640 714 all the default filters. 641 715 642 716 :param stream: the markup event stream 643 717 :param ctxt: the template context (not used) 644 718 :param translate_text: whether text nodes should be translated (used … … 676 750 except AttributeError: 677 751 dgettext = lambda _, y: gettext(y) 678 752 dngettext = lambda _, s, p, n: ngettext(s, p, n) 753 try: 754 if IS_PYTHON2: 755 pgettext = self.translate.upgettext 756 dpgettext = self.translate.dupgettext 757 npgettext = self.translate.unpgettext 758 dnpgettext = self.translate.dunpgettext 759 else: 760 pgettext = self.translate.pgettext 761 dpgettext = self.translate.dpgettext 762 npgettext = self.translate.npgettext 763 dnpgettext = self.translate.dnpgettext 764 except AttributeError: 765 pgettext = lambda _, y: gettext(y) 766 dpgettext = lambda d, _, y: dgettext(d, y) 767 npgettext = lambda _, s, p, n: ngettext(s, p, n) 768 dnpgettext = lambda d, _, s, p, n: dngettext(d, s, p, n) 679 769 if ctxt: 680 770 ctxt['_i18n.gettext'] = gettext 681 771 ctxt['_i18n.ngettext'] = ngettext 682 772 ctxt['_i18n.dgettext'] = dgettext 683 773 ctxt['_i18n.dngettext'] = dngettext 774 ctxt['_i18n.pgettext'] = pgettext 775 ctxt['_i18n.npgettext'] = npgettext 776 ctxt['_i18n.dpgettext'] = dpgettext 777 ctxt['_i18n.dnpgettext'] = dnpgettext 684 778 685 779 if ctxt and ctxt.get('_i18n.domain'): 686 780 # TODO: This can cause infinite recursion if dgettext is defined 687 781 # via the AttributeError case above! 688 gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)782 gettext = partial(dgettext, ctxt.get('_i18n.domain')) 689 783 784 if ctxt and ctxt.get('_i18n.context'): 785 if getattr(gettext, 'func', None): 786 gettext = partial(dpgettext, 787 ctxt['_i18n.domain'], 788 ctxt['_i18n.context']) 789 else: 790 gettext = partial(pgettext, ctxt['_i18n.context']) 791 690 792 for kind, data, pos in stream: 691 793 692 794 # skip chunks that should not be localized … … 737 839 elif kind is SUB: 738 840 directives, substream = data 739 841 current_domain = None 842 current_context = None 740 843 for idx, directive in enumerate(directives): 741 844 # Organize directives to make everything work 742 845 # FIXME: There's got to be a better way to do this! … … 747 850 # Put domain directive as the first one in order to 748 851 # update context before any other directives evaluation 749 852 directives.insert(0, directives.pop(idx)) 853 if isinstance(directive, ContextDirective): 854 # Grab current (msg)context and update context 855 current_context = directive.context 856 ctxt.push({'_i18n.context': current_context}) 857 # Put context directive either first in the case of 858 # no domain, or 2nd in the case there is a domain, to 859 # update context before any other directives evaluation 860 directives.insert(1 if current_domain else 0, 861 directives.pop(idx)) 750 862 751 863 # If this is an i18n directive, no need to translate text 752 864 # nodes here … … 754 866 isinstance(d, ExtractableI18NDirective) 755 867 for d in directives 756 868 ]) 869 757 870 substream = list(self(substream, ctxt, 758 871 translate_text=not is_i18n_directive, 759 872 translate_attrs=translate_attrs)) … … 761 874 762 875 if current_domain: 763 876 ctxt.pop() 877 if current_context: 878 ctxt.pop() 764 879 else: 765 880 yield kind, data, pos 766 881 767 882 def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS, 768 search_text=True, comment_stack=None ):883 search_text=True, comment_stack=None, context_stack=None): 769 884 """Extract localizable strings from the given template stream. 770 885 771 886 For every string found, this function yields a ``(lineno, function, 772 887 message, comments)`` tuple, where: 773 888 774 889 * ``lineno`` is the number of the line on which the string was found, 775 890 * ``function`` is the name of the ``gettext`` function used (if the 776 891 string was extracted from embedded Python code), and … … 779 894 arguments). 780 895 * ``comments`` is a list of comments related to the message, extracted 781 896 from ``i18n:comment`` attributes found in the markup 782 897 783 898 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 784 899 ... <head> 785 900 ... <title>Example</title> … … 796 911 6, None, u'Example' 797 912 7, '_', u'Hello, %(name)s' 798 913 8, 'ngettext', (u'You have %d item', u'You have %d items', None) 799 914 800 915 :param stream: the event stream to extract strings from; can be a 801 916 regular stream or a template stream 802 917 :param gettext_functions: a sequence of function names that should be … … 804 919 functions 805 920 :param search_text: whether the content of text nodes should be 806 921 extracted (used internally) 807 922 808 923 :note: Changed in 0.4.1: For a function with multiple string arguments 809 924 (such as ``ngettext``), a single item with a tuple of strings is 810 925 yielded, instead an item for each string argument. … … 815 930 search_text = False 816 931 if comment_stack is None: 817 932 comment_stack = [] 933 if context_stack is None: 934 context_stack = [] 818 935 skip = 0 819 936 820 937 xml_lang = XML_NAMESPACE['lang'] … … 841 958 elif not skip and search_text and kind is TEXT: 842 959 text = data.strip() 843 960 if text and [ch for ch in text if ch.isalpha()]: 844 yield pos[1], None, text, comment_stack[-1:] 961 yield contextify(pos[1], None, text, comment_stack[-1:], 962 context_stack[-1:]) 845 963 846 964 elif kind is EXPR or kind is EXEC: 847 965 for funcname, strings in extract_from_code(data, … … 852 970 elif kind is SUB: 853 971 directives, substream = data 854 972 in_comment = False 973 in_context = False 855 974 856 975 for idx, directive in enumerate(directives): 857 976 # Do a first loop to see if there's a comment directive … … 865 984 for message in self.extract( 866 985 substream, gettext_functions, 867 986 search_text=search_text and not skip, 868 comment_stack=comment_stack): 987 comment_stack=comment_stack, 988 context_stack=context_stack): 869 989 yield message 870 990 directives.pop(idx) 991 elif isinstance(directive, ContextDirective): 992 in_context = True 993 context_stack.append(directive.context) 994 if len(directives) == 1: 995 for message in self.extract( 996 substream, gettext_functions, 997 search_text=search_text and not skip, 998 comment_stack=comment_stack, 999 context_stack=context_stack): 1000 yield message 1001 directives.pop(idx) 871 1002 elif not isinstance(directive, I18NDirective): 872 1003 # Remove all other non i18n directives from the process 873 1004 directives.pop(idx) 874 1005 875 if not directives and not in_comment :1006 if not directives and not in_comment and not in_context: 876 1007 # Extract content if there's no directives because 877 1008 # strip was pop'ed and not because comment was pop'ed. 878 1009 # Extraction in this case has been taken care of. … … 886 1017 for message in directive.extract(self, 887 1018 substream, gettext_functions, 888 1019 search_text=search_text and not skip, 889 comment_stack=comment_stack): 1020 comment_stack=comment_stack, 1021 context_stack=context_stack): 890 1022 yield message 891 1023 else: 892 1024 for message in self.extract( 893 1025 substream, gettext_functions, 894 1026 search_text=search_text and not skip, 895 comment_stack=comment_stack): 1027 comment_stack=comment_stack, 1028 context_stack=context_stack): 896 1029 yield message 897 1030 898 1031 if in_comment: 899 1032 comment_stack.pop() 900 1033 1034 if in_context: 1035 context_stack.pop() 1036 901 1037 def get_directive_index(self, dir_cls): 902 1038 total = len(self._dir_order) 903 1039 if dir_cls in self._dir_order: … … 907 1043 def setup(self, template): 908 1044 """Convenience function to register the `Translator` filter and the 909 1045 related directives with the given template. 910 1046 911 1047 :param template: a `Template` instance 912 1048 """ 913 1049 template.filters.insert(0, self) … … 929 1065 930 1066 class MessageBuffer(object): 931 1067 """Helper class for managing internationalized mixed content. 932 1068 933 1069 :since: version 0.5 934 1070 """ 935 1071 936 1072 def __init__(self, directive=None): 937 1073 """Initialize the message buffer. 938 1074 939 1075 :param directive: the directive owning the buffer 940 1076 :type directive: I18NDirective 941 1077 """ … … 962 1098 963 1099 def append(self, kind, data, pos): 964 1100 """Append a stream event to the buffer. 965 1101 966 1102 :param kind: the stream event kind 967 1103 :param data: the event data 968 1104 :param pos: the position of the event in the source … … 994 1130 params = "(%s)" % params 995 1131 raise IndexError("%d parameters%s given to 'i18n:%s' but " 996 1132 "%d or more expressions used in '%s', line %s" 997 % (len(self.orig_params), params, 1133 % (len(self.orig_params), params, 998 1134 self.directive.tagname, 999 1135 len(self.orig_params) + 1, 1000 1136 os.path.basename(pos[0] or … … 1004 1140 self._add_event(self.stack[-1], (kind, data, pos)) 1005 1141 self.values[param] = (kind, data, pos) 1006 1142 else: 1007 if kind is START: 1143 if kind is START: 1008 1144 self.string.append('[%d:' % self.order) 1009 1145 self.stack.append(self.order) 1010 1146 self._add_event(self.stack[-1], (kind, data, pos)) … … 1026 1162 def translate(self, string, regex=re.compile(r'%\((\w+)\)s')): 1027 1163 """Interpolate the given message translation with the events in the 1028 1164 buffer and return the translated stream. 1029 1165 1030 1166 :param string: the translated message string 1031 1167 """ 1032 1168 substream = None … … 1121 1257 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')): 1122 1258 """Parse a translated message using Genshi mixed content message 1123 1259 formatting. 1124 1260 1125 1261 >>> parse_msg("See [1:Help].") 1126 1262 [(0, 'See '), (1, 'Help'), (0, '.')] 1127 1263 1128 1264 >>> parse_msg("See [1:our [2:Help] page] for details.") 1129 1265 [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')] 1130 1266 1131 1267 >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].") 1132 1268 [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')] 1133 1269 1134 1270 >>> parse_msg("[1:] Bilder pro Seite anzeigen.") 1135 1271 [(1, ''), (0, ' Bilder pro Seite anzeigen.')] 1136 1272 1137 1273 :param string: the translated message string 1138 1274 :return: a list of ``(order, string)`` tuples 1139 1275 :rtype: `list` … … 1165 1301 1166 1302 def extract_from_code(code, gettext_functions): 1167 1303 """Extract strings from Python bytecode. 1168 1304 1169 1305 >>> from genshi.template.eval import Expression 1170 1306 >>> expr = Expression('_("Hello")') 1171 1307 >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS)) 1172 1308 [('_', u'Hello')] 1173 1309 1174 1310 >>> expr = Expression('ngettext("You have %(num)s item", ' 1175 1311 ... '"You have %(num)s items", num)') 1176 1312 >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS)) 1177 1313 [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))] 1178 1314 1179 1315 :param code: the `Code` object 1180 1316 :type code: `genshi.template.eval.Code` 1181 1317 :param gettext_functions: a sequence of function names … … 1217 1353 1218 1354 def extract(fileobj, keywords, comment_tags, options): 1219 1355 """Babel extraction method for Genshi templates. 1220 1356 1221 1357 :param fileobj: the file-like object the messages should be extracted from 1222 1358 :param keywords: a list of keywords (i.e. function names) that should be 1223 1359 recognized as translation functions -
genshi/filters/tests/i18n.py
82 82 83 83 if IS_PYTHON2: 84 84 def dungettext(self, domain, singular, plural, numeral): 85 return self._domain_call('ungettext', domain, singular, plural, numeral) 85 return self._domain_call( 86 'ungettext', domain, singular, plural, numeral) 86 87 else: 87 88 def dngettext(self, domain, singular, plural, numeral): 88 return self._domain_call('ngettext', domain, singular, plural, numeral) 89 return self._domain_call( 90 'ngettext', domain, singular, plural, numeral) 89 91 92 def upgettext(self, context, message): 93 try: 94 return self._catalog[(context, message)] 95 except KeyError: 96 if self._fallback: 97 return self._fallback.upgettext(context, message) 98 return unicode(message) 90 99 100 if not IS_PYTHON2: 101 pgettext = upgettext 102 del upgettext 103 104 if IS_PYTHON2: 105 def dupgettext(self, domain, context, message): 106 return self._domain_call('upgettext', domain, context, message) 107 else: 108 def dpgettext(self, domain, context, message): 109 return self._domain_call('pgettext', domain, context, message) 110 111 def unpgettext(self, context, msgid1, msgid2, n): 112 try: 113 return self._catalog[(context, msgid1, self.plural(n))] 114 except KeyError: 115 if self._fallback: 116 return self._fallback.unpgettext(context, msgid1, msgid2, n) 117 if n == 1: 118 return msgid1 119 else: 120 return msgid2 121 122 if not IS_PYTHON2: 123 npgettext = unpgettext 124 del unpgettext 125 126 if IS_PYTHON2: 127 def dunpgettext(self, domain, context, msgid1, msgid2, n): 128 return self._domain_call('unpgettext', context, msgid1, msgid2, n) 129 else: 130 def dnpgettext(self, domain, context, msgid1, msgid2, n): 131 return self._domain_call('npgettext', context, msgid1, msgid2, n) 132 133 91 134 class TranslatorTestCase(unittest.TestCase): 92 135 93 136 def test_translate_included_attribute_text(self): … … 1451 1494 <p>Vohs John Doe</p> 1452 1495 </div> 1453 1496 </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render()) 1454 1497 1455 1498 def test_translate_i18n_choose_and_singular_with_py_strip(self): 1456 1499 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 1457 1500 xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 1481 1524 </div> 1482 1525 </html>""", tmpl.generate( 1483 1526 one=1, two=2, fname='John',lname='Doe').render()) 1484 1527 1485 1528 def test_translate_i18n_choose_and_plural_with_py_strip(self): 1486 1529 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 1487 1530 xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 2004 2047 (34, '_', 'Update', [])], messages) 2005 2048 2006 2049 2050 class ContextDirectiveTestCase(unittest.TestCase): 2051 def test_extract_msgcontext(self): 2052 buf = StringIO("""<html xmlns:py="http://genshi.edgewall.org/" 2053 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2054 <p i18n:ctxt="foo">Foo, bar.</p> 2055 <p>Foo, bar.</p> 2056 </html>""") 2057 results = list(extract(buf, ['_'], [], {})) 2058 self.assertEqual((3, 'pgettext', ('foo', 'Foo, bar.'), []), results[0]) 2059 self.assertEqual((4, None, 'Foo, bar.', []), results[1]) 2060 2061 def test_translate_msgcontext(self): 2062 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2063 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2064 <p i18n:ctxt="foo">Foo, bar.</p> 2065 <p>Foo, bar.</p> 2066 </html>""") 2067 translations = { 2068 ('foo', 'Foo, bar.'): 'Fooo! Barrr!', 2069 'Foo, bar.': 'Foo --- bar.' 2070 } 2071 translator = Translator(DummyTranslations(translations)) 2072 translator.setup(tmpl) 2073 self.assertEqual("""<html> 2074 <p>Fooo! Barrr!</p> 2075 <p>Foo --- bar.</p> 2076 </html>""", tmpl.generate().render()) 2077 2078 def test_translate_msgcontext_with_domain(self): 2079 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2080 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2081 <p i18n:domain="bar" i18n:ctxt="foo">Foo, bar. <span>foo</span></p> 2082 <p>Foo, bar.</p> 2083 </html>""") 2084 translations = DummyTranslations({ 2085 ('foo', 'Foo, bar.'): 'Fooo! Barrr!', 2086 'Foo, bar.': 'Foo --- bar.' 2087 }) 2088 translations.add_domain('bar', { 2089 ('foo', 'foo'): 'BARRR', 2090 ('foo', 'Foo, bar.'): 'Bar, bar.' 2091 }) 2092 2093 translator = Translator(translations) 2094 translator.setup(tmpl) 2095 self.assertEqual("""<html> 2096 <p>Bar, bar. <span>BARRR</span></p> 2097 <p>Foo --- bar.</p> 2098 </html>""", tmpl.generate().render()) 2099 2100 def test_translate_msgcontext_with_plurals(self): 2101 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2102 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2103 <i18n:ctxt name="foo"> 2104 <p i18n:choose="num; num"> 2105 <span i18n:singular="">There is ${num} bar</span> 2106 <span i18n:plural="">There are ${num} bars</span> 2107 </p> 2108 </i18n:ctxt> 2109 </html>""") 2110 translations = DummyTranslations({ 2111 ('foo', 'There is %(num)s bar', 0): 'Hay %(num)s barre', 2112 ('foo', 'There is %(num)s bar', 1): 'Hay %(num)s barres' 2113 }) 2114 2115 translator = Translator(translations) 2116 translator.setup(tmpl) 2117 self.assertEqual("""<html> 2118 <p> 2119 <span>Hay 1 barre</span> 2120 </p> 2121 </html>""", tmpl.generate(num=1).render()) 2122 self.assertEqual("""<html> 2123 <p> 2124 <span>Hay 2 barres</span> 2125 </p> 2126 </html>""", tmpl.generate(num=2).render()) 2127 2128 def test_translate_context_with_msg(self): 2129 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2130 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2131 <p i18n:ctxt="foo" i18n:msg="num"> 2132 Foo <span>There is ${num} bar</span> Bar 2133 </p> 2134 </html>""") 2135 translations = DummyTranslations({ 2136 ('foo', 'Foo [1:There is %(num)s bar] Bar'): 2137 'Voh [1:Hay %(num)s barre] Barre' 2138 }) 2139 translator = Translator(translations) 2140 translator.setup(tmpl) 2141 self.assertEqual("""<html> 2142 <p>Voh <span>Hay 1 barre</span> Barre</p> 2143 </html>""", tmpl.generate(num=1).render()) 2144 2145 2007 2146 def suite(): 2008 2147 suite = unittest.TestSuite() 2009 2148 suite.addTest(doctest.DocTestSuite(Translator.__module__)) … … 2012 2151 suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test')) 2013 2152 suite.addTest(unittest.makeSuite(DomainDirectiveTestCase, 'test')) 2014 2153 suite.addTest(unittest.makeSuite(ExtractTestCase, 'test')) 2154 suite.addTest(unittest.makeSuite(ContextDirectiveTestCase, 'test')) 2015 2155 return suite 2016 2156 2017 2157 if __name__ == '__main__': -
examples/bench/bigtable.py
10 10 import timeit 11 11 from StringIO import StringIO 12 12 from genshi.builder import tag 13 from genshi.filters.i18n import Translator 14 from genshi.filters.tests.i18n import DummyTranslations 13 15 from genshi.template import MarkupTemplate, NewTextTemplate 14 16 15 17 try: … … 56 58 </table> 57 59 """) 58 60 61 genshi_tmpl_i18n = MarkupTemplate(""" 62 <table xmlns:py="http://genshi.edgewall.org/" 63 xmlns:i18n="http://genshi.edgewall.org/i18n"> 64 <tr py:for="row in table"> 65 <td py:for="c in row.values()">${c}</td> 66 </tr> 67 </table> 68 """) 69 t = Translator(DummyTranslations()) 70 t.setup(genshi_tmpl_i18n) 71 59 72 genshi_tmpl2 = MarkupTemplate(""" 60 73 <table xmlns:py="http://genshi.edgewall.org/">$table</table> 61 74 """) … … 103 116 stream = genshi_tmpl.generate(table=table) 104 117 stream.render('html', strip_whitespace=False) 105 118 119 def test_genshi_i18n(): 120 """Genshi template w/ i18n""" 121 stream = genshi_tmpl_i18n.generate(table=table) 122 stream.render('html', strip_whitespace=False) 123 106 124 def test_genshi_text(): 107 125 """Genshi text template""" 108 126 stream = genshi_text_tmpl.generate(table=table) … … 167 185 et.tostring(_table) 168 186 169 187 if cet: 170 def test_cet(): 188 def test_cet(): 171 189 """cElementTree""" 172 190 _table = cet.Element('table') 173 191 for row in table: … … 196 214 197 215 198 216 def run(which=None, number=10): 199 tests = ['test_builder', 'test_genshi', 'test_genshi_ text',217 tests = ['test_builder', 'test_genshi', 'test_genshi_i18n', 'test_genshi_text', 200 218 'test_genshi_builder', 'test_mako', 'test_kid', 'test_kid_et', 201 219 'test_et', 'test_cet', 'test_clearsilver', 'test_django'] 202 220
