useExtracted:i18n 的 Tailwind 版?
2025年11月7日 · 作者 Jan Amann很长一段时间里,我都是 Tailwind 的怀疑者。但有一天,我决定_给它五分钟_——然后我就入坑了。它的核心理念实在是太务实、太不可抗拒了。只要你试过,就再也回不去了。
这让我思考:“i18n 的 Tailwind 会是什么样子呢?”
首先,next-intl 进展得非常不错,并且最近已经突破了每周 100 万次下载(感谢大家!)。所以看起来有很多人对它目前的工作方式相当满意。而像 useTranslations 这样的核心 API 当然也会一直在。
但明天我们还会像今天这样写代码吗?
显然不会。越来越多的 AI 代理融入了我们的工作流,像 Cursor 和 Claude Code 这样的创新也日新月异。
那么,什么样的库适合 AI 优先的开发呢?大概是:一开始就对人类开发者非常友好。Tailwind 并不是“为 AI 打造”,而是为简化人类的样式编写而诞生的绝佳方案。如今,它成了代理工具默认的样式选择。
那什么造就了 Tailwind 的独特之处?
设计原则
如果我们看看 Tailwind 的设计,那么一个遵循相同原则的 i18n 解决方案可能会是这样:
- 并置(Colocation):就像 Tailwind 避免了需要管理独立的样式表那样,添加、更新或移除消息时不应需要手动管理 JSON 消息目录。不过,消息目录可以作为编译目标。
- 本地推理(Local reasoning):生成式 AI 对 Tailwind 很擅长,因为它只需要很小的上下文窗口。为了避免上下文污染,读取整个消息目录应该被避免(至少在不借助工具调用的情况下)。
- 不必给事物命名:不需要绞尽脑汁想名字会带来巨大的效率提升,因此应尽可能避免手动编写键(keys)。
- 清理(Purging):当代码被快速更改时,不应该留下任何“死代码”。类似于 Tailwind 可以清理未使用的样式,我们也应该自动清理未使用的消息。
- 最小化(Minification):Tailwind 的类名对包体积非常友好。同样地,消息也应使用最小化的键,以确保包尽可能小。
- 适合原型、可用于生产:无论是用于快速原型还是生产级应用,Tailwind 的外观都应该完全一致。以同样方式,应当只有一个 API,避免在项目规模和复杂度上事先做出结构性决策。
- 便于重构:在组件之间移动代码对于 Tailwind 来说是无缝的,你的消息也应该如此。
虽然 next-intl 对其中一些问题已经有了解决方案,但实际上仍有提升空间。因此,在大约两个月前发布了一个 RFC,今天我非常兴奋地分享我认为可能就是答案的方案:
import {useExtracted} from 'next-intl';
function InlineMessages() {
const t = useExtracted();
return <h1>{t('Look ma, no keys!')}</h1>;
}useExtracted 现场演示
我已经让你读了太多文字了,下面给你看个演示。
我们先从一个已经安装了 next-intl 的应用开始,把之前写死的文本标签改成支持翻译的:
不用写键(keys),也不用调 CLI,就像平常一样直接运行 next dev。
而且你会免费获得始终保持同步的 JSON 目录。
拥有你喜欢的 useTranslations 功能,却无需键名
如果你之前做过国际化,你会知道需要翻译的远不止是简单字符串。
所以我们会用到一些 ICU 功能:
没错,TypeScript 会自动校验:当你在消息中需要使用数字格式化时,你放了 number 格式化器——完全无需额外设置。这样可以确保用 Intl.NumberFormat 将数字格式化为符合地区习惯的可读字符串。
当然 t.rich 也支持:
t.rich('Please refer to the <link>guidelines</link>.', {
link: (chunks) => <Link href="/guidelines">{chunks}</Link>
});并且还有适用于服务器组件及相关环境的异步版本:
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>
);
}好,我们先退一步。看起来,使用内联消息的组件非常容易为 AI 生成,而且不需要把冗长的消息目录加载进它们宝贵的上下文窗口。
但是用 AI 来_翻译_你的消息怎么样?
上下文才是关键
虽然为译者提供上下文一直很重要,但在 AI 时代,用易于理解的文本方式提供上下文似乎就更关键了。
如果你之前用过 auth.login.title 这种手动设计的键,那么它在一定程度上已经帮助澄清了消息的意图。
但如果翻译看起来是这样:
{
"0MXX5B": "Welcome back!"
}就没那么容易理解了。
但再次强调,为 AI 设计的简单办法,其实就是为人类设计简单办法。我们已经找到了一个解决方案——而且是在30 年前!
GNU gettext 引入了 .po 文件作为消息目录,格式如下:
#. Greeting shown to user when logging back in
#: src/app/(auth)/login/page.tsx
msgid "0MXX5B"
msgstr "Welcome back!"这里包含:可选描述、文件路径引用、ID 以及文本本身。
这正是你现在可以在 next-intl 中使用的:
注意:启用 .po 格式后,也会激活 Turbopack 的一个加载器,它会帮你把本地目录解析为普通的 JavaScript 对象,以便在应用中使用。它的本质就是一个编译目标。
当然,如果你喜欢 JSON 格式,也完全没问题。另外,即将支持自定义格式化器,允许你用自己喜欢的格式包含文件引用和描述信息。
现在,我们准备翻译了
这样,我们就能获得更准确且对用户更友好的翻译。
首先,添加一种新的语言环境:
不需要剪贴复制。当你添加新语言时,next-intl 会自动为所有支持的消息创建空条目。
接下来,你可以在编辑器里翻译这些消息,或者使用基于 AI 的翻译服务,比如 Crowdin:
更多内容请参考 本地化管理文档。
这是什么魔法?
useExtracted 在幕后与 Next.js 有两个衔接点:
1. 用于提取的 Turbopack 加载器
核心是一个加载器:它会针对包含 useExtracted 的源码文件调用处理逻辑。
Note that only Next.js 16+ is supported, since it introduced an optimization for Turbopack that allows to peek inside files to check if they even use the useExtracted hook before actually processing the file.
如果发现 useExtracted,则由 SWC(Next.js 使用的 Rust 编译器)解析文件,然后一个 JavaScript 转换器会将其编译为调用 useTranslations:
import {useTranslations} from 'next-intl';
function InlineMessages() {
const t = useTranslations();
// 取用压缩后的键
return <h1>{t('dPSc42')}</h1>;
}另外,如果这次保存中消息发生了变化,加载器会生成更新后的消息目录(包含源语言和目标语言),从而 Turbopack 的 HMR 会立即反映到运行中的应用上。
如果保存时消息未变,提取过程将直接跳过。
2. 用于消息目录的 Turbopack 加载器
为了支持 .po(以及未来的自定义格式),会启用第二个加载器,高效读取你的目录并将其转换为普通 JavaScript 对象:
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async () => {
const locale = 'en';
// 例如 `{"NhX4DJ": "Hello"}`
const messages = (await import(`../../messages/${locale}.po`)).default;
// ...
});哦对了,Webpack 也支持。
准备好试试了吗?
我本可以继续讲,比如 useExtracted 在测试环境中开箱即用,甚至无需编译,但我想我已经说得够多了。
如果你也对此感兴趣,欢迎反馈。
试试 演示应用,并把你的 反馈 发给我。如果你是早期用户,可以在 next-intl@4.5 版本中试用此功能,但请注意它目前处于实验阶段,功能还会变化。
另外,再次强调:你今天用的 useTranslations API 不会消失。如果你对此满意,请继续使用。个人认为 useExtracted 很有潜力,但时间会检验一切。
随着功能逐渐稳定,如果你是 🌐 learn.next-intl.dev 的会员,你将第一时间收到最佳实践和深入教程。
— Jan
PS:特别感谢 gettext、Lingui、FormatJS、WordPress 和 Zendesk 这类项目与公司——正是他们在“消息提取”领域的开拓性工作推动了这一切。也要感谢 Jan Nicklas 为 next-yak 所做的工作:他推动了 Turbopack 的边界,并分享了他的见解。
相关阅读:
Let’s keep in touch: