migrate-i18next-to-lingui
Migrate i18next to Lingui
Migration Checklist
- [ ] Step 1: Install Lingui packages
- [ ] Step 2: Create lingui.config.js
- [ ] Step 3: Set up build tooling (Babel/SWC/Vite)
- [ ] Step 4: Replace i18n initialization
- [ ] Step 5: Migrate React components (useTranslation → useLingui, Trans → Trans)
- [ ] Step 6: Migrate JS/TS strings (t() → t``)
- [ ] Step 7: Migrate plurals
- [ ] Step 8: Migrate namespaces
- [ ] Step 9: Convert existing translation catalogs
- [ ] Step 10: Run lingui extract && lingui compile
- [ ] Step 11: Remove i18next packages
Step 1: Install Lingui
# Core (always required)
npm install @lingui/core @lingui/react
# CLI (dev)
npm install --save-dev @lingui/cli
# Macro support - pick one based on build tool:
# Babel
npm install --save-dev babel-plugin-macros
# SWC
npm install --save-dev @lingui/swc-plugin
# Vite
npm install --save-dev @lingui/vite-plugin
Step 2: Create lingui.config.js
import { defineConfig } from "@lingui/cli";
export default defineConfig({
sourceLocale: "en",
locales: ["en", "de", "fr"], // match your existing locales
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
},
],
});
Step 3: Configure Build Tooling
Vite (vite.config.ts):
import { lingui } from "@lingui/vite-plugin";
import react from "@vitejs/plugin-react";
export default { plugins: [react(), lingui()] };
Babel (.babelrc or babel.config.js):
{ "plugins": ["macros"] }
Next.js with SWC (next.config.js):
module.exports = {
experimental: {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
};
Step 4: Replace i18n Initialization
Before (i18next):
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
i18next.use(initReactI18next).init({
lng: "en",
resources: { en: { translation: { key: "Hello world" } } },
});
After (Lingui):
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { messages } from "./locales/en/messages";
i18n.load("en", messages);
i18n.activate("en");
function App() {
return <I18nProvider i18n={i18n}>{/* app */}</I18nProvider>;
}
Dynamic locale switching:
async function changeLocale(locale) {
const { messages } = await import(`./locales/${locale}/messages`);
i18n.load(locale, messages);
i18n.activate(locale);
}
Step 5: Migrate React Components
useTranslation → useLingui
Before:
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation();
return <p>{t("greeting")}</p>;
}
After:
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
return <p>{t`Hello World`}</p>;
}
Trans component
Before:
import { Trans } from "react-i18next";
<Trans i18nKey="welcome">Hello World!</Trans>
After:
import { Trans } from "@lingui/react/macro";
<Trans>Hello World!</Trans>
For explicit IDs (when preserving i18next keys):
<Trans id="welcome">Hello World!</Trans>
Trans with interpolation
Before:
<Trans i18nKey="greeting" values={{ name }}>Hello {{ name }}!</Trans>
After:
<Trans>Hello {name}!</Trans>
Step 6: Migrate JS/TS Strings
Simple strings
| i18next | Lingui |
|---|---|
t('key') |
t\Message text`` |
t('key', { name }) |
t\Hello ${name}`` |
t('key', { defaultValue: 'Hi' }) |
t\Hi`` |
Before:
import i18next from "i18next";
const msg = i18next.t("greeting", { name: "Tom" });
After:
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const msg = t`Hello ${name}`;
}
In vanilla JS (outside components):
import { t } from "@lingui/core/macro";
const msg = t`Hello ${name}`;
Preserving explicit keys from i18next
If you want to keep the i18next message IDs:
import { t } from "@lingui/core/macro";
// i18next: t('navigation.home')
const msg = t({ id: "navigation.home", message: "Home" });
Module-level / lazy strings
Before (i18next):
const LABELS = {
active: "Active",
inactive: "Inactive",
};
// translated at render time
After (Lingui):
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
const LABELS = {
active: msg`Active`,
inactive: msg`Inactive`,
};
function StatusDisplay({ status }) {
const { _ } = useLingui();
return <div>{_(LABELS[status])}</div>;
}
Step 7: Migrate Plurals
i18next uses separate keys per plural form. Lingui uses ICU MessageFormat in a single message.
Before (i18next):
{
"item_one": "{{count}} item",
"item_other": "{{count}} items"
}
t("item", { count });
After (Lingui) - JSX:
import { Plural } from "@lingui/react/macro";
<Plural value={count} one="# item" other="# items" />
After (Lingui) - JS strings:
import { plural } from "@lingui/core/macro";
const msg = plural(count, {
one: "# item",
other: "# items",
});
Exact matches (i18next _0):
<Plural
value={count}
_0="No items"
one="# item"
other="# items"
/>
Step 8: Migrate Namespaces
i18next namespaces (useTranslation('common')) map to Lingui catalog paths. Two approaches:
Option A - Single catalog (simplest):
Remove namespaces and use one unified catalog. Update lingui.config.js to include all source directories.
Option B - Multiple catalogs (preserves namespace separation):
// lingui.config.js
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/common",
include: ["src/components/common"],
},
{
path: "<rootDir>/src/locales/{locale}/auth",
include: ["src/components/auth"],
},
]
With multiple catalogs, load them all at startup:
import { messages as commonMessages } from "./locales/en/common";
import { messages as authMessages } from "./locales/en/auth";
i18n.load("en", { ...commonMessages, ...authMessages });
i18n.activate("en");
Step 9: Convert Existing Translation Catalogs
See catalog-conversion.md for and patterns.
Key concept: i18next uses JSON with dotted keys; Lingui uses .po files with the message as the ID (or an explicit ID you provide).
Step 10: Build & Verify
npx lingui extract # extracts all messages → .po files
npx lingui compile # compiles .po → .js message catalogs
Add to package.json:
{
"scripts": {
"i18n:extract": "lingui extract",
"i18n:compile": "lingui compile"
}
}
For TypeScript:
npx lingui compile --typescript
Step 11: Remove i18next
npm uninstall i18next react-i18next
Common Patterns Reference
Date/Number Formatting
Before (i18next):
t("intlDateTime", { val: new Date() });
After (Lingui):
import { useLingui } from "@lingui/react/macro";
function Component() {
const { i18n } = useLingui();
return <span>{new Intl.DateTimeFormat(i18n.locale).format(date)}</span>;
}
Context (disambiguation)
Before (i18next):
t("right", { context: "direction" });
After (Lingui):
<Trans context="direction">right</Trans>
// or
t({ message: "right", context: "direction" });
Gender / Select
Before (i18next):
{ "pronoun_male": "He", "pronoun_female": "She", "pronoun_other": "They" }
After (Lingui):
import { select } from "@lingui/core/macro";
const pronoun = select(gender, {
male: "He",
female: "She",
other: "They",
});
Pitfalls
- **Don't call
t\...`at module level.** Usemsg`...`instead and translate with_(descriptor)` at render time. - After locale change, call both
i18n.load(locale, messages)andi18n.activate(locale). - Wrap the entire app in
<I18nProvider i18n={i18n}>before any component usesuseLinguiorTrans. - Always run
lingui compilebefore building for production; the app imports compiled.jscatalogs, not.pofiles. - Generated IDs change if message text changes. If stability matters, use explicit
idprops.
Additional Resources
More from lingui/skills
lingui-best-practices
Implement internationalization with Lingui in React and JavaScript applications. Use when adding i18n, translating UI, working with Trans/useLingui/Plural, extracting messages, compiling catalogs, or when the user mentions Lingui, internationalization, i18n, translations, locales, message extraction, ICU MessageFormat, or working with .po files.
148enhanced-message-context
Provide additional context for messages based on the codebase and the context of the message to improve the quality of the translations.
96swc-plugin-compatibility
Diagnose and fix Lingui SWC plugin compatibility errors with Next.js, Rspack, or other SWC runtimes. Use when seeing errors like "failed to invoke plugin", "failed to run Wasm plugin transform", "out of bounds memory access", or "LayoutError" during builds with @lingui/swc-plugin.
47