Translations
JSON-based i18n for your entire FastAPI application — validation messages, API responses, model content, and UI labels. Works out of the box with automatic locale detection and graceful fallbacks. Zero mandatory dependencies.
Single-language apps benefit too. Centralizing all text in JSON means you change a label in one place — not in 20 scattered Python strings — and you're ready for i18n whenever you need it.
Introduction
fastkit-i18n's translation system covers four distinct use cases that all work together:
Field errors formatted in the user's locale — pairs with fastkit-core's validation-error formatting.
Success and error messages returned in the user's language.
Multi-language fields via TranslatableMixin.
Centralized text management even in single-language apps.
Quick Example
{
"messages": {
"welcome": "Welcome to fastkit-i18n!",
"hello": "Hello, {name}!",
"items_count": "You have {count} items"
},
"auth": {
"login": "Log In",
"logout": "Log Out"
}
}
{
"messages": {
"welcome": "¡Bienvenido a fastkit-i18n!",
"hello": "¡Hola, {name}!",
"items_count": "Tienes {count} artículos"
},
"auth": {
"login": "Iniciar Sesión",
"logout": "Cerrar Sesión"
}
}
from fastkit_i18n import _, set_locale
# Simple translation — uses current locale
greeting = _('messages.welcome') # "Welcome to fastkit-i18n!"
# With parameters
hello = _('messages.hello', name='Alice') # "Hello, Alice!"
count = _('messages.items_count', count=5) # "You have 5 items"
# Switch locale
set_locale('es')
greeting_es = _('messages.welcome') # "¡Bienvenido a fastkit-i18n!"
# Force a specific locale regardless of current
login_fr = _('auth.login', locale='fr') # "Connexion" (if fr.json exists)
Setup
Directory structure
your-project/
├── translations/
│ ├── en.json # English — default, must be 100% complete
│ ├── es.json # Spanish
│ ├── fr.json # French
│ └── de.json # German
└── main.py
Automatic initialization
The translation manager initializes automatically on first use — no setup code required:
from fastkit_i18n import _
# First call auto-initializes from 'translations/' directory
text = _('messages.welcome')
Manual initialization
For a custom directory, a non-default locale, or multiple managers, construct TranslationManager explicitly. See API Reference below for the full method list:
from fastkit_i18n import TranslationManager, set_translation_manager
manager = TranslationManager(
translations_dir='lang',
default_locale='es',
fallback_locale='en',
)
set_translation_manager(manager)
Translation Files
File format
Flat or nested JSON — access any level with dot notation:
{
"app": {
"name": "My Application",
"tagline": "Built with fastkit-i18n"
},
"auth": {
"login": "Log In",
"logout": "Log Out",
"welcome": "Welcome back, {username}!"
},
"messages": {
"success": "Operation completed successfully",
"saved": "{model} saved successfully"
},
"validation": {
"required": "The {field} field is required",
"email": "The {field} must be a valid email address",
"min_length": "The {field} must be at least {min} characters"
}
}
_('app.name') # "My Application"
_('auth.welcome', username='Alice') # "Welcome back, Alice!"
_('validation.min_length', field='name', min=3) # "The name must be at least 3 characters"
Parameter substitution
Use {parameter} placeholders for dynamic content:
{
"greetings": {
"hello": "Hello, {name}!",
"welcome": "Welcome, {name}. You have {count} new messages."
},
"notifications": {
"comment": "{user} commented on your post: {comment}"
}
}
_('greetings.hello', name='Alice')
# "Hello, Alice!"
_('greetings.welcome', name='Bob', count=5)
# "Welcome, Bob. You have 5 new messages."
_('notifications.comment', user='Charlie', comment='Great post!')
# "Charlie commented on your post: Great post!"
If a placeholder in the template has no matching keyword argument, _() returns the string as-is (with the unresolved {placeholder}) rather than raising — a typo in a translation file never crashes a request.
Supported language codes
Any ISO 639-1 code works — just create the corresponding {code}.json file. Locale codes aren't validated against a fixed list.
Using Translations
The _() helper
The primary way to translate — import it anywhere in your code:
from fastkit_i18n import _
# Current locale
message = _('messages.success')
# With parameters
greeting = _('messages.hello', name='Alice')
# Force specific locale (ignores current)
text_es = _('messages.welcome', locale='es')
# Missing key — returns the key itself, never raises
missing = _('missing.key') # → "missing.key"
Locale management
from fastkit_i18n import set_locale, get_locale
set_locale('es')
current = get_locale() # 'es'
# All subsequent _() calls use 'es' until changed
welcome = _('messages.welcome') # Spanish version
set_locale()/get_locale() are the same shared, context-local locale used by LocaleMiddleware and TranslatableMixin — set it once (e.g. per request) and everything reads it consistently.
TranslationManager directly
For checking key existence, listing locales, or reloading files from disk — see the Quick Start walkthrough for hands-on examples, or the full method list in API Reference below.
Fallback Behavior
Missing translations never break your app — fastkit-i18n falls back gracefully through a three-level chain:
set_locale('fr'); _('messages.welcome') → French translation
_('messages.new_feature') → English translation
_('missing.key') → "missing.key"
Partial translations work fine. Your es.json can be 70% complete — the missing 30% quietly fall back to the fallback locale. No errors, no empty strings. Pass fallback=False to _() to disable this and get the raw key back instead.
Configuration
There's no config framework to wire up — set the app-wide default locale once, wherever your app starts:
from fastkit_i18n import set_default_locale
# Call once, e.g. at import time or in your app's startup
set_default_locale("es")
This is the same app-wide default read by TranslationManager(), TranslatableMixin, and LocaleMiddleware whenever none of them are given an explicit locale — set it in one place, everything else picks it up:
from fastkit_i18n import set_default_locale, TranslationManager, get_default_locale
set_default_locale("es")
manager = TranslationManager() # default_locale resolves to "es" automatically
get_default_locale() # "es"
# Explicit args always win over the app-wide default
manager2 = TranslationManager(default_locale="en") # stays "en"
If you're reading the locale from an environment variable or your own settings object, just pass it to set_default_locale() yourself — fastkit-i18n stays out of the config-management business on purpose (see fastkit-core if you want a full settings/config layer).
API Integration
The simplest approach — LocaleMiddleware handles detection automatically, no manual setup per-route needed:
from fastapi import FastAPI
from fastkit_i18n import LocaleMiddleware, _
app = FastAPI()
app.add_middleware(LocaleMiddleware) # auto-detects from Accept-Language / ?lang= / cookie
@app.get("/")
def root():
# locale already set by middleware
return {"message": _('messages.welcome')}
See the Middleware guide for the full detection priority, per-scope behavior, and a custom-dependency alternative for frameworks without middleware support.
Best Practices
Organize by feature
{
"auth": {
"login": "Log In",
"logout": "Log Out",
"welcome": "Welcome back, {username}!"
},
"products": {
"created": "Product '{name}' created",
"delete_confirm": "Delete '{name}'?"
},
"errors": {
"not_found": "Resource not found",
"unauthorized": "Unauthorized access"
},
"validation": {
"required": "{field} is required",
"min": "{field} must be at least {min} characters"
}
}
Keep default language 100% complete
en.json
100% — must be complete
es.json
80% — falls back to en
fr.json
60% — falls back to en
Test translation coverage
def test_critical_translations():
from fastkit_i18n import TranslationManager
manager = TranslationManager()
locales = manager.get_available_locales()
required = ['messages.welcome', 'auth.login', 'errors.not_found']
for locale in locales:
for key in required:
assert manager.has(key, locale=locale), \
f"Missing key '{key}' in locale '{locale}'"
Never hardcode text in code
return {
"message": "Operation successful"
}
return {
"message": _('messages.success')
}
API Reference
Module-level functions
{placeholder} replacements._(). GNU gettext convention for familiarity._() calls — and TranslatableMixin/LocaleMiddleware, which share this same context — until changed.'en'), falling back to the app-wide default if unset.TranslationManager instance, lazily created on first use.TranslationManager
default_locale falls back to the app-wide default (get_default_locale()) when omitted. fallback_locale falls back to default_locale when omitted.fallback=True by default — falls back to the fallback locale, then to the key itself.bool — check if a key exists for a locale.['en', 'es', 'fr'])..json files from disk. Useful in development, or after writing new translation files.set_locale()/get_locale(), which is the preferred API.Complete Example
A Task Manager API with full English/Spanish translation support:
{
"app": {
"name": "Task Manager"
},
"tasks": {
"title": "Tasks",
"created": "Task '{title}' created successfully",
"updated": "Task '{title}' updated",
"deleted": "Task deleted",
"completed": "{count} task(s) completed",
"empty": "No tasks yet"
},
"errors": {
"task_not_found": "Task not found",
"unauthorized": "You are not authorized to perform this action"
}
}
{
"app": {
"name": "Gestor de Tareas"
},
"tasks": {
"title": "Tareas",
"created": "Tarea '{title}' creada con éxito",
"updated": "Tarea '{title}' actualizada",
"deleted": "Tarea eliminada",
"completed": "{count} tarea(s) completada(s)",
"empty": "Aún no hay tareas"
},
"errors": {
"task_not_found": "Tarea no encontrada",
"unauthorized": "No tienes autorización para realizar esta acción"
}
}
from fastapi import FastAPI, HTTPException
from fastkit_i18n import LocaleMiddleware, _
app = FastAPI()
app.add_middleware(LocaleMiddleware) # auto-detects Accept-Language
TASKS = {}
next_id = 1
@app.get("/")
def root():
return {"app": _('app.name'), "section": _('tasks.title')}
@app.get("/tasks")
def list_tasks():
if not TASKS:
return {"message": _('tasks.empty'), "tasks": []}
return {"tasks": list(TASKS.values())}
@app.post("/tasks", status_code=201)
def create_task(title: str):
global next_id
TASKS[next_id] = {"id": next_id, "title": title}
next_id += 1
return {"message": _('tasks.created', title=title), "id": next_id - 1}
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
if task_id not in TASKS:
raise HTTPException(404, detail=_('errors.task_not_found'))
return TASKS[task_id]
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
if task_id not in TASKS:
raise HTTPException(404, detail=_('errors.task_not_found'))
del TASKS[task_id]
# English (default)
curl http://localhost:8000/
# {"app": "Task Manager", "section": "Tasks"}
# Spanish
curl http://localhost:8000/ \
-H "Accept-Language: es"
# {"app": "Gestor de Tareas", "section": "Tareas"}
# Create task in Spanish
curl -X POST "http://localhost:8000/tasks?title=Buy%20milk" \
-H "Accept-Language: es"
# {"message": "Tarea 'Buy milk' creada con éxito", "id": 1}
# Error in Spanish
curl http://localhost:8000/tasks/999 \
-H "Accept-Language: es"
# {"detail": "Tarea no encontrada"}