欢迎光临
我们一直在努力

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

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

前言

各位前端小伙伴,今天咱们来聊聊国际化中最容易踩坑的领域——复数规则。想象一下:

  • 英语: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 一 英语、法语、中文(仅1)
two 二 阿拉伯语、威尔士语
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来帮你搞定!


    如果这篇文章对你有帮助,欢迎点赞、收藏、转发!你的支持是我最大的动力~

    赞(0)
    未经允许不得转载:171主机测评 » 前端国际化:复数规则与文案匹配深度解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址