p.enthalabs

GitHub - Onlykh/translatable

![Image 1: Python 3.10+](https://www.python.org/downloads/)![Image 2: License: MIT](https://github.com/Onlykh/translatable/blob/main/LICENSE)

ORM-agnostic i18n for Python. Store translations in JSON columns, resolve by locale via a context variable — no extra tables, no `.po` files.

Inspired by spatie/laravel-translatable, built with Python idioms: `contextvars`, dict subclasses, optional extras.

Install

[](https://github.com/onlykh/translatable#install) > PyPI release coming soon. Install directly from GitHub for now.

core only

pip install git+https://github.com/onlykh/translatable.git

with SQLAlchemy support

pip install "translatable[sqlalchemy] @ git+https://github.com/onlykh/translatable.git"

with Starlette / FastAPI support

pip install "translatable[starlette] @ git+https://github.com/onlykh/translatable.git"

with Flask support

pip install "translatable[flask] @ git+https://github.com/onlykh/translatable.git"

everything

pip install "translatable[all] @ git+https://github.com/onlykh/translatable.git"

Quickstart

[](https://github.com/onlykh/translatable#quickstart)

from translatable import configure, set_locale

configure(default_locale="en", available_locales=["en", "fr", "ar"])

t = {"en": "Hello", "fr": "Bonjour"} from translatable import Translations

title = Translations(t) set_locale("fr") print(title) # Bonjour

SQLAlchemy 2.0

[](https://github.com/onlykh/translatable#sqlalchemy-20)

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from translatable.ext.sqlalchemy import translatable_column, MutableTranslations

class Base(DeclarativeBase): pass

class Article(Base): __tablename__ = "articles" id: Mapped[int] = mapped_column(primary_key=True)

from translatable import set_locale

set_locale("en") article.title = "Hello" # merges into current locale article.title["fr"] = "Bonjour" # merge other locales article.title.set_all({"en": "Hi"}) # explicit full replace

FastAPI / Starlette

[](https://github.com/onlykh/translatable#fastapi--starlette)

from fastapi import FastAPI from translatable.ext.starlette import TranslatableMiddleware

app = FastAPI() app.add_middleware(TranslatableMiddleware)

Query by locale

[](https://github.com/onlykh/translatable#query-by-locale)

from translatable.ext.sqlalchemy import by_locale

session.scalars( select(Article).where(by_locale(Article.title, "fr") == "Bonjour") ).all()

Dialect-specific SQL is generated automatically (PostgreSQL `->>`, SQLite `json_extract`, MySQL `JSON_EXTRACT`).

Configuration

[](https://github.com/onlykh/translatable#configuration) | Option | Default | Description | | --- | --- | --- | | `default_locale` | required | Fallback locale | | `available_locales` | `None` | Allowed locales (validated on `set_locale`) | | `fallback_map` | `{}` | e.g. `{"fr_ca": "fr"}` | | `on_missing` | `"fallback"` | `"fallback"` | `"empty"` | `"raise"` |

Concurrency

[](https://github.com/onlykh/translatable#concurrency) String and dict assignments **merge** into existing translations. Use `set_all()` to replace everything.

For concurrent updates to the same row, use one of:

1. **Optimistic locking** — SQLAlchemy `version_id_col` 2. **`atomic_set_locale()`** — PostgreSQL JSONB `||` merge (atomic per key) 3. **`SELECT FOR UPDATE`** — row lock during read-modify-write

from translatable.ext.sqlalchemy import atomic_set_locale

atomic_set_locale(session, Article, {"id": 1}, "title", "fr", "Bonjour")

PostgreSQL indexing

[](https://github.com/onlykh/translatable#postgresql-indexing) If you filter often by locale on PostgreSQL, add a GIN index in your migration:

CREATE INDEX ix_articles_title ON articles USING GIN (title);

SQLite and MySQL do not support equivalent JSON indexes; filtering may scan the full table.

Development

[](https://github.com/onlykh/translatable#development)

pip install -e ".[dev]" pytest ruff check src tests

License

[](https://github.com/onlykh/translatable#license) MIT