关于 lingui-best-practices
lingui-best-practices 是面向 JavaScript 开发者的 Lingui 国际化技能,涵盖 React 与原生 JavaScript 应用的消息标记、提取、翻译和目录管理实践。
主要功能
- 01指导选择 Lingui 宏
- 02处理复数与动态消息
- 03配置和加载翻译目录
- 04安排提取、编译与校验
- 05提供目录维护建议
适用场景
- 01为 React 界面添加可翻译文本
- 02为原生 JavaScript 字符串实现国际化
- 03将目录提取和编译纳入构建流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lingui/ skills --skill lingui-best-practices安装后在 Claude Code 中输入/lingui-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/common-mistakes.mdconfiguration.mdintegration-recipes.md详细介绍
Lingui 最佳实践
Lingui 是一个功能强大的 JavaScript 国际化(i18n)框架。本技能介绍在 React 和原生 JavaScript 应用中实现 i18n 的最佳实践。
快速入门工作流
标准的 Lingui 工作流包括以下步骤:
- 使用
I18nProvider包装应用 - 使用宏(
Trans、t等)标记待翻译的消息 - 提取消息:
lingui extract - 翻译目录
- 编译目录:
lingui compile - 在应用中加载并激活语言区域
核心包
从以下包导入:
// React macros (recommended)
import { Trans, Plural, Select, useLingui } from "@lingui/react/macro";
// Core macros for vanilla JS
import { t, msg, plural, select } from "@lingui/core/macro";
// Runtime (rarely used directly)
import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";
设置 I18nProvider
使用 I18nProvider 包装应用:
import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";
import { messages } from "./locales/en/messages";
i18n.load("en", messages);
i18n.activate("en");
function App() {
return (
<I18nProvider i18n={i18n}>
{/* Your app */}
</I18nProvider>
);
}
翻译 UI 文本
选择合适的宏
此流程图假设在构建时运行宏转换(Babel、SWC 或 Vite 插件)。对于不经过构建步骤的代码——Node.js 脚本、无服务器函数、测试运行器——则应使用运行时宏:请参阅 integration-recipes.md。
请按顺序回答以下问题:
- 消息是否取决于计数? → 使用
Plural(JSX)或plural(字符串)。切勿用普通Trans包装依赖计数的字符串——这会将英语复数规则固化到消息中。 - 内容是否为 JSX? → 使用
Trans - 字符串是否位于组件内(属性、提示、函数参数)?→ 使用
useLingui()+t`...` - 字符串是否定义在组件外(模块作用域、常量、配置)?→ 使用
msg描述符,并在渲染时通过t(descriptor)或_(descriptor)解析 - 代码是否不属于 React? → 使用来自
@lingui/core/macro的t
如果字符串需要翻译者注释,请使用流程图所选宏的对象形式——t`…` 和 msg`…` 都无法附加注释。在包装时就作出这个决定毫不费力;事后转换整个代码库则不然。请参阅增强消息上下文技能。
有些目标位置要求使用纯字符串,无法接受宏——例如 Zod 消息、aria-label 中的计数、服务器函数的返回值、Intl 格式化器。这些场景各有对应的做法:integration-recipes.md。
使用 Trans 翻译 JSX 内容
Trans 宏是翻译 JSX 的主要方式:
import { Trans } from "@lingui/react/macro";
// Simple text
<Trans>Hello World</Trans>
// With variables
<Trans>Hello {userName}</Trans>
// With components (rich text)
<Trans>
Read the <a href="/docs">documentation</a> for more info.
</Trans>
// Extracted as: "Read the <0>documentation</0> for more info."
使用场景:任何 JSX 元素中的可翻译文本。
使用 useLingui 处理非 JSX 内容
对于 JSX 之外的字符串(属性、提示、函数调用):
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const handleClick = () => {
alert(t`Action completed!`);
};
return (
<div>
<img src="..." alt={t`Image description`} />
<button onClick={handleClick}>{t`Click me`}</button>
</div>
);
}
使用场景:元素属性、提示、函数参数,以及任何非 JSX 字符串。
宏钩子返回**i18n 和 t**,并且两者都绑定到 React 上下文——因此,一个钩子即可读取当前激活的语言区域、按该语言区域进行格式化,并让组件订阅语言区域变更。只需一个导入即可——来自 @lingui/react 的运行时 useLingui 适用于未经过宏转换的代码:
import { useLingui } from "@lingui/react/macro";
const { t, i18n } = useLingui();
i18n.locale; // "de-DE" — the active locale
new Intl.NumberFormat(i18n.locale) // format against it
使用 msg 实现延迟翻译
如果需要在模块级别或数组/对象中定义消息:
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
// Module-level constants
const STATUSES = {
active: msg`Active`,
inactive: msg`Inactive`,
pending: msg`Pending`,
};
function StatusList() {
const { _ } = useLingui();
return Object.entries(STATUSES).map(([key, message]) => (
<div key={key}>{_(message)}</div>
));
}
使用场景:模块级常量、消息数组、条件式消息选择。
描述符会改变字段类型
msg 会将 string 字段转换为 MessageDescriptor,因此 TypeScript 会在每个使用位置指出问题——这正是可以安全地批量应用此转换的原因。有两类问题不会被它捕获:
React 键仍能通过编译。 key={item.label} 会变成对象键,React 会将其字符串化为 [object Object]——每一行的键都相同,导致协调过程退化,而唯一的信号只是控制台警告。应使用标识符作为键,绝不要使用文案:
// ❌ compiles; every key is now identical
{NAV.map((item) => <li key={item.label}>{t(item.label)}</li>)}
// ✅
{NAV.map((item) => <li key={item.to}>{t(item.label)}</li>)}
字符串方法会产生类型错误,但容易诱使人采用错误的修复方式。 LABELS[k].toLowerCase() 无法通过编译——这是正确的;但 t(LABELS[k]).toLowerCase() 并不是正确的修复方法。将译文转为小写会破坏遵循大小写规则的语言(例如德语名词),而对没有大小写区分的文字则毫无作用。如果确实需要小写变体,应将其作为另一条拥有独立注释的消息。
复数形式
对依赖数量的消息使用 Plural 宏:
import { Plural } from "@lingui/react/macro";
<Plural
value={messageCount}
one="You have # message"
other="You have # messages"
/>
占位符 # 会替换为实际值。
精确匹配
使用 _N 语法进行精确数字匹配(优先于复数形式):
<Plural
value={count}
_0="No messages"
one="One message"
other="# messages"
/>
与变量和组件搭配使用
与 Trans 结合,用于复杂消息:
<Plural
value={count}
one={`You have # message, ${userName}`}
other={
<Trans>
You have <strong>#</strong> messages, {userName}
</Trans>
}
/>
格式化日期和数字
直接使用 Intl:
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { i18n } = useLingui();
const lastLogin = new Date();
const dateFormatter = useMemo(() => new Intl.DateTimeFormat(i18n.locale), [i18n.locale]);
return <Trans>Last login: {dateFormatter.format(lastLogin)}</Trans>;
}
消息 ID 和上下文
显式 ID
提供自定义 ID,以便使用稳定的消息键:
<Trans id="header.welcome">Welcome to our app</Trans>
用于消除歧义的上下文
当相同文本有不同含义时,使用 context:
<Trans context="direction">right</Trans>
<Trans context="correctness">right</Trans>
这些会创建独立的目录条目。
仅当相同文本确实需要不同译文时才使用 context,不要将其用作命名空间方案(auth.login、settings.title)。含义相同的重复字符串应共用一个目录条目,只翻译一次。
给译者的注释
添加上下文以供译者参考:
<Trans comment="Greeting shown on homepage">Hello World</Trans>
配置
基本的 lingui.config.js:
import { defineConfig } from "@lingui/cli";
export default defineConfig({
sourceLocale: "en",
locales: ["en", "es", "fr", "de"],
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
exclude: ["**/node_modules/**"],
},
],
});
有关详细的配置模式,请参阅 configuration.md。
Lingui 6 说明
Lingui 6(2026 年 4 月)仅支持 ESM,且需要 Node.js ≥ 22.19(或 ≥ 24)。如果项目无法满足此要求,请将所有 @lingui/* 软件包固定到 ^5。
已弃用的字符串形式 format: "po" 和 formatOptions 选项已在 v6 中移除。省略 format(PO 仍为默认值),或传入格式化器实例进行配置:
import { defineConfig } from "@lingui/cli";
import { formatter } from "@lingui/format-po";
export default defineConfig({
// ...
format: formatter({ lineNumbers: false }),
});
lineNumbers: false 可缩小目录差异,因为几乎每次源文件编辑都会更改行号注释。
目录维护
将提取和编译接入项目,避免遗漏:
{
"scripts": {
"lingui:extract": "lingui extract",
"lingui:compile": "lingui compile",
"dev": "lingui compile && vite",
"build": "lingui compile && vite build"
}
}
- 在现有的
dev/build脚本前添加lingui compile &&——切勿替换现有脚本,也不要依赖prebuild钩子:pnpm ≥ 7 和 Yarn Berry 默认不会运行前置/后置钩子。 - 按扩展名忽略已编译的目录文件,切勿按目录忽略。 像
src/locales/这样的目录规则也会忽略.po文件——它们才是翻译的真实来源:
# ✅ compiled artifacts only
src/locales/**/messages.ts
src/locales/**/messages.js
# ❌ never — silently untracks the .po sources too
# src/locales/
使用 git check-ignore 验证:已编译文件必须匹配,其同名 .po 文件不得匹配。忽略已编译目录文件只有在每次构建前都运行 lingui compile 时才安全——两者缺一不可。
- 让
compileNamespace与应用导入目录文件的方式相匹配。 如果代码将./locales/en/messages作为.ts文件导入,请在lingui.config中设置compileNamespace: "ts",这样运行普通的lingui compile就会重新生成完全对应的产物,无需担心有人忘记传入--typescript标志。 - Vite 替代方案:使用
@lingui/vite-plugin时,应用可以直接动态导入.po目录文件(await import(\./locales/${locale}/messages.po`)`)——插件会即时编译,因此无需编写脚本处理或通过 gitignore 忽略已编译的目录文件。 - 添加 CI 漂移检查,将目录状态纳入 PR 的约定(Lingui 6.8+;旧版本使用
lingui compile && lingui extract --clean && git diff --exit-code -- src/locales):
"i18n:check": "lingui check sync"
当有人添加或编辑消息却未重新运行提取时,此检查会使构建失败,且不会写入任何内容。检查标志应与团队提取时使用的标志一致(如果提取脚本使用 --clean,则使用 check sync --clean;--overwrite 同理);否则,检查所比对的输出并非团队实际生成的内容。
- 单独检查缺失的译文:使用
lingui check missing,检查在应用fallbackLocales后仍然缺失的译文(compile --strict规则,请参阅 configuration.md)。如果译文通过 TMS 异步提供,请在发布流程中运行该检查,而不是在每个 PR 中运行——否则,在译者补齐译文前,每个功能 PR 都会失败。
最佳实践
始终使用宏
优先使用宏,而非运行时组件。宏会在构建时编译,从而减小打包体积:
// ✅ Good - uses macro
import { Trans } from "@lingui/react/macro";
// ❌ Avoid - runtime only
import { Trans } from "@lingui/react";
保持消息简单
避免在消息中使用复杂表达式——它们会被替换为占位符:
// ❌ Bad - loses context
<Trans>Hello {user.name.toUpperCase()}</Trans>
// Extracted as: "Hello {0}"
// ✅ Good - clear variable name
const userName = user.name.toUpperCase();
<Trans>Hello {userName}</Trans>
// Extracted as: "Hello {userName}"
如果不便提取到局部变量中,可以使用 ph() 在行内命名占位符:
import { ph } from "@lingui/core/macro";
// Extracted as: "Hello {name}" instead of "Hello {0}"
t`Hello ${ph({ name: getUserName() })}`;
ph() 也适用于 Trans、Plural 和 Select。
JSX 使用 Trans,字符串使用 t
选择合适的工具:
// ✅ For JSX content
<h1><Trans>Welcome</Trans></h1>
// ✅ For string values
const { t } = useLingui();
<img alt={t`Profile picture`} />
不要在模块级别使用宏
宏需要组件上下文——请改用 msg:
// ❌ Bad - won't work
import { t } from "@lingui/core/macro";
const LABELS = [t`Red`, t`Green`, t`Blue`];
// ✅ Good - use msg for lazy translation
import { msg } from "@lingui/core/macro";
const LABELS = [msg`Red`, msg`Green`, msg`Blue`];
不要包装非 UI 字符串
并非所有字符串都是消息。以下内容不要包装:
- CSS 类和
className值 console.*/ 日志记录器输出以及面向开发者的错误代码- 导入路径、URL、API 路由、查询键
- 对象键、枚举值、全大写常量、
data-testid值 - 会用于比较或持久化的值(状态、slug)
URL 的区域设置前缀属于路由问题,而非翻译问题——不要用宏包装路径。
使用 ESLint 插件
安装并配置 eslint-plugin-lingui,自动捕获常见错误:
npm install --save-dev eslint-plugin-lingui
// eslint.config.js
import pluginLingui from "eslint-plugin-lingui";
export default [
pluginLingui.configs["flat/recommended"],
];
区域设置元数据:集中定义
在不导入 React 或框架的共享模块中定义一次区域设置信息,这样配置、中间件、测试和组件都可以安全使用:
// src/i18n/locales.ts
export const locales = ["en", "es", "fr", "ar"] as const;
export type Locale = (typeof locales)[number];
export const sourceLocale: Locale = "en";
const RTL_LOCALES = new Set(["ar", "he", "fa", "ur"]);
export const getDirection = (locale: string): "ltr" | "rtl" =>
RTL_LOCALES.has(locale.split("-")[0]) ? "rtl" : "ltr";
// "Deutsch", not "German" — each language name rendered in its own language
export const localeDisplayName = (locale: string) =>
new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale;
// `null` is in the signature on purpose: detect() and headers.get() both return it
export function resolveLocale(candidate: string | null | undefined): Locale {
if (!candidate) return sourceLocale;
if ((locales as readonly string[]).includes(candidate)) return candidate as Locale;
const base = candidate.split("-")[0]; // es-MX → es
return (locales as readonly string[]).includes(base) ? (base as Locale) : sourceLocale;
}
出现以下情况,说明实现有误:getDirection 或 Intl.DisplayNames 在多个文件中定义;dir="rtl" 条件判断散落各处;语言名称映射需要手动维护。
布局注意事项:不要同时保留 src/i18n.ts 和 src/i18n/——在模块解析时,扁平文件会遮蔽目录中的 index.ts,构建仍会通过,但应用实际上会悄悄出错。选择一种布局即可。
常见模式
动态切换区域设置
import { i18n } from "@lingui/core";
async function changeLocale(locale) {
const { messages } = await import(`./locales/${locale}/messages`);
i18n.load(locale, messages);
i18n.activate(locale);
}
动态加载目录
import { useEffect } from "react";
import { i18n } from "@lingui/core";
function loadCatalog(locale) {
return import(`./locales/${locale}/messages`);
}
function App() {
useEffect(() => {
loadCatalog("en").then(catalog => {
i18n.load("en", catalog.messages);
i18n.activate("en");
});
}, []);
return <I18nProvider i18n={i18n}>{/* ... */}</I18nProvider>;
}
消息中的全局应用值
在许多消息中都会出现的值——产品名称、支持邮箱地址、应用版本——应作为占位符传入。这样,重命名时只需更改一个值,所有消息都能保留其 ID 和翻译。从 Lingui 6.10 开始,可在实例上注册一次,并在消息中直接使用占位符:
// once, at startup — or setupI18n({ variables }) when you create the instance
i18n.setVariables({ appName: "Acme Notes" });
// in a component
const { t } = useLingui();
t`Questions about {appName}? Ask in the help center.`;
<Trans>Welcome to {"{appName}"}</Trans>; // the string literal keeps the braces as a placeholder
- 调用处传入的值优先;
undefined和null会回退到默认值。 - 未设置或拼写错误的名称会渲染为空字符串,且不会发出警告——请在首次渲染前设置默认值。
- 函数默认值(
() => user.plan)会在每次渲染时读取,但其背后的值发生变化时不会触发重新渲染。i18n.setVariable(name, value)和setVariables会触发change,因此I18nProvider会重新渲染。 - 译者看到的是原样的
{appName}:请添加注释说明其内容(enhanced-message-context)。
使用 useLingui 进行记忆化
使用记忆化时,请使用宏版本的 t 函数:
import { useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
import { useMemo } from "react";
const welcomeMessage = msg`Welcome!`;
function MyComponent() {
const { t } = useLingui(); // Macro version - reference changes with locale
// ✅ Safe - t reference updates with locale
const message = useMemo(() => t(welcomeMessage), [t]);
return <div>{message}</div>;
}
故障排除
如果遇到问题:
- 未提取消息:检查
lingui.config.js中的include模式 - 未应用翻译:确保使用
lingui compile编译目录 - 运行时错误:确认
I18nProvider包裹了应用 - 类型错误:TypeScript 项目请运行
lingui compile --typescript
如需了解详细的常见错误和陷阱,请参阅 common-mistakes.md。
如需了解 Lingui 与要求使用普通字符串的库之间的衔接问题——验证架构、属性中的复数形式、带值的 i18n._()、服务端组合的消息、Intl 格式化器——以及无需构建步骤的代码,请参阅 integration-recipes.md。这些场景都有一种能通过编译、能够发布却存在错误的写法;这些指南会先介绍陷阱。