Babel locale-aware numbers, currency, percent and dates: working versions (tested 2026-10-16)
Format numbers, currency, percent and dates for en_US, de_DE and fr_FR with Babel, and see how symbol placement, currency digits and parse rules change per locale.
passed6 passed · 0 failed2026-10-16 21:56 UTC- Status
- passed
- Last passed
- 2026-10-16 21:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
babel@2.18.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("babel-locale-number-currency") (install), or see them on GitHub.
requirements.txt
babel==2.18.0 pytest==9.1.1
locales.py
from __future__ import annotations
from datetime import date
from decimal import Decimal
from babel.dates import format_date
from babel.numbers import format_currency, format_decimal, format_percent, parse_decimal
LOCALES = ("en_US", "de_DE", "fr_FR")
def money(amount: float | Decimal | int, currency: str, locale: str) -> str:
# Babel moves the symbol around and may pick a disambiguated one per locale.
return format_currency(amount, currency, locale=locale)
def percent(fraction: float | Decimal, locale: str) -> str:
# Babel multiplies by 100 itself, pass the 0..1 fraction.
return format_percent(fraction, locale=locale)
def decimal(value: float | Decimal, locale: str, pattern: str | None = None) -> str:
# Default pattern is "#,##0.###", so it silently rounds to 3 fraction digits.
return format_decimal(value, format=pattern, locale=locale)
def parse_amount(text: str, locale: str) -> Decimal:
# Grouping and decimal symbols are taken from the locale, not guessed from text.
return parse_decimal(text, locale=locale)
def long_date(value: date, locale: str) -> str:
return format_date(value, format="long", locale=locale)
tests/test_locales.py
from datetime import date
from decimal import Decimal
import pytest
from babel.numbers import NumberFormatError
from locales import decimal, long_date, money, parse_amount, percent
def test_currency_symbol_and_placement_vary_by_locale():
assert money(1234.5, "USD", "en_US") == "$1,234.50"
assert money(1234.5, "USD", "de_DE") == "1.234,50\u00a0$"
assert money(1234.5, "USD", "fr_FR") == "1\u202f234,50\u00a0$US"
def test_currency_digits_follow_the_currency_not_the_locale():
assert money(1234, "JPY", "en_US") == "\u00a51,234"
assert money(1234, "JPY", "fr_FR") == "1\u202f234\u00a0JPY"
def test_percent_spacing_is_locale_specific():
assert percent(0.256, "en_US") == "26%"
assert percent(0.256, "de_DE") == "26\u00a0%"
assert percent(0.256, "fr_FR") == "26\u00a0%"
def test_decimal_rounds_to_three_fraction_digits_unless_pinned():
assert decimal(1.23456, "en_US") == "1.235"
assert decimal(1.23456, "de_DE") == "1,235"
assert decimal(1.23456, "fr_FR", pattern="#,##0.#####") == "1,23456"
def test_parse_decimal_is_strict_about_the_matching_locale():
assert parse_amount("1.234,56", "de_DE") == Decimal("1234.56")
# Gotcha: en_US treats "," as grouping, drops it, and returns 1.23456 with no error.
assert parse_amount("1.234,56", "en_US") == Decimal("1.23456")
# fr_FR groups with a narrow no-break space, so a "." group separator is invalid.
with pytest.raises(NumberFormatError):
parse_amount("1.234,56", "fr_FR")
def test_long_date_order_and_month_case():
assert long_date(date(2024, 3, 5), "en_US") == "March 5, 2024"
assert long_date(date(2024, 3, 5), "de_DE") == "5. M\u00e4rz 2024"
assert long_date(date(2024, 3, 5), "fr_FR") == "5 mars 2024"
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.