i18n-keyless for Python
Keyless translations for a Python server, a script or a build step. Write the source string
where a key would go, t("Welcome to our app", lang), and it resolves through the
i18n-keyless API: a missing string is translated by AI once, for every language, and served
from memory from then on. No .po files, no message catalogue, no key to name.
Quick start
pip install i18n-keyless # or: uv add i18n-keyless
import i18n_keyless as i18n
i18n.init(api_key="your-key", primary="en", supported=["en", "fr", "es"]) # once, at start
print(i18n.t("Welcome to our app", "fr")) # "Bienvenue dans notre application"
print(i18n.t("Welcome to our app", "es")) # switch language: pass another code
Done. Run it: the first call POSTs the string to the API and returns the translation; every later call, in any language, is served from memory. Python >= 3.9, no dependency.
Get a key at https://i18n-keyless.com/#get-api-key.
How it works
init()validates the config and loads every language of the default namespace in one request (GET /translate/), like the node SDK. Call it once, at process start. Another namespace is loaded after its first miss.t(key, lang)in the primary language returns the key: no lookup, no request.- In another language, a string the store has is returned at once. A string it does not
have is POSTed to
/translatesynchronously with every language ofsupported; the API answers with the whole row, the answer is cached for every language, and the translation is returned. The page is complete when it is served: no flash of source text, no second render. - Concurrent misses of one string (N request handlers rendering the same page) share one request; at most 30 requests are in flight at once, process-wide.
- When the batch drains, the namespaces that missed are refetched on a daemon thread
(
GET /translate/, withIf-None-Match: an unchanged namespace costs a bodyless 304), so a cell the AI filled later or a dashboard edit reaches the process. - Usage analytics, like the node SDK: the UTC date each string was last served is
recorded on every call and the cumulative map is POSTed at most once every 10 s from a
daemon thread that never keeps the process alive. It feeds the dashboard's "last used"
column so unused strings can be pruned. Call
flush_usage()before a script exits.
Every request has a 10 s timeout and is retried twice with backoff (500 ms, 1500 ms) on a
network error, a timeout, a 429, a 5xx or an unparsable body. Any other 4xx is not
retried. t() never raises: a failed request logs an error and returns the source text.
t_or_raise() is the same call for a script or a build step, where a wrong language is
worse than a crash: it raises TranslationError.
Configuration
i18n.init(
api_key="...", # required (sent as `Authorization: Bearer`)
primary="en", # the language the source strings are written in
supported=["en", "fr", "es"], # every language the app serves; stored by the API as the project's list
api_url=None, # a self-hosted backend or a proxy, no trailing slash
default_namespace=None, # the namespace of every call that passes none
bundle_path=None, # the directory of a precompiled bundle, see below
debug=False, # DEBUG lines on the `i18n_keyless` logger
on_init=None, # called once with the primary language
handle_translate=None, # custom handlers, see below
get_all_translations_for_all_languages=None,
send_translations_usage=None,
)
init() also takes a Config dataclass (i18n.Config(...)). For several projects in one
process, build your own client: client = i18n.I18nKeyless(); client.init(config).
The three network modes, in priority order
- Custom handlers.
handle_translate(key)replacesPOST /translateand receives the key only; it returns{"ok": True, "data": {"translation": {"en": "...", "fr": "..."}}}.get_all_translations_for_all_languages()replacesGET /translate/and returns the all-languages envelope.send_translations_usage(default_bucket)replaces the usage POST and receives thedefaultnamespace's map only. - Self-hosted:
api_url="https://your.server", a backend or a proxy that speaks the wire format (https://docs.i18n-keyless.com/docs/guides/proxy-mode). - The official service,
https://api.i18n-keyless.com.
The precompiled bundle
Export the dictionaries at build time with the MCP export_bundle tool or from
GET /translate/bundle: a directory (i18n-keyless/ by convention) holding manifest.json
and one <namespace>/<lang>.json per dictionary. Point bundle_path at it:
i18n.init(api_key="...", primary="en", supported=["en", "fr", "es"], bundle_path="./i18n-keyless")
init() reads every dictionary the manifest lists into the store and skips the boot fetch of
a namespace the manifest covers, so a process whose bundle holds every rendered string makes
no dictionary request at all. Nothing else changes: a string the bundle does not hold still
misses and POSTs, and the refetch after a batch of misses still runs. A missing or malformed
manifest.json raises BundleError at init(); a missing dictionary file is logged and its
pair fetched as before.
Per-call options
i18n.t("8 hours", "fr", context="duration") # one string, two meanings: stored as "8 hours__duration"
i18n.t("Hello {{name}}", "fr", replace={"{{name}}": "Ada"}) # placeholders, replaced after the translation
i18n.t("Pay", "fr", namespace="checkout") # an i18n-keyless namespace
i18n.t("Hi", "fr", namespace="chat-42", unpersisted_namespace=True) # transient: never reported in usage
i18n.t("Hola mundo", "en", origin_language="es") # user generated content written in Spanish
i18n.t("Hello", "fr", force_temporary={"fr": "Salut"}) # overwrite the stored French cell, permanently
replace: every placeholder is a literal, all are replaced in one pass, and a placeholder
whose replacement is empty is left in place, exactly like the JavaScript SDKs. The
imperative function never trims the source string.
Languages
supported and lang take the 48 codes of the API (i18n.AVAILABLE_LANGS):
ar bn ca zh-Hans zh-Hant hr cs da nl en en-GB fi fr fr-CA de el gu he hi hu id it ja kn ko ms ml mr no or pl pt pt-BR pa ro ru sk sl es es-MX sv ta te th tr uk ur vi. To turn an
Accept-Language value or a user setting into one of them:
i18n.resolve_lang("pt_BR", supported=["pt", "en"], fallback="en") # "pt"
i18n.resolve_lang("zh-TW") # "zh-Hant"
i18n.to_app_store_locale("fr") # "fr-FR"
In a web framework
The port is framework-free: call t() with the request's language. init() runs once at
process start (a module import, an app factory, a ready() hook).
Django: a template tag in yourapp/templatetags/keyless.py, then {% load keyless %}
and {% t "Welcome to our app" %} in a template. The language comes from Django's own
translation.get_language(), so LocaleMiddleware and ?lang= keep working.
from django import template
from django.utils import translation
import i18n_keyless as i18n
register = template.Library()
@register.simple_tag
def t(text, context=None):
lang = i18n.resolve_lang(translation.get_language(), supported=i18n.get_supported_languages(), fallback="en")
return i18n.t(text, lang, context=context)
Flask:
import i18n_keyless as i18n
from flask import Flask, request
app = Flask(__name__)
i18n.init(api_key=..., primary="en", supported=["en", "fr", "es"])
@app.get("/")
def home():
lang = i18n.resolve_lang(request.args.get("lang") or request.accept_languages.best, supported=["en", "fr", "es"], fallback="en")
return f"<h1>{i18n.t('Welcome to our app', lang)}</h1>"
FastAPI: the same, with i18n.init() in the lifespan and t() in a dependency that
resolves the language from the Accept-Language header. t() blocks for the request on a
miss (once per string per process): call it in a threadpool (run_in_threadpool) in an
async handler if a first render must never wait on the network.
Self-hosted backend or proxy
Point api_url at a server that speaks the four-route wire format (GET /translate/,
GET /translate/{lang}, POST /translate, POST /translate/last-used-translations).
Every request carries Content-Type: application/json, Authorization: Bearer <api_key>,
Version: 3.6.1 (the wire dialect: v3 language codes) and sdk: python (a server label,
counted like node: by its connection, never by a device id). Nothing else, no cookie.
Limitations
- Plurals. One source string per form, with a
context(context="one",context="other"): the API translates strings, not ICU messages. - Misses block the call (once per string per process), like
awaitForTranslationin the node SDK. A miss costs one round trip, capped at 3 × 10 s. - One store per process. Workers (gunicorn, uvicorn) each fetch the dictionaries at boot; a dashboard edit reaches a process at its next refetch, after a miss.
force_temporaryin the primary language sends nothing, like the client SDKs (the node SDK sends it). Pass another language.- A source string is capped at 2000 characters (
contextandnamespaceat 200). Long-form content is one translation per Markdown block: https://docs.i18n-keyless.com/docs/guides/long-form-content
Publishing to PyPI
This directory lives inside the i18n-keyless monorepo and is not an npm workspace. The
package builds from here, with the version read from src/i18n_keyless/version.py
(written by scripts/set-version.mjs, shared by every SDK):
cd ports/python
uv build
uv publish # PyPI: https://pypi.org/project/i18n-keyless/
Development
cd ports/python
uv run pytest
Tests run on a scripted transport: no network, no key. tests/test_vectors.py replays
every file of the monorepo's shared protocol vectors (conformance/vectors/*.json):
language codes, locale resolution, storage key, replace, namespace resolution, retry
decisions, backoff scenarios, translate, dictionary and usage requests and responses, the
queue (dedupe and the 30-in-flight cap), the runtime label, the precompiled bundle rules. tests/test_integration.py
drives the client end to end.
Deliberate differences from the JavaScript SDKs: a miss is translated on the spot and
returned (the node rule), not queued and rendered on the next pass (the client rule), so two
contexts of one key in one batch are two requests (the SDK queue makes one); the store has a
namespace dimension (the node store has none); ETags are remembered per (API key, all,
namespace). Usage analytics follow the node SDK
(sdk: python is a server label with the node rules).
Metadata
Release files for i18n-keyless 3.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| i18n_keyless-3.8.0.tar.gz | 28.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| i18n_keyless-3.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.4 kB
Release files / i18n_keyless-3.8.0.tar.gz
| Download URL | i18n_keyless-3.8.0.tar.gz |
|---|---|
| Size | 28.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a28134cfa5f3692c8c59dbbbc07d808c3ad32436298a93251115df622b73ab7a
|
|
BLAKE2b-256 checksum How to use checksums |
810c3080f5cde58f7452b247c39c03f6000893bb48dcf17b4f215139d9ae23b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / i18n_keyless-3.8.0-py3-none-any.whl
| Download URL | i18n_keyless-3.8.0-py3-none-any.whl |
|---|---|
| Size | 27.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17cec0d1f4368dbe0a644c0e399deeec3813ff3e11b9b8f3dab7cbefcafeacb6
|
|
BLAKE2b-256 checksum How to use checksums |
88ac873c963dadf38b1063ab6d0602cc31df9e0435faa0772f848c124d115ce5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|