How to add i18n to a React app in 2026
If you searched for "how to add i18n to a React app", most results will walk you through react-i18next — install, configure, set up a key naming convention, write locale JSON files, wire up a language switcher, and you're translated. Two days of work for an MVP-stage app.
This guide shows the modern alternative: i18n-keyless. You'll go from zero to multilingual in 5 minutes. We'll cover the setup, the core API, language switching, edge cases, and how to handle interpolation.
TL;DR
npm install i18n-keyless-react
import * as I18nKeyless from "i18n-keyless-react";
I18nKeyless.init({
API_KEY: "YOUR_API_KEY",
storage: window.localStorage,
languages: { primary: "en", supported: ["en", "fr", "de", "es"] },
});
// Anywhere in your JSX:
<h1><I18nKeyless>Welcome to Acme</I18nKeyless></h1>
That's it. Source string is the key. AI translates on first encounter. Cached forever. See the quick-setup guide for the full reference.
Step 1: Get an API key
Visit i18n-keyless.com and sign up. You'll get an API key.
If you want to skip signup and try it first, the literal string demo-i18nkeyless works for sandboxed testing on common dev origins.
Step 2: Install the React SDK
npm install i18n-keyless-react
That's the only dependency. No babel plugin, no vite plugin, no message extractor. The SDK is a thin React wrapper over a fetch-and-cache layer.
Step 3: Initialize
In your app entry — typically main.tsx (Vite), index.tsx (Create React App), or top of your routing config:
// main.tsx
import * as I18nKeyless from "i18n-keyless-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
I18nKeyless.init({
API_KEY: "YOUR_API_KEY",
storage: window.localStorage,
languages: {
primary: "en", // The language you write strings in
supported: ["en", "fr", "de", "es"], // What you want to translate to
},
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
primary is the language your code is written in (the source language for AI translation). supported is the set of locales you want to support.
Storage options
The SDK caches translations locally. You provide the storage adapter. Three common options:
// Option 1: window.localStorage (simplest)
storage: window.localStorage
// Option 2: IndexedDB via idb-keyval (better for large translation sets)
import { get, set, del } from "idb-keyval";
storage: {
getItem: async (k) => (await get(k)) ?? null,
setItem: async (k, v) => { await set(k, v); },
removeItem: async (k) => { await del(k); },
}
// Option 3: in-memory (testing or short-lived sessions)
const cache = new Map();
storage: {
getItem: (k) => cache.get(k) ?? null,
setItem: (k, v) => { cache.set(k, v); },
removeItem: (k) => { cache.delete(k); },
}
Use localStorage for most apps. Use IndexedDB if you have hundreds of translated strings and want better performance / quota.
Step 4: Translate strings
Two patterns: the <I18nKeyless> component (for JSX) and the getTranslation function (for non-JSX contexts).
<I18nKeyless> component
import { I18nKeyless } from "i18n-keyless-react";
function Hero() {
return (
<section>
<h1><I18nKeyless>Welcome to Acme</I18nKeyless></h1>
<p><I18nKeyless>The fastest way to ship multilingual apps.</I18nKeyless></p>
<button><I18nKeyless>Get started</I18nKeyless></button>
</section>
);
}
When the user's locale is en (the primary language), the components render the source string verbatim. When it's fr, the SDK looks up the French translation, falls back to English while waiting, and renders.
getTranslation function
For places where you can't use a component — aria-label, placeholder, title, third-party component props, error messages thrown from a function:
import { getTranslation } from "i18n-keyless-react";
function Search() {
return (
<input
type="search"
placeholder={getTranslation("Search products...")}
aria-label={getTranslation("Search input")}
/>
);
}
getTranslation is the function-call equivalent of <I18nKeyless>. Same source-string-as-key model.
Step 5: Build a language switcher
The SDK exposes a hook for current language and setter:
import { useI18nKeyless } from "i18n-keyless-react";
function LanguageSwitcher() {
const { currentLanguage, setLanguage, supportedLanguages } = useI18nKeyless();
return (
<select
value={currentLanguage}
onChange={(e) => setLanguage(e.target.value)}
>
{supportedLanguages.map((lang) => (
<option key={lang} value={lang}>
{lang.toUpperCase()}
</option>
))}
</select>
);
}
Drop it in your navbar. setLanguage("fr") triggers a re-render with the new locale; the SDK fetches missing translations on demand.
For language-name labels (e.g., "Français" instead of "FR"), use Intl.DisplayNames:
const displayNames = new Intl.DisplayNames([currentLanguage], { type: "language" });
displayNames.of("fr"); // → "Français" or "French" depending on currentLanguage
Step 6: Handle interpolation
Need to substitute a runtime value into a translated string? Use the replace prop:
import { I18nKeylessText } from "i18n-keyless-react";
function Welcome({ user }) {
return (
<h1>
<I18nKeylessText replace={{ '{name}': user.name }}>
{`Welcome, {name}!`}
</I18nKeylessText>
</h1>
);
}
The {name} placeholder in the source string gets replaced with user.name at render time, in any locale. The placeholder syntax is preserved through translation — French renders as "Bienvenue, {name} !" with {name} substituted at render.
Two things to notice:
- Use
<I18nKeylessText>(not<I18nKeyless>) when you needreplace. Plain<I18nKeyless>is for static strings. - The keys in
replaceinclude the literal placeholder including the braces ('{name}'), not the bare variable name.
For multiple values:
<I18nKeylessText replace={{ '{first}': "Alice", '{count}': items.length }}>
{`{first} has {count} new messages`}
</I18nKeylessText>
See the value replacement guide for more — the placeholder syntax is configurable ({name}, [name], %name%, <name>, etc.).
Step 7: Handle pluralization
For simple binary plurals (singular vs plural), branch in JSX:
{count === 0 && <I18nKeyless>No items in cart</I18nKeyless>}
{count === 1 && <I18nKeyless>1 item in cart</I18nKeyless>}
{count > 1 && (
<I18nKeylessText replace={{ '{count}': count }}>
{`{count} items in cart`}
</I18nKeylessText>
)}
For locale-aware pluralization (Polish, Arabic, Russian have multiple plural forms), use Intl.PluralRules:
function ItemCount({ count, locale }) {
const rules = new Intl.PluralRules(locale);
const form = rules.select(count); // 'zero' | 'one' | 'few' | 'many' | 'other'
const messages = {
zero: "No items",
one: "1 item",
other: `{count} items`,
};
return (
<I18nKeylessText replace={{ '{count}': count }}>
{messages[form] ?? messages.other}
</I18nKeylessText>
);
}
If your app needs full ICU MessageFormat for many strings, see the comparison with react-intl — react-intl is more expressive for ICU-heavy use cases.
Step 8: Detect the user's preferred language on first visit
The SDK doesn't auto-detect language by default — you set the language explicitly. To pick a sensible default on first visit:
function detectInitialLanguage(): string {
const stored = localStorage.getItem("preferred-lang");
if (stored) return stored;
const supported = ["en", "fr", "de", "es"];
const browserLang = navigator.language.split("-")[0];
return supported.includes(browserLang) ? browserLang : "en";
}
I18nKeyless.init({
API_KEY: "YOUR_API_KEY",
storage: window.localStorage,
languages: {
primary: "en",
supported: ["en", "fr", "de", "es"],
initial: detectInitialLanguage(),
},
});
After init, your language switcher writes to localStorage.preferred-lang so subsequent visits remember the choice.
Step 9: Handle SSR / SSG (Vite, Next.js)
This guide is for client-side React apps (Vite, CRA). For Next.js with App Router or Pages Router, use i18n-keyless-node on the server side. See the dedicated how to add i18n to a Next.js app guide.
Common patterns
Translating Markdown content
If you have user-facing Markdown that needs translation:
import { I18nKeyless } from "i18n-keyless-react";
import ReactMarkdown from "react-markdown";
function Article({ content }: { content: string }) {
return (
<I18nKeyless renderer={(text) => <ReactMarkdown>{text}</ReactMarkdown>}>
{content}
</I18nKeyless>
);
}
Translating dynamic content from an API
Server-controlled strings (e.g., notification text) shouldn't be wrapped in <I18nKeyless> because the source isn't known at build time. Either:
- Translate them server-side using
i18n-keyless-nodebefore sending to the client. - Or render the source string and let the client translate at runtime:
<I18nKeyless>{notification.text}</I18nKeyless>. Works, but sends new strings to your translation backend on demand.
Disambiguating same source, different meaning
"Open" can mean "open the menu" (verb) or "open status" (adjective). Translations differ. Use the context feature:
<I18nKeyless context="verb-action"><I18nKeyless>Open</I18nKeyless></I18nKeyless>
<I18nKeyless context="adjective-status"><I18nKeyless>Open</I18nKeyless></I18nKeyless>
See the context guide.
Comparison: traditional vs keyless
If you've used react-i18next, here's the equivalent setup:
// react-i18next: ~80 lines of config, locale files, providers
// 1. Install: i18next, react-i18next, i18next-browser-languagedetector
// 2. Create i18n.ts with init config
// 3. Create public/locales/en/common.json, fr/common.json, etc.
// 4. Wrap app with <I18nextProvider>
// 5. In components: const { t } = useTranslation(); t("hero.title")
// i18n-keyless: ~3 lines + JSX
I18nKeyless.init({ API_KEY, storage, languages: { primary, supported } });
<I18nKeyless>Welcome to Acme</I18nKeyless>
If you have an existing react-i18next setup and want to migrate, see migrate from i18next.
When to use a different approach
Be honest: keyless isn't the right tool for every team. Use a different approach if:
- ❌ You have human translators in your workflow → use a TMS (Lokalise, Crowdin) on top of i18next/react-intl.
- ❌ Your app is ICU-heavy with complex plural rules → use react-intl.
- ❌ You need offline-first with no backend dependency → use i18next with bundled locale files.
- ❌ Compliance requires translations in your repo → use i18next.
For everyone else — startup, indie SaaS, MVP — keyless is faster.
FAQ
Does this work with Vite?
Yes. The SDK is framework-agnostic React. Works with Vite, CRA, Parcel, esbuild, etc.
Does it support React 19?
Yes. The SDK uses standard React patterns (hooks, components) and is compatible with React 18 and 19.
What about React Native?
Same SDK package — i18n-keyless-react works for both React and React Native. See the dedicated React Native guide.
How do I test components that use <I18nKeyless>?
In your test setup, initialize with an in-memory storage and the same primary language. Components render the source string verbatim when no translation is available, so most tests just assert on the source text.
What if the translation backend is unreachable?
Cached translations keep serving. New strings render in the source language. No errors thrown. Graceful degradation.
Next steps
- Get an API key: i18n-keyless.com.
- Quick setup: Detailed setup guide.
- Compare options: Best i18next alternatives.
- Other frameworks: Next.js · React Native · Node.js.
- Read the philosophy: The complete guide to keyless i18n.