From 259ecfc8c2552c2f0c81676f03231471f992ba16 Mon Sep 17 00:00:00 2001 From: Ryan Li <13653603496@163.com> Date: Mon, 17 Aug 2026 22:46:42 +0800 Subject: [PATCH 1/2] Revert frontend.py downgrade, keep regex fix only --- babel/messages/catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5e6c28255..692caedc9 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -74,7 +74,7 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): \% (?:\(([\w]*)\))? ( - [-#0\ +]?(?:\*|[\d]+)? + [-#0+]?(?:\*|[\d]+)? (?:\.(?:\*|[\d]+))? [hlL]? ) From 8d8c3cf7ab8a1db874ebe1425b8c5214ef4a72b7 Mon Sep 17 00:00:00 2001 From: Ryan Li <13653603496@163.com> Date: Sun, 23 Aug 2026 11:45:28 +0800 Subject: [PATCH 2/2] fix(catalog): refine PYTHON_FORMAT regex to avoid prose false positives Allow space flag only when followed by explicit width or precision. Add test cases in test_catalog.py and test_checkers.py. --- babel/messages/catalog.py | 7 ++++--- tests/messages/test_catalog.py | 12 ++++++++++++ tests/messages/test_checkers.py | 6 ++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 692caedc9..125381ec1 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -74,10 +74,11 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): \% (?:\(([\w]*)\))? ( - [-#0+]?(?:\*|[\d]+)? - (?:\.(?:\*|[\d]+))? - [hlL]? + (?:[-#0+]?(?:\*|[\d]+)?(?:\.(?:\*|[\d]+))?) + | + (?:\ +(?:\.[\d]+|[\d]+(?:\.[\d]+)?)) ) + [hlL]? ([diouxXeEfFgGcrs%]) ''', re.VERBOSE, diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 7c730d325..817b42825 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -38,6 +38,18 @@ def test_message_python_format(): assert catalog.PYTHON_FORMAT.search('foo %()s') +def test_message_python_format_prose_percent(): + # literal percent signs in prose must not be mistaken for placeholders + assert not catalog.PYTHON_FORMAT.search('100 % done') + assert not catalog.PYTHON_FORMAT.search('50 % off') + assert not catalog.PYTHON_FORMAT.search('10% of') + assert not catalog.PYTHON_FORMAT.search('10% der') + # space-flag placeholders with explicit width/precision are still valid + assert catalog.PYTHON_FORMAT.search('% 5d') + assert catalog.PYTHON_FORMAT.search('% .2f') + assert catalog.PYTHON_FORMAT.search('% 5.2f') + + def test_message_python_brace_format(): assert not catalog._has_python_brace_format('') assert not catalog._has_python_brace_format('foo') diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index 9b0b4b779..a66f39dd2 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -390,6 +390,7 @@ def test_python_format_valid(msgid, msgstr): '%(foo)d', "incompatible format for placeholder 'foo': 'd' and 's' are not compatible", ), + ('% 5d', '% 5s', "incompatible format for placeholder 1: 'd' and 's' are not compatible"), ], ) def test__validate_format_invalid(msgid, msgstr, error): @@ -412,6 +413,11 @@ def test__validate_format_invalid(msgid, msgstr, error): ('%(foo)s', 'foo'), ('%(foo)s', '%(foo)s %(foo)s'), ('%(bar)s foo %(n)d', '%(n)d foo %(bar)s'), + # literal percent signs in prose are not placeholders + ('100 % done', '100 % erledigt'), + ('50 % off', '50 % Rabatt'), + # space-flag placeholders with explicit width/precision are still checked + ('% 5d', '% 5d'), ], ) def test__validate_format_valid(msgid, msgstr):