]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add support for `{% trans trimmed ... %}`
authorAdrian Moennich <adrian@planetcoding.net>
Fri, 17 Feb 2017 22:49:39 +0000 (23:49 +0100)
committerAdrian <adrian@planetcoding.net>
Mon, 3 Jul 2017 14:37:45 +0000 (16:37 +0200)
Same behavior as in Django: All linebreaks and the whitespace
surrounding linebreaks are replaced with a single space.

closes #504

CHANGES
docs/api.rst
docs/extensions.rst
docs/templates.rst
jinja2/defaults.py
jinja2/ext.py
tests/test_ext.py

diff --git a/CHANGES b/CHANGES
index 81098b3e7c61f37dcff7bdd97e26f794094d493e..b12dd4f50f7c2e7bd7638449fc4536ca016d9e85 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -19,6 +19,9 @@ Version 2.10
 - Added a `namespace` function that creates a special object which allows
   attribute assignment using the `set` tag.  This can be used to carry data
   across scopes, e.g. from a loop body to code that comes after the loop.
+- Added a `trimmed` modifier to `{% trans %}` to strip linebreaks and
+  surrounding whitespace. Also added a new policy to enable this for all
+  `trans` blocks.
 
 Version 2.9.6
 -------------
index b983e8f05f2fbecab3f5fc2a0e7997db81d68326..fedc1c73de553e37b1a0a6b38299df48f70e0836 100644 (file)
@@ -611,6 +611,13 @@ Example::
     Keyword arguments to be passed to the dump function.  The default is
     ``{'sort_keys': True}``.
 
+.. _ext-i18n-trimmed:
+
+``ext.i18n.trimmed``:
+    If this is set to `True`, ``{% trans %}`` blocks of the
+    :ref:`i18n-extension` will always unify linebreaks and surrounding
+    whitespace as if the `trimmed` modifier was used.
+
 
 Utilities
 ---------
index cd0934696eac1f134f575e8c3e5038b326065ecf..00adee9fb0c36cc610f33461a38e6f324e986d9d 100644 (file)
@@ -111,6 +111,15 @@ The usage of the `i18n` extension for template designers is covered as part
 
 .. _newstyle-gettext:
 
+Whitespace Trimming
+~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 2.10
+
+Linebreaks and surrounding whitespace can be automatically trimmed by enabling
+the ``ext.i18n.trimmed`` :ref:`policy <ext-i18n-trimmed>`.
+
+
 Newstyle Gettext
 ~~~~~~~~~~~~~~~~
 
index 650b55e63a649b804011c404d716ae9528219e11..62852c4b8c972d2fb825412b08be434c44667ba6 100644 (file)
@@ -1492,6 +1492,22 @@ which should be used for pluralizing by adding it as parameter to `pluralize`::
     {% trans ..., user_count=users|length %}...
     {% pluralize user_count %}...{% endtrans %}
 
+When translating longer blocks of text, whitespace and linebreaks result in
+rather ugly and error-prone translation strings.  To avoid this, a trans block
+can be marked as trimmed which will replace all linebreaks and the whitespace
+surrounding them with a single space and remove leading/trailing whitespace::
+
+    {% trans trimmed book_title=book.title %}
+        This is {{ book_title }}.
+        You should read it!
+    {% endtrans %}
+
+If trimming is enabled globally, the `notrimmed` modifier can be used to
+disable it for a `trans` block.
+
+.. versionadded:: 2.10
+   The `trimmed` and `notrimmed` modifiers have been added.
+
 It's also possible to translate strings in expressions.  For that purpose,
 three functions exist:
 
index 6970e88815f35163e3a065e958dc240faa251087..7c93dec0aeb202495ad4c3e7d25e2d11c507e775 100644 (file)
@@ -48,6 +48,7 @@ DEFAULT_POLICIES = {
     'truncate.leeway':      5,
     'json.dumps_function':  None,
     'json.dumps_kwargs':    {'sort_keys': True},
+    'ext.i18n.trimmed':     False,
 }
 
 
index 75e1f3b625e88276adec819c9b02b4f3196e4efd..0734a84f73d0ddb735a004c9e0416e018a7d50bd 100644 (file)
@@ -10,6 +10,8 @@
     :copyright: (c) 2017 by the Jinja Team.
     :license: BSD.
 """
+import re
+
 from jinja2 import nodes
 from jinja2.defaults import BLOCK_START_STRING, \
      BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \
@@ -223,6 +225,7 @@ class InternationalizationExtension(Extension):
         plural_expr = None
         plural_expr_assignment = None
         variables = {}
+        trimmed = None
         while parser.stream.current.type != 'block_end':
             if variables:
                 parser.stream.expect('comma')
@@ -241,6 +244,9 @@ class InternationalizationExtension(Extension):
             if parser.stream.current.type == 'assign':
                 next(parser.stream)
                 variables[name.value] = var = parser.parse_expression()
+            elif trimmed is None and name.value in ('trimmed', 'notrimmed'):
+                trimmed = name.value == 'trimmed'
+                continue
             else:
                 variables[name.value] = var = nodes.Name(name.value, 'load')
 
@@ -256,7 +262,7 @@ class InternationalizationExtension(Extension):
 
         parser.stream.expect('block_end')
 
-        plural = plural_names = None
+        plural = None
         have_plural = False
         referenced = set()
 
@@ -297,6 +303,13 @@ class InternationalizationExtension(Extension):
         elif plural_expr is None:
             parser.fail('pluralize without variables', lineno)
 
+        if trimmed is None:
+            trimmed = self.environment.policies['ext.i18n.trimmed']
+        if trimmed:
+            singular = self._trim_whitespace(singular)
+            if plural:
+                plural = self._trim_whitespace(plural)
+
         node = self._make_node(singular, plural, variables, plural_expr,
                                bool(referenced),
                                num_called_num and have_plural)
@@ -306,6 +319,9 @@ class InternationalizationExtension(Extension):
         else:
             return node
 
+    def _trim_whitespace(self, string, _ws_re=re.compile(r'\s*\n\s*')):
+        return _ws_re.sub(' ', string.strip())
+
     def _parse_block(self, parser, allow_pluralize):
         """Parse until the next block tag with a given name."""
         referenced = []
@@ -583,6 +599,8 @@ def babel_extract(fileobj, keywords, comment_tags, options):
         auto_reload=False
     )
 
+    if getbool(options, 'trimmed'):
+        environment.policies['ext.i18n.trimmed'] = True
     if getbool(options, 'newstyle_gettext'):
         environment.newstyle_gettext = True
 
index 65a30cab09470adda9ad936c79bb0ceb91f054bd..c3b028ff9553f1922140699587244af6a8e20443 100644 (file)
@@ -91,6 +91,13 @@ i18n_env.globals.update({
     'gettext':      gettext,
     'ngettext':     ngettext
 })
+i18n_env_trimmed = Environment(extensions=['jinja2.ext.i18n'])
+i18n_env_trimmed.policies['ext.i18n.trimmed'] = True
+i18n_env_trimmed.globals.update({
+    '_':            gettext,
+    'gettext':      gettext,
+    'ngettext':     ngettext
+})
 
 newstyle_i18n_env = Environment(
     loader=DictLoader(newstyle_i18n_templates),
@@ -270,6 +277,36 @@ class TestInternationalization(object):
         tmpl = i18n_env.get_template('stringformat.html')
         assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5'
 
+    def test_trimmed(self):
+        tmpl = i18n_env.from_string(
+            '{%- trans trimmed %}  hello\n  world  {% endtrans -%}')
+        assert tmpl.render() == 'hello world'
+
+    def test_trimmed_policy(self):
+        s = '{%- trans %}  hello\n  world  {% endtrans -%}'
+        tmpl = i18n_env.from_string(s)
+        trimmed_tmpl = i18n_env_trimmed.from_string(s)
+        assert tmpl.render() == '  hello\n  world  '
+        assert trimmed_tmpl.render() == 'hello world'
+
+    def test_trimmed_policy_override(self):
+        tmpl = i18n_env_trimmed.from_string(
+            '{%- trans notrimmed %}  hello\n  world  {% endtrans -%}')
+        assert tmpl.render() == '  hello\n  world  '
+
+    def test_trimmed_vars(self):
+        tmpl = i18n_env.from_string(
+            '{%- trans trimmed x="world" %}  hello\n  {{ x }} {% endtrans -%}')
+        assert tmpl.render() == 'hello world'
+
+    def test_trimmed_varname_trimmed(self):
+        # unlikely variable name, but when used as a variable
+        # it should not enable trimming
+        tmpl = i18n_env.from_string(
+            '{%- trans trimmed = "world" %}  hello\n  {{ trimmed }}  '
+            '{% endtrans -%}')
+        assert tmpl.render() == '  hello\n  world  '
+
     def test_extract(self):
         from jinja2.ext import babel_extract
         source = BytesIO('''
@@ -284,6 +321,37 @@ class TestInternationalization(object):
             (4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), [])
         ]
 
+    def test_extract_trimmed(self):
+        from jinja2.ext import babel_extract
+        source = BytesIO('''
+        {{ gettext(' Hello  \n  World') }}
+        {% trans trimmed %} Hello  \n  World{% endtrans %}
+        {% trans trimmed %}{{ users }} \n user
+        {%- pluralize %}{{ users }} \n users{% endtrans %}
+        '''.encode('ascii'))  # make python 3 happy
+        assert list(babel_extract(source,
+                                  ('gettext', 'ngettext', '_'), [], {})) == [
+            (2, 'gettext', u' Hello  \n  World', []),
+            (4, 'gettext', u'Hello World', []),
+            (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), [])
+        ]
+
+    def test_extract_trimmed_option(self):
+        from jinja2.ext import babel_extract
+        source = BytesIO('''
+        {{ gettext(' Hello  \n  World') }}
+        {% trans %} Hello  \n  World{% endtrans %}
+        {% trans %}{{ users }} \n user
+        {%- pluralize %}{{ users }} \n users{% endtrans %}
+        '''.encode('ascii'))  # make python 3 happy
+        opts = {'trimmed': 'true'}
+        assert list(babel_extract(source,
+                                  ('gettext', 'ngettext', '_'), [], opts)) == [
+            (2, 'gettext', u' Hello  \n  World', []),
+            (4, 'gettext', u'Hello World', []),
+            (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), [])
+        ]
+
     def test_comment_extract(self):
         from jinja2.ext import babel_extract
         source = BytesIO('''