From 7a7c789d5a7ce1c62e3739cb2624454189410ece Mon Sep 17 00:00:00 2001 From: Bouke Haarsma Date: Sat, 2 Nov 2013 20:01:17 +0100 Subject: [PATCH] Fixed #5849 -- Strip whitespace from blocktrans Add the trimmed option to the blocktrans tag to trim any newlines and whitespace from its content. This allows the developer to indent the blocktrans tag without adding new lines and whitespace to the msgid in the PO file. Thanks to mpessas for the initial patch and Dmitri Fedortchenko for the report. --- django/templatetags/i18n.py | 15 ++++++++++++--- django/utils/translation/__init__.py | 8 +++++++- django/utils/translation/trans_real.py | 24 ++++++++++++++++++------ docs/releases/1.7.txt | 9 +++++++++ docs/topics/i18n/translation.txt | 24 ++++++++++++++++++++++++ tests/i18n/commands/templates/test.html | 12 ++++++++++++ tests/i18n/test_extraction.py | 11 +++++++++++ tests/i18n/tests.py | 11 +++++++++++ 8 files changed, 104 insertions(+), 10 deletions(-) diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 7c54ce034c6..7c29f63c590 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -97,14 +97,16 @@ class TranslateNode(Node): class BlockTranslateNode(Node): + def __init__(self, extra_context, singular, plural=None, countervar=None, - counter=None, message_context=None): + counter=None, message_context=None, trimmed=False): self.extra_context = extra_context self.singular = singular self.plural = plural self.countervar = countervar self.counter = counter self.message_context = message_context + self.trimmed = trimmed def render_token_list(self, tokens): result = [] @@ -115,7 +117,10 @@ class BlockTranslateNode(Node): elif token.token_type == TOKEN_VAR: result.append('%%(%s)s' % token.contents) vars.append(token.contents) - return ''.join(result), vars + msg = ''.join(result) + if self.trimmed: + msg = translation.trim_whitespace(msg) + return msg, vars def render(self, context, nested=False): if self.message_context: @@ -438,6 +443,8 @@ def do_block_translate(parser, token): '"context" in %r tag expected ' 'exactly one argument.') % bits[0] six.reraise(TemplateSyntaxError, TemplateSyntaxError(msg), sys.exc_info()[2]) + elif option == "trimmed": + value = True else: raise TemplateSyntaxError('Unknown argument for %r tag: %r.' % (bits[0], option)) @@ -453,6 +460,8 @@ def do_block_translate(parser, token): message_context = None extra_context = options.get('with', {}) + trimmed = options.get("trimmed", False) + singular = [] plural = [] while parser.tokens: @@ -474,7 +483,7 @@ def do_block_translate(parser, token): raise TemplateSyntaxError("'blocktrans' doesn't allow other block tags (seen %r) inside it" % token.contents) return BlockTranslateNode(extra_context, singular, plural, countervar, - counter, message_context) + counter, message_context, trimmed=trimmed) @register.tag diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index e4e8c6e1be6..9d46ce67e4a 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -2,7 +2,7 @@ Internationalization support. """ from __future__ import unicode_literals - +import re from django.utils.encoding import force_text from django.utils.functional import lazy from django.utils import six @@ -218,3 +218,9 @@ def get_language_info(lang_code): return LANG_INFO[generic_lang_code] except KeyError: raise KeyError("Unknown language code %s and %s." % (lang_code, generic_lang_code)) + +trim_whitespace_re = re.compile('\s*\n\s*') + + +def trim_whitespace(s): + return trim_whitespace_re.sub(' ', s.strip()) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index e3081dd19ac..ff2d8ca25d6 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -19,7 +19,7 @@ from django.utils._os import upath from django.utils.safestring import mark_safe, SafeData from django.utils import six from django.utils.six import StringIO -from django.utils.translation import TranslatorCommentWarning +from django.utils.translation import TranslatorCommentWarning, trim_whitespace # Translations are cached in a dictionary for every language+app tuple. @@ -530,6 +530,7 @@ def blankout(src, char): """ return dot_re.sub(char, src) + context_re = re.compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""") inline_re = re.compile(r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*""") block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""") @@ -553,6 +554,7 @@ def templatize(src, origin=None): message_context = None intrans = False inplural = False + trimmed = False singular = [] plural = [] incomment = False @@ -582,20 +584,29 @@ def templatize(src, origin=None): endbmatch = endblock_re.match(t.contents) pluralmatch = plural_re.match(t.contents) if endbmatch: + if trimmed: + singular = trim_whitespace(''.join(singular)) + else: + singular = ''.join(singular) + if inplural: - if message_context: - out.write(' npgettext(%r, %r, %r,count) ' % (message_context, ''.join(singular), ''.join(plural))) + if trimmed: + plural = trim_whitespace(''.join(plural)) else: - out.write(' ngettext(%r, %r, count) ' % (''.join(singular), ''.join(plural))) + plural = ''.join(plural) + if message_context: + out.write(' npgettext(%r, %r, %r,count) ' % (message_context, singular, plural)) + else: + out.write(' ngettext(%r, %r, count) ' % (singular, plural)) for part in singular: out.write(blankout(part, 'S')) for part in plural: out.write(blankout(part, 'P')) else: if message_context: - out.write(' pgettext(%r, %r) ' % (message_context, ''.join(singular))) + out.write(' pgettext(%r, %r) ' % (message_context, singular)) else: - out.write(' gettext(%r) ' % ''.join(singular)) + out.write(' gettext(%r) ' % singular) for part in singular: out.write(blankout(part, 'S')) message_context = None @@ -678,6 +689,7 @@ def templatize(src, origin=None): message_context = message_context.strip("'") intrans = True inplural = False + trimmed = 'trimmed' in t.split_contents() singular = [] plural = [] elif cmatches: diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 8c5e81d0f3d..9501da7ca1b 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -342,6 +342,15 @@ Internationalization still read from in 1.7. Sessions will be migrated to the new ``_language`` key as they are written. +* The :ttag:`blocktrans` now supports a ``trimmed`` option. This + option will remove newline characters from the beginning and the end of the + content of the ``{% blocktrans %}`` tag, replace any whitespace at the + beginning and end of a line and merge all lines into one using a space + character to separate them. This is quite useful for indenting the content of + a ``{% blocktrans %}`` tag without having the indentation characters end up + in the corresponding entry in the PO file, which makes the translation + process easier. + Management Commands ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 41887d16ba2..6b862bb6d94 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -674,6 +674,30 @@ markers` using the ``context`` keyword: {% blocktrans with name=user.username context "greeting" %}Hi {{ name }}{% endblocktrans %} +Another feature ``{% blocktrans %}`` supports is the ``trimmed`` option. This +option will remove newline characters from the beginning and the end of the +content of the ``{% blocktrans %}`` tag, replace any whitespace at the beginning +and end of a line and merge all lines into one using a space character to +separate them. This is quite useful for indenting the content of a ``{% +blocktrans %}`` tag without having the indentation characters end up in the +corresponding entry in the PO file, which makes the translation process easier. + +For instance, the following ``{% blocktrans %}`` tag:: + + {% blocktrans trimmed %} + First sentence. + Second paragraph. + {% endblocktrans %} + +will result in the entry ``"First sentence. Second paragraph."`` in the PO file, +compared to ``"\n First sentence.\n Second sentence.\n"``, if the ``trimmed`` +option had not been specified. + +.. versionchanged:: 1.7 + + The ``trimmed`` option was added. + + String literals passed to tags and filters ------------------------------------------ diff --git a/tests/i18n/commands/templates/test.html b/tests/i18n/commands/templates/test.html index 6cb4493ef66..bd777285498 100644 --- a/tests/i18n/commands/templates/test.html +++ b/tests/i18n/commands/templates/test.html @@ -82,3 +82,15 @@ continued here.{% endcomment %} {% trans "Translatable literal with context wrapped in double quotes" context "Context wrapped in double quotes" as var %} {% blocktrans context 'Special blocktrans context wrapped in single quotes' %}Translatable literal with context wrapped in single quotes{% endblocktrans %} {% blocktrans context "Special blocktrans context wrapped in double quotes" %}Translatable literal with context wrapped in double quotes{% endblocktrans %} + + +{# BasicExtractorTests.test_blocktrans_trimmed #} +{% blocktrans %} + Text with a few + line breaks. +{% endblocktrans %} +{% blocktrans trimmed %} + Again some text with a few + line breaks, this time + should be trimmed. +{% endblocktrans %} diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index 633184f7c54..87535540aea 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -121,6 +121,17 @@ class BasicExtractorTests(ExtractorTests): self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents) self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents) + def test_blocktrans_trimmed(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE)) + with open(self.PO_FILE, 'r') as fp: + po_contents = force_text(fp.read()) + # should not be trimmed + self.assertNotMsgId('Text with a few line breaks.', po_contents) + # should be trimmed + self.assertMsgId("Again some text with a few line breaks, this time should be trimmed.", po_contents) + def test_force_en_us_locale(self): """Value of locale-munging option used by the command is the right one""" from django.core.management.commands.makemessages import Command diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 42e743dedfb..32eadfbb466 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -245,6 +245,17 @@ class TranslationTests(TransRealMixin, TestCase): rendered = t.render(Context()) self.assertEqual(rendered, 'Andere: Es gibt 5 Kommentare') + # Using trimmed + t = Template('{% load i18n %}{% blocktrans trimmed %}\n\nThere\n\t are 5 \n\n comments\n{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'There are 5 comments') + t = Template('{% load i18n %}{% blocktrans with num_comments=5 context "comment count" trimmed %}\n\nThere are \t\n \t {{ num_comments }} comments\n\n{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Es gibt 5 Kommentare') + t = Template('{% load i18n %}{% blocktrans context "other super search" count number=2 trimmed %}\n{{ number }} super \n result{% plural %}{{ number }} super results{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, '2 andere Super-Ergebnisse') + # Mis-uses self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}') self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context %}{% endblocktrans %}')