Babel provides i18n and l10n for Python applications. pip install babel. Numbers: from babel.numbers import format_decimal, format_currency, format_percent. format_decimal(1234567.89, locale="en_US") → “1,234,567.89”. format_decimal(1234567.89, locale="de_DE") → “1.234.567,89”. format_currency(1234.56, "USD", locale="en_US") → “$1,234.56”. format_currency(1234.56, "EUR", locale="fr_FR") → “1 234,56 €”. format_percent(0.253, locale="en_US") → “25%”. Dates: from babel.dates import format_date, format_time, format_datetime, format_timedelta. format_date(date.today(), locale="en_US") → “Jan 5, 2024”. format_date(date.today(), format="full", locale="de") → “Freitag, 5. Januar 2024”. format_time(datetime.now(), locale="ja_JP"). format_datetime(dt, locale="zh_CN"). format_timedelta(timedelta(hours=2), locale="fr_FR") → “dans 2 heures”. Locale: from babel import Locale. Locale("de", "DE"). Locale.parse("de_DE"). locale.display_name. locale.number_symbols. locale.currency_symbols. Timezones: from babel.dates import get_timezone, format_datetime. tz = get_timezone("America/New_York"). format_datetime(dt, tzinfo=tz, locale="en_US"). get_timezone_name(tz, locale="en"). Translations: gettext, ngettext, lazy_gettext. Flask-Babel: flask_babel.Babel(app). @babel.localeselector. Claude Code generates Babel formatters, Flask-Babel integrations, and locale-aware display layers.
CLAUDE.md for Babel
## Babel Stack
- Version: babel >= 2.14 | pip install babel
- Numbers: format_decimal(n, locale="de_DE") | format_currency(n, "EUR", locale="fr_FR")
- Dates: format_date(d, format="full", locale="ja") | format_datetime(dt, locale="zh_CN")
- Timedelta: format_timedelta(td, add_direction=True, locale="fr") → "in 2 hours"
- Locale: Locale.parse("de_DE").display_name | locale.currency_symbols["USD"]
- Timezone: get_timezone("America/New_York") | format_datetime(dt, tzinfo=tz, locale="en")
- Flask: Flask-Babel extension for request.locale detection + gettext
Babel Internationalization Pipeline
# app/i18n.py — Babel number, date, currency, and timezone formatting
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from typing import Any
from babel import Locale
from babel.dates import (
format_date,
format_datetime,
format_time,
format_timedelta,
get_timezone,
get_timezone_name,
)
from babel.numbers import (
format_compact_decimal,
format_currency,
format_decimal,
format_percent,
format_scientific,
get_currency_name,
get_currency_symbol,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Number formatting
# ─────────────────────────────────────────────────────────────────────────────
def format_number(value: float, locale: str = "en_US") -> str:
"""
Format a number using locale-specific grouping and decimal separators.
US: 1,234,567.89 | German: 1.234.567,89 | French: 1 234 567,89
"""
return format_decimal(value, locale=locale)
def format_number_compact(value: float, locale: str = "en_US") -> str:
"""
Compact notation: 1200000 → "1.2M" (en_US) or "1,2 Mio." (de_DE).
Useful for dashboards where space is limited.
"""
return format_compact_decimal(value, locale=locale)
def format_money(
value: float,
currency: str = "USD",
locale: str = "en_US",
format_type: str = "standard",
) -> str:
"""
Format a monetary value with the correct symbol and separators.
format_type: "standard" ($1,234.56), "accounting" (parentheses for negative).
"""
return format_currency(value, currency, locale=locale, format_type=format_type)
def format_pct(value: float, decimal_quantization: bool = True, locale: str = "en_US") -> str:
"""Format a ratio as a percentage: 0.253 → "25%"."""
return format_percent(value, locale=locale)
def demo_numbers() -> None:
number = 1_234_567.89
locales = ["en_US", "de_DE", "fr_FR", "ja_JP", "ar_SA", "hi_IN"]
print(" Number formatting:")
for loc in locales:
print(f" {loc:10} → {format_decimal(number, locale=loc)}")
print("\n Currency formatting ($1,234.56):")
samples = [
("USD", "en_US"), ("EUR", "de_DE"), ("EUR", "fr_FR"),
("JPY", "ja_JP"), ("GBP", "en_GB"), ("CNY", "zh_CN"),
]
for curr, loc in samples:
print(f" {curr}/{loc:10} → {format_currency(1234.56, curr, locale=loc)}")
# ─────────────────────────────────────────────────────────────────────────────
# 2. Date and time formatting
# ─────────────────────────────────────────────────────────────────────────────
def format_locale_date(
d: date,
locale: str = "en_US",
fmt: str = "medium",
) -> str:
"""
Format a date object using locale-specific format.
fmt: "short" (1/5/24), "medium" (Jan 5, 2024), "long" (January 5, 2024),
"full" (Friday, January 5, 2024).
Babel translates month/day names to the target locale.
"""
return format_date(d, format=fmt, locale=locale)
def format_locale_datetime(
dt: datetime,
locale: str = "en_US",
fmt: str = "medium",
tz_name: str | None = None,
) -> str:
"""Format a datetime with optional timezone conversion."""
tz = get_timezone(tz_name) if tz_name else None
return format_datetime(dt, format=fmt, locale=locale, tzinfo=tz)
def format_relative_time(td: timedelta, locale: str = "en_US") -> str:
"""
Format a timedelta as a human-readable relative string.
add_direction=True → "in 2 hours" or "2 hours ago".
"""
return format_timedelta(td, add_direction=True, locale=locale)
def demo_dates() -> None:
today = date(2024, 1, 5)
now = datetime(2024, 1, 5, 14, 30, 0)
fmts = ["short", "medium", "long", "full"]
locales = ["en_US", "de_DE", "fr_FR", "zh_CN", "ja_JP", "ar_SA"]
print(" Date formats (en_US):")
for f in fmts:
print(f" {f:8} → {format_date(today, format=f, locale='en_US')}")
print("\n Full date by locale:")
for loc in locales:
print(f" {loc:10} → {format_date(today, format='full', locale=loc)}")
print("\n Relative time:")
deltas = [timedelta(minutes=5), timedelta(hours=2), timedelta(days=3), timedelta(weeks=2)]
for loc in ["en_US", "de_DE", "fr_FR", "zh_CN"]:
print(f" [{loc}] {format_timedelta(timedelta(hours=2), add_direction=True, locale=loc)}")
# ─────────────────────────────────────────────────────────────────────────────
# 3. Locale introspection
# ─────────────────────────────────────────────────────────────────────────────
def get_locale_info(locale_str: str) -> dict[str, Any]:
"""
Return metadata about a locale identifier.
Locale.parse("de_DE") returns a Locale object with rich introspection.
"""
try:
locale = Locale.parse(locale_str)
except Exception as e:
return {"error": str(e)}
return {
"identifier": str(locale),
"display_name": locale.display_name,
"language": locale.language,
"territory": locale.territory,
"decimal_symbol": locale.number_symbols.get("decimal"),
"group_symbol": locale.number_symbols.get("group"),
"currency_symbol": locale.currency_symbols.get(locale.territory or "US", ""),
"first_week_day": locale.first_week_day, # 0=Mon, 6=Sun
"days": list(locale.days["wide"]["format"].values()),
}
def list_locales(language: str) -> list[str]:
"""List all available locales for a given language code."""
from babel import core
return sorted(
s for s in core.locale_identifiers()
if s.startswith(language + "_") or s == language
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Timezone display
# ─────────────────────────────────────────────────────────────────────────────
def format_with_timezone(
dt: datetime,
from_tz: str,
to_tz: str,
locale: str = "en_US",
) -> str:
"""
Convert a naive datetime from one timezone to another and format it.
get_timezone() returns a pytz/zoneinfo-compatible timezone object.
"""
from_zone = get_timezone(from_tz)
to_zone = get_timezone(to_tz)
# Localize naive datetime
if dt.tzinfo is None:
from pytz import timezone as pytz_tz
dt = pytz_tz(from_tz).localize(dt)
tz_name = get_timezone_name(to_zone, locale=locale)
formatted = format_datetime(dt, format="long", locale=locale, tzinfo=to_zone)
return f"{formatted} ({tz_name})"
# ─────────────────────────────────────────────────────────────────────────────
# 5. Flask-Babel integration pattern
# ─────────────────────────────────────────────────────────────────────────────
FLASK_BABEL_EXAMPLE = '''
from flask import Flask, request, g
from flask_babel import Babel, format_currency, format_date, gettext as _
app = Flask(__name__)
babel = Babel(app)
SUPPORTED_LOCALES = ["en", "de", "fr", "ja", "zh"]
@babel.localeselector
def get_locale():
"""Detect locale from Accept-Language header or user preference."""
return request.accept_languages.best_match(SUPPORTED_LOCALES) or "en"
@app.route("/price/<float:amount>")
def show_price(amount: float):
locale = get_locale()
formatted = format_currency(amount, "USD", locale=locale)
return {"price": formatted, "locale": locale}
# In Jinja2 templates:
# {{ amount | format_currency("USD") }}
# {{ date_value | format_date("full") }}
# {{ _("Hello, %(name)s!", name=user.name) }}
'''
# ─────────────────────────────────────────────────────────────────────────────
# 6. Currency info helpers
# ─────────────────────────────────────────────────────────────────────────────
def get_currency_info(currency_code: str, locale: str = "en_US") -> dict[str, str]:
"""Return display name and symbol for a currency code."""
return {
"code": currency_code,
"name": get_currency_name(currency_code, locale=locale),
"symbol": get_currency_symbol(currency_code, locale=locale),
}
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Number formatting ===")
demo_numbers()
print("\n=== Date formatting ===")
demo_dates()
print("\n=== Locale info ===")
for loc in ["en_US", "de_DE", "fr_FR", "ja_JP"]:
info = get_locale_info(loc)
print(f" [{loc}] {info.get('display_name')} — "
f"decimal={info.get('decimal_symbol')!r} "
f"group={info.get('group_symbol')!r}")
print("\n=== Currency info ===")
for code in ["USD", "EUR", "JPY", "GBP", "CHF"]:
info = get_currency_info(code)
print(f" {info['code']}: {info['name']:25} ({info['symbol']})")
print("\n=== Compact numbers ===")
for n in [1_200, 15_000, 1_200_000, 2_400_000_000]:
print(f" {n:>15,} → {format_compact_decimal(n, locale='en_US')}")
For the locale (stdlib) alternative — Python’s built-in locale module uses the OS system locale (set via locale.setlocale()), which is a process-global change that breaks thread safety and CI reproducibility; Babel passes the locale as a function argument (format_currency(value, "EUR", locale="fr_FR")), making per-request locale formatting safe in multi-threaded web servers without any global state. For the babel vs. arrow alternative — Arrow is a datetime manipulation library (“3 days ago” creation and relative difference), while Babel is a display/formatting library — they solve different problems and are often used together: Arrow for datetime arithmetic, Babel for rendering the result in a user’s locale with format_date(arrow_dt.date(), format="full", locale=user_locale). The Claude Skills 360 bundle includes Babel skill sets covering format_decimal for locale-aware number formatting, format_currency with currency code and locale, format_compact_decimal for compact notation, format_date/format_time/format_datetime with short/medium/long/full formats, format_timedelta with add_direction, Locale.parse() introspection, get_locale_info() metadata extraction, get_currency_name/symbol helpers, format_with_timezone using get_timezone, and Flask-Babel localeselector pattern. Start with the free tier to try internationalization formatting code generation.