summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2020-03-10 15:56:32 +0100
committerCarlton Gibson <carlton.gibson@noumenal.es>2020-03-10 16:04:58 +0100
commit996be04c3ceb456754d9d527d4d708f30727f07e (patch)
tree5b88460fbf2d2587e3de9452e978aaa236df5a24
parentaa0948e23801cd79798a5ceeff5ad0ecd584b982 (diff)
downloaddjango-996be04c3ceb456754d9d527d4d708f30727f07e.tar.gz
[2.2.x] Fixed #30439 -- Added support for different plural forms for a language.
Thanks to Michal Čihař for review. Backport of e3e48b00127c09eafe6439d980a82fc5c591b673 from master
-rw-r--r--django/utils/translation/trans_real.py75
-rw-r--r--docs/releases/2.2.12.txt5
-rw-r--r--docs/topics/i18n/translation.txt11
-rw-r--r--tests/i18n/other/locale/fr/LC_MESSAGES/django.mobin528 -> 580 bytes
-rw-r--r--tests/i18n/other/locale/fr/LC_MESSAGES/django.po13
-rw-r--r--tests/i18n/tests.py16
6 files changed, 105 insertions, 15 deletions
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 98e3d0f51b..6fe29aa8fc 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -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):
"""
diff --git a/docs/releases/2.2.12.txt b/docs/releases/2.2.12.txt
index ca443a72ab..8585b12e7d 100644
--- a/docs/releases/2.2.12.txt
+++ b/docs/releases/2.2.12.txt
@@ -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`).
diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt
index b52e18e189..3aa235b202 100644
--- a/docs/topics/i18n/translation.txt
+++ b/docs/topics/i18n/translation.txt
@@ -279,14 +279,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:
diff --git a/tests/i18n/other/locale/fr/LC_MESSAGES/django.mo b/tests/i18n/other/locale/fr/LC_MESSAGES/django.mo
index 478338bc88..d86cae8f91 100644
--- a/tests/i18n/other/locale/fr/LC_MESSAGES/django.mo
+++ b/tests/i18n/other/locale/fr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/tests/i18n/other/locale/fr/LC_MESSAGES/django.po b/tests/i18n/other/locale/fr/LC_MESSAGES/django.po
index dafb6139ae..7626e5f6d5 100644
--- a/tests/i18n/other/locale/fr/LC_MESSAGES/django.po
+++ b/tests/i18n/other/locale/fr/LC_MESSAGES/django.po
@@ -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)." \ No newline at end of file
+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"
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index 2377c8992e..54de250cce 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -98,6 +98,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: