鸿蒙应用全球购物车 — 技术实现篇


一、业务需求(为什么这么设计)
"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 同理
];
要点:
names是 locale → 名称映射:商品名随语言切换,取不到时回退英文(itemName()逻辑);price存纯数字:显示时按当前 locale 的币种格式化(fmtMoney),数据层不绑定币种;id是稳定 key:ForEach用item.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 兼容要点
PluralRules.select(n)返回string,直接比较即可(cat === 'one');- 复数文案表用
Map<string, string>+buildPluralTexts(lang)工厂函数构建(避免嵌套字面量推断问题); String.replace('{n}', n)第二个参数需转字符串(${n});@State数组更新用slice()拷贝后整体赋值;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 条结果”)都是同一套模板。
更多推荐




所有评论(0)