From d937f022960e3e8e2ab56d5fb3a68690ffd3e3a9 Mon Sep 17 00:00:00 2001 From: Ravindu Pabasara Karunarathna Date: Sat, 25 Jul 2026 19:07:40 +0530 Subject: [PATCH] Let ordinal translations interpolate the number (#270) --- src/humanize/number.py | 9 ++++++++- tests/test_i18n.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/humanize/number.py b/src/humanize/number.py index 2fb22c60..579803b5 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -110,7 +110,14 @@ def ordinal(value: NumberOrString, gender: str = "male") -> str: return str(value) gender = "male" if gender == "male" else "female" digit = 0 if value % 100 in (11, 12, 13) else value % 10 - return f"{value}{P_(f'{digit} ({gender})', _ORDINAL_SUFFIXES[digit])}" + suffix = P_(f"{digit} ({gender})", _ORDINAL_SUFFIXES[digit]) + # Most languages append a suffix to the number ("2nd"), but some position the + # number inside the ordinal word (e.g. Romanian "al 2-lea", "a 2-a"). A + # translation can opt into that by including a "%s"/"%d" placeholder for the + # number; without one, the classic "" form is kept. + if "%s" in suffix or "%d" in suffix: + return suffix % value + return f"{value}{suffix}" def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 83afb898..f3e609a2 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -252,6 +252,36 @@ def test_ordinal_genders( humanize.i18n.deactivate() +def test_ordinal_interpolated_number(monkeypatch: pytest.MonkeyPatch) -> None: + """Ordinals whose translation carries a placeholder position the number + themselves, e.g. Romanian "al 2-lea" / "a 2-a". Regression test for #270. + """ + from humanize import number + + def fake_pgettext(msgctxt: str, message: str) -> str: + return "a %s-a" if "female" in msgctxt else "al %s-lea" + + monkeypatch.setattr(number, "P_", fake_pgettext) + assert humanize.ordinal(2) == "al 2-lea" + assert humanize.ordinal(23) == "al 23-lea" + assert humanize.ordinal(2, gender="female") == "a 2-a" + + # A %d placeholder is accepted as well. + monkeypatch.setattr(number, "P_", lambda ctx, msg: "%d-lea") + assert humanize.ordinal(7) == "7-lea" + + +def test_ordinal_suffix_unchanged(monkeypatch: pytest.MonkeyPatch) -> None: + """A translation without a placeholder keeps the classic + form, so existing locales are unaffected by the interpolation support. + """ + from humanize import number + + monkeypatch.setattr(number, "P_", lambda ctx, msg: "º") + assert humanize.ordinal(5) == "5º" + assert humanize.ordinal(21) == "21º" + + def test_default_locale_path_defined__spec__() -> None: i18n = importlib.import_module("humanize.i18n") assert i18n._get_default_locale_path() is not None