HarmonyOS 7 ResourceManager:多语言键闭包与字号溢出验收
一、德语按钮没丢翻译,却还是没通过这轮验收
LocaleGate 是我们给 Release 包加的一道本地化预检。事情起因很小:版本 7.2.0(72031) 的确认支付页,在中文和英文设备上都正常,切到德语后,底部按钮只剩“Zahlung bestä…”。资源文件里确实有完整译文,ResourceManager 也成功取到了字符串,所以常规的“缺少 key 检查”给出全绿结果。
接着把系统字体调到 1.3×,阿拉伯语订阅页的金额说明又压住了右侧图标。这个问题更麻烦:key 存在、占位符数量相同、页面也没有崩溃,只是最终截图不符合交付要求。继续靠人工切语言、调字号、逐页截图,既慢,也很难保证每次版本都检查同一组状态。

本次 Demo 使用项目 LocaleGate、页面 ReleaseAuditPage、任务 I18N-2142,时间 21:42。目标语言为 zh_CN / en_US / de_DE / ar_SA,检查 CheckoutConfirmPage 与 SubscriptionPage,字体倍率为 1.0× 和 1.3×,总计 16 张验收快照。初次扫描得到 missing=3、signatureMismatch=1、overflow=7、screenshots=9/16;修复后四项分别为 0、0、0、16/16,状态 PASS。
二、把“本地化完整”拆成三层证据
这次复盘后,我不再用一个绿色对勾概括本地化。真正能进入提审包的证据至少有三层。
第一层是资源闭包:基准语言的 146 个 key,在另外三个 locale 中都存在,不能依赖运行时回退掩盖遗漏。第二层是参数签名:%{public}s、数量占位符和 plural/select 分支要一致,防止德语译文存在却在格式化时失败。第三层是页面可见性:字符串拿得到,不代表在 360vp、RTL 和 1.3× 字体下完整显示。
项目因此没有继续往 string.json 检查脚本里塞条件,而是拆成独立管线:
LocaleGate/
├── entry/src/main/ets/pages/ReleaseAuditPage.ets
├── entry/src/main/ets/pages/CheckoutConfirmPage.ets
├── entry/src/main/ets/pages/SubscriptionPage.ets
├── entry/src/main/ets/audit/TextProbe.ets
├── tools/locale/collect-resources.ts
├── tools/locale/compare-signatures.ts
├── tools/locale/run-snapshot-matrix.ts
└── reports/locale-report.json
工具脚本负责资源静态闭包;ArkUI 测试页负责实际布局;ReleaseAuditPage 只读取报告并展示最终门禁。这样 CI 失败时能直接指出是 key、参数还是像素层问题,而不是留下一句“德语有问题”。
三、第一道检查是闭包,不是“能回退就算成功”
第一段代码解决资源回退掩盖缺失的问题。脚本以 zh_CN 为基准,读取四套 resources/*/element/string.json,把 key 与占位符签名规范化。缺少项、额外项和签名不一致分别记录,任何一项非 0 都不能进入截图阶段。
// tools/locale/compare-signatures.ts
type ResourceMap = Map<string, string>
function signature(text: string): string[] {
const tokens = text.match(/%\{public\}[sdf]|\{\w+,\s*(plural|select),/g) ?? []
return tokens.map(token => token.replace(/\s+/g, '')).sort()
}
export function compare(base: ResourceMap, target: ResourceMap): LocaleDiff {
const missing = [...base.keys()].filter(key => !target.has(key))
const extra = [...target.keys()].filter(key => !base.has(key))
const mismatch = [...base.keys()].filter(key => {
if (!target.has(key)) return false
return signature(base.get(key)!).join('|') !==
signature(target.get(key)!).join('|')
})
return { missing, extra, mismatch }
}
这里没有把 extra 一律当错误。灰度功能可能先在某个 locale 出现,所以报告会展示 extra,但门禁只对缺失和签名不一致失败。当前项目的明确约束是:Release 分支新增基准 key 时,其他 locale 必须同批补齐,不能让 ResourceManager 回退到中文后悄悄上线。
初次报告里,de_DE 缺少 subscription_trial_hint 与 payment_network_retry,ar_SA 缺少 account_export_notice;payment_total_label 的德语译文丢了 %{public}s。这些都不是截图肉眼最容易发现的问题,但会在不同数据条件下变成运行时错误。
四、溢出检查要用实际字号和约束宽度
第二段代码解决“译文完整却被 UI 截断”。TextProbe 在测试页中拿到容器实际宽度,再用同字体、同字号计算文本需要的宽度。只要组件配置 maxLines(1),测量值超过可用宽度 1 vp 就记为 overflow;多行文本则同时检查行数与高度。
// TextProbe.ets(页面探针核心片段)
import { measure } from '@kit.ArkUI'
export function inspectSingleLine(input: ProbeInput): ProbeResult {
const size = measure.measureTextSize({
textContent: input.text,
fontSize: input.fontSize * input.fontScale,
fontWeight: input.fontWeight,
constraintWidth: input.availableWidth
})
const overflow = size.width > input.availableWidth + 1
return {
locale: input.locale,
page: input.page,
componentId: input.componentId,
requiredWidth: Number(size.width.toFixed(1)),
availableWidth: input.availableWidth,
fontScale: input.fontScale,
overflow
}
}
测量必须发生在页面完成布局之后。过早读取宽度会拿到 0;连续切换语言时,旧页面的 onAreaChange 也可能晚到。测试控制器因此给每个 locale/fontScale 组合分配 generation,只有当前 generation 的探针结果能写入报告。
这一步也没有试图“自动缩小字号”。支付确认按钮的文案如果过长,优先允许按钮增高或调整布局;强行把德语缩到 11 fp 虽然能过截图,却破坏可读性。阿拉伯语还需要 RTL 实际布局,不能只拿字符串长度估算。
五、16 张截图必须来自同一组可复现状态
初版自动化只负责打开页面,结果每次金额、网络状态和倒计时都不同,截图无法比较。最终 runner 给两个页面注入固定 Fixture:订单 ORD-2142、金额 ¥ 268.00、试用期 7 天、网络状态 ONLINE。每张图都带 locale、fontScale、page 和 caseId。
// tools/locale/run-snapshot-matrix.ts
const locales = ['zh_CN', 'en_US', 'de_DE', 'ar_SA']
const scales = [1.0, 1.3]
const pages = ['CheckoutConfirmPage', 'SubscriptionPage']
for (const locale of locales) {
for (const fontScale of scales) {
for (const page of pages) {
const caseId = `${locale}-${fontScale}-${page}`
await device.applyLocale(locale)
await device.applyFontScale(fontScale)
await app.openAuditCase(page, 'I18N-2142', caseId)
const probes = await app.waitForStableLayout(caseId, 300)
await report.add(caseId, probes, await device.capture(caseId))
}
}
}
await report.assert({ missing: 0, signatureMismatch: 0, overflow: 0, shots: 16 })
waitForStableLayout 不是固定 sleep 300 ms,而是要求 300 ms 内组件矩形和探针结果不再变化;动画、字体加载或异步金额更新都会重新计时。执行结束后 runner 恢复设备原 locale 和字体倍率,避免把测试环境污染到下一条用例。
截图只是证据之一。runner 同时保存组件 ID、需要宽度、可用宽度和最终矩形;以后按钮主题变化导致 4 vp 偏移时,不需要仅靠像素差猜原因。任务结束、失败或用户停止时,都在 finally 中恢复环境并关闭测试 Ability。

六、从 7 个溢出点定位真正的布局责任
初次运行的关键日志如下:
21:42:04.118 LocaleGate I18N-2142 RESOURCE_SCAN keys=146 locales=4 missing=3 mismatch=1
21:42:11.604 LocaleGate I18N-2142 OVERFLOW locale=de_DE scale=1.3 id=confirmPay required=214.6vp available=176vp
21:42:14.227 LocaleGate I18N-2142 OVERFLOW locale=ar_SA scale=1.3 id=trialHint required=198.2vp available=184vp
21:42:29.903 LocaleGate I18N-2142 FAILED overflow=7 screenshots=9/16
7 个问题并不是 7 次改文案。confirmPay 的责任在按钮布局:中文设计稿把左右 padding 写死,修复后改为允许两行并保持最小高度。trialHint 的责任在 RTL 容器:图标仍固定在右侧,修复为按布局方向调整顺序。另有 2 个问题来自字符串签名错误,补齐参数后测量宽度自然变化。
修复过程中最有价值的是“不允许跳过失败 locale”。旧脚本遇到资源格式化异常会继续下一项,最后只少几张截图;新脚本把 16 张视为完整矩阵,只要不是 16/16 就无法 PASS。这样 screenshots=9/16 不会被误解成“已经抽查了 9 张”。
七、最终门禁:资源 146/146,截图 16/16
第二次运行的 ReleaseAuditPage 显示版本 7.2.0(72031)、任务 I18N-2142、4 个 locale、2 个页面、2 档字号。资源键 146/146,missing 0,signature mismatch 0,overflow 0,截图 16/16,最终状态 PASS,耗时 31.8 秒。

手机页没有把 16 张缩略图全部塞进一屏,而是展示矩阵摘要:zh_CN 4/4、en_US 4/4、de_DE 4/4、ar_SA 4/4。点击某行才能查看对应截图和组件报告。这样运行图仍是一个真实审核页面,不会变成九宫格或宣传海报。
底部按钮是“查看报告”和“导出证据”。处于 FAILED 时,“生成提审包”按钮禁用;PASS 后才允许继续。报告文件以 taskId 和版本号命名,重复执行同一版本会创建新 runId,不覆盖上一轮,便于确认修复前后差异。
八、边界:自动化能挡住确定性问题,不能替代语言审校
LocaleGate 能发现缺 key、参数签名不一致、确定性截断和矩阵缺图,却判断不了德语是否自然、阿拉伯语是否符合语境,也无法自动证明按钮文案是否准确。语言质量仍需要译审;自动化只负责把“结构完整”和“画面可见”变成可重复证据。
截图门禁也不应无限扩大。本文固定 360vp、1.0×/1.3× 和两个关键页面,是因为它们正好覆盖当前提审风险;若产品支持更多窗口尺寸,应按风险增加 profile,而不是一次枚举所有像素宽度。测试矩阵越大,越需要明确每个 profile 为什么存在。
这次真正收口的,不是德语按钮那 38.6 vp 的差值,而是发布前的判断方式。ResourceManager 提供正确字符串,ArkUI 负责真实布局,自动化 runner 固定状态并留证;三层结果同时通过,版本才从“看起来翻译完了”变成“有证据可以交付”。
更多推荐



所有评论(0)