news 2026/9/19 20:11:15

Front-End-Checklist 复数化(Pluralization)实战指南:用 Intl.PluralRules 与 ICU MessageFormat 正确处理多语言复数形态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Front-End-Checklist 复数化(Pluralization)实战指南:用 Intl.PluralRules 与 ICU MessageFormat 正确处理多语言复数形态

Front-End-Checklist 复数化(Pluralization)实战指南:用 Intl.PluralRules 与 ICU MessageFormat 正确处理多语言复数形态

【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist

本文是 Front-End-Checklist 项目中 i18n 类别下的pluralization规则(关联文档:skills/pluralization/references/rule.md)的完整技术解读。围绕“如何用Intl.PluralRules或 ICU 感知的 i18n 库,为每种语言选择正确的语法复数类别”这一核心主题展开,包含完整的代码示例、CLDR 复数类别对照表、react-intl / i18next 接入方案、序数处理、反模式清单与可落地的验证方案。读完本文,你将掌握在任意多语言前端项目中彻底替换count === 1 ? singular : plural二元判断的能力,并能在代码评审与 CI 中系统化地发现复数化缺陷。

为什么复数化是国际化中最容易被忽视的坑

每一种语言都有自己关于“名词如何随数量变化”的规则。英语只有两种形式——数量恰好为 1 时用单数(singular),其余情况一律用复数(plural)。但阿拉伯语、俄语、波兰语等大量语言分别使用三种、四种甚至六种不同的语法形式。Unicode CLDR 项目系统性地收录了这些规则,并为每种语言分配了一组命名复数类别(plural categories)。

Front-End-Checklist 将该规则定位为medium 优先级、intermediate 难度、约 25 分钟工作量(见 packages/content/rules/en/i18n/pluralization.mdx),原因在于:它的修复本身不难,但漏修时产生的错误极其隐蔽——在英语开发环境下完全看不出问题,一旦发布到阿拉伯语、俄语等市场,页面就会输出语法错误的文案,直接损害翻译质量与本地用户信任。

核心结论:在英语里成立count === 1 ? singular : plural的二元判断,放到阿拉伯语上会在六种可能的计数中有五种输出错误的语法形式。

CLDR 复数类别:六种类别与各语言子集

CLDR 定义了Intl.PluralRules使用的六种类别名称:zeroonetwofewmanyother。每种语言只使用其中一部分类别:

语言使用的类别示例计数
英语one,other1 → one;0、2–∞ → other
德语one,other1 → one;0、2–∞ → other
法语one,other0、1 → one;2–∞ → other
俄语one,few,many,other1、21、31 → one;2–4、22–24 → few;5–20、25–30 → many;小数 → other
波兰语one,few,many,other1 → one;2–4(不含 12–14)→ few;5–21... → many
阿拉伯语zero,one,two,few,many,other全部六种类别,复杂的取模规则
日语other所有计数只有一种形式
捷克语one,few,many,other有生命/无生命性别的差异影响类别选择

这张表本身就足以说明问题:同样是“1 件商品”,英语、法语、俄语、阿拉伯语各自落入不同的类别集合;同样是“0 件”,法语归入one、英语归入other。任何硬编码的英语语法后缀(比如count === 1 ? '' : 's')都无法在这些语言上成立。

用 Intl.PluralRules 选择正确的翻译键

Intl.PluralRules接收一个 locale,并为给定的 count 返回对应的 CLDR 类别。用返回的类别去翻译文件中选取预先翻译好的字符串变体即可:

// pluralize.ts /** * Return the CLDR plural category for a count in a given locale. * The returned key maps to one of the message variants in your * translation file (e.g. messages.items.one, messages.items.other). */ export function getPluralCategory( count: number, locale: string ): Intl.LDMLPluralRule { return new Intl.PluralRules(locale).select(count); } // Usage example const locale = 'ru'; // Russian const messages = { items: { one: '{count} элемент', few: '{count} элемента', many: '{count} элементов', other: '{count} элемента', // decimals }, }; function formatItemCount(count: number, locale: string): string { const category = getPluralCategory(count, locale); const template = messages.items[category] ?? messages.items.other; return template.replace('{count}', String(count)); } formatItemCount(1, 'ru'); // "1 элемент" formatItemCount(3, 'ru'); // "3 элемента" formatItemCount(11, 'ru'); // "11 элементов" formatItemCount(21, 'ru'); // "21 элемент"

注意?? messages.items.other这一兜底逻辑:当某个类别缺失时退回到other,避免运行时崩溃——但正如后文警示的,兜底不等于正确。

缓存 PluralRules 实例:不要在渲染循环里反复构造

每次调用都 new 一个Intl.PluralRules对象代价高昂。应该按 locale 用Map缓存实例,避免在渲染循环或消息格式化函数中反复分配:

// Cached version const cache = new Map<string, Intl.PluralRules>(); function getPluralRules(locale: string): Intl.PluralRules { if (!cache.has(locale)) { cache.set(locale, new Intl.PluralRules(locale)); } return cache.get(locale)!; }

仓库中的真实实现

Front-End-Checklist 仓库的 i18n 包正是按这一思路实现的。packages/i18n/src/utils.ts 中定义了getPlural工具:

/** * Get plural form based on count */ export function getPlural(count: number, locale: SupportedLocale): Intl.LDMLPluralRule { const pr = new Intl.PluralRules(locale) return pr.select(count) }

它与 packages/i18n/src/index.ts 一起被导出(export { formatDate, formatRelativeTime, getNativeName, getPlural, i18n, isRTL }),并有对应的单元测试覆盖:packages/i18n/src/tests/i18n.test.ts 中的expect(getPlural(2, 'en')).toBe('other')验证了英语计数 2 落入other类别。测试同时覆盖了initI18n从存储恢复语言偏好、changeLanguage持久化选择、isRTL('ar')对阿拉伯语从右到左的判定等行为,可作为你落地时编写测试的参考。该包支持的 14 种 locale(en/fr/es/de/it/pt/ja/zh/ko/ru/ar/he/fa/ur)定义在 packages/i18n/src/types.ts,其中既有ruar这类多类别语言,也有ja这类仅other的语言,正好覆盖了复数化的各种极端情况。

ICU MessageFormat 与 react-intl:声明式复数选择

react-intl 使用 ICU MessageFormat 语法,把复数选择声明式地写进消息字符串本身。所有复数变体定义在翻译文件中,由库自动挑选正确的那个:

// en.json { "inbox.messageCount": "{count, plural, one {You have # message} other {You have # messages}}", "cart.itemCount": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}" }
// ar.json (Arabic — all 6 categories required) { "inbox.messageCount": "{count, plural, zero {ليس لديك رسائل} one {لديك رسالة واحدة} two {لديك رسالتان} few {لديك # رسائل} many {لديك # رسالة} other {لديك # رسالة}}" }
// InboxCount.tsx import { useIntl } from 'react-intl'; interface Props { count: number; } export function InboxCount({ count }: Props) { const intl = useIntl(); return ( <p> {intl.formatMessage( { id: 'inbox.messageCount' }, { count } )} </p> ); }

关键语法点:

  • plural块内的大括号分支键就是 CLDR 类别名(zero/one/two/few/many/other),也可以直接用=0=1这样的精确数字匹配(如cart.itemCount中的=0)。
  • #令牌会被替换为格式化后的 count 值,无需手动拼接数字。
  • 库会根据当前 locale 与 count,选择键匹配 CLDR 类别的那个分支。

这种方式的优势在于复数逻辑完全由数据驱动:翻译人员负责填充所有分支,开发人员只负责传count,不会出现“代码写死英语单复数、翻译无法覆盖”的结构性问题。

序数复数形式:1st / 2nd / 3rd

Intl.PluralRules通过type: 'ordinal'选项同样处理序数(1st、2nd、3rd):

const ordinal = new Intl.PluralRules('en-US', { type: 'ordinal' }); const suffixes: Record<Intl.LDMLPluralRule, string> = { one: 'st', two: 'nd', few: 'rd', other: 'th', zero: 'th', // not used in English ordinals many: 'th', // not used in English ordinals }; function formatOrdinal(n: number): string { const category = ordinal.select(n); return `${n}${suffixes[category]}`; } formatOrdinal(1); // "1st" formatOrdinal(2); // "2nd" formatOrdinal(3); // "3rd" formatOrdinal(4); // "4th" formatOrdinal(21); // "21st"

注意序数类别与基数类别是两套独立的规则:英语序数里1→one2→two3→few、其余→other,与基数“1 单数其余复数”完全不同。type默认值为cardinal,需要序数时必须显式传入{ type: 'ordinal' }

i18next 中的复数化:后缀键 + intlPlurals 插件

i18next 通过追加由Intl.PluralRules推导出的后缀来解析复数键。配置 i18next 实例使用内置的intlPlurals插件,并在翻译 JSON 中定义带后缀的键:

// i18n.ts import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; i18next .use(initReactI18next) .init({ lng: 'en', resources: { en: { translation: { // i18next v4+ uses _one / _other suffixes by default itemCount_one: 'You have {{count}} item', itemCount_other: 'You have {{count}} items', }, }, ar: { translation: { // Arabic needs all 6 suffixes: _zero _one _two _few _many _other itemCount_zero: 'ليس لديك عناصر', itemCount_one: 'لديك عنصر واحد', itemCount_two: 'لديك عنصران', itemCount_few: 'لديك {{count}} عناصر', itemCount_many: 'لديك {{count}} عنصرًا', itemCount_other: 'لديك {{count}} عنصر', }, }, }, });
// Usage in a component import { useTranslation } from 'react-i18next'; function ItemCount({ count }: { count: number }) { const { t } = useTranslation(); // i18next selects the correct _suffix key automatically return <span>{t('itemCount', { count })}</span>; }

调用t('itemCount', { count })时,i18next 自动补全为itemCount_one/itemCount_other(或阿拉伯语的六个后缀键之一),组件代码完全不感知复数类别。仓库自身的 i18n 包同样基于 i18next + react-i18next 构建,packages/i18n/src/index.ts 展示了initI18n的初始化方式:fallbackLng指向DEFAULT_CONFIG.defaultLocale,并关闭了插值转义(escapeValue: false)。

缺失复数键的静默回退陷阱

如果某个翻译文件漏掉了其 locale 必需的复数键(例如阿拉伯语少了_many),大多数 i18n 库会静默回退到_other。结果输出在语法上是错的,而且这个 bug 在英语开发环境中完全不可见。因此必须把复数键完整性校验纳入 CI 翻译检查

反模式清单:评审时一眼就能识别的坏味道

// ❌ Binary branch — wrong for Arabic, Russian, Polish, and many others const label = count === 1 ? 'item' : 'items'; // ❌ String concatenation — untranslatable word order const message = 'You have ' + count + ' new messages'; // ❌ Hardcoded English grammar suffix const suffix = count === 1 ? '' : 's'; const label = `${count} message${suffix}`; // ✅ Let Intl.PluralRules select the right pre-translated form const category = new Intl.PluralRules(locale).select(count); const label = translations[category].replace('{count}', String(count)); // ✅ Or use ICU syntax in your i18n library // "{count, plural, one {# message} other {# messages}}"

三类反模式的共同问题:

  1. 二元分支:只适用于英语等少数语言,对其他语言必然出错。
  2. 字符串拼接:不仅复数错误,还把词序写死(“You have N new messages”无法翻译成“你有 N 条新消息”这类词序不同的语言),从根本上破坏可翻译性。
  3. 硬编码英语后缀:把英语语法规则(加s)泄漏进代码,等于宣告该应用永不支持非英语市场。

代码评审时,对渲染计数的组件与翻译文件重点检查:任何“数字 + 文本”的拼接、任何只在 single/plural 之间二选一的三元表达式、任何缺少目标 locale 必需复数键的 i18next/react-intl 消息。

验证方案:从 CI 自动化到人工抽查

该规则在 packages/content/rules/en/i18n/pluralization.mdx 的sources中明确了验证基准:以 MDN 的Intl.PluralRules文档与 Unicode CLDR Plural Rules 规范为准绳,检查渲染后的国际化行为本身,而不是只看源字符串或配置。

自动化检查

  • 全库搜索涉及计数的字符串拼接模式——任何count + " item"、含 count 变量的模板字符串、未经 i18n 库委托的三元count === 1 ?判断。
  • 在 CI 中增加 lint 步骤或自定义 ESLint 规则,对 i18n 感知的文件中名为counttotallength的变量参与模板字符串或字符串拼接的情况给出警告。

人工检查

  • 对英语之外的每个受支持 locale,打开翻译文件,逐一确认该 locale 的 CLDR 规则要求的全部复数类别键都存在(阿拉伯语需 6 个、俄语需 4 个、日语 1 个即可)。
  • 在 Storybook story 或测试中以locale='ar'(阿拉伯语)渲染展示计数的组件,分别传入 0、1、2、5、11 覆盖全部六种 CLDR 类别。仓库的 packages/i18n/src/tests/i18n.test.ts 可作为这类测试的范例——它用getPlural(2, 'en')验证类别归属,实际项目可扩展为对arru逐一断言。

相关规则联动

复数化并非孤立的国际化问题。在 packages/content/rules/en/i18n/pluralization.mdx 的relatedRules中,该规则与以下规则联动,评审时建议一并检查:

  • currency-formatting:两者都依赖 Intl 家族 API 的 locale 感知逻辑,locale 差异被忽略时都会静默出错;
  • text-expansion:复数形式可能比单数形式显著更长,布局必须像容纳翻译文本膨胀一样容纳复数膨胀;
  • translation-stringslistitem:在实际审计中常与复数化规则相交,影响同一处实现决策。

总结

复数化的正确做法是一条清晰的技术路线:永远不要手写单复数分支或拼接计数文案,让Intl.PluralRules或 ICU 感知的 i18n 库(react-intl、i18next)基于 CLDR 规则替你选择语法类别。落地时注意三点:按 locale 缓存Intl.PluralRules实例避免性能损耗;为每个目标 locale 补齐其全部必需复数键并在 CI 中校验完整性;用阿拉伯语等六类别语言 + 0/1/2/5/11 计数做测试覆盖。做到这三点,你的应用就能在任意语言下输出语法正确的数量文案——这正是 Front-End-Checklist 这份清单存在的意义:把最容易在“看不见的地方”翻车的国际化细节,变成可检查、可测试、可评审的工程规范。

【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 20:10:04

用 D435i 做室内避障:从接线到跑通的快速路径

用 D435i 做室内避障&#xff1a;从接线到跑通的快速路径 【免费下载链接】librealsense RealSense SDK 项目地址: https://gitcode.com/GitHub_Trending/li/librealsense 场景很具体&#xff1a;机器人向前开&#xff0c;前方桌角离它 60 厘米&#xff0c;你得拿到一个…

作者头像 李华
网站建设 2026/9/19 20:04:52

Ray 蒙特卡洛估算 π 实战:用 Task 并行采样、Actor 跟踪进度

Ray 蒙特卡洛估算 π 实战&#xff1a;用 Task 并行采样、Actor 跟踪进度 【免费下载链接】ray Ray is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads. 项目地址: https://gitcode.com/gh_mir…

作者头像 李华
网站建设 2026/9/19 20:03:46

RIOT 中 C++ 与 C 混合编程实战指南:以 riot_and_cpp 示例为例

RIOT 中 C 与 C 混合编程实战指南&#xff1a;以 riot_and_cpp 示例为例 【免费下载链接】RIOT RIOT - The friendly OS for IoT 项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT 本篇技术指南围绕 RIOT 官方语言支持示例中的 riot_and_cpp 展开&#xff0c;系…

作者头像 李华