mcpField
M
migrate-i18next-to-lingui

migrate-i18next-to-lingui

github.com/lingui开发10176 次安装1 周前更新
代码仓库

关于 migrate-i18next-to-lingui

migrate-i18next-to-lingui 是帮助开发者将 React 项目国际化实现从 i18next 迁移到 Lingui 的技能。它按步骤覆盖代码、目录与构建工具迁移,并包含验证和遗漏字符串检查。

主要功能

  1. 01按步骤迁移初始化与 React 组件
  2. 02转换字符串、复数和命名空间
  3. 03配置 Vite、Babel、Next.js 等构建工具
  4. 04验证目录、类型和构建结果
  5. 05检查遗留 i18next 与未本地化字符串

适用场景

  1. 01将 React 应用从 i18next 迁移到 Lingui
  2. 02迁移现有翻译目录和复数消息
  3. 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 字符串

简单字符串
i18nextLingui
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 属性。

其他资源

评论

支持 Markdown