Ticket #580: msgctxt.2.patch
| File msgctxt.2.patch, 41.6 KB (added by eric.oconnell@…, 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/tests/i18n.py
62 62 else: 63 63 return msgid2 64 64 65 def dungettext(self, domain, singular, plural, numeral):66 return self._domain_call('ungettext', domain, singular, plural, numeral)65 def dungettext(self, domain, msgid1, msgid2, n): 66 return self._domain_call('ungettext', domain, msgid1, msgid2, n) 67 67 68 def upgettext(self, context, message): 69 try: 70 return self._catalog[(context, message)] 71 except KeyError: 72 if self._fallback: 73 return self._fallback.upgettext(context, message) 74 return unicode(message) 68 75 76 def dupgettext(self, domain, context, message): 77 return self._domain_call('upgettext', domain, context, message) 78 79 def unpgettext(self, context, msgid1, msgid2, n): 80 try: 81 return self._catalog[(context, msgid1, self.plural(n))] 82 except KeyError: 83 if self._fallback: 84 return self._fallback.unpgettext(context, msgid1, msgid2, n) 85 if n == 1: 86 return msgid1 87 else: 88 return msgid2 89 90 def dunpgettext(self, domain, context, msgid1, msgid2, n): 91 return self._domain_call('npgettext', context, msgid1, msgid2, n) 92 93 69 94 class TranslatorTestCase(unittest.TestCase): 70 95 71 96 def test_translate_included_attribute_text(self): … … 1417 1442 <p>Vohs John Doe</p> 1418 1443 </div> 1419 1444 </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render()) 1420 1445 1421 1446 def test_translate_i18n_choose_and_singular_with_py_strip(self): 1422 1447 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 1423 1448 xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 1447 1472 </div> 1448 1473 </html>""", tmpl.generate( 1449 1474 one=1, two=2, fname='John',lname='Doe').render()) 1450 1475 1451 1476 def test_translate_i18n_choose_and_plural_with_py_strip(self): 1452 1477 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 1453 1478 xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 1965 1990 (34, '_', 'Update', [])], messages) 1966 1991 1967 1992 1993 class ContextDirectiveTestCase(unittest.TestCase): 1994 def test_extract_msgcontext(self): 1995 buf = StringIO("""<html xmlns:py="http://genshi.edgewall.org/" 1996 xmlns:i18n="http://genshi.edgewall.org/i18n"> 1997 <p i18n:ctxt="foo">Foo, bar.</p> 1998 <p>Foo, bar.</p> 1999 </html>""") 2000 results = list(extract(buf, ['_'], [], {})) 2001 self.assertEqual((3, 'pgettext', ('foo', 'Foo, bar.'), []), results[0]) 2002 self.assertEqual((4, None, 'Foo, bar.', []), results[1]) 2003 2004 def test_translate_msgcontext(self): 2005 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2006 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2007 <p i18n:ctxt="foo">Foo, bar.</p> 2008 <p>Foo, bar.</p> 2009 </html>""") 2010 translations = { 2011 ('foo', 'Foo, bar.'): 'Fooo! Barrr!', 2012 'Foo, bar.': 'Foo --- bar.' 2013 } 2014 translator = Translator(DummyTranslations(translations)) 2015 translator.setup(tmpl) 2016 self.assertEqual("""<html> 2017 <p>Fooo! Barrr!</p> 2018 <p>Foo --- bar.</p> 2019 </html>""", tmpl.generate().render()) 2020 2021 def test_translate_msgcontext_with_domain(self): 2022 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2023 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2024 <p i18n:domain="bar" i18n:ctxt="foo">Foo, bar. <span>foo</span></p> 2025 <p>Foo, bar.</p> 2026 </html>""") 2027 translations = DummyTranslations({ 2028 ('foo', 'Foo, bar.'): 'Fooo! Barrr!', 2029 'Foo, bar.': 'Foo --- bar.' 2030 }) 2031 translations.add_domain('bar', { 2032 ('foo', 'foo'): 'BARRR', 2033 ('foo', 'Foo, bar.'): 'Bar, bar.' 2034 }) 2035 2036 translator = Translator(translations) 2037 translator.setup(tmpl) 2038 self.assertEqual("""<html> 2039 <p>Bar, bar. <span>BARRR</span></p> 2040 <p>Foo --- bar.</p> 2041 </html>""", tmpl.generate().render()) 2042 2043 def test_translate_msgcontext_with_plurals(self): 2044 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2045 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2046 <i18n:ctxt name="foo"> 2047 <p i18n:choose="num; num"> 2048 <span i18n:singular="">There is ${num} bar</span> 2049 <span i18n:plural="">There are ${num} bars</span> 2050 </p> 2051 </i18n:ctxt> 2052 </html>""") 2053 translations = DummyTranslations({ 2054 ('foo', 'There is %(num)s bar', 0): 'Hay %(num)s barre', 2055 ('foo', 'There is %(num)s bar', 1): 'Hay %(num)s barres' 2056 }) 2057 2058 translator = Translator(translations) 2059 translator.setup(tmpl) 2060 self.assertEqual("""<html> 2061 <p> 2062 <span>Hay 1 barre</span> 2063 </p> 2064 </html>""", tmpl.generate(num=1).render()) 2065 self.assertEqual("""<html> 2066 <p> 2067 <span>Hay 2 barres</span> 2068 </p> 2069 </html>""", tmpl.generate(num=2).render()) 2070 2071 def test_translate_context_with_msg(self): 2072 tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" 2073 xmlns:i18n="http://genshi.edgewall.org/i18n"> 2074 <p i18n:ctxt="foo" i18n:msg="num"> 2075 Foo <span>There is ${num} bar</span> Bar 2076 </p> 2077 </html>""") 2078 translations = DummyTranslations({ 2079 ('foo', 'Foo [1:There is %(num)s bar] Bar'): 2080 'Voh [1:Hay %(num)s barre] Barre' 2081 }) 2082 translator = Translator(translations) 2083 translator.setup(tmpl) 2084 self.assertEqual("""<html> 2085 <p>Voh <span>Hay 1 barre</span> Barre</p> 2086 </html>""", tmpl.generate(num=1).render()) 2087 2088 1968 2089 def suite(): 1969 2090 suite = unittest.TestSuite() 1970 2091 suite.addTest(doctest.DocTestSuite(Translator.__module__)) … … 1973 2094 suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test')) 1974 2095 suite.addTest(unittest.makeSuite(DomainDirectiveTestCase, 'test')) 1975 2096 suite.addTest(unittest.makeSuite(ExtractTestCase, 'test')) 2097 suite.addTest(unittest.makeSuite(ContextDirectiveTestCase, 'test')) 1976 2098 return suite 1977 2099 1978 2100 if __name__ == '__main__': -
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 … … 59 60 """Simple interface for directives to support messages extraction.""" 60 61 61 62 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 62 search_text=True, comment_stack=None ):63 search_text=True, comment_stack=None, context_stack=None): 63 64 raise NotImplementedError 64 65 65 66 67 contexted = { 68 None: 'pgettext', 69 'gettext': 'pgettext', 70 'ngettext': 'pngettext', 71 'dgettext': 'dpgettext', 72 'dngettext': 'dnpgettext' 73 } 74 75 76 def contextify(line, func, msg, comment, context): 77 if context: 78 context = context[0] 79 func = contexted.get(func) 80 if func is None: 81 raise Exception("failure, bogus extraction method") 82 if isinstance(msg, tuple): 83 msg = (context, tuple[0], tuple[1]) 84 else: 85 msg = (context, msg) 86 return line, func, msg, comment 87 88 66 89 class CommentDirective(I18NDirective): 67 90 """Implementation of the ``i18n:comment`` template directive which adds 68 91 translation comments. 69 92 70 93 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 71 94 ... <p i18n:comment="As in Foo Bar">Foo</p> 72 95 ... </html>''') … … 86 109 class MsgDirective(ExtractableI18NDirective): 87 110 r"""Implementation of the ``i18n:msg`` directive which marks inner content 88 111 as translatable. Consider the following examples: 89 112 90 113 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 91 114 ... <div i18n:msg=""> 92 115 ... <p>Foo</p> … … 94 117 ... </div> 95 118 ... <p i18n:msg="">Foo <em>bar</em>!</p> 96 119 ... </html>''') 97 120 98 121 >>> translator = Translator() 99 122 >>> translator.setup(tmpl) 100 123 >>> list(translator.extract(tmpl.stream)) … … 154 177 155 178 def __call__(self, stream, directives, ctxt, **vars): 156 179 gettext = ctxt.get('_i18n.gettext') 157 if ctxt.get('_i18n.domain'): 180 if ctxt.get('_i18n.domain') and ctxt.get('_i18n.context'): 181 dpgettext = ctxt.get('_i18n.dpgettext') 182 assert hasattr(dpgettext, '__call__'), \ 183 'No domain/context gettext function passed' 184 gettext = lambda msg: dpgettext(ctxt.get('_i18n.domain'), 185 ctxt.get('_i18n.context'), 186 msg) 187 elif ctxt.get('_i18n.domain'): 158 188 dgettext = ctxt.get('_i18n.dgettext') 159 189 assert hasattr(dgettext, '__call__'), \ 160 190 'No domain gettext function passed' 161 191 gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg) 192 elif ctxt.get('_i18n.context'): 193 pgettext = ctxt.get('_i18n.pgettext') 194 assert hasattr(pgettext, '__call__'), \ 195 'No context gettext function passed' 196 gettext = lambda msg: pgettext(ctxt.get('_i18n.context'), msg) 162 197 163 198 def _generate(): 164 199 msgbuf = MessageBuffer(self) … … 182 217 return _apply_directives(_generate(), directives, ctxt, vars) 183 218 184 219 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 185 search_text=True, comment_stack=None ):220 search_text=True, comment_stack=None, context_stack=None): 186 221 msgbuf = MessageBuffer(self) 187 222 strip = False 188 223 … … 206 241 if not strip: 207 242 msgbuf.append(*previous) 208 243 209 yield self.lineno, None, msgbuf.format(), comment_stack[-1:] 244 yield contextify( 245 self.lineno, None, msgbuf.format(), comment_stack[-1:], context_stack[-1:]) 210 246 211 247 212 248 class ChooseBranchDirective(I18NDirective): … … 243 279 ctxt['_i18n.choose.%s' % self.tagname] = msgbuf 244 280 245 281 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 246 search_text=True, comment_stack=None, msgbuf=None): 282 search_text=True, comment_stack=None, context_stack=None, 283 msgbuf=None): 247 284 stream = iter(stream) 248 285 previous = stream.next() 249 286 … … 281 318 class ChooseDirective(ExtractableI18NDirective): 282 319 """Implementation of the ``i18n:choose`` directive which provides plural 283 320 internationalisation of strings. 284 321 285 322 This directive requires at least one parameter, the one which evaluates to 286 323 an integer which will allow to choose the plural/singular form. If you also 287 324 have expressions inside the singular and plural version of the string you 288 325 also need to pass a name for those parameters. Consider the following 289 326 examples: 290 327 291 328 >>> tmpl = MarkupTemplate('''\ 292 329 <html xmlns:i18n="http://genshi.edgewall.org/i18n"> 293 330 ... <div i18n:choose="num; num"> … … 364 401 365 402 ngettext = ctxt.get('_i18n.ngettext') 366 403 assert hasattr(ngettext, '__call__'), 'No ngettext function available' 404 npgettext = ctxt.get('_i18n.npgettext') 405 if not npgettext: 406 npgettext = lambda c, s, p, n: ngettext(s, p, n) 367 407 dngettext = ctxt.get('_i18n.dngettext') 368 408 if not dngettext: 369 409 dngettext = lambda d, s, p, n: ngettext(s, p, n) 410 dnpgettext = ctxt.get('_i18n.dnpgettext') 411 if not dnpgettext: 412 dnpgettext = lambda d, c, s, p, n: dngettext(d, s, p, n) 370 413 371 414 new_stream = [] 372 415 singular_stream = None … … 397 440 else: 398 441 new_stream.append(event) 399 442 400 if ctxt.get('_i18n.domain'): 443 if ctxt.get('_i18n.context') and ctxt.get('_i18n.domain'): 444 ngettext = lambda s, p, n: dnpgettext(ctxt.get('_i18n.domain'), 445 ctxt.get('_i18n.context'), 446 s, p, n) 447 elif ctxt.get('_i18n.context'): 448 ngettext = lambda s, p, n: npgettext(ctxt.get('_i18n.context'), 449 s, p, n) 450 elif ctxt.get('_i18n.domain'): 401 451 ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'), 402 452 s, p, n) 403 453 … … 426 476 ctxt.pop() 427 477 428 478 def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS, 429 search_text=True, comment_stack=None ):479 search_text=True, comment_stack=None, context_stack=None): 430 480 strip = False 431 481 stream = iter(stream) 432 482 previous = stream.next() … … 450 500 if isinstance(directive, SingularDirective): 451 501 for message in directive.extract(translator, 452 502 substream, gettext_functions, search_text, 453 comment_stack, msgbuf=singular_msgbuf):503 comment_stack, context_stack, msgbuf=singular_msgbuf): 454 504 yield message 455 505 elif isinstance(directive, PluralDirective): 456 506 for message in directive.extract(translator, 457 507 substream, gettext_functions, search_text, 458 comment_stack, msgbuf=plural_msgbuf):508 comment_stack, context_stack, msgbuf=plural_msgbuf): 459 509 yield message 460 510 elif not isinstance(directive, StripDirective): 461 511 singular_msgbuf.append(*previous) … … 474 524 singular_msgbuf.append(*previous) 475 525 plural_msgbuf.append(*previous) 476 526 477 yield self.lineno, 'ngettext', \527 yield contextify(self.lineno, 'ngettext', \ 478 528 (singular_msgbuf.format(), plural_msgbuf.format()), \ 479 comment_stack[-1:]529 comment_stack[-1:], context_stack[-1:]) 480 530 481 531 def _is_plural(self, numeral, ngettext): 482 532 # XXX: should we test which form was chosen like this!?!?!? … … 490 540 class DomainDirective(I18NDirective): 491 541 """Implementation of the ``i18n:domain`` directive which allows choosing 492 542 another i18n domain(catalog) to translate from. 493 543 494 544 >>> from genshi.filters.tests.i18n import DummyTranslations 495 545 >>> tmpl = MarkupTemplate('''\ 496 546 <html xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 543 593 ctxt.pop() 544 594 545 595 596 class ContextDirective(I18NDirective): 597 __slots__ = ['context'] 598 599 def __init__(self, value, template=None, namespaces=None, lineno=-1, 600 offset=-1): 601 Directive.__init__(self, None, template, namespaces, lineno, offset) 602 self.context = value 603 604 @classmethod 605 def attach(cls, template, stream, value, namespaces, pos): 606 if type(value) is dict: 607 value = value.get('name') 608 return super(ContextDirective, cls).attach(template, stream, value, 609 namespaces, pos) 610 611 def __call__(self, stream, directives, ctxt, **vars): 612 ctxt.push({'_i18n.context': self.context}) 613 for event in _apply_directives(stream, directives, ctxt, vars): 614 yield event 615 ctxt.pop() 616 617 546 618 class Translator(DirectiveFactory): 547 619 """Can extract and translate localizable strings from markup streams and 548 620 templates. 549 621 550 622 For example, assume the following template: 551 623 552 624 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 553 625 ... <head> 554 626 ... <title>Example</title> … … 558 630 ... <p>${_("Hello, %(name)s") % dict(name=username)}</p> 559 631 ... </body> 560 632 ... </html>''', filename='example.html') 561 633 562 634 For demonstration, we define a dummy ``gettext``-style function with a 563 635 hard-coded translation table, and pass that to the `Translator` initializer: 564 636 565 637 >>> def pseudo_gettext(string): 566 638 ... return { 567 639 ... 'Example': 'Beispiel', 568 640 ... 'Hello, %(name)s': 'Hallo, %(name)s' 569 641 ... }[string] 570 642 >>> translator = Translator(pseudo_gettext) 571 643 572 644 Next, the translator needs to be prepended to any already defined filters 573 645 on the template: 574 646 575 647 >>> tmpl.filters.insert(0, translator) 576 648 577 649 When generating the template output, our hard-coded translations should be 578 650 applied as expected: 579 651 580 652 >>> print(tmpl.generate(username='Hans', _=pseudo_gettext)) 581 653 <html> 582 654 <head> … … 587 659 <p>Hallo, Hans</p> 588 660 </body> 589 661 </html> 590 662 591 663 Note that elements defining ``xml:lang`` attributes that do not contain 592 664 variable expressions are ignored by this filter. That can be used to 593 665 exclude specific parts of a template from being extracted and translated. … … 596 668 directives = [ 597 669 ('domain', DomainDirective), 598 670 ('comment', CommentDirective), 671 ('ctxt', ContextDirective), 599 672 ('msg', MsgDirective), 600 673 ('choose', ChooseDirective), 601 674 ('singular', SingularDirective), … … 614 687 def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS, 615 688 include_attrs=INCLUDE_ATTRS, extract_text=True): 616 689 """Initialize the translator. 617 690 618 691 :param translate: the translation function, for example ``gettext`` or 619 692 ``ugettext``. 620 693 :param ignore_tags: a set of tag names that should not be localized … … 622 695 :param extract_text: whether the content of text nodes should be 623 696 extracted, or only text in explicit ``gettext`` 624 697 function calls 625 698 626 699 :note: Changed in 0.6: the `translate` parameter can now be either 627 700 a ``gettext``-style function, or an object compatible with the 628 701 ``NullTransalations`` or ``GNUTranslations`` interface … … 635 708 def __call__(self, stream, ctxt=None, translate_text=True, 636 709 translate_attrs=True): 637 710 """Translate any localizable strings in the given stream. 638 711 639 712 This function shouldn't be called directly. Instead, an instance of 640 713 the `Translator` class should be registered as a filter with the 641 714 `Template` or the `TemplateLoader`, or applied as a regular stream 642 715 filter. If used as a template filter, it should be inserted in front of 643 716 all the default filters. 644 717 645 718 :param stream: the markup event stream 646 719 :param ctxt: the template context (not used) 647 720 :param translate_text: whether text nodes should be translated (used … … 671 744 except AttributeError: 672 745 dgettext = lambda _, y: gettext(y) 673 746 dngettext = lambda _, s, p, n: ngettext(s, p, n) 747 try: 748 pgettext = self.translate.upgettext 749 dpgettext = self.translate.dupgettext 750 npgettext = self.translate.unpgettext 751 dnpgettext = self.translate.dunpgettext 752 except AttributeError: 753 pgettext = lambda _, y: gettext(y) 754 dpgettext = lambda d, _, y: dgettext(d, y) 755 npgettext = lambda _, s, p, n: ngettext(s, p, n) 756 dnpgettext = lambda d, _, s, p, n: dngettext(d, s, p, n) 757 674 758 if ctxt: 675 759 ctxt['_i18n.gettext'] = gettext 676 760 ctxt['_i18n.ngettext'] = ngettext 677 761 ctxt['_i18n.dgettext'] = dgettext 678 762 ctxt['_i18n.dngettext'] = dngettext 763 ctxt['_i18n.pgettext'] = pgettext 764 ctxt['_i18n.npgettext'] = npgettext 765 ctxt['_i18n.dpgettext'] = dpgettext 766 ctxt['_i18n.dnpgettext'] = dnpgettext 679 767 680 768 if ctxt and ctxt.get('_i18n.domain'): 681 gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)769 gettext = partial(dgettext, ctxt.get('_i18n.domain')) 682 770 771 if ctxt and ctxt.get('_i18n.context'): 772 if getattr(gettext, 'func', None): 773 gettext = partial(dpgettext, 774 ctxt['_i18n.domain'], 775 ctxt['_i18n.context']) 776 else: 777 gettext = partial(pgettext, ctxt['_i18n.context']) 778 683 779 for kind, data, pos in stream: 684 780 685 781 # skip chunks that should not be localized … … 730 826 elif kind is SUB: 731 827 directives, substream = data 732 828 current_domain = None 829 current_context = None 733 830 for idx, directive in enumerate(directives): 734 831 # Organize directives to make everything work 735 832 # FIXME: There's got to be a better way to do this! … … 740 837 # Put domain directive as the first one in order to 741 838 # update context before any other directives evaluation 742 839 directives.insert(0, directives.pop(idx)) 840 if isinstance(directive, ContextDirective): 841 # Grab current (msg)context and update context 842 current_context = directive.context 843 ctxt.push({'_i18n.context': current_context}) 844 # Put context directive either first in the case of 845 # no domain, or 2nd in the case there is a domain, to 846 # update context before any other directives evaluation 847 directives.insert(1 if current_domain else 0, 848 directives.pop(idx)) 743 849 744 850 # If this is an i18n directive, no need to translate text 745 851 # nodes here … … 747 853 isinstance(d, ExtractableI18NDirective) 748 854 for d in directives 749 855 ]) 856 750 857 substream = list(self(substream, ctxt, 751 858 translate_text=not is_i18n_directive, 752 859 translate_attrs=translate_attrs)) … … 754 861 755 862 if current_domain: 756 863 ctxt.pop() 864 if current_context: 865 ctxt.pop() 757 866 else: 758 867 yield kind, data, pos 759 868 760 869 def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS, 761 search_text=True, comment_stack=None ):870 search_text=True, comment_stack=None, context_stack=None): 762 871 """Extract localizable strings from the given template stream. 763 872 764 873 For every string found, this function yields a ``(lineno, function, 765 874 message, comments)`` tuple, where: 766 875 767 876 * ``lineno`` is the number of the line on which the string was found, 768 877 * ``function`` is the name of the ``gettext`` function used (if the 769 878 string was extracted from embedded Python code), and … … 772 881 arguments). 773 882 * ``comments`` is a list of comments related to the message, extracted 774 883 from ``i18n:comment`` attributes found in the markup 775 884 776 885 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> 777 886 ... <head> 778 887 ... <title>Example</title> … … 789 898 6, None, u'Example' 790 899 7, '_', u'Hello, %(name)s' 791 900 8, 'ngettext', (u'You have %d item', u'You have %d items', None) 792 901 793 902 :param stream: the event stream to extract strings from; can be a 794 903 regular stream or a template stream 795 904 :param gettext_functions: a sequence of function names that should be … … 797 906 functions 798 907 :param search_text: whether the content of text nodes should be 799 908 extracted (used internally) 800 909 801 910 :note: Changed in 0.4.1: For a function with multiple string arguments 802 911 (such as ``ngettext``), a single item with a tuple of strings is 803 912 yielded, instead an item for each string argument. … … 808 917 search_text = False 809 918 if comment_stack is None: 810 919 comment_stack = [] 920 if context_stack is None: 921 context_stack = [] 811 922 skip = 0 812 923 813 924 xml_lang = XML_NAMESPACE['lang'] … … 834 945 elif not skip and search_text and kind is TEXT: 835 946 text = data.strip() 836 947 if text and [ch for ch in text if ch.isalpha()]: 837 yield pos[1], None, text, comment_stack[-1:] 948 yield contextify(pos[1], None, text, comment_stack[-1:], 949 context_stack[-1:]) 838 950 839 951 elif kind is EXPR or kind is EXEC: 840 952 for funcname, strings in extract_from_code(data, … … 845 957 elif kind is SUB: 846 958 directives, substream = data 847 959 in_comment = False 960 in_context = False 848 961 849 962 for idx, directive in enumerate(directives): 850 963 # Do a first loop to see if there's a comment directive … … 858 971 for message in self.extract( 859 972 substream, gettext_functions, 860 973 search_text=search_text and not skip, 861 comment_stack=comment_stack): 974 comment_stack=comment_stack, 975 context_stack=context_stack): 862 976 yield message 863 977 directives.pop(idx) 978 elif isinstance(directive, ContextDirective): 979 in_context = True 980 context_stack.append(directive.context) 981 if len(directives) == 1: 982 for message in self.extract( 983 substream, gettext_functions, 984 search_text=search_text and not skip, 985 comment_stack=comment_stack, 986 context_stack=context_stack): 987 yield message 988 directives.pop(idx) 864 989 elif not isinstance(directive, I18NDirective): 865 990 # Remove all other non i18n directives from the process 866 991 directives.pop(idx) 867 992 868 if not directives and not in_comment :993 if not directives and not in_comment and not in_context: 869 994 # Extract content if there's no directives because 870 995 # strip was pop'ed and not because comment was pop'ed. 871 996 # Extraction in this case has been taken care of. … … 879 1004 for message in directive.extract(self, 880 1005 substream, gettext_functions, 881 1006 search_text=search_text and not skip, 882 comment_stack=comment_stack): 1007 comment_stack=comment_stack, 1008 context_stack=context_stack): 883 1009 yield message 884 1010 else: 885 1011 for message in self.extract( 886 1012 substream, gettext_functions, 887 1013 search_text=search_text and not skip, 888 comment_stack=comment_stack): 1014 comment_stack=comment_stack, 1015 context_stack=context_stack): 889 1016 yield message 890 1017 891 1018 if in_comment: 892 1019 comment_stack.pop() 893 1020 1021 if in_context: 1022 context_stack.pop() 1023 894 1024 def get_directive_index(self, dir_cls): 895 1025 total = len(self._dir_order) 896 1026 if dir_cls in self._dir_order: … … 900 1030 def setup(self, template): 901 1031 """Convenience function to register the `Translator` filter and the 902 1032 related directives with the given template. 903 1033 904 1034 :param template: a `Template` instance 905 1035 """ 906 1036 template.filters.insert(0, self) … … 922 1052 923 1053 class MessageBuffer(object): 924 1054 """Helper class for managing internationalized mixed content. 925 1055 926 1056 :since: version 0.5 927 1057 """ 928 1058 929 1059 def __init__(self, directive=None): 930 1060 """Initialize the message buffer. 931 1061 932 1062 :param directive: the directive owning the buffer 933 1063 :type directive: I18NDirective 934 1064 """ … … 955 1085 956 1086 def append(self, kind, data, pos): 957 1087 """Append a stream event to the buffer. 958 1088 959 1089 :param kind: the stream event kind 960 1090 :param data: the event data 961 1091 :param pos: the position of the event in the source … … 987 1117 params = "(%s)" % params 988 1118 raise IndexError("%d parameters%s given to 'i18n:%s' but " 989 1119 "%d or more expressions used in '%s', line %s" 990 % (len(self.orig_params), params, 1120 % (len(self.orig_params), params, 991 1121 self.directive.tagname, 992 1122 len(self.orig_params) + 1, 993 1123 os.path.basename(pos[0] or … … 997 1127 self._add_event(self.stack[-1], (kind, data, pos)) 998 1128 self.values[param] = (kind, data, pos) 999 1129 else: 1000 if kind is START: 1130 if kind is START: 1001 1131 self.string.append('[%d:' % self.order) 1002 1132 self.stack.append(self.order) 1003 1133 self._add_event(self.stack[-1], (kind, data, pos)) … … 1019 1149 def translate(self, string, regex=re.compile(r'%\((\w+)\)s')): 1020 1150 """Interpolate the given message translation with the events in the 1021 1151 buffer and return the translated stream. 1022 1152 1023 1153 :param string: the translated message string 1024 1154 """ 1025 1155 substream = None … … 1108 1238 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')): 1109 1239 """Parse a translated message using Genshi mixed content message 1110 1240 formatting. 1111 1241 1112 1242 >>> parse_msg("See [1:Help].") 1113 1243 [(0, 'See '), (1, 'Help'), (0, '.')] 1114 1244 1115 1245 >>> parse_msg("See [1:our [2:Help] page] for details.") 1116 1246 [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')] 1117 1247 1118 1248 >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].") 1119 1249 [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')] 1120 1250 1121 1251 >>> parse_msg("[1:] Bilder pro Seite anzeigen.") 1122 1252 [(1, ''), (0, ' Bilder pro Seite anzeigen.')] 1123 1253 1124 1254 :param string: the translated message string 1125 1255 :return: a list of ``(order, string)`` tuples 1126 1256 :rtype: `list` … … 1152 1282 1153 1283 def extract_from_code(code, gettext_functions): 1154 1284 """Extract strings from Python bytecode. 1155 1285 1156 1286 >>> from genshi.template.eval import Expression 1157 1287 >>> expr = Expression('_("Hello")') 1158 1288 >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS)) 1159 1289 [('_', u'Hello')] 1160 1290 1161 1291 >>> expr = Expression('ngettext("You have %(num)s item", ' 1162 1292 ... '"You have %(num)s items", num)') 1163 1293 >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS)) 1164 1294 [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))] 1165 1295 1166 1296 :param code: the `Code` object 1167 1297 :type code: `genshi.template.eval.Code` 1168 1298 :param gettext_functions: a sequence of function names … … 1202 1332 1203 1333 def extract(fileobj, keywords, comment_tags, options): 1204 1334 """Babel extraction method for Genshi templates. 1205 1335 1206 1336 :param fileobj: the file-like object the messages should be extracted from 1207 1337 :param keywords: a list of keywords (i.e. function names) that should be 1208 1338 recognized as translation functions -
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
