HarmonyOS多语商品价签连载01:金额单位与本地化展示分离【共创稿事节】
商品价签切换语言时,金额里的分组符和小数符也会变化。中文页面上的一千二百九十九元,在德语区域习惯下会采用另一种数字写法。如果业务把格式化字符串保存成价格,下次切换区域再从字符串取回数值,就可能把分组符误认成小数符,让同一商品出现不同金额。
这套多语商品价签工具把金额保存为最小货币单位的整数,区域设置只参与展示。手机上可以查看单个价签,平板后续用于并排核对不同语言版本。开篇聚焦金额单位、数字格式化和页面状态三者的分工,为后续多语文案、长文本适配和导出检查建立一致的数据入口。

一、先定义价格的数据契约
示例商品为轻量保温壶,价格记录保存商品身份、名称、币种和整数金额。人民币记录的一元对应一百分,因此一千二百九十九元保存为129900。展示时除以一百得到主要单位数值,原始记录保持不变。
| 字段 | 样例 | 含义 |
|---|---|---|
| productId | flask-light | 稳定商品身份 |
| title | 轻量保温壶 | 当前显示名称 |
| currency | CNY | 币种标识 |
| minor | 129900 | 以分为单位的整数金额 |
| locale | de-DE | 当前数字展示区域 |
locale 属于展示状态,不属于商品价格本身。改变它不会执行换汇,也不会改变货币种类。本篇固定支持人民币和两位小数,后续接入其他币种时,应明确该币种的最小单位与小数位规则,不能把除以一百当成全部币种通用的公式。
// model/PriceRecord.ets
export interface PriceRecord {
productId: string;
title: string;
currency: string;
minor: number;
}
export function validatePrice(record: PriceRecord): void {
if (record.productId.trim().length === 0 || record.currency !== 'CNY') {
throw new Error('商品身份或币种不符合当前价签规则');
}
if (!Number.isSafeInteger(record.minor) ||
record.minor < 0 || record.minor > 100000000) {
throw new Error('金额必须为业务范围内的非负整数分');
}
}
金额上限是一百万元,对应一亿分,是示例产品明确的业务约束,并非平台金额上限。safe integer 检查防止输入已经失去整数精度,业务上限进一步收紧取值范围。负数、非数值和小数分都在入口拒绝,格式化函数不承担修正错误金额的职责。
二、把数字格式化放到展示出口
数字格式化使用 Localization Kit 的 intl.NumberFormat,输入为数值与区域,输出是给用户阅读的字符串。华为数字格式化指南提供该模块的导入与格式化方式。这里选择普通十进制格式,并把币种作为独立文字展示,避免不同区域使用相同货币符号时产生歧义。
// model/PriceFormat.ets
import { intl } from '@kit.LocalizationKit';
import { PriceRecord, validatePrice } from './PriceRecord';
export function displayPrice(record: PriceRecord, locale: string): string {
validatePrice(record);
if (locale !== 'zh-CN' && locale !== 'de-DE') {
throw new Error('暂不支持此展示区域');
}
const formatter: intl.NumberFormat = new intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: true
});
return formatter.format(record.minor / 100);
}
支持区域采用明确的允许列表,避免从任意输入拼出区域参数。格式化器对区域和选项敏感,如果后续建立缓存,键应包含区域、小数位、分组策略等全部影响展示的配置。只按语言缓存,会把同一语言下不同地区的格式错误地合并。
这段逻辑有意不使用 parseFloat,也不把逗号和点号机械互换。格式化结果是展示终点,不能作为数据回写入口。字符串中还可能存在空格或区域相关符号,依靠删除标点恢复金额会把显示习惯误当成存储协议。
三、切换区域采用成功后提交状态
切换按钮不能先修改区域标签,再等待价格转换成功。如果转换失败,页面会出现“德语区域配着上一份中文格式”的混合状态。正确的顺序是先准备候选结果,成功后一起提交区域与展示值,失败时保留之前的有效组合,并展示可理解的错误信息。
// pages/Index.ets
import { PriceRecord } from '../model/PriceRecord';
import { displayPrice } from '../model/PriceFormat';
@Entry
@Component
struct Index {
private record: PriceRecord = {
productId: 'flask-light', title: '轻量保温壶',
currency: 'CNY', minor: 129900
};
@State locale: string = 'de-DE';
@State amountText: string = '';
@State errorText: string = '';
aboutToAppear(): void {
this.selectLocale(this.locale);
}
private selectLocale(next: string): void {
try {
const candidate: string = displayPrice(this.record, next);
this.amountText = candidate;
this.locale = next;
this.errorText = '';
} catch {
this.errorText = '价签更新失败,请检查金额和展示区域';
}
}
build() {
Column({ space: 20 }) {
Text('多语商品价签').fontSize(24)
Text(this.record.title).fontSize(20)
Text(this.record.currency + ' ' + this.amountText).fontSize(32)
Row({ space: 12 }) {
Button('zh-CN').onClick(() => { this.selectLocale('zh-CN'); })
Button('de-DE').onClick(() => { this.selectLocale('de-DE'); })
}
Text('当前区域 ' + this.locale)
Text('原始金额 ' + this.record.minor + ' 分')
Text('语言切换不改变金额')
Text(this.errorText).fontColor('#A63020')
}
.width('100%')
.padding(24)
}
}
页面初始化也走同一个提交方法,避免首次进入和后续点击采用两套格式化规则。错误信息不直接拼接异常对象,防止把底层信息带给用户;排查时可以在开发环境记录错误类型、区域和商品身份。价格数据是否可以写入诊断信息,应结合业务权限决定。

本篇使用基础 ArkUI 页面与本地化模块,不增加网络请求。格式化数字并不要求读取用户联系方式或位置信息。区域由页面显式选择,后续如果跟随系统区域,还需要处理配置变化时的重新计算,不能把页面首次进入时的格式化结果永久保存。
四、展示精度与计算精度不要混为一谈
两位小数让129900分展示为完整货币金额,但它并没有替业务定义优惠、税费或结算规则。多个价签求和应基于整数分完成,再对总额进行展示;不能把每个格式化结果重新解析并相加。涉及比例运算时,应在金额计算层明确定义舍入位置和舍入方法。
当用户输入带小数的价格时,也不宜直接用浮点数乘一百再存入整数列。更稳妥的后续方案是接收受约束的十进制文本,拆分整数与小数部分,校验位数后构造整数分。本篇没有开放价格编辑入口,因此数据始终来自前述整数契约。
这项分层也有助于多人核对:负责商品资料的人查看原始金额与币种,负责语言展示的人检查数字格式和长文案,两个角色引用同一商品身份。展示上的标点差异不会被记录成价格变更,真正的金额调整则必须进入独立的数据修订流程。
布局适配还需要照顾较长的数字字符串。价签区域应预留币种文字与金额之间的间距,并允许商品名称独立换行;不宜为维持一行展示而持续缩小字号。窄屏展示单张完整价签,宽屏再增加对照列,金额契约始终保持一致。若开发者采用固定宽度容器,至少要把业务允许的最大金额纳入预览样本。
另一个容易漏掉的场景是商品记录更新。当前页面的记录是固定样本,后续接入数据源后,应在完整记录通过校验后替换旧记录,再重新生成展示值。如果先更新名称、随后才更新金额,就可能短暂把新商品名称与旧价格放在一起。商品身份和金额修订号应当随同一份快照提交,区域切换则继续只影响快照的展示。
数字区域与应用界面语言还要分别管理。本篇按钮改变的是数字展示区域,所以商品名称和界面提示仍保留中文;后续文案资源按语言键加载时,也不会改变整数金额。当某种语言的商品名称缺失,可以使用明确的回退语言,但价签仍应显示实际采用的数字区域,避免把语言资源回退和金额格式回退混成一条隐含规则。
五、围绕不变量检查区域切换
检查不能只比对屏幕上的一行文字,还应检查切换前后数据是否保持不变。商品身份、币种和整数金额构成不变量,区域和数字字符串则允许变化。若发现价格变化,应先追踪记录来源,而不是在组件样式层寻找原因。
| 检查样本 | 检查目标 |
|---|---|
| 129900分,切换两个区域 | 原始金额保持129900,分组与小数符按区域变化 |
| 零分 | 展示完整两位小数,不显示空值 |
| 一分 | 展示主要单位的小数部分,不丢失末位 |
| 负数或非整数分 | 数据校验拒绝 |
| 不支持的区域 | 保留上一份有效价签并提示 |

开发时记录的有效区域与格式化输出应当配对检查。肉眼看起来相近的数字可能使用不同字符,自动核对应比较结构化数据与指定区域结果;用于归档的截图则应同时包含商品身份或可追溯上下文,防止脱离数据来源单独流转。
后续宽屏多语言对照将复用 PriceRecord,分别生成不同区域的展示行,不创建多个可编辑价格副本。这样一份金额可以支撑多种阅读习惯,也能让编辑、计算与展示各自拥有清晰的职责。
更多推荐

所有评论(0)