Docs / fastkit-i18n / 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. 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:

Validation Messages

Field errors formatted in the user's locale — pairs with fastkit-core's validation-error formatting.

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

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

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

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

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

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

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. Locale codes aren't validated against a fixed list.

Using Translations

The _() helper

The primary way to translate — import it anywhere in your code:

python
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

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

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

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

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

python
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

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_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 {
    "message": "Operation successful"
}
✓ Good — translated
python
return {
    "message": _('messages.success')
}

API Reference

Module-level 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 context-local locale. Affects all subsequent _() calls — and TranslatableMixin/LocaleMiddleware, which share this same context — until changed.
get_locale()
Returns the current context-local locale string (e.g. 'en'), falling back to the app-wide default if unset.
set_default_locale(locale) / get_default_locale()
Set/get the app-wide default locale, used whenever no context locale or explicit locale is given. See Configuration above.
get_translation_manager()
Returns the global TranslationManager instance, lazily created on first use.
set_translation_manager(manager)
Replace the global manager. Useful for custom directories, custom locales, or test isolation.

TranslationManager

__init__(translations_dir="translations", default_locale=None, fallback_locale=None)
default_locale falls back to the app-wide default (get_default_locale()) when omitted. fallback_locale falls back to default_locale when omitted.
get(key, locale=None, fallback=True, **kwargs)
Get translated string. fallback=True by default — falls back to the fallback locale, then to the key itself.
has(key, locale=None)
Returns bool — check if a key exists for a locale.
get_all(locale=None)
Returns the full translation dict for a locale.
get_available_locales()
Returns list of language codes with translation files (e.g. ['en', 'es', 'fr']).
reload()
Re-reads all .json files from disk. Useful in development, or after writing new translation files.
set_manager_locale(locale) / get_manager_locale()
Set/get locale on this specific manager instance. Rarely needed directly — these read/write the same shared context as the module-level set_locale()/get_locale(), which is the preferred API.

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, 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]
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?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"}