返回

React 中的复数处理入门

Archie McKenzie avatarArchie McKenzie
guidepluralsinternationalizationreactnextjs

什么是复数处理?

我经常看到一些应用会显示很生硬的提示,例如:

您有 1 条新消息

这清楚地说明,这位开发者没有认真考虑过用户体验。

React 应用通常都需要处理复数形式——比如通知数量、列表长度或搜索结果。 而且,要把复数处理好并不难,尤其是当你的应用只需要支持英文时。 但很多新手开发者仍然会陷入一些不良实践之中,特别是在构建多语言界面时。

硬编码复数逻辑

许多项目——包括一些规模巨大、业务关键的重要项目——都会在代码中直接硬编码复数逻辑。

export default function Example({ n }) {
  return (
    <p>
      显示 {n} 个项目
    </p>
  )
}

但复数形式往往比在单词末尾加一个 "s" 要复杂得多。 有些名词有不规则复数形式,比如 "child" 和 "children"。 有时,句子的其他部分也需要随之改变以匹配变化后的单词,比如 "is" 和 "are" 会根据数量不同而变化。

下表展示了一些常见场景:

ScenarioExamplesNotes
Viewer count"1 person is watching"
"2 people are watching"
不规则名词("person" → "people"),同时还需要修改动词。
Item deletion"Delete this message?"
"Delete these 2 messages?"
指示代词变化("this" vs. "these")以及名词复数变化。
Search results"No results"
"1 result found"
"2 results found"
针对零个、一个和多个结果使用不同的表达方式。

使用条件表达式很快就会变得难以维护。

而当你需要让应用支持其他语言时,这会变成一场噩梦。 在英语中行之有效的做法,在波兰语或阿拉伯语这类语言中往往完全失效,因为它们在处理数量时有完全不同的规则。 与我们合作的公司常常一再延后做国际化, 就是因为重构这类硬编码 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"

但如果你需要更复杂的逻辑,比如:

"无人观看"
"1 人正在观看"
"2 人正在观看"

在这个阶段,你应该认真考虑使用一个低维护成本的国际化(“i18n”)库。

虽然开发者通常认为 i18n 库只用于多语言界面, 但即使是在单语言应用中,它们在处理复数和变量格式化方面也非常有用。

目前有许多 React i18n 库可供选择,包括我们的 gt-react(如果你使用 Next.js,则可以使用 gt-next)。 使用 gt-react 展示英文复数形式非常简单:

import { Plural } from 'gt-react'

function Example({ count }) {
  return (
    <Plural n={count} zero={'无人观看'} one={`${count} 人正在观看`}>
      {count} 人正在观看
    </Plural>
  )
}

UI 会根据 n 的值进行条件渲染。

大多数库会使用 JavaScript 的 Intl 对象 来决定展示哪一种复数形式。 这意味着在英文中,你会使用 "one" 表示单数,使用 "other" 表示复数。 如果传给 n 的数值与任一已提供的 prop 都不匹配,我们的 <Plural> 组件会回退为渲染其子元素。

即使是仅支持英文的应用,在这里使用库也是最佳实践,也能让后续的国际化工作变得更加容易。


国际化(i18n)与复数形式

为界面提供多语言支持会让复数形式的展示复杂得多。

  • 在阿拉伯语中,名词会根据数量是零、一个、两个还是多个而采用不同形式
  • 在西班牙语、德语和意大利语中,大数字使用句号而不是逗号,因此 1,000,000 会写成 1.000.000
  • 在印地语中,数字按两位一组进行分组,因此 1,000,000 会写成 10,00,000

对于一个国际化应用,你应该使用专门的库,这类库会在其文档中说明如何处理复数形式和数字格式化。

为不同语言格式化数字

你也可以使用 Intl 对象来格式化数字。 最简单的方式是使用内置的 toLocaleString() 方法。

默认情况下,它会使用运行时当前的本地化设置(locale):

const n = 1000000
n.toLocaleString() // 当运行时语言环境为 "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 的 Intl 对象支持的六种复数形式是:zeroonetwofewmanyother。 虽然英语只使用 one(“单数”)和 other(“复数”), 但像阿拉伯语和波兰语这样的语言有多于这两种的复数形式。

例如,说英语的用户可能会期望:

"无人观看"
"1 人正在观看"
"2 人正在观看"

比如,说阿拉伯语的用户可能会期望:单数、 双数(当数量恰好为两个时),以及小量复数和大量复数形式都使用不同的表达方式:

"无人观看"
"1 人观看"
"2 人观看"
"3 人观看"
"11 人观看"

这正是国际化库发挥关键作用的地方。 每种语言在何时以及如何处理复数都有自己的规则, 因此最好依赖专门的库来保证正确实现。

一个优秀的国际化库会完成两件事:

  1. 根据 locale 决定应使用哪种复数形式(onemanyother 等)
  2. 在对应语言中找到与该形式匹配的翻译

如果你已经在使用国际化库, 请查阅其文档中关于复数格式的说明。 几乎所有库都有专门介绍如何处理复数渲染的文档。

将所有构建模块组合在一起

如果你还没有使用国际化库,可以考虑试试 gt-react!

gt-react 的 <Plural> 组件:

  • 提供一种简单实用的方式,以正确渲染复数形式
  • 可与 <Num> 格式化组件无缝配合使用
  • 可与 <T> 翻译组件无缝配合使用,该组件集成了我们的免费翻译服务,可自动生成复数形式

将所有构建模块组合在一起后,我们就拥有了一个完整的多语言界面:

import { T, Plural, Num } from 'gt-react'

// 开箱即用,支持 100+ 种语言
function Example({ count }) {
  return (
    <T>
      <Plural
        n={count}
        zero={'暂无观看'}
        one={
          <>
            <Num>{count}</Num> 人正在观看
          </>
        }
      >
        <Num>{count}</Num> 人正在观看
      </Plural>
    </T>
  )
}

想了解更多?请查看我们的文档,了解如何配置 gt-reactgt-next