From 314ade95421c2dcc534e054c57f7884028a38811 Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Mon, 10 Aug 2026 20:06:36 +0200 Subject: [PATCH] Fix crash when Plural-Forms header has xgettext placeholder values read_po() crashed with ValueError when a POT/PO Plural-Forms header still had the unexpanded xgettext placeholders (nplurals=INTEGER; plural=EXPRESSION;). Fall back to the same defaults used elsewhere in Catalog._set_mime_headers (2 plurals, '(n != 1)'), mirroring the existing po-revision-date placeholder guard. Fixes python-babel/babel#1154 --- babel/messages/catalog.py | 11 +++++++++-- tests/messages/test_catalog.py | 16 ++++++++++++++++ tests/messages/test_pofile.py | 14 ++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index e01fd5677..efc8a1ad3 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -579,8 +579,15 @@ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None: self.charset = params['charset'].lower() elif name == 'plural-forms': params = parse_separated_header(f" ;{value}") - self._num_plurals = int(params.get('nplurals', 2)) - self._plural_expr = params.get('plural', '(n != 1)') + # nplurals/plural values may be left as the untranslated + # xgettext placeholders (e.g. in a fresh POT template); + # fall back to the defaults in that case instead of + # crashing on int(), mirroring the po-revision-date guard + # for the analogous 'YEAR' placeholder below. + num_plurals = params.get('nplurals', 2) + self._num_plurals = 2 if num_plurals == 'INTEGER' else int(num_plurals) + plural_expr = params.get('plural', '(n != 1)') + self._plural_expr = '(n != 1)' if plural_expr == 'EXPRESSION' else plural_expr elif name == 'pot-creation-date': self.creation_date = _parse_datetime_header(value) elif name == 'po-revision-date': diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 4a60208c8..0b0a05110 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -462,6 +462,22 @@ def test_catalog_mime_headers_set_locale(): ] +def test_catalog_mime_headers_xgettext_plural_forms_placeholder(): + """ + A freshly-generated POT template from xgettext may leave the + Plural-Forms header with unexpanded placeholder tokens + (``nplurals=INTEGER; plural=EXPRESSION;``) instead of real values. + Parsing such a header should not raise, and should fall back to the + same defaults used elsewhere in the catalog. + """ + cat = catalog.Catalog() + cat.mime_headers = [ + ('Plural-Forms', 'nplurals=INTEGER; plural=EXPRESSION;'), + ] + assert cat.num_plurals == 2 + assert cat.plural_expr == '(n != 1)' + + def test_catalog_mime_headers_type_coercion(): """ Test that mime headers' keys and values are coerced to strings diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index cdbb58262..ffd62955b 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -146,6 +146,20 @@ def test_issue_1087(): assert pofile.read_po(buf).locale is None +def test_issue_1154(): + # A freshly-generated POT template from xgettext may leave the + # Plural-Forms header with unexpanded placeholder tokens instead of + # real values; parsing it should not raise. + buf = StringIO(r''' +msgid "" +msgstr "" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +''') + cat = pofile.read_po(buf) + assert cat.num_plurals == 2 + assert cat.plural_expr == '(n != 1)' + + @pytest.mark.parametrize("case", ['msgid "foo"', 'msgid "foo"\nmsgid_plural "foos"']) @pytest.mark.parametrize("abort_invalid", [False, True]) def test_issue_1134(case: str, abort_invalid: bool):