FormatJS ships Python 3.12+ bindings for its Rust parsers and runtimes. Wheels
use Python's stable ABI, so one cp312-abi3 wheel works on CPython 3.12 and
newer for the same platform.
Packages#
| Package | Rust backend | Use it for |
|---|---|---|
py_intl | formatjs_intl | Catalog lookup, locale negotiation, and fallback |
icu_messageformat | formatjs_icu_messageformat | Formatting one ICU message |
icu_messageformat_parser | formatjs_icu_messageformat_parser | Parsing or printing ICU MessageFormat ASTs |
icu_skeleton_parser | formatjs_icu_skeleton_parser | Parsing ICU number and date/time skeletons |
Install only the layer needed by the application:
python -m pip install py_intl
Format messages#
from datetime import date
from intl import Intl, IntlError
errors: list[IntlError] = []
intl = Intl(
["fr-CA", "fr"],
"en",
{
"en": {},
"fr": {"tasks": "{count, plural, one {# tâche} other {# tâches}}"},
},
on_error=errors.append,
)
assert intl.locale == "fr"
assert intl.format_message("tasks", values={"count": 2}) == "2 tâches"
Create or select an Intl instance for each request locale. Generated message
IDs depend only on the normalized default message and description, so catalogs
use the same IDs across locales.
formatjs extract "src/**/*.py" extracts literal define_message(...)
declarations and format_message(...) calls. Inline calls accept dynamic
values. IDs are optional; missing IDs use the same 10-character generated ID
as Rust.
title = intl.format_message(
default_message="Welcome, {name}!",
values={"name": user.name},
)
Reusable descriptors match the backend authoring shape used by Bazel consumers:
from intl import define_message
TITLE = define_message(default_message="Welcome, {name}!")
title = intl.format_message(TITLE, values={"name": user.name})
Python date and datetime values work with ICU date and time arguments:
created = intl.format_message(
default_message="Created {value, date, medium}",
values={"value": date(2024, 1, 2)},
)
Datetime fields are formatted as supplied. Convert timezone-aware values to the desired presentation timezone before formatting.
Like @formatjs/intl, formatting returns the formatted value directly.
Recovered missing translations and formatting failures are sent to on_error.
Use icu_messageformat when catalog lookup happens elsewhere:
from icu_messageformat import IcuMessageFormat
message = IcuMessageFormat(
"{count, plural, one {# item} other {# items}}",
locale="en",
)
assert message.format({"count": 2}) == "2 items"
Parse messages and skeletons#
from icu_messageformat_parser import parse, print_ast
from icu_skeleton_parser import parse_number_skeleton
ast = parse("Hello, {name}!")
assert print_ast(ast) == "Hello, {name}!"
options = parse_number_skeleton("currency/USD compact-short")
Python APIs use snake_case. The py_intl distribution imports as intl;
other distribution and import package names match. Hermetic cross-compiled
wheels target macOS and manylinux 2.28 on arm64 and x86_64.
Generated message contracts#
Generate typed Python wrappers from a FormatJS source catalog:
{
"cart.total": {"defaultMessage": "{count, plural, other {# items}}"},
"cart.empty": {"defaultMessage": "Your cart is empty"}
}
python -m intl.codegen en.json --out messages.py
Keep both generated files: messages.py supplies runtime wrappers;
messages.pyi supplies message-specific TypedDict contracts. The stub uses
typing_extensions.ReadOnly for Python 3.12 compatibility. The wrappers add
no runtime dependency on typing_extensions.
from messages import cart_total, cart_empty
cart_total(intl, values={"count": 2})
cart_empty(intl)
# Type errors:
cart_total(intl)
cart_total(intl, values={"count": "two"})
Names derive from catalog IDs by replacing punctuation with underscores;
leading digits and Python keywords receive a message_ prefix. Collisions
are rejected. String catalogs and descriptors with defaultMessage and an
optional string description are supported.
Contracts come from the Rust ICU parser, including apostrophe escapes and every
nested branch. Numbers/plurals require int | float; dates/times accept
date | datetime | int | float. Repeated arguments must satisfy every role.
All contract fields are required and readonly to the type checker. Empty messages
allow omitted values. Runtime dictionaries are not frozen.
Python's type system treats bool as an int, so static numeric checks cannot
exclude booleans. Runtime formatting still checks values and translated messages.
Rich tags are rejected during generation because Python Intl has no rich callback
API. Raw Intl.format_message calls keep their existing dynamic interface;
use the generated wrappers for per-message checks. Generate from source catalogs,
not independently from each translation.