[3.0.x] Fixed #30439 -- Added support for different plural forms for a language.

Thanks to Michal Čihař for review.
Backport of e3e48b0012 from master
This commit is contained in:
Claude Paroz 2020-03-10 15:56:32 +01:00 committed by Carlton Gibson
parent 525274f79b
commit d9f1792c76
7 changed files with 106 additions and 15 deletions

View File

@ -57,6 +57,63 @@ def reset_cache(**kwargs):
get_supported_language_variant.cache_clear()
class TranslationCatalog:
"""
Simulate a dict for DjangoTranslation._catalog so as multiple catalogs
with different plural equations are kept separate.
"""
def __init__(self, trans=None):
self._catalogs = [trans._catalog.copy()] if trans else [{}]
self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)]
def __getitem__(self, key):
for cat in self._catalogs:
try:
return cat[key]
except KeyError:
pass
raise KeyError(key)
def __setitem__(self, key, value):
self._catalogs[0][key] = value
def __contains__(self, key):
return any(key in cat for cat in self._catalogs)
def items(self):
for cat in self._catalogs:
yield from cat.items()
def keys(self):
for cat in self._catalogs:
yield from cat.keys()
def update(self, trans):
# Merge if plural function is the same, else prepend.
for cat, plural in zip(self._catalogs, self._plurals):
if trans.plural.__code__ == plural.__code__:
cat.update(trans._catalog)
break
else:
self._catalogs.insert(0, trans._catalog)
self._plurals.insert(0, trans.plural)
def get(self, key, default=None):
missing = object()
for cat in self._catalogs:
result = cat.get(key, missing)
if result is not missing:
return result
return default
def plural(self, msgid, num):
for cat, plural in zip(self._catalogs, self._plurals):
tmsg = cat.get((msgid, plural(num)))
if tmsg is not None:
return tmsg
raise KeyError
class DjangoTranslation(gettext_module.GNUTranslations):
"""
Set up the GNUTranslations context with regard to output charset.
@ -103,7 +160,7 @@ class DjangoTranslation(gettext_module.GNUTranslations):
self._add_fallback(localedirs)
if self._catalog is None:
# No catalogs found for this language, set an empty catalog.
self._catalog = {}
self._catalog = TranslationCatalog()
def __repr__(self):
return "<DjangoTranslation lang:%s>" % self.__language
@ -174,9 +231,9 @@ class DjangoTranslation(gettext_module.GNUTranslations):
# Take plural and _info from first catalog found (generally Django's).
self.plural = other.plural
self._info = other._info.copy()
self._catalog = other._catalog.copy()
self._catalog = TranslationCatalog(other)
else:
self._catalog.update(other._catalog)
self._catalog.update(other)
if other._fallback:
self.add_fallback(other._fallback)
@ -188,6 +245,18 @@ class DjangoTranslation(gettext_module.GNUTranslations):
"""Return the translation language name."""
return self.__to_language
def ngettext(self, msgid1, msgid2, n):
try:
tmsg = self._catalog.plural(msgid1, n)
except KeyError:
if self._fallback:
return self._fallback.ngettext(msgid1, msgid2, n)
if n == 1:
tmsg = msgid1
else:
tmsg = msgid2
return tmsg
def translation(language):
"""

View File

@ -4,9 +4,10 @@ Django 2.2.12 release notes
*Expected April 1, 2020*
Django 2.2.12 fixes several bugs in 2.2.11.
Django 2.2.12 fixes a bug in 2.2.11.
Bugfixes
========
* ...
* Added the ability to handle ``.po`` files containing different plural
equations for the same language (:ticket:`30439`).

View File

@ -9,4 +9,5 @@ Django 3.0.5 fixes several bugs in 3.0.4.
Bugfixes
========
* ...
* Added the ability to handle ``.po`` files containing different plural
equations for the same language (:ticket:`30439`).

View File

@ -277,14 +277,9 @@ In a case like this, consider something like the following::
a format specification for argument 'name', as in 'msgstr[0]', doesn't exist in 'msgid'
.. note:: Plural form and po files
.. versionchanged: 2.2.12
Django does not support custom plural equations in po files. As all
translation catalogs are merged, only the plural form for the main Django po
file (in ``django/conf/locale/<lang_code>/LC_MESSAGES/django.po``) is
considered. Plural forms in all other po files are ignored. Therefore, you
should not use different plural equations in your project or application po
files.
Added support for different plural equations in ``.po`` files.
.. _contextual-markers:

View File

@ -14,7 +14,10 @@ msgstr ""
"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"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n==0 ? 1 : 2);\n"
# Plural form is purposefully different from the normal French plural to test
# multiple plural forms for one language.
#: template.html:3
# Note: Intentional: variable name is translated.
@ -24,4 +27,10 @@ msgstr "Mon nom est %(personne)s."
#: template.html:3
# Note: Intentional: the variable name is badly formatted (missing 's' at the end)
msgid "My other name is %(person)s."
msgstr "Mon autre nom est %(person)."
msgstr "Mon autre nom est %(person)."
msgid "%d singular"
msgid_plural "%d plural"
msgstr[0] "%d singulier"
msgstr[1] "%d pluriel1"
msgstr[2] "%d pluriel2"

View File

@ -125,6 +125,22 @@ class TranslationTests(SimpleTestCase):
self.assertEqual(g('%d year', '%d years', 1) % 1, '1 year')
self.assertEqual(g('%d year', '%d years', 2) % 2, '2 years')
@override_settings(LOCALE_PATHS=extended_locale_paths)
@translation.override('fr')
def test_multiple_plurals_per_language(self):
"""
Normally, French has 2 plurals. As other/locale/fr/LC_MESSAGES/django.po
has a different plural equation with 3 plurals, this tests if those
plural are honored.
"""
self.assertEqual(ngettext("%d singular", "%d plural", 0) % 0, "0 pluriel1")
self.assertEqual(ngettext("%d singular", "%d plural", 1) % 1, "1 singulier")
self.assertEqual(ngettext("%d singular", "%d plural", 2) % 2, "2 pluriel2")
french = trans_real.catalog()
# Internal _catalog can query subcatalogs (from different po files).
self.assertEqual(french._catalog[('%d singular', 0)], '%d singulier')
self.assertEqual(french._catalog[('%d hour', 0)], '%d heure')
def test_override(self):
activate('de')
try: