Docs / fastkit-core / Translations

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:

Validation Messages

Field errors automatically translated based on the user's locale.

API Responses

Success and error messages returned in the user's language.

Model Content

Multi-language fields via TranslatableMixin.

UI Labels

Centralized text management even in single-language apps.

Quick Example

json
{
  "messages": {
    "welcome":     "Welcome to FastKit Core!",
    "hello":       "Hello, {name}!",
    "items_count": "You have {count} items"
  },
  "auth": {
    "login":  "Log In",
    "logout": "Log Out"
  }
}
json
{
  "messages": {
    "welcome":     "¡Bienvenido a FastKit Core!",
    "hello":       "¡Hola, {name}!",
    "items_count": "Tienes {count} artículos"
  },
  "auth": {
    "login":  "Iniciar Sesión",
    "logout": "Cerrar Sesión"
  }
}
python
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

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

python
from fastkit_core.i18n import _

# First call auto-initializes from 'translations/' directory
text = _('messages.welcome')

Manual initialization

For custom directory or multiple managers:

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

json
{
  "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"
  }
}
python
_('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:

json
{
  "greetings": {
    "hello":   "Hello, {name}!",
    "welcome": "Welcome, {name}. You have {count} new messages."
  },
  "notifications": {
    "comment": "{user} commented on your post: {comment}"
  }
}
python
_('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

enEnglish
esSpanish
frFrench
deGerman
itItalian
ptPortuguese
nlDutch
ruRussian
zhChinese
jaJapanese
koKorean
arArabic

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:

python
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

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

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

Key found in current locale
set_locale('fr'); _('messages.welcome') → French translation
↓ if missing
Falls back to default locale (en)
_('messages.new_feature') → English translation
↓ if missing
Returns the key itself
_('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

python
# 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')
bash
# .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:

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

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

python
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

json
{
  "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

python
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

❌ Bad — hardcoded
python
return success_response(
    message="Operation successful"
)
✓ Good — translated
python
return success_response(
    message=_('messages.success')
)

API Reference

Helper functions

_(key, locale=None, **replacements)
Translate a key using dot notation. Returns the key itself if not found. Accepts any keyword arguments as {placeholder} replacements.
gettext(key, locale=None, **replacements)
Alias for _(). GNU gettext convention for familiarity.
set_locale(locale)
Set the current locale in context. Affects all subsequent _() calls until changed.
get_locale()
Returns the current locale string (e.g. 'en').
get_translation_manager()
Returns the global TranslationManager instance.
set_translation_manager(manager)
Replace the global manager. Useful for custom directories or testing.

TranslationManager

get(key, locale, fallback, **kw)
Get translated string. fallback=True by default — falls back to default locale then to key.
has(key, locale)
Returns bool — check if key exists for locale.
get_all(locale)
Returns full translation dict for locale.
get_available_locales()
Returns list of language codes with translation files (e.g. ['en', 'es', 'fr']).
set_locale(locale) / get_locale()
Set/get locale on the manager instance.
reload()
Re-reads all .json files from disk. Useful in development.

Complete Example

A Task Manager API with full English/Spanish translation support:

json
{
  "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"
  }
}
json
{
  "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"
  }
}
python
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)
bash
# 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"}