- 前端
- 后端
【免费下载链接】next-i18next
The easiest way to translate your NextJs apps.
本指南以仓库中的 app-router-no-locale-path 示例 为主体,系统讲解 next-i18next v16 在 Next.js App Router 下实现"URL 中不出现语言前缀"的国际化方案:地址保持/about而非/en/about,语言由 Cookie 记忆、由 Proxy 中间件检测,并通过useChangeLanguage()完成切换。读完本文,你将掌握getT()/useT()的服务器与客户端渲染用法、createProxy()的 Cookie 检测链路,以及I18nProvider水合与语言同步的完整原理。
一、为什么需要"无语言前缀"方案
在 App Router 场景下,next-i18next 的默认做法(localeInPath: true)会在 URL 中加入语言段,例如/en/about、/de/about,由[lng]动态段承载语言参数。而"无语言前缀"模式(localeInPath: false)选择:
- URL 保持干净:
/about而不是/en/about,对 SEO 与分享链接更友好; - 语言切换基于 Cookie:用户选择的语言写入 Cookie,刷新、跳转后仍被记住;
- 不产生 URL 重定向:Proxy 中间件只负责检测语言并注入请求头,不做 302 跳转。
该示例明确演示了如下核心能力(见 README):
- 服务端组件使用
getT()(来自next-i18next/server); - 客户端组件使用
useT()(来自next-i18next/client); - 基于 Cookie 的语言检测由 proxy.js 中的
createProxy()完成,无 URL 重定向; useChangeLanguage()钩子负责切换语言;I18nProvider负责客户端水合。
二、快速开始
进入示例目录并启动开发服务器(package.json 中定义了dev/build/start等脚本):
npm install npm run dev示例同时提供npm run build与npm start用于生产构建与启动。值得注意的依赖版本组合(package.json):next-i18next ^16.0.10、next ^16.3.1、i18next ^26.4.0、react-i18next ^17.0.12、react ^19.2.8,其中next-i18next16.x 是针对 App Router 与 Next 16proxy.ts的当代实现。
项目结构概览:
examples/app-router-no-locale-path/ ├── i18n.config.js # i18next 核心配置(含 localeInPath: false) ├── proxy.js # createProxy 中间件入口 ├── next.config.js # 纯 Next 配置(无 i18n 相关改动) └── app/ ├── layout.js # 根布局:initServerI18next + I18nProvider ├── page.js # 服务端组件首页 ├── client-page/ # 客户端组件页(useT) ├── second-page/ # 服务端组件页(getT) ├── second-client-page/ # 客户端组件页(useT) ├── components/ # Header / Footer(服务端版 + 客户端版 + LangSelect) └── i18n/locales/ # en / de / it 三语资源三、核心配置:i18n.config.js 逐项解读
i18n.config.js 是整个方案的"总开关":
// 开发模式从磁盘读取,使 reloadOnPrerender 无需重启 dev server 即可感知修改; // 生产模式使用可被打包器追踪的 import(),让语言文件进入 serverless 函数产物。 const resourceLoader = process.env.NODE_ENV === 'development' ? async (language, namespace) => { const fs = await import('fs/promises') const path = await import('path') const content = await fs.readFile( path.resolve(process.cwd(), `app/i18n/locales/${language}/${namespace}.json`), 'utf-8' ) return JSON.parse(content) } : (language, namespace) => import(`./app/i18n/locales/${language}/${namespace}.json`) /** @type {import('next-i18next/proxy').I18nConfig} */ const i18nConfig = { supportedLngs: ['en', 'de', 'it'], fallbackLng: 'en', defaultNS: 'translation', localeInPath: false, ns: ['translation', 'footer', 'client-page', 'second-page', 'second-client-page'], resourceLoader, reloadOnPrerender: process.env.NODE_ENV === 'development', } module.exports = i18nConfig各字段含义与底层行为如下(类型定义见 src/appRouter/types.ts):
| 配置项 | 本示例取值 | 说明 |
|---|---|---|
supportedLngs | ['en', 'de', 'it'] | 支持的语言列表,用于语言检测匹配与<html lang>输出 |
fallbackLng | 'en' | 兜底语言;检测不到任何匹配时使用 |
defaultNS | 'translation' | 默认命名空间;normalizeConfig 中缺省为'common' |
ns | 5 个命名空间 | 全部已知命名空间,缺省为[defaultNS] |
localeInPath | false | 关键开关:false表示 URL 不含语言段,见下文 |
resourceLoader | 双模式函数 | 自定义资源加载器,覆盖默认的localePath文件读取 |
reloadOnPrerender | 仅开发模式 | 每次预渲染前重载资源,实现改语言文件即时生效 |
3.1resourceLoader:开发与生产的双轨设计
该函数是示例中最有工程价值的设计之一,注释中解释了动机:开发模式下 bundler 会跨 HMR 周期缓存动态import()的 JSON 模块,导致首次编辑后热更新失效;因此开发环境直接fs.readFile从磁盘读取,配合reloadOnPrerender: true实现改文件即生效。生产环境则必须使用可被打包器追踪的import(),否则语言文件不会被打进 serverless 函数产物(Vercel / Lambda 等平台上public/下的文件经 CDN 分发,运行时不保证存在于文件系统)。
这与 src/appRouter/server.ts 中createResourceBackend的逻辑一致:若配置了resourceLoader,则包装进i18next-resources-to-backend;否则走默认的文件系统读取,并在 serverless/Edge 环境下抛出引导性错误,提示改用resourceLoader动态导入方案。
3.2localeInPath: false意味着什么
在 src/appRouter/types.ts 中,localeInPath的类型是boolean | 'internal',默认值为true(见 normalizeConfig)。置为false后,Proxy 中间件走"无语言路径模式"分支——不做任何重定向与重写,只把检测到的语言写入自定义请求头,见下一节。
四、createProxy 中间件:Cookie 优先的语言检测
proxy.js 只有十余行:
import { createProxy } from 'next-i18next/proxy' import i18nConfig from './i18n.config' export const proxy = createProxy(i18nConfig) export const config = { matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|site.webmanifest).*)'], }createProxy返回一个标准 Next.js 中间件函数(源码见 src/appRouter/proxy/index.ts),并在 proxy/index.ts 中导出createMiddleware作为兼容别名。matcher负责排除 API 路由、静态资源与站点元数据,避免中间件干扰这些路径。
4.1 语言检测的三级回退
中间件内部按以下顺序解析语言(src/appRouter/proxy/index.ts):
- Cookie 优先:读取
cookieName(默认i18next)对应的值,若存在则匹配到supportedLngs; - Accept-Language 头:无有效 Cookie 时,解析请求的
Accept-Language头(languageDetector.ts 中parseAcceptLanguage负责按 q 值排序,matchLanguage按"精确匹配 → 前缀匹配 →(可选)反向区域匹配"三级规则选择); - fallbackLng 兜底:两者都无效时返回配置的
fallbackLng。
值得注意的细节:Cookie 只会在"语言实际发生变化"时才写入响应(persistLanguage判断req.cookies.get(cookieName)?.value === lng则跳过),注释解释了原因——Next 会把中间件设置的 Cookie 合并进请求的可变 Cookie,任何被修改的 Cookie 都会触发重新验证,若无条件写入会导致每个 Server Action 都重新拉取整页。
4.2 无语言路径模式:只设头、不跳转
关键分支在 src/appRouter/proxy/index.ts:
} else { // No-locale-path mode: don't redirect, just set the header const headers = new Headers(req.headers) headers.set(config.headerName, lng) const response = NextResponse.next({ request: { headers } }) return response }检测结果通过headerName(默认x-i18next-current-language)注入请求头,交由服务端渲染链路消费。测试 createProxy.test.ts 验证了这一行为:请求/about时不触发重定向,且x-i18next-current-language被正确设为en。
五、服务端初始化与根布局:I18nProvider 水合
app/layout.js 串联起服务端初始化与客户端水合:
import './global.css' import { dir } from 'i18next' import { initServerI18next, getT, getResources } from 'next-i18next/server' import { I18nProvider } from 'next-i18next/client' import i18nConfig from '../i18n.config' initServerI18next(i18nConfig) export async function generateMetadata() { const { t } = await getT() return { title: t('title'), description: 'A playground to explore new Next.js app directory features such as nested layouts, instant loading states, streaming, and component level data fetching.' } } export default async function RootLayout({ children }) { const { t, i18n, lng } = await getT() const resources = getResources(i18n) return ( <I18nProvider language={lng} resources={resources} supportedLngs={i18nConfig.supportedLngs}> <html lang={lng} dir={dir(lng)}> <head /> <body> {children} </body> </html> </I18nProvider> ) }几个关键点:
initServerI18next(i18nConfig)在模块作用域执行一次,初始化模块级共享实例;源码 src/appRouter/server.ts 显示重复调用只会替换配置、保留共享实例。若想避开模块求值顺序问题,可改用createServerI18next(config)返回绑定配置的{ getT, getResources, generateI18nStaticParams }(src/appRouter/server.ts)。getT()返回{ t, i18n, lng }(GetTResult,见 types.ts),lng是本次请求解析出的语言。getResources(i18n)从共享实例的 store 抽取全部已加载资源,序列化进 HTML 供客户端水合;源码 server.ts 支持按namespaces与languages参数裁剪,多语言项目可用它控制首屏载荷。I18nProvider接收language、resources、supportedLngs三个属性完成客户端实例水合;dir(lng)用于设置文档方向(如阿拉伯语 RTL)。- 无语言前缀模式下没有
params.lng,因此getT()的语言来源是请求头(而非 URL 参数),这正是 Proxy 中间件注入的x-i18next-current-language。
5.1 服务端检测的完整顺序
src/appRouter/server.ts 的detectLanguage展示了服务端语言解析顺序:
- 根路由参数(
next/root-params,Next 16.3+):若当前布局存在[lng]/[locale]段则优先取用,且读取 root params 不破坏静态预渲染(区别于会 opt-out 的headers()/cookies()); - 自定义请求头:读取 Proxy 注入的
headerName; - Cookie:读取
cookieName并校验是否在supportedLngs中(支持nonExplicitSupportedLngs的区域匹配); - fallbackLng 兜底。
该函数以 React 的cache()包裹,同一渲染周期内多次getT只做一次检测。
六、服务端组件翻译:getT
首页 app/page.js 是典型的服务端组件用法:
import Link from 'next/link' import { Trans } from 'react-i18next/TransWithoutContext' import { Header } from './components/Header' import { Footer } from './components/Footer' import { getT } from 'next-i18next/server' export default async function Page() { const { t, i18n } = await getT() return ( <> <main> <Header heading={t('h1')} /> <h2> <Trans t={t} i18n={i18n} i18nKey="welcome"> Welcome to Next.js 16 <small>with the App Router</small> and next-i18next </Trans> </h2> ... </main> <Footer/> </> ) }要点:
getT()默认使用defaultNS(translation),也可传命名空间参数,如await getT('second-page');- 服务端组件中无法使用依赖 Context 的
<Trans>,因此示例采用react-i18next/TransWithoutContext的Trans,并显式传入t与i18n; t('welcome')对应的资源中内嵌 HTML 占位符(见 en/translation.json 的"welcome": "Welcome to Next.js 16 <1>with the App Router</1> and next-i18next"),<Trans>的默认子节点与i18nKey配合完成内联元素映射。
generateMetadata同样可以调用await getT()取t('title'),服务端页面(如 second-page/page.js)与布局(second-page/layout.js)的用法完全一致。
七、客户端组件翻译:useT
app/client-page/page.js 演示客户端组件与复数翻译:
'use client' import Link from 'next/link' import { useT } from 'next-i18next/client' import { Header } from '../components/Header' import { Footer } from '../components/Footer/client' import { useState } from 'react' export default function Page() { const { t } = useT('client-page') const [counter, setCounter] = useState(0) return ( <> <main> <Header heading={t('h1')} /> <p>{t('counter', { count: counter })}</p> ... </main> <Footer path="/client-page" /> </> ) }对应资源 en/client-page.json 展示了 i18next 复数规则(counter_zero/counter_one/counter_other),t('counter', { count })会自动选择正确形式。useT源码见 src/appRouter/client.tsx:它封装react-i18next的useTranslation,并在localeInPath模式下同步 URL 参数语言;无语言路径模式下则直接使用I18nProvider设定好的语言。
八、语言切换:useChangeLanguage + LangSelect
语言切换器 LangSelect.js 是客户端组件:
"use client" import { useChangeLanguage } from 'next-i18next/client' import { useRouter } from "next/navigation" // languages 来自 i18next 实例(i18n.options.supportedLngs),而不是导入 i18n.config: // 该配置是服务端代码(携带 resourceLoader),导入它会把它带进浏览器 bundle。 export const LangSelect = ({ currentLanguage, languages = [] }) => { const router = useRouter() const changeLanguage = useChangeLanguage() // i18next 会把调试语言 'cimode' 追加进 supportedLngs const options = languages.filter((lang) => lang !== 'cimode' && lang !== currentLanguage) const handleChangeLanguage = (e, lang) => { e.preventDefault() changeLanguage(lang) router.refresh() } return ( <div> { options.map((lang) => { return ( <span key={lang}> <button onClick={(e) => handleChangeLanguage(e, lang)} type="button">{lang}</button> </span> ) }) } </div> ) }注释揭示了两个重要细节:
- 语言列表来自
i18n.options.supportedLngs(由FooterBase传入i18n.options.supportedLngs,见 FooterBase.js),而非直接导入i18n.config——后者是携带resourceLoader的服务端代码,导入会污染浏览器 bundle; - 切换流程是
changeLanguage(lang)写 Cookie + 更新 i18next 实例,随后router.refresh()触发服务端重渲染,让新语言的资源随下一次响应送达。
8.1 useChangeLanguage 的底层实现
源码 src/appRouter/client.tsx 展示了该钩子的完整逻辑:
export function useChangeLanguage(cookieName = 'i18next', cookieOptions: CookieOptions = {}) { const { i18n } = useTranslation() const router = useRouter() const { domain, secure, sameSite = 'lax', path = '/', maxAge = 365 * 24 * 60 * 60 } = cookieOptions return useCallback(async (newLng: string) => { let cookie = `${cookieName}=${newLng};path=${path};max-age=${maxAge};SameSite=${sameSite[0].toUpperCase()}${sameSite.slice(1)}` if (domain) cookie += `;domain=${domain}` if (secure) cookie += ';Secure' document.cookie = cookie if (i18n.getDataByLanguage(newLng)) await i18n.changeLanguage(newLng) router.refresh() }, [i18n, router, cookieName, domain, secure, sameSite, path, maxAge]) }要点:
- 默认 Cookie 名
i18next,maxAge一年,与 Proxy 的cookieMaxAge默认值(365 * 24 * 60 * 60秒,见 config.ts)对齐; cookieOptions(domain/secure/sameSite/path/maxAge)与 Proxy 的cookieOptions使用同一类型(types.ts),保证两边写 Cookie 的作用域一致;- 只有当目标语言资源已存在于实例 store 时才立即
changeLanguage,否则交给I18nProvider在router.refresh()送回资源后再切换——这是为了避免资源未到时语言被解析回 fallback 的竞态,对应的回归测试见 client.ondemand.test.tsx。
8.2 I18nProvider 的语言同步
src/appRouter/client.tsx 中I18nProvider的useEffect在language/resources变化时(导航或router.refresh()之后)增量合并服务端送来的资源包,并在有新资源或语言变化时调用instance.changeLanguage(language),确保切换后客户端立即渲染新语言而非 fallback。
九、共享 Footer 的服务端/客户端双实现
示例用 Footer/index.js(服务端版)与 Footer/client.js(客户端版)演示了"同一组件两种运行时"的惯用模式:
// index.js —— 服务端版(服务端组件页使用) import { FooterBase } from './FooterBase' import { getT } from 'next-i18next/server' export const Footer = async ({ path }) => { const { t, i18n, lng } = await getT('footer') return <FooterBase t={t} i18n={i18n} lng={lng} path={path} /> }// client.js —— 客户端版(客户端组件页使用) 'use client' import { FooterBase } from './FooterBase' import { useT } from 'next-i18next/client' export const Footer = ({ path }) => { const { t, i18n } = useT('footer') return <FooterBase t={t} i18n={i18n} path={path} attachOnClick/> }两版共用纯展示组件FooterBase,差别仅在翻译来源(getTvsuseT)与命名空间(footer)。footer.json中的description键注释道明"非页面组件需要自己的命名空间"(en/footer.json),languageSwitcher键则通过<Trans>的{{lng}}插值输出"Switch from X to: "。页面侧只需按运行时选../components/Footer或../components/Footer/client导入即可。
十、与 localeInPath 模式的对比与扩展
本示例把localeInPath置为false,是最"干净"的 URL 形态。若对照 app-router-simple 示例(同样配置supportedLngs、fallbackLng、defaultNS、resourceLoader、reloadOnPrerender,但走默认的[lng]动态段),可以总结三种形态(见 types.ts):
localeInPath | URL 形态 | 语言来源 | 典型场景 |
|---|---|---|---|
true(默认) | /en/about,[lng]段 | URL 参数 | 多语言 SEO,每语言独立 URL |
'internal' | /about,内部重写到/{lng}/about | Proxy 检测 + 内部重写 | 保持干净 URL 同时让每语言路由可静态预渲染 |
false(本示例) | /about,无重写 | 请求头(Cookie → Accept-Language → fallback) | 纯 Cookie 记忆,URL 完全不含语言 |
'internal'模式在 proxy/index.ts 中通过NextResponse.rewrite把干净 URL 重写到检测语言对应的内部路径,浏览器地址栏不变。hideDefaultLocale则适用于localeInPath: true时省略默认语言前缀的形态。三者的语言检测逻辑(Cookie → Accept-Language → fallback)完全一致,只是对 URL 的处理策略不同。
十一、配置参数速查表
以下参数均可通过I18nConfig配置(完整定义见 src/appRouter/types.ts),默认值见 normalizeConfig:
| 参数 | 默认值 | 作用 |
|---|---|---|
supportedLngs | 必填(或i18n.locales) | 支持语言列表 |
fallbackLng | 必填(或i18n.defaultLocale) | 兜底语言 |
defaultNS | 'common' | 默认命名空间 |
ns | [defaultNS] | 已知命名空间列表 |
localeInPath | true | false或'internal'时 URL 不含语言 |
localeParamName | 'lng' | 语言路由段名称(如'locale'对应app/[locale]) |
hideDefaultLocale | false | 默认语言不加前缀(配合localeInPath: true) |
localePath | '/locales' | 语言文件相对public/的路径 |
localeStructure | '{{lng}}/{{ns}}' | 目录结构模板 |
localeExtension | 'json' | 语言文件扩展名 |
resourceLoader | 内置文件/路径加载 | 自定义资源加载函数 |
resources | 无 | 预打包资源(提供后跳过动态加载) |
cookieName | 'i18next' | 语言 Cookie 名 |
headerName | 'x-i18next-current-language' | 传递给服务端组件的语言头 |
cookieMaxAge | 365*24*60*60秒 | Cookie 有效期 |
persistCookie | true | 是否写 Cookie(他人管理系统持有时设false) |
cookieOptions | {} | Cookie 附加属性(domain/secure/sameSite/path/maxAge) |
ignoredPaths | ['/api','/_next','/static'] | 中间件忽略的路径前缀 |
basePath | 无 | 中间件只处理此前缀下的请求(混合路由场景) |
reloadOnPrerender | 无 | 开发模式渲染前重载资源 |
nonExplicitSupportedLngs | false | 支持'en'匹配'en-US'的区域回退 |
use | [] | 额外 i18next 插件(如 http/locize backend) |
i18nextOptions | {} | 合并进 init 的其余 i18next 选项 |
十二、测试验证与工程实践
仓库为这套机制提供了双端测试证据:
- Proxy 行为:src/appRouter/proxy/createProxy.test.ts 中的
'works in no-locale-path mode'用例断言请求/about时不触发重定向、x-i18next-current-language头正确写入;'falls back to default language when cookie has unsupported value'验证了 Cookie 值不合法时回退到 fallback;'skips ignored paths'与'skips static file extensions'验证中间件的白名单行为。 - 客户端水合:src/appRouter/client.ondemand.test.tsx 用真实 i18next + react-i18next(无 mock)复现了
router.refresh()后新语言资源到达、客户端从 fallback 恢复为正确语言的完整流程。
实际工程中还有两个可借鉴点:一是LangSelect中过滤cimode(i18next 追加的调试语言)避免渲染多余按钮;二是语言列表取自运行中的 i18next 实例而非直接导入服务端配置文件,保证浏览器 bundle 不被resourceLoader污染。
结语
无语言前缀方案让 URL 保持简洁,同时通过"Proxy 检测(Cookie → Accept-Language → fallback)+ 请求头传递 +useChangeLanguage写 Cookie 并router.refresh()"的闭环,实现了完全基于 Cookie 的语言记忆与切换。对照 app-router-no-locale-path 示例 的页面结构(服务端首页、客户端计数页、第二页、第二客户端页)与 i18n/locales 下的 en/de/it 三语资源,你可以快速落地一套"URL 无语言前缀"的 App Router 国际化应用;需要每语言独立 URL 做 SEO 时,仅需将localeInPath改回true(参考 app-router-simple),或改用'internal'兼得两者。
- 前端
- 后端
【免费下载链接】next-i18next
The easiest way to translate your NextJs apps.
相关推荐
3步快速掌握QQ聊天记录备份:新手也能轻松导出的完整指南
3步快速掌握QQ聊天记录备份:新手也能轻松导出的完整指南 你是否曾为丢失珍贵的QQ聊天记录而烦恼?或者需要备份重要的对话却不知从何下手?QQ聊天记录承载着我们的
数据工程桌面应用Mac Mouse Fix:如何把十美元鼠标用出 Apple 触控板的手感
Mac Mouse Fix:如何把十美元鼠标用出 Apple 触控板的手感 Mac Mouse Fix 是一款面向 macOS 的开源鼠标增强应用,核心目标很直
桌面应用系统编程告别语言障碍:react-i18next多语言切换实战指南
告别语言障碍:react i18next多语言切换实战指南 你是否还在为React应用的国际化问题烦恼?用户投诉界面语言混乱?运营反馈多地区推广受阻?本文将带你
前端国际化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考