Next.js Edge Middleware 实现 i18n 自动本地化重定向:edge-middleware/i18n 模板源码解析
【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples
本文围绕当前仓库中的 edge-middleware/i18n 示例,深入讲解如何在 Next.js 中利用 Edge Middleware 结合accept-language请求头与geolocation地理信息,将首页请求自动重写到对应的本地化页面(/[locale]/[country])。读完本文,你将掌握基于 Edge Middleware 的 i18n 重定向完整实现思路、字典数据层设计,以及它与静态生成(SSG)页面协同工作的实战方案。
示例解决的问题
当一个全球化的网站只有一个首页时,不同语言、不同国家的用户访问到的应该是"自己的版本"。传统做法是在服务端渲染时读取请求头做条件渲染,而在本示例中,这一判断被前移到Edge Middleware(边缘中间件):它在请求到达页面之前执行,直接改写 URL 路径,让 Next.js 把请求路由到对应的本地化页面。
从 middleware.ts 的注释可以看到,该示例的设计目标是:
- 仅对首页(路径
/)生效,通过matcher: '/'精确匹配; - 依据
accept-language请求头解析用户首选语言(locale); - 依据 Vercel Edge Network 提供的
geolocation请求头推断用户所在国家(country); - 将路径重写为
/{locale}/{country},由静态页面承接渲染。
其核心价值在于:本地化路由的动态决策发生在边缘层,页面本身可以是完全静态的,兼顾了用户体验(自动适配语言)与性能(静态页面全球 CDN 缓存)。
边缘中间件核心实现
匹配规则与中间件入口
edge-middleware/i18n/middleware.ts 首先通过export const config声明只匹配根路径:
import { geolocation } from '@vercel/functions' import { type NextRequest, NextResponse } from 'next/server' // only run middleware on home page export const config = { matcher: '/', }matcher: '/'意味着中间件仅在用户访问站点首页时触发,其他路径直接放行,避免对全站请求造成不必要的边缘执行开销。
读取请求头并执行重写
中间件默认导出函数的核心逻辑位于 middleware.ts:
export default function middleware(req: NextRequest) { const country = geolocation(req).country?.toLowerCase() || 'us' const locale = req.headers.get('accept-language')?.split(',')?.[0] || 'en-US' // Rewrite the path (`/`) to the localized page (pages/[locale]/[country]) req.nextUrl.pathname = `/${locale}/${country}` return NextResponse.rewrite(req.nextUrl) }两行关键代码的含义:
- 国家推断:
geolocation(req)来自@vercel/functions包,读取 Vercel 边缘网络注入的地理位置请求头(如x-vercel-ip-country),取country字段并转为小写;若无法识别(例如本地开发环境缺少相关请求头),则回退为默认值us。 - 语言推断:
req.headers.get('accept-language')取浏览器发送的语言协商请求头,split(',')[0]拿到客户端优先级最高的语言标签(如zh-CN、en-US、es-ES),缺失时回退为en-US。
随后通过NextResponse.rewrite(req.nextUrl)将 URL 路径原地改写为/{locale}/{country}。注意这里使用的是rewrite(重写)而非 redirect(重定向):浏览器地址栏仍显示/,但内部请求被路由到本地化页面,用户无感知,也不产生额外的网络往返。
本地化字典数据层
中间件只负责"路由决策",实际的文案翻译由一套轻量的字典模块提供,位于 lib 目录下,由三个文件组成。
类型定义
lib/types.ts 定义了字典的 TypeScript 接口,保证所有语言的文案结构一致:
export interface Dictionary { title: string subtitle: string link: string greet: string }四个字段分别对应页面上的标题、副标题、文档链接文案与问候语。
字典常量
lib/constants.ts 以Record<string, Dictionary>的形式维护了五套语言文案:default(兜底英语)、en(英语)、es(西班牙语)、fr(法语)、cn(简体中文):
export const DICTIONARIES: Record<string, Dictionary> = { default: { title: 'i18n Example', greet: 'Hello!, we could not detect your locale so we defaulted to english.', subtitle: 'Localized text based on geolocation headers', link: 'See headers documentation', }, // en / es / fr / cn ... }其中default键承担兜底职责:当请求中的 locale 不在字典范围内时,页面使用该套文案并明确提示用户"未能检测到你的语言设置,已回退到英语"。从仓库实现看,新增语言只需在DICTIONARIES中追加一个键值对,无需改动路由逻辑。
字典获取 API
lib/api.ts 封装了字典的获取接口,屏蔽了数据来源细节:
const api = { dictionaries: { fetch: async (locale): Promise<Dictionary> => DICTIONARIES[locale] || DICTIONARIES['default'], }, } export default apifetch在键不存在时回退到DICTIONARIES['default'],与middleware.ts中的||兜底逻辑形成双保险:中间件层兜底 locale 格式,数据层兜底字典内容。
本地化页面的静态渲染
动态路由结构
页面位于 pages/[locale]/[country].tsx,采用 Next.js Pages Router 的两级动态路由。由于国家数量众多(仓库 public/flags 目录下包含了 250+ 个国家的旗帜 SVG),示例刻意不预生成所有路径,而是使用fallback: 'blocking':
export const getStaticPaths: GetStaticPaths = async () => { // We don't want to specify all possible countries as we get those from the headers return { paths: [], fallback: 'blocking', } }这表示:任何/{locale}/{country}路径首次被访问时,Next.js 会在服务端阻塞式生成该静态页面并缓存,后续访问直接命中缓存——既覆盖了中间件可能产生的任意国家组合,又避免了海量预构建。
按语言取字典
getStaticProps 中通过api.dictionaries.fetch(locale)按路由参数获取对应语言的字典,并设置revalidate: false保持纯静态:
export const getStaticProps: GetStaticProps<unknown, Params> = async ({ params: { country, locale }, }) => { // Get dictionary const dictionary = await api.dictionaries.fetch(locale) return { props: { country, dictionary, locale, }, revalidate: false, } }页面内容渲染
组件接收country、locale、dictionary三个 props,渲染内容包括:字典驱动的标题与副标题、/flags/${country.toLowerCase()}.svg动态加载的国家旗帜,以及展示当前locale值的调试信息块(便于验证中间件的语言推断结果)。布局通过CountryPage.Layout = Layout挂载@vercel/examples-ui提供的页面外壳。
在 pages/_app.tsx 中,应用级布局会读取pageProps.dictionary动态设置页面标题与描述,使浏览器标签页也随语言变化。
目录结构与运行方式
关键文件一览
| 文件 | 职责 |
|---|---|
| middleware.ts | 边缘层 i18n 决策:解析 locale/country 并 rewrite 到本地化路径 |
| lib/constants.ts | 多语言字典常量(含 default 兜底) |
| lib/types.ts | 字典类型定义 |
| lib/api.ts | 字典获取接口封装 |
| pages/[locale]/[country].tsx | 本地化落地页(SSG + blocking fallback) |
| pages/_app.tsx | 全局布局与动态标题 |
| public/flags | 各国旗帜 SVG 资源 |
| package.json | 依赖与脚本(next、@vercel/functions等) |
克隆与本地运行
仓库 README 提供了两种使用方式,其中本地开发推荐通过create-next-app以该模板为蓝本初始化项目:
pnpm create next-app --example https://github.com/vercel/examples/tree/main/edge-middleware/i18n i18n随后进入项目目录启动开发服务器:
pnpm dev需要说明的是:geolocation依赖 Vercel Edge Network 注入的地理位置请求头,本地开发时该请求头通常缺失,因此中间件会回退到默认值us;accept-language则来自本机浏览器/请求设置,可在本地直接验证语言切换效果。若需本地调试国家逻辑,可在中间件中临时注入测试请求头观察重写结果。
生产构建与启动命令定义在 package.json 中:
pnpm build # next build pnpm start # next start云端部署
README 同时提供一键部署方式(点击 Vercel Deploy 按钮即可将模板部署到云端)。仓库根部的 vercel.json 声明了部署相关配置:
{ "buildCommand": "pnpm turbo build", "ignoreCommand": "pnpm dlx turbo-ignore" }即云端构建使用 Turborepo 流水线(对应 turbo.json 中的build/lintpipeline),并通过turbo-ignore实现基于变更的构建跳过。部署到 Vercel 后,geolocation请求头即真实生效,不同地区的用户访问/会自动获得对应国家的本地化页面。
扩展思路与注意事项
- 中间件仅匹配首页:
matcher: '/'限定了本示例只对根路径做 i18n 决策。若要让全站 URL 都携带 locale 前缀(如/es/about),需要扩展 matcher 规则并在中间件中处理相对路径拼接,避免破坏静态资源与 API 路由。 - locale 归一化:示例直接使用
accept-language的首个标签作为字典键,而浏览器可能发送zh-CN、zh-Hans等复合标签;生产环境通常需要做"语言标签 → 支持语言"的归一化映射(如提取主语言码、配置别名表),否则会频繁落入default兜底。 - 改写 vs 重定向:rewrite 对用户透明且无额外跳转,利于 SEO 与体验;若希望地址栏展示规范化的 locale 路径(便于分享链接),可改用
NextResponse.redirect,但要注意避免重定向循环。 - 静态生成与边缘决策的分工:本示例展示了"边缘层做动态路由决策、页面层保持纯静态"的经典组合,
fallback: 'blocking'让任意国家组合都能按需生成并缓存,是这类模式落地时值得复用的关键配置。
总体而言,edge-middleware/i18n 以不足 20 行的中间件代码,完整演示了"请求头驱动 + 边缘重写 + 静态页面承接"的轻量 i18n 方案,是理解 Edge Middleware 在真实业务场景中价值的优秀参考实现。
【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考