Quick Start
From install to a fully translated FastAPI endpoint in a few minutes. This page covers the core pattern — write JSON translations, wire up automatic locale detection, and (optionally) a translatable model.
Setup
Create a translations/ directory next to your app, with one JSON file per locale:
your-project/
├── translations/
│ ├── en.json # default locale — should be 100% complete
│ └── es.json
└── main.py
{
"messages": {
"welcome": "Welcome to the API!",
"hello": "Hello, {name}!"
},
"errors": {
"not_found": "Resource not found"
}
}
{
"messages": {
"welcome": "¡Bienvenido a la API!",
"hello": "¡Hola, {name}!"
},
"errors": {
"not_found": "Recurso no encontrado"
}
}
_() initializes automatically from the translations/ directory on first use — no setup code required.
Your First Translation
from fastkit_i18n import _
message = _("messages.welcome")
# "Welcome to the API!"
greeting = _("messages.hello", name="Alice")
# "Hello, Alice!"
missing = _("nonexistent.key")
# "nonexistent.key" — never raises, returns the key itself
Switching Locale
Use set_locale() to change the current locale — every _() call afterward uses it until changed again:
from fastkit_i18n import _, set_locale, get_locale
set_locale("es")
_("messages.welcome") # "¡Bienvenido a la API!"
get_locale() # "es"
# Force a specific locale regardless of current
_("messages.welcome", locale="en") # "Welcome to the API!"
set_locale() is context-local (built on Python's contextvars), so it's safe under concurrent async requests — one request's locale never leaks into another's.
Using TranslationManager Directly
_() is a thin wrapper around a single global TranslationManager instance. Access that instance directly with get_translation_manager() when you need more than a plain lookup — checking whether a key exists, listing loaded locales, or reloading files from disk:
from fastkit_i18n import get_translation_manager
manager = get_translation_manager()
manager.get("messages.welcome") # same lookup _() does
manager.get("messages.hello", name="Alice") # with variables
manager.has("messages.welcome") # True
manager.has("messages.welcome", locale="de") # False - no de.json
manager.get_available_locales() # ["en", "es"]
manager.get_all("es") # full es.json as a dict
manager.reload() # re-read JSON files from disk
Use set_translation_manager() to replace the global instance entirely — point it at a different directory, or change the default/fallback locale. Every subsequent _() call uses the new instance:
from fastkit_i18n import TranslationManager, set_translation_manager, _
custom_manager = TranslationManager(
translations_dir="locales", # instead of the default "translations/"
default_locale="es",
fallback_locale="en",
)
set_translation_manager(custom_manager)
_("messages.welcome") # now resolved through custom_manager
Every keyword argument to TranslationManager() is optional — omit default_locale/fallback_locale and it falls back to the app-wide default set via set_default_locale():
| Method | Description |
|---|---|
get(key, locale=None, fallback=True, **kwargs) | Translate a key. Falls back to the fallback locale, then returns the key itself if still not found. |
has(key, locale=None) | Check whether a key exists for a given (or current) locale. |
get_all(locale=None) | Return the full translation dict for a locale. |
get_available_locales() | List locale codes with a loaded translation file, e.g. ["en", "es", "fr"]. |
reload() | Re-read all .json files from translations_dir from disk. |
Common in tests: build a TranslationManager pointed at a temp directory with fixture translation files, call set_translation_manager() in setup, and restore the original afterward — keeps tests fully isolated from the app's real translation files.
FastAPI Integration
Add LocaleMiddleware and locale detection happens automatically — no manual set_locale() calls in your routes:
from fastapi import FastAPI
from fastkit_i18n import LocaleMiddleware, _
app = FastAPI()
app.add_middleware(LocaleMiddleware)
@app.get("/")
def root():
return {"message": _("messages.welcome")}
@app.get("/hello/{name}")
def hello(name: str):
return {"message": _("messages.hello", name=name)}
Locale is detected in this order: Accept-Language header → ?lang= query parameter → locale cookie → app default.
curl http://localhost:8000/ -H "Accept-Language: es"
# {"message": "¡Bienvenido a la API!"}
curl "http://localhost:8000/?lang=es"
# {"message": "¡Bienvenido a la API!"}
curl http://localhost:8000/ --cookie "locale=es"
# {"message": "¡Bienvenido a la API!"}
See the Middleware guide for the full priority chain and configuration options.
A Translatable Model
For content that lives in the database — blog posts, product names — TranslatableMixin stores every locale in one JSON column, and a field reads/writes like a plain string:
from sqlalchemy import JSON
from sqlalchemy.orm import Mapped, mapped_column
from fastkit_i18n import TranslatableMixin
class Article(TranslatableMixin, Base):
__translatable__ = ["title", "content"]
title: Mapped[dict] = mapped_column(JSON)
content: Mapped[dict] = mapped_column(JSON)
article = Article()
article.set_locale("en")
article.title = "Getting Started with FastAPI"
article.set_locale("es")
article.title = "Comenzando con FastAPI"
article.set_locale("en")
article.title # "Getting Started with FastAPI"
Requires the [sqlalchemy] extra. See the Database Models guide for SQLModel support, fallback locales, and validation.
Complete Example
Everything together — a two-endpoint API with automatic locale detection:
from fastapi import FastAPI, HTTPException
from fastkit_i18n import LocaleMiddleware, _
app = FastAPI()
app.add_middleware(LocaleMiddleware)
TASKS = {1: "Buy milk", 2: "Walk the dog"}
@app.get("/")
def root():
return {"message": _("messages.welcome")}
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
if task_id not in TASKS:
raise HTTPException(404, detail=_("errors.not_found"))
return {"task": TASKS[task_id]}
# English (default)
curl http://localhost:8000/
# {"message": "Welcome to the API!"}
# Spanish
curl http://localhost:8000/ -H "Accept-Language: es"
# {"message": "¡Bienvenido a la API!"}
# Error in Spanish
curl http://localhost:8000/tasks/999 -H "Accept-Language: es"
# {"detail": "Recurso no encontrado"}