如何(不)在 React 组件外使用翻译
2023年4月21日 · 作者 Jan Amann你是否曾经好奇,为什么 next-intl 不提供在 React 组件外消费翻译的 API?
使用 next-intl 为你的应用程序实现国际化的传统方式是使用 useTranslations 钩子:
import {useTranslations} from 'next-intl';
function About() {
const t = useTranslations('About');
return <h1>{t('title')}</h1>;
}为什么不可能在比如说工具函数中格式化消息?这里是否缺少了什么?
这看起来像是不必要的限制,但缺乏此功能是刻意为之,目的是鼓励采用经过验证的模式以避免潜在问题——尤其是那些容易被忽视的问题。
示例:格式化错误消息
假设你有一个 FeedbackForm 组件,会将用户反馈发送到后端接口。不幸的是,由于反馈量很大,服务器偶尔会返回 504 状态码。为了提升用户体验,你想实现自动重试并向用户提供适当的反馈。
以下是一个幼稚的实现。你能发现此实现有什么问题吗?
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';
function sendFeedback() {
// ❌ 不好的实现:返回格式化后的消息
API.sendFeedback().catch((error) => {
// 如果是网关超时,通知用户我们将在5分钟后重试
if (error.status === 504) {
// (这里为了示例假设 `t` 已定义)
return t('timeout', {nextAttempt: addMinutes(new Date(), 5)});
}
});
}
function FeedbackForm({user}) {
const t = useTranslations('Form');
const [errorMessage, setErrorMessage] = useState();
function onSubmit() {
sendFeedback().catch((errorMessage) => {
setErrorMessage(errorMessage);
});
}
return (
<form onSubmit={onSubmit}>
{errorMessage != null && <p>{errorMessage}</p>}
...
</form>
);
}你发现问题了吗?
我们一起来看看:
nextAttempt的值是在一个由事件处理器调用的工具函数中插值到消息里的。随着临近重试超时时间,我们无法保持剩余时间的更新。- 如果用户切换语言,错误消息仍会保持之前选择的语言,导致体验不连贯。
更好的方式:在渲染时格式化
为避免上述问题,我们可以在 React 的渲染阶段格式化消息,将数据结构变成人类可读的字符串。
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';
function FeedbackForm({user}) {
const t = useTranslations('Form');
const [retry, setRetry] = useState();
const now = useNow({
// 每分钟更新一次
updateInterval: 1000 * 60
});
function onSubmit() {
// ✅ 好的实现:将数据结构存储于状态中
API.sendFeedback().catch((error) => {
if (error.status === 504) {
setRetry(addMinutes(now, 5));
}
});
}
return (
<form onSubmit={onSubmit}>
{retry != null && <p>{t('timeout', {nextAttempt: retry - now})}</p>}
...
</form>
);
}现在,我们可以通过交互式倒计时提供更好的用户体验,显示距离下一次尝试的时间。
此外,这种方式对可能出现的意外状态更健壮,例如用户在超时消息显示时切换语言。
证明规则的例外
如果你使用的是 Next.js,你可能想在 API 路由、路由处理程序或者 Metadata API 中对国际化消息进行翻译。
next-intl/server 提供了一组可等待的函数版本,这些函数通常是以钩子形式在组件内调用的。它们与 React 无关,可以用于这些场景。
import {getTranslations} from 'next-intl/server';
// `locale` 是通过 Next.js 的 `params` 接收到的
const locale = params.locale;
// 这会创建一个与 `useTranslations` 返回的函数相同的函数
const t = await getTranslations({locale});
// 结果:"Hello world!"
t('hello', {name: 'world'});这看起来很熟悉
如果你使用 React 已经有一段时间了,可能经历过从 component{DidMount,DidUpdate,WillUnmount} 到 useEffect 的转变。useEffect 更优的原因在于它促使开发者将应用保持同步,从而一系列潜在问题自然消失。
通过限制自己只在渲染期间格式化消息,我们处于类似的情形:翻译消息的渲染输出总是与应用状态同步,我们可以信赖应用的一致性。
相关内容:在结构化消息文档中“如何复用消息?”
Let’s keep in touch: