Edgewall Software

Ticket #284: genshi_i18n_custom_format.patch

File genshi_i18n_custom_format.patch, 36.4 KB (added by palgarvio, 14 years ago)
  • genshi/filters/i18n.py

    diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py
    a b  
    1919"""
    2020
    2121from gettext import NullTranslations
    22 import os
    2322import re
    2423from types import FunctionType
    2524
     25try:
     26    from babel.messages.catalog import PYTHON_FORMAT
     27except ImportError:
     28    PYTHON_FORMAT = None
     29
    2630from genshi.core import Attrs, Namespace, QName, START, END, TEXT, START_NS, \
    2731                        END_NS, XML_NAMESPACE, _ensure, StreamEventKind
    2832from genshi.template.eval import _ast
     
    6872    >>> translator = Translator()
    6973    >>> translator.setup(tmpl)
    7074    >>> list(translator.extract(tmpl.stream))
    71     [(2, None, u'Foo', [u'As in Foo Bar'])]
     75    [(2, None, u'Foo', {'comments': [u'As in Foo Bar']})]
    7276    >>>
    7377    """
    7478
     
    9599
    96100    >>> translator = Translator()
    97101    >>> translator.setup(tmpl)
    98     >>> list(translator.extract(tmpl.stream))
    99     [(2, None, u'[1:Foo]\n    [2:Bar]', []), (6, None, u'Foo [1:bar]!', [])]
     102    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
     103    [(2, None, u'[1:Foo]\n    [2:Bar]', {'flags': ['genshi-format'],
     104                                         'comments': []}),
     105     (6, None, u'Foo [1:bar]!', {'flags': ['genshi-format'],
     106                                 'comments': []})]
    100107    >>> print tmpl.generate().render()
    101108    <html>
    102109      <div><p>Foo</p>
     
    113120    ... </html>''')
    114121    >>> translator.setup(tmpl)
    115122    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
    116     [(2, None, u'[1:First Name: %(fname)s]\n    [2:Last Name: %(lname)s]', []),
    117     (6, None, u'Foo [1:bar]!', [])]
     123    [(2, None, u'[1:First Name: %(fname)s]\n    [2:Last Name: %(lname)s]',
     124     {'flags': ['genshi-format', 'python-format'], 'comments': []}),
     125    (6, None, u'Foo [1:bar]!', {'flags': ['genshi-format'], 'comments': []})]
    118126    >>>
    119127    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    120128    ...   <div i18n:msg="fname, lname">
     
    193201            msgbuf.append(*previous)
    194202
    195203        if msgbuf.valid:
    196             yield (None, msgbuf.format(),
    197                    filter(None, [ctxt.get('_i18n.comment')]))
     204            message = msgbuf.format()
     205            info = {
     206                'comments': filter(None, [ctxt.get('_i18n.comment')])
     207            }
     208            if len(parse_msg(message)) > 1:
     209                info['flags'] = ['genshi-format']
     210            if PYTHON_FORMAT and bool(filter(None,
     211                                             [PYTHON_FORMAT.search(id) for id in
     212                                              [message] if id])):
     213                info.setdefault('flags', []).append('python-format')
     214            yield None, message, info
    198215
    199216
    200217class InnerChooseDirective(I18NDirective):
     
    264281    >>> translator.setup(tmpl)
    265282    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
    266283    [(2, 'ngettext', (u'There is %(num)s coin',
    267                       u'There are %(num)s coins'), [])]
     284                      u'There are %(num)s coins'),
     285                      {'flags': ['python-format'], 'comments': []})]
    268286    >>>
    269287    >>> tmpl = MarkupTemplate('''\
    270288        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     
    299317    >>> translator.setup(tmpl)
    300318    >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
    301319    [(2, 'ngettext', (u'There is %(num)s coin',
    302                       u'There are %(num)s coins'), [])]
     320                      u'There are %(num)s coins'),
     321                      {'flags': ['python-format'], 'comments': []})]
    303322    >>>
    304323    """
    305324
     
    410429                singular_msgbuf.append(kind, event, pos)
    411430                plural_msgbuf.append(kind, event, pos)
    412431        if singular_msgbuf.valid and plural_msgbuf.valid:
    413             yield 'ngettext', \
    414                 (singular_msgbuf.format(), plural_msgbuf.format()), \
    415                 filter(None, [ctxt.get('_i18n.comment')])
     432            singular = singular_msgbuf.format()
     433            plural = plural_msgbuf.format()
     434            info = {'comments': filter(None, [ctxt.get('_i18n.comment')])}
     435            if (len(parse_msg(singular)) or len(parse_msg(plural))) > 1:
     436                info.setdefault('flags', []).append('genshi-format')
     437            if PYTHON_FORMAT and bool(filter(None,
     438                                             [PYTHON_FORMAT.search(id) for id in
     439                                              [singular, plural] if id])):
     440                info.setdefault('flags', []).append('python-format')
     441            yield 'ngettext', (singular, plural), info
    416442
    417443
    418444class DomainDirective(I18NDirective):
     
    728754        """Extract localizable strings from the given template stream.
    729755
    730756        For every string found, this function yields a ``(lineno, function,
    731         message, comments)`` tuple, where:
     757        message, info)`` tuple, where:
    732758
    733759        * ``lineno`` is the number of the line on which the string was found,
    734760        * ``function`` is the name of the ``gettext`` function used (if the
    735761          string was extracted from embedded Python code), and
    736         *  ``message`` is the string itself (a ``unicode`` object, or a tuple
    737            of ``unicode`` objects for functions with multiple string
    738            arguments).
    739         *  ``comments`` is a list of comments related to the message, extracted
    740            from ``i18n:comment`` attributes found in the markup
     762        * ``message`` is the string itself (a ``unicode`` object, or a tuple
     763          of ``unicode`` objects for functions with multiple string
     764          arguments).
     765        * `info`` is a dictionary that contains additional information about the
     766          message being extracted like:
     767              `comments`: A list of comments embedded in the source code;
     768                          With genshi you define the comments to be extracted
     769                          from the ``i18n:comment`` attributes found in the
     770                          markup;
     771              `context`: A string containing the message context; Not yet used
     772                         in genshi.
     773               `flags`: A list of flags used on the message, ie,
     774                       ``python-format``, etc, or even a custom flag,
     775                       ``genshi-format`` for genshi nested markup;
    741776
    742777        >>> from genshi.template import MarkupTemplate
    743778        >>>
     
    752787        ...   </body>
    753788        ... </html>''', filename='example.html')
    754789        >>>
    755         >>> for line, func, msg, comments in Translator().extract(tmpl.stream):
    756         ...    print "%d, %r, %r" % (line, func, msg)
    757         3, None, u'Example'
    758         6, None, u'Example'
    759         7, '_', u'Hello, %(name)s'
    760         8, 'ngettext', (u'You have %d item', u'You have %d items', None)
     790        >>> for line, func, msg, info in Translator().extract(tmpl.stream):
     791        ...    print "%d, %r, %r, %r" % (
     792        ...             line, func,msg, info) #doctest: +NORMALIZE_WHITESPACE
     793        3, None, u'Example', {'comments': []}
     794        6, None, u'Example', {'comments': []}
     795        7, '_', u'Hello, %(name)s', {'flags': ['python-format'], 'comments': []}
     796        8, 'ngettext', (u'You have %d item', u'You have %d items', None),
     797                                    {'flags': ['python-format'], 'comments': []}
    761798
    762799        :param stream: the event stream to extract strings from; can be a
    763800                       regular stream or a template stream
     
    807844                            text = value.strip()
    808845                            if text:
    809846                                # XXX: Do we need to grab i18n:comment from ctxt ???
    810                                 yield pos[1], None, text, []
     847                                yield pos[1], None, text, {'comments': []}
    811848                    else:
    812                         for lineno, funcname, text, comments in self.extract(
    813                                             _ensure(value), gettext_functions,
    814                                             search_text=False,
    815                                             error_callback=error_callback):
    816                             yield lineno, funcname, text, comments
     849                        for lineno, funcname, text, info in \
     850                            self.extract(_ensure(value), gettext_functions,
     851                                        search_text=False,
     852                                        error_callback=error_callback):
     853                            yield lineno, funcname, text, info
    817854
    818855                if msgbuf:
    819856                    msgbuf.append(kind, data, pos)
     
    822859                if not msgbuf:
    823860                    text = data.strip()
    824861                    if text and filter(None, [ch.isalpha() for ch in text]):
    825                         yield pos[1], None, text, \
    826                                     filter(None, [ctxt.get('_i18n.comment')])
     862                        yield pos[1], None, text, {
     863                            'comments': filter(None,
     864                                               [ctxt.get('_i18n.comment')])}
    827865                else:
    828866                    msgbuf.append(kind, data, pos)
    829867
    830868            elif not skip and msgbuf and kind is END:
    831869                msgbuf.append(kind, data, pos)
    832870                if not msgbuf.depth and msgbuf.valid:
    833                     yield msgbuf.lineno, None, msgbuf.format(), \
    834                                                   filter(None, [msgbuf.comment])
     871                    message = msgbuf.format()
     872                    info = {'comments': filter(None, [msgbuf.comment])}
     873                    if len(message) > 1:
     874                        singular, plural = message
     875                        if (len(parse_msg(singular)) or
     876                                                    len(parse_msg(plural))) > 1:
     877                            info.setdefault('flags', []).append('genshi-format')
     878                        if PYTHON_FORMAT and bool(filter(
     879                            None, [PYTHON_FORMAT.search(id)
     880                                   for id in [message] if id])):
     881                            info.setdefault('flags', []).append('python-format')
     882                    elif len(parse_msg(message)) > 1:
     883                        info.setdefault('flags', []).append('genshi-format')
     884                        if PYTHON_FORMAT and bool(filter(
     885                            None, [PYTHON_FORMAT.search(id)
     886                                   for id in [message] if id])):
     887                            info.setdefault('flags', []).append('python-format')
     888                    yield msgbuf.lineno, None, message, info
    835889                    msgbuf = None
    836890
    837891            elif kind is EXPR or kind is EXEC:
    838892                if msgbuf:
    839893                    msgbuf.append(kind, data, pos)
    840                 for funcname, strings in extract_from_code(data,
    841                                                            gettext_functions):
    842                     # XXX: Do we need to grab i18n:comment from ctxt ???
    843                     yield pos[1], funcname, strings, []
     894                for funcname, strings, info in extract_from_code(
     895                                                    data, gettext_functions):
     896                    yield pos[1], funcname, strings, info
    844897
    845898            elif kind is SUB:
    846899                directives, substream = data
     
    860913                                search_text=search_text and not skip,
    861914                                msgbuf=msgbuf, ctxt=ctxt,
    862915                                error_callback=error_callback)
    863                             for lineno, funcname, text, comments in messages:
    864                                 yield lineno, funcname, text, comments
     916                            for lineno, funcname, text, info in messages:
     917                                yield lineno, funcname, text, info
    865918                        directives.pop(idx)
    866919                    elif not isinstance(directive, I18NDirective):
    867920                        # Remove all other non i18n directives from the process
     
    874927                    messages = self.extract(
    875928                        substream, gettext_functions,
    876929                        search_text=search_text and not skip, msgbuf=msgbuf)
    877                     for lineno, funcname, text, comments in messages:
    878                         yield lineno, funcname, text, comments
     930                    for lineno, funcname, text, info in messages:
     931                        yield lineno, funcname, text, info
    879932
    880933                for directive in directives:
    881934                    if isinstance(directive, I18NDirectiveExtract):
    882935                        messages = directive.extract(
    883936                                substream, ctxt, error_callback=error_callback
    884937                        )
    885                         for funcname, text, comments in messages:
    886                             yield pos[1], funcname, text, comments
     938                        for funcname, text, info in messages:
     939                            yield pos[1], funcname, text, info
    887940                    else:
    888941                        messages = self.extract(
    889942                            substream, gettext_functions,
    890943                            search_text=search_text and not skip, msgbuf=msgbuf,
    891944                            error_callback=error_callback)
    892                         for lineno, funcname, text, comments in messages:
    893                             yield lineno, funcname, text, comments
     945                        for lineno, funcname, text, info in messages:
     946                            yield lineno, funcname, text, info
    894947                if comment:
    895948                    ctxt.pop()
    896949
     
    11501203
    11511204    >>> expr = Expression('_("Hello")')
    11521205    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    1153     [('_', u'Hello')]
     1206    [('_', u'Hello', {'comments': []})]
    11541207
    11551208    >>> expr = Expression('ngettext("You have %(num)s item", '
    11561209    ...                            '"You have %(num)s items", num)')
    1157     >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    1158     [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
     1210    >>> list(extract_from_code(
     1211    ...      expr, Translator.GETTEXT_FUNCTIONS)) #doctest: +NORMALIZE_WHITESPACE
     1212    [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None),
     1213      {'flags': ['python-format'], 'comments': []})]
    11591214
    11601215    :param code: the `Code` object
    11611216    :type code: `genshi.template.eval.Code`
     
    11741229            [_add(arg) for arg in node.args]
    11751230            _add(node.starargs)
    11761231            _add(node.kwargs)
     1232            info = {'comments': []}
    11771233            if len(strings) == 1:
    11781234                strings = strings[0]
     1235                if PYTHON_FORMAT and bool(filter(None, [PYTHON_FORMAT.search(id)
     1236                                                        for id in [strings]
     1237                                                        if id])):
     1238                    info.setdefault('flags', []).append('python-format')
    11791239            else:
    11801240                strings = tuple(strings)
    1181             yield node.func.id, strings
     1241                if PYTHON_FORMAT and bool(filter(None, [PYTHON_FORMAT.search(id)
     1242                                                        for id in strings
     1243                                                        if id])):
     1244                    info.setdefault('flags', []).append('python-format')
     1245            yield node.func.id, strings, info
    11821246        elif node._fields:
    11831247            children = []
    11841248            for field in node._fields:
     
    11891253                elif isinstance(child, _ast.AST):
    11901254                    children.append(child)
    11911255            for child in children:
    1192                 for funcname, strings in _walk(child):
    1193                     yield funcname, strings
     1256                for funcname, strings, info in _walk(child):
     1257                    yield funcname, strings, info
    11941258    return _walk(code.ast)
    11951259
    11961260
    1197 def extract(fileobj, keywords, comment_tags, options):
     1261def extract(fileobj, keywords, comment_tags, options, error_callback):
    11981262    """Babel extraction method for Genshi templates.
    11991263
     1264    This function generates tuples of the form:
     1265
     1266        ``(lineno, funcname, message, info)``
     1267
     1268    * `info`` is a dictionary that contains additional information about the
     1269      message being extracted like:
     1270          `comments`: A list of comments embedded in the source code;
     1271                      With genshi you define the comments to be extracted
     1272                      from the ``i18n:comment`` attributes found in the
     1273                      markup;
     1274          `context`: A string containing the message context; Not yet used
     1275                     in genshi.
     1276           `flags`: A list of flags used on the message, ie,
     1277                   ``python-format``, etc, or even a custom flag,
     1278                   ``genshi-format`` for genshi nested markup;
     1279
    12001280    :param fileobj: the file-like object the messages should be extracted from
    12011281    :param keywords: a list of keywords (i.e. function names) that should be
    12021282                     recognized as translation functions
    12031283    :param comment_tags: a list of translator tags to search for and include
    12041284                         in the results
    12051285    :param options: a dictionary of additional options (optional)
    1206     :return: an iterator over ``(lineno, funcname, message, comments)`` tuples
     1286    :param error_callback: an error function called if defined, and, in case
     1287                           of errors, to send them to the user.
     1288                           Accepts three arguments;
     1289                           ``filename``, ``line_number``, ``error_message``
     1290    :return: an iterator over ``(lineno, funcname, message, info)`` tuples
    12071291    :rtype: ``iterator``
    12081292    """
    12091293    template_class = options.get('template_class', MarkupTemplate)
     
    12291313    tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
    12301314                          encoding=encoding)
    12311315
    1232     error_callback = options.get('error_callback')
    1233 
    12341316    translator = Translator(None, ignore_tags, include_attrs, extract_text)
    12351317    if hasattr(tmpl, 'add_directives'):
    12361318        tmpl.add_directives(Translator.NAMESPACE, translator)
    1237     for message in translator.extract(tmpl.stream, gettext_functions=keywords,
    1238                                       error_callback=error_callback):
    1239         yield message
     1319    for lineno, funcname, messages, info in translator.extract(
     1320        tmpl.stream, gettext_functions=keywords, error_callback=error_callback):
     1321        yield lineno, funcname, messages, info
    12401322
    12411323
    12421324def setup_i18n(tmpl, translator):
  • genshi/filters/tests/i18n.py

    diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py
    a b  
    8888        translator = Translator(extract_text=False)
    8989        messages = list(translator.extract(tmpl.stream))
    9090        self.assertEqual(1, len(messages))
    91         self.assertEqual((3, 'ngettext', (u'Singular', u'Plural', None), []),
     91        self.assertEqual((3, 'ngettext', (u'Singular', u'Plural', None),
     92                          {'comments': []}),
    9293                         messages[0])
    9394
    9495    def test_extract_plural_form(self):
     
    9899        translator = Translator()
    99100        messages = list(translator.extract(tmpl.stream))
    100101        self.assertEqual(1, len(messages))
    101         self.assertEqual((2, 'ngettext', (u'Singular', u'Plural', None), []),
     102        self.assertEqual((2, 'ngettext', (u'Singular', u'Plural', None),
     103                          {'comments': []}),
    102104                         messages[0])
    103105
    104106    def test_extract_funky_plural_form(self):
     
    108110        translator = Translator()
    109111        messages = list(translator.extract(tmpl.stream))
    110112        self.assertEqual(1, len(messages))
    111         self.assertEqual((2, 'ngettext', (None, None), []), messages[0])
     113        self.assertEqual((2, 'ngettext', (None, None), {'comments': []}), messages[0])
    112114
    113115    def test_extract_gettext_with_unicode_string(self):
    114116        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    117119        translator = Translator()
    118120        messages = list(translator.extract(tmpl.stream))
    119121        self.assertEqual(1, len(messages))
    120         self.assertEqual((2, 'gettext', u'Gr\xfc\xdfe', []), messages[0])
     122        self.assertEqual((2, 'gettext', u'Gr\xfc\xdfe', {'comments': []}), messages[0])
    121123
    122124    def test_extract_included_attribute_text(self):
    123125        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    126128        translator = Translator()
    127129        messages = list(translator.extract(tmpl.stream))
    128130        self.assertEqual(1, len(messages))
    129         self.assertEqual((2, None, u'Foo', []), messages[0])
     131        self.assertEqual((2, None, u'Foo', {'comments': []}), messages[0])
    130132
    131133    def test_extract_attribute_expr(self):
    132134        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    135137        translator = Translator()
    136138        messages = list(translator.extract(tmpl.stream))
    137139        self.assertEqual(1, len(messages))
    138         self.assertEqual((2, '_', u'Save', []), messages[0])
     140        self.assertEqual((2, '_', u'Save', {'comments': []}), messages[0])
    139141
    140142    def test_extract_non_included_attribute_interpolated(self):
    141143        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    144146        translator = Translator()
    145147        messages = list(translator.extract(tmpl.stream))
    146148        self.assertEqual(1, len(messages))
    147         self.assertEqual((2, None, u'Foo', []), messages[0])
     149        self.assertEqual((2, None, u'Foo', {'comments': []}), messages[0])
    148150
    149151    def test_extract_text_from_sub(self):
    150152        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    153155        translator = Translator()
    154156        messages = list(translator.extract(tmpl.stream))
    155157        self.assertEqual(1, len(messages))
    156         self.assertEqual((2, None, u'Foo', []), messages[0])
     158        self.assertEqual((2, None, u'Foo', {'comments': []}), messages[0])
    157159
    158160    def test_ignore_tag_with_fixed_xml_lang(self):
    159161        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
     
    170172        translator = Translator()
    171173        messages = list(translator.extract(tmpl.stream))
    172174        self.assertEqual(1, len(messages))
    173         self.assertEqual((2, None, u'(c) 2007 Edgewall Software', []),
     175        self.assertEqual((2, None, u'(c) 2007 Edgewall Software', {'comments': []}),
    174176                         messages[0])
    175177
    176178    def test_ignore_attribute_with_expression(self):
     
    468470        tmpl.add_directives(Translator.NAMESPACE, translator)
    469471        messages = list(translator.extract(tmpl.stream))
    470472        self.assertEqual(1, len(messages))
    471         self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0])
     473        self.assertEqual((3, None, u'Foo', {'comments': [u'As in foo bar']}),
     474                         messages[0])
    472475        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
    473476            xmlns:i18n="http://genshi.edgewall.org/i18n">
    474477          <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
     
    477480        tmpl.add_directives(Translator.NAMESPACE, translator)
    478481        messages = list(translator.extract(tmpl.stream))
    479482        self.assertEqual(1, len(messages))
    480         self.assertEqual((3, None, u'Foo', ['As in foo bar']), messages[0])
     483        self.assertEqual((3, None, u'Foo', {'comments': [u'As in foo bar']}),
     484                         messages[0])
    481485
    482486    def test_translate_i18n_msg_with_comment(self):
    483487        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    499503        translator = Translator()
    500504        messages = list(translator.extract(tmpl.stream))
    501505        self.assertEqual(2, len(messages))
    502         self.assertEqual((3, None, u'Foo bar', []), messages[0])
    503         self.assertEqual((3, None, u'Foo', []), messages[1])
     506        self.assertEqual((3, None, u'Foo bar', {'comments': []}), messages[0])
     507        self.assertEqual((3, None, u'Foo', {'comments': []}), messages[1])
    504508
    505509    def test_translate_i18n_msg_with_attr(self):
    506510        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    554558        messages = list(translator.extract(tmpl.stream))
    555559        self.assertEqual(1, len(messages))
    556560        self.assertEqual(
    557             (3, None, u'Changed %(date)s ago by %(author)s', []), messages[0]
     561            (3, None, u'Changed %(date)s ago by %(author)s',
     562             {'flags': ['python-format'], 'comments': []}), messages[0]
    558563        )
    559564
    560565    def test_i18n_msg_ticket_300_translate(self):
     
    584589        messages = list(translator.extract(tmpl.stream))
    585590        self.assertEqual(1, len(messages))
    586591        self.assertEqual(
    587             (3, None, u'[1:[2:Translation\\[\xa00\xa0\\]]: [3:One coin]]', []), messages[0]
     592            (3, None, u'[1:[2:Translation\\[\xa00\xa0\\]]: [3:One coin]]',
     593             {'flags': ['genshi-format'], 'comments': []}), messages[0]
    588594        )
    589595
    590596    def test_i18n_msg_ticket_251_translate(self):
     
    10031009        tmpl.add_directives(Translator.NAMESPACE, translator)
    10041010        messages = list(translator.extract(tmpl.stream))
    10051011        self.assertEqual(2, len(messages))
    1006         self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
    1007         self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     1012        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), {'comments': []}), messages[0])
     1013        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), {'comments': []}), messages[1])
    10081014
    10091015    def test_extract_i18n_choose_as_directive(self):
    10101016        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    10221028        tmpl.add_directives(Translator.NAMESPACE, translator)
    10231029        messages = list(translator.extract(tmpl.stream))
    10241030        self.assertEqual(2, len(messages))
    1025         self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
    1026         self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), []), messages[1])
     1031        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), {'comments': []}), messages[0])
     1032        self.assertEqual((7, 'ngettext', (u'FooBar', u'FooBars'), {'comments': []}), messages[1])
    10271033
    10281034    def test_extract_i18n_choose_as_attribute_with_params(self):
    10291035        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    10381044        messages = list(translator.extract(tmpl.stream))
    10391045        self.assertEqual(1, len(messages))
    10401046        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1041                                           u'Foos %(fname)s %(lname)s'), []),
     1047                                          u'Foos %(fname)s %(lname)s'),
     1048                          {'flags': ['python-format'], 'comments': []}),
    10421049                         messages[0])
    10431050
    10441051    def test_extract_i18n_choose_as_attribute_with_params_and_domain_as_param(self):
     
    10551062        messages = list(translator.extract(tmpl.stream))
    10561063        self.assertEqual(1, len(messages))
    10571064        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1058                                           u'Foos %(fname)s %(lname)s'), []),
     1065                                          u'Foos %(fname)s %(lname)s'),
     1066                          {'flags': ['python-format'], 'comments': []}),
    10591067                         messages[0])
    10601068
    10611069    def test_extract_i18n_choose_as_directive_with_params(self):
     
    10751083        messages = list(translator.extract(tmpl.stream))
    10761084        self.assertEqual(2, len(messages))
    10771085        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1078                                           u'Foos %(fname)s %(lname)s'), []),
     1086                                          u'Foos %(fname)s %(lname)s'),
     1087                          {'flags': ['python-format'], 'comments': []}),
    10791088                         messages[0])
    10801089        self.assertEqual((7, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1081                                           u'Foos %(fname)s %(lname)s'), []),
     1090                                          u'Foos %(fname)s %(lname)s'),
     1091                          {'flags': ['python-format'], 'comments': []}),
    10821092                         messages[1])
    10831093
    10841094    def test_extract_i18n_choose_as_directive_with_params_and_domain_as_directive(self):
     
    11001110        messages = list(translator.extract(tmpl.stream))
    11011111        self.assertEqual(2, len(messages))
    11021112        self.assertEqual((4, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1103                                           u'Foos %(fname)s %(lname)s'), []),
     1113                                          u'Foos %(fname)s %(lname)s'),
     1114                          {'flags': ['python-format'], 'comments': []}),
    11041115                         messages[0])
    11051116        self.assertEqual((9, 'ngettext', (u'Foo %(fname)s %(lname)s',
    1106                                           u'Foos %(fname)s %(lname)s'), []),
     1117                                          u'Foos %(fname)s %(lname)s'),
     1118                          {'flags': ['python-format'], 'comments': []}),
    11071119                         messages[1])
    11081120
    11091121    def test_extract_i18n_choose_as_attribute_with_params_and_comment(self):
     
    11201132        self.assertEqual(1, len(messages))
    11211133        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
    11221134                                          u'Foos %(fname)s %(lname)s'),
    1123                           [u'As in Foo Bar']),
     1135                          {'flags': ['python-format'],
     1136                           'comments': [u'As in Foo Bar']}),
    11241137                         messages[0])
    11251138
    11261139    def test_extract_i18n_choose_as_directive_with_params_and_comment(self):
     
    11371150        self.assertEqual(1, len(messages))
    11381151        self.assertEqual((3, 'ngettext', (u'Foo %(fname)s %(lname)s',
    11391152                                          u'Foos %(fname)s %(lname)s'),
    1140                           [u'As in Foo Bar']),
     1153                          {'flags': ['python-format'],
     1154                           'comments': [u'As in Foo Bar']}),
    11411155                         messages[0])
    11421156
    11431157    def test_translate_i18n_domain_with_nested_inlcudes(self):
     
    13691383        tmpl.add_directives(Translator.NAMESPACE, translator)
    13701384        messages = list(translator.extract(tmpl.stream))
    13711385        self.assertEqual(1, len(messages))
    1372         self.assertEqual((3, None, u'Please see [1:Help] for details.', []),
     1386        self.assertEqual((3, None, u'Please see [1:Help] for details.',
     1387                          {'flags': ['genshi-format'], 'comments': []}),
    13731388                         messages[0])
    13741389
    13751390    def test_extract_i18n_msg_with_py_strip_and_comment(self):
     
    13841399        messages = list(translator.extract(tmpl.stream))
    13851400        self.assertEqual(1, len(messages))
    13861401        self.assertEqual((3, None, u'Please see [1:Help] for details.',
    1387                           ['Foo']), messages[0])
     1402                          {'flags': ['genshi-format'], 'comments': [u'Foo']}),
     1403                          messages[0])
    13881404
    13891405    def test_extract_i18n_choose_as_attribute_and_py_strip(self):
    13901406        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    13981414        tmpl.add_directives(Translator.NAMESPACE, translator)
    13991415        messages = list(translator.extract(tmpl.stream))
    14001416        self.assertEqual(1, len(messages))
    1401         self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), []), messages[0])
     1417        self.assertEqual((3, 'ngettext', (u'FooBar', u'FooBars'), {'comments': []}), messages[0])
    14021418
    14031419    def test_translate_i18n_domain_with_inline_directive_on_START_NS(self):
    14041420        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     
    14721488            <p>${ngettext("You have %d item", "You have %d items", num)}</p>
    14731489          </body>
    14741490        </html>""")
    1475         results = list(extract(buf, ['_', 'ngettext'], [], {}))
     1491        results = list(extract(buf, ['_', 'ngettext'], [], {}, None))
    14761492        self.assertEqual([
    1477             (3, None, u'Example', []),
    1478             (6, None, u'Example', []),
    1479             (7, '_', u'Hello, %(name)s', []),
     1493            (3, None, u'Example', {'comments': []}),
     1494            (6, None, u'Example', {'comments': []}),
     1495            (7, '_', u'Hello, %(name)s', {'flags': ['python-format'], 'comments': []}),
    14801496            (8, 'ngettext', (u'You have %d item', u'You have %d items', None),
    1481                              []),
     1497                             {'flags': ['python-format'], 'comments': []}),
    14821498        ], results)
    14831499
    14841500    def test_extraction_without_text(self):
     
    14881504        </html>""")
    14891505        results = list(extract(buf, ['_', 'ngettext'], [], {
    14901506            'extract_text': 'no'
    1491         }))
     1507        }, None))
    14921508        self.assertEqual([
    1493             (3, 'ngettext', (u'Singular', u'Plural', None), []),
     1509            (3, 'ngettext', (u'Singular', u'Plural', None), {'comments': []}),
    14941510        ], results)
    14951511
    14961512    def test_text_template_extraction(self):
     
    15051521        Foobar""")
    15061522        results = list(extract(buf, ['_', 'ngettext'], [], {
    15071523            'template_class': 'genshi.template:TextTemplate'
    1508         }))
     1524        }, None))
    15091525        self.assertEqual([
    1510             (1, '_', u'Dear %(name)s', []),
    1511             (3, 'ngettext', (u'Your item:', u'Your items', None), []),
    1512             (7, None, u'All the best,\n        Foobar', [])
     1526            (1, '_', u'Dear %(name)s', {'flags': ['python-format'], 'comments': []}),
     1527            (3, 'ngettext', (u'Your item:', u'Your items', None),
     1528             {'comments': []}),
     1529            (7, None, u'All the best,\n        Foobar', {'comments': []})
    15131530        ], results)
    15141531
    15151532    def test_extraction_with_keyword_arg(self):
    15161533        buf = StringIO("""<html xmlns:py="http://genshi.edgewall.org/">
    15171534          ${gettext('Foobar', foo='bar')}
    15181535        </html>""")
    1519         results = list(extract(buf, ['gettext'], [], {}))
     1536        results = list(extract(buf, ['gettext'], [], {}, None))
    15201537        self.assertEqual([
    1521             (2, 'gettext', (u'Foobar'), []),
     1538            (2, 'gettext', (u'Foobar'), {'comments': []}),
    15221539        ], results)
    15231540
    15241541    def test_extraction_with_nonstring_arg(self):
    15251542        buf = StringIO("""<html xmlns:py="http://genshi.edgewall.org/">
    15261543          ${dgettext(curdomain, 'Foobar')}
    15271544        </html>""")
    1528         results = list(extract(buf, ['dgettext'], [], {}))
     1545        results = list(extract(buf, ['dgettext'], [], {}, None))
    15291546        self.assertEqual([
    1530             (2, 'dgettext', (None, u'Foobar'), []),
     1547            (2, 'dgettext', (None, u'Foobar'), {'comments': []}),
    15311548        ], results)
    15321549
    15331550    def test_extraction_inside_ignored_tags(self):
     
    15391556            });
    15401557          </script>
    15411558        </html>""")
    1542         results = list(extract(buf, ['_'], [], {}))
    1543         self.assertEqual([
    1544             (5, '_', u'Please wait...', []),
    1545         ], results)
     1559        results = list(extract(buf, ['_'], [], {}, None))
     1560        self.assertEqual([(5, '_', u'Please wait...', {'comments': []})],
     1561                         results)
    15461562
    15471563    def test_extraction_inside_ignored_tags_with_directives(self):
    15481564        buf = StringIO("""<html xmlns:py="http://genshi.edgewall.org/">
     
    15521568            </py:if>
    15531569          </script>
    15541570        </html>""")
    1555         self.assertEqual([], list(extract(buf, ['_'], [], {})))
     1571        self.assertEqual([], list(extract(buf, ['_'], [], {}, None)))
    15561572
    15571573    def test_extract_py_def_directive_with_py_strip(self):
    15581574        # Failed extraction from Trac
     
    15971613        messages = list(translator.extract(tmpl.stream))
    15981614        self.assertEqual(10, len(messages))
    15991615        self.assertEqual([
    1600             (3, None, u'View differences', []),
    1601             (6, None, u'inline', []),
    1602             (8, None, u'side by side', []),
    1603             (10, None, u'Show', []),
    1604             (13, None, u'lines around each change', []),
    1605             (16, None, u'Ignore:', []),
    1606             (20, None, u'Blank lines', []),
    1607             (25, None, u'Case changes',[]),
    1608             (30, None, u'White space changes', []),
    1609             (34, '_', u'Update', [])], messages)
     1616            (3, None, u'View differences', {'comments': []}),
     1617            (6, None, u'inline', {'comments': []}),
     1618            (8, None, u'side by side', {'comments': []}),
     1619            (10, None, u'Show', {'comments': []}),
     1620            (13, None, u'lines around each change', {'comments': []}),
     1621            (16, None, u'Ignore:', {'comments': []}),
     1622            (20, None, u'Blank lines', {'comments': []}),
     1623            (25, None, u'Case changes', {'comments': []}),
     1624            (30, None, u'White space changes', {'comments': []}),
     1625            (34, '_', u'Update', {'comments': []})
     1626        ], messages)
    16101627
    16111628
    16121629def suite():