From 3f1a0c0040b9a950584f4b309a1b670b0e709de5 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 30 Jan 2013 20:28:16 +0100 Subject: [PATCH] Fixed #19160 -- Made lazy plural translations usable. Many thanks to Alexey Boriskin, Claude Paroz and Julien Phalip. --- django/utils/functional.py | 3 +- django/utils/translation/__init__.py | 35 +++++++++++-- docs/releases/1.6.txt | 4 ++ docs/topics/i18n/translation.txt | 35 +++++++++++++ .../other/locale/de/LC_MESSAGES/django.mo | Bin 1449 -> 1998 bytes .../other/locale/de/LC_MESSAGES/django.po | 26 ++++++++++ tests/regressiontests/i18n/tests.py | 49 ++++++++++++++++-- 7 files changed, 143 insertions(+), 9 deletions(-) diff --git a/django/utils/functional.py b/django/utils/functional.py index 661518e3cc..1b5200c98c 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -157,8 +157,7 @@ def lazy(func, *resultclasses): return bytes(self) % rhs elif self._delegate_text: return six.text_type(self) % rhs - else: - raise AssertionError('__mod__ not supported for non-string types') + return self.__cast() % rhs def __deepcopy__(self, memo): # Instances of this class are effectively immutable. It's just a diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index 803bbb746a..7a48376a52 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -85,11 +85,40 @@ def npgettext(context, singular, plural, number): return _trans.npgettext(context, singular, plural, number) gettext_lazy = lazy(gettext, str) -ngettext_lazy = lazy(ngettext, str) ugettext_lazy = lazy(ugettext, six.text_type) -ungettext_lazy = lazy(ungettext, 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): return _trans.activate(language) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 79fa3ffb86..5e1d959f60 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -35,6 +35,10 @@ Minor features :class:`~django.forms.URLField` use the new type attributes available in HTML5 (type='email', type='url'). +* The ``number`` argument for :ref:`lazy plural translations + ` can be provided at translation time rather than + at definition time. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 3cf08e7ddf..f45be3c63d 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -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 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() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo index f825e3918b7fd74b7af37e55e704b260101367b7..e208c8249f6b334e54e8a1f8061f9f60383d8874 100644 GIT binary patch literal 1998 zcmbu9&u<$=6vwCF7HWP~Z~z3VCIX|FGKtrrO0})4rin{RVq&Gs4{?dc-q+2hyEE3z zY@6^ma6{ZUaO4921qUi2{sRyfBu*Uo2l&p~PS!2pAV!+~%#Zipyf<&&{`_L?ONMp< z{dM%O&|miBGx(q#f_?A^ya+x7&x5~%Z-ak=Tj0ef7`q7`fZooxU=4f-2H;QN^Wd2$ z8CwM}fa~BU_!{^**a5!*p9X&e_h9=E(4W6^ma#X%ufbQr@4>ggU%*embLSZQ0New; zpWlHmfj@$8fWLw;#m+v(*d_2hxB*@UUjaV@FN0rz-tQkk9AA!~191#{2HnT*?M$1G z$=}_xlLZ{Z>gYZO-(aR}2>-af;75(c5t$(=*R+iWH#9P2F|)j}sEWL4xgH$Q(4AJ! zvPR4ktz($7MFzc@sbw3|(XvZ(V%@`GCUfaht!lGUk14%ph+L|Y+Ggtgk~uZ)I};CQ z?6Y|ScCN%+0n4I&nuNz6NPkn6D`b(6ZK(4cx5$UOP_71fLtLVlm%2CEwYc5skmXvr zeeOf6_2d!L4m#7_|6pdUXA5}5ElD%nua##G$l$c``ZO~>!H=(7Hr1=luOqd13?e!j!Cyy*Zh4f+R9)r6eQ7MY&WF)0apgqLyK|R}=>P@$5r>Wstgh zKP!yP#4T-d8*()&r?%6+%E!abqT&l39yIH0=1FqvD9hZWUM$~1Mc;EgmgY2aG!{ge z6-W)=Ez4St3mYryc!hjz7y0t1lsdk+y|ccy-QDZ2MVoh;WkJuVFHDyz`U~viG|TRl zrQ@A_w9ID=%$KcLW}7n0^n9EDCRR1KfvWGCga-bpJdg&KtG|H1yMeCdNjjJ$q522d z8viQrcVIo4vNaXsXA$z#$X6;`mTHRM@%1UDz9GNIH9nSjM8J~ILp7(410*K-WULt!K$9vYJrVj)%*qCyHmAxWc!q}akj1zE8XtgXeuMl4c9 zFqNbeFCpXtLQIiM$OYv6%?BUP%pPWD_x)nypMHg%3nAw;PA4=(KdB~Zh%_;UJ{;od zIdn2#Vjfo*zz;0q5i1xqMXs=oB^;v{->_rIR1OSug9NL^aDWM%VFX_=gL`z~H{Rk8 zRx#)jxx_~-U>~_9&&V0XO?5+!Uf1Y{{3k8qXXK%}xQ6#mf8z`}o6OfnmfrZPPqvk_ lyFF{SHGj<*(@`(ivbuIMTW?g#^;)M{ez?28-o3h0#vjDdC(!@^ diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po index a471d3814b..3676893ca3 100644 --- a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po +++ b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po @@ -41,6 +41,32 @@ msgid_plural "%d results" msgstr[0] "%d Resultat" 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 #, python-format msgid "The result was %(percent)s%%" diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index d9843c228a..fc1a832b89 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -22,10 +22,15 @@ from django.utils._os import upath from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText from django.utils import six from django.utils.six import PY3 -from django.utils.translation import (ugettext, ugettext_lazy, activate, - deactivate, gettext_lazy, pgettext, npgettext, to_locale, - get_language_info, get_language, get_language_from_request, trans_real) - +from django.utils.translation import (activate, deactivate, + get_language, get_language_from_request, get_language_info, + 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 if can_run_extraction_tests: @@ -95,6 +100,42 @@ class TranslationTests(TestCase): s2 = pickle.loads(pickle.dumps(s1)) 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) def test_pgettext(self): trans_real._active = local()