Skip to content
文档路由代理 / 中间件

代理 / 中间件

next-intl 中间件可以通过 createMiddleware 创建。

它接收一个 routing 配置,并负责:

  1. 语言环境协商
  2. 应用相关的重定向和重写
  3. 为搜索引擎提供备用链接

示例:

proxy.ts
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

语言环境检测

语言环境的协商基于您的路由配置,考虑了您对 localePrefixdomainslocaleDetection 以及 localeCookie 的设置。

基于前缀的路由(默认)

想看视频讲解吗?

默认情况下,使用基于前缀的路由 来确定请求的语言环境。

在此情况下,语言环境检测的优先级如下:

  1. 路径名中包含语言环境前缀(例如 /en/about
  2. 存在包含先前检测到语言环境的 cookie
  3. 根据 accept-language 请求头 可以匹配的语言环境
  4. 最后使用 defaultLocale

用户可以通过访问带前缀的路由来更改语言环境。这将优先于保存在 cookie 中的先前匹配语言环境或 accept-language 请求头,并且会更新之前的 cookie 值。

示例流程:

  1. 用户请求 /,基于 accept-language 请求头匹配到 en 语言环境。
  2. 用户被重定向到 /en
  3. 应用渲染 <Link locale="de" href="/">切换到德语</Link>,允许用户切换语言到 de
  4. 用户点击该链接,发起对 /de 的请求。
  5. 中间件会添加一个 cookie 来记住用户偏好的 de 语言环境。
  6. 用户后来再次请求 /,中间件会基于 cookie 重定向到 /de
哪种算法用于将 accept-language 请求头与可用语言环境匹配?

为了基于应用中的可用选项确定最佳匹配语言环境,中间件使用了 @formatjs/intl-localematcher 的“最佳适配”(best fit)算法。预计此算法比 RFC 4647 中指定的更为保守的“查找”(lookup)算法能提供更好的结果。

举个例子,假设您的应用支持以下语言环境:

  1. en-US
  2. de-DE

“查找”算法通过逐步从用户的 accept-language 请求头中移除子标签,直到找到匹配项为止。这意味着,如果用户浏览器发送了 en-GB 作为 accept-language,该算法将找不到匹配,导致使用默认语言环境。

相比之下,“最佳适配”算法会计算用户 accept-language 请求头和可用语言环境之间的“距离”,同时考虑区域信息。因此,该算法能将 en-US 识别为最佳匹配。

基于域名的路由

想看视频讲解吗?

如果您使用了 domains 设置,中间件将会将请求与可用域名匹配,以确定最佳匹配的语言环境。域名从 x-forwarded-host 请求头读取,若无则退回到 host(托管平台通常默认提供这些请求头)。

语言环境检测的优先级如下:

  1. 路径名包含语言环境前缀(例如 ca.example.com/fr
  2. cookie 中存储了语言环境,且该语言环境在当前域名上受支持
  3. 基于 accept-language 请求头 匹配该域名支持的语言环境
  4. 作为保底,使用该域名的 defaultLocale

因为中间件知道所有域名,如果某个域名收到一个不被支持的语言环境请求(例如 en.example.com/fr),它会重定向到支持该语言环境的其他域名。

示例流程:

  1. 用户请求 us.example.com,基于该域的 defaultLocale 匹配到 en 语言环境。
  2. 应用渲染 <Link locale="fr" href="/">切换到法语</Link>,允许用户切换语言到 fr
  3. 点击链接后,请求 us.example.com/fr 被发起。
  4. 中间件识别用户想切换至另一个域名,响应重定向到 ca.example.com/fr

匹配规则配置

中间件旨在仅在页面上运行,而非独立于用户语言环境而提供的任意文件(例如 /favicon.ico)。

一种常用策略是匹配所有不以特定前缀(如 /_next)开头且不包含点(.)的路由,因为通常点表示静态文件。但如果您有某些路径中允许有点(例如 /users/jane.doe),则应显式为这些路径配置匹配规则。

proxy.ts
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 中间件接收请求之前修改请求,或修改响应,甚至基于动态配置创建中间件。

proxy.ts
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

proxy.ts
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'),则代理 / 中间件不会运行。尽管如此,您仍可使用基于前缀的路由 来实现国际化,但存在若干限制。

静态导出限制:

  1. 必须使用语言环境前缀(与 localePrefix: 'always' 相同)
  2. 服务器端无法协商语言环境(与 localeDetection: false 功能对应)
  3. 无法使用 pathnames,因为这些需要服务器端重写
  4. 需要采用静态渲染

此外,还会受到 Next.js 记录的其它限制影响。

若选择此方案,您可能希望在应用根目录添加一个重定向:

app/page.tsx
import {redirect} from 'next/navigation';
 
// 当请求 `/` 时,重定向用户到默认语言环境
export default function RootPage() {
  redirect('/en');
}

若您添加了如上根页面(app/page.tsx),还需添加根布局(app/layout.tsx),即使只是简单传递 children

app/layout.tsx
export default function RootLayout({children}) {
  return children;
}

故障排查

“某个页面的代理 / 中间件未运行。”

要解决此问题,请确认:

  1. 已正确设置代理 / 中间件,例如在 src/proxy.ts 文件中。
  2. 您的 matcher 能正确匹配应用中的所有路由,包括带有点(.)的动态路由(例如 /users/jane.doe)。
  3. 如果您在组合其他中间件,确认中间件函数已正确调用。
  4. 如果需要静态渲染,请确保遵循静态渲染指南,而非依赖诸如 force-static 等 hack 手段。

“尽管路径中包含语言环境前缀,页面内容仍未本地化。”

这很可能是因为您的代理 / 中间件没有在该请求中运行,导致可能启动了 i18n/request.ts 中的回退逻辑。

“无法找到 next-intl 语言环境,因为代理 / 中间件未在此请求运行,且 getRequestConfig 没有返回 locale。”

如果此请求不应运行中间件(例如未启用基于语言环境的路由),则应从 getRequestConfig 显式返回 locale 来避免此错误。

如果此请求应运行中间件,请确认您的中间件配置正确

请注意,若在运行完 getRequestConfig 后仍无可用语言环境,next-intl 会调用 notFound() 函数以中止页面渲染。建议因此添加not-found 页面