在这里插入图片描述
在这里插入图片描述

一、业务需求(为什么这么设计)

"3 件商品"翻译成英语是 “3 items”,翻译成俄语是 “3 товара”,翻译成波兰语是 “3 produkty”——同一个数字 3,不同语言的量词形态完全不同。这不是翻译能解决的,是复数语法规则:英语只有单/复两形,俄语有单数(1)/少量(2-4)/多数(5+)三形,中文根本没有复数形态。

购物车是最典型的复数场景:商品数量、结算提示、未读数、库存警告,全部需要按语言的复数规则选择正确形态。本应用用全球购物车承载 PluralRules 能力。

二、总体架构

┌─ 数据层:CART(商品,names 多语言表 + 单价)+ LANGS(5 语言)
├─ 复数层:buildPluralTexts(每语言 category→文案表)+ pluralCategory()
├─ 格式化层:itemWord / cartMessage / fmtMoney
└─ 状态层:@StorageLink 语言 + @State qty[] / demoN

核心设计:先选 category,再选文案PluralRules.select(n) 返回该数字在目标语言下的复数分类('one' | 'few' | 'many' | 'other'),再用分类索引文案表取对应形态——两步分离,文案维护与语法判定解耦。

数据模型:语言表与购物车

语言表定义 5 种语言,覆盖三类复数体系(英语 2 类、俄语/波兰语 4 类、中文/日语 1 类):

interface LangCfg {
  code: string;
  name: string;
  flag: string;
}

const LANGS: LangCfg[] = [
  { code: 'en_US', name: 'English', flag: '🇺🇸' },
  { code: 'zh_CN', name: '简体中文', flag: '🇨🇳' },
  { code: 'ru_RU', name: 'Русский', flag: '🇷🇺' },
  { code: 'pl_PL', name: 'Polski', flag: '🇵🇱' },
  { code: 'ja_JP', name: '日本語', flag: '🇯🇵' }
];

购物车商品把"名称"建模为多语言表(Map<locale, name>),价格是纯数字:

interface CartItem {
  id: string;
  names: Map<string, string>;   // locale -> 商品名
  price: number;                // 本币金额
}

const CART: CartItem[] = [
  {
    id: 'c1',
    names: buildNames([
      ['en_US', 'Wireless Mouse'], ['zh_CN', '无线鼠标'], ['ru_RU', 'Беспроводная мышь'],
      ['pl_PL', 'Myszka bezprzewodowa'], ['ja_JP', 'ワイヤレスマウス']
    ]),
    price: 25.99
  },
  // c2 USB-C Cable、c3 Laptop Stand 同理
];

要点:

  1. names 是 locale → 名称映射:商品名随语言切换,取不到时回退英文(itemName() 逻辑);
  2. price 存纯数字:显示时按当前 locale 的币种格式化(fmtMoney),数据层不绑定币种;
  3. id 是稳定 keyForEachitem.id

三、核心实现

3.1 复数分类(本应用核心)

function pluralCategory(code: string, n: number): string {
  try {
    const pr = new intl.PluralRules(code);
    return pr.select(n);
  } catch (err) {
    return 'other';
  }
}

pr.select(n) 返回 'zero' | 'one' | 'two' | 'few' | 'many' | 'other' 六种分类之一。各语言的分类规则(ICU 标准):

语言 n=1 n=2 n=3 n=5 分类数
en_US one other other other 2
zh_CN other other other other 1(无复数)
ru_RU one few few many 4
pl_PL one few few many 4
ja_JP other other other other 1

注意:中文/日文的 select(1) === 'other'——没有复数语法时所有数字都是 other。这是 PluralRules 的重要认知:分类是语法规则,不是"1 就是 one"。

3.2 文案形态表

每个语言维护 category → 文案 表,用分类索引:

/** 各语言的复数形态文案(category -> 文案) */
function buildPluralTexts(lang: string): PluralTexts {
  const item = new Map<string, string>();
  const msg = new Map<string, string>();
  if (lang === 'en_US') {
    item.set('one', 'item'); item.set('other', 'items');
    msg.set('one', 'You have 1 item in your cart');
    msg.set('other', 'You have {n} items in your cart');
  } else if (lang === 'zh_CN') {
    item.set('other', '件商品');
    msg.set('other', '购物车中共有 {n} 件商品');
  } else if (lang === 'ru_RU') {
    item.set('one', 'товар'); item.set('few', 'товара'); item.set('many', 'товаров');
    item.set('other', 'товара');
    msg.set('one', 'В корзине 1 товар');
    msg.set('few', 'В корзине {n} товара');
    msg.set('many', 'В корзине {n} товаров');
    msg.set('other', 'В корзине {n} товара');
  }
  // pl_PL、ja_JP 同理
  return { itemForms: item, messageForms: msg };
}

文案表人工维护,分类自动判定——PluralRules 只回答"这个数字是什么类",具体用哪个词形由翻译团队维护,职责清晰。

3.3 消息模板替换

function cartMessage(code: string, n: number): string {
  const cat = pluralCategory(code, n);
  const texts = buildPluralTexts(code);
  const tmpl = texts.messageForms.get(cat) ?? '';
  return tmpl.replace('{n}', `${n}`);
}
// ru, n=3: "В корзине 3 товара"
// ru, n=5: "В корзине 5 товаров"

模板里用 {n} 占位符,避免把数字硬拼进不同语序的句子(俄语数字位置与英语不同,模板天然解决语序问题)。

3.4 数量步进器联动

@State qty: number[] 三个商品各自数量,-/+ 按钮更新数组(slice() 拷贝后整体赋值触发刷新),结算区实时重算:

private adjust(i: number, delta: number): void {
  const next = Math.max(1, this.qty[i] + delta);
  const arr = this.qty.slice();
  arr[i] = next;
  this.qty = arr;
}

private totalQty(): number {
  let s = 0;
  this.qty.forEach((q: number) => { s += q; });
  return s;
}

结算提示 cartMessage(locale, totalQty()) 随总数变化自动选对复数形态。

3.5 复数探索器

private demoCat(): string {
  return pluralCategory(this.currentLocale, this.demoN);
}

探索器让用户拨动 demoN,实时看到每个数字在目标语言下的分类——“为什么 21 在波兰语里是 one,2 却是 few”,一眼看懂语法规则。

四、语言与降级

5 种语言覆盖三类复数体系(英:2 类;俄/波:4 类;中/日:1 类)。商品名按语言取表,缺失回退英文(与 01 内容层降级一致)。

五、ArkTS 兼容要点

  1. PluralRules.select(n) 返回 string,直接比较即可(cat === 'one');
  2. 复数文案表用 Map<string, string> + buildPluralTexts(lang) 工厂函数构建(避免嵌套字面量推断问题);
  3. String.replace('{n}', n) 第二个参数需转字符串(${n});
  4. @State 数组更新用 slice() 拷贝后整体赋值;
  5. intl 调用 try/catch 兜底。

六、性能与扩展

  • 每次 pluralCategory 新建 PluralRules:购物车 3 项 + 结算 1 次 + 探索器 1 次 ≈ 5 次/渲染,可接受;长列表应缓存 code → PluralRules
  • 扩展:序数(ordinal 类型,1st/2nd/3rd)、zero 分类(阿拉伯语 0 单独形态)、复数与货币组合(“3 items for $25.99”)。

七、小结

复数本地化的正确姿势是两步走PluralRules.select() 判定分类(语法层),文案表按分类取形态(翻译层)。把"这个数字该用什么形式"交给 ICU 标准实现,把"每种形式写什么词"交给翻译——两者永远不混。购物车、消息未读数、通知中心、搜索结果(“找到 3 条结果”)都是同一套模板。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐