前端国际化:复数规则与文案匹配深度解析

前端国际化:复数规则与文案匹配深度解析 前端国际化复数规则与文案匹配深度解析前言各位前端小伙伴今天咱们来聊聊国际化中最容易踩坑的领域——复数规则。想象一下英语1 apple, 2 apples简单的单复数中文1个苹果, 2个苹果几乎不需要变法语1 pomme, 2 pommes, 0 pommes和英语类似俄语1 яблоко, 2 яблока, 5 яблок三种复数形式阿拉伯语1 تفاحة, 2 تفاحتين, 3-10 تفاحات, 11-99 تفاحة, 100 تفاحات五种复数形式是不是觉得头大别急今天咱们就用JavaScript的Intl.PluralRules来搞定这些复杂的复数规则Intl.PluralRules 基础创建复数规则判断器// 中文复数规则 const chinesePlural new Intl.PluralRules(zh-CN); console.log(chinesePlural.select(0)); // other console.log(chinesePlural.select(1)); // one console.log(chinesePlural.select(2)); // other console.log(chinesePlural.select(100)); // other // 英语复数规则 const englishPlural new Intl.PluralRules(en-US); console.log(englishPlural.select(0)); // other console.log(englishPlural.select(1)); // one console.log(englishPlural.select(2)); // other console.log(englishPlural.select(100)); // other // 俄语复数规则三种形式 const russianPlural new Intl.PluralRules(ru-RU); console.log(russianPlural.select(1)); // one console.log(russianPlural.select(2)); // few console.log(russianPlural.select(5)); // many console.log(russianPlural.select(11)); // many console.log(russianPlural.select(21)); // one复数类别说明类别含义常见语言zero零阿拉伯语one一英语、法语、中文仅1two二阿拉伯语、威尔士语few少量俄语2-4、波兰语many大量俄语≥5、阿拉伯语other其他所有语言都有实战复数文案匹配基础复数文案const messages { en-US: { apple: { one: 1 apple, other: {count} apples }, banana: { one: 1 banana, other: {count} bananas } }, zh-CN: { apple: { one: 1个苹果, other: {count}个苹果 }, banana: { one: 1根香蕉, other: {count}根香蕉 } }, ru-RU: { apple: { one: 1 яблоко, few: {count} яблока, many: {count} яблок, other: {count} яблока } } }; function getPluralMessage(locale, key, count) { const pluralRules new Intl.PluralRules(locale); const category pluralRules.select(count); const message messages[locale]?.[key]; if (!message) { return ${count} ${key}; } const template message[category] || message.other; return template.replace({count}, String(count)); } // 使用示例 console.log(getPluralMessage(en-US, apple, 1)); // 1 apple console.log(getPluralMessage(en-US, apple, 5)); // 5 apples console.log(getPluralMessage(zh-CN, apple, 1)); // 1个苹果 console.log(getPluralMessage(zh-CN, apple, 5)); // 5个苹果 console.log(getPluralMessage(ru-RU, apple, 1)); // 1 яблоко console.log(getPluralMessage(ru-RU, apple, 2)); // 2 яблока console.log(getPluralMessage(ru-RU, apple, 5)); // 5 яблок复杂场景时间单位function formatTimeAgo(seconds, locale zh-CN) { const units [ { unit: year, seconds: 31536000 }, { unit: month, seconds: 2592000 }, { unit: day, seconds: 86400 }, { unit: hour, seconds: 3600 }, { unit: minute, seconds: 60 }, { unit: second, seconds: 1 } ]; const messages { zh-CN: { year: { one: 1年前, other: {count}年前 }, month: { one: 1个月前, other: {count}个月前 }, day: { one: 1天前, other: {count}天前 }, hour: { one: 1小时前, other: {count}小时前 }, minute: { one: 1分钟前, other: {count}分钟前 }, second: { one: 刚刚, other: {count}秒前 } }, en-US: { year: { one: 1 year ago, other: {count} years ago }, month: { one: 1 month ago, other: {count} months ago }, day: { one: 1 day ago, other: {count} days ago }, hour: { one: 1 hour ago, other: {count} hours ago }, minute: { one: 1 minute ago, other: {count} minutes ago }, second: { one: just now, other: {count} seconds ago } } }; for (const { unit, seconds: unitSeconds } of units) { const count Math.floor(seconds / unitSeconds); if (count 1) { const pluralRules new Intl.PluralRules(locale); const category pluralRules.select(count); const message messages[locale]?.[unit]; const template message?.[category] || message?.other || ${count} ${unit}s ago; return template.replace({count}, String(count)); } } return locale zh-CN ? 刚刚 : just now; } // 使用示例 console.log(formatTimeAgo(30, zh-CN)); // 30秒前 console.log(formatTimeAgo(60, zh-CN)); // 1分钟前 console.log(formatTimeAgo(3600, zh-CN)); // 1小时前 console.log(formatTimeAgo(86400, en-US)); // 1 day ago console.log(formatTimeAgo(172800, en-US)); // 2 days ago复数规则进阶获取复数规则详情function getPluralCategories(locale) { const rules new Intl.PluralRules(locale); const categories new Set(); // 测试常见数值获取所有类别 for (let i 0; i 100; i) { categories.add(rules.select(i)); } return Array.from(categories); } console.log(getPluralCategories(en-US)); // [one, other] console.log(getPluralCategories(zh-CN)); // [one, other] console.log(getPluralCategories(ru-RU)); // [one, few, many, other] console.log(getPluralCategories(ar-SA)); // [zero, one, two, few, many, other]自定义复数规则function createCustomPluralRules(categories, rules) { return { select: (count) { for (const [category, condition] of Object.entries(rules)) { if (condition(count)) { return category; } } return other; } }; } // 自定义中文复数规则区分0、1、多 const customChineseRules createCustomPluralRules( [zero, one, other], { zero: (n) n 0, one: (n) n 1 } ); console.log(customChineseRules.select(0)); // zero console.log(customChineseRules.select(1)); // one console.log(customChineseRules.select(5)); // other实战案例购物车商品数量国际化购物车组件class ShoppingCart { constructor(locale zh-CN) { this.locale locale; this.items []; this.pluralRules new Intl.PluralRules(locale); this.messages { zh-CN: { empty: 购物车是空的, item: { one: 购物车中有 1 件商品, other: 购物车中有 {count} 件商品 }, checkout: 去结算 }, en-US: { empty: Your cart is empty, item: { one: There is 1 item in your cart, other: There are {count} items in your cart }, checkout: Checkout }, ru-RU: { empty: Корзина пустая, item: { one: В корзине 1 товар, few: В корзине {count} товара, many: В корзине {count} товаров, other: В корзине {count} товара }, checkout: Оформить заказ } }; } addItem(item) { this.items.push(item); } removeItem(index) { this.items.splice(index, 1); } getStatusMessage() { const count this.items.length; if (count 0) { return this.messages[this.locale].empty; } const category this.pluralRules.select(count); const message this.messages[this.locale].item; const template message[category] || message.other; return template.replace({count}, String(count)); } } // 使用示例 const cart new ShoppingCart(ru-RU); console.log(cart.getStatusMessage()); // Корзина пустая cart.addItem({ name: Товар 1 }); console.log(cart.getStatusMessage()); // В корзине 1 товар cart.addItem({ name: Товар 2 }); console.log(cart.getStatusMessage()); // В корзине 2 товара cart.addItem({ name: Товар 3 }); cart.addItem({ name: Товар 4 }); cart.addItem({ name: Товар 5 }); console.log(cart.getStatusMessage()); // В корзине 5 товаров结合 i18next 使用配置复数规则import i18n from i18next; import { initReactI18next } from react-i18next; i18n .use(initReactI18next) .init({ resources: { en: { translation: { items: { one: 1 item, other: {{count}} items } } }, ru: { translation: { items: { one: 1 товар, few: {{count}} товара, many: {{count}} товаров, other: {{count}} товара } } } }, lng: en, interpolation: { escapeValue: false }, pluralSeparator: _, keySeparator: false }); // 使用 i18n.t(items, { count: 1 }); // 1 item i18n.t(items, { count: 2 }); // 2 items i18n.changeLanguage(ru); i18n.t(items, { count: 2 }); // 2 товара i18n.t(items, { count: 5 }); // 5 товаров最佳实践总结1. 预定义复数文案const pluralMessages { en-US: { notification: { one: You have 1 notification, other: You have {{count}} notifications } } };2. 统一复数处理函数function translatePlural(key, count, locale) { const pluralRules new Intl.PluralRules(locale); const category pluralRules.select(count); const messages getMessages(locale); const template messages[key]?.[category] || messages[key]?.other; return template?.replace({{count}}, String(count)) || ${count}; }3. 测试各种边界情况function testPluralRules(locale, key) { const testCases [0, 1, 2, 3, 4, 5, 10, 11, 20, 21, 100]; console.log(Testing ${locale} - ${key}:); for (const count of testCases) { console.log( ${count}: ${translatePlural(key, count, locale)}); } }常见问题与解决方案Q1: 某些语言复数规则太复杂解决方案使用成熟的国际化库// i18next 自动处理复数规则 i18n.t(items, { count: 5 });Q2: 如何处理小数的复数解决方案根据实际需求决定是否取整function selectPluralCategory(count, locale) { const rules new Intl.PluralRules(locale); // 如果是小数根据业务规则处理 if (!Number.isInteger(count)) { count Math.floor(count); // 或 Math.round(count) } return rules.select(count); }Q3: 如何处理范围值解决方案分别处理起始和结束值function formatRange(start, end, locale) { const startCategory new Intl.PluralRules(locale).select(start); const endCategory new Intl.PluralRules(locale).select(end); // 根据业务逻辑决定如何显示 if (start end) { return getPluralMessage(locale, item, start); } return ${start}-${end} items; }总结复数规则的国际化确实复杂但只要掌握了Intl.PluralRules就能轻松应对各种语言的复数需求。关键记住不要硬编码复数规则使用Intl.PluralRules自动判断预定义所有复数类别确保覆盖zero、one、two、few、many、other测试边界情况0、1、2、特殊数字如11、21等下次遇到复数文案问题别再手动判断了让Intl API来帮你搞定如果这篇文章对你有帮助欢迎点赞、收藏、转发你的支持是我最大的动力