Edgewall Software

Ticket #580: msgctxt.3.patch

File msgctxt.3.patch, 43.2 KB (added by hodgestar, 10 years ago)

Update of msgctxt.2.patch to work with trunk (i.e. 0.8.x).

  • doc/i18n.txt

     
    99localizable strings from templates, as well as a template filter and special
    1010directives that can apply translations to templates as they get rendered.
    1111
    12 This support is based on `gettext`_ message catalogs and the `gettext Python 
     12This support is based on `gettext`_ message catalogs and the `gettext Python
    1313module`_. The extraction process can be used from the API level, or through
    1414the front-ends implemented by the `Babel`_ project, for which Genshi provides
    1515a plugin.
     
    3939However, this approach results in significant “character noise” in templates,
    4040making them harder to read and preview.
    4141
    42 The ``genshi.filters.Translator`` filter allows you to get rid of the 
     42The ``genshi.filters.Translator`` filter allows you to get rid of the
    4343explicit `gettext`_ function calls, so you can (often) just continue to write:
    4444
    4545.. code-block:: genshi
     
    5454          corresponding ``gettext`` function in embedded Python expressions.
    5555
    5656You 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 
     57doesn't really make sense to translate the content of the HTML
    5858``<script></script>`` element. Both ``<script>`` and ``<style>`` are excluded
    5959by default.
    6060
    61 Attribute values can also be automatically translated. The default is to 
     61Attribute values can also be automatically translated. The default is to
    6262consider the attributes ``abbr``, ``alt``, ``label``, ``prompt``, ``standby``,
    6363``summary``, and ``title``, which is a list that makes sense for HTML
    6464documents.  Of course, you can tell the translator to use a different set of
     
    7777  <p xml:lang="en">Hello, world!</p>
    7878
    7979On 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 
     80expression, the element contents and attributes are still considered for
    8181automatic translation:
    8282
    8383.. code-block:: genshi
     
    337337  </div>
    338338
    339339
     340``i18n.ctxt``
     341-------------
     342
     343Sometimes a source string can have two different meanings. Without resorting to
     344splitting these two occurrences into different domains, gettext provides a
     345means to specify a *context* for each translatable string. For instance, the
     346word "volunteer" can either mean the noun, one who volunteers, or the verb,
     347to volunteer.
     348
     349The ``i18n:ctxt`` directive allows you to mark a scope with a particular
     350context. 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
    340359Extraction
    341360==========
    342361
    343362The ``Translator`` class provides a class method called ``extract``, which is
    344 a generator yielding all localizable strings found in a template or markup 
     363a generator yielding all localizable strings found in a template or markup
    345364stream. This includes both literal strings in text nodes and attribute values,
    346365as well as strings in ``gettext()`` calls in embedded Python code. See the API
    347366documentation for details on how to use this method directly.
     
    351370-----------------
    352371
    353372This 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 
     373by the `Babel`_ project. Babel provides a command-line interface as well as
     374commands that can be used from ``setup.py`` scripts using `Setuptools`_ or
    356375`Distutils`_.
    357376
    358377.. _`setuptools`: http://peak.telecommunity.com/DevCenter/setuptools
    359378.. _`distutils`: http://docs.python.org/dist/dist.html
    360379
    361 The first thing you need to do to make Babel extract messages from Genshi 
     380The first thing you need to do to make Babel extract messages from Genshi
    362381templates is to let Babel know which files are Genshi templates. This is done
    363382using a “mapping configuration”, which can be stored in a configuration file,
    364383or specified directly in your ``setup.py``.
     
    407426
    408427``include_attrs``
    409428-----------------
    410 Comma-separated list of attribute names that should be considered to have 
     429Comma-separated list of attribute names that should be considered to have
    411430localizable values. Only used for markup templates.
    412431
    413432``ignore_tags``
    414433---------------
    415 Comma-separated list of tag names that should be ignored. Only used for markup 
     434Comma-separated list of tag names that should be ignored. Only used for markup
    416435templates.
    417436
    418437``extract_text``
    419438----------------
    420439Whether text outside explicit ``gettext`` function calls should be extracted.
    421440By 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 
     441the ``include_attrs`` list are extracted. If this option is disabled, only
    423442strings in ``gettext`` function calls are extracted.
    424443
    425444.. note:: If you disable this option, and do not make use of the
     
    446465
    447466  from genshi.filters import Translator
    448467  from genshi.template import MarkupTemplate
    449  
     468
    450469  template = MarkupTemplate("...")
    451470  template.filters.insert(0, Translator(translations.ugettext))
    452471
     
    457476
    458477  from genshi.filters import Translator
    459478  from genshi.template import MarkupTemplate
    460  
     479
    461480  template = MarkupTemplate("...")
    462481  translator = Translator(translations.ugettext)
    463482  translator.setup(template)
     
    473492Related Considerations
    474493======================
    475494
    476 If you intend to produce an application that is fully prepared for an 
     495If you intend to produce an application that is fully prepared for an
    477496international audience, there are a couple of other things to keep in mind:
    478497
    479498-------
     
    482501
    483502Use ``unicode`` internally, not encoded bytestrings. Only encode/decode where
    484503data 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 
     504and not just with bytes, which is an important distinction for example when
    486505calculating the length of a piece of text. When you need to decode/encode, it's
    487506probably a good idea to use UTF-8.
    488507
     
    490509Date and Time
    491510-------------
    492511
    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 
     512If your application uses datetime information that should be displayed to users
     513in different timezones, you should try to work with UTC (universal time)
     514internally. Do the conversion from and to "local time" when the data enters or
     515exits the system. Make use the Python `datetime`_ module and the third-party
    497516`pytz`_ package.
    498517
    499518--------------------------
    500519Formatting and Locale Data
    501520--------------------------
    502521
    503 Make sure you check out the functionality provided by the `Babel`_ project for 
     522Make sure you check out the functionality provided by the `Babel`_ project for
    504523things like number and date formatting, locale display strings, etc.
    505524
    506525.. _`datetime`: http://docs.python.org/lib/module-datetime.html
  • genshi/filters/i18n.py

     
    2222    any
    2323except NameError:
    2424    from genshi.util import any
     25from functools import partial
    2526from gettext import NullTranslations
    2627import os
    2728import re
    2829from types import FunctionType
    2930
    30 from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
    31                         XML_NAMESPACE, _ensure, StreamEventKind
     31from genshi.core import (
     32    Attrs, Namespace, QName, START, END, TEXT,
     33    XML_NAMESPACE, _ensure, StreamEventKind)
    3234from genshi.template.eval import _ast
    3335from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
    3436from genshi.template.directives import Directive, StripDirective
     
    6062    """Simple interface for directives to support messages extraction."""
    6163
    6264    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):
    6466        raise NotImplementedError
    6567
    6668
     69contexted = {
     70    None: 'pgettext',
     71    'gettext': 'pgettext',
     72    'ngettext': 'pngettext',
     73    'dgettext': 'dpgettext',
     74    'dngettext': 'dnpgettext'
     75}
     76
     77
     78def 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
    6791class CommentDirective(I18NDirective):
    6892    """Implementation of the ``i18n:comment`` template directive which adds
    6993    translation comments.
    70    
     94
    7195    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    7296    ...   <p i18n:comment="As in Foo Bar">Foo</p>
    7397    ... </html>''')
     
    87111class MsgDirective(ExtractableI18NDirective):
    88112    r"""Implementation of the ``i18n:msg`` directive which marks inner content
    89113    as translatable. Consider the following examples:
    90    
     114
    91115    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    92116    ...   <div i18n:msg="">
    93117    ...     <p>Foo</p>
     
    95119    ...   </div>
    96120    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
    97121    ... </html>''')
    98    
     122
    99123    >>> translator = Translator()
    100124    >>> translator.setup(tmpl)
    101125    >>> list(translator.extract(tmpl.stream))
     
    155179
    156180    def __call__(self, stream, directives, ctxt, **vars):
    157181        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'):
    159190            dgettext = ctxt.get('_i18n.dgettext')
    160191            assert hasattr(dgettext, '__call__'), \
    161192                'No domain gettext function passed'
    162193            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)
    163199
    164200        def _generate():
    165201            msgbuf = MessageBuffer(self)
     
    183219        return _apply_directives(_generate(), directives, ctxt, vars)
    184220
    185221    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):
    187223        msgbuf = MessageBuffer(self)
    188224        strip = False
    189225
     
    207243        if not strip:
    208244            msgbuf.append(*previous)
    209245
    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:])
    211248
    212249
    213250class ChooseBranchDirective(I18NDirective):
     
    244281        ctxt['_i18n.choose.%s' % self.tagname] = msgbuf
    245282
    246283    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):
    248286        stream = iter(stream)
    249287        previous = stream.next()
    250288
     
    282320class ChooseDirective(ExtractableI18NDirective):
    283321    """Implementation of the ``i18n:choose`` directive which provides plural
    284322    internationalisation of strings.
    285    
     323
    286324    This directive requires at least one parameter, the one which evaluates to
    287325    an integer which will allow to choose the plural/singular form. If you also
    288326    have expressions inside the singular and plural version of the string you
    289327    also need to pass a name for those parameters. Consider the following
    290328    examples:
    291    
     329
    292330    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    293331    ...   <div i18n:choose="num; num">
    294332    ...     <p i18n:singular="">There is $num coin</p>
     
    362400
    363401        ngettext = ctxt.get('_i18n.ngettext')
    364402        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)
    365406        dngettext = ctxt.get('_i18n.dngettext')
    366407        if not dngettext:
    367408            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)
    368412
    369413        new_stream = []
    370414        singular_stream = None
     
    395439            else:
    396440                new_stream.append(event)
    397441
    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'):
    399450            ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'),
    400451                                                 s, p, n)
    401452
     
    424475        ctxt.pop()
    425476
    426477    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):
    428479        strip = False
    429480        stream = iter(stream)
    430481        previous = stream.next()
     
    448499                    if isinstance(directive, SingularDirective):
    449500                        for message in directive.extract(translator,
    450501                                substream, gettext_functions, search_text,
    451                                 comment_stack, msgbuf=singular_msgbuf):
     502                                comment_stack, context_stack, msgbuf=singular_msgbuf):
    452503                            yield message
    453504                    elif isinstance(directive, PluralDirective):
    454505                        for message in directive.extract(translator,
    455506                                substream, gettext_functions, search_text,
    456                                 comment_stack, msgbuf=plural_msgbuf):
     507                                comment_stack, context_stack, msgbuf=plural_msgbuf):
    457508                            yield message
    458509                    elif not isinstance(directive, StripDirective):
    459510                        singular_msgbuf.append(*previous)
     
    472523            singular_msgbuf.append(*previous)
    473524            plural_msgbuf.append(*previous)
    474525
    475         yield self.lineno, 'ngettext', \
     526        yield contextify(self.lineno, 'ngettext', \
    476527            (singular_msgbuf.format(), plural_msgbuf.format()), \
    477             comment_stack[-1:]
     528                         comment_stack[-1:], context_stack[-1:])
    478529
    479530    def _is_plural(self, numeral, ngettext):
    480531        # XXX: should we test which form was chosen like this!?!?!?
     
    488539class DomainDirective(I18NDirective):
    489540    """Implementation of the ``i18n:domain`` directive which allows choosing
    490541    another i18n domain(catalog) to translate from.
    491    
     542
    492543    >>> from genshi.filters.tests.i18n import DummyTranslations
    493544    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    494545    ...   <p i18n:msg="">Bar</p>
     
    540591        ctxt.pop()
    541592
    542593
     594class 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
    543616class Translator(DirectiveFactory):
    544617    """Can extract and translate localizable strings from markup streams and
    545618    templates.
    546    
     619
    547620    For example, assume the following template:
    548    
     621
    549622    >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    550623    ...   <head>
    551624    ...     <title>Example</title>
     
    555628    ...     <p>${_("Hello, %(name)s") % dict(name=username)}</p>
    556629    ...   </body>
    557630    ... </html>''', filename='example.html')
    558    
     631
    559632    For demonstration, we define a dummy ``gettext``-style function with a
    560633    hard-coded translation table, and pass that to the `Translator` initializer:
    561    
     634
    562635    >>> def pseudo_gettext(string):
    563636    ...     return {
    564637    ...         'Example': 'Beispiel',
    565638    ...         'Hello, %(name)s': 'Hallo, %(name)s'
    566639    ...     }[string]
    567640    >>> translator = Translator(pseudo_gettext)
    568    
     641
    569642    Next, the translator needs to be prepended to any already defined filters
    570643    on the template:
    571    
     644
    572645    >>> tmpl.filters.insert(0, translator)
    573    
     646
    574647    When generating the template output, our hard-coded translations should be
    575648    applied as expected:
    576    
     649
    577650    >>> print(tmpl.generate(username='Hans', _=pseudo_gettext))
    578651    <html>
    579652      <head>
     
    584657        <p>Hallo, Hans</p>
    585658      </body>
    586659    </html>
    587    
     660
    588661    Note that elements defining ``xml:lang`` attributes that do not contain
    589662    variable expressions are ignored by this filter. That can be used to
    590663    exclude specific parts of a template from being extracted and translated.
     
    593666    directives = [
    594667        ('domain', DomainDirective),
    595668        ('comment', CommentDirective),
     669        ('ctxt', ContextDirective),
    596670        ('msg', MsgDirective),
    597671        ('choose', ChooseDirective),
    598672        ('singular', SingularDirective),
     
    611685    def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
    612686                 include_attrs=INCLUDE_ATTRS, extract_text=True):
    613687        """Initialize the translator.
    614        
     688
    615689        :param translate: the translation function, for example ``gettext`` or
    616690                          ``ugettext``.
    617691        :param ignore_tags: a set of tag names that should not be localized
     
    619693        :param extract_text: whether the content of text nodes should be
    620694                             extracted, or only text in explicit ``gettext``
    621695                             function calls
    622        
     696
    623697        :note: Changed in 0.6: the `translate` parameter can now be either
    624698               a ``gettext``-style function, or an object compatible with the
    625699               ``NullTransalations`` or ``GNUTranslations`` interface
     
    632706    def __call__(self, stream, ctxt=None, translate_text=True,
    633707                 translate_attrs=True):
    634708        """Translate any localizable strings in the given stream.
    635        
     709
    636710        This function shouldn't be called directly. Instead, an instance of
    637711        the `Translator` class should be registered as a filter with the
    638712        `Template` or the `TemplateLoader`, or applied as a regular stream
    639713        filter. If used as a template filter, it should be inserted in front of
    640714        all the default filters.
    641        
     715
    642716        :param stream: the markup event stream
    643717        :param ctxt: the template context (not used)
    644718        :param translate_text: whether text nodes should be translated (used
     
    676750            except AttributeError:
    677751                dgettext = lambda _, y: gettext(y)
    678752                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)
    679769            if ctxt:
    680770                ctxt['_i18n.gettext'] = gettext
    681771                ctxt['_i18n.ngettext'] = ngettext
    682772                ctxt['_i18n.dgettext'] = dgettext
    683773                ctxt['_i18n.dngettext'] = dngettext
     774                ctxt['_i18n.pgettext'] = pgettext
     775                ctxt['_i18n.npgettext'] = npgettext
     776                ctxt['_i18n.dpgettext'] = dpgettext
     777                ctxt['_i18n.dnpgettext'] = dnpgettext
    684778
    685779        if ctxt and ctxt.get('_i18n.domain'):
    686780            # TODO: This can cause infinite recursion if dgettext is defined
    687781            #       via the AttributeError case above!
    688             gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
     782            gettext = partial(dgettext, ctxt.get('_i18n.domain'))
    689783
     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
    690792        for kind, data, pos in stream:
    691793
    692794            # skip chunks that should not be localized
     
    737839            elif kind is SUB:
    738840                directives, substream = data
    739841                current_domain = None
     842                current_context = None
    740843                for idx, directive in enumerate(directives):
    741844                    # Organize directives to make everything work
    742845                    # FIXME: There's got to be a better way to do this!
     
    747850                        # Put domain directive as the first one in order to
    748851                        # update context before any other directives evaluation
    749852                        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))
    750862
    751863                # If this is an i18n directive, no need to translate text
    752864                # nodes here
     
    754866                    isinstance(d, ExtractableI18NDirective)
    755867                    for d in directives
    756868                ])
     869
    757870                substream = list(self(substream, ctxt,
    758871                                      translate_text=not is_i18n_directive,
    759872                                      translate_attrs=translate_attrs))
     
    761874
    762875                if current_domain:
    763876                    ctxt.pop()
     877                if current_context:
     878                    ctxt.pop()
    764879            else:
    765880                yield kind, data, pos
    766881
    767882    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):
    769884        """Extract localizable strings from the given template stream.
    770        
     885
    771886        For every string found, this function yields a ``(lineno, function,
    772887        message, comments)`` tuple, where:
    773        
     888
    774889        * ``lineno`` is the number of the line on which the string was found,
    775890        * ``function`` is the name of the ``gettext`` function used (if the
    776891          string was extracted from embedded Python code), and
     
    779894           arguments).
    780895        *  ``comments`` is a list of comments related to the message, extracted
    781896           from ``i18n:comment`` attributes found in the markup
    782        
     897
    783898        >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
    784899        ...   <head>
    785900        ...     <title>Example</title>
     
    796911        6, None, u'Example'
    797912        7, '_', u'Hello, %(name)s'
    798913        8, 'ngettext', (u'You have %d item', u'You have %d items', None)
    799        
     914
    800915        :param stream: the event stream to extract strings from; can be a
    801916                       regular stream or a template stream
    802917        :param gettext_functions: a sequence of function names that should be
     
    804919                                  functions
    805920        :param search_text: whether the content of text nodes should be
    806921                            extracted (used internally)
    807        
     922
    808923        :note: Changed in 0.4.1: For a function with multiple string arguments
    809924               (such as ``ngettext``), a single item with a tuple of strings is
    810925               yielded, instead an item for each string argument.
     
    815930            search_text = False
    816931        if comment_stack is None:
    817932            comment_stack = []
     933        if context_stack is None:
     934            context_stack = []
    818935        skip = 0
    819936
    820937        xml_lang = XML_NAMESPACE['lang']
     
    841958            elif not skip and search_text and kind is TEXT:
    842959                text = data.strip()
    843960                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:])
    845963
    846964            elif kind is EXPR or kind is EXEC:
    847965                for funcname, strings in extract_from_code(data,
     
    852970            elif kind is SUB:
    853971                directives, substream = data
    854972                in_comment = False
     973                in_context = False
    855974
    856975                for idx, directive in enumerate(directives):
    857976                    # Do a first loop to see if there's a comment directive
     
    865984                            for message in self.extract(
    866985                                    substream, gettext_functions,
    867986                                    search_text=search_text and not skip,
    868                                     comment_stack=comment_stack):
     987                                    comment_stack=comment_stack,
     988                                    context_stack=context_stack):
    869989                                yield message
    870990                        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)
    8711002                    elif not isinstance(directive, I18NDirective):
    8721003                        # Remove all other non i18n directives from the process
    8731004                        directives.pop(idx)
    8741005
    875                 if not directives and not in_comment:
     1006                if not directives and not in_comment and not in_context:
    8761007                    # Extract content if there's no directives because
    8771008                    # strip was pop'ed and not because comment was pop'ed.
    8781009                    # Extraction in this case has been taken care of.
     
    8861017                        for message in directive.extract(self,
    8871018                                substream, gettext_functions,
    8881019                                search_text=search_text and not skip,
    889                                 comment_stack=comment_stack):
     1020                                comment_stack=comment_stack,
     1021                                context_stack=context_stack):
    8901022                            yield message
    8911023                    else:
    8921024                        for message in self.extract(
    8931025                                substream, gettext_functions,
    8941026                                search_text=search_text and not skip,
    895                                 comment_stack=comment_stack):
     1027                                comment_stack=comment_stack,
     1028                                context_stack=context_stack):
    8961029                            yield message
    8971030
    8981031                if in_comment:
    8991032                    comment_stack.pop()
    9001033
     1034                if in_context:
     1035                    context_stack.pop()
     1036
    9011037    def get_directive_index(self, dir_cls):
    9021038        total = len(self._dir_order)
    9031039        if dir_cls in self._dir_order:
     
    9071043    def setup(self, template):
    9081044        """Convenience function to register the `Translator` filter and the
    9091045        related directives with the given template.
    910        
     1046
    9111047        :param template: a `Template` instance
    9121048        """
    9131049        template.filters.insert(0, self)
     
    9291065
    9301066class MessageBuffer(object):
    9311067    """Helper class for managing internationalized mixed content.
    932    
     1068
    9331069    :since: version 0.5
    9341070    """
    9351071
    9361072    def __init__(self, directive=None):
    9371073        """Initialize the message buffer.
    938        
     1074
    9391075        :param directive: the directive owning the buffer
    9401076        :type directive: I18NDirective
    9411077        """
     
    9621098
    9631099    def append(self, kind, data, pos):
    9641100        """Append a stream event to the buffer.
    965        
     1101
    9661102        :param kind: the stream event kind
    9671103        :param data: the event data
    9681104        :param pos: the position of the event in the source
     
    9941130                    params = "(%s)" % params
    9951131                raise IndexError("%d parameters%s given to 'i18n:%s' but "
    9961132                                 "%d or more expressions used in '%s', line %s"
    997                                  % (len(self.orig_params), params, 
     1133                                 % (len(self.orig_params), params,
    9981134                                    self.directive.tagname,
    9991135                                    len(self.orig_params) + 1,
    10001136                                    os.path.basename(pos[0] or
     
    10041140            self._add_event(self.stack[-1], (kind, data, pos))
    10051141            self.values[param] = (kind, data, pos)
    10061142        else:
    1007             if kind is START: 
     1143            if kind is START:
    10081144                self.string.append('[%d:' % self.order)
    10091145                self.stack.append(self.order)
    10101146                self._add_event(self.stack[-1], (kind, data, pos))
     
    10261162    def translate(self, string, regex=re.compile(r'%\((\w+)\)s')):
    10271163        """Interpolate the given message translation with the events in the
    10281164        buffer and return the translated stream.
    1029        
     1165
    10301166        :param string: the translated message string
    10311167        """
    10321168        substream = None
     
    11211257def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')):
    11221258    """Parse a translated message using Genshi mixed content message
    11231259    formatting.
    1124    
     1260
    11251261    >>> parse_msg("See [1:Help].")
    11261262    [(0, 'See '), (1, 'Help'), (0, '.')]
    1127    
     1263
    11281264    >>> parse_msg("See [1:our [2:Help] page] for details.")
    11291265    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
    1130    
     1266
    11311267    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
    11321268    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
    1133    
     1269
    11341270    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
    11351271    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
    1136    
     1272
    11371273    :param string: the translated message string
    11381274    :return: a list of ``(order, string)`` tuples
    11391275    :rtype: `list`
     
    11651301
    11661302def extract_from_code(code, gettext_functions):
    11671303    """Extract strings from Python bytecode.
    1168    
     1304
    11691305    >>> from genshi.template.eval import Expression
    11701306    >>> expr = Expression('_("Hello")')
    11711307    >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
    11721308    [('_', u'Hello')]
    1173    
     1309
    11741310    >>> expr = Expression('ngettext("You have %(num)s item", '
    11751311    ...                            '"You have %(num)s items", num)')
    11761312    >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
    11771313    [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
    1178    
     1314
    11791315    :param code: the `Code` object
    11801316    :type code: `genshi.template.eval.Code`
    11811317    :param gettext_functions: a sequence of function names
     
    12171353
    12181354def extract(fileobj, keywords, comment_tags, options):
    12191355    """Babel extraction method for Genshi templates.
    1220    
     1356
    12211357    :param fileobj: the file-like object the messages should be extracted from
    12221358    :param keywords: a list of keywords (i.e. function names) that should be
    12231359                     recognized as translation functions
  • genshi/filters/tests/i18n.py

     
    8282
    8383    if IS_PYTHON2:
    8484        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)
    8687    else:
    8788        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)
    8991
     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)
    9099
     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
    91134class TranslatorTestCase(unittest.TestCase):
    92135
    93136    def test_translate_included_attribute_text(self):
     
    14511494            <p>Vohs John Doe</p>
    14521495          </div>
    14531496        </html>""", tmpl.generate(two=2, fname='John', lname='Doe').render())
    1454        
     1497
    14551498    def test_translate_i18n_choose_and_singular_with_py_strip(self):
    14561499        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
    14571500            xmlns:i18n="http://genshi.edgewall.org/i18n">
     
    14811524          </div>
    14821525        </html>""", tmpl.generate(
    14831526            one=1, two=2, fname='John',lname='Doe').render())
    1484        
     1527
    14851528    def test_translate_i18n_choose_and_plural_with_py_strip(self):
    14861529        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
    14871530            xmlns:i18n="http://genshi.edgewall.org/i18n">
     
    20042047            (34, '_', 'Update', [])], messages)
    20052048
    20062049
     2050class 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
    20072146def suite():
    20082147    suite = unittest.TestSuite()
    20092148    suite.addTest(doctest.DocTestSuite(Translator.__module__))
     
    20122151    suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test'))
    20132152    suite.addTest(unittest.makeSuite(DomainDirectiveTestCase, 'test'))
    20142153    suite.addTest(unittest.makeSuite(ExtractTestCase, 'test'))
     2154    suite.addTest(unittest.makeSuite(ContextDirectiveTestCase, 'test'))
    20152155    return suite
    20162156
    20172157if __name__ == '__main__':
  • examples/bench/bigtable.py

     
    1010import timeit
    1111from StringIO import StringIO
    1212from genshi.builder import tag
     13from genshi.filters.i18n import Translator
     14from genshi.filters.tests.i18n import DummyTranslations
    1315from genshi.template import MarkupTemplate, NewTextTemplate
    1416
    1517try:
     
    5658</table>
    5759""")
    5860
     61genshi_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""")
     69t = Translator(DummyTranslations())
     70t.setup(genshi_tmpl_i18n)
     71
    5972genshi_tmpl2 = MarkupTemplate("""
    6073<table xmlns:py="http://genshi.edgewall.org/">$table</table>
    6174""")
     
    103116    stream = genshi_tmpl.generate(table=table)
    104117    stream.render('html', strip_whitespace=False)
    105118
     119def test_genshi_i18n():
     120    """Genshi template w/ i18n"""
     121    stream = genshi_tmpl_i18n.generate(table=table)
     122    stream.render('html', strip_whitespace=False)
     123
    106124def test_genshi_text():
    107125    """Genshi text template"""
    108126    stream = genshi_text_tmpl.generate(table=table)
     
    167185        et.tostring(_table)
    168186
    169187if cet:
    170     def test_cet(): 
     188    def test_cet():
    171189        """cElementTree"""
    172190        _table = cet.Element('table')
    173191        for row in table:
     
    196214
    197215
    198216def 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',
    200218             'test_genshi_builder', 'test_mako', 'test_kid', 'test_kid_et',
    201219             'test_et', 'test_cet', 'test_clearsilver', 'test_django']
    202220