useExtracted: i18n 的 Tailwind 版?
2025年11月7日 · 作者 Jan Amann曾经很长一段时间,我对 Tailwind 持怀疑态度。但有一天,我决定 试用五分钟 ——结果一发不可收拾。它的核心理念实在是太务实了,一旦尝试过,就回不去了。
这让我思考:“i18n 的 Tailwind 会是什么样子呢?”
首先,next-intl 表现非常不错,最近刚刚突破了 每周 100 万次下载量(感谢大家!)。这说明有很多人对它现有的使用方式很满意。而且它的核心 API 例如 useTranslations 也肯定会继续存在。
但明天我们还会像今天这样写代码吗?
显然不会。越来越多的 AI 代理融入了我们的工作流,像 Cursor 和 Claude Code 这样的创新也日新月异。
那么,什么样的库适合 AI 优先的开发呢?大概是:一开始对人类开发者就非常友好。Tailwind 并非“为 AI 打造”,而是为简化人类的样式编写而诞生的绝佳方案。如今,它成了代理工具默认的样式选择。
那什么造就了 Tailwind 的独特之处?
设计原则
如果我们看看 Tailwind 的设计,那么一个遵循相同原则的 i18n 解决方案可能是这样的:
- 共置(Colocation):类似 Tailwind 避免管理分离的样式表,添加、更新或删除消息时不应需要手动管理 JSON 消息目录。但消息目录依然可以作为编译目标。
- 局部推理(Local reasoning):生成式 AI 之所以擅长 Tailwind,是因为它只需很小的上下文窗口。读取整个消息目录会导致上下文污染,因此应避免(至少不应在无工具调用时这么做)。
- 无需命名(No naming of things):不必想名字极大提升生产力,因此应尽可能避免手动定义键。
- 清理(Purging):代码经常修改时不应留下死代码。类似 Tailwind 会清除未使用的样式,我们也应自动清除未使用的消息。
- 压缩(Minification):Tailwind 类名极简,消息也应使用压缩后的键,确保包体积最小。
- 既适合原型又适合生产(Prototype-friendly, production-ready):无论是快速原型还是生产应用,Tailwind 都表现一致。同样,应该有单一 API,避免事先对项目大小和复杂度做结构性判断。
- 便于重构(Refactoring-friendly):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 的应用开始,把之前写死的文本标签改成支持翻译的:
不用写键,也不用调 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 的源码文件调用。
注意仅支持 Next.js 16+,因为其引入了 Turbopack 优化,能在处理文件前预先检查是否真的用到了 useExtracted。
如果发现 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
附言:特别感谢像 gettext、Lingui、FormatJS、Wordpress 和 Zendesk 这些项目和公司在消息提取领域的开创性工作。我也感谢 Jan Nicklas 在 next-yak 方面的成果,他推动了 Turbopack 的边界并分享了相关见解。
相关阅读:
Let’s keep in touch: