Skip to content

路由配置

defineRouting

可在 中间件导航 API 之间共享的路由配置,可以通过 defineRouting 函数进行定义。

src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // 支持的所有语言列表
  locales: ['en', 'de'],
 
  // 当没有匹配到语言时使用
  defaultLocale: 'en'
});

根据你的路由需求,你可能还想考虑其他设置——请看下文。

如果在构建时语言未知怎么办?

如果你构建的是可在运行时添加或移除语言的应用,可以为中间件每次请求动态提供路由配置

对应的导航 API 你可以在这种情况下省略 locales 参数来创建。

不过,如果你定义了其他路由配置,请确保中间件和导航 API 之间保持同步。

localePrefix

默认情况下,你 app 的路径名会在一个前缀下可用,该前缀与目录结构匹配(例如 /en/aboutapp/[locale]/about/page.tsx)。不过,你可以通过配置 localePrefix 设置,来选择性地移除前缀或针对不同语言自定义前缀。

了解更多:

localePrefix: 'always'(默认)

默认情况下,路径名总会以语言前缀开始(例如 /en/about)。

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
  localePrefix: 'always'
});

localePrefix: 'as-needed'

如果你希望默认语言无前缀(例如 /about),而其他语言保持前缀(例如 /de/about),可以这样配置:

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
  localePrefix: 'as-needed'
});

注意:

  1. 使用此路由策略时,确保你的 matcher 可以检测未加前缀的路径名。
  2. 中间件默认会设置一个cookie来记住用户的语言偏好。如果路径中没有显式语言前缀,可能会基于 cookie 值的语言检测将用户重定向到匹配到的最新语言(例如 //de)。
  3. 如果请求了多余的语言前缀如 /en/about,中间件会自动重定向到无前缀版本 /about。这在你从其他语言重定向并想先更新可能的 cookie 值时很有用(例如<Link /> 依赖此机制)。

localePrefix: 'never'

如果你想基于用户设置将语言提供给 next-intl,可以考虑完全不使用基于语言的路由。

当然,你也可以配置中间件使 URL 永远不显示语言前缀,适用场景包括:

  1. 使用基于域名的路由,且每个域只有一种语言
  2. 使用 cookie 判断语言,同时启用静态渲染
routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
  localePrefix: 'never'
});

此时,所有语言的请求会在内部重写为具有语言前缀,但 URL 不显示。你仍需将所有页面放在 [locale] 文件夹内,以便路由能接收 locale 参数。

注意:

  1. 使用此路由策略时,确保你的 matcher 能检测未加前缀的路径名。
  2. 由于 URL 可能不具备语言唯一性,该模式会禁用alternate links。因此请考虑自行添加,或托管一个通过 alternates 关联本地化页面的sitemap
  3. 你可以考虑将语言 cookie 的 maxAge 属性设为较长时间,以便跨会话记住用户偏好。

prefixes

如果希望自定义用户看到的前缀,可以提供按语言映射的前缀:

routing.ts
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 会照原样使用)
    }
  }
});

注意:

  1. 你应调整matcher以匹配自定义前缀。
  2. 自定义前缀只对用户可见,在内部会重写为对应语言。因此 [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 中间件可以重写进来的请求到共享路径名。

routing.ts
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]

以及路由配置:

routing.ts
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 进行重新验证。

另见:vercel/next.js#59825

domains

想看视频讲解?

如果想基于不同域名提供本地化内容,可以通过 domains 设置,提供域名与语言之间的映射列表。

示例:

  • us.example.comen-US
  • ca.example.comen-CA
  • ca.example.com/frfr-CA
  • fr.example.comfr-FR

很多时候,domains 设置会和localePrefix 配合使用,达到上述效果。也可以使用自定义前缀按语言定制用户可见前缀。

routing.ts
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 配置吗?

目前框架不直接支持,但你仍可以为每个域名单独构建应用,通过环境变量注入不同的路由配置来实现。

示例:

routing.ts
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 值的语言检测。

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
  localeDetection: false
});

这样仅用语言前缀和可能匹配的域名来确定语言。

如果用户切换了与 accept-language 头不匹配的语言,next-intl 会设置名为 NEXT_LOCALE 的会话 cookie,包含最近检测到的语言,用于记住用户语言偏好。

默认该 cookie 设置具有以下属性:

  1. sameSite:设置为 lax,允许 cookie 在来自外部站点时设置。
  2. path:默认不设置,但如果配置了basePath则使用该值。

如果有更特殊需求,可调整配置:

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
 
  // 会与默认配置合并
  localeCookie: {
    // 自定义 cookie 名称
    name: 'USER_LOCALE',
    // 过期时间:一年
    maxAge: 60 * 60 * 24 * 365
  }
});

… 或彻底关闭 cookie:

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
 
  localeCookie: false
});

中间件会自动设置 link 头,告知搜索引擎你的内容有不同语言版本。此功能会自动基于你的路由配置生成正确的链接。

不过,有些时候你可能想自行提供这些链接:

  1. 某些页面仅提供特定语言版本
  2. 使用 CMS 等外部系统管理页面路径名

此时可通过设置 alternateLinksfalse 来关闭自动行为:

routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  // ...
 
  alternateLinks: false
});

了解更多:

next.config.ts

除路由配置外,next-intl 还会纳入你 next.config.ts 中的设置。

basePath

中间件及导航 API 会自动考虑你在 Next.js 配置中设置的basePath

唯一例外是 getPathname 函数,它返回的是不带 basePath 的纯路径名。如果需要,可手动在返回值前加上 basePath。

使用 basePath 时,请确保你的 matcher 包含显式根路径:

proxy.ts
export const config = {
  // matcher 是相对于 basePath 的
  matcher: [
    // 此条目用于处理 basePath 的根,应始终包含
    '/'
 
    // ... 其他 matcher 配置
  ]
};

trailingSlash

如果你在 Next.js 配置中将 trailingSlash 设为 true,中间件和导航 API 会自动考虑此设置。

注意,如果你使用了pathnames,你的内部路径和外部路径可以带或不带终止斜杠,系统会内部进行规范化。