Skip to content
文档环境服务器和客户端组件

服务器组件和客户端组件的国际化

React 服务器组件 允许你实现仅保留在服务器端的组件,前提是它们不需要 React 的交互功能,例如 useStateuseEffect

这同样适用于处理国际化。

page.tsx
import {useTranslations} from 'next-intl';
 
// 由于此组件不使用 React 的任何交互功能,
// 它可以作为服务器组件运行。
 
export default function HomePage() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

将国际化移至服务器端能够解锁新的性能层次,同时将客户端留给交互功能。

服务器端国际化的好处:

  1. 你的消息永远不会离开服务器,也不需要传递给客户端
  2. 用于国际化的库代码不需要在客户端加载
  3. 不需要拆分消息,比如基于路由或组件
  4. 客户端没有运行时成本

在服务器组件中使用国际化

服务器组件可以通过两种方式声明:

  1. 异步组件
  2. 非异步的普通组件

在典型应用中,你很可能会遇到这两种类型的组件。next-intl 提供了适用于对应组件类型的 API。

异步组件

这类组件主要关注数据获取,不能使用钩子。因此,next-intl 提供了一整套可等待的函数版本,代替通常在组件内部以钩子方式调用的函数。

page.tsx
import {getTranslations} from 'next-intl/server';
 
export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
 
  return (
    <PageLayout title={t('title', {username: user.name})}>
      <UserDetails user={user} />
    </PageLayout>
  );
}

以下函数可用:

  • getTranslations
  • getFormatter
  • getNow
  • getTimeZone
  • getMessages
  • getLocale

非异步组件

未用 async 关键字声明且不使用 useState 等交互功能的组件被称为共享组件。这些组件可视其被导入的位置,作为服务器组件或客户端组件渲染。

在 Next.js 中,服务器组件是默认,因此共享组件通常会作为服务器组件执行:

UserDetails.tsx
import {useTranslations} from 'next-intl';
 
export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');
 
  // 该组件默认作为服务器组件执行。
  // 但若由客户端组件导入,则作为客户端组件执行。
  return (
    <section>
      <h2>{t('title')}</h2>
      <p>{t('followers', {count: user.numFollowers})}</p>
    </section>
  );
}

如果你从共享组件中导入了 useTranslationsuseFormatteruseLocaleuseNowuseTimeZonenext-intl 会自动提供适合该组件执行环境(服务器或客户端)的实现。

服务器组件集成如何工作?

next-intl 利用 react-server 条件导出 加载针对服务器或客户端组件使用优化的代码。客户端钩子如 useTranslations 的配置通过 useContext 读取,而服务器端通过 i18n/request.ts 加载。

钩子目前主要为客户端组件设计,因为它们通常是有状态的或不适合服务器环境。不过,类似 useId 这样的钩子也可以用于服务器组件。同样,next-intl 提供的基于钩子的 API 无论是在服务器还是客户端组件中使用,外观均相同。

当前这种模式的一条限制是钩子不能在异步组件中调用。因此,next-intl 提供了另一套面向此用例的可等待 API

我应该为组件使用异步函数还是非异步函数?

若你实现的组件符合共享组件的定义,将其实现为非异步函数可能更有益。这允许组件在服务器或客户端环境中使用,使其非常灵活。即便你不打算在哪些客户端运行某个组件,这种兼容性仍有帮助,比如简化测试。

不过,你无需拘泥于使用非异步函数处理国际化——按你的应用需求选择即可。

性能方面,异步函数和钩子可以互换使用。配置来自 i18n/request.ts,首次使用时仅加载一次,相关实现内部均采用基于请求的缓存。唯一细微差别在于:异步函数的优点是调用后渲染可以立即恢复。相比之下,若钩子调用触发了 i18n/request.ts 的初始化,组件会挂起直到配置解析完成,然后重新渲染,可能会重新执行钩子调用之前的组件逻辑。但一旦请求中配置解析完成,钩子将同步执行,无挂起,相较异步函数因无需等待微任务队列清空,开销更小(参见相应 React RFC 中通过重放执行恢复挂起组件)。

在客户端组件中使用国际化

根据情况,你可能需要在客户端组件中处理国际化。向客户端提供所有消息是最简单的起步方式,因此 next-intl 在渲染 NextIntlClientProvider 时会自动完成此操作。这对许多应用来说是合理的做法。

然而,如果你希望优化应用性能,可以更有选择性地将消息传递给客户端:

layout.tsx
<NextIntlClientProvider
  // 不向客户端传递任何消息
  messages={null}
>
  ...
</NextIntlClientProvider>

你仍然可以为应用中的某些部分添加另一个 NextIntlClientProvider 实例,以便在那里向客户端传递消息。

以下列出了在客户端组件中使用 next-intl 翻译的几种方式,按性能优先顺序排列:

方案 1:将翻译后的标签传递给客户端组件

首选方式是从服务器组件将处理后的标签以 props 或 children 传给客户端组件。

FAQEntry.tsx
import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 一个客户端组件
import FAQContent from './FAQContent';
 
export default function FAQEntry() {
  // 在服务器组件中调用 `useTranslations`...
  const t = useTranslations('FAQEntry');
 
  // ...并将翻译内容传给客户端组件
  return (
    <Expandable title={t('title')}>
      <FAQContent content={t('description')} />
    </Expandable>
  );
}
Expandable.tsx
'use client';
 
import {useState} from 'react';
 
function Expandable({title, children}) {
  const [expanded, setExpanded] = useState(false);
 
  function onToggle() {
    setExpanded(!expanded);
  }
 
  return (
    <div>
      <button onClick={onToggle}>{title}</button>
      {expanded && <div>{children}</div>}
    </div>
  );
}

这样,我们即使在翻译只在服务器端执行的情况下,也能在翻译内容上使用 React 的交互功能(如 useState)。

更多内容请参阅 Next.js 文档:将服务器组件作为 props 传递给客户端组件

示例:如何实现语言切换器?

如果你将语言切换实现为交互式选择框,你可以通过从服务器组件渲染标签,仅将 select 元素标记为客户端组件,从而保持国际化在服务器端进行。

LocaleSwitcher.tsx
import {useLocale, useTranslations} from 'next-intl';
import {locales} from '@/config';
 
// 一个客户端组件,监听 `select` 的 `change` 事件,
// 使用 `useRouter` 切换语言,并使用 `useTransition`
// 在切换期间显示加载状态。
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
 
export default function LocaleSwitcher() {
  const t = useTranslations('LocaleSwitcher');
  const locale = useLocale();
 
  return (
    <LocaleSwitcherSelect defaultValue={locale} label={t('label')}>
      {locales.map((cur) => (
        <option key={cur} value={cur}>
          {t('locale', {locale: cur})}
        </option>
      ))}
    </LocaleSwitcherSelect>
  );
}

示例实现 (演示)

另见: useRouter

示例:如何实现一个表单?

表单需要客户端状态来显示加载指示和校验错误。

为了保持国际化在服务器端,将交互部分拆分到叶子组件中,而非整个表单都标记为 'use client'; 是个有用的结构方法。

示例:

app/register/page.tsx
import {useTranslations} from 'next-intl';
 
// 客户端组件,以便使用 `useActionState`
// 可能显示提交后的错误。
import RegisterForm from './RegisterForm';
 
// 客户端组件,用于在提交时禁用输入框。
import FormField from './FormField';
 
// 客户端组件,用于在提交时禁用提交按钮。
import FormSubmitButton from './FormSubmitButton';
 
export default function RegisterPage() {
  const t = useTranslations('RegisterPage');
 
  function registerAction() {
    'use server';
    // ...
  }
 
  return (
    <RegisterForm action={registerAction}>
      <FormField label={t('firstName')} name="firstName" />
      <FormField label={t('lastName')} name="lastName" />
      <FormField label={t('email')} name="email" />
      <FormField label={t('password')} name="password" />
      <FormSubmitButton label={t('submit')} />
    </RegisterForm>
  );
}

方案 2:将状态移到服务器端

你可能遇到需要动态状态(如分页)反映在翻译消息中的情况。

Pagination.tsx
function Pagination({curPage, totalPages}) {
  const t = useTranslations('Pagination');
  return <p>{t('info', {curPage, totalPages})}</p>;
}

你仍可以使用以下方式在服务器端管理翻译:

  1. 页面参数或查询参数
  2. Cookie
  3. 数据库状态

尤其是页面和查询参数经常是很好的选项,因为它们还带来额外好处,比如分享 URL 时能保持应用状态,且与浏览器历史集成。

💡

Smashing Magazine 上有一篇关于 在服务器组件中使用 next-intl 的文章,探讨了通过实际示例使用查询参数(尤其是关于添加交互性的部分)。

方案 3:提供单独的消息

如果你需要在无法移至服务器端的组件中合入动态状态,可以用 NextIntlClientProvider 包裹这些组件,并提供相关的消息。

Counter.tsx
import pick from 'lodash/pick';
import {NextIntlClientProvider, useMessages} from 'next-intl';
import ClientCounter from './ClientCounter';
 
export default function Counter() {
  // 接收 `i18n/request.ts` 中提供的消息…
  const messages = useMessages();
 
  return (
    <NextIntlClientProvider
      messages={
        // …并提供相关消息
        pick(messages, 'ClientCounter')
      }
    >
      <ClientCounter />
    </NextIntlClientProvider>
  );
}
如何知道我需要向客户端提供哪些消息?

目前,需要基于对被包裹组件实现的了解,有选择地挑选消息传给客户端。

自动化的基于编译器的方案正在 next-intl#1 中评估。

方案 4:提供所有消息

如果你正在构建一个高度动态的应用,大部分组件都使用 React 的交互功能,可能更愿意将所有消息都提供给客户端组件——这也是 next-intl 的默认行为。

layout.tsx
import {NextIntlClientProvider} from 'next-intl';
 
export default async function RootLayout(/* ... */) {
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>...</NextIntlClientProvider>
      </body>
    </html>
  );
}
客户端加载消息如何影响性能?

根据你应用的需求,你可能想监控你的 核心网页指标,以确保应用满足性能目标。

若你将消息传递给 NextIntlClientProvider,Next.js 会在流式渲染时将它们注入页面标记中,供客户端组件使用。这可能增加总阻塞时间,进而影响交互到下一次绘制时间指标。若你想提升这些指标,可以更有选择性地传递消息给客户端。

但通常的优化原则是:先测量后优化。如果你的应用性能已经很好,则无需优化。

当前有两项研究致力于最大化客户端消息使用的性能:

  1. 消息的自动摇树优化
  2. 消息的预编译

目标是优化你已使用的 next-intl 模式,让你的应用无需改动即可获得一流性能。

故障排查

“无法调用 useTranslations,因为未找到 NextIntlClientProvider 的上下文。”

你开发时可能会遇到此错误或类似关于 useFormatter 的错误。

原因可能是:

  1. 你刻意在客户端组件调用钩子,但组件树中没有 NextIntlClientProvider 作为祖先。此时,你可以将组件用 NextIntlClientProvider 包裹 以解决该错误。
  2. 实际调用钩子的组件意外被纳入客户端模块图,尽管你预计它是服务器组件。此时尝试通过 将该组件作为 children 传递 给客户端组件。

“函数不能直接传递给客户端组件,因为它们不可序列化。”

当你试图将不可序列化的属性传给 NextIntlClientProvider 时,可能会遇到此错误。

该组件接受以下不可序列化的属性:

  1. onError
  2. getMessageFallback

若要配置它们,可以用另一个标记了 'use client' 的组件包裹 NextIntlClientProvider 并定义相关属性。

参见:如何向 NextIntlClientProvider 提供不可序列化的属性,如 onError