Skip to content

Commit d9f1792

Browse files
claudepcarltongibson
authored andcommitted
[3.0.x] Fixed #30439 -- Added support for different plural forms for a language.
Thanks to Michal Čihař for review. Backport of e3e48b0 from master
1 parent 525274f commit d9f1792

File tree

7 files changed

+107
-16
lines changed

7 files changed

+107
-16
lines changed

django/utils/translation/trans_real.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,63 @@ def reset_cache(**kwargs):
5757
get_supported_language_variant.cache_clear()
5858

5959

60+
class TranslationCatalog:
61+
"""
62+
Simulate a dict for DjangoTranslation._catalog so as multiple catalogs
63+
with different plural equations are kept separate.
64+
"""
65+
def __init__(self, trans=None):
66+
self._catalogs = [trans._catalog.copy()] if trans else [{}]
67+
self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)]
68+
69+
def __getitem__(self, key):
70+
for cat in self._catalogs:
71+
try:
72+
return cat[key]
73+
except KeyError:
74+
pass
75+
raise KeyError(key)
76+
77+
def __setitem__(self, key, value):
78+
self._catalogs[0][key] = value
79+
80+
def __contains__(self, key):
81+
return any(key in cat for cat in self._catalogs)
82+
83+
def items(self):
84+
for cat in self._catalogs:
85+
yield from cat.items()
86+
87+
def keys(self):
88+
for cat in self._catalogs:
89+
yield from cat.keys()
90+
91+
def update(self, trans):
92+
# Merge if plural function is the same, else prepend.
93+
for cat, plural in zip(self._catalogs, self._plurals):
94+
if trans.plural.__code__ == plural.__code__:
95+
cat.update(trans._catalog)
96+
break
97+
else:
98+
self._catalogs.insert(0, trans._catalog)
99+
self._plurals.insert(0, trans.plural)
100+
101+
def get(self, key, default=None):
102+
missing = object()
103+
for cat in self._catalogs:
104+
result = cat.get(key, missing)
105+
if result is not missing:
106+
return result
107+
return default
108+
109+
def plural(self, msgid, num):
110+
for cat, plural in zip(self._catalogs, self._plurals):
111+
tmsg = cat.get((msgid, plural(num)))
112+
if tmsg is not None:
113+
return tmsg
114+
raise KeyError
115+
116+
60117
class DjangoTranslation(gettext_module.GNUTranslations):
61118
"""
62119
Set up the GNUTranslations context with regard to output charset.
@@ -103,7 +160,7 @@ def __init__(self, language, domain=None, localedirs=None):
103160
self._add_fallback(localedirs)
104161
if self._catalog is None:
105162
# No catalogs found for this language, set an empty catalog.
106-
self._catalog = {}
163+
self._catalog = TranslationCatalog()
107164

108165
def __repr__(self):
109166
return "<DjangoTranslation lang:%s>" % self.__language
@@ -174,9 +231,9 @@ def merge(self, other):
174231
# Take plural and _info from first catalog found (generally Django's).
175232
self.plural = other.plural
176233
self._info = other._info.copy()
177-
self._catalog = other._catalog.copy()
234+
self._catalog = TranslationCatalog(other)
178235
else:
179-
self._catalog.update(other._catalog)
236+
self._catalog.update(other)
180237
if other._fallback:
181238
self.add_fallback(other._fallback)
182239

@@ -188,6 +245,18 @@ def to_language(self):
188245
"""Return the translation language name."""
189246
return self.__to_language
190247

248+
def ngettext(self, msgid1, msgid2, n):
249+
try:
250+
tmsg = self._catalog.plural(msgid1, n)
251+
except KeyError:
252+
if self._fallback:
253+
return self._fallback.ngettext(msgid1, msgid2, n)
254+
if n == 1:
255+
tmsg = msgid1
256+
else:
257+
tmsg = msgid2
258+
return tmsg
259+
191260

192261
def translation(language):
193262
"""

docs/releases/2.2.12.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ Django 2.2.12 release notes
44

55
*Expected April 1, 2020*
66

7-
Django 2.2.12 fixes several bugs in 2.2.11.
7+
Django 2.2.12 fixes a bug in 2.2.11.
88

99
Bugfixes
1010
========
1111

12-
* ...
12+
* Added the ability to handle ``.po`` files containing different plural
13+
equations for the same language (:ticket:`30439`).

docs/releases/3.0.5.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ Django 3.0.5 fixes several bugs in 3.0.4.
99
Bugfixes
1010
========
1111

12-
* ...
12+
* Added the ability to handle ``.po`` files containing different plural
13+
equations for the same language (:ticket:`30439`).

docs/topics/i18n/translation.txt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,9 @@ In a case like this, consider something like the following::
277277

278278
a format specification for argument 'name', as in 'msgstr[0]', doesn't exist in 'msgid'
279279

280-
.. note:: Plural form and po files
281-
282-
Django does not support custom plural equations in po files. As all
283-
translation catalogs are merged, only the plural form for the main Django po
284-
file (in ``django/conf/locale/<lang_code>/LC_MESSAGES/django.po``) is
285-
considered. Plural forms in all other po files are ignored. Therefore, you
286-
should not use different plural equations in your project or application po
287-
files.
280+
.. versionchanged: 2.2.12
281+
282+
Added support for different plural equations in ``.po`` files.
288283

289284
.. _contextual-markers:
290285

52 Bytes
Binary file not shown.

tests/i18n/other/locale/fr/LC_MESSAGES/django.po

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ msgstr ""
1414
"MIME-Version: 1.0\n"
1515
"Content-Type: text/plain; charset=UTF-8\n"
1616
"Content-Transfer-Encoding: 8bit\n"
17-
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
17+
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n==0 ? 1 : 2);\n"
18+
19+
# Plural form is purposefully different from the normal French plural to test
20+
# multiple plural forms for one language.
1821

1922
#: template.html:3
2023
# Note: Intentional: variable name is translated.
@@ -24,4 +27,10 @@ msgstr "Mon nom est %(personne)s."
2427
#: template.html:3
2528
# Note: Intentional: the variable name is badly formatted (missing 's' at the end)
2629
msgid "My other name is %(person)s."
27-
msgstr "Mon autre nom est %(person)."
30+
msgstr "Mon autre nom est %(person)."
31+
32+
msgid "%d singular"
33+
msgid_plural "%d plural"
34+
msgstr[0] "%d singulier"
35+
msgstr[1] "%d pluriel1"
36+
msgstr[2] "%d pluriel2"

tests/i18n/tests.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,22 @@ def test_plural_null(self):
125125
self.assertEqual(g('%d year', '%d years', 1) % 1, '1 year')
126126
self.assertEqual(g('%d year', '%d years', 2) % 2, '2 years')
127127

128+
@override_settings(LOCALE_PATHS=extended_locale_paths)
129+
@translation.override('fr')
130+
def test_multiple_plurals_per_language(self):
131+
"""
132+
Normally, French has 2 plurals. As other/locale/fr/LC_MESSAGES/django.po
133+
has a different plural equation with 3 plurals, this tests if those
134+
plural are honored.
135+
"""
136+
self.assertEqual(ngettext("%d singular", "%d plural", 0) % 0, "0 pluriel1")
137+
self.assertEqual(ngettext("%d singular", "%d plural", 1) % 1, "1 singulier")
138+
self.assertEqual(ngettext("%d singular", "%d plural", 2) % 2, "2 pluriel2")
139+
french = trans_real.catalog()
140+
# Internal _catalog can query subcatalogs (from different po files).
141+
self.assertEqual(french._catalog[('%d singular', 0)], '%d singulier')
142+
self.assertEqual(french._catalog[('%d hour', 0)], '%d heure')
143+
128144
def test_override(self):
129145
activate('de')
130146
try:

0 commit comments

Comments
 (0)