一、德语按钮没丢翻译,却还是没通过这轮验收

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 固定状态并留证;三层结果同时通过,版本才从“看起来翻译完了”变成“有证据可以交付”。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐