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.
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 Core's translation system covers three distinct use cases that all work together:
Field errors automatically translated based on the user's locale.
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 Core!",
"hello": "Hello, {name}!",
"items_count": "You have {count} items"
},
"auth": {
"login": "Log In",
"logout": "Log Out"
}
}
{
"messages": {
"welcome": "¡Bienvenido a FastKit Core!",
"hello": "¡Hola, {name}!",
"items_count": "Tienes {count} artículos"
},
"auth": {
"login": "Iniciar Sesión",
"logout": "Cerrar Sesión"
}
}
from fastkit_core.i18n import _, set_locale
# Simple translation — uses current locale
greeting = _('messages.welcome') # "Welcome to FastKit Core!"
# 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 Core!"
# 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
├── config/
│ └── app.py
└── main.py
Automatic initialization
The translation manager initializes automatically on first use — no setup code required:
from fastkit_core.i18n import _
# First call auto-initializes from 'translations/' directory
text = _('messages.welcome')
Manual initialization
For custom directory or multiple managers:
from fastkit_core.i18n import TranslationManager, set_translation_manager
manager = TranslationManager(translations_dir='lang')
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 Core"
},
"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!"
Supported language codes
Any ISO 639-1 code works — just create the corresponding {code}.json file.
Using Translations
The _() helper
The primary way to translate — import it anywhere in your code:
from fastkit_core.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_core.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
TranslationManager directly
For advanced use cases — checking availability, listing locales, reloading:
from fastkit_core.i18n import get_translation_manager
manager = get_translation_manager()
# Get translation
text = manager.get('messages.welcome')
hello = manager.get('messages.hello', name='Alice')
# Check key existence
if manager.has('messages.custom'):
text = manager.get('messages.custom')
# List available languages
locales = manager.get_available_locales() # ['en', 'es', 'fr']
# Get all translations for a locale
all_en = manager.get_all(locale='en')
# Reload files (useful in development)
manager.reload()
Fallback Behavior
Missing translations never break your app — FastKit Core 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 English. No errors, no empty strings.
Configuration
# config/app.py
import os
TRANSLATIONS_PATH = os.getenv('APP_TRANSLATIONS_PATH', 'translations')
DEFAULT_LANGUAGE = os.getenv('APP_DEFAULT_LANGUAGE', 'en')
FALLBACK_LANGUAGE = os.getenv('APP_FALLBACK_LANGUAGE', 'en')
# .env
APP_DEFAULT_LANGUAGE=es
APP_FALLBACK_LANGUAGE=en
APP_TRANSLATIONS_PATH=lang
API Integration
The simplest approach — LocaleMiddleware from the HTTP module handles detection automatically. No manual setup per-route needed:
from fastapi import FastAPI
from fastkit_core.http import LocaleMiddleware
from fastkit_core.i18n import _
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')}
For more control — detect locale manually in each route or as a shared dependency:
from fastapi import Depends, Header
from fastkit_core.i18n import set_locale, _
async def detect_language(accept_language: str = Header(default='en')):
language = accept_language.split(',')[0].split('-')[0]
set_locale(language)
return language
# Apply to specific routes
@app.get("/", dependencies=[Depends(detect_language)])
def root():
return {"message": _('messages.welcome')}
# Or get locale in route for logging/response
@app.get("/users/{user_id}")
def get_user(user_id: int, lang: str = Depends(detect_language)):
return {
"greeting": _('auth.welcome', username=f'User {user_id}'),
"language": lang
}
Use _() directly in response messages and exceptions:
from fastkit_core.http import success_response, NotFoundException
from fastkit_core.i18n import _
@app.post("/products", status_code=201)
def create_product(data: ProductCreate):
product = service.create(data)
return success_response(
data=product.model_dump(),
message=_('messages.product_created', name=data.name)
)
@app.get("/products/{product_id}")
def get_product(product_id: int):
product = service.find(product_id)
if not product:
raise NotFoundException(_('errors.product_not_found'))
return success_response(data=product.model_dump())
@app.delete("/products/{product_id}", status_code=204)
def delete_product(product_id: int):
service.delete(product_id)
# 204 — no content, no message needed
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_core.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 success_response(
message="Operation successful"
)
return success_response(
message=_('messages.success')
)
API Reference
Helper functions
{placeholder} replacements._(). GNU gettext convention for familiarity._() calls until changed.'en').TranslationManager instance.TranslationManager
fallback=True by default — falls back to default locale then to key.bool — check if key exists for locale.['en', 'es', 'fr'])..json files from disk. Useful in development.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, Depends
from fastkit_core.http import (
success_response, paginated_response,
NotFoundException,
register_exception_handlers,
LocaleMiddleware, get_pagination
)
from fastkit_core.i18n import _
app = FastAPI()
register_exception_handlers(app)
app.add_middleware(LocaleMiddleware) # auto-detects Accept-Language
@app.get("/")
def root():
return {"app": _('app.name'), "section": _('tasks.title')}
@app.get("/tasks")
def list_tasks(pagination: dict = Depends(get_pagination)):
tasks, meta = service.paginate(**pagination)
return paginated_response(
items=[t.to_dict() for t in tasks],
pagination=meta,
message=_('tasks.title')
)
@app.post("/tasks", status_code=201)
def create_task(data: TaskCreate):
task = service.create(data)
return success_response(
data=task.model_dump(),
message=_('tasks.created', title=data.title)
)
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
task = service.find(task_id)
if not task:
raise NotFoundException(_('errors.task_not_found'))
return success_response(data=task.model_dump())
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
service.delete(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 \
-H "Accept-Language: es" \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk"}'
# {"success": true, "message": "Tarea 'Buy milk' creada con éxito", ...}
# Error in Spanish
curl http://localhost:8000/tasks/999 \
-H "Accept-Language: es"
# {"success": false, "message": "Tarea no encontrada"}