Skip to content

渲染翻译内容

想看视频吗?

术语

  • Locale (语言区域):我们用该术语描述包含用户语言和格式偏好的标识符。除了语言之外,locale 还可以包含可选的区域信息(例如 en-US)。
  • Messages(消息):这些是以 locale 为单位分组的键/翻译对集合(例如 en-US.json)。

消息结构

消息通常定义在 JSON 文件中:

en.json
{
  "About": {
    "title": "关于我们"
  }
}

你可以在 React 组件内部使用 useTranslations 钩子渲染消息:

About.tsx
import {useTranslations} from 'next-intl';
 
function About() {
  const t = useTranslations('About');
  return <h1>{t('title')}</h1>;
}

要在组件中获取所有可用消息,可以省略命名空间路径:

const t = useTranslations();
 
t('About.title');

了解更多:

💡

翻译人员可以使用本地化管理解决方案如 Crowdin 来协作完成消息翻译。

如何为消息提供更多结构化?

你可以选择将消息结构定义为嵌套对象。

en.json
{
  "auth": {
    "SignUp": {
      "title": "注册",
      "form": {
        "placeholder": "请输入你的名字",
        "submit": "提交"
      }
    }
  }
}
SignUp.tsx
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 消息语法,允许表达语言细节,并将消息中的状态处理与应用代码分离。

静态消息

静态消息将按原样使用:

en.json
"message": "Hello world!"
t('message'); // "Hello world!"

参数插入

动态值可以通过花括号插入到消息中:

en.json
"message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"
参数名支持哪些字符?

参数名必须是字母数字,可以包含下划线。其它字符(包括连字符)不支持。

基数复数(Cardinal Pluralization)

plural 参数表达物品数量的复数:

en.json
"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 来判断一个数字对应哪个复数标签。fewmany 的具体范围因 locale 而异(详见 Unicode CLDR 中语言复数规则表格)。

序数复数(Ordinal Pluralization)

selectordinal 参数基于顺序数字实行复数:

en.json
"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:不匹配其他类别时使用。

fewmany 所涵盖范围因 locale 不同而异(详见 Unicode CLDR 的语言复数规则表格)。

除此之外,next-intl 支持 =value 语法(如 =3),优先匹配特定数字。

枚举值选择

select 参数映射标识符至可读标签,类似 JS 的 switch 语句:

en.json
"message": "{gender, select, female {She is} male {He is} other {They are}} online."
t('message', {gender: 'female'}); // "She is online."

注意other 项必填,且在无具体匹配时使用。

选择值支持哪些字符?

值必须是字母数字,可包含下划线。其它字符(包括破折号)不支持。

因此,比如你映射 locale 至可读字符串时,应先将破折号映射为下划线:

en.json
"label": "{locale, select, en_GB {英国英语} en_US {美式英语} other {未知}}"
const locale = 'en-GB';
t('message', {locale: locale.replaceAll('-', '_')});

转义

因为花括号用于插入参数,要在消息中使用实际符号,可以用单引号 ' 包裹:

en.json
"message": "使用单引号转义花括号(如 '{name}')"
t('message'); // "使用单引号转义花括号(如 {name})"

富文本

你可以用自定义标签格式化富文本,使用 t.rich 映射至 React 组件:

en.json
{
  "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 组件就可以提供样式化标签和文本整体布局:

components/RichText.tsx
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 解析器要求闭合标签:

en.json
{
  "message": "Hello,<br></br>how are you?"
}
t.rich('message', {
  br: () => <br />
});
如何向标签传递属性?

属性只能在调用处设置,不能在消息里:

en.json
{
  "message": "去 <profile>我的个人资料</profile>"
}
t.rich('message', {
  profile: (chunks) => <Link href="/profile">{chunks}</Link>
});

如果你有必须配置在消息里的属性值,可以从单独的消息中读取后作为属性传递:

en.json
{
  "message": "访问这个 <partner>合作伙伴网站</partner>。",
  "partnerHref": "https://partner.example.com"
}
t.rich('message', {
  partner: (chunks) => <a href={t('partnerHref')}>{chunks}</a>
});

如果想本地化路径名,可以考虑使用 pathnames

HTML 标记

渲染富文本通常用富文本格式化,但若需要输出原生 HTML 标记,可用 t.markup 函数:

en.json
{
  "markup": "这是 <important>重要的</important> 内容"
}
// 返回 '这是 <b>重要的</b> 内容'
t.markup('markup', {
  important: (chunks) => `<b>${chunks}</b>`
});

注意,t.markup 中的函数接收和返回的都是字符串,chunks 也为字符串,标签函数应返回字符串格式。

原始消息

消息总会被解析,例如富文本格式需要相应标签。如果想跳过解析(如消息里存有原始 HTML),有专用 API:

en.json
{
  "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 组件里将翻译键映射为数组:

en.json
{
  "CompanyStats": {
    "yearsOfService": {
      "title": "服务年限",
      "value": "34"
    },
    "happyClients": {
      "title": "满意客户",
      "value": "1,000+"
    },
    "partners": {
      "title": "产品",
      "value": "5,000+"
    }
  }
}
CompanyStats.tsx
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 钩子获取所有消息,再提取键:

CompanyStats.tsx
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 本地化还需:

  1. 在相关处设置 dir 属性
  2. 布局镜像,如用CSS 逻辑属性
  3. 元素镜像,如自定义图标方向

了解更多: