[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() 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): class DjangoTranslation(gettext_module.GNUTranslations):
""" """
Set up the GNUTranslations context with regard to output charset. Set up the GNUTranslations context with regard to output charset.
@ -103,7 +160,7 @@ class DjangoTranslation(gettext_module.GNUTranslations):
self._add_fallback(localedirs) self._add_fallback(localedirs)
if self._catalog is None: if self._catalog is None:
# No catalogs found for this language, set an empty catalog. # No catalogs found for this language, set an empty catalog.
self._catalog = {} self._catalog = TranslationCatalog()
def __repr__(self): def __repr__(self):
return "<DjangoTranslation lang:%s>" % self.__language 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). # Take plural and _info from first catalog found (generally Django's).
self.plural = other.plural self.plural = other.plural
self._info = other._info.copy() self._info = other._info.copy()
self._catalog = other._catalog.copy() self._catalog = TranslationCatalog(other)
else: else:
self._catalog.update(other._catalog) self._catalog.update(other)
if other._fallback: if other._fallback:
self.add_fallback(other._fallback) self.add_fallback(other._fallback)
@ -188,6 +245,18 @@ class DjangoTranslation(gettext_module.GNUTranslations):
"""Return the translation language name.""" """Return the translation language name."""
return self.__to_language 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): def translation(language):
""" """

View File

@ -4,9 +4,10 @@ Django 2.2.12 release notes
*Expected April 1, 2020* *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 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 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' 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 Added support for different plural equations in ``.po`` files.
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.
.. _contextual-markers: .. _contextual-markers:

View File

@ -14,7 +14,10 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 #: template.html:3
# Note: Intentional: variable name is translated. # Note: Intentional: variable name is translated.
@ -25,3 +28,9 @@ msgstr "Mon nom est %(personne)s."
# Note: Intentional: the variable name is badly formatted (missing 's' at the end) # Note: Intentional: the variable name is badly formatted (missing 's' at the end)
msgid "My other name is %(person)s." 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', 1) % 1, '1 year')
self.assertEqual(g('%d year', '%d years', 2) % 2, '2 years') 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): def test_override(self):
activate('de') activate('de')
try: try: