From 31b27bce0dfb3742c63e6713c0947ae52077c71b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 14 Aug 2026 09:51:13 +0300 Subject: [PATCH] catalog: Keep explicitly declared plural forms without a locale If a catalog declares plural forms but no language header, Babel would previously lose the plural information. --- babel/messages/catalog.py | 25 +++++++++---------- tests/messages/test_pofile_write.py | 38 ++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5686b3141..5e6c28255 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -551,7 +551,8 @@ def _get_mime_headers(self) -> list[tuple[str, str]]: if self.locale_identifier: headers.append(('Language', str(self.locale_identifier))) headers.append(('Language-Team', language_team)) - if self.locale is not None: + if self.locale is not None or self._num_plurals is not None: + # Keep explicit plural forms even when no locale set. headers.append(('Plural-Forms', self.plural_forms)) headers += [ ('MIME-Version', '1.0'), @@ -663,12 +664,11 @@ def num_plurals(self) -> int: >>> Catalog(locale='ga').num_plurals 5 """ - if self._num_plurals is None: - num = 2 - if self.locale: - num = get_plural(self.locale)[0] - self._num_plurals = num - return self._num_plurals + if self._num_plurals is not None: + return self._num_plurals + if self.locale: + return get_plural(self.locale)[0] + return 2 @property def plural_expr(self) -> str: @@ -681,12 +681,11 @@ def plural_expr(self) -> str: >>> Catalog(locale='ding').plural_expr # unknown locale '(n != 1)' """ - if self._plural_expr is None: - expr = '(n != 1)' - if self.locale: - expr = get_plural(self.locale)[1] - self._plural_expr = expr - return self._plural_expr + if self._plural_expr is not None: + return self._plural_expr + if self.locale: + return get_plural(self.locale)[1] + return '(n != 1)' @property def plural_forms(self) -> str: diff --git a/tests/messages/test_pofile_write.py b/tests/messages/test_pofile_write.py index 0145f7928..6f02b5b42 100644 --- a/tests/messages/test_pofile_write.py +++ b/tests/messages/test_pofile_write.py @@ -11,7 +11,7 @@ # history and logs, available at https://github.com/python-babel/babel/commits/master/. from datetime import datetime -from io import BytesIO +from io import BytesIO, StringIO from babel.messages import Catalog, Message, pofile @@ -439,3 +439,39 @@ def test_wrap_with_enclosed_file_locations(): #: \xe2\x81\xa8test utils.py\xe2\x81\xa9:3 msgid "foo" msgstr ""''' + + +def test_explicit_plural_forms_survive_without_a_locale(): + """Plural forms declared in the header must not need a locale to survive.""" + catalog = pofile.read_po(StringIO( + 'msgid ""\n' + 'msgstr ""\n' + '"Content-Type: text/plain; charset=utf-8\\n"\n' + '"Plural-Forms: nplurals=3; plural=(n % 9 == 2 ? 2 : n % 21 == 4 ? 0 : 1)\\n"\n' + '\n' + 'msgid "file"\n' + 'msgid_plural "files"\n' + 'msgstr[0] "flerb"\n' + 'msgstr[1] "flöäarb"\n' + 'msgstr[2] "flirblurb"\n', + )) + assert catalog.locale is None + assert catalog.num_plurals == 3 + + buf = BytesIO() + pofile.write_po(buf, catalog) + content = buf.getvalue().decode() + assert 'Plural-Forms: nplurals=3' in content + assert 'msgstr[2] "flirblurb"' in content + assert pofile.read_po(StringIO(content)).num_plurals == 3 # roundtrip test + + +def test_a_template_declares_no_plural_forms(): + """Reading `num_plurals` must not make a locale-less catalog claim plural forms.""" + catalog = Catalog() + catalog.add(('file', 'files')) + assert catalog.num_plurals == 2 + + buf = BytesIO() + pofile.write_po(buf, catalog) + assert 'Plural-Forms' not in buf.getvalue().decode()