如果你的 Next.js 应用已经翻译了,但浏览器标签页、404 页面、表单错误和链接预览仍然是英文,原因几乎总是同一个:这些字符串产生于组件之外,所以 useTranslations 调用从未碰到它们。globalize.now 是 AI 驱动的本地化基础设施,AI Agent 给 App Router 项目添加 i18n 时,我们经常看到这一层只做了一半:可见页面转换了,其余部分却没有。
下面逐一说明这些字符串分别藏在哪里,以及对应的 next-intl 调用。所有示例均遵循当前的 next-intl 文档,获取日期为 2026-10-28。
App Router 应用里,未翻译的字符串藏在哪里?
它们藏在六个位置,这些位置并不是渲染文本的 React 组件。如果编码 Agent 是逐个组件添加 i18n,就会转换可见 UI,却径直跳过这些地方:
generateMetadata中的页面标题和描述。- Server Actions 中的校验错误和结果消息。
not-found.tsx,即 404 页面。error.tsx,即运行时错误页面。- 生成的文件,例如
opengraph-image.tsx、manifest.ts和sitemap.ts。 - 返回文本的路由处理器(Route Handler)。
它们的共同点是:都不会以常规方式渲染在你的 [locale] 布局树里,所以 locale 必须显式传入,或者换一种方式读取。Next.js 16 路由那篇文章 讲的是路由接线,本文讲的则是这套接线够不到的地方。
页面标题和描述怎么翻译?
在 generateMetadata 中调用 next-intl/server 的 getTranslations。页面和布局都可以导出该函数,它是异步的,所以应使用可 await 的 API:
// app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';
export async function generateMetadata() {
const t = await getTranslations('Metadata');
return {title: t('title'), description: t('description')};
}
陷阱出在静态写法上。Agent 常常因为更简短而写成 export const metadata = {title: 'Dashboard'},但常量无法调用翻译函数。把它改成函数形式,并为每种语言在翻译目录中添加相同的键。这部分文本同样面向搜索引擎,所以只翻译应用而不翻译 metadata,对排名几乎没有帮助。
Server Action 和校验消息怎么翻译?
把 locale 传给 action,调用 getTranslations 时同时传入 namespace 和该 locale。Server Action 并不知道页面当前是哪种语言,因此 next-intl 文档要求你自己提供,例如作为参数传入:
'use server';
import {getTranslations} from 'next-intl/server';
export async function subscribe(locale: string, formData: FormData) {
const t = await getTranslations({locale, namespace: 'Subscribe'});
// ...validate, then return t('success') or t('invalidEmail')
}
同一份文档里还有一个注意事项:需要考虑访客在 action 的消息仍显示在屏幕上时切换语言的情况。URL 中有 [locale] 段时,路由变化会自动处理这种情况;没有的话,你可能需要自己清除消息,比如给组件设置 key={locale},让它的状态重置。
对于使用 Zod 的表单校验,文档推荐的做法是 error map。Zod 的每次 safeParse 调用都可以接受一个,而这个 map 可以调用 getTranslations。把它放进一个异步辅助函数,再把结果作为每次解析的 error 选项传入。这样 schema 里只有规则,没有英文句子,AI 生成的表单也不会再把 "Invalid email" 带进拉脱维亚语页面。
404 和错误页面怎么翻译?
把 not-found.tsx 放进 app/[locale]/,再添加一个 catch-all,让它真正被触发。当路由段调用 notFound() 时,Next.js 才会渲染该文件,而未知 URL 本身不会去调用它:
// app/[locale]/[...rest]/page.tsx
import {notFound} from 'next/navigation';
export default function CatchAllPage() {
notFound();
}
next-intl 中间件未匹配的请求根本不带 locale,所以 [locale] 的 404 对它们永远不会生效。文档给出的方案是 app/global-not-found.tsx,Next.js 文档将其标注为实验性功能,它会自行渲染 html 和 body。对于未知的 [locale] 值,文档说明应调用 notFound(),通常写在 i18n/request.ts 中。
error.tsx 则不同。Next.js 要求它是 Client Component,而且它渲染在你的布局之下,因此需要一个至少携带 Error 命名空间的 NextIntlClientProvider。只传这些 messages,不要传整个翻译目录:
// app/[locale]/layout.tsx (excerpt)
const messages = await getMessages();
// ...
<NextIntlClientProvider locale={locale} messages={pick(messages, 'Error')}>
这样 useTranslations('Error') 就能在 error.tsx 里正常工作了。该页面并不覆盖 global-error,如果你有这个文件,请手动检查。
分享图片、manifest 和 sitemap 呢?
它们都需要显式提供 locale,因为都处在布局树之外。对于 opengraph-image.tsx,从 params 读取 locale,再调用 getTranslations({locale, namespace})。如果自定义了 localePrefix,文档提醒生成的路由可能无法访问,因为 Next.js 看不到 proxy(中间件)的重写;解决办法是让 matcher 跳过以 opengraph-image 结尾的路径。
app/manifest.ts 位于 [locale] 之外,next-intl 无法从路径推断 locale。选一个有代表性的(通常是默认语言),显式传入即可。对于 sitemap.ts,添加 alternates.languages 条目,并用导航模块里的异步 getPathname({locale, href}) 构建每个 URL,再加上主机名前缀。next-intl 默认还会发送 link 响应头来声明各语言版本;如果需要控制由 CMS 驱动的 slug,可以改用 sitemap。
路由处理器(Route Handler)怎么处理?
从 search param、路由段或 accept-language 请求头中获取 locale,用 hasLocale(routing.locales, locale) 校验,校验失败就返回 400。然后调用 getTranslations({locale, namespace})。跳过校验意味着任意字符串都会进入翻译目录的查找。
怎么找出 Agent 遗留的英文?
在项目根目录运行下面这些命令。它们刻意写得很粗略:目标是得到一份待人工检查的清单,而不是一个分数。
# 1. Static metadata that cannot be translated
grep -rn "export const metadata" app
# 2. Titles and descriptions already in generateMetadata
grep -rn "title:" app --include=page.tsx --include=layout.tsx
# 3. Server Actions with literal return messages
grep -rln "use server" app src 2>/dev/null | xargs grep -n "message:\|error:"
# 4. Zod schemas with inline messages
grep -rn "message:\s*['\"]" app src 2>/dev/null
# 5. 404 and error files, and whether a catch-all exists
ls app/\[locale\]/not-found.tsx app/\[locale\]/error.tsx "app/[locale]/[...rest]/page.tsx" 2>&1
前四条结果中,任何纯英文字面量都应该进翻译目录。Agent 为什么总会添加硬编码字符串 解释了这种情况为何反复出现:Agent 遵循的是可见的模式,而可见的模式就是组件。如果你用 Copilot,Copilot 的指令文件限制 在这里同样适用。
这对你的翻译目录意味着什么?
它会新增键,而键正是漂移的起点。上面每一项都会往翻译目录里加入新条目,比如 Metadata、Error、Subscribe 和各类校验消息,每一条都需要为你发布的每种语言补上翻译。如果一个项目只翻译一次组件,之后又新增 Server Action 或页面标题,那它正是翻译文件为何会不同步里描述的那个项目。
next-intl 在运行时提供翻译,我们并不取代它。globalize.now 位于更上一层:你在应用里连接仓库,代码库被一次性转换成翻译目录,之后推送任务会在新的目录单元出现时翻译它们,并以待你审阅的变更形式交付。Next.js 集成 介绍了配置方式,而 在已有 i18n 配置中我们会动什么、不动什么 则覆盖了 next-intl 已被手动接入的情况。
从哪里开始
跑一遍这五条 grep,把静态的 metadata 导出改掉,添加 catch-all 路由,再给 error.tsx 配上 provider。然后检查每种语言的翻译目录里是否有新增的命名空间。如果你交付的是 AI 构建的应用,希望转换和翻译有人托管而不是自己维护,vibe coder 页面 有整体介绍,开发者参考 涵盖 Agent 配置,定价 则单独成页。
globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。
免费试用 globalize.now