mcpField
L
lingui-best-practices

lingui-best-practices

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

关于 lingui-best-practices

lingui-best-practices 是面向 JavaScript 开发者的 Lingui 国际化技能,涵盖 React 与原生 JavaScript 应用的消息标记、提取、翻译和目录管理实践。

主要功能

  1. 01指导选择 Lingui 宏
  2. 02处理复数与动态消息
  3. 03配置和加载翻译目录
  4. 04安排提取、编译与校验
  5. 05提供目录维护建议

适用场景

  1. 01为 React 界面添加可翻译文本
  2. 02为原生 JavaScript 字符串实现国际化
  3. 03将目录提取和编译纳入构建流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lingui/skills --skill lingui-best-practices

安装后在 Claude Code 中输入/lingui-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
common-mistakes.md
configuration.md
integration-recipes.md

详细介绍

Lingui 最佳实践

Lingui 是一个功能强大的 JavaScript 国际化(i18n)框架。本技能介绍在 React 和原生 JavaScript 应用中实现 i18n 的最佳实践。

快速入门工作流

标准的 Lingui 工作流包括以下步骤:

  1. 使用 I18nProvider 包装应用
  2. 使用宏(Trans、t 等)标记待翻译的消息
  3. 提取消息:lingui extract
  4. 翻译目录
  5. 编译目录:lingui compile
  6. 在应用中加载并激活语言区域

核心包

从以下包导入:

// 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。

请按顺序回答以下问题:

  1. 消息是否取决于计数? → 使用 Plural(JSX)或 plural(字符串)。切勿用普通 Trans 包装依赖计数的字符串——这会将英语复数规则固化到消息中。
  2. 内容是否为 JSX? → 使用 Trans
  3. 字符串是否位于组件内(属性、提示、函数参数)?→ 使用 useLingui() + t`...`
  4. 字符串是否定义在组件外(模块作用域、常量、配置)?→ 使用 msg 描述符,并在渲染时通过 t(descriptor) 或 _(descriptor) 解析
  5. 代码是否不属于 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>;
}

故障排除

如果遇到问题:

  1. 未提取消息:检查 lingui.config.js 中的 include 模式
  2. 未应用翻译:确保使用 lingui compile 编译目录
  3. 运行时错误:确认 I18nProvider 包裹了应用
  4. 类型错误:TypeScript 项目请运行 lingui compile --typescript

如需了解详细的常见错误和陷阱,请参阅 common-mistakes.md。

如需了解 Lingui 与要求使用普通字符串的库之间的衔接问题——验证架构、属性中的复数形式、带值的 i18n._()、服务端组合的消息、Intl 格式化器——以及无需构建步骤的代码,请参阅 integration-recipes.md。这些场景都有一种能通过编译、能够发布却存在错误的写法;这些指南会先介绍陷阱。

评论

支持 Markdown