Fixed #19160 -- Made lazy plural translations usable.

Many thanks to Alexey Boriskin, Claude Paroz and Julien Phalip.
This commit is contained in:
Aymeric Augustin 2013-01-30 20:28:16 +01:00
parent 47ddd6a408
commit 3f1a0c0040
7 changed files with 143 additions and 9 deletions

View File

@ -157,8 +157,7 @@ def lazy(func, *resultclasses):
return bytes(self) % rhs return bytes(self) % rhs
elif self._delegate_text: elif self._delegate_text:
return six.text_type(self) % rhs return six.text_type(self) % rhs
else: return self.__cast() % rhs
raise AssertionError('__mod__ not supported for non-string types')
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
# Instances of this class are effectively immutable. It's just a # Instances of this class are effectively immutable. It's just a

View File

@ -85,11 +85,40 @@ def npgettext(context, singular, plural, number):
return _trans.npgettext(context, singular, plural, number) return _trans.npgettext(context, singular, plural, number)
gettext_lazy = lazy(gettext, str) gettext_lazy = lazy(gettext, str)
ngettext_lazy = lazy(ngettext, str)
ugettext_lazy = lazy(ugettext, six.text_type) ugettext_lazy = lazy(ugettext, six.text_type)
ungettext_lazy = lazy(ungettext, six.text_type)
pgettext_lazy = lazy(pgettext, six.text_type) pgettext_lazy = lazy(pgettext, six.text_type)
npgettext_lazy = lazy(npgettext, six.text_type)
def lazy_number(func, resultclass, number=None, **kwargs):
if isinstance(number, int):
kwargs['number'] = number
proxy = lazy(func, resultclass)(**kwargs)
else:
class NumberAwareString(resultclass):
def __mod__(self, rhs):
if isinstance(rhs, dict) and number:
try:
number_value = rhs[number]
except KeyError:
raise KeyError('Your dictionary lacks key \'%s\'. '
'Please provide it, because it is required to '
'determine whether string is singular or plural.'
% number)
else:
number_value = rhs
kwargs['number'] = number_value
return func(**kwargs) % rhs
proxy = lazy(lambda **kwargs: NumberAwareString(), NumberAwareString)(**kwargs)
return proxy
def ngettext_lazy(singular, plural, number=None):
return lazy_number(ngettext, str, singular=singular, plural=plural, number=number)
def ungettext_lazy(singular, plural, number=None):
return lazy_number(ungettext, six.text_type, singular=singular, plural=plural, number=number)
def npgettext_lazy(context, singular, plural, number=None):
return lazy_number(npgettext, six.text_type, context=context, singular=singular, plural=plural, number=number)
def activate(language): def activate(language):
return _trans.activate(language) return _trans.activate(language)

View File

@ -35,6 +35,10 @@ Minor features
:class:`~django.forms.URLField` use the new type attributes available in :class:`~django.forms.URLField` use the new type attributes available in
HTML5 (type='email', type='url'). HTML5 (type='email', type='url').
* The ``number`` argument for :ref:`lazy plural translations
<lazy-plural-translations>` can be provided at translation time rather than
at definition time.
Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================

View File

@ -414,6 +414,41 @@ convert them to strings, because they should be converted as late as possible
(so that the correct locale is in effect). This necessitates the use of the (so that the correct locale is in effect). This necessitates the use of the
helper function described next. helper function described next.
.. _lazy-plural-translations:
Lazy translations and plural
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 1.6
When using lazy translation for a plural string (``[u]n[p]gettext_lazy``), you
generally don't know the ``number`` argument at the time of the string
definition. Therefore, you are authorized to pass a key name instead of an
integer as the ``number`` argument. Then ``number`` will be looked up in the
dictionary under that key during string interpolation. Here's example::
class MyForm(forms.Form):
error_message = ungettext_lazy("You only provided %(num)d argument",
"You only provided %(num)d arguments", 'num')
def clean(self):
# ...
if error:
raise forms.ValidationError(self.error_message % {'num': number})
If the string contains exactly one unnamed placeholder, you can interpolate
directly with the ``number`` argument::
class MyForm(forms.Form):
error_message = ungettext_lazy("You provided %d argument",
"You provided %d arguments")
def clean(self):
# ...
if error:
raise forms.ValidationError(self.error_message % number)
Joining strings: string_concat() Joining strings: string_concat()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -41,6 +41,32 @@ msgid_plural "%d results"
msgstr[0] "%d Resultat" msgstr[0] "%d Resultat"
msgstr[1] "%d Resultate" msgstr[1] "%d Resultate"
#: models.py:11
msgid "%d good result"
msgid_plural "%d good results"
msgstr[0] "%d gutes Resultat"
msgstr[1] "%d guten Resultate"
#: models.py:11
msgctxt "Exclamation"
msgid "%d good result"
msgid_plural "%d good results"
msgstr[0] "%d gutes Resultat!"
msgstr[1] "%d guten Resultate!"
#: models.py:11
msgid "Hi %(name)s, %(num)d good result"
msgid_plural "Hi %(name)s, %(num)d good results"
msgstr[0] "Hallo %(name)s, %(num)d gutes Resultat"
msgstr[1] "Hallo %(name)s, %(num)d guten Resultate"
#: models.py:11
msgctxt "Greeting"
msgid "Hi %(name)s, %(num)d good result"
msgid_plural "Hi %(name)s, %(num)d good results"
msgstr[0] "Willkommen %(name)s, %(num)d gutes Resultat"
msgstr[1] "Willkommen %(name)s, %(num)d guten Resultate"
#: models.py:13 #: models.py:13
#, python-format #, python-format
msgid "The result was %(percent)s%%" msgid "The result was %(percent)s%%"

View File

@ -22,10 +22,15 @@ from django.utils._os import upath
from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText
from django.utils import six from django.utils import six
from django.utils.six import PY3 from django.utils.six import PY3
from django.utils.translation import (ugettext, ugettext_lazy, activate, from django.utils.translation import (activate, deactivate,
deactivate, gettext_lazy, pgettext, npgettext, to_locale, get_language, get_language_from_request, get_language_info,
get_language_info, get_language, get_language_from_request, trans_real) to_locale, trans_real,
gettext, gettext_lazy,
ugettext, ugettext_lazy,
ngettext, ngettext_lazy,
ungettext, ungettext_lazy,
pgettext, pgettext_lazy,
npgettext, npgettext_lazy)
from .commands.tests import can_run_extraction_tests, can_run_compilation_tests from .commands.tests import can_run_extraction_tests, can_run_compilation_tests
if can_run_extraction_tests: if can_run_extraction_tests:
@ -95,6 +100,42 @@ class TranslationTests(TestCase):
s2 = pickle.loads(pickle.dumps(s1)) s2 = pickle.loads(pickle.dumps(s1))
self.assertEqual(six.text_type(s2), "test") self.assertEqual(six.text_type(s2), "test")
@override_settings(LOCALE_PATHS=extended_locale_paths)
def test_ungettext_lazy(self):
s0 = ungettext_lazy("%d good result", "%d good results")
s1 = ngettext_lazy(str("%d good result"), str("%d good results"))
s2 = npgettext_lazy('Exclamation', '%d good result', '%d good results')
with translation.override('de'):
self.assertEqual(s0 % 1, "1 gutes Resultat")
self.assertEqual(s0 % 4, "4 guten Resultate")
self.assertEqual(s1 % 1, str("1 gutes Resultat"))
self.assertEqual(s1 % 4, str("4 guten Resultate"))
self.assertEqual(s2 % 1, "1 gutes Resultat!")
self.assertEqual(s2 % 4, "4 guten Resultate!")
s3 = ungettext_lazy("Hi %(name)s, %(num)d good result", "Hi %(name)s, %(num)d good results", 4)
s4 = ungettext_lazy("Hi %(name)s, %(num)d good result", "Hi %(name)s, %(num)d good results", 'num')
s5 = ngettext_lazy(str("Hi %(name)s, %(num)d good result"), str("Hi %(name)s, %(num)d good results"), 4)
s6 = ngettext_lazy(str("Hi %(name)s, %(num)d good result"), str("Hi %(name)s, %(num)d good results"), 'num')
s7 = npgettext_lazy('Greeting', "Hi %(name)s, %(num)d good result", "Hi %(name)s, %(num)d good results", 4)
s8 = npgettext_lazy('Greeting', "Hi %(name)s, %(num)d good result", "Hi %(name)s, %(num)d good results", 'num')
with translation.override('de'):
self.assertEqual(s3 % {'num': 4, 'name': 'Jim'}, "Hallo Jim, 4 guten Resultate")
self.assertEqual(s4 % {'name': 'Jim', 'num': 1}, "Hallo Jim, 1 gutes Resultat")
self.assertEqual(s4 % {'name': 'Jim', 'num': 5}, "Hallo Jim, 5 guten Resultate")
with six.assertRaisesRegex(self, KeyError, 'Your dictionary lacks key.*'):
s4 % {'name': 'Jim'}
self.assertEqual(s5 % {'num': 4, 'name': 'Jim'}, str("Hallo Jim, 4 guten Resultate"))
self.assertEqual(s6 % {'name': 'Jim', 'num': 1}, str("Hallo Jim, 1 gutes Resultat"))
self.assertEqual(s6 % {'name': 'Jim', 'num': 5}, str("Hallo Jim, 5 guten Resultate"))
with six.assertRaisesRegex(self, KeyError, 'Your dictionary lacks key.*'):
s6 % {'name': 'Jim'}
self.assertEqual(s7 % {'num': 4, 'name': 'Jim'}, "Willkommen Jim, 4 guten Resultate")
self.assertEqual(s8 % {'name': 'Jim', 'num': 1}, "Willkommen Jim, 1 gutes Resultat")
self.assertEqual(s8 % {'name': 'Jim', 'num': 5}, "Willkommen Jim, 5 guten Resultate")
with six.assertRaisesRegex(self, KeyError, 'Your dictionary lacks key.*'):
s8 % {'name': 'Jim'}
@override_settings(LOCALE_PATHS=extended_locale_paths) @override_settings(LOCALE_PATHS=extended_locale_paths)
def test_pgettext(self): def test_pgettext(self):
trans_real._active = local() trans_real._active = local()