如何在 React 中进行复数形式处理
为什么 React 中的复数形式处理很重要
我们经常会看到一些应用显示出这样别扭的消息:
您有 1 条新消息
这往往说明开发者并没有认真考虑用户体验。
React 应用经常需要进行复数形式处理——比如通知数量、列表项长度或搜索结果数量。 而且,想把英语里的复数形式处理对,其实并不难,尤其是在应用只需要支持英语时。 但新手开发者很容易踩进各种坏习惯,尤其是在构建多语言界面时。
本指南将介绍如何正确进行英语复数形式处理、按不同区域设置格式化数字,以及如何在 React 和 Next.js 中构建一套完整的多语言复数系统。
硬编码复数形式的问题
许多项目——甚至包括一些规模惊人、举足轻重的项目——都会把复数形式的处理逻辑硬编码进去。
export default function Example({ n }) {
return (
<p>
Displaying {n} item{n === 1 ? '' : 's'}
</p>
)
}但复数变化往往不只是给单词末尾加一个 "s" 这么简单。 有些名词的复数形式是不规则的,比如 "child" 和 "children"。 有时,句子的其他部分也需要跟着变化来呼应名词,例如要根据数量使用 "is" 和 "are"。
下表展示了一些常见场景:
| 场景 | 示例 | 说明 |
|---|---|---|
| 观看人数 | "1 person is watching" "2 people are watching" | 需要处理不规则名词 ("person" → "people") 以及动词变化。 |
| 删除项目 | "Delete this message?" "Delete these 2 messages?" | 既有指示词变化 ("this" 与 "these") ,也有名词的复数变化。 |
| 搜索结果 | "No results" "1 result found" "2 results found" | 对 0、1 和多个结果,需要使用不同的表达方式。 |
使用条件表达式很快就会变得难以处理。
而当你需要将应用发布到其他语言时,情况就更棘手了。 在英语中行得通的做法,在波兰语或阿拉伯语这类处理数量规则完全不同的语言里,往往会彻底失效。 与我们合作的公司经常会因为重构这类硬编码 UI 太痛苦, 而一再推迟国际化。
如何处理英语中的复数形式
在英语中,在应用中正确使用复数形式通常很简单。
对于简单的名词复数变化,可以编写一个工具函数:
export function pluralize(
count: number,
singular: string,
plural: string = singular + 's'
) {
return `${count === 1 ? singular : plural}`;
}现在只需一个函数就能处理所有复数形式逻辑, 连不规则复数形式也适用:
pluralize(2, 'user') // "users"
pluralize(2, 'person', 'people') // "people"
pluralize(2, 'child', 'children') // "children"但如果你需要更复杂的逻辑,比如:
"No one is watching"
"1 person is watching"
"2 people are watching"
在这个阶段,你应该认真考虑采用一个易于维护的国际化 ("i18n") 库。
虽然开发者常常认为 i18n 库只适用于多语言界面,
但即使在单语言应用中,它们在复数处理和变量格式化方面也非常有用。
在底层,大多数 i18n 库都使用 JavaScript 内置的 Intl.PluralRules API 来判断任意语言应使用的正确复数形式。
React 的 i18n 库有很多,包括我们的 gt-react (如果你使用 Next.js,则是 gt-next)。 使用 gt-react 显示英文复数形式非常简单:
import { Plural } from 'gt-react'
function Example({ count }) {
return (
<Plural n={count} zero={'No one is watching'} one={`${count} person is watching`}>
{count} people are watching
</Plural>
)
}UI 会根据 n 的值按条件渲染。
大多数库都会使用 JavaScript 的 Intl.PluralRules API 来决定应显示哪种复数形式。
这意味着在英语中,你会用 "one" 表示单数,用 "other" 表示复数。
如果传给 n 的数字与已提供的任何属性都不匹配,我们的 <Plural> 组件会回退为渲染其子元素。
即使应用只支持英语,在这里使用库也是最佳实践,并且能让后续的国际化轻松得多。
多语言 React 应用中的复数形式处理 (i18n)
如果你的应用只面向英语用户,上述方法可能已经够用了——但大多数生产环境中的应用最终都需要支持多种语言。这时事情就变得更有意思了。
- 在阿拉伯语中,名词会根据数量是零、一个、两个还是多个而采用不同的形式
- 在西班牙语、德语和意大利语中,大数字使用句点而不是逗号,因此 1,000,000 会写成 1.000.000
- 在印地语中,数字按两位一组分组,因此 1,000,000 会写成 10,00,000
对于国际化应用,你应该使用专门的 i18n 库,这些库通常会在各自的文档中说明如何进行复数形式处理和数字格式化。
按不同区域设置格式化数字
你可以使用 Intl 对象按任意区域设置格式化数字。
最简单的做法是使用内置的 toLocaleString() 方法。
默认情况下,这会使用 runtime 的当前区域设置:
const n = 1000000
n.toLocaleString() // 当 runtime 区域设置为 "en-US"(美式英语)时,显示 1,000,000
n.toLocaleString('de') // 1.000.000,因为区域设置已指定为 "de"(德语)gt-react 还提供了一个 <Num> 组件,其底层依赖 Intl.NumberFormat 实现。
import { Num } from 'gt-react'
// 语言为 "en-US" 时显示 1,000,000
// 语言为 "de" 时显示 1.000.000
// 语言为 "hi" 时显示 10,00,000
export default function Example() {
return <Num>1000000</Num>
}理解不同语言中的复数形式
JavaScript's Intl.PluralRules API 支持的六种复数形式是:zero、one、two、few、many、other。
虽然英语只使用 one ("单数") 和 other ("复数") ,
但像阿拉伯语和波兰语这样的语言并不只有这两种形式。
例如,使用英语的用户可能会期望:
"No one is watching"
"1 person is watching"
"2 people are watching"
而对于讲阿拉伯语的用户,单数、 双数 (即数量恰好为两个时) 以及少量和大量复数形式可能需要使用不同的表达方式:
"لا أحد يشاهد"
"1 شخص يشاهد"
"2 شخصان يشاهدان"
"3 أشخاص يشاهدون"
"11 شخصاً يشاهدون"
这正是国际化库不可或缺的地方。 每种语言对于何时以及如何显示复数形式都有自己的规则, 因此最好依赖专门的库来正确处理。
一个好的 i18n 库会做两件事:
- 根据区域设置判断应使用哪种复数形式 (
one、many、other等) - 找到对应该形式的正确语言翻译
如果你已经在使用国际化库, 请查阅其文档,了解复数格式化的相关信息。 几乎所有库都有专门说明如何渲染复数形式的文档。
完整示例:多语言 React 应用中的复数形式
如果你还没有使用国际化库,可以考虑 gt-react!
gt-react 的 <Plural> 组件:
- 提供了一种简单的声明式方式,可在 React 中正确渲染复数形式
- 可与
<Num>格式化组件原生协同工作 - 可与
<T>翻译组件原生协同工作;该组件可接入我们的免费翻译服务,自动生成复数形式
把这些构建模块组合起来,下面是一个完整的多语言组件:
import { T, Plural, Num } from 'gt-react'
// 开箱即用,支持 100+ 种语言
function Example({ count }) {
return (
<T>
<Plural
n={count}
zero={'No one is watching'}
one={
<>
<Num>{count}</Num> person is watching
</>
}
>
<Num>{count}</Num> people are watching
</Plural>
</T>
)
}后续步骤
准备好在 React 应用中正确处理复数形式了吗?请查看以下快速入门指南:
- gt-react 快速入门指南 (适用于 React 应用)
- gt-next 快速入门指南 (适用于 Next.js 应用)
<Plural>API 参考 (查看完整的组件 API)
复数变化是 React 中最常见的 i18n 难点之一——一开始就处理好,能大幅减少后续重构工作。