服务器组件和客户端组件的国际化
React 服务器组件 允许你实现仅保留在服务器端的组件,前提是它们不需要 React 的交互功能,例如 useState 和 useEffect。
这同样适用于处理国际化。
import {useTranslations} from 'next-intl';
// 由于此组件不使用 React 的任何交互功能,
// 它可以作为服务器组件运行。
export default function HomePage() {
const t = useTranslations('HomePage');
return <h1>{t('title')}</h1>;
}将国际化移至服务器端能够解锁新的性能层次,同时将客户端留给交互功能。
服务器端国际化的好处:
- 你的消息永远不会离开服务器,也不需要传递给客户端
- 用于国际化的库代码不需要在客户端加载
- 不需要拆分消息,比如基于路由或组件
- 客户端没有运行时成本
在服务器组件中使用国际化
服务器组件可以通过两种方式声明:
- 异步组件
- 非异步的普通组件
在典型应用中,你很可能会遇到这两种类型的组件。next-intl 提供了适用于对应组件类型的 API。
异步组件
这类组件主要关注数据获取,不能使用钩子。因此,next-intl 提供了一整套可等待的函数版本,代替通常在组件内部以钩子方式调用的函数。
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>
);
}以下函数可用:
getTranslationsgetFormattergetNowgetTimeZonegetMessagesgetLocale
非异步组件
未用 async 关键字声明且不使用 useState 等交互功能的组件被称为共享组件。这些组件可视其被导入的位置,作为服务器组件或客户端组件渲染。
在 Next.js 中,服务器组件是默认,因此共享组件通常会作为服务器组件执行:
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>
);
}如果你从共享组件中导入了 useTranslations、useFormatter、useLocale、useNow 和 useTimeZone,next-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 时会自动完成此操作。这对许多应用来说是合理的做法。
然而,如果你希望优化应用性能,可以更有选择性地将消息传递给客户端:
<NextIntlClientProvider
// 不向客户端传递任何消息
messages={null}
>
...
</NextIntlClientProvider>你仍然可以为应用中的某些部分添加另一个 NextIntlClientProvider 实例,以便在那里向客户端传递消息。
以下列出了在客户端组件中使用 next-intl 翻译的几种方式,按性能优先顺序排列:
方案 1:将翻译后的标签传递给客户端组件
首选方式是从服务器组件将处理后的标签以 props 或 children 传给客户端组件。
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>
);
}'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 元素标记为客户端组件,从而保持国际化在服务器端进行。
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'; 是个有用的结构方法。
示例:
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:将状态移到服务器端
你可能遇到需要动态状态(如分页)反映在翻译消息中的情况。
function Pagination({curPage, totalPages}) {
const t = useTranslations('Pagination');
return <p>{t('info', {curPage, totalPages})}</p>;
}你仍可以使用以下方式在服务器端管理翻译:
- 页面参数或查询参数
- Cookie
- 数据库状态
尤其是页面和查询参数经常是很好的选项,因为它们还带来额外好处,比如分享 URL 时能保持应用状态,且与浏览器历史集成。
Smashing Magazine 上有一篇关于 在服务器组件中使用 next-intl 的文章,探讨了通过实际示例使用查询参数(尤其是关于添加交互性的部分)。
方案 3:提供单独的消息
如果你需要在无法移至服务器端的组件中合入动态状态,可以用 NextIntlClientProvider 包裹这些组件,并提供相关的消息。
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 的默认行为。
import {NextIntlClientProvider} from 'next-intl';
export default async function RootLayout(/* ... */) {
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>...</NextIntlClientProvider>
</body>
</html>
);
}客户端加载消息如何影响性能?
根据你应用的需求,你可能想监控你的 核心网页指标,以确保应用满足性能目标。
若你将消息传递给 NextIntlClientProvider,Next.js 会在流式渲染时将它们注入页面标记中,供客户端组件使用。这可能增加总阻塞时间,进而影响交互到下一次绘制时间指标。若你想提升这些指标,可以更有选择性地传递消息给客户端。
但通常的优化原则是:先测量后优化。如果你的应用性能已经很好,则无需优化。
当前有两项研究致力于最大化客户端消息使用的性能:
目标是优化你已使用的 next-intl 模式,让你的应用无需改动即可获得一流性能。
故障排查
“无法调用 useTranslations,因为未找到 NextIntlClientProvider 的上下文。”
你开发时可能会遇到此错误或类似关于 useFormatter 的错误。
原因可能是:
- 你刻意在客户端组件调用钩子,但组件树中没有
NextIntlClientProvider作为祖先。此时,你可以将组件用NextIntlClientProvider包裹 以解决该错误。 - 实际调用钩子的组件意外被纳入客户端模块图,尽管你预计它是服务器组件。此时尝试通过 将该组件作为
children传递 给客户端组件。
“函数不能直接传递给客户端组件,因为它们不可序列化。”
当你试图将不可序列化的属性传给 NextIntlClientProvider 时,可能会遇到此错误。
该组件接受以下不可序列化的属性:
若要配置它们,可以用另一个标记了 'use client' 的组件包裹 NextIntlClientProvider 并定义相关属性。