多语言路由:用 next-intl 做中英文(en/zh)
YouTeacher 落地页用英文和中文提供同一套页面。它在 Next.js App Router 之上使用 next-intl,并且把语言(locale)放在 URL 路径里,而不是放在 cookie 或请求头里。整套机制挂在一份 locale 定义之上,由五个各司其职的小部件撑起来。
locale 只有一个真相来源
src/i18n/locales.ts 把支持的语言声明为 ['en', 'zh'],把 en 定为默认语言,并导出一个 isValidLocale 类型守卫。其余每个部件都从这里 import,所以「支持哪些语言」只存在于一个地方。
locale 存在于 URL 里
路由是带 locale 前缀、且始终显式的。中间件(src/middleware.ts)用 localePrefix: 'always' 包裹 next-intl/middleware,所以裸的 / 会被重定向到带前缀的路径(/en),每个真实页面都落在 /en/... 或 /zh/... 之下。它的 matcher 只匹配 / 以及以 /en、/zh 开头的路径,静态资源和内部路由不受影响。
App Router 用一个 [locale] 动态段与之对应。src/app/[locale]/layout.tsx 读取这一段,如果它不是已知语言,就调用 notFound()——未知前缀是 404,而不是悄悄回退到默认语言。
消息按请求加载
src/i18n/request.ts 是 next-intl 的请求配置。对每个请求,它取解析出的 requestLocale,在缺失或非法时回退到默认语言,然后动态 import 对应的消息目录(src/i18n/messages/<locale>.json)。这些目录是带命名空间的 JSON——metadata、header、hero 等——所以组件是按命名空间和 key 取译文的,而不是按扁平的字符串 id。
按语言预渲染
layout 导出 generateStaticParams(),遍历 locale 列表,因此两种语言变体在构建时就被预渲染,而不是按需渲染。它调用 setRequestLocale(locale) 为该请求钉住当前语言,随后加载消息并交给客户端的 Providers 包装组件。
generateMetadata 通过 getTranslations 从 metadata 命名空间取页面标题和描述,并设置双语站点所需的 SEO 管线:按语言的 canonical、连接 /en 与 /zh 的 alternates.languages,以及取值为 zh_CN 或 en_US 的 OpenGraph locale。
语言切换器
src/components/Header.tsx 渲染页头里的语言切换。它用 useLocale() 读当前语言、用 usePathname() 读当前路径。切换语言时,它把当前路径上已有的 locale 前缀剥掉,再 push /<新locale><剩余路径>——所以你会停在同一个页面上、只是换成另一种语言,而不会被弹回首页。
为什么是这个形状
把 locale 放进 URL,意味着每种语言变体都是一个真实、可链接、可被索引的 URL——这正是 canonical 与 alternates 元数据要对外声明的东西。把语言列表收在一个文件里,让中间件 matcher、静态参数和切换按钮都从它派生,意味着加入第三种语言基本上只是改数据,而不是重写。