diff --git a/django/template/base.py b/django/template/base.py index 71a97fda80..f15f8a6ff6 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -825,10 +825,13 @@ class Variable(object): # We're dealing with a literal, so it's already been "resolved" value = self.literal if self.translate: + is_safe = isinstance(value, SafeData) + msgid = value.replace('%', '%%') + msgid = mark_safe(msgid) if is_safe else msgid if self.message_context: - return pgettext_lazy(self.message_context, value) + return pgettext_lazy(self.message_context, msgid) else: - return ugettext_lazy(value) + return ugettext_lazy(msgid) return value def __repr__(self): diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 4e77aaece4..050ef2c741 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -7,6 +7,7 @@ from django.template import Library, Node, TemplateSyntaxError, Variable from django.template.base import TOKEN_TEXT, TOKEN_VAR, render_value_in_context from django.template.defaulttags import token_kwargs from django.utils import six, translation +from django.utils.safestring import SafeData, mark_safe register = Library() @@ -86,6 +87,11 @@ class TranslateNode(Node): self.message_context.resolve(context)) output = self.filter_expression.resolve(context) value = render_value_in_context(output, context) + # Restore percent signs. Percent signs in template text are doubled + # so they are not interpreted as string format flags. + is_safe = isinstance(value, SafeData) + value = value.replace('%%', '%') + value = mark_safe(value) if is_safe else value if self.asvar: context[self.asvar] = value return '' diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 8a2527022d..e05b5613a5 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -534,7 +534,6 @@ block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*? endblock_re = re.compile(r"""^\s*endblocktrans$""") plural_re = re.compile(r"""^\s*plural$""") constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""") -one_percent_re = re.compile(r"""(?` nor :class:`Context.current_app ` are set, the :ttag:`url` template tag will now use the namespace of the current request. diff --git a/tests/i18n/commands/locale/fr/LC_MESSAGES/django.po b/tests/i18n/commands/locale/fr/LC_MESSAGES/django.po index 26bf6f133c..ace094f168 100644 --- a/tests/i18n/commands/locale/fr/LC_MESSAGES/django.po +++ b/tests/i18n/commands/locale/fr/LC_MESSAGES/django.po @@ -17,55 +17,5 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -#. Translators: Django template comment for translators -#: templates/test.html:9 -#, python-format -msgid "I think that 100%% is more that 50%% of anything." -msgstr "" - -#: templates/test.html:10 -#, python-format -msgid "I think that 100%% is more that 50%% of %(obj)s." -msgstr "" - -#: templates/test.html:70 -#, python-format -msgid "Literal with a percent symbol at the end %%" -msgstr "" - -#: templates/test.html:71 -#, python-format -msgid "Literal with a percent %% symbol in the middle" -msgstr "" - -#: templates/test.html:72 -#, python-format -msgid "Completed 50%% of all the tasks" -msgstr "" - -#: templates/test.html:73 -#, python-format -msgctxt "ctx0" -msgid "Completed 99%% of all the tasks" -msgstr "" - -#: templates/test.html:74 -#, python-format -msgid "Shouldn't double escape this sequence: %% (two percent signs)" -msgstr "" - -#: templates/test.html:75 -#, python-format -msgctxt "ctx1" -msgid "Shouldn't double escape this sequence %% either" -msgstr "" - -#: templates/test.html:76 -#, python-format -msgid "Looks like a str fmt spec %%s but shouldn't be interpreted as such" -msgstr "Translation of the above string" - -#: templates/test.html:77 -#, python-format -msgid "Looks like a str fmt spec %% o but shouldn't be interpreted as such" -msgstr "Translation contains %% for the above string" +msgid "year" +msgstr "année" diff --git a/tests/i18n/commands/locale/hr/LC_MESSAGES/django.po b/tests/i18n/commands/locale/hr/LC_MESSAGES/django.po index 663ca0000f..556dded3bd 100644 --- a/tests/i18n/commands/locale/hr/LC_MESSAGES/django.po +++ b/tests/i18n/commands/locale/hr/LC_MESSAGES/django.po @@ -17,55 +17,5 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -#. Translators: Django template comment for translators -#: templates/test.html:9 -#, python-format -msgid "I think that 100%% is more that 50%% of anything." -msgstr "" - -#: templates/test.html:10 -#, python-format -msgid "I think that 100%% is more that 50%% of %(obj)s." -msgstr "" - -#: templates/test.html:70 -#, python-format -msgid "Literal with a percent symbol at the end %%" -msgstr "" - -#: templates/test.html:71 -#, python-format -msgid "Literal with a percent %% symbol in the middle" -msgstr "" - -#: templates/test.html:72 -#, python-format -msgid "Completed 50%% of all the tasks" -msgstr "" - -#: templates/test.html:73 -#, python-format -msgctxt "ctx0" -msgid "Completed 99%% of all the tasks" -msgstr "" - -#: templates/test.html:74 -#, python-format -msgid "Shouldn't double escape this sequence: %% (two percent signs)" -msgstr "" - -#: templates/test.html:75 -#, python-format -msgctxt "ctx1" -msgid "Shouldn't double escape this sequence %% either" -msgstr "" - -#: templates/test.html:76 -#, python-format -msgid "Looks like a str fmt spec %%s but shouldn't be interpreted as such" -msgstr "Translation of the above string" - -#: templates/test.html:77 -#, python-format -msgid "Looks like a str fmt spec %% o but shouldn't be interpreted as such" -msgstr "Translation contains %% for the above string" +msgid "hello world" +msgstr "bok svijete" diff --git a/tests/i18n/commands/locale/it/LC_MESSAGES/django.po b/tests/i18n/commands/locale/it/LC_MESSAGES/django.po deleted file mode 100644 index 445199580d..0000000000 --- a/tests/i18n/commands/locale/it/LC_MESSAGES/django.po +++ /dev/null @@ -1,30 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-12-04 04:59-0600\n" -"PO-Revision-Date: 2011-12-10 20:29-0300\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -#, python-format -msgid "Completed 50%% of all the tasks" -msgstr "IT translation of Completed 50%% of all the tasks" - -#, python-format -msgid "Looks like a str fmt spec %%s but shouldn't be interpreted as such" -msgstr "Translation of the above string" - -#, python-format -msgid "Looks like a str fmt spec %% o but shouldn't be interpreted as such" -msgstr "IT translation contains %% for the above string" diff --git a/tests/i18n/commands/templates/test.html b/tests/i18n/commands/templates/test.html index 32920476e2..9901415496 100644 --- a/tests/i18n/commands/templates/test.html +++ b/tests/i18n/commands/templates/test.html @@ -5,10 +5,6 @@ string's meaning unveiled {% trans "This literal should be included." %} {% trans "This literal should also be included wrapped or not wrapped depending on the use of the --no-wrap option." %} -{# Translators: Django template comment for translators #} -

{% blocktrans %}I think that 100% is more that 50% of anything.{% endblocktrans %}

-{% blocktrans with 'txt' as obj %}I think that 100% is more that 50% of {{ obj }}.{% endblocktrans %} - {% comment %}Some random comment Some random comment Translators: One-line translator comment #1 @@ -67,17 +63,6 @@ continued here.{% endcomment %} {% blocktrans context "Special blocktrans context #3" count 2 %}Translatable literal #8c-singular{% plural %}Translatable literal #8c-plural{% endblocktrans %} {% blocktrans with a=1 context "Special blocktrans context #4" %}Translatable literal #8d {{ a }}{% endblocktrans %} -{% blocktrans with a=1 %}Blocktrans extraction shouldn't double escape this: %%, a={{ a }}{% endblocktrans %} - -{% trans "Literal with a percent symbol at the end %" %} -{% trans "Literal with a percent % symbol in the middle" %} -{% trans "Completed 50% of all the tasks" %} -{% trans "Completed 99% of all the tasks" context "ctx0" %} -{% trans "Shouldn't double escape this sequence: %% (two percent signs)" %} -{% trans "Shouldn't double escape this sequence %% either" context "ctx1" %} -{% trans "Looks like a str fmt spec %s but shouldn't be interpreted as such" %} -{% trans "Looks like a str fmt spec % o but shouldn't be interpreted as such" %} - {% trans "Translatable literal with context wrapped in single quotes" context 'Context wrapped in single quotes' as var %} {% 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 %} @@ -94,4 +79,4 @@ continued here.{% endcomment %} line breaks, this time should be trimmed. {% endblocktrans %} -{% trans "I'm on line 97" %} +{% trans "I'm on line 82" %} diff --git a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..ebc475caa2 Binary files /dev/null and b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo differ diff --git a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..734f139b5c --- /dev/null +++ b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,52 @@ +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" + +#: templates/percents.html:3 +#, python-format +msgid "Literal with a percent symbol at the end %%" +msgstr "Littérale avec un symbole de pour cent à la fin %%" + +#: templates/percents.html:4 +#, python-format +msgid "Literal with a percent %% symbol in the middle" +msgstr "Pour cent littérale %% avec un symbole au milieu" + +#: templates/percents.html:6 +#, python-format +msgid "It is 100%%" +msgstr "Il est de 100%%" + +#: templates/percents.html:7 +#, python-format +msgctxt "female" +msgid "It is 100%%" +msgstr "Elle est de 100%%" + +#: templates/percents.html:8 +#, python-format +msgid "Looks like a str fmt spec %%s but should not be interpreted as such" +msgstr "" +"On dirait un spec str fmt %%s mais ne devrait pas être interprété comme plus " +"disponible" + +#: templates/percents.html:9 +#, python-format +msgid "Looks like a str fmt spec %% o but should not be interpreted as such" +msgstr "" +"On dirait un spec str fmt %% o mais ne devrait pas être interprété comme " +"plus disponible" + +#: templates/percents.html:11 +#, python-format +msgid "1 percent sign %%, 2 percent signs %%%%, 3 percent signs %%%%%%" +msgstr "" +"1 %% signe pour cent, signes %%%% 2 pour cent, trois signes de pourcentage %%" +"%%%%" + +#: templates/percents.html:12 +#, python-format +msgid "%(name)s says: 1 percent sign %%, 2 percent signs %%%%" +msgstr "%(name)s dit: 1 pour cent signe %%, deux signes de pourcentage %%%%" diff --git a/tests/i18n/sampleproject/manage.py b/tests/i18n/sampleproject/manage.py new file mode 100755 index 0000000000..87a0ec369a --- /dev/null +++ b/tests/i18n/sampleproject/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import os +import sys + +sys.path.append(os.path.abspath(os.path.join('..', '..', '..'))) + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sampleproject.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/tests/i18n/sampleproject/sampleproject/__init__.py b/tests/i18n/sampleproject/sampleproject/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/i18n/sampleproject/sampleproject/settings.py b/tests/i18n/sampleproject/sampleproject/settings.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/i18n/sampleproject/templates/percents.html b/tests/i18n/sampleproject/templates/percents.html new file mode 100644 index 0000000000..506c0610c5 --- /dev/null +++ b/tests/i18n/sampleproject/templates/percents.html @@ -0,0 +1,12 @@ +{% load i18n %} + +{% trans "Literal with a percent symbol at the end %" %} +{% trans "Literal with a percent % symbol in the middle" %} + +{% trans "It is 100%" %} +{% trans "It is 100%" context "female" %} +{% trans "Looks like a str fmt spec %s but should not be interpreted as such" %} +{% trans "Looks like a str fmt spec % o but should not be interpreted as such" %} + +{% trans "1 percent sign %, 2 percent signs %%, 3 percent signs %%%" %} +{% blocktrans with name="Simon" %}{{name}} says: 1 percent sign %, 2 percent signs %%{% endblocktrans %} diff --git a/tests/i18n/sampleproject/update_catalogs.py b/tests/i18n/sampleproject/update_catalogs.py new file mode 100755 index 0000000000..131d3e268b --- /dev/null +++ b/tests/i18n/sampleproject/update_catalogs.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +""" +Helper script to update sampleproject's translation catalogs. + +When a bug has been identified related to i18n, this helps capture the issue +by using catalogs created from management commands. + +Example: + +The string "Two %% Three %%%" renders differently using trans and blocktrans. +This issue is difficult to debug, it could be a problem with extraction, +interpolation, or both. + +How this script helps: + * Add {% trans "Two %% Three %%%" %} and blocktrans equivalent to templates. + * Run this script. + * Test extraction - verify the new msgid in sampleproject's django.po. + * Add a translation to sampleproject's django.po. + * Run this script. + * Test interpolation - verify templatetag rendering, test each in a template + that is rendered using an activated language from sampleproject's locale. + * Tests should fail, issue captured. + * Fix issue. + * Run this script. + * Tests all pass. +""" + +import os +import re +import sys + +proj_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(proj_dir, '..', '..', '..'))) + + +def update_translation_catalogs(): + """Run makemessages and compilemessages in sampleproject.""" + from django.core.management import call_command + + prev_cwd = os.getcwd() + + os.chdir(proj_dir) + call_command('makemessages') + call_command('compilemessages') + + # keep the diff friendly - remove 'POT-Creation-Date' + pofile = os.path.join(proj_dir, 'locale', 'fr', 'LC_MESSAGES', 'django.po') + + with open(pofile) as f: + content = f.read() + content = re.sub(r'^"POT-Creation-Date.+$\s', '', content, flags=re.MULTILINE) + with open(pofile, 'w') as f: + f.write(content) + + os.chdir(prev_cwd) + + +if __name__ == "__main__": + update_translation_catalogs() diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py index df2911868e..e784b0a991 100644 --- a/tests/i18n/test_compilation.py +++ b/tests/i18n/test_compilation.py @@ -81,31 +81,6 @@ class PoFileContentsTests(MessageCompilationTests): self.assertTrue(os.path.exists(self.MO_FILE)) -class PercentRenderingTests(MessageCompilationTests): - # Ticket #11240 -- Testing rendering doesn't belong here but we are trying - # to keep tests for all the stack together - - LOCALE = 'it' - MO_FILE = 'locale/%s/LC_MESSAGES/django.mo' % LOCALE - - def setUp(self): - super(PercentRenderingTests, self).setUp() - self.addCleanup(os.unlink, os.path.join(self.test_dir, self.MO_FILE)) - - def test_percent_symbol_escaping(self): - with override_settings(LOCALE_PATHS=[os.path.join(self.test_dir, 'locale')]): - from django.template import Template, Context - call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO()) - with translation.override(self.LOCALE): - t = Template('{% load i18n %}{% trans "Looks like a str fmt spec %% o but shouldn\'t be interpreted as such" %}') - rendered = t.render(Context({})) - self.assertEqual(rendered, 'IT translation contains %% for the above string') - - t = Template('{% load i18n %}{% trans "Completed 50%% of all the tasks" %}') - rendered = t.render(Context({})) - self.assertEqual(rendered, 'IT translation of Completed 50%% of all the tasks') - - class MultipleLocaleCompilationTests(MessageCompilationTests): MO_FILE_HR = None diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index 556cba7fa4..62d9361b9f 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -149,54 +149,22 @@ class BasicExtractorTests(ExtractorTests): self.assertTrue(os.path.exists(self.PO_FILE)) with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp: po_contents = fp.read() - self.assertIn('#. Translators: This comment should be extracted', po_contents) self.assertNotIn('This comment should not be extracted', po_contents) - # Comments in templates - self.assertIn('#. Translators: Django template comment for translators', po_contents) - self.assertIn("#. Translators: Django comment block for translators\n#. string's meaning unveiled", po_contents) + # Comments in templates + self.assertIn('#. Translators: This comment should be extracted', po_contents) + self.assertIn("#. Translators: Django comment block for translators\n#. string's meaning unveiled", po_contents) self.assertIn('#. Translators: One-line translator comment #1', po_contents) self.assertIn('#. Translators: Two-line translator comment #1\n#. continued here.', po_contents) - self.assertIn('#. Translators: One-line translator comment #2', po_contents) self.assertIn('#. Translators: Two-line translator comment #2\n#. continued here.', po_contents) - self.assertIn('#. Translators: One-line translator comment #3', po_contents) self.assertIn('#. Translators: Two-line translator comment #3\n#. continued here.', po_contents) - self.assertIn('#. Translators: One-line translator comment #4', po_contents) self.assertIn('#. Translators: Two-line translator comment #4\n#. continued here.', po_contents) - self.assertIn('#. Translators: One-line translator comment #5 -- with non ASCII characters: áéíóúö', po_contents) self.assertIn('#. Translators: Two-line translator comment #5 -- with non ASCII characters: áéíóúö\n#. continued here.', po_contents) - def test_templatize_trans_tag(self): - # ticket #11240 - 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()) - self.assertMsgId('Literal with a percent symbol at the end %%', po_contents) - self.assertMsgId('Literal with a percent %% symbol in the middle', po_contents) - self.assertMsgId('Completed 50%% of all the tasks', po_contents) - self.assertMsgId('Completed 99%% of all the tasks', po_contents) - self.assertMsgId("Shouldn't double escape this sequence: %% (two percent signs)", po_contents) - self.assertMsgId("Shouldn't double escape this sequence %% either", po_contents) - self.assertMsgId("Looks like a str fmt spec %%s but shouldn't be interpreted as such", po_contents) - self.assertMsgId("Looks like a str fmt spec %% o but shouldn't be interpreted as such", po_contents) - - def test_templatize_blocktrans_tag(self): - # ticket #11966 - 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()) - self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents) - 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) @@ -208,8 +176,8 @@ class BasicExtractorTests(ExtractorTests): # should be trimmed self.assertMsgId("Again some text with a few line breaks, this time should be trimmed.", po_contents) # #21406 -- Should adjust for eaten line numbers - self.assertMsgId("I'm on line 97", po_contents) - self.assertLocationCommentPresent(self.PO_FILE, 97, 'templates', 'test.html') + self.assertMsgId("I'm on line 82", po_contents) + self.assertLocationCommentPresent(self.PO_FILE, 82, 'templates', 'test.html') def test_force_en_us_locale(self): """Value of locale-munging option used by the command is the right one""" diff --git a/tests/i18n/test_percents.py b/tests/i18n/test_percents.py new file mode 100644 index 0000000000..4ff8e71171 --- /dev/null +++ b/tests/i18n/test_percents.py @@ -0,0 +1,157 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +import os + +from django.template import Context, Template +from django.test import SimpleTestCase, override_settings +from django.utils._os import upath +from django.utils.encoding import force_text +from django.utils.translation import activate, get_language, trans_real + +from .test_extraction import ExtractorTests + +SAMPLEPROJECT_DIR = os.path.join(os.path.dirname(os.path.abspath(upath(__file__))), 'sampleproject') +SAMPLEPROJECT_LOCALE = os.path.join(SAMPLEPROJECT_DIR, 'locale') + + +@override_settings(LOCALE_PATHS=[SAMPLEPROJECT_LOCALE]) +class FrenchTestCase(SimpleTestCase): + """Tests using the French translations of the sampleproject.""" + + PO_FILE = os.path.join(SAMPLEPROJECT_LOCALE, 'fr', 'LC_MESSAGES', 'django.po') + + def setUp(self): + self._language = get_language() + self._translations = trans_real._translations + activate('fr') + + def tearDown(self): + trans_real._translations = self._translations + activate(self._language) + + +class ExtractingStringsWithPercentSigns(FrenchTestCase, ExtractorTests): + """ + Tests the extracted string found in the gettext catalog. + + Ensures that percent signs are python formatted. + + These tests should all have an analogous translation tests below, ensuring + the python formatting does not persist through to a rendered template. + """ + + def setUp(self): + super(ExtractingStringsWithPercentSigns, self).setUp() + with open(self.PO_FILE, 'r') as fp: + self.po_contents = force_text(fp.read()) + + def test_trans_tag_with_percent_symbol_at_the_end(self): + self.assertMsgId('Literal with a percent symbol at the end %%', self.po_contents) + + def test_trans_tag_with_percent_symbol_in_the_middle(self): + self.assertMsgId('Literal with a percent %% symbol in the middle', self.po_contents) + self.assertMsgId('It is 100%%', self.po_contents) + + def test_trans_tag_with_string_that_look_like_fmt_spec(self): + self.assertMsgId('Looks like a str fmt spec %%s but should not be interpreted as such', self.po_contents) + self.assertMsgId('Looks like a str fmt spec %% o but should not be interpreted as such', self.po_contents) + + def test_adds_python_format_to_all_percent_signs(self): + self.assertMsgId('1 percent sign %%, 2 percent signs %%%%, 3 percent signs %%%%%%', self.po_contents) + self.assertMsgId('%(name)s says: 1 percent sign %%, 2 percent signs %%%%', self.po_contents) + + +class RenderingTemplatesWithPercentSigns(FrenchTestCase): + """ + Test rendering of templates that use percent signs. + + Ensures both trans and blocktrans tags behave consistently. + + Refs #11240, #11966, #24257 + """ + + def test_translates_with_a_percent_symbol_at_the_end(self): + expected = 'Littérale avec un symbole de pour cent à la fin %' + + trans_tpl = Template('{% load i18n %}{% trans "Literal with a percent symbol at the end %" %}') + self.assertEqual(trans_tpl.render(Context({})), expected) + + block_tpl = Template( + '{% load i18n %}{% blocktrans %}Literal with a percent symbol at ' + 'the end %{% endblocktrans %}' + ) + self.assertEqual(block_tpl.render(Context({})), expected) + + def test_translates_with_percent_symbol_in_the_middle(self): + expected = 'Pour cent littérale % avec un symbole au milieu' + + trans_tpl = Template('{% load i18n %}{% trans "Literal with a percent % symbol in the middle" %}') + self.assertEqual(trans_tpl.render(Context({})), expected) + + block_tpl = Template( + '{% load i18n %}{% blocktrans %}Literal with a percent % symbol ' + 'in the middle{% endblocktrans %}' + ) + self.assertEqual(block_tpl.render(Context({})), expected) + + def test_translates_with_percent_symbol_using_context(self): + trans_tpl = Template('{% load i18n %}{% trans "It is 100%" %}') + self.assertEqual(trans_tpl.render(Context({})), 'Il est de 100%') + trans_tpl = Template('{% load i18n %}{% trans "It is 100%" context "female" %}') + self.assertEqual(trans_tpl.render(Context({})), 'Elle est de 100%') + + block_tpl = Template('{% load i18n %}{% blocktrans %}It is 100%{% endblocktrans %}') + self.assertEqual(block_tpl.render(Context({})), 'Il est de 100%') + block_tpl = Template('{% load i18n %}{% blocktrans context "female" %}It is 100%{% endblocktrans %}') + self.assertEqual(block_tpl.render(Context({})), 'Elle est de 100%') + + def test_translates_with_string_that_look_like_fmt_spec_with_trans(self): + # tests "%s" + expected = ('On dirait un spec str fmt %s mais ne devrait pas être interprété comme plus disponible') + trans_tpl = Template( + '{% load i18n %}{% trans "Looks like a str fmt spec %s but ' + 'should not be interpreted as such" %}' + ) + self.assertEqual(trans_tpl.render(Context({})), expected) + block_tpl = Template( + '{% load i18n %}{% blocktrans %}Looks like a str fmt spec %s but ' + 'should not be interpreted as such{% endblocktrans %}' + ) + self.assertEqual(block_tpl.render(Context({})), expected) + + # tests "% o" + expected = ('On dirait un spec str fmt % o mais ne devrait pas être interprété comme plus disponible') + trans_tpl = Template( + '{% load i18n %}{% trans "Looks like a str fmt spec % o but should not be ' + 'interpreted as such" %}' + ) + self.assertEqual(trans_tpl.render(Context({})), expected) + block_tpl = Template( + '{% load i18n %}{% blocktrans %}Looks like a str fmt spec % o but should not be ' + 'interpreted as such{% endblocktrans %}' + ) + self.assertEqual(block_tpl.render(Context({})), expected) + + def test_translates_multiple_percent_signs(self): + expected = ('1 % signe pour cent, signes %% 2 pour cent, trois signes de pourcentage %%%') + + trans_tpl = Template( + '{% load i18n %}{% trans "1 percent sign %, 2 percent signs %%, ' + '3 percent signs %%%" %}' + ) + self.assertEqual(trans_tpl.render(Context({})), expected) + block_tpl = Template( + '{% load i18n %}{% blocktrans %}1 percent sign %, 2 percent signs ' + '%%, 3 percent signs %%%{% endblocktrans %}' + ) + self.assertEqual(block_tpl.render(Context({})), expected) + + block_tpl = Template( + '{% load i18n %}{% blocktrans %}{{name}} says: 1 percent sign %, ' + '2 percent signs %%{% endblocktrans %}' + ) + self.assertEqual( + block_tpl.render(Context({"name": "Django"})), + 'Django dit: 1 pour cent signe %, deux signes de pourcentage %%' + ) diff --git a/tests/template_tests/syntax_tests/test_basic.py b/tests/template_tests/syntax_tests/test_basic.py index b72d6e6414..da45fe2839 100644 --- a/tests/template_tests/syntax_tests/test_basic.py +++ b/tests/template_tests/syntax_tests/test_basic.py @@ -323,3 +323,12 @@ class BasicSyntaxTests(SimpleTestCase): msg = "Unclosed tag 'if'. Looking for one of: elif, else, endif." with self.assertRaisesMessage(TemplateSyntaxError, msg): self.engine.render_to_string('template') + + @setup({'tpl-str': '%s', 'tpl-percent': '%%', 'tpl-weird-percent': '% %s'}) + def test_ignores_strings_that_look_like_format_interpolation(self): + output = self.engine.render_to_string('tpl-str') + self.assertEqual(output, '%s') + output = self.engine.render_to_string('tpl-percent') + self.assertEqual(output, '%%') + output = self.engine.render_to_string('tpl-weird-percent') + self.assertEqual(output, '% %s') diff --git a/tests/template_tests/syntax_tests/test_i18n.py b/tests/template_tests/syntax_tests/test_i18n.py index 6013251366..4fbbfffd78 100644 --- a/tests/template_tests/syntax_tests/test_i18n.py +++ b/tests/template_tests/syntax_tests/test_i18n.py @@ -511,3 +511,13 @@ class I18nTagTests(SimpleTestCase): msg = "The 'noop' option was specified more than once." with self.assertRaisesMessage(TemplateSyntaxError, msg): self.engine.render_to_string('template') + + @setup({'template': '{% load i18n %}{% trans "%s" %}'}) + def test_trans_tag_using_a_string_that_looks_like_str_fmt(self): + output = self.engine.render_to_string('template') + self.assertEqual(output, '%s') + + @setup({'template': '{% load i18n %}{% blocktrans %}%s{% endblocktrans %}'}) + def test_blocktrans_tag_using_a_string_that_looks_like_str_fmt(self): + output = self.engine.render_to_string('template') + self.assertEqual(output, '%s')