Skip to main content

Laravel-style i18n for FastAPI. JSON translation files, automatic locale detection, and translatable SQLAlchemy/SQLModel models — without touching a framework.

Project description

FastKit i18n

Laravel-style i18n for FastAPI. JSON translation files, automatic locale detection, and translatable SQLAlchemy/SQLModel models — without touching a framework. Pairs with fastkit-core for translated Pydantic validation errors.

PyPI version Python License: MIT Part of FastKit

FastAPI i18n Localization


The problem

FastAPI has no built-in answer for internationalization. Not in models, not in validation errors, not in responses. The typical approach looks like this:

# Manual locale handling — repeated across every endpoint
article = session.get(Article, 1)
locale = request.headers.get("Accept-Language", "en")[:2]
title = article.title.get(locale) or article.title.get("en")  # manual fallback

# English-only Pydantic errors
# Inconsistent response shapes per endpoint

fastkit-i18n solves all three problems with one install.


Installation

pip install fastkit-i18n

Requirements: Python 3.10+, no mandatory dependencies beyond the standard library. The TranslatableMixin needs SQLAlchemy — install it with pip install fastkit-i18n[sqlalchemy] when you need it. _() and LocaleMiddleware need nothing extra.


Quick start

1. Set up translation files

translations/
├── en.json
├── de.json
└── fr.json
// translations/en.json
{
  "validation": {
    "required": "The {field} field is required.",
    "email": "The {field} field must be a valid email address."
  },
  "messages": {
    "welcome": "Welcome, {name}!",
    "created": "{resource} created successfully."
  }
}

2. Initialize at startup

# main.py
from fastapi import FastAPI
from fastkit_i18n import TranslationManager, set_translation_manager
from fastkit_i18n.middleware import LocaleMiddleware

app = FastAPI()
app.add_middleware(LocaleMiddleware)

set_translation_manager(
    TranslationManager(
        translations_dir="translations/",
        default_locale="en",
        fallback_locale="en",
    )
)

3. Translate anywhere

from fastkit_i18n import _

_("messages.welcome", name="Maria")            # → "Welcome, Maria!"
_("messages.welcome", name="Maria", locale="de")  # → "Willkommen, Maria!"

The locale set by LocaleMiddleware per request is picked up automatically — no need to pass it explicitly in route handlers.


Features

_() — translation helper

Laravel-style dot notation with named interpolation:

from fastkit_i18n import _

# Simple key
_("validation.required")

# With interpolation
_("validation.string_too_short", field="name", min_length=3)

# Explicit locale — overrides request locale
_("messages.welcome", name="Ana", locale="fr")

LocaleMiddleware — automatic locale detection

Register once. Every request sets the correct locale automatically.

app.add_middleware(LocaleMiddleware)

Resolution order:

  1. Accept-Language request header (e.g. Accept-Language: de)
  2. ?lang= query parameter (e.g. /articles?lang=fr)
  3. locale cookie
  4. Default locale from TranslationManager

TranslatableMixin — i18n directly in SQLAlchemy models

Declare translatable fields once. Read and write them like normal strings — the mixin handles locale routing automatically.

from sqlalchemy import JSON
from sqlalchemy.orm import Mapped, mapped_column
from fastkit_i18n import TranslatableMixin

class Article(TranslatableMixin, Base):
    __translatable__ = ["title", "content"]
    __fallback_locale__ = "en"

    title: Mapped[dict] = mapped_column(JSON)
    content: Mapped[dict] = mapped_column(JSON)
article = Article()

# Write
article.set_locale("en")
article.title = "How to build scalable APIs"

article.set_locale("de")
article.title = "Wie man skalierbare APIs entwickelt"

# Read — always returns current locale with fallback
article.set_locale("fr")
print(article.title)  # "How to build scalable APIs" — English fallback

# All translations at once
article.get_translations("title")
# {"en": "How to build scalable APIs", "de": "Wie man skalierbare APIs entwickelt"}

With LocaleMiddleware registered, article.title reads the correct locale automatically per request — no set_locale() needed in route handlers.

Works with SQLModel too

SQLModel is built directly on top of SQLAlchemy's ORM (a SQLModel(table=True) class is a real SQLAlchemy mapped class), so TranslatableMixin works with it the same way — no separate integration needed:

from sqlmodel import SQLModel, Field, Column, JSON
from fastkit_i18n import TranslatableMixin

class Article(TranslatableMixin, SQLModel, table=True):
    __translatable__ = ["title", "content"]

    id: int | None = Field(default=None, primary_key=True)
    title: dict = Field(sa_column=Column(JSON))
    content: dict = Field(sa_column=Column(JSON))

TranslatableMixin must come first in the base class list. It overrides __setattr__/__getattribute__ to make translatable fields look like plain strings, and it doesn't cooperate with super() there — if SQLModel (or any other class that also overrides these) comes first, its version wins for writes while TranslatableMixin's still wins for reads, so a field can silently look empty instead of raising an error. Get the order backwards and fastkit-i18n raises a TypeError immediately when the class is defined, telling you exactly what to fix — it doesn't fail silently at runtime.

Known limitation: because writes to translatable fields bypass Pydantic's own attribute bookkeeping, they don't show up in model_fields_set. If you rely on article.model_dump(exclude_unset=True), translatable fields you've set will be excluded as if untouched. Use get_translations() / has_translation() instead of model_dump() when you need to know which translatable fields were actually set.


Translated Pydantic validation errors

Formatting a Pydantic ValidationError into per-language messages isn't something fastkit-i18n does itself — mapping error types (missing, string_too_short, value_error.email, ...) to messages is validation-framework logic, not i18n logic, and keeping it out keeps this package dependency-free for that use case.

What fastkit-i18n provides is the primitive that logic is built on: _(), already resolving to the correct per-request locale via LocaleMiddleware. In the FastKit ecosystem this formatting lives in fastkit-core, which calls straight into _():

# Inside fastkit-core's error formatter (not part of this package)
from fastkit_i18n import _

_("validation.required", field="title")
# → "The title field is required." (or the current request's locale)

If you're not using fastkit-core, you can write the same kind of formatter yourself — walk exc.errors(), map each error['type'] to a validation.* key (see the file format below for the suggested key names), and call _().


Using with FastKit Core

fastkit-i18n is the standalone extraction of the i18n module from fastkit-core. If you use fastkit-core, you already have these features — no separate install needed.

Install just translations:

pip install fastkit-i18n

Or install the full FastKit toolkit:

pip install fastkit-core  # includes fastkit-i18n

Why not babel or gettext?

babel and gettext are excellent tools, but they require a compilation step (.po.mo), binary files in your repo, and a non-trivial setup process. For most FastAPI applications, JSON files are simpler to manage, easy to version in Git, straightforward to edit by non-developers, and ready to sync with translation platforms like Crowdin or Weblate.

fastkit-i18n is intentionally focused on JSON-based translations. If you need .po/.mo support, babel is the right tool.


Translation file format

Standard JSON with dot-notation keys and {variable} interpolation:

{
  "validation": {
    "required": "The {field} field is required.",
    "string_too_short": "The {field} must be at least {min_length} characters.",
    "string_too_long": "The {field} must not exceed {max_length} characters.",
    "email": "The {field} must be a valid email address.",
    "unique": "The {field} has already been taken."
  },
  "auth": {
    "invalid_credentials": "Invalid email or password.",
    "unauthorized": "You are not authorized to perform this action."
  },
  "messages": {
    "welcome": "Welcome, {name}!",
    "created": "{resource} created successfully.",
    "updated": "{resource} updated successfully.",
    "deleted": "{resource} deleted successfully."
  }
}

Part of the FastKit ecosystem

fastkit-i18n is part of FastKit — a collection of production-tested building blocks for FastAPI that bring the developer experience of Laravel to the Python ecosystem.

Package Description
fastkit-core Full toolkit — repository pattern, service layer, caching, events, HTTP utilities
fastkit-i18n This package — i18n standalone
fastkit-cli Code generation — scaffold modules, manage migrations and seeders
mailbridge Multi-provider email for any Python project

Documentation

Full documentation at fastkit.org/docs/fastkit-i18n


License

MIT — see LICENSE

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastkit_i18n-1.0.0.tar.gz (14.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastkit_i18n-1.0.0-py3-none-any.whl (17.3 kB view details)

Uploaded Python 3

File details

Details for the file fastkit_i18n-1.0.0.tar.gz.

File metadata

  • Download URL: fastkit_i18n-1.0.0.tar.gz
  • Upload date:
  • Size: 14.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fastkit_i18n-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b329a33ae35c00659b09f9704b5e9a6644ac0cabd9a20653bc8962671146c6b5
MD5 a39c68f5c44868a560f08f84e1e9b54d
BLAKE2b-256 61d14bff60804e6ae76b292a11fec04d9a208092ea2f19dbfe91cd094be009fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastkit_i18n-1.0.0.tar.gz:

Publisher: test_publish.yaml on fastkit-org/fastkit-i18n

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastkit_i18n-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: fastkit_i18n-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 17.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fastkit_i18n-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5e60fa9f0a1e75f10e3956286009a44ce88079d74f454c148c2937ac066298ad
MD5 f7d76e02c485d4ccfbe6fd8709590af0
BLAKE2b-256 9a2d9bde67d67fbf2ad9fd5f8bedf2b7bd37f81ff760ba632d610e964639f661

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastkit_i18n-1.0.0-py3-none-any.whl:

Publisher: test_publish.yaml on fastkit-org/fastkit-i18n

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page