路由配置
defineRouting
可在 中间件 和 导航 API 之间共享的路由配置,可以通过 defineRouting 函数进行定义。
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// 支持的所有语言列表
locales: ['en', 'de'],
// 当没有匹配到语言时使用
defaultLocale: 'en'
});根据你的路由需求,你可能还想考虑其他设置——请看下文。
如果在构建时语言未知怎么办?
如果你构建的是可在运行时添加或移除语言的应用,可以为中间件每次请求动态提供路由配置。
对应的导航 API 你可以在这种情况下省略 locales 参数来创建。
不过,如果你定义了其他路由配置,请确保中间件和导航 API 之间保持同步。
localePrefix
默认情况下,你 app 的路径名会在一个前缀下可用,该前缀与目录结构匹配(例如 /en/about → app/[locale]/about/page.tsx)。不过,你可以通过配置 localePrefix 设置,来选择性地移除前缀或针对不同语言自定义前缀。
了解更多:
localePrefix: 'always'(默认)
默认情况下,路径名总会以语言前缀开始(例如 /en/about)。
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'always'
});localePrefix: 'as-needed'
如果你希望默认语言无前缀(例如 /about),而其他语言保持前缀(例如 /de/about),可以这样配置:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'as-needed'
});注意:
- 使用此路由策略时,确保你的
matcher可以检测未加前缀的路径名。 - 中间件默认会设置一个cookie来记住用户的语言偏好。如果路径中没有显式语言前缀,可能会基于 cookie 值的语言检测将用户重定向到匹配到的最新语言(例如
/→/de)。 - 如果请求了多余的语言前缀如
/en/about,中间件会自动重定向到无前缀版本/about。这在你从其他语言重定向并想先更新可能的 cookie 值时很有用(例如<Link />依赖此机制)。
localePrefix: 'never'
如果你想基于用户设置将语言提供给 next-intl,可以考虑完全不使用基于语言的路由。
当然,你也可以配置中间件使 URL 永远不显示语言前缀,适用场景包括:
- 使用基于域名的路由,且每个域只有一种语言
- 使用 cookie 判断语言,同时启用静态渲染
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'never'
});此时,所有语言的请求会在内部重写为具有语言前缀,但 URL 不显示。你仍需将所有页面放在 [locale] 文件夹内,以便路由能接收 locale 参数。
注意:
- 使用此路由策略时,确保你的
matcher能检测未加前缀的路径名。 - 由于 URL 可能不具备语言唯一性,该模式会禁用alternate links。因此请考虑自行添加,或托管一个通过
alternates关联本地化页面的sitemap。 - 你可以考虑将语言 cookie 的
maxAge属性设为较长时间,以便跨会话记住用户偏好。
prefixes
如果希望自定义用户看到的前缀,可以提供按语言映射的前缀:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en-US', 'de-AT', 'zh'],
defaultLocale: 'en-US',
localePrefix: {
mode: 'always',
prefixes: {
'en-US': '/us',
'de-AT': '/eu/at'
// (/zh 会照原样使用)
}
}
});注意:
- 你应调整
matcher以匹配自定义前缀。 - 自定义前缀只对用户可见,在内部会重写为对应语言。因此
[locale]段对应语言,而非前缀。
我能在应用中读取匹配的前缀吗?
由于自定义前缀会被内部重写为语言,你无法直接访问前缀。但可以从语言中提取区域信息:
import {useLocale} from 'next-intl';
function Component() {
// 假设语言是 'en-US'
const locale = useLocale();
// 提取 "US" 区域
const {region} = new Intl.Locale(locale);
}区域必须是有效的ISO 3166-1 alpha-2 国家代码或联合国 M49 区域代码。传入 Intl.Locale 时,区域代码不区分大小写,内部会转换为大写。你也可以结合语言与非母语使用的区域(例如 en-AT 表示奥地利用的英语)。
除了区域外,语言标签还能编码更多属性,例如数字系统。
如果想在语言中编码自定义信息,可以用带 -x- 前缀的任意私有扩展(例如 en-US-x-usd)。Intl.Locale 构造函数会忽略私有扩展,但你可以手动从语言字符串中提取它们。
pathnames
想看视频讲解?
许多应用选择本地化路径名,尤其是在关注搜索引擎优化时。
示例:
/en/about/de/über-uns
通常你只想内部定义一次这些路由,next-intl 中间件可以重写进来的请求到共享路径名。
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en-US', 'en-UK', 'de'],
defaultLocale: 'en-US',
// `pathnames` 对象存储内部路径与外部路径的键值对。
// 根据语言,外部路径会被重写到共享的内部路径。
pathnames: {
// 如果所有语言用相同路径,一条外部路径即可
'/': '/',
'/blog': '/blog',
// 如果不同语言用不同路径,可指定对应外部路径
'/services': {
de: '/leistungen'
},
// 非 ASCII 字符编码会自动处理
'/about': {
de: '/über-uns'
},
// 支持用中括号定义动态参数
'/news/[articleSlug]': {
de: '/neuigkeiten/[articleSlug]'
},
// 与动态段重叠的静态路径将优先匹配静态路径
'/news/just-in': {
de: '/neuigkeiten/aktuell'
},
// 支持(可选)catch-all 段
'/categories/[...slug]': {
de: '/kategorien/[...slug]'
}
}
});本地化路径名映射到通过基于文件系统的 Next.js 路由所创建的单个内部路径。以上例中,/de/über-uns 会由页面 /[locale]/about/page.tsx 处理。
如何本地化 CMS 驱动的 URL?
假设有路由 /news/[articleSlug],你希望路径名本地化为:
/en/news/launch-of-new-product
/de/neuigkeiten/produktneuheit则可以用 pathnames 本地化路径名中静态部分:
'/news/[articleSlug]': {
de: '/neuigkeiten/[articleSlug]'
}… 动态部分则使用 CMS 提供的本地化 slug。
切记让语言切换器及 alternate links 也能识别 CMS 驱动的 URL,确保指向正确的本地化路径名。
了解更多:
如何重新验证本地化路径名?
视路由是静态生成(构建时)还是动态生成(运行时),revalidatePath 需针对本地化路径名或内部路径名调用。
举例:
app
└── [locale]
└── news
└── [slug]以及路由配置:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de'],
defaultLocale: 'en',
pathnames: {
'/news/[slug]': {
en: '/news/[slug]',
de: '/neuigkeiten/[slug]'
}
}
});若 some-article 被包含在 generateStaticParams,重新验证方式为:
// 构建时静态生成
revalidatePath('/de/news/some-article');
// 运行时动态生成:
revalidatePath('/de/neuigkeiten/some-article');如果不确定,可使用 revalidateTag 进行重新验证。
domains
想看视频讲解?
如果想基于不同域名提供本地化内容,可以通过 domains 设置,提供域名与语言之间的映射列表。
示例:
us.example.com:en-USca.example.com:en-CAca.example.com/fr:fr-CAfr.example.com:fr-FR
很多时候,domains 设置会和localePrefix 配合使用,达到上述效果。也可以使用自定义前缀按语言定制用户可见前缀。
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en-US', 'en-CA', 'fr-CA', 'fr-FR'],
defaultLocale: 'en-US',
domains: [
{
domain: 'us.example.com',
defaultLocale: 'en-US',
locales: ['en-US']
},
{
domain: 'ca.example.com',
defaultLocale: 'en-CA',
locales: ['en-CA', 'fr-CA']
},
{
domain: 'fr.example.com',
defaultLocale: 'fr-FR',
locales: ['fr-FR']
}
],
localePrefix: {
mode: 'as-needed',
prefixes: {
// 为 `ca.example.com/fr` 提供更简洁前缀
'fr-CA': '/fr'
}
}
});语言必须在所有域名间唯一,通常使用区域变体以避免冲突。但请注意,如果整体语言已满足需求,未必需要为每个语言提供消息。
如果没有匹配的域名,中间件会基于前缀回退到默认语言匹配(例如,开发环境的 localhost)。
另见:基于域名的路由
我可以为不同域名使用不同的 localePrefix 配置吗?
目前框架不直接支持,但你仍可以为每个域名单独构建应用,通过环境变量注入不同的路由配置来实现。
示例:
import {defineRouting} from 'next-intl/routing';
const isUsDomain =
process.env.VERCEL_PROJECT_PRODUCTION_URL === 'us.example.com';
export const routing = defineRouting({
locales: isUsDomain ? ['en-US'] : ['en-CA', 'fr-CA'],
defaultLocale: isUsDomain ? 'en-US' : 'en-CA',
localePrefix: isUsDomain ? 'never' : 'always'
});localeDetection
中间件会基于路由配置和请求,进行匹配语言检测,并针对匹配的语言直接放行请求,或重定向到匹配语言。
如果你希望完全依赖 URL 来解析语言,可以将 localeDetection 设为 false,这会禁用基于 accept-language 头和之前访问可能存在的 cookie 值的语言检测。
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localeDetection: false
});这样仅用语言前缀和可能匹配的域名来确定语言。
localeCookie
如果用户切换了与 accept-language 头不匹配的语言,next-intl 会设置名为 NEXT_LOCALE 的会话 cookie,包含最近检测到的语言,用于记住用户语言偏好。
默认该 cookie 设置具有以下属性:
如果有更特殊需求,可调整配置:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
// 会与默认配置合并
localeCookie: {
// 自定义 cookie 名称
name: 'USER_LOCALE',
// 过期时间:一年
maxAge: 60 * 60 * 24 * 365
}
});… 或彻底关闭 cookie:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localeCookie: false
});基于 GDPR 合规建议使用什么 maxAge?
为了开箱即用的合规,next-intl 默认不设置 cookie 的 max-age,将其作为会话 cookie,在浏览器关闭时失效。
请注意,法律法规因地区而异,建议你独立核实。我们力求保持信息最新,但不保证完全准确。
了解更多:
alternateLinks (hreflang)
中间件会自动设置 link 头,告知搜索引擎你的内容有不同语言版本。此功能会自动基于你的路由配置生成正确的链接。
不过,有些时候你可能想自行提供这些链接:
- 某些页面仅提供特定语言版本
- 使用 CMS 等外部系统管理页面路径名
此时可通过设置 alternateLinks 为 false 来关闭自动行为:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
alternateLinks: false
});了解更多:
自动包含哪些 alternate links?
使用中间件默认设置,对于 / 的响应,link 头可能如下:
link: <https://example.com/en>; rel="alternate"; hreflang="en",
<https://example.com/de>; rel="alternate"; hreflang="de",
<https://example.com/>; rel="alternate"; hreflang="x-default"其中 x-default 用于指向一种在用户浏览器语言不匹配时可使用的变体。本例中的 x-default 用于语言选择与检测,实际是发起 307 重定向到最佳匹配语言。
为了提供完整 URL,域名从 x-forwarded-host 头读取,若无则回退到 host (托管平台通常自动提供这些头)。
你中间件配置的选项,如 domains、pathnames 和 basePath,都会自动纳入。
我可以定制 alternate links 吗?
如果想定制,你可以关闭自动功能自行实现,或者如果只需做些小调整,可以合成中间件,在中间件执行后添加自定义逻辑:
import createMiddleware from 'next-intl/middleware';
import LinkHeader from 'http-link-header';
import {NextRequest} from 'next/server';
import {routing} from './i18n/routing';
const handleI18nRouting = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
const response = handleI18nRouting(request);
// 示例:移除 `x-default` 条目
const link = LinkHeader.parse(response.headers.get('link'));
link.refs = link.refs.filter((entry) => entry.hreflang !== 'x-default');
response.headers.set('link', link.toString());
return response;
}next.config.ts
除路由配置外,next-intl 还会纳入你 next.config.ts 中的设置。
basePath
中间件及导航 API 会自动考虑你在 Next.js 配置中设置的basePath。
唯一例外是 getPathname 函数,它返回的是不带 basePath 的纯路径名。如果需要,可手动在返回值前加上 basePath。
使用 basePath 时,请确保你的 matcher 包含显式根路径:
export const config = {
// matcher 是相对于 basePath 的
matcher: [
// 此条目用于处理 basePath 的根,应始终包含
'/'
// ... 其他 matcher 配置
]
};trailingSlash
如果你在 Next.js 配置中将 trailingSlash 设为 true,中间件和导航 API 会自动考虑此设置。
注意,如果你使用了pathnames,你的内部路径和外部路径可以带或不带终止斜杠,系统会内部进行规范化。