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 "%%".
This commit is contained in:
Doug Beck 2015-04-15 17:01:11 -04:00 committed by Tim Graham
parent d772d812cf
commit b7508896fb
20 changed files with 340 additions and 217 deletions

View File

@ -825,10 +825,13 @@ class Variable(object):
# We're dealing with a literal, so it's already been "resolved" # We're dealing with a literal, so it's already been "resolved"
value = self.literal value = self.literal
if self.translate: if self.translate:
is_safe = isinstance(value, SafeData)
msgid = value.replace('%', '%%')
msgid = mark_safe(msgid) if is_safe else msgid
if self.message_context: if self.message_context:
return pgettext_lazy(self.message_context, value) return pgettext_lazy(self.message_context, msgid)
else: else:
return ugettext_lazy(value) return ugettext_lazy(msgid)
return value return value
def __repr__(self): def __repr__(self):

View File

@ -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.base import TOKEN_TEXT, TOKEN_VAR, render_value_in_context
from django.template.defaulttags import token_kwargs from django.template.defaulttags import token_kwargs
from django.utils import six, translation from django.utils import six, translation
from django.utils.safestring import SafeData, mark_safe
register = Library() register = Library()
@ -86,6 +87,11 @@ class TranslateNode(Node):
self.message_context.resolve(context)) self.message_context.resolve(context))
output = self.filter_expression.resolve(context) output = self.filter_expression.resolve(context)
value = render_value_in_context(output, 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: if self.asvar:
context[self.asvar] = value context[self.asvar] = value
return '' return ''

View File

@ -534,7 +534,6 @@ block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?
endblock_re = re.compile(r"""^\s*endblocktrans$""") endblock_re = re.compile(r"""^\s*endblocktrans$""")
plural_re = re.compile(r"""^\s*plural$""") plural_re = re.compile(r"""^\s*plural$""")
constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""") constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
one_percent_re = re.compile(r"""(?<!%)%(?!%)""")
def templatize(src, origin=None): def templatize(src, origin=None):
@ -631,7 +630,7 @@ def templatize(src, origin=None):
else: else:
singular.append('%%(%s)s' % t.contents) singular.append('%%(%s)s' % t.contents)
elif t.token_type == TOKEN_TEXT: elif t.token_type == TOKEN_TEXT:
contents = one_percent_re.sub('%%', t.contents) contents = t.contents.replace('%', '%%')
if inplural: if inplural:
plural.append(contents) plural.append(contents)
else: else:
@ -667,7 +666,7 @@ def templatize(src, origin=None):
g = g.strip('"') g = g.strip('"')
elif g[0] == "'": elif g[0] == "'":
g = g.strip("'") g = g.strip("'")
g = one_percent_re.sub('%%', g) g = g.replace('%', '%%')
if imatch.group(2): if imatch.group(2):
# A context is provided # A context is provided
context_match = context_re.match(imatch.group(2)) context_match = context_re.match(imatch.group(2))

View File

@ -940,6 +940,11 @@ Miscellaneous
whitespace by default. This can be disabled by setting the new whitespace by default. This can be disabled by setting the new
:attr:`~django.forms.CharField.strip` argument to ``False``. :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>` * If neither :attr:`request.current_app <django.http.HttpRequest.current_app>`
nor :class:`Context.current_app <django.template.Context>` are set, the nor :class:`Context.current_app <django.template.Context>` are set, the
:ttag:`url` template tag will now use the namespace of the current request. :ttag:`url` template tag will now use the namespace of the current request.

View File

@ -17,55 +17,5 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n"
#. Translators: Django template comment for translators msgid "year"
#: templates/test.html:9 msgstr "année"
#, 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"

View File

@ -17,55 +17,5 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n"
#. Translators: Django template comment for translators msgid "hello world"
#: templates/test.html:9 msgstr "bok svijete"
#, 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"

View File

@ -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"

View File

@ -5,10 +5,6 @@ string's meaning unveiled
{% trans "This literal should be included." %} {% 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." %} {% 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 {% comment %}Some random comment
Some random comment Some random comment
Translators: One-line translator comment #1 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 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 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 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 %} {% 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 single quotes' %}Translatable literal with context wrapped in single quotes{% endblocktrans %}
@ -94,4 +79,4 @@ continued here.{% endcomment %}
line breaks, this time line breaks, this time
should be trimmed. should be trimmed.
{% endblocktrans %} {% endblocktrans %}
{% trans "I'm on line 97" %} {% trans "I'm on line 82" %}

View File

@ -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 %%%%"

View File

@ -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)

View File

@ -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 %}

View File

@ -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()

View File

@ -81,31 +81,6 @@ class PoFileContentsTests(MessageCompilationTests):
self.assertTrue(os.path.exists(self.MO_FILE)) 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): class MultipleLocaleCompilationTests(MessageCompilationTests):
MO_FILE_HR = None MO_FILE_HR = None

View File

@ -149,54 +149,22 @@ class BasicExtractorTests(ExtractorTests):
self.assertTrue(os.path.exists(self.PO_FILE)) self.assertTrue(os.path.exists(self.PO_FILE))
with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp: with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp:
po_contents = fp.read() po_contents = fp.read()
self.assertIn('#. Translators: This comment should be extracted', po_contents)
self.assertNotIn('This comment should not 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: One-line translator comment #1', po_contents)
self.assertIn('#. Translators: Two-line translator comment #1\n#. continued here.', 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: One-line translator comment #2', po_contents)
self.assertIn('#. Translators: Two-line translator comment #2\n#. continued here.', 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: One-line translator comment #3', po_contents)
self.assertIn('#. Translators: Two-line translator comment #3\n#. continued here.', 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: One-line translator comment #4', po_contents)
self.assertIn('#. Translators: Two-line translator comment #4\n#. continued here.', 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: 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) 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): def test_blocktrans_trimmed(self):
os.chdir(self.test_dir) os.chdir(self.test_dir)
management.call_command('makemessages', locale=[LOCALE], verbosity=0) management.call_command('makemessages', locale=[LOCALE], verbosity=0)
@ -208,8 +176,8 @@ class BasicExtractorTests(ExtractorTests):
# should be trimmed # should be trimmed
self.assertMsgId("Again some text with a few line breaks, this time should be trimmed.", po_contents) self.assertMsgId("Again some text with a few line breaks, this time should be trimmed.", po_contents)
# #21406 -- Should adjust for eaten line numbers # #21406 -- Should adjust for eaten line numbers
self.assertMsgId("I'm on line 97", po_contents) self.assertMsgId("I'm on line 82", po_contents)
self.assertLocationCommentPresent(self.PO_FILE, 97, 'templates', 'test.html') self.assertLocationCommentPresent(self.PO_FILE, 82, 'templates', 'test.html')
def test_force_en_us_locale(self): def test_force_en_us_locale(self):
"""Value of locale-munging option used by the command is the right one""" """Value of locale-munging option used by the command is the right one"""

157
tests/i18n/test_percents.py Normal file
View File

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

View File

@ -323,3 +323,12 @@ class BasicSyntaxTests(SimpleTestCase):
msg = "Unclosed tag 'if'. Looking for one of: elif, else, endif." msg = "Unclosed tag 'if'. Looking for one of: elif, else, endif."
with self.assertRaisesMessage(TemplateSyntaxError, msg): with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.render_to_string('template') 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')

View File

@ -511,3 +511,13 @@ class I18nTagTests(SimpleTestCase):
msg = "The 'noop' option was specified more than once." msg = "The 'noop' option was specified more than once."
with self.assertRaisesMessage(TemplateSyntaxError, msg): with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.render_to_string('template') 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')