PAYATHON 2026

Next.js 如何无闪烁地重定向旧版 -es URL 并初始化 i18next?

数据小陈

可以在页面渲染前处理旧 URL:在 Middleware 或 Proxy 中返回重定向响应,并通过 Cookie 保存 es。目标页面在服务端读取 Cookie,把语言传给 TranslationsProvider,在首次渲染前准备好翻译资源。

这样可以避免进入 not-found.tsx 后再跳转。要消除默认语言切换到西班牙语时的闪烁,还需要让 Provider 使用正确的初始语言,不能继续依赖目标组件挂载后的 changeLanguage()。

为什么当前实现会闪烁?

现有流程会先渲染 Not Found 组件,再运行 useEffect、写入 sessionStorage 并跳转。目标页面随后通过另一个 Effect 切换语言,因此有两个可能出现闪烁的阶段:404 空白页面短暂显示,以及目标页面先显示默认语言。

sessionStorage 只能在浏览器中访问,服务端无法用它决定首次渲染的语言。将它放入 Effect 是避免服务端访问浏览器 API 的常见做法,但事件处理函数也能访问浏览器 API。这里需要调整语言信息的传递位置。

Next.js 16 将 middleware.ts 文件约定更名为 proxy.ts。下面使用 Next.js 16 的写法;使用此前 Middleware 文件约定的项目,可以将文件名和导出函数名分别改为 middleware.ts、middleware。文件应与 app 目录同级;使用 src/app 时,放在 src 下。Proxy 在路由渲染前执行,这里不需要访问 i18next 上下文。Next.js Proxy 文档

// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const ES_AUTO_REDIRECTS = new Map<string, string>([
  ['/page1-es', '/page1'],
  ['/page2-es', '/page2'],
]);

export function proxy(request: NextRequest) {
  const target = ES_AUTO_REDIRECTS.get(request.nextUrl.pathname);

  if (!target) {
    return NextResponse.next();
  }

  // 使用核心路径,不添加语言参数。
  const response = NextResponse.redirect(
    new URL(target, request.url),
    307,
  );

  response.cookies.set('app-locale', 'es', {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure: request.nextUrl.protocol === 'https:',
  });

  response.headers.set('Cache-Control', 'private, no-store');
  return response;
}

export const config = {
  matcher: ['/page1-es', '/page2-es'],
};

浏览器收到响应后保存 Cookie,再请求 /page1 或 /page2。语言通过 Cookie 传递,地址栏无需增加参数。NextResponse 支持构造重定向响应并设置响应 Cookie。NextResponse 文档

这个示例只匹配明确列出的旧地址,其他未知路径可以继续交给 not-found.tsx 处理。

2. 在目标页面读取语言,而不是挂载后再切换

下面是 /page1 的页面逻辑。TranslationsProvider、App 和 i18nNamespaces 沿用项目现有定义与导入,/page2 采用相同处理。

// app/page1/page.tsx
import { cookies } from 'next/headers';

// 保留项目中 TranslationsProvider、App、i18nNamespaces 的导入。

export default async function Page1() {
  const cookieStore = await cookies();
  const storedLocale = cookieStore.get('app-locale')?.value;
  const locale = storedLocale === 'es' ? 'es' : 'en';

  return (
    <TranslationsProvider
      namespaces={i18nNamespaces}
      locale={locale}
    >
      <App page="page1" />
    </TranslationsProvider>
  );
}

这里以 en 作为默认语言,结果只有 en 和 es 两种。若项目已有用户语言偏好,需要按实际规则合并,避免直接覆盖。

Server Component 可以通过 cookies() 读取请求 Cookie;示例使用 await cookies()。按请求读取 Cookie 会影响页面的渲染和缓存策略,不能再假定所有用户都能共享同一份静态语言页面。Next.js cookies 文档

原代码中的 locale={params.locale} 也需要统一处理。如果路由参数仍决定语言,而 Cookie 只在组件挂载后覆盖它,首屏语言仍可能不一致。

3. 确保 Provider 首次渲染就使用西班牙语

仅把 es 传给 Provider,还不足以保证没有语言闪烁。题目没有提供 TranslationsProvider 的实现,需要检查它是否满足以下条件:

  • 使用传入的 locale 初始化 i18next。
  • 在渲染翻译内容前,已加载该语言所需的 namespace 资源。
  • 服务端输出与客户端 hydration 使用相同的语言和初始翻译资源。

初始化逻辑可以按下面的顺序组织。这里的 resources 表示项目已经加载好的翻译资源,加载方式沿用现有实现:

import { createInstance } from 'i18next';

const i18n = createInstance();

await i18n.init({
  lng: locale,
  fallbackLng: 'en',
  ns: i18nNamespaces,
  defaultNS: 'appTranslations',
  resources,
});

// 初始化完成后,再使用该实例渲染翻译内容。

这段代码说明初始化顺序,并非完整的 TranslationsProvider 实现。若服务端参与翻译渲染,应按请求创建独立实例,避免并发请求互相修改语言。跨 Server Component 与 Client Component 边界时,应传递可序列化的语言和资源数据,不要直接传递 i18next 实例。

i18next 要求等待初始化完成后再使用翻译函数;react-i18next 的 SSR 文档也说明了向客户端传递初始语言和翻译资源的方式。i18next API 文档、react-i18next SSR 文档

完成初始化调整后,可以删除目标组件中读取 sessionStorage 并调用 changeLanguage() 的启动逻辑,避免再次覆盖语言。

使用时需要注意什么?

示例中的 Cookie 是语言偏好,会随后续请求继续发送,通常也会在同一浏览器的标签页之间共享。它与原来按标签页存储、读取后删除的 sessionStorage 标记并不完全等价。如果只希望影响一次跳转,需要另外设计消费与清除流程;不要在 Server Component 渲染期间直接删除 Cookie。由于示例设置了 httpOnly: true,后续语言切换若要更新该 Cookie,应通过 Route Handler 或 Server Action 完成。

示例使用 307 并禁止缓存这次重定向响应,以便每次访问旧地址时执行语言设置。若要改成永久重定向,需要考虑浏览器和 CDN 缓存对 Set-Cookie 流程的影响。

如果项目已有国际化 Middleware 或 Proxy,应把旧地址映射合并进去,并检查它是否会再次给 /page1 添加语言前缀。若站内仍通过 <Link> 指向这些有语言设置副作用的旧地址,也应考虑关闭这些链接的预取。

这套方案需要能够处理请求的 Next.js 服务端或部署平台支持。纯静态导出需要在托管层另行实现重定向与语言传递。它能消除当前由 404 Effect 和挂载后切换语言引起的闪烁,但网络等待、loading.tsx 或翻译资源尚未准备好造成的加载状态,仍需分别处理。

备注:内容仅供参考。