Skip to content

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.

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.

Keep i18next running; add the LionRapid packages next to it (registry setup in Authentication):

Terminal window
npm install @lionrapid/core @lionrapid/react @lionrapid/formatters
npm install --save-dev @lionrapid/cli

The i18next init() options map onto the builder configuration:

src/i18n/lionrapid.ts
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.

// Before
import { I18nextProvider } from 'react-i18next';
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>;
// After
import { 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 migration
import { 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 */
}

The hook shape is deliberately close:

// Before
import { 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>
);
}
// After
import { 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 call t('greeting').
  • Nested keys keep their dot notation: t('user.profile.name') works in both libraries. Do not flatten your key structure.

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:

migrate-translations.mjs
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).

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.

Once components are migrated, generate types from your translation files:

Terminal window
npx lionrapid types generate

Now 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:

Terminal window
npm uninstall i18next react-i18next i18next-http-backend

and delete the i18next config, the wrapper hook from step 3, and the local public/locales/ files once their content is imported into the platform.

  • 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 --noEmit clean
  • Language switching, plurals, and fallbacks tested per locale
  • i18next packages and local locale files removed