如何为含[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`);
核心问题
- 能否为路径名实现正确的类型约束(自动补全、拼写检查)?
- 是否有其他方案能更接近需求?
补充信息
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:其他替代方案
使用next-intl库:
该库内置了带类型的路由生成功能,完美适配Next.js App目录与locale路径前缀,和i18next兼容度高,无需手动处理类型提取,还支持动态参数替换。手动封装路由枚举:
将所有逻辑路径定义为字符串常量枚举,然后生成带locale的路径,虽然能获得类型提示,但需要手动维护枚举,不如自动生成的类型灵活,适合路由结构稳定的项目:export enum LogicalRoutes { About = "/about", ProfileWelcome = "/profile/welcome", // 其他路由... } export const i18nPathname = (locale: SupportedLocale, route: LogicalRoutes) => `/${locale}${route}` as Route<`/${SupportedLocale}${typeof route}`>;扩展支持动态参数:
若后续添加更多动态路径参数(如/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
相关产品推荐
相关产品推荐

