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 %}')