Migration from i18next
If your app already uses i18next, you can migrate incrementally: install the LionRapid packages alongside i18next, create the LionRapid client, switch the provider and hooks component by component, convert the translation strings, and only then remove i18next. Type generation comes last and is where the payoff lands — keys and ICU parameters become compile-time checked.
Why migrate
Section titled “Why migrate”| Concern | i18next | LionRapid |
|---|---|---|
| Type safety | Manual/partial typings | Types generated from the server |
| ICU MessageFormat | Plugin required | Built in via the formatters plugin |
| Translations live | Static files shipped with app | On the platform, fetched at runtime |
| New/missing keys | Manual extraction workflows | Auto-synced to the platform in batches |
| Caching | Basic | Cache-first chain: memory + storage + network |
| Change awareness | Callbacks/limited events | Typed event bus |
The structural difference to internalize: with i18next your translations are build artifacts; with LionRapid they are server state. Translators publish, your app picks the changes up — no redeploy.
Step 1: install alongside i18next
Section titled “Step 1: install alongside i18next”Keep i18next running; add the LionRapid packages next to it (registry setup in Authentication):
npm install @lionrapid/core @lionrapid/react @lionrapid/formattersnpm install --save-dev @lionrapid/cliStep 2: create the LionRapid instance
Section titled “Step 2: create the LionRapid instance”The i18next init() options map onto the builder configuration:
import { LionRapidBuilder, MemoryRepository, NetworkRepository,} from '@lionrapid/core';import { coreFormattersPlugin } from '@lionrapid/formatters';
export async function createLionRapid() { return LionRapidBuilder.init({ defaultLocale: 'en', // i18next: lng fallbackLocale: 'en', // i18next: fallbackLng namespace: 'common', // i18next: defaultNS }) .use(new MemoryRepository({ enabled: true })) .use( new NetworkRepository({ // i18next: backend.loadPath enabled: true, options: { baseUrl: 'https://your-lionrapid-host', apiKey: process.env.LIONRAPID_INTEGRATION_KEY, preloadNamespaces: [ { locale: 'en', namespace: 'common', eager: true }, ], }, }) ) .use(coreFormattersPlugin) .build();}Where i18next loads JSON files from loadPath, LionRapid reads from the
platform’s integration endpoints (or a
remapped backend
during transition). Namespaces you listed in ns are either preloaded, as
above, or loaded lazily on first use.
Step 3: swap the provider
Section titled “Step 3: swap the provider”// Beforeimport { I18nextProvider } from 'react-i18next';
<I18nextProvider i18n={i18n}> <App /></I18nextProvider>;
// Afterimport { LionRapidProvider } from '@lionrapid/react';
<LionRapidProvider instance={lionRapid}> <App /></LionRapidProvider>;During a gradual migration, nest both providers and move components over one at a time. A thin wrapper hook keeps component diffs small:
// useTranslate.ts — temporary bridge during migrationimport { useTranslation } from 'react-i18next';import { useLionRapid } from '@lionrapid/react';
const USE_LIONRAPID = import.meta.env.VITE_USE_LIONRAPID === 'true';
export function useTranslate(namespace?: string) { // Both hooks return a compatible `t(key, params)` shape. /* eslint-disable react-hooks/rules-of-hooks */ return USE_LIONRAPID ? useLionRapid(namespace) : useTranslation(namespace); /* eslint-enable react-hooks/rules-of-hooks */}Step 4: update hook usage
Section titled “Step 4: update hook usage”The hook shape is deliberately close:
// Beforeimport { useTranslation } from 'react-i18next';
function Inbox() { const { t, i18n } = useTranslation('common'); return ( <div> <p>{t('greeting', { name: 'Alice' })}</p> <button onClick={() => i18n.changeLanguage('es')}>Español</button> </div> );}
// Afterimport { useLionRapid } from '@lionrapid/react';
function Inbox() { const { t, changeLanguage } = useLionRapid('common'); return ( <div> <p>{t('greeting', { name: 'Alice' })}</p> <button onClick={() => changeLanguage('es')}>Español</button> </div> );}API mapping for the pieces that differ:
| i18next | LionRapid |
|---|---|
useTranslation('ns') |
useLionRapid('ns') |
const { t } = useTranslation() |
const { t } = useLionRapid() (same shape) |
const { ready } = … |
const { ready } = useLionRapid('ns') |
i18n.changeLanguage('es') |
changeLanguage('es') from the hook, or useChangeLanguage() |
i18n.language |
locale from the hook, or useLocale() |
i18n.t('key') (outside React) |
lionRapid.t('key') on the core instance |
i18n.exists('key') |
lionRapid.isCached('key') (cache-level) |
i18n.on('languageChanged', …) |
lionRapid.bus.on('language:change:success', …) |
getFixedT / keyPrefix |
useLionRapid('ns', { keyPrefix: '…' }) |
Two conventions carry over unchanged, which does most of the migration for you:
- Namespace syntax is the same:
t('common:greeting'), or bind the namespace in the hook and callt('greeting'). - Nested keys keep their dot notation:
t('user.profile.name')works in both libraries. Do not flatten your key structure.
Step 5: convert translation strings
Section titled “Step 5: convert translation strings”Two mechanical changes in the catalogs, both scriptable.
Interpolation syntax — i18next’s {{name}} becomes ICU’s {name}:
// Before // After{ "greeting": "Hello {{name}}!" } { "greeting": "Hello {name}!" }Plurals — i18next’s suffix keys collapse into one ICU message:
// Before{ "items_one": "{{count}} item", "items_other": "{{count}} items"}
// After{ "items": "{count, plural, one {# item} other {# items}}"}A one-shot conversion script for JSON catalogs:
import fs from 'node:fs';
export function convertCatalog(json) { const out = {};
for (const [key, value] of Object.entries(json)) { if (typeof value === 'object' && value !== null) { out[key] = convertCatalog(value); // keep nesting — dots still work continue; } if (/_(one|other|plural)$/.test(key)) continue; // merged below out[key] = String(value).replace(/\{\{(\w+)\}\}/g, '{$1}'); }
// Merge i18next plural suffix keys into ICU messages. for (const key of Object.keys(json)) { const m = key.match(/^(.*)_(one)$/); if (!m) continue; const base = m[1]; const one = String(json[`${base}_one`]).replace(/\{\{(\w+)\}\}/g, '{$1}'); const other = String( json[`${base}_other`] ?? json[`${base}_plural`] ?? json[`${base}_one`] ).replace(/\{\{(\w+)\}\}/g, '{$1}'); out[base] = `{count, plural, one {${one}} other {${other}}}`; }
return out;}
const file = process.argv[2];const converted = convertCatalog(JSON.parse(fs.readFileSync(file, 'utf8')));fs.writeFileSync(file, JSON.stringify(converted, null, 2));Review the output before importing it into your continuous project —
plural merging in particular deserves human eyes for languages with more
than two forms (ICU supports zero, one, two, few, many, other,
and exact =N matches; translators fill these per language on the
platform).
Step 6: replace Trans components
Section titled “Step 6: replace Trans components”The prop names match, but children are matched to markup differently.
i18next matches by tag name or a components map; LionRapid’s <Trans>
matches children by position to the indexed tags in the translation:
// Before (i18next) — translation: "Welcome, <strong>{{name}}</strong>!"import { Trans } from 'react-i18next';
<Trans i18nKey="welcome" values={{ name: 'Alice' }}> Welcome, <strong>{'{{name}}'}</strong>!</Trans>;
// After (LionRapid) — translation: "Welcome, <1>{name}</1>!"import { Trans } from '@lionrapid/react';
<Trans i18nKey="welcome" values={{ name: 'Alice' }}> <strong /></Trans>;The first child renders <1>…</1>, the second <2>…</2>, and so on. On
the platform side, translations store markup as indexed tags with a styles
map, which is what lets translators reorder styled spans safely — see
the Trans component for the
full behavior, including server-side style fallbacks and defaultValue
with inline HTML.
Step 7: generate types and remove i18next
Section titled “Step 7: generate types and remove i18next”Once components are migrated, generate types from your translation files:
npx lionrapid types generateNow the compiler verifies every key and parameter — the class of bug i18next migrations tend to introduce (renamed keys, changed parameters) becomes a build failure instead of a blank string in production. Wire the same command into CI.
Then remove the old stack:
npm uninstall i18next react-i18next i18next-http-backendand delete the i18next config, the wrapper hook from step 3, and the local
public/locales/ files once their content is imported into the platform.
Migration checklist
Section titled “Migration checklist”- LionRapid packages installed; registry access working in CI
- Client instance created; provider mounted (nested during transition)
- Components migrated hook by hook; wrapper hook removed at the end
- Catalogs converted:
{{var}}→{var}, plural suffixes → ICU - Catalog content imported into the continuous project
-
<Trans>children converted to positional elements - Types generated and imported;
tsc --noEmitclean - Language switching, plurals, and fallbacks tested per locale
- i18next packages and local locale files removed