Skip to content
文档使用指南useExtracted

useExtracted(实验性功能)

作为手动管理命名空间和键的替代方案,next-intl 提供了一个额外的 API,它的工作方式类似于 useTranslations,但会自动从你的源文件中提取消息。

import {useExtracted} from 'next-intl';
 
function InlineMessages() {
  const t = useExtracted();
  return <h1>{t('Look ma, no keys!')}</h1>;
}

提取会通过 Turbo 或 Webpack loader 自动集成到 next devnext build,你无需手动触发。

当上述文件被编译时,会执行以下操作:

  1. 将内联消息以自动分配的键提取到你的源语言文件中:
messages/en.json
{
  "VgH3tb": "Look ma, no keys!"
}
  1. 通过添加空条目或删除过时条目,保持目标语言文件同步:
messages/de.json
{
  "VgH3tb": ""
}
  1. 编译文件,将 useExtracted 替换成 useTranslations
import {useTranslations} from 'next-intl';
 
function InlineMessages() {
  const t = useTranslations();
  return <h1>{t('VgH3tb')}</h1>;
}

链接:

入门

该 API 当前为实验性功能,需要在 next.config.ts 中启用:

next.config.ts
import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin({
  experimental: {
    // 启用 useExtracted 的使用
    extract: true,
 
    messages: {
      // 目录的相对路径
      path: './messages',
 
      // 格式为 'json'、'po' 或自定义格式(见下文)
      format: 'json',
 
      // 可填写 'infer',根据 `path` 中匹配的文件自动检测语言区域,
      // 也可填写明确的语言区域数组
      locales: 'infer',
 
      // 定义哪个语言区域包含规范源字符串
      sourceLocale: 'en'
    },
 
    // 源代码文件的相对路径
    srcPath: './src'
  }
});
 
const config: NextConfig = {};
export default withNextIntl(config);

这样,每次运行 next devnext build 时,都会根据发现的消息自动提取,并保持你的消息同步。

详情请参见 createNextIntlPlugin

我可以手动提取消息吗?

虽然消息提取设计为无缝集成你的开发工作流——基于运行 next devnext build,你也可以手动提取消息:

import {unstable_extractMessages} from 'next-intl/extractor';
 
await unstable_extractMessages({
  srcPath: './src',
  messages: {
    path: './messages',
    format: 'po',
    locales: 'infer',
    sourceLocale: 'en'
  }
});
 
console.log('✔ 消息已提取');

这在你开发包(例如组件库)时非常有用,此时你没有运行 Next.js 开发服务器,但想连同包一起提供消息。

另请参阅:单仓库和外部包

内联消息

ICU 消息

你熟悉的 useTranslations 中所有 ICU 特性 都支持,依然可以照常使用:

// 参数插值
t('Hello {name}!', {name: 'Jane'});
// 基数复数形式
t(
  'You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}.',
  {count: 3580}
);
// 序数复数形式
t(
  "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!",
  {year: 22}
);
// 选择值
t('{gender, select, female {She is} male {He is} other {They are}} online.', {
  gender: 'female'
});
// 富文本
t.rich('Please refer to the <link>guidelines</link>.', {
  link: (chunks) => <Link href="/guidelines">{chunks}</Link>
});

唯一的例外是 t.raw,此功能不打算与消息提取一起使用。

描述信息

为了给消息(AI)译者提供更多上下文,你可以添加描述信息:

<button onClick={onSlideRight}>
  {t({
    message: 'Right',
    description: 'Advance to the next slide'
  })}
</button>

显式 ID

如果你想使用显式 ID 而非自动生成的,可以选择提供:

<button onClick={onSlideRight}>
  {t({
    id: 'carousel.next',
    message: 'Right'
  })}
</button>

这在标签在多个地方使用但在其它语言需要不同翻译时非常有用。这是一个较少需要使用的“逃生舱”。

命名空间

如果你希望将消息组织到特定命名空间下,可传递给 useExtracted

function Modal() {
  const t = useExtracted('design-system');
  return (
    <>
      <button>{t('Close')}</button>
      ...
    </>
  );
}

这会将调用 t 的消息提取到对应的命名空间中:

{
  "design-system": {
    "5VpL9Z": "Close"
  }
}

命名空间适用于以下情况:

  1. 库: 如果你在 monorepo 中有多个包,可以将不同包的消息合并到同一个目录,避免包之间的键冲突。
  2. 拆分: 如果你只想传递部分消息给客户端,这有助于相应地分组(例如 <NextIntlClientProvider messages={messages.client}>)。

建议不要过度使用命名空间,因为如果涉及重构命名空间,移动组件间的消息可能会变得困难。

await getExtracted()

在异步函数例如服务端组件、Metadata 和服务端 Action 中,使用来自 next-intl/server 的异步版本:

page.tsx
import {getExtracted} from 'next-intl/server';
 
export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getExtracted();
 
  return (
    <PageLayout title={t('Hello {name}!', {name: user.name})}>
      <UserDetails user={user} />
    </PageLayout>
  );
}

可选编译

虽然消息提取主要设计用于运行中的 Next.js 应用,useExtracted 也可以不被编译为 useTranslations,此时将直接使用内联消息而非替换成翻译键。

例如这对测试很有用:

import {expect, it} from 'vitest';
import {NextIntlClientProvider} from 'next-intl';
import {renderToString} from 'react-dom/server';
 
function Component() {
  const t = useExtracted();
  return t('Hello {name}!', {name: 'Jane'});
}
 
it('renders', () => {
  const html = renderToString(
    // 不需要传递任何消息
    <NextIntlClientProvider locale="en" timeZone="UTC">
      <Component />
    </NextIntlClientProvider>
  );
 
  // ✅ 将使用内联消息
  expect(html).toContain('Hello Jane!');
});

格式

消息可以提取为 .json.po,或使用自定义文件格式——有关配置详情,请参阅 messages.format

建议: 由于 useExtracted 会自动生成键,推荐使用 PO 文件,因为它们支持为消息提供更多上下文信息,如文件引用和描述,对(AI)译者很有帮助。

💡

基于 AI 的翻译可以使用 eloqnt/studio 等工具实现自动化。

静态分析

消息提取依赖于静态分析

实际上,这意味着:

  1. t 必须将字面字符串作为其消息参数
  2. t 必须在从 useExtractedgetExtracted 获取它的同一函数体中调用

有效用法:

import {useExtracted} from 'next-intl';
 
function Example({name}) {
  // ✅ 使 `t` 在此组件中可用
  const t = useExtracted();
 
  // ✅ 支持字符串字面量
  t('Hello there!');
 
  // ✅ 可以使用参数传入动态值
  t('Hello {name}!', {name});
 
  function onClick() {
    // ✅ 支持在事件处理函数中使用
    t('You clicked the button!');
  }
 
  // ✅ 支持在 JSX 中使用
  return <button onClick={onClick}>{t('Click me')}</button>;
}

相比之下,以下是不支持的模式示例:

import {useExtracted} from 'next-intl';
 
function Example({key}) {
  const t = useExtracted();
 
  // ❌ `key` 只有在运行时才可知
  t(key);
 
  // ❌ 将 `t` 传递给另一个函数
  getName(t);
}
 
// ❌ 重新导出该 hook
export const useExtractedExport = useExtracted;
 
async function AsyncExample() {
  // ❌ 将 `t` 传递给 `Promise.all`
  const [t] = await Promise.all([getExtracted()]);
 
  // 注意:`getExtracted` 在内部进行了缓存,即使是
  // 第一次调用通常也耗时不到 1 毫秒。
  // 因此,无需将其并行化。
}

请注意,useExtracted 可以在组件中调用,也可以在 hook 中调用——如果你想共享提取出的标签,这可能会很有用。

如何翻译类似枚举的值?

由于消息需要支持静态分析,因此不支持像 t(status) 这样的查找。相反,你可以在 t 处于作用域内的位置,将每个值映射到一个内联消息。

在服务器端,异步函数可以通过 getExtracted 获取 t

import {getExtracted} from 'next-intl/server';
 
type OrderStatus = 'pending' | 'shipped' | 'delivered';
 
async function getOrderStatusLabels(): Promise<Record<OrderStatus, string>> {
  const t = await getExtracted();
 
  return {
    pending: t('Pending'),
    shipped: t('Shipped'),
    delivered: t('Delivered')
  };
}

返回结果是一个普通的字符串对象,因此也可以将其传递给客户端组件:

export default async function OrdersPage() {
  const statusLabels = await getOrderStatusLabels();
  return <OrderList statusLabels={statusLabels} />;
}

如果标签只在客户端需要,可以改用 hook。请注意,该 hook 可以返回一个接收待翻译值的函数:

import {useExtracted} from 'next-intl';
 
function useOrderStatusLabel() {
  const t = useExtracted();
 
  return function getOrderStatusLabel(status: OrderStatus) {
    switch (status) {
      case 'pending':
        return t('Pending');
      case 'shipped':
        return t('Shipped');
      case 'delivered':
        return t('Delivered');
    }
  };
}
function OrderStatus({status}: {status: OrderStatus}) {
  const getOrderStatusLabel = useOrderStatusLabel();
  return <span>{getOrderStatusLabel(status)}</span>;
}

由于返回的是字符串,因此可以在任何地方使用该标签——不仅可以在 JSX 中使用,也可以用于例如 aria-label、排序或 toast 消息。

或者,如果你更喜欢使用单条消息,可以使用 select

如何本地化 Zod 的验证错误?

Zod schema 通常定义在模块作用域中,而 t 在那里不可用。

推荐的做法是让 schema 与语言环境无关,然后在 t 处于作用域内的位置,将它生成的结构化错误转换为消息——可以通过每次解析时使用的错误映射,也可以通过共享的异步辅助函数来实现。

参见:使用 Zod 进行验证时,如何本地化错误消息?

Monorepo 和外部包

无论你的应用引入的是调用 useExtracted 的外部模块、monorepo 中的同级包,还是安装在 node_modules 中的可复用库,通常都有两种设置方式可供选择。

1. 不将消息随外部包一起发布

一些团队使用 monorepo 和外部包只是为了组织上的便利,但从最终部署的角度来看,只有一个“入口点”:即一个最终会被部署并使用所有包的 Next.js 应用。

此时,配置 srcPath 以包含外部包的源代码目录:

const withNextIntl = createNextIntlPlugin({
  experimental: {
    extract: true,
    messages: {
      path: './messages',
      format: 'json',
      locales: 'infer',
      sourceLocale: 'en'
    },
    srcPath: [
      // 第一方消息
      './src',
 
      // monorepo 中的同级包
      '../ui/src',
 
      // `node_modules` 中已安装的包
      './node_modules/@acme/components'
    ]
  }
});

这样会将第一方消息和外部包消息都提取到主应用目录中。

2. 将消息随外部包一起发布

如果你的共享包会被多个应用使用,那么你可能希望在所有使用方之间复用消息。

为此,你可以在构建过程中使用 unstable_extractMessages 执行一次性提取,以提取共享消息:

import {unstable_extractMessages} from 'next-intl/extractor';
 
await unstable_extractMessages({
  srcPath: './src',
  messages: {
    path: './messages',
    format: 'po',
    locales: 'infer',
    sourceLocale: 'en'
  }
});

在使用该包的应用中,你可以配置 extract.path 以仅提取第一方消息,同时使用 messages.path 包含外部包的消息:

next.config.ts
const withNextIntl = createNextIntlPlugin({
  experimental: {
    // 仅提取第一方消息
    extract: {
      path: './messages'
    },
 
    // 加载时转换第一方消息和外部消息
    // (例如对于 .po 文件)
    messages: {
      path: [
        // 第一方消息
        './messages',
 
        // monorepo 中的同级包
        '../ui/messages',
 
        // `node_modules` 中已安装的包
        './node_modules/@acme/components/messages'
      ],
 
      // 取决于你的偏好
      format: 'po',
      locales: 'infer',
      sourceLocale: 'en'
    },
 
    srcPath: './src'
  }
});

此外,你还应使用 transpilePackages,以便在外部包中将 useExtracted 编译为 useTranslations

next.config.ts
const nextConfig = {
  // ...
  transpilePackages: ['@acme/ui', '@acme/components']
};
 
// ...

之后,你可以在 getRequestConfig 中将第一方消息与外部消息合并:

i18n/request.ts
const messages = {
  ...(await import(`@acme/ui/messages/${locale}.po`)).default,
  ...(await import(`@acme/components/messages/${locale}.po`)).default,
  ...(await import(`../../messages/${locale}.po`)).default
};
 
// ...
💡

为避免不同包之间的键发生冲突,可以考虑使用命名空间