关于 migrate-i18next-to-lingui
migrate-i18next-to-lingui 是帮助开发者将 React 项目国际化实现从 i18next 迁移到 Lingui 的技能。它按步骤覆盖代码、目录与构建工具迁移,并包含验证和遗漏字符串检查。
主要功能
- 01按步骤迁移初始化与 React 组件
- 02转换字符串、复数和命名空间
- 03配置 Vite、Babel、Next.js 等构建工具
- 04验证目录、类型和构建结果
- 05检查遗留 i18next 与未本地化字符串
适用场景
- 01将 React 应用从 i18next 迁移到 Lingui
- 02迁移现有翻译目录和复数消息
- 03验证构建配置并查找遗漏的界面字符串
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lingui/ skills --skill migrate-i18next-to-lingui安装后在 Claude Code 中输入/migrate-i18next-to-lingui确认已加载。
附带文件references/catalog-conversion.mdreferences/migration-patterns.md
详细介绍
将 i18next 迁移到 Lingui
迁移清单
- [ ] 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
第 1 步:安装 Lingui
Node 版本要求:Lingui 6 仅支持 ESM,要求 Node.js ≥ 22.19(或 ≥ 24)。在较旧版本的 Node 上,请将所有
@lingui/*包固定为^5。
# 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 @lingui/babel-plugin-lingui-macro
# SWC
npm install --save-dev @lingui/swc-plugin
# Vite (also install the Babel macro plugin above — see Step 3)
npm install --save-dev @lingui/vite-plugin
第 2 步:创建 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"],
},
],
});
第 3 步:配置构建工具
Vite(vite.config.ts):
import { lingui } from "@lingui/vite-plugin";
import react from "@vitejs/plugin-react";
export default {
plugins: [
react({ babel: { plugins: ["@lingui/babel-plugin-lingui-macro"] } }),
lingui(),
],
};
单独使用 lingui() 不会转换宏——react() 中的 Babel 宏插件才会执行转换(要求 @vitejs/plugin-react@^5;v6 移除了 babel 选项,请改用 SWC 变体)。
lingui() 插件还会即时编译 .po 目录文件,因此在 Vite 中,应用可以直接导入目录文件,并完全跳过 lingui compile 步骤:
// Dynamic import; the .po extension is mandatory
const { messages } = await import(`./locales/${locale}/messages.po`);
对于 TypeScript,需声明模块,以便 .po 导入通过类型检查:
// src/vite-env.d.ts
declare module "*.po" {
import type { Messages } from "@lingui/core";
export const messages: Messages;
}
Babel(.babelrc 或 babel.config.js):
{ "plugins": ["macros"] }
使用 SWC 的 Next.js(next.config.js):
module.exports = {
experimental: {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
};
构建工具注意事项:@lingui/swc-plugin 必须与 SWC 运行时版本匹配;插件条目必须是 [name, options] 元组(单独的字符串会静默禁用宏);@vitejs/plugin-react@6 则完全移除了 Babel 支持——这三项详见 swc-plugin-compatibility 技能。
第 4 步:替换 i18n 初始化
之前(i18next):
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
i18next.use(initReactI18next).init({
lng: "en",
resources: { en: { translation: { key: "Hello world" } } },
});
之后(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>;
}
动态切换语言:
async function changeLocale(locale) {
const { messages } = await import(`./locales/${locale}/messages`);
i18n.load(locale, messages);
i18n.activate(locale);
}
第 5 步:迁移 React 组件
useTranslation → useLingui
之前:
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation();
return <p>{t("greeting")}</p>;
}
之后:
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
return <p>{t`Hello World`}</p>;
}
Trans 组件
之前:
import { Trans } from "react-i18next";
<Trans i18nKey="welcome">Hello World!</Trans>
之后:
import { Trans } from "@lingui/react/macro";
<Trans>Hello World!</Trans>
使用显式 ID(保留 i18next 键时):
<Trans id="welcome">Hello World!</Trans>
带插值的 Trans
之前:
<Trans i18nKey="greeting" values={{ name }}>Hello {{ name }}!</Trans>
之后:
<Trans>Hello {name}!</Trans>
第 6 步:迁移 JS/TS 字符串
简单字符串
| i18next | Lingui |
|---|---|
t('key') | t\Message text`` |
t('key', { name }) | t\Hello ${name}`` |
t('key', { defaultValue: 'Hi' }) | t\Hi`` |
之前:
import i18next from "i18next";
const msg = i18next.t("greeting", { name: "Tom" });
之后:
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const msg = t`Hello ${name}`;
}
在原生 JS 中(组件外):
import { t } from "@lingui/core/macro";
const msg = t`Hello ${name}`;
保留 i18next 中的显式键
如果要保留 i18next 消息 ID:
import { t } from "@lingui/core/macro";
// i18next: t('navigation.home')
const msg = t({ id: "navigation.home", message: "Home" });
模块级 / 延迟加载的字符串
之前(i18next):
const LABELS = {
active: "Active",
inactive: "Inactive",
};
// translated at render time
之后(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>;
}
第 7 步:迁移复数
i18next 为每种复数形式使用独立的键。Lingui 在单条消息中使用 ICU MessageFormat。
之前(i18next):
{
"item_one": "{{count}} item",
"item_other": "{{count}} items"
}
t("item", { count });
之后(Lingui)- JSX:
import { Plural } from "@lingui/react/macro";
<Plural value={count} one="# item" other="# items" />
之后(Lingui)- JS 字符串:
import { plural } from "@lingui/core/macro";
const msg = plural(count, {
one: "# item",
other: "# items",
});
精确匹配(i18next _0):
<Plural
value={count}
_0="No items"
one="# item"
other="# items"
/>
第 8 步:迁移命名空间
i18next 命名空间(useTranslation('common'))对应 Lingui 的目录路径。有两种做法:
选项 A - 单个目录(最简单):
移除命名空间,使用统一目录。更新 lingui.config.js,纳入所有源目录。
选项 B - 多个目录(保留命名空间分隔):
// lingui.config.js
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/common",
include: ["src/components/common"],
},
{
path: "<rootDir>/src/locales/{locale}/auth",
include: ["src/components/auth"],
},
]
使用多个目录时,在启动时加载所有目录:
import { messages as commonMessages } from "./locales/en/common";
import { messages as authMessages } from "./locales/en/auth";
i18n.load("en", { ...commonMessages, ...authMessages });
i18n.activate("en");
第 9 步:转换现有翻译目录
有关转换和模式,请参阅 catalog-conversion.md。
**关键概念:**i18next 使用带点分隔键的 JSON;Lingui 使用 .po 文件,并将消息本身作为 ID(或使用你提供的显式 ID)。
第 10 步:构建并验证
按以下顺序运行完整的验证流程——每个阶段都会发现前一阶段无法发现的问题:
npx lingui extract --clean # extracts all messages → .po files, drops obsolete entries
npx lingui compile # compiles .po → runtime message catalogs
npx tsc --noEmit # type errors from changed imports/APIs
npm run build # macro transform actually runs in the real build
在 Vite 中使用 @lingui/vite-plugin 直接导入 .po 目录文件(第 3 步)时,请跳过 lingui compile——插件会在开发或构建时进行编译,无需管理编译后的目录文件。
在 package.json 中添加:
{
"scripts": {
"i18n:extract": "lingui extract",
"i18n:compile": "lingui compile"
}
}
对于 TypeScript:
npx lingui compile --typescript
回顾检查:查找迁移遗漏的字符串
构建成功只能证明已迁移的字符串可用,并不能证明所有字符串都已迁移。请执行两项检查:
1. 不残留 i18next 内容。 以下命令都应无结果:
grep -rn "from ['\"]i18next\|from ['\"]react-i18next" src/
grep -rn "useTranslation\|i18nKey\|i18next.t(" src/
2. 不存在未包装的面向用户字符串。 在整个代码树上运行 eslint-plugin-lingui 的 no-unlocalized-strings 规则,以捕获从未出现在 i18next 中的硬编码字符串,以及遗留为纯字符串的 defaultValue 文本:
// eslint.config.js
import pluginLingui from "eslint-plugin-lingui";
export default [
{
plugins: { lingui: pluginLingui },
rules: {
"lingui/no-unlocalized-strings": ["warn", {
ignore: ["^[A-Z0-9_-]+$"], // enums, constants
ignoreNames: ["className", "src", "data-testid"],
ignoreFunctions: ["console.*", "cn", "cva"],
}],
},
},
];
这项扫描有意扩大了覆盖范围——请检查每个命中项,并将其包装起来,或确认它不是 UI 字符串(类名、键、URL)。之后也请保留已安装的插件,作为长期防护措施。请报告你有意保留为未包装状态的字符串,不要默默跳过。
如需完整的审计流程——包括经过调整的扫描器配置、有界的修复轮次和剩余项报告——请使用 find-unwrapped-strings 技能。上述检查是其针对迁移范围的子集。
第 11 步:移除 i18next
npm uninstall i18next react-i18next
常见模式参考
日期/数字格式化
之前(i18next):
t("intlDateTime", { val: new Date() });
之后(Lingui):
import { useLingui } from "@lingui/react/macro";
function Component() {
const { i18n } = useLingui();
return <span>{new Intl.DateTimeFormat(i18n.locale).format(date)}</span>;
}
上下文(消除歧义)
之前(i18next):
t("right", { context: "direction" });
之后(Lingui):
<Trans context="direction">right</Trans>
// or
t({ message: "right", context: "direction" });
性别/选择
之前(i18next):
{ "pronoun_male": "He", "pronoun_female": "She", "pronoun_other": "They" }
之后(Lingui):
import { select } from "@lingui/core/macro";
const pronoun = select(gender, {
male: "He",
female: "She",
other: "They",
});
注意事项
- **不要在模块级别调用
t\...`。** 请改用msg`...`,并在渲染时使用_(descriptor)` 进行翻译。 - 更改语言区域设置后,请同时调用
i18n.load(locale, messages)和i18n.activate(locale)。 - 在任何组件使用
useLingui或Trans之前,请用<I18nProvider i18n={i18n}>包裹整个应用。 - 生产构建前务必运行
lingui compile;应用导入的是已编译的.js目录,而不是.po文件。 - 消息文本更改时,生成的 ID 也会改变。 如果需要保持稳定,请使用显式的
id属性。