渲染翻译内容
想看视频吗?
术语
- Locale (语言区域):我们用该术语描述包含用户语言和格式偏好的标识符。除了语言之外,locale 还可以包含可选的区域信息(例如
en-US)。 - Messages(消息):这些是以 locale 为单位分组的键/翻译对集合(例如
en-US.json)。
消息结构
消息通常定义在 JSON 文件中:
{
"About": {
"title": "关于我们"
}
}你可以在 React 组件内部使用 useTranslations 钩子渲染消息:
import {useTranslations} from 'next-intl';
function About() {
const t = useTranslations('About');
return <h1>{t('title')}</h1>;
}要在组件中获取所有可用消息,可以省略命名空间路径:
const t = useTranslations();
t('About.title');了解更多:
翻译人员可以使用本地化管理解决方案如 Crowdin 来协作完成消息翻译。
如何为消息提供更多结构化?
你可以选择将消息结构定义为嵌套对象。
{
"auth": {
"SignUp": {
"title": "注册",
"form": {
"placeholder": "请输入你的名字",
"submit": "提交"
}
}
}
}import {useTranslations} from 'next-intl';
function SignUp() {
// 提供包含该组件所有消息的最低公共祖先路径
const t = useTranslations('auth.SignUp');
return (
<>
<h1>{t('title')}</h1>
<form>
<input
// 其余层级使用 '.' 访问嵌套消息字段
placeholder={t('form.placeholder')}
/>
<button type="submit">{t('form.submit')}</button>
</form>
</>
);
}如何在组件外使用翻译?
next-intl 主要基于 useTranslations API,用于在 React 组件内消费翻译。虽然这看似限制,但这是刻意为之,旨在推动使用成熟的模式,避免容易忽视的问题。
不过有一个例外:在服务器动作、元数据和路由处理函数中使用 next-intl。
如果你想深入了解该设计决策的背景,推荐阅读这篇博客文章:如何(不)在 React 组件外使用翻译。
我可以用其他方式组织消息吗?
命名空间键不能包含字符“.”,因为它用于表示嵌套——其它字符正常使用。
如果你之前使用扁平结构且在消息键中包含“.”,可以按下面代码转换为嵌套结构:
import {set} from 'lodash';
const input = {
'one.one': '1.1',
'one.two': '1.2',
'two.one.one': '2.1.1'
};
const output = Object.entries(input).reduce(
(acc, [key, value]) => set(acc, key, value),
{}
);输出结果:
{
"one": {
"one": "1.1",
"two": "1.2"
},
"two": {
"one": {
"one": "2.1.1"
}
}
}这样保持了层级结构,同时去掉了重复的父级键名。你可以在单次脚本中执行此转换,也可以在将消息传给 next-intl 前先进行转换。
如果你之前使用可读文本作为键,也可以在传给 next-intl 前将 . 替换成别的符号(如 _),但一般推荐使用 ID 作为键。若主要目的是在编辑器中查看可读标签,可使用 VSCode 集成 来展示消息中的人类可读标签。
ICU 消息
next-intl 使用 ICU 消息语法,允许表达语言细节,并将消息中的状态处理与应用代码分离。
静态消息
静态消息将按原样使用:
"message": "Hello world!"t('message'); // "Hello world!"参数插入
动态值可以通过花括号插入到消息中:
"message": "Hello {name}!"t('message', {name: 'Jane'}); // "Hello Jane!"基数复数(Cardinal Pluralization)
用 plural 参数表达物品数量的复数:
"message": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."t('message', {count: 3580}); // "You have 3,580 followers."注意,使用 # 将对值进行数字格式化。
支持哪些复数形式?
不同语言有不同的复数规则,消息应依据这些规则设计。
例如,英文有两种形式:单数(如 “1 follower”)和其他(如 “0 followers”,“2 followers”)。中文只有一种形式,阿拉伯语则有六种。
另一方面,从可用性考虑,常会考虑增加特殊零的情况(如 “No followers yet” 替代 “0 followers”)。
你可以用 =value 精确匹配数字(如 =0),也可用以下标签指定语言语法规则:
zero:适用于有零物品语法的语言(如拉脱维亚语、威尔士语)。one:适用于单数语法的语言(如英语、德语)。two:适用于双数语法(如阿拉伯语、威尔士语)。few:适用于少数项目语法(如阿拉伯语、克罗地亚语)。many:适用于大量项目语法(如阿拉伯语、克罗地亚语)。other:用于不匹配其它类别的情况。
next-intl 利用 Intl.PluralRules 来判断一个数字对应哪个复数标签。few 和 many 的具体范围因 locale 而异(详见 Unicode CLDR 中语言复数规则表格)。
序数复数(Ordinal Pluralization)
用 selectordinal 参数基于顺序数字实行复数:
"message": "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!"支持哪些序数复数形式?
不同语言支持不同序数复数形式。
例如英语有四种:th、st、nd、rd(如 1st, 2nd, 3rd, 4th… 11th, 12th, 13th… 21st, 22nd, 23rd, 24th 等)。中文和阿拉伯语仅用一种形式。
next-intl 使用 Intl.PluralRules 判定给定数值应使用哪个 标签。
支持以下标签:
zero:零的语法(如拉脱维亚语、威尔士语)one:单数语法(如英语、德语)two:双数语法(如阿拉伯语、威尔士语)few:少量项目语法(如阿拉伯语、波兰语、克罗地亚语)many:大量项目语法(如阿拉伯语、波兰语、克罗地亚语)other:不匹配其他类别时使用。
few 和 many 所涵盖范围因 locale 不同而异(详见 Unicode CLDR 的语言复数规则表格)。
除此之外,next-intl 支持 =value 语法(如 =3),优先匹配特定数字。
枚举值选择
用 select 参数映射标识符至可读标签,类似 JS 的 switch 语句:
"message": "{gender, select, female {She is} male {He is} other {They are}} online."t('message', {gender: 'female'}); // "She is online."注意:other 项必填,且在无具体匹配时使用。
选择值支持哪些字符?
值必须是字母数字,可包含下划线。其它字符(包括破折号)不支持。
因此,比如你映射 locale 至可读字符串时,应先将破折号映射为下划线:
"label": "{locale, select, en_GB {英国英语} en_US {美式英语} other {未知}}"const locale = 'en-GB';
t('message', {locale: locale.replaceAll('-', '_')});转义
因为花括号用于插入参数,要在消息中使用实际符号,可以用单引号 ' 包裹:
"message": "使用单引号转义花括号(如 '{name}')"t('message'); // "使用单引号转义花括号(如 {name})"富文本
你可以用自定义标签格式化富文本,使用 t.rich 映射至 React 组件:
{
"message": "请参考 <guidelines>指南</guidelines>。"
}// 返回 `<>请参考 <a href="/guidelines">指南</a>。</>`
t.rich('message', {
guidelines: (chunks) => <a href="/guidelines">{chunks}</a>
});标签可以任意嵌套(如 这是 <important><very>非常</very>重要</important>)。
如何在应用中复用标签?
常用通用富文本标签可以定义在共享模块中,并在需要处导入,配合 t.rich 使用。
一个便捷模式是使用一个组件,通过 render prop 提供通用标签:
import {useTranslations} from 'next-intl';
import RichText from '@/components/RichText';
function AboutPage() {
const t = useTranslations('AboutPage');
return <RichText>{(tags) => t.rich('description', tags)}</RichText>;
}这样 RichText 组件就可以提供样式化标签和文本整体布局:
import {ReactNode} from 'react';
// 可用标签
type Tag = 'p' | 'b' | 'i';
type Props = {
children(tags: Record<Tag, (chunks: ReactNode) => ReactNode>): ReactNode
};
export default function RichText({children}: Props) {
return (
<div className="prose">
{children({
p: (chunks: ReactNode) => <p>{chunks}</p>,
b: (chunks: ReactNode) => <b className="font-semibold">{chunks}</b>,
i: (chunks: ReactNode) => <i className="italic">{chunks}</i>
})}
</div>
);
}如果需要把共享标签与组件内的值结合,可以用扩展操作符合并:
function UserPage({username}) {
const t = useTranslations('UserPage');
return (
<RichText>{(tags) => t.rich('description', {...tags, username})}</RichText>
);
}如何使用无子内容的“自闭合”标签?
消息可使用无子内容的标签,但语法上 ICU 解析器要求闭合标签:
{
"message": "Hello,<br></br>how are you?"
}t.rich('message', {
br: () => <br />
});如何向标签传递属性?
属性只能在调用处设置,不能在消息里:
{
"message": "去 <profile>我的个人资料</profile>"
}t.rich('message', {
profile: (chunks) => <Link href="/profile">{chunks}</Link>
});如果你有必须配置在消息里的属性值,可以从单独的消息中读取后作为属性传递:
{
"message": "访问这个 <partner>合作伙伴网站</partner>。",
"partnerHref": "https://partner.example.com"
}t.rich('message', {
partner: (chunks) => <a href={t('partnerHref')}>{chunks}</a>
});如果想本地化路径名,可以考虑使用 pathnames。
HTML 标记
渲染富文本通常用富文本格式化,但若需要输出原生 HTML 标记,可用 t.markup 函数:
{
"markup": "这是 <important>重要的</important> 内容"
}// 返回 '这是 <b>重要的</b> 内容'
t.markup('markup', {
important: (chunks) => `<b>${chunks}</b>`
});注意,t.markup 中的函数接收和返回的都是字符串,chunks 也为字符串,标签函数应返回字符串格式。
原始消息
消息总会被解析,例如富文本格式需要相应标签。如果想跳过解析(如消息里存有原始 HTML),有专用 API:
{
"content": "<h1>标题</h1><p>这是原始HTML</p>"
}<div dangerouslySetInnerHTML={{__html: t.raw('content')}} />重要:向
dangerouslySetInnerHTML
传入内容前,请务必清理和防范跨站脚本攻击(XSS)。
原始消息的值可以是任意有效 JSON 类型:字符串、布尔值、对象和数组。
可选消息
如果某些消息只在个别 locale 支持,可以用 t.has 检测当前 locale 是否含该消息:
const t = useTranslations('About');
t.has('title'); // true
t.has('unknown'); // false除了它,你还可以设置回退消息,例如来自默认语言的消息,以应对部分 locale 的不完整翻译。
消息数组
若需渲染消息列表,推荐做法是在 React 组件里将翻译键映射为数组:
{
"CompanyStats": {
"yearsOfService": {
"title": "服务年限",
"value": "34"
},
"happyClients": {
"title": "满意客户",
"value": "1,000+"
},
"partners": {
"title": "产品",
"value": "5,000+"
}
}
}import {useTranslations} from 'next-intl';
function CompanyStats() {
const t = useTranslations('CompanyStats');
const items = [
{
title: t('yearsOfService.title'),
value: t('yearsOfService.value')
},
{
title: t('happyClients.title'),
value: t('happyClients.value')
},
{
title: t('partners.title'),
value: t('partners.value')
}
];
return (
<ul>
{items.map((item, index) => (
<li key={index}>
<h2>{item.title}</h2>
<p>{item.value}</p>
</li>
))}
</ul>
);
}该方法既能使用 ICU 功能,也可以享用消息的静态校验。
如果不同 locale 的项目数不一样怎么办?
要动态遍历某个命名空间下所有键,可以使用 useMessages 钩子获取所有消息,再提取键:
import {useTranslations, useMessages} from 'next-intl';
function CompanyStats() {
const t = useTranslations('CompanyStats');
const messages = useMessages();
const keys = Object.keys(messages.CompanyStats);
return (
<ul>
{keys.map((key) => (
<li key={key}>
<h2>{t(`${key}.title`)}</h2>
<p>{t(`${key}.value`)}</p>
</li>
))}
</ul>
);
}从右向左的语言
阿拉伯语、希伯来语和波斯语等使用从右向左书写(简称 RTL)。此类语言从页面右侧开始书写,向左延伸。
示例:
النص في اللغة العربية _مثلا_ يُقرأ من اليمين لليسار除了提供翻译消息外,正确 RTL 本地化还需:
了解更多: