summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoug Beck <doug@douglasbeck.com>2015-04-15 17:01:11 -0400
committerTim Graham <timograham@gmail.com>2015-08-12 10:23:34 -0400
commitb7508896fbe19ec2cdeb81565cd587091b6b68d0 (patch)
tree5efb501dcc753cdaf1314ba826ca59cd44c4297b
parentd772d812cf583e0f99e21850f26addf1d59bfa35 (diff)
Fixed #24257 -- Corrected i18n handling of percent signs.
Refactored tests to use a sample project. Updated extraction: * Removed special handling of single percent signs. * When extracting messages from template text, doubled all percent signs so they are not interpreted by gettext as string format flags. All strings extracted by gettext, if containing a percent sign, will now be labeled "#, python-format". Updated translation: * Used "%%" for "%" in template text before calling gettext. * Updated {% trans %} rendering to restore "%" from "%%".
-rw-r--r--django/template/base.py7
-rw-r--r--django/templatetags/i18n.py6
-rw-r--r--django/utils/translation/trans_real.py5
-rw-r--r--docs/releases/1.9.txt5
-rw-r--r--tests/i18n/commands/locale/fr/LC_MESSAGES/django.po54
-rw-r--r--tests/i18n/commands/locale/hr/LC_MESSAGES/django.po54
-rw-r--r--tests/i18n/commands/locale/it/LC_MESSAGES/django.po30
-rw-r--r--tests/i18n/commands/templates/test.html17
-rw-r--r--tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mobin0 -> 1196 bytes
-rw-r--r--tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po52
-rwxr-xr-xtests/i18n/sampleproject/manage.py12
-rw-r--r--tests/i18n/sampleproject/sampleproject/__init__.py0
-rw-r--r--tests/i18n/sampleproject/sampleproject/settings.py0
-rw-r--r--tests/i18n/sampleproject/templates/percents.html12
-rwxr-xr-xtests/i18n/sampleproject/update_catalogs.py60
-rw-r--r--tests/i18n/test_compilation.py25
-rw-r--r--tests/i18n/test_extraction.py40
-rw-r--r--tests/i18n/test_percents.py157
-rw-r--r--tests/template_tests/syntax_tests/test_basic.py9
-rw-r--r--tests/template_tests/syntax_tests/test_i18n.py10
20 files changed, 339 insertions, 216 deletions
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"""(?<!%)%(?!%)""")
def templatize(src, origin=None):
@@ -631,7 +630,7 @@ def templatize(src, origin=None):
else:
singular.append('%%(%s)s' % t.contents)
elif t.token_type == TOKEN_TEXT:
- contents = one_percent_re.sub('%%', t.contents)
+ contents = t.contents.replace('%', '%%')
if inplural:
plural.append(contents)
else:
@@ -667,7 +666,7 @@ def templatize(src, origin=None):
g = g.strip('"')
elif g[0] == "'":
g = g.strip("'")
- g = one_percent_re.sub('%%', g)
+ g = g.replace('%', '%%')
if imatch.group(2):
# A context is provided
context_match = context_re.match(imatch.group(2))
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 4aab9aa070..f6ff0e6105 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -940,6 +940,11 @@ Miscellaneous
whitespace by default. This can be disabled by setting the new
:attr:`~django.forms.CharField.strip` argument to ``False``.
+* Template text that is translated and uses two or more consecutive percent
+ signs, e.g. ``"%%"``, may have a new `msgid` after ``makemessages`` is run
+ (most likely the translation will be marked fuzzy). The new ``msgid`` will be
+ marked ``"#, python-format"``.
+
* If neither :attr:`request.current_app <django.http.HttpRequest.current_app>`
nor :class:`Context.current_app <django.template.Context>` 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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\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 #}
-<p>{% blocktrans %}I think that 100% is more that 50% of anything.{% endblocktrans %}</p>
-{% 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
--- /dev/null
+++ b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo
Binary files 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
--- /dev/null
+++ b/tests/i18n/sampleproject/sampleproject/__init__.py
diff --git a/tests/i18n/sampleproject/sampleproject/settings.py b/tests/i18n/sampleproject/sampleproject/settings.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/i18n/sampleproject/sampleproject/settings.py
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: 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')