Docs / fastkit-i18n / Quick Start

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:

text
your-project/
├── translations/
│   ├── en.json     # default locale — should be 100% complete
│   └── es.json
└── main.py
json
{
  "messages": {
    "welcome": "Welcome to the API!",
    "hello":   "Hello, {name}!"
  },
  "errors": {
    "not_found": "Resource not found"
  }
}
json
{
  "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

python
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:

python
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:

python
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:

python
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():

MethodDescription
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:

python
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.

bash
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:

python
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:

python
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]}
bash
# 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"}