Skip to content

Commit

Permalink
[3.0.x] Fixed #30439 -- Added support for different plural forms for …
Browse files Browse the repository at this point in the history
…a language.

Thanks to Michal Čihař for review.
Backport of e3e48b0 from master
  • Loading branch information
claudep authored and carltongibson committed Mar 10, 2020
1 parent 525274f commit d9f1792
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 16 deletions.
75 changes: 72 additions & 3 deletions django/utils/translation/trans_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -103,7 +160,7 @@ def __init__(self, language, domain=None, localedirs=None):
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
Expand Down Expand Up @@ -174,9 +231,9 @@ def merge(self, other):
# 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)

Expand All @@ -188,6 +245,18 @@ def to_language(self):
"""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):
"""
Expand Down
5 changes: 3 additions & 2 deletions docs/releases/2.2.12.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
3 changes: 2 additions & 1 deletion docs/releases/3.0.5.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
11 changes: 3 additions & 8 deletions docs/topics/i18n/translation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.
.. versionchanged: 2.2.12

Added support for different plural equations in ``.po`` files.

.. _contextual-markers:

Expand Down
Binary file modified tests/i18n/other/locale/fr/LC_MESSAGES/django.mo
Binary file not shown.
13 changes: 11 additions & 2 deletions tests/i18n/other/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
16 changes: 16 additions & 0 deletions tests/i18n/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ def test_plural_null(self):
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:
Expand Down

0 comments on commit d9f1792

Please sign in to comment.