Skip to content
博客next-intl 3.22

next-intl 3.22:逐步前进

2024年10月21日 · 作者 Jan Amann

在过去几个月中,已经发布了多个小版本,每个版本都为 next-intl 带来了一些渐进式的改进。虽然每个版本本身都是对现有功能的提升,但它们同时也是向着更统一、更简洁的 API 形态迈进的更大进程的一部分。

今天,发布了另一个小版本,标志着这一进程的最终步骤:next-intl@3.22

尽管此版本完全向后兼容,但它包含了一些现有 API 的现代替代方案。因此,现在是一个很好的机会来回顾这些变化,并酌情考虑进行迁移。

近期改进:

  1. defineRouting:类型安全、集中的路由配置(v3.18 引入)
  2. i18n/request.ts:精简的文件组织(v3.19 引入)
  3. await requestLocale:为 Next.js 15 做准备(v3.22 引入)
  4. createNavigation:简化的导航 API(v3.22 引入)
  5. setRequestLocale:静态渲染(v3.22 标记为稳定)
  6. defaultTranslationValues:被弃用,建议采用用户空间模式(v3.22 引入)

让我们更详细地看看这些变化。

defineRouting

之前,中间件和导航 API 之间共享的配置需要格外小心以保持同步。随着 defineRouting 的引入,这些配置现在可以以类型安全的方式集中管理:

i18n/routing.ts
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting({
  locales: ['en-US', 'en-GB'],
  defaultLocale: 'en-US',
  localePrefix: {
    mode: 'always',
    prefixes: {
      'en-US': '/us',
      'en-GB': '/uk'
    }
  },
  pathnames: {
    '/': '/',
    '/organization': {
      'en-US': '/organization',
      'en-GB': '/organisation'
    }
  }
});

这个 routing 配置通常定义在像 i18n/routing.ts 这样的集中位置,可以用于以前需要单独配置的地方:

middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
export default createMiddleware(routing);
 
// ...

如果你在 v3.22 之前使用过 defineRouting,并且向 createMiddleware 传递了中间件选项作为第二个参数,现在可以改为传递给 defineRouting

文档已持续更新以反映这些变更,并建议直接在 i18n/routing.ts 中创建导航 API 以简化流程。当然,如果你更喜欢单独维护导航 API,这也是完全可以的。

i18n/request.ts

如果你使用基于语言环境的路由,通常会有两个 next-intl 配置文件:

  1. 路由配置(通过 defineRouting 定义)
  2. 请求配置(通过 getRequestConfig 定义)

虽然历史上(2)建议放在 i18n.ts,但默认位置现在已改为 i18n/request.ts,以简化文件组织并更明确地表达此文件的用途:

└── src
    └── i18n
        ├── routing.ts (1)
        └── request.ts (2)

因此,i18n/request.ts 现在被认为是新的默认位置,并在文档中广泛推荐。

如果你想使用自定义位置,可以在 next.config.mjs 中进行配置:

next.config.mjs
import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin('./somewhere/else/request.ts');
 
// ...

await requestLocalegetRequestConfig

Next.js 15 即将到来,带来了请求 API 的 异步化变化。为配合此变更,传递给 getRequestConfiglocale 已被替代为 requestLocale,并伴有一些细微差别:

  1. 现在须对 requestLocale 进行 await
  2. 结果可能为 undefined,因此需提供回退
  3. 需要在 getRequestConfig 中返回 locale
+ import {routing} from './i18n/routing';
 
export default getRequestConfig(async ({
-  locale
+  requestLocale
}) => {
+  // 这通常对应于 `[locale]` 路由段
+  let locale = await requestLocale;
 
-  // 验证传入的 `locale` 参数是否合法
-  if (!routing.locales.includes(locale as any)) notFound();
+  // 确保传入的 locale 有效
+  if (!locale || !routing.locales.includes(locale as any)) {
+    locale = routing.defaultLocale;
+  }
 
  return {
+    locale,
    // ...
  };
});

虽然略显啰嗦,但这次改动允许你在中间件不能提供 locale(例如全局国家选择页面 / 或全局 404 页面)的边缘情况下返回回退 locale。

另外,建议在这里返回有效的 locale 以应对 [locale] 路由段中遇到未知值的情况。但你也可以选择在一个集中位置(如你的根布局)进行校验并有条件地调用 notFound()

createNavigation

新增的 createNavigation 函数取代了以下旧 API:

  1. createSharedPathnamesNavigation
  2. createLocalizedPathnamesNavigation

此新函数是现有导航功能的重新实现,统一了两种使用场景下的 API,同时修复了之前 API 的一些瑕疵。此外,该实现针对 Next.js 15 进行了更新,能无警告运行。

用法

i18n/routing.ts
import {createNavigation} from 'next-intl/navigation';
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting(/* ... */);
 
export const {Link, redirect, usePathname, useRouter} =
  createNavigation(routing);

迁移到 createNavigation

createNavigation 通常可作为无缝替代,但可能需做以下调整:

  1. createNavigation 期望接收完整的路由配置。理想做法是通过 defineRouting 定义并将结果传入。唯一例外是构建时未知 locales 情况
  2. 如果使用过 createLocalizedPathnamesNavigation 并且已Link 组件的 href 属性上进行组合,则不应再传递泛型 Pathname 类型参数。
- ComponentProps<typeof Link<Pathname>>
+ ComponentProps<typeof Link>
  1. 如果使用过 redirect,现在必须显式提供 locale(即使只是当前 locale)。此前直接传递的 href(字符串或对象)需用对象包裹,并赋值给 href 属性。此改动是为了配合 Next.js 15 的准备。
// 获取当前 locale
// ... 普通组件内:
const locale = useLocale();
// ... 异步组件内:
const locale = await getLocale();
- redirect('/about')
+ redirect({href: '/about', locale})
 
- redirect({pathname: '/users/[id]', params: {id: 2}})
+ redirect({href: {pathname: '/users/[id]', params: {id: 2}}, locale})
  1. 如果你使用过 getPathname 并且之前手动添加了 locale 前缀,现在不要这样做了 —— getPathname 会根据你的路由策略自动处理。
- '/'+ locale + getPathname(/* ... */)
+ getPathname(/* ... */);
  1. 如果你同时使用 localePrefix: 'as-needed'domains,且调用了 getPathname,现在需要传入 domain 参数(请参阅 特殊情况:domainslocalePrefix: 'as-needed' 配合使用)。

setRequestLocale 标记为稳定

如果你依赖于静态渲染,可能之前用过 unstable_setRequestLocale API。这个函数现在被标记为稳定,因为它在可预见的未来仍会继续使用。

- import {unstable_setRequestLocale} from 'next-intl/server';
+ import {setRequestLocale} from 'next-intl/server';

大约一年前,我在 Next.js 仓库开启了讨论 #58862,试图探讨 Next.js 是否能提供一种在服务端组件中访问用户 locale 的方法,而不会牺牲易用性或产生渲染问题。尽管该问题颇受关注,目前排在过去一年最受欢迎讨论第 2 名,但 Next.js 团队迄今尚未对此发表回应。据我理解,这确实不是一个容易解决的问题,但如果有机会,我非常愿意参与合作。

尽管我仍然乐观地认为未来某天能让 setRequestLocale API 变得多余,但“unstable”(不稳定)前缀已经不再合适——该 API 自推出以来已被证明稳定可靠。

defaultTranslationValues(弃用)

defaultTranslationValues 允许你在整个应用中共享全局值用于消息。最常见的例子是共享的富文本元素(例如 b: (chunks) => <b>{chunks}</b>)。

但随着时间推移,该功能暴露出了弊端:

  1. 无法跨服务端组件边界自动序列化(见 #611
  2. 干扰类型安全的参数(见 #410

因此,该功能将被弃用,文档现建议使用更好的替代方案

import {useTranslations} from 'next-intl';
import RichText from '@/components/RichText';
 
function AboutPage() {
  const t = useTranslations('AboutPage');
  return <RichText>{(tags) => t.rich('description', tags)}</RichText>;
}

接下来?

这些发布说明原计划作为下一个大版本的一部分发布。不过,我很高兴能将这些变化逐步推送而且不产生破坏性变更。

所有改进都源于与众多 next-intl 用户和贡献者的对话。我非常荣幸能收到大家如此多的反馈,并确保通过一个精简、统一的包能够满足广泛的国际化用例。同时,衷心感谢所有参与测试该版本预发行版的朋友们。

有了这些变化,用户可以在 v3 系列中按自己的节奏升级到现代 API。话虽如此,4.0 版本已经在筹备中,主要目标是清理废弃功能,确保 next-intl 保持极简主义。

——Jan


Let’s keep in touch: