i18n在NextJS中路径国际化跳转方案对比

528 阅读4分钟

📌 背景问题

在使用 Next.js App Router + 多语言(i18n)项目中,默认不会自动将 /a/b 跳转到 /zh/a/b/en/a/b,导致出现 404(not-found)的问题。

📌 原因详解:

1.App Router 模式是基于文件系统路由的,每个页面都必须在 app/ 目录下有对应的目录结构;

  • 例如你创建了 src/app/[locale]/a/b/page.tsx,那么 只会匹配 /zh/a/b/en/a/b 这类路径;
  • 当你访问 /a/b(没有 [locale] 前缀)时,Next.js 无法找到对应的页面 → 抛出 404 not-found

2.Next.js 不会默认将 /a/b 自动转发到 /zh/a/b

  • 它不会猜测用户想要什么语言;
  • i18n 配置(如 i18n.localesdefaultLocale)只影响 构建时国际化支持,并不会做 URL 重写。

3.使用了 App Router 而不是 Pages Router

  • 在旧的 Pages Router 中你可以通过 next.config.jsi18n 配置来开启自动语言前缀;
  • 但 App Router 下,这种行为需要你手动实现(通过中间件、服务器逻辑、客户端跳转等)。

以下是四种方案对比

方案描述优点缺点适用场景
① Fastify 中间件跳转(我现在的方案)server.ts 中拦截所有请求,未带 locale 的路径统一重定向✅ 控制早,支持异步接口✅ 兼容 App Router✅ 可读 cookie、请求头等✅ 适合复杂业务逻辑❌ 需自定义 server.ts,部署较重✅ 适合 SSR、多语言、支持动态语言判断
② Next.js middleware.ts使用中间件做跳转逻辑,运行于 edge✅ 原生支持、无需自定义 server✅ 靠近框架层❌ 无法访问接口(fetch 会被限制)❌ 不适合复杂逻辑(如多商户)✅ 静态多语言站点,语言写死在 cookie
③ 页面级跳转(如 /a/b 渲染 LocaleRedirect为每个路径建个 page.tsx,内部判断并跳转✅ 不影响 SSR 架构,适合纯前端项目❌ 每个路径都要建文件❌ 页面加载后跳转 → 闪烁❌ 结构繁琐,已被淘汰,不推荐
④ Nginx 或 CDN 重写规则在网关层判断路径并重定向✅ 不进应用逻辑、跳转快❌ 无法判断 cookie、header❌ 无法使用远程接口判断语言✅ 静态站点或不依赖用户上下文
⑤ 在 layout.tsx 中使用 redirect()(App Router 特性)layout 文件中判断 locale 并重定向✅ 可访问 cookie/header,结构整洁❌ 只能在 layout 层做❌ 页面已渲染一部分,跳转晚✅ 对 SSR 不敏感、前后端同构简单项目

场景推荐

你的需求推荐方案
✅ 使用 Fastify 接管 server.ts首选方案 ①(你已用)
✅ 想让一切保持 Next 原生、无需服务端✅ 可用方案 ②(middleware.ts)
❌ 项目中路由已大量使用 /zh/...✅ 应用构建时预设 locale 前缀
✅ 不想让用户看到白屏或闪烁跳转✅ 优先用服务端中间件(方案 ① / ②)
❌ 动态读取语言的接口被限制✅ 使用 cookie fallback 方式
✅ 静态页面部署(如 Netlify)✅ 可选方案 ④(Nginx 或 CDN 跳转)

我的方案

我采用的是 “Fastify 插件 + 自定义服务”方案,这是目前在 **支持 SSR + 动态语言判断(如远程 /fePublicInfo)**的前提下最稳妥、灵活、强大的方案。

我的项目优先级以及处理是:

  1. 请求进入 Fastify;
  2. setupLocaleRedirect 拦截,判断 URL 是否缺 locale;
  3. 语言来源优先级:接口返回——fePublicInfo > cookie > 默认值;
  4. 自动重定向;
  5. 后续再执行 Sentry、安全头、权限判断、中间件注册等。

具体实现方法

server.ts注册最早的中间件:

await server.register(setupLocaleRedirect); // 👈 在所有路由处理前执行

核心代码逻辑(setupLocaleRedirect.ts):

server.addHook('onRequest', async (req, reply) => {
  const url = req.url;

  // ✅ 判断当前请求路径是否已经带了语言前缀(如 /zh /en)
  const hasLocale = /^\/(zh|en)(\/|$)/.test(url);

  // ✅ 忽略静态资源
  const isStatic = url.startsWith('/_next') || url.startsWith('/favicon');

  if (hasLocale || isStatic) return;

  // ✅ 默认语言 fallback 为 en
  let detectedLocale: 'zh' | 'en' = 'en';

  // ✅ 尝试从 /fePublicInfo 接口中获取语言
  try {
    const response = await fetch('https://www.xxx.com/fePublicInfo', {
      headers: {
        cookie: req.raw.headers.cookie || '',
      },
    });
    const data = await response.json();
    if (data?.language === 'zh' || data?.language === 'en') {
      detectedLocale = data.language;
    }
  } catch {}

  // ✅ 若接口失败,则尝试从 cookie 取
  const cookieLang = req.cookies?.lan;
  if (!detectedLocale && (cookieLang === 'zh' || cookieLang === 'en')) {
    detectedLocale = cookieLang;
  }

  // ✅ 写入 cookie,供后续前端读取
  reply.setCookie('lan', detectedLocale, {
    path: '/',
    httpOnly: false,
  });

  // ✅ 真正的核心:重定向逻辑
  const redirectTo = `/${detectedLocale}${url}`;
  reply.redirect(302, redirectTo); // 👈 执行跳转
});

✅ 总结它解决的是什么问题?

场景如果没有 setupLocaleRedirect有它之后
用户访问 /a/b❌ 报 404(因为没有 (no-locale)/a/b/page.tsx✅ 自动 302 跳转到 /zh/a/b/en/a/b
用户访问静态资源 /favicon.ico✅ 正常✅ 依然正常,不会被重定向
用户访问 /zh/a/b✅ 正常✅ 正常,不触发重定向
判断语言方式只能靠客户端/中间件读取 cookie✅ 优先用接口、然后 cookie

✅ 为什么不用 Next.js middleware.ts 来跳转?

项目结构是否推荐
Next 原生中间件(middleware.ts)❌ 在 App Router 中功能受限,不支持读取接口
Fastify 插件 + server.ts✅ 支持 header、cookie、远程请求、任意跳转逻辑