changeset 892:1de952fd479e

i18n: Support extraction of attributes in markup embedded in ``i18n:msg`` and ``i18n:choose`` directives. See also #380.
author cmlenz
date Wed, 21 Apr 2010 10:42:41 +0000
parents b40dbfee9ba6
children bf76a0fe20ae
files genshi/filters/i18n.py genshi/filters/tests/i18n.py
diffstat 2 files changed, 310 insertions(+), 76 deletions(-) [+]
line wrap: on
line diff
--- a/genshi/filters/i18n.py
+++ b/genshi/filters/i18n.py
@@ -44,6 +44,9 @@
 SUB_START = StreamEventKind('SUB_START')
 SUB_END = StreamEventKind('SUB_END')
 
+GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext', 'dgettext', 'dngettext',
+                     'ugettext', 'ungettext')
+
 
 class I18NDirective(Directive):
     """Simple interface for i18n directives to support messages extraction."""
@@ -55,7 +58,8 @@
 class ExtractableI18NDirective(I18NDirective):
     """Simple interface for directives to support messages extraction."""
 
-    def extract(self, stream, comment_stack):
+    def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+                search_text=True, comment_stack=None):
         raise NotImplementedError
 
 
@@ -73,8 +77,8 @@
     """
     __slots__ = ['comment']
 
-    def __init__(self, value, template, hints=None, namespaces=None,
-                 lineno=-1, offset=-1):
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
+                 offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.comment = value
 
@@ -133,12 +137,13 @@
     translators. Stripping it is not that important since it's on the html
     source, the rendered output will remain the same.
     """
-    __slots__ = ['params']
+    __slots__ = ['params', 'lineno']
 
-    def __init__(self, value, template, hints=None, namespaces=None,
-                 lineno=-1, offset=-1):
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
+                 offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.params = [param.strip() for param in value.split(',') if param]
+        self.lineno = lineno
 
     @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
@@ -176,24 +181,37 @@
 
         return _apply_directives(_generate(), directives, ctxt, vars)
 
-    def extract(self, stream, comment_stack):
+    def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+                search_text=True, comment_stack=None):
         msgbuf = MessageBuffer(self)
+        strip = False
 
         stream = iter(stream)
         previous = stream.next()
         if previous[0] is START:
+            for message in translator._extract_attrs(previous,
+                                                     gettext_functions,
+                                                     search_text=search_text):
+                yield message
             previous = stream.next()
+            strip = True
         for event in stream:
+            if event[0] is START:
+                for message in translator._extract_attrs(event,
+                                                         gettext_functions,
+                                                         search_text=search_text):
+                    yield message
             msgbuf.append(*previous)
             previous = event
-        msgbuf.append(*previous)
+        if not strip:
+            msgbuf.append(*previous)
 
-        yield None, msgbuf.format(), comment_stack[-1:]
+        yield self.lineno, None, msgbuf.format(), comment_stack[-1:]
 
 
 class ChooseBranchDirective(I18NDirective):
     __slots__ = ['params']
-        
+
     def __call__(self, stream, directives, ctxt, **vars):
         self.params = ctxt.get('_i18n.choose.params', [])[:]
         msgbuf = MessageBuffer(self)
@@ -226,17 +244,30 @@
         ctxt['_i18n.choose.%s' % type(self).__name__] = msgbuf
 
 
-    def extract(self, stream, comment_stack, msgbuf):
+    def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+                search_text=True, comment_stack=None, msgbuf=None):
         stream = iter(stream)
         previous = stream.next()
+
         if previous[0] is START:
+            # skip the enclosing element
+            for message in translator._extract_attrs(previous,
+                                                     gettext_functions,
+                                                     search_text=search_text):
+                yield message
             previous = stream.next()
+
         for event in stream:
+            if previous[0] is START:
+                for message in translator._extract_attrs(previous,
+                                                         gettext_functions,
+                                                         search_text=search_text):
+                    yield message
             msgbuf.append(*previous)
             previous = event
+
         if previous[0] is not END:
             msgbuf.append(*previous)
-        return msgbuf
 
 
 class SingularDirective(ChooseBranchDirective):
@@ -307,15 +338,16 @@
     [(2, 'ngettext', (u'There is %(num)s coin',
                       u'There are %(num)s coins'), [])]
     """
-    __slots__ = ['numeral', 'params']
+    __slots__ = ['numeral', 'params', 'lineno']
 
-    def __init__(self, value, template, hints=None, namespaces=None,
-                 lineno=-1, offset=-1):
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
+                 offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
         params = [v.strip() for v in value.split(';')]
         self.numeral = self._parse_expr(params.pop(0), template, lineno, offset)
         self.params = params and [name.strip() for name in
                                   params[0].split(',') if name] or []
+        self.lineno = lineno
 
     @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
@@ -343,6 +375,7 @@
         dngettext = ctxt.get('_i18n.dngettext')
         if not dngettext:
             dngettext = lambda d, s, p, n: ngettext(s, p, n)
+
         for kind, event, pos in stream:
             if kind is SUB:
                 subdirectives, substream = event
@@ -407,7 +440,7 @@
         plural_test = u'\xcc\xfb+\xd3Pn\x9d\tT\xec\x1d\xda\x1a\x88\x00'
         translation = ngettext(singular_test, plural_test,
                                self.numeral.evaluate(ctxt))
-        if translation==singular_test:
+        if translation == singular_test:
             chosen_msgbuf = singular_msgbuf
             chosen_stream = singular_stream
         else:
@@ -431,33 +464,56 @@
 
         ctxt.pop()
 
-    def extract(self, stream, comment_stack):
+    def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+                search_text=True, comment_stack=None):
+        strip = False
         stream = iter(stream)
         previous = stream.next()
-        if previous is START:
-            stream.next()
+
+        if previous[0] is START:
+            # skip the enclosing element
+            for message in translator._extract_attrs(previous,
+                                                     gettext_functions,
+                                                     search_text=search_text):
+                yield message
+            previous = stream.next()
+            strip = True
 
         singular_msgbuf = MessageBuffer(self)
         plural_msgbuf = MessageBuffer(self)
 
-        for kind, event, pos in stream:
-            if kind is SUB:
-                subdirectives, substream = event
-                for subdirective in subdirectives:
-                    if isinstance(subdirective, SingularDirective):
-                        singular_msgbuf = subdirective.extract(substream, comment_stack,
-                                                               singular_msgbuf)
-                    elif isinstance(subdirective, PluralDirective):
-                        plural_msgbuf = subdirective.extract(substream, comment_stack,
-                                                             plural_msgbuf)
-                    elif not isinstance(subdirective, StripDirective):
-                        singular_msgbuf.append(kind, event, pos)
-                        plural_msgbuf.append(kind, event, pos)
+        for event in stream:
+            if previous[0] is SUB:
+                directives, substream = previous[1]
+                for directive in directives:
+                    if isinstance(directive, SingularDirective):
+                        for message in directive.extract(translator,
+                                substream, gettext_functions, search_text,
+                                comment_stack, msgbuf=singular_msgbuf):
+                            yield message
+                    elif isinstance(directive, PluralDirective):
+                        for message in directive.extract(translator,
+                                substream, gettext_functions, search_text,
+                                comment_stack, msgbuf=plural_msgbuf):
+                            yield message
+                    elif not isinstance(directive, StripDirective):
+                        singular_msgbuf.append(*previous)
+                        plural_msgbuf.append(*previous)
             else:
-                singular_msgbuf.append(kind, event, pos)
-                plural_msgbuf.append(kind, event, pos)
+                if previous[0] is START:
+                    for message in translator._extract_attrs(previous,
+                                                             gettext_functions,
+                                                             search_text):
+                        yield message
+                singular_msgbuf.append(*previous)
+                plural_msgbuf.append(*previous)
+            previous = event
 
-        yield 'ngettext', \
+        if not strip:
+            singular_msgbuf.append(*previous)
+            plural_msgbuf.append(*previous)
+
+        yield self.lineno, 'ngettext', \
             (singular_msgbuf.format(), plural_msgbuf.format()), \
             comment_stack[-1:]
 
@@ -499,8 +555,8 @@
     """
     __slots__ = ['domain']
 
-    def __init__(self, value, template, hints=None, namespaces=None,
-                 lineno=-1, offset=-1):
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
+                 offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.domain = value and value.strip() or '__DEFAULT__' 
 
@@ -734,9 +790,6 @@
             else:
                 yield kind, data, pos
 
-    GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext', 'dgettext', 'dngettext',
-                         'ugettext', 'ungettext')
-
     def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
                 search_text=True, comment_stack=None):
         """Extract localizable strings from the given template stream.
@@ -801,24 +854,15 @@
 
             if kind is START and not skip:
                 tag, attrs = data
-
                 if tag in self.ignore_tags or \
                         isinstance(attrs.get(xml_lang), basestring):
                     skip += 1
                     continue
 
-                for name, value in attrs:
-                    if search_text and isinstance(value, basestring):
-                        if name in self.include_attrs:
-                            text = value.strip()
-                            if text:
-                                # XXX: Do we need to grab i18n:comment from comment_stack ???
-                                yield pos[1], None, text, []
-                    else:
-                        for lineno, funcname, text, comments in self.extract(
-                                _ensure(value), gettext_functions,
-                                search_text=False):
-                            yield lineno, funcname, text, comments
+                for message in self._extract_attrs((kind, data, pos),
+                                                   gettext_functions,
+                                                   search_text=search_text):
+                    yield message
 
             elif not skip and search_text and kind is TEXT:
                 text = data.strip()
@@ -844,12 +888,11 @@
                         if len(directives) == 1:
                             # in case we're in the presence of something like:
                             # <p i18n:comment="foo">Foo</p>
-                            messages = self.extract(
-                                substream, gettext_functions,
-                                search_text=search_text and not skip,
-                                comment_stack=comment_stack)
-                            for lineno, funcname, text, comments in messages:
-                                yield lineno, funcname, text, comments
+                            for message in self.extract(
+                                    substream, gettext_functions,
+                                    search_text=search_text and not skip,
+                                    comment_stack=comment_stack):
+                                yield message
                         directives.pop(idx)
                     elif not isinstance(directive, I18NDirective):
                         # Remove all other non i18n directives from the process
@@ -859,23 +902,24 @@
                     # Extract content if there's no directives because
                     # strip was pop'ed and not because comment was pop'ed.
                     # Extraction in this case has been taken care of.
-                    messages = self.extract(
-                        substream, gettext_functions,
-                        search_text=search_text and not skip)
-                    for lineno, funcname, text, comments in messages:
-                        yield lineno, funcname, text, comments
+                    for message in self.extract(
+                            substream, gettext_functions,
+                            search_text=search_text and not skip):
+                        yield message
 
                 for directive in directives:
                     if isinstance(directive, ExtractableI18NDirective):
-                        messages = directive.extract(substream, comment_stack)
-                        for funcname, text, comments in messages:
-                            yield pos[1], funcname, text, comments
+                        for message in directive.extract(self,
+                                substream, gettext_functions,
+                                search_text=search_text and not skip,
+                                comment_stack=comment_stack):
+                            yield message
                     else:
-                        messages = self.extract(
-                            substream, gettext_functions,
-                            search_text=search_text and not skip)
-                        for lineno, funcname, text, comments in messages:
-                            yield lineno, funcname, text, comments
+                        for message in self.extract(
+                                substream, gettext_functions,
+                                search_text=search_text and not skip,
+                                comment_stack=comment_stack):
+                            yield message
 
                 if in_comment:
                     comment_stack.pop()
@@ -896,6 +940,18 @@
         if hasattr(template, 'add_directives'):
             template.add_directives(Translator.NAMESPACE, self)
 
+    def _extract_attrs(self, event, gettext_functions, search_text):
+        for name, value in event[1][1]:
+            if search_text and isinstance(value, basestring):
+                if name in self.include_attrs:
+                    text = value.strip()
+                    if text:
+                        yield event[2][1], None, text, []
+            else:
+                for message in self.extract(_ensure(value), gettext_functions,
+                                            search_text=False):
+                    yield message
+
 
 class MessageBuffer(object):
     """Helper class for managing internationalized mixed content.
@@ -957,7 +1013,7 @@
                                  "%d or more expressions used in '%s', line %s"
                                  % (len(self.orig_params), params, 
                                     self.directive.tagname,
-                                    len(self.orig_params)+1,
+                                    len(self.orig_params) + 1,
                                     os.path.basename(pos[0] or
                                                      'In-memory Template'),
                                     pos[1]))
@@ -1076,6 +1132,7 @@
                     else:
                         yield event
 
+
 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')):
     """Parse a translated message using Genshi mixed content message
     formatting.
@@ -1126,12 +1183,12 @@
     
     >>> from genshi.template.eval import Expression
     >>> expr = Expression('_("Hello")')
-    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
+    >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
     [('_', u'Hello')]
     
     >>> expr = Expression('ngettext("You have %(num)s item", '
     ...                            '"You have %(num)s items", num)')
-    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
+    >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
     [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
     
     :param code: the `Code` object
--- a/genshi/filters/tests/i18n.py
+++ b/genshi/filters/tests/i18n.py
@@ -254,6 +254,73 @@
           Für Details siehe bitte <a href="help.html">Hilfe</a>
         </html>""", tmpl.generate().render())
 
+    def test_extract_i18n_msg_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" title="A helpful paragraph">
+            Please see <a href="help.html" title="Click for help">Help</a>
+          </p>
+        </html>""")
+        translator = Translator()
+        translator.setup(tmpl)
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(3, len(messages))
+        self.assertEqual('A helpful paragraph', messages[0][2])
+        self.assertEqual(3, messages[0][0])
+        self.assertEqual('Click for help', messages[1][2])
+        self.assertEqual(4, messages[1][0])
+        self.assertEqual('Please see [1:Help]', messages[2][2])
+        self.assertEqual(3, messages[2][0])
+
+    def test_translate_i18n_msg_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" title="A helpful paragraph">
+            Please see <a href="help.html" title="Click for help">Help</a>
+          </p>
+        </html>""")
+        translator = Translator(lambda msgid: {
+            'A helpful paragraph': 'Ein hilfreicher Absatz',
+            'Click for help': u'Klicken für Hilfe',
+            'Please see [1:Help]': u'Siehe bitte [1:Hilfe]'
+        }[msgid])
+        translator.setup(tmpl)
+        self.assertEqual(u"""<html>
+          <p title="Ein hilfreicher Absatz">Siehe bitte <a href="help.html" title="Klicken für Hilfe">Hilfe</a></p>
+        </html>""", tmpl.generate().render(encoding=None))
+
+    def test_extract_i18n_msg_elt_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:msg params="">
+            Please see <a href="help.html" title="Click for help">Help</a>
+          </i18n:msg>
+        </html>""")
+        translator = Translator()
+        translator.setup(tmpl)
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(2, len(messages))
+        self.assertEqual('Click for help', messages[0][2])
+        self.assertEqual(4, messages[0][0])
+        self.assertEqual('Please see [1:Help]', messages[1][2])
+        self.assertEqual(3, messages[1][0])
+
+    def test_translate_i18n_msg_elt_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:msg params="">
+            Please see <a href="help.html" title="Click for help">Help</a>
+          </i18n:msg>
+        </html>""")
+        translator = Translator(lambda msgid: {
+            'Click for help': u'Klicken für Hilfe',
+            'Please see [1:Help]': u'Siehe bitte [1:Hilfe]'
+        }[msgid])
+        translator.setup(tmpl)
+        self.assertEqual(u"""<html>
+          Siehe bitte <a href="help.html" title="Klicken für Hilfe">Hilfe</a>
+        </html>""", tmpl.generate().render(encoding=None))
+
     def test_extract_i18n_msg_nested(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
@@ -931,7 +998,7 @@
           <p>FooBars</p>
           <p>FooBar</p>
         </html>""", tmpl.generate(one=1, two=2).render())
-        
+
     def test_translate_i18n_choose_as_directive_singular_and_plural_with_strip(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
@@ -1242,6 +1309,115 @@
                           ['As in Foo Bar']),
                          messages[0])
 
+    def test_extract_i18n_choose_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:choose="num; num" title="Things">
+            <i18n:singular>
+              There is <a href="$link" title="View thing">${num} thing</a>.
+            </i18n:singular>
+            <i18n:plural>
+              There are <a href="$link" title="View things">${num} things</a>.
+            </i18n:plural>
+          </p>
+        </html>""")
+        translator = Translator()
+        translator.setup(tmpl)
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(4, len(messages))
+        self.assertEqual((3, None, 'Things', []), messages[0])
+        self.assertEqual((5, None, 'View thing', []), messages[1])
+        self.assertEqual((8, None, 'View things', []), messages[2])
+        self.assertEqual(
+            (3, 'ngettext', ('There is [1:%(num)s thing].',
+                             'There are [1:%(num)s things].'), []),
+            messages[3])
+
+    def test_translate_i18n_msg_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:choose="num; num" title="Things">
+            <i18n:singular>
+              There is <a href="$link" title="View thing">${num} thing</a>.
+            </i18n:singular>
+            <i18n:plural>
+              There are <a href="$link" title="View things">${num} things</a>.
+            </i18n:plural>
+          </p>
+        </html>""")
+        translations = DummyTranslations({
+            'Things': 'Sachen',
+            'View thing': 'Sache betrachten',
+            'View things': 'Sachen betrachten',
+            ('There is [1:%(num)s thing].', 0): 'Da ist [1:%(num)s Sache].',
+            ('There is [1:%(num)s thing].', 1): 'Da sind [1:%(num)s Sachen].'
+        })
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual(u"""<html>
+          <p title="Sachen">
+            Da ist <a href="/things" title="Sache betrachten">1 Sache</a>.
+          </p>
+        </html>""", tmpl.generate(link="/things", num=1).render(encoding=None))
+        self.assertEqual(u"""<html>
+          <p title="Sachen">
+            Da sind <a href="/things" title="Sachen betrachten">3 Sachen</a>.
+          </p>
+        </html>""", tmpl.generate(link="/things", num=3).render(encoding=None))
+
+    def test_extract_i18n_choose_as_element_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:choose numeral="num" params="num">
+            <p i18n:singular="" title="Things">
+              There is <a href="$link" title="View thing">${num} thing</a>.
+            </p>
+            <p i18n:plural="" title="Things">
+              There are <a href="$link" title="View things">${num} things</a>.
+            </p>
+          </i18n:choose>
+        </html>""")
+        translator = Translator()
+        translator.setup(tmpl)
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(5, len(messages))
+        self.assertEqual((4, None, 'Things', []), messages[0])
+        self.assertEqual((5, None, 'View thing', []), messages[1])
+        self.assertEqual((7, None, 'Things', []), messages[2])
+        self.assertEqual((8, None, 'View things', []), messages[3])
+        self.assertEqual(
+            (3, 'ngettext', ('There is [1:%(num)s thing].',
+                             'There are [1:%(num)s things].'), []),
+            messages[4])
+
+    def test_translate_i18n_msg_as_element_with_attributes(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <i18n:choose numeral="num" params="num">
+            <p i18n:singular="" title="Things">
+              There is <a href="$link" title="View thing">${num} thing</a>.
+            </p>
+            <p i18n:plural="" title="Things">
+              There are <a href="$link" title="View things">${num} things</a>.
+            </p>
+          </i18n:choose>
+        </html>""")
+        translations = DummyTranslations({
+            'Things': 'Sachen',
+            'View thing': 'Sache betrachten',
+            'View things': 'Sachen betrachten',
+            ('There is [1:%(num)s thing].', 0): 'Da ist [1:%(num)s Sache].',
+            ('There is [1:%(num)s thing].', 1): 'Da sind [1:%(num)s Sachen].'
+        })
+        translator = Translator(translations)
+        translator.setup(tmpl)
+        self.assertEqual(u"""<html>
+            <p title="Sachen">Da ist <a href="/things" title="Sache betrachten">1 Sache</a>.</p>
+        </html>""", tmpl.generate(link="/things", num=1).render(encoding=None))
+        self.assertEqual(u"""<html>
+            <p title="Sachen">Da sind <a href="/things" title="Sachen betrachten">3 Sachen</a>.</p>
+        </html>""", tmpl.generate(link="/things", num=3).render(encoding=None))
+
     def test_translate_i18n_domain_with_nested_inlcudes(self):
         import os, shutil, tempfile
         from genshi.template.loader import TemplateLoader
@@ -1581,6 +1757,7 @@
           <p>BarFoo</p>
         """, tmpl.generate().render())
 
+
 class ExtractTestCase(unittest.TestCase):
 
     def test_markup_template_extraction(self):
Copyright (C) 2012-2017 Edgewall Software