From 38b266279d4bd94756dd1960798758d783ac321a Mon Sep 17 00:00:00 2001 From: Parijat Biswas Date: Fri, 30 Oct 2026 03:24:04 +0530 Subject: [PATCH 1/2] plural: Normalize negative JavaScript inputs Use absolute values before evaluating generated plural rules, matching Python and the Unicode plural operand definition. Cover source output and JavaScript execution for negative and positive inputs. Fixes https://github.com/python-babel/babel/issues/1413 --- babel/plural.py | 6 ++++-- tests/test_plural.py | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/babel/plural.py b/babel/plural.py index 14cadceff..310f8bbd1 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -180,7 +180,9 @@ def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRu function. This function depends on no external library: >>> to_javascript({'one': 'n is 1'}) - "(function(n) { return (n == 1) ? 'one' : 'other'; })" + "(function(n) { n = Math.abs(n); return (n == 1) ? 'one' : 'other'; })" + + Negative numbers use their absolute value, as in :class:`PluralRule`. Implementation detail: The function generated will probably evaluate expressions involved into range operations multiple times. This has the @@ -191,7 +193,7 @@ def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRu :raise RuleError: if the expression is malformed """ to_js = _JavaScriptCompiler().compile - result = ['(function(n) { return '] + result = ['(function(n) { n = Math.abs(n); return '] for tag, ast in PluralRule.parse(rule).abstract: result.append(f"{to_js(ast)} ? {tag!r} : ") result.append('%r; })' % _fallback_tag) diff --git a/tests/test_plural.py b/tests/test_plural.py index 4da07429d..f861268b3 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -10,6 +10,9 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at https://github.com/python-babel/babel/commits/master/. import decimal +import json +import shutil +import subprocess import pytest @@ -68,7 +71,37 @@ def test_plural_other_is_ignored(): def test_to_javascript(): src = plural.to_javascript({'one': 'n is 1'}) - assert src == "(function(n) { return (n == 1) ? 'one' : 'other'; })" + assert src == "(function(n) { n = Math.abs(n); return (n == 1) ? 'one' : 'other'; })" + + +@pytest.mark.parametrize('rules', [ + {}, + {'one': 'n is 1'}, + {'one': 'i is 1'}, + {'few': 'n mod 10 in 2..4'}, + {'few': 'n within 2..4'}, +]) +def test_to_javascript_uses_absolute_value(rules): + src = plural.to_javascript(rules) + assert src.startswith('(function(n) { n = Math.abs(n); return ') + + +@pytest.mark.skipif(shutil.which('node') is None, reason='Node.js is not installed') +@pytest.mark.parametrize('rule', [ + plural.PluralRule({'one': 'n is 1', 'few': 'n in 2..4'}), + plural.PluralRule({'one': 'i is 1'}), + plural.PluralRule({'few': 'n mod 10 in 2..4'}), + plural.PluralRule({'few': 'n within 2..4'}), +]) +def test_to_javascript_negative_numbers(rule): + values = [-24, -12, -4, -2.5, -2, -1.5, -1, 0, 1, 1.5, 2, 2.5, 4, 12, 24] + source = plural.to_javascript(rule) + script = ( + f'const plural = {source};' + f'console.log(JSON.stringify({json.dumps(values)}.map(n => plural(n))));' + ) + result = subprocess.run(['node', '-e', script], capture_output=True, text=True, check=True) + assert json.loads(result.stdout) == [rule(value) for value in values] def test_to_python(): @@ -114,7 +147,7 @@ def test_plural_within_rules(): p = plural.PluralRule({'one': 'n is 1', 'few': 'n within 2,4,7..9'}) assert repr(p) == "" assert plural.to_javascript(p) == ( - "(function(n) { " + "(function(n) { n = Math.abs(n); " "return ((n == 2) || (n == 4) || (n >= 7 && n <= 9))" " ? 'few' : (n == 1) ? 'one' : 'other'; })" ) From 0c92865a3b8ebbc2d4f99a8d5c72182d6bc6bf00 Mon Sep 17 00:00:00 2001 From: Parijat Biswas Date: Mon, 2 Nov 2026 12:14:39 +0530 Subject: [PATCH 2/2] Remove redundant JavaScript plural regression tests --- tests/test_plural.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/tests/test_plural.py b/tests/test_plural.py index f861268b3..94cebf6d8 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -10,9 +10,6 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at https://github.com/python-babel/babel/commits/master/. import decimal -import json -import shutil -import subprocess import pytest @@ -74,36 +71,6 @@ def test_to_javascript(): assert src == "(function(n) { n = Math.abs(n); return (n == 1) ? 'one' : 'other'; })" -@pytest.mark.parametrize('rules', [ - {}, - {'one': 'n is 1'}, - {'one': 'i is 1'}, - {'few': 'n mod 10 in 2..4'}, - {'few': 'n within 2..4'}, -]) -def test_to_javascript_uses_absolute_value(rules): - src = plural.to_javascript(rules) - assert src.startswith('(function(n) { n = Math.abs(n); return ') - - -@pytest.mark.skipif(shutil.which('node') is None, reason='Node.js is not installed') -@pytest.mark.parametrize('rule', [ - plural.PluralRule({'one': 'n is 1', 'few': 'n in 2..4'}), - plural.PluralRule({'one': 'i is 1'}), - plural.PluralRule({'few': 'n mod 10 in 2..4'}), - plural.PluralRule({'few': 'n within 2..4'}), -]) -def test_to_javascript_negative_numbers(rule): - values = [-24, -12, -4, -2.5, -2, -1.5, -1, 0, 1, 1.5, 2, 2.5, 4, 12, 24] - source = plural.to_javascript(rule) - script = ( - f'const plural = {source};' - f'console.log(JSON.stringify({json.dumps(values)}.map(n => plural(n))));' - ) - result = subprocess.run(['node', '-e', script], capture_output=True, text=True, check=True) - assert json.loads(result.stdout) == [rule(value) for value in values] - - def test_to_python(): func = plural.to_python({'one': 'n is 1', 'few': 'n in 2..4'}) assert func(1) == 'one'