Fixed #30439 -- Added support for different plural forms for a language.
Thanks to Michal Čihař for review.
This commit is contained in:
parent
591e2270dc
commit
e3e48b0012
|
@ -58,6 +58,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.
|
||||||
|
@ -104,7 +161,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
|
||||||
|
@ -175,9 +232,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)
|
||||||
|
|
||||||
|
@ -189,6 +246,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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue