处理不受支持的语言代码

问题

某些应用通过 URL 路径(例如,/en//fr/)来判断语言,但用户可以手动输入任意值,如 /xx/about。当该值不在支持的语言列表中时,应用可能会崩溃、显示通用错误,或出现未翻译内容,无法有效引导用户回到正常体验。

解决方案

使用中间件拦截所有传入请求。该中间件会根据一份权威的支持语言列表校验 URL 中的语言代码。如果代码不受支持,请求会在到达应用逻辑之前被重定向到“未找到”页面。

步骤

1. 定义支持的语言

创建一个中心化的配置文件,用于存储有效语言代码(locale)列表。这样可以让中间件和应用其他部分复用该列表。

// i18n.config.ts
export const locales = ['en', 'es', 'fr'];

2. 创建中间件文件

在项目根目录(或 src/ 目录)下新建名为 middleware.ts 的文件。Next.js 会自动检测并在请求时运行该文件。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { locales } from './i18n.config';

export function middleware(request: NextRequest) {
  // Logic will go here in the next step
}

3. 添加校验逻辑

middleware 函数内部,从请求中获取 pathname。我们需要检查路径的第一个片段(例如 en,在 /en/about 中),判断其是否为有效且受支持的语言。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { locales } from './i18n.config';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Handle the root path separately
  // (This will be handled by the next recipe: "Detecting a user's
  // preferred language"). For now, we just let it pass.
  if (pathname === '/') {
    return NextResponse.next();
  }

  // 2. Extract the language code from the path
  const langCode = pathname.split('/')[1];

  // 3. Check if the language code is in our list
  if (locales.includes(langCode)) {
    // Language is valid, continue to the requested page
    return NextResponse.next();
  }

  // 4. If the language is not valid, rewrite to a 404 page
  // This keeps the invalid URL in the browser bar
  const url = request.nextUrl.clone();
  url.pathname = `/404`; // Assumes you have an app/404.tsx file
  return NextResponse.rewrite(url);
}

该逻辑会检查每个请求。如果 URL 是 /fr/aboutlangCodefr,在 locales 中找到,请求继续处理。如果 URL 是 /xx/aboutlangCodexx,未找到,则用户会直接看到 404 页面,应用不会尝试处理无效请求。

4. 配置 middleware 匹配器

为了提高 middleware 的效率,您应指定其运行的路径。我们希望它在页面请求时运行,但跳过静态文件和 API 路由。

config 文件的末尾添加一个 middleware.ts 对象。

// middleware.ts

// ... (the middleware function from above)

export const config = {
  matcher: [
    // Skip all paths that start with:
    // - api (API routes)
    // - _next/static (static files)
    // - _next/image (image optimization files)
    // - favicon.ico (favicon file)
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

此正则表达式指示 middleware 在所有路径上运行,会排除通常用于静态资源或 API 调用的路径。这样可以避免对每个图片、字体或数据请求进行不必要的校验。