Browse Source

Fixed #30439 -- Added support for different plural forms for a language.

Thanks to Michal Čihař for review.
Claude Paroz 5 years ago
parent
commit
e3e48b0012

+ 72 - 3
django/utils/translation/trans_real.py

@@ -58,6 +58,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.
@@ -104,7 +161,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
@@ -175,9 +232,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)
 
@@ -189,6 +246,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):
     """

+ 3 - 2
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`).

+ 2 - 1
docs/releases/3.0.5.txt

@@ -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`).

+ 3 - 8
docs/topics/i18n/translation.txt

@@ -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:
 

BIN
tests/i18n/other/locale/fr/LC_MESSAGES/django.mo


+ 11 - 2
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)."
+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 - 0
tests/i18n/tests.py

@@ -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: