starlette-i18n
Request-safe gettext localization and locale negotiation for Starlette applications.
Requirements
- Python 3.11+ (CI validates Python 3.11, 3.12, 3.13, and 3.14)
- Starlette newer than 1.2.0 and below 2.0
Install
uv add starlette-i18n
pip install starlette-i18n
Quick start
Compile gettext catalogs before starting the application, then load them once:
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette_i18n import LocaleMiddleware, TranslationCatalog, gettext
catalog = TranslationCatalog.load("locales", domain="messages", default_locale="en")
app = Starlette()
app.add_middleware(LocaleMiddleware, catalog=catalog)
async def homepage(request):
return PlainTextResponse(gettext("Success"))
app.add_route("/", homepage)
The selected Locale is also available as request.state.locale. The public translation helpers are gettext, ngettext, gettext_lazy, and get_locale.
Locale selection
LocaleMiddleware selects one catalog for each HTTP request in this order:
- A valid configured locale cookie (
localeby default). - The request's
Accept-Languageheader. - The catalog's configured default locale.
Header values are parsed as a q-weighted priority list and resolved using RFC 4647 lookup. Exact complex tags work: a zh-CN request selects a zh_CN catalog directory, while pt-BR can fall back to a pt catalog. Invalid values and q=0 entries are ignored. A wildcard falls back to the configured default when no explicit supported locale matches.
Responses include Content-Language. The middleware adds configured selection inputs (Cookie and Accept-Language by default) to Vary so shared caches do not serve the wrong representation.
Configure another cookie name, or disable cookies entirely:
app.add_middleware(LocaleMiddleware, catalog=catalog, cookie_name="language")
app.add_middleware(LocaleMiddleware, catalog=catalog, cookie_name=None)
ASGI and Jinja safety
The active locale is stored in a ContextVar and is reset after every request, including exceptions. Concurrent requests therefore cannot leak translations into one another.
Do not install gettext functions into a shared Jinja environment per request: Jinja's installation mutates global environment state. Pass a request-local callable instead:
html = template.render(request=request, _=request.state.locale.translate)
Then use {{ _("Success") }} in the template. See examples/jinja_app.py.
Examples
uv run --with uvicorn examples/basic_usage.py
uv run --with 'uvicorn[standard]' uvicorn examples.uvicorn_runner:app --reload
uv run --with gunicorn --with uvicorn gunicorn -c examples/gunicorn_conf.py examples.gunicorn_runner:app
curl -H 'Accept-Language: ru' http://127.0.0.1:8000/
Migrating from 2.x
3.0 is a breaking release. It requires Python 3.11+, moves the package to src/, uses pyproject.toml and uv, and replaces LocaleDefaultMiddleware, LocaleFromCookieMiddleware, LocaleFromHeaderMiddleware, and the old ordering-dependent LocaleMiddleware with the single LocaleMiddleware shown above. Load catalogs with TranslationCatalog.load() instead of load_gettext_translations().
Further documentation
Development
uv sync --all-groups
make lint
make test
make build
License
starlette-i18n is distributed under the Apache License 2.0. Report vulnerabilities through the security policy.
Release files for starlette-i18n 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| starlette_i18n-3.0.0.tar.gz | 64.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| starlette_i18n-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.4 kB
Release files / starlette_i18n-3.0.0.tar.gz
| Download URL | starlette_i18n-3.0.0.tar.gz |
|---|---|
| Size | 64.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af4557c07158b6b785ba6b18bff108ef4a914f041f9ac52cb424cab85a76880f
|
|
BLAKE2b-256 checksum How to use checksums |
b0991c82ee6457fce547fc1d87cd6be92a5dd6f6f12a9154754c28d67cf6e440
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 21, 2026.
Transparency logRelease files / starlette_i18n-3.0.0-py3-none-any.whl
| Download URL | starlette_i18n-3.0.0-py3-none-any.whl |
|---|---|
| Size | 12.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
62319160d97090736abb888d7f4bc2e3218b5599dc6515916145dc5d3da7654a
|
|
BLAKE2b-256 checksum How to use checksums |
0d9e20630439800ad775b17766f9ed93f393a37812104f052c2da8b150d02828
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 21, 2026.
Transparency log