代理 / 中间件
next-intl 中间件可以通过 createMiddleware 创建。
它接收一个 routing 配置,并负责:
- 语言环境协商
- 应用相关的重定向和重写
- 为搜索引擎提供备用链接
示例:
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// 匹配所有路径名,除了
// - 以 `/api`、`/trpc`、`/_next` 或 `/_vercel` 开头的路径
// - 包含点(例如 `favicon.ico`)的路径
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};注意: 在 Next.js 16 之前,proxy.ts 被称为 middleware.ts。
语言环境检测
语言环境的协商基于您的路由配置,考虑了您对 localePrefix、domains、localeDetection 以及 localeCookie 的设置。
基于前缀的路由(默认)
想看视频讲解吗?
默认情况下,使用基于前缀的路由 来确定请求的语言环境。
在此情况下,语言环境检测的优先级如下:
- 路径名中包含语言环境前缀(例如
/en/about) - 存在包含先前检测到语言环境的 cookie
- 根据
accept-language请求头 可以匹配的语言环境 - 最后使用
defaultLocale
用户可以通过访问带前缀的路由来更改语言环境。这将优先于保存在 cookie 中的先前匹配语言环境或 accept-language 请求头,并且会更新之前的 cookie 值。
示例流程:
- 用户请求
/,基于accept-language请求头匹配到en语言环境。 - 用户被重定向到
/en。 - 应用渲染
<Link locale="de" href="/">切换到德语</Link>,允许用户切换语言到de。 - 用户点击该链接,发起对
/de的请求。 - 中间件会添加一个 cookie 来记住用户偏好的
de语言环境。 - 用户后来再次请求
/,中间件会基于 cookie 重定向到/de。
哪种算法用于将 accept-language 请求头与可用语言环境匹配?
为了基于应用中的可用选项确定最佳匹配语言环境,中间件使用了 @formatjs/intl-localematcher 的“最佳适配”(best fit)算法。预计此算法比 RFC 4647 中指定的更为保守的“查找”(lookup)算法能提供更好的结果。
举个例子,假设您的应用支持以下语言环境:
en-USde-DE
“查找”算法通过逐步从用户的 accept-language 请求头中移除子标签,直到找到匹配项为止。这意味着,如果用户浏览器发送了 en-GB 作为 accept-language,该算法将找不到匹配,导致使用默认语言环境。
相比之下,“最佳适配”算法会计算用户 accept-language 请求头和可用语言环境之间的“距离”,同时考虑区域信息。因此,该算法能将 en-US 识别为最佳匹配。
基于域名的路由
想看视频讲解吗?
如果您使用了 domains 设置,中间件将会将请求与可用域名匹配,以确定最佳匹配的语言环境。域名从 x-forwarded-host 请求头读取,若无则退回到 host(托管平台通常默认提供这些请求头)。
语言环境检测的优先级如下:
- 路径名包含语言环境前缀(例如
ca.example.com/fr) - cookie 中存储了语言环境,且该语言环境在当前域名上受支持
- 基于
accept-language请求头 匹配该域名支持的语言环境 - 作为保底,使用该域名的
defaultLocale
因为中间件知道所有域名,如果某个域名收到一个不被支持的语言环境请求(例如 en.example.com/fr),它会重定向到支持该语言环境的其他域名。
示例流程:
- 用户请求
us.example.com,基于该域的defaultLocale匹配到en语言环境。 - 应用渲染
<Link locale="fr" href="/">切换到法语</Link>,允许用户切换语言到fr。 - 点击链接后,请求
us.example.com/fr被发起。 - 中间件识别用户想切换至另一个域名,响应重定向到
ca.example.com/fr。
匹配规则配置
中间件旨在仅在页面上运行,而非独立于用户语言环境而提供的任意文件(例如 /favicon.ico)。
一种常用策略是匹配所有不以特定前缀(如 /_next)开头且不包含点(.)的路由,因为通常点表示静态文件。但如果您有某些路径中允许有点(例如 /users/jane.doe),则应显式为这些路径配置匹配规则。
export const config = {
// 匹配条目通过逻辑“或”连接,因此
// 只要匹配其中一条,中间件就会被调用。
matcher: [
// 匹配所有路径名,除了
// - 以 `/api`、`/_next` 或 `/_vercel` 开头的路径
// - 包含点(例如 `favicon.ico`)的路径
'/((?!api|_next|_vercel|.*\\..*).*)',
// 但是,匹配所有 `/users` 下的路径,且可选带语言前缀
'/([\\w-]+)?/users/(.+)'
]
};注意,一些第三方提供商比如 Vercel Analytics 通常使用内部端点,这些端点会重写为外部 URL(例如 /_vercel/insights/view)。请确保将这些请求排除在您的中间件匹配规则外,避免它们被错误重写。
组合其他中间件
调用 createMiddleware 会返回下面类型的函数:
function middleware(request: NextRequest): NextResponse;如果您需要添加额外逻辑,可以在 next-intl 中间件接收请求之前修改请求,或修改响应,甚至基于动态配置创建中间件。
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
export default async function proxy(request: NextRequest) {
// 第一步:使用传入请求(示例)
const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';
// 第二步:创建并调用 next-intl 中间件(示例)
const handleI18nRouting = createMiddleware({
locales: ['en', 'de'],
defaultLocale
});
const response = handleI18nRouting(request);
// 第三步:修改响应(示例)
response.headers.set('x-your-custom-locale', defaultLocale);
return response;
}
export const config = {
// 仅匹配国际化路径名
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};示例:附加重写规则
如果您需要处理除了 next-intl 提供的重写之外的其它重写,可以在 next-intl 中间件执行后有条件地调用 NextResponse.rewrite()。
此示例当设置了特定 cookie 时,会将请求 /[locale]/profile 重写为 /[locale]/profile/new。
import createMiddleware from 'next-intl/middleware';
import {NextRequest, NextResponse} from 'next/server';
import {routing} from './i18n/routing';
const handleI18nRouting = createMiddleware(routing);
export default async function proxy(request: NextRequest) {
let response = handleI18nRouting(request);
// 当设置了 NEW_PROFILE cookie 时进行额外重写
if (response.ok) {
// (不针对错误或重定向)
const [, locale, ...rest] = new URL(
response.headers.get('x-middleware-rewrite') || request.url
).pathname.split('/');
const pathname = '/' + rest.join('/');
if (
pathname === '/profile' &&
request.cookies.get('NEW_PROFILE')?.value === 'true'
) {
response = NextResponse.rewrite(
new URL(`/${locale}/profile/new`, request.url),
{headers: response.headers}
);
}
}
return response;
}
export const config = {
// 仅匹配国际化路径名
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};您可以基于路由配置和实际需求进行自定义。
入门模板(认证、SaaS、多租户)
🌐 learn.next-intl.dev 提供额外的中间件组合入门模板。
包含:
app-router-auth搭配 Better Auth:带有用户设置中语言环境的受保护认证应用。app-router-saas搭配 Better Auth:公共路由上带语言环境前缀,提供登录后访问受保护应用。app-router-tenants:高级组合模式,支持多租户和主页路由,同时支持多语言环境。
无代理 / 无中间件使用(静态导出)
若您使用 Next.js 的静态导出 功能(output: 'export'),则代理 / 中间件不会运行。尽管如此,您仍可使用基于前缀的路由 来实现国际化,但存在若干限制。
静态导出限制:
- 必须使用语言环境前缀(与
localePrefix: 'always'相同) - 服务器端无法协商语言环境(与
localeDetection: false功能对应) - 无法使用
pathnames,因为这些需要服务器端重写 - 需要采用静态渲染
此外,还会受到 Next.js 记录的其它限制影响。
若选择此方案,您可能希望在应用根目录添加一个重定向:
import {redirect} from 'next/navigation';
// 当请求 `/` 时,重定向用户到默认语言环境
export default function RootPage() {
redirect('/en');
}若您添加了如上根页面(app/page.tsx),还需添加根布局(app/layout.tsx),即使只是简单传递 children:
export default function RootLayout({children}) {
return children;
}故障排查
“某个页面的代理 / 中间件未运行。”
要解决此问题,请确认:
- 已正确设置代理 / 中间件,例如在
src/proxy.ts文件中。 - 您的
matcher能正确匹配应用中的所有路由,包括带有点(.)的动态路由(例如/users/jane.doe)。 - 如果您在组合其他中间件,确认中间件函数已正确调用。
- 如果需要静态渲染,请确保遵循静态渲染指南,而非依赖诸如
force-static等 hack 手段。
“尽管路径中包含语言环境前缀,页面内容仍未本地化。”
这很可能是因为您的代理 / 中间件没有在该请求中运行,导致可能启动了 i18n/request.ts 中的回退逻辑。
“无法找到 next-intl 语言环境,因为代理 / 中间件未在此请求运行,且 getRequestConfig 没有返回 locale。”
如果此请求不应运行中间件(例如未启用基于语言环境的路由),则应从 getRequestConfig 显式返回 locale 来避免此错误。
如果此请求应运行中间件,请确认您的中间件配置正确。
请注意,若在运行完 getRequestConfig 后仍无可用语言环境,next-intl 会调用 notFound() 函数以中止页面渲染。建议因此添加not-found 页面。