# i18n-keyless > Ultimate DX for i18n. A keyless internationalization SaaS: write strings in your source language, get AI-powered translations at runtime — no JSON files, no translation keys, no manual upkeep. i18n-keyless lets you ship multi-language apps without managing translation keys or files. You write text in your primary language (French or English), wrap it in a component or call, and the SDK fetches AI-generated translations on the fly, caches them locally, and lets you override them from a dashboard or directly in code. Official SDKs: React, React Native (`i18n-keyless-react`), and Node.js (`i18n-keyless-node`). This file is a single-page Markdown summary of the documentation, intended to be pasted into an LLM context window so the assistant has full coverage of the API surface and conventions. ## Core concepts - **Keyless**: the source string itself is the translation key — no IDs to manage. - **Primary language**: `fr` or `en` only. This is the language you write your code in. - **Supported target languages**: `fr`, `en`, `nl`, `it`, `de`, `es`, `pl`, `pt`, `ro`, `hu`, `sv`, `tr`, `ja`, `cn`, `cz`, `ru`, `ko`, `ar`. Also exported as the runtime const `AVAILABLE_LANGS` from `i18n-keyless-react` and `i18n-keyless-node` (see Type reference below). - **Storage** (React / React Native only): required, used to cache translations locally. Must implement `getItem`, `setItem`, `removeItem` (sync or async). - **AI translations**: on cache miss, the server generates a translation with Mistral, stores it server-side and pushes it to the client cache. - **Overrides**: edit a translation in the dashboard at https://i18n-keyless.com/dashboard, or use `forceTemporary` from code. - **API key**: get one at https://i18n-keyless.com/#get-api-key. ## Installation ### React (web) ```bash npm install i18n-keyless-react ``` Initialize once in `main.tsx` / `index.tsx` before rendering the app: ```ts import * as I18nKeyless from "i18n-keyless-react"; I18nKeyless.init({ API_KEY: "YOUR_API_KEY", storage: window.localStorage, // or any get/set/del-compatible adapter languages: { primary: "fr", // "fr" | "en" supported: ["en", "fr"], fallback: "en", // optional initWithDefault: "fr", // optional }, }); ``` For IndexedDB, wrap `idb-keyval` in a `{ getItem, setItem, removeItem }` adapter. ### React Native ```bash npx expo install i18n-keyless-react react-native-mmkv npx expo prebuild ``` ```ts import * as I18nKeyless from "i18n-keyless-react"; import { MMKV } from "react-native-mmkv"; I18nKeyless.init({ API_KEY: "YOUR_API_KEY", storage: new MMKV(), languages: { primary: "fr", supported: ["en", "fr"] }, }); ``` `AsyncStorage` (`@react-native-async-storage/async-storage`) is also supported. ### Node.js ```bash npm install i18n-keyless-node ``` ```ts import * as I18nKeyless from "i18n-keyless-node"; I18nKeyless.init({ API_KEY: "YOUR_API_KEY", languages: { primary: "fr", supported: ["en", "fr"] }, }); ``` No storage parameter on Node. ## React / React Native API (`i18n-keyless-react`) ### `I18nKeylessText` — translation component Use it inline to wrap any text. The text content is also the translation key. ```tsx import { I18nKeylessText } from "i18n-keyless-react";

Bonjour le monde

``` Props: - `children: ReactNode` — the source text. - `context?: string` — disambiguation hint (e.g. `"This is a back button"`). - `replace?: Record` — placeholder substitution (e.g. `{ "{name}": user.name }`). - `forceTemporary?: Record` — override per language, e.g. `{ en: "Back" }`. - `debug?: boolean` — log internals for this call. ### `getTranslation(text, options?)` — string-returning hook Use when you need a translated string in a prop (placeholder, `aria-label`, tab label, etc.) instead of as JSX. ```tsx import { getTranslation } from "i18n-keyless-react"; ``` Same options as `I18nKeylessText`. Returns `string`. Must be called inside a React component (it's a hook). ### `useCurrentLanguage()` — current language hook Returns `Lang | null` (null until set). ```ts const current = useCurrentLanguage(); // "fr" | "en" | ... | null ``` ### `setCurrentLanguage(lang)` — switch language ```ts import { setCurrentLanguage } from "i18n-keyless-react"; setCurrentLanguage("es"); ``` Triggers re-render of all consumers. Can be called from anywhere (not a hook). ### `getSupportedLanguages()` — configured languages Returns the `Lang[]` you passed to `init`. Useful for building a language selector. ### `useI18nKeyless(selector)` — Zustand store accessor Low-level access to the store. Common selectors: ```ts const primary = useI18nKeyless((s) => s.config.languages.primary); const supported = useI18nKeyless((s) => s.config.languages.supported); const lastRefresh = useI18nKeyless((s) => s.lastRefresh); // for forcing re-renders ``` ## Node.js API (`i18n-keyless-node`) ### `awaitForTranslation(text, language, options?)` Returns a `Promise`. **Must be awaited** — calling without `await` will trigger 429 rate limit errors. ```ts import { awaitForTranslation, type Lang } from "i18n-keyless-node"; const title = await awaitForTranslation("Viens voir l'application", user.lang as Lang); const body = await awaitForTranslation( "Bonjour {name} !", user.lang as Lang, { replace: { "{name}": user.name }, context: "push notification" } ); ``` Options: - `context?: string` - `replace?: Record` - `forceTemporary?: string` — note: a single string here, not a record (target language is already known). - `debug?: boolean` ### `getSupportedLanguages()` Same as in the React SDK. ## Common patterns ### Disambiguate ambiguous words ```tsx Retour Proche ``` ### Replace dynamic values Any pattern can be a placeholder: `{name}`, `{{name}}`, `[name]`, `%name%`, ``, plain `name`, `{user_name}`, etc. ```tsx Hello {name} ! ``` ### Force a translation from code When the AI gets it wrong and you can't (or won't) fix it from the dashboard: ```tsx Retour ``` ### Build a language selector ```tsx import { useCurrentLanguage, setCurrentLanguage, useI18nKeyless } from "i18n-keyless-react"; const current = useCurrentLanguage(); const primary = useI18nKeyless((s) => s.config.languages.primary); ``` ### Translate Markdown `i18n-keyless` only translates strings, so use Markdown for inline styling. With `react-markdown`, force re-render with `lastRefresh`: ```tsx import { useI18nKeyless, getTranslation } from "i18n-keyless-react"; import ReactMarkdown from "react-markdown"; const lastRefresh = useI18nKeyless((s) => s.lastRefresh); {getTranslation(children)} ``` ### Wrap in a `MyText` component Recommended in React Native (for fonts/styles) and useful in React too. Forward `context`, `replace`, `forceTemporary`, `debug` through an `i18nProps` prop, and add a `skipTranslation` escape hatch for already-translated content. ### Debug a single call ```tsx Hello world getTranslation("Hello world", { debug: true }); await awaitForTranslation("Hello world", "fr", { debug: true }); ``` ## Server-Side Rendering (SSR) Works under SSR (TanStack Start, Next, Remix, Astro, Expo Router server output, and any Node/modern-edge runtime) since `i18n-keyless-react >= 2.0.0` (TanStack Start / Vite SSR needs `>= 2.3.2`). v2 is a **drop-in for SPAs** — no code changes. The server is **read-only**: usage analytics are never sent from the server, so SSR adds no API traffic (on a long-lived server it's reported once per process boot, not once per user — fewer calls than a SPA). Translate-on-miss is unchanged: a key with no translation yet is still requested once. `storage` is **optional on the server** (defaults to an in-memory store, also exported as `createMemoryStorage()`) and still required in the browser. Pass `ssr: true` to `init` to force read-only mode explicitly (useful on serverless cold starts). Runtime: Node ≥ 20.10 or a modern bundler (Vite, esbuild, webpack 5, Rollup 3+). `runWithI18nKeyless` needs `AsyncLocalStorage` (Node and most edge runtimes; Cloudflare Workers needs a flag — where unavailable, use `` for `` and the scope degrades to a no-op). The SSR additions are inert in the browser: `node:async_hooks` is loaded via a guarded dynamic import, so it never enters browser/React Native bundles and the browser path is a no-op. There are two modes. ### The two translation paths (the part most people get wrong) Two paths resolve the language through two different mechanisms: - **Component path**: `` / `` → `` (React context). Reads context first, then the ALS, then the store. - **Function path**: imperative `getTranslation(key)` → the ALS scope set by `runWithI18nKeyless`. `getTranslation` is a plain function and **cannot read React context**, so it depends on the ALS. How much wiring you need depends on whether the component tree renders **inside** or **outside** the ALS scope — which is framework-specific: - **Remix / React Router 7** — `entry.server` renders the tree *inside* `runWithI18nKeyless`, so on the server both paths work from the ALS alone — **no Provider needed**. For the client, serialize a snapshot (`getUsedTranslationsSnapshot()` in a ``; }); // client — seed synchronously, then init, then hydrate import { hydrateFromServer, init } from "i18n-keyless-react"; const el = document.getElementById("i18n-keyless"); if (el) hydrateFromServer(JSON.parse(el.textContent)); // { lang, translations } — BEFORE first render init({ languages: { primary: "fr", supported: ["fr", "en"] }, API_KEY, storage: window.localStorage }); hydrateRoot(document, ); ``` `init`'s async hydrate treats the seed as authoritative (won't reset to primary on a cold cache). `hydrateFromServer` is SSR-only and opt-in — SPA/non-SSR apps never call it. Separately (`>= 2.2.0`), `getTranslation` defers its usage-analytics write to a `queueMicrotask`, so calling it during render no longer triggers React's "Cannot update a component while rendering a different component" warning; the server records nothing. **Large translation sets (`>= 2.3.0`).** By default the snapshot embeds the full language set in every page's HTML — fine for small/medium sets (it doubles as a warm cache for instant client navigation). For thousands of keys, serialize only the keys the page rendered: swap `getRequestScope()` for `getUsedTranslationsSnapshot()` at the serialization site. During render, each `getTranslation(...)` / `` adds its key to a per-request `Set` (no store write, no re-render); the snapshot returns just those keys (intersected with available). The full set stays in scope so any key still resolves mid-render — only the serialized payload narrows; concurrent requests are isolated. Keep calling `init()` on the client: its background fetch loads the full set so post-hydration navigation has every key (until then, misses resolve via translate-on-miss). Measure first: `JSON.stringify(getRequestScope().translations).length`. **Not compatible with TanStack Start** (or any setup whose body renders outside the ALS / uses code-split routes): keys aren't recorded, so the subset misses body keys → mismatch. Use the full map there. ### SSR exports - `getServerTranslations(lang) => Promise` — fetch a language's translations on the server, cached per process. - `clearServerTranslationsCache(lang?) => void` — evict one/all languages from that cache. - `runWithI18nKeyless(scope, fn) => Promise` — run a server render with `{ lang, translations }` active so `getTranslation` and `` resolve in `lang`. - `getRequestScope() => { lang, translations } | undefined` — read the active request scope (advanced) — serialize for the client (full set). - `getUsedTranslationsSnapshot() => { lang, translations } | undefined` (`>= 2.3.0`) — like `getRequestScope` but `translations` is narrowed to only the keys this render touched — for large sets. **Render-inside-ALS only** — incompatible with TanStack Start (and any setup whose body renders outside the ALS or uses code-split routes): keys aren't recorded, so the subset misses body keys → hydration mismatch; serialize the full map there. `undefined` outside a `runWithI18nKeyless` render. - `I18nKeylessProvider({ lang, translations, children })` — per-request context for ``; seeds the store on the client for flash-free hydration of the component form. - `hydrateFromServer({ lang, translations }) => void` (`>= 2.2.0`) — client-only; seed the store *synchronously* before `hydrateRoot` so the `getTranslation` function form is correct on first render. No-op without `lang`. - `createMemoryStorage() => Storage` — in-memory storage adapter (server default). - `init({ …, ssr? })` — `storage` optional on the server; `ssr: true` forces read-only. ### Migrating from an SSR workaround - Remove `ssr: { noExternal: ['i18n-keyless-*'] }` from your bundler config — v2's build is valid native Node ESM and externalizes cleanly. - Remove any no-op server storage — omit `storage` on the server; it defaults to in-memory. - To render non-primary languages, adopt the localized-SSR flow above (`getServerTranslations` + `runWithI18nKeyless` + ``). ### Framework recipes Copy-paste integration guides per meta-framework live at: - TanStack Start (**requires `>= 2.3.2`**) — https://i18n-keyless.com/docs/ssr/tanstack-start (in `src/server.ts`: `createStartHandler(defaultStreamHandler)` + wrap the WHOLE `fetch` in `runWithI18nKeyless`, export via `createServerEntry({ fetch })` — not just `defaultStreamHandler`; `` from the root-route loader; call `getTranslation` only in loaders/`head()`, never in a component body; client just `initI18nClient()` — no `hydrateFromServer`; `?lang=` is the single source of truth; serialize the full map, not `getUsedTranslationsSnapshot()`). - Remix / React Router 7 framework mode — https://i18n-keyless.com/docs/ssr/remix (wrap `renderToPipeableStream` in `runWithI18nKeyless` in `app/entry.server.tsx`; tree renders inside the ALS so both paths resolve from it — **no Provider**; serialize via a tiny `` (uses `getUsedTranslationsSnapshot()`) rendered after the app in `root.tsx`; `entry.client.tsx` calls `hydrateFromServer(snapshot)` before `hydrateRoot`, then `initI18nClient()`). - Next.js App Router — https://i18n-keyless.com/docs/ssr/nextjs (no render hook to wrap, so no `runWithI18nKeyless`: `app/[lang]/layout.tsx` (server) fetches `getServerTranslations` → a `"use client"` `Providers` boundary rendering `` and calling `hydrateFromServer` + `initI18nClient` in an effect; imperative `getTranslation` renders the primary language until that effect — prefer ``). - Astro (React islands) — https://i18n-keyless.com/docs/ssr/astro (`.astro` page fetches `getServerTranslations`, passes `lang` + `translations` as island props; island renders `` and runs `hydrateFromServer` + `initI18nClient` in an effect; prefer `` over imperative `getTranslation` for zero flash). ### Example apps Runnable examples (one per framework), each with a passing test suite, running offline against a bundled mock backend (no API key needed): https://github.com/arnaudambro/i18n-keyless/tree/main/examples — docs at https://i18n-keyless.com/docs/examples. Covers `vite-react` (SPA), `tanstack-start` (SSR+SPA, needs `>= 2.3.2`), `remix-rr7` (SSR), `nextjs` (App Router SSR), `astro` (SSR islands), `node` (`i18n-keyless-node` + `awaitForTranslation`), `react-native` (MMKV / AsyncStorage), and `expo`. Run: `cd examples/ && cp .env.example .env && npm install && npm run dev` (leave the API key empty + run `examples/_mock-server` to try offline). ### Changelog - `2.3.2` — fix SSR request scope under TanStack Start / Vite: the per-request AsyncLocalStorage is now pinned to a single `globalThis` slot shared across V8 realms / module graphs (was duplicated, so `?lang=en` rendered the primary language with a hydration mismatch). SPA, Next.js, Remix, Astro, and Node were unaffected. No API change. - `2.3.0` — `getUsedTranslationsSnapshot()` for per-page key subsets on large translation sets. - `2.2.0` — `hydrateFromServer()` synchronous client seed for the `getTranslation` function form; `getTranslation` defers usage recording to a microtask (no setState-during-render warning). - `2.1.0` — AsyncLocalStorage request scope (`runWithI18nKeyless`, `getRequestScope`). - `2.0.0` — SSR support (`getServerTranslations`, ``, read-only server, optional server `storage`). ## Type reference ```ts // Importable runtime const — exported from i18n-keyless-react and i18n-keyless-node // (re-exported from i18n-keyless-core). Use it to support every available language // without hardcoding the list, or to build language selectors / validation. const AVAILABLE_LANGS = [ "fr", "en", "nl", "it", "de", "es", "pl", "pt", "ro", "hu", "sv", "tr", "ja", "cn", "cz", "ru", "ko", "ar", ] as const; type Lang = (typeof AVAILABLE_LANGS)[number]; // "fr" | "en" | "nl" | "it" | "de" | "es" | "pl" | "pt" | "ro" // | "hu" | "sv" | "tr" | "ja" | "cn" | "cz" | "ru" | "ko" | "ar" type PrimaryLang = "fr" | "en"; type LanguagesConfig = { primary: PrimaryLang; supported: Lang[]; fallback?: Lang; initWithDefault?: Lang; }; type TranslationOptions = { context?: string; debug?: boolean; forceTemporary?: Partial>; // Node: string replace?: Record; }; ``` ### Using `AVAILABLE_LANGS` ```ts import { AVAILABLE_LANGS } from "i18n-keyless-react"; // or "i18n-keyless-node" // Support every available language without hardcoding the list I18nKeyless.init({ API_KEY: "YOUR_API_KEY", storage: window.localStorage, languages: { primary: "fr", supported: [...AVAILABLE_LANGS], }, }); // Validate a user-provided language code at runtime const isLang = (l: string): l is Lang => (AVAILABLE_LANGS as readonly string[]).includes(l); ``` ## Gotchas - `awaitForTranslation` **must** be awaited. Fire-and-forget calls will hit rate limits. - `init` must run before any translation call; in React, do it in your entry file before rendering. - Storage adapter methods can be sync or async, but must all be present (`getItem`, `setItem`, `removeItem`). - The `primary` language can only be `fr` or `en`. Source strings must be written in that language. - Translations are cached on-device — if you change a translation in the dashboard, cached clients pick it up via the next refresh, not instantly. - For `react-markdown` (and similar memoized renderers), pass `key={lastRefresh}` so the tree re-renders when translations refresh. ## Resources - Website: https://i18n-keyless.com - Docs: https://i18n-keyless.com/docs/quick-setup - SSR guide: https://i18n-keyless.com/docs/ssr - Dashboard: https://i18n-keyless.com/dashboard - Get an API key: https://i18n-keyless.com/#get-api-key - GitHub: https://github.com/arnaudambro/i18n-keyless