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 dev 和 next build,你无需手动触发。
当上述文件被编译时,会执行以下操作:
- 将内联消息以自动分配的键提取到你的源语言文件中:
{
"VgH3tb": "Look ma, no keys!"
}- 通过添加空条目或删除过时条目,保持目标语言文件同步:
{
"VgH3tb": ""
}- 编译文件,将
useExtracted替换成useTranslations:
import {useTranslations} from 'next-intl';
function InlineMessages() {
const t = useTranslations();
return <h1>{t('VgH3tb')}</h1>;
}链接:
入门
该 API 当前为实验性功能,需要在 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 dev 或 next build 时,都会根据发现的消息自动提取,并保持你的消息同步。
详情请参见 createNextIntlPlugin。
我可以手动提取消息吗?
虽然消息提取设计为无缝集成你的开发工作流——基于运行 next dev 和 next 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"
}
}命名空间适用于以下情况:
- 库: 如果你在 monorepo 中有多个包,可以将不同包的消息合并到同一个目录,避免包之间的键冲突。
- 拆分: 如果你只想传递部分消息给客户端,这有助于相应地分组(例如
<NextIntlClientProvider messages={messages.client}>)。
建议不要过度使用命名空间,因为如果涉及重构命名空间,移动组件间的消息可能会变得困难。
await getExtracted()
在异步函数例如服务端组件、Metadata 和服务端 Action 中,使用来自 next-intl/server 的异步版本:
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 等工具实现自动化。
静态分析
消息提取依赖于静态分析。
实际上,这意味着:
t必须将字面字符串作为其消息参数t必须在从useExtracted或getExtracted获取它的同一函数体中调用
有效用法:
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 处于作用域内的位置,将它生成的结构化错误转换为消息——可以通过每次解析时使用的错误映射,也可以通过共享的异步辅助函数来实现。
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 包含外部包的消息:
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:
const nextConfig = {
// ...
transpilePackages: ['@acme/ui', '@acme/components']
};
// ...之后,你可以在 getRequestConfig 中将第一方消息与外部消息合并:
const messages = {
...(await import(`@acme/ui/messages/${locale}.po`)).default,
...(await import(`@acme/components/messages/${locale}.po`)).default,
...(await import(`../../messages/${locale}.po`)).default
};
// ...为避免不同包之间的键发生冲突,可以考虑使用命名空间。