You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含[locale]路径段的i18n应用实现类型安全路由?

国际化(i18n)Next.js应用中带类型约束的路由生成方案

问题背景

此前通过请求头(Cookie与accept-language)获取locale时,Next.js的实验性特性typedRoutes表现完美。近期决定将locale纳入路径名,希望直接生成含当前locale的路径以减少重定向的往返请求与计算开销,使用i18next作为国际化工具。

当前实现的i18nPathname函数虽无类型错误,但返回类型仅为string,无法获得自动补全与拼写检查支持,和直接使用字符串插值效果一致。

现有代码

// src/i18n/settings.ts
import { InitOptions } from "i18next";
import type { Route } from "next";

import * as config from "@/services/config-service/service-public.mjs";

export const SUPPORTED_LOCALES = ["en-US"] as const;

export const FALLBACK_LOCALE = SUPPORTED_LOCALES[0];

export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];

export const cookieName = config.i18nName;

export const getOptions = () =>
    ({
        debug: config.debugI18n,
        fallbackLng: FALLBACK_LOCALE,
        supportedLngs: SUPPORTED_LOCALES
    }) satisfies InitOptions as InitOptions;

export const i18nPathname = <
    Locale extends SupportedLocale,
    LogicalPathname = string extends Route<`/${Locale}${infer Pathname}`>
        ? Pathname
        : never
>(
    locale: SupportedLocale,
    logicalPathname: LogicalPathname
) => (logicalPathname === "/" ? `/${locale}` : `/${locale}${logicalPathname}`);
// 页面或action中的使用示例
redirect(i18nPathname(locale, "/profile/welcome"));
redirect(`/${locale}/profile/welcome`);

核心问题

  1. 能否为路径名实现正确的类型约束(自动补全、拼写检查)?
  2. 是否有其他方案能更接近需求?

补充信息

App目录结构

src/app/
├── [locale]/
│  ├── _actions/
│  ├── about/
│  ├── design-system/
│  │  ├── cards/
│  │  ├── elevation/
│  │  ├── input/
│  │  ├── interactive/
│  │  ├── palette/
│  │  ├── table/
│  │  └── typography/
│  ├── profile/
│  │  ├── verification/
│  │  │  └── _actions/
│  │  ├── verification-prompt/
│  │  │  └── _actions/
│  │  └── welcome/
│  ├── profiles/
│  │  └── new/
│  │     └── _actions/
│  └── sessions/
│     ├── assistance/
│     │  └── password/
│     │     ├── _actions/
│     │     ├── new/
│     │     │  └── _actions/
│     │     └── prompt/
│     └── new/
│        └── _actions/
└── api/
   └── cron/
      ├── password-resets/
      └── sessions/

Next.js Route类型定义

// .next/types/link.d.ts
export type Route<T extends string = string> =
  __next_route_internal_types__.RouteImpl<T>

type SafeSlug<S extends string> = S extends `${string}/${string}`
  ? never
  : S extends `${string}${SearchOrHash}`
  ? never
  : S extends ''
  ? never
  : S

type StaticRoutes = 
  | `/api/cron/password-resets`
  | `/api/cron/sessions`
type DynamicRoutes<T extends string = string> = 
  | `/${SafeSlug<T>}`
  | `/${SafeSlug<T>}/about`
  | `/${SafeSlug<T>}/profile/verification`
  | `/${SafeSlug<T>}/profile/verification-prompt`
  | `/${SafeSlug<T>}/sessions/assistance`
  | `/${SafeSlug<T>}/sessions/assistance/password/new`
  | `/${SafeSlug<T>}/profile`
  | `/${SafeSlug<T>}/profile/welcome`
  | `/${SafeSlug<T>}/sessions/assistance/password`
  | `/${SafeSlug<T>}/sessions/new`
  | `/${SafeSlug<T>}/sessions/assistance/password/prompt`
  | `/${SafeSlug<T>}/sessions`
  | `/${SafeSlug<T>}/profiles/new`
  | `/${SafeSlug<T>}/design-system/input`
  | `/${SafeSlug<T>}/design-system`
  | `/${SafeSlug<T>}/design-system/palette`
  | `/${SafeSlug<T>}/design-system/table`
  | `/${SafeSlug<T>}/design-system/cards`
  | `/${SafeSlug<T>}/design-system/interactive`
  | `/${SafeSlug<T>}/design-system/typography`
  | `/${SafeSlug<T>}/design-system/elevation`

type RouteImpl<T> = 
  | StaticRoutes
  | SearchOrHash
  | WithProtocol
  | `${StaticRoutes}${SearchOrHash}`
  | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)

计划后续添加除[locale]外更多动态路径参数。


解决方案

问题1:实现带类型约束的路径生成

利用Next.js自动生成的DynamicRoutes内部类型,提取出不带locale前缀的逻辑路径,重构i18nPathname函数以获得类型提示与自动补全:

// src/i18n/settings.ts
import { InitOptions } from "i18next";
import type { Route } from "next";

import * as config from "@/services/config-service/service-public.mjs";

export const SUPPORTED_LOCALES = ["en-US"] as const;
export const FALLBACK_LOCALE = SUPPORTED_LOCALES[0];
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const cookieName = config.i18nName;

export const getOptions = () =>
    ({
        debug: config.debugI18n,
        fallbackLng: FALLBACK_LOCALE,
        supportedLngs: SUPPORTED_LOCALES
    }) satisfies InitOptions as InitOptions;

// 从Next.js自动生成的动态路由类型中提取逻辑路径(移除locale前缀)
type DynamicRoutes = __next_route_internal_types__.DynamicRoutes;
type LogicalPathname = DynamicRoutes extends `/${infer _Locale}/${infer Path}` 
  ? Path extends "" ? "" : `/${Path}` 
  : never;

// 带类型约束的路径生成函数
export const i18nPathname = <T extends LogicalPathname>(
    locale: SupportedLocale,
    logicalPathname: T
): Route<`/${SupportedLocale}${T}`> => {
    return (logicalPathname === "/" ? `/${locale}` : `/${locale}${logicalPathname}`) as Route<`/${SupportedLocale}${T}`>;
};

效果说明:

  • 调用i18nPathname时,logicalPathname参数会自动补全所有合法的逻辑路径(如/profile/welcome)
  • 拼写错误会触发TypeScript类型报错
  • 返回值被识别为合法的Route类型,和直接写字符串插值的路径拥有相同的类型保障

问题2:其他替代方案

  1. 使用next-intl库:
    该库内置了带类型的路由生成功能,完美适配Next.js App目录与locale路径前缀,和i18next兼容度高,无需手动处理类型提取,还支持动态参数替换。

  2. 手动封装路由枚举:
    将所有逻辑路径定义为字符串常量枚举,然后生成带locale的路径,虽然能获得类型提示,但需要手动维护枚举,不如自动生成的类型灵活,适合路由结构稳定的项目:

    export enum LogicalRoutes {
      About = "/about",
      ProfileWelcome = "/profile/welcome",
      // 其他路由...
    }
    
    export const i18nPathname = (locale: SupportedLocale, route: LogicalRoutes) => 
      `/${locale}${route}` as Route<`/${SupportedLocale}${typeof route}`>;
    
  3. 扩展支持动态参数:
    若后续添加更多动态路径参数(如/posts/[id]),可扩展函数支持参数替换,同时保留类型检查:

    export const i18nPathname = <T extends LogicalPathname>(
        locale: SupportedLocale,
        logicalPathname: T,
        params?: Record<string, string | number>
    ): Route<string> => {
        let path = logicalPathname;
        if (params) {
            Object.entries(params).forEach(([key, value]) => {
                path = path.replace(`[${key}]`, String(value)) as T;
            });
        }
        return (path === "/" ? `/${locale}` : `/${locale}${path}`) as Route<string>;
    };
    
    // 使用示例
    i18nPathname(locale, "/posts/[id]", { id: 123 }); // 返回 "/en-US/posts/123",带类型检查
    

内容的提问来源于stack exchange,提问作者ravinggenius

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 23:24:52