文章、日志、审核结果与配图均为构造样例/界面示意,不是华为应用市场官方审核结论、真实审核接口调用或已完成的真机测试。本文的“BLOCK”只表示团队自定义发布门禁未通过。

一、一句英文文案为什么会挡住交付

进入多语言发布阶段时,最容易被忽略的并不是少翻译了一句完整文案,而是翻译后的参数占位符偷偷改变了类型。以订单金额为例,默认资源里约定把格式化后的金额文本作为字符串传入,英文翻译却把 %1$s 写成 %1$d。界面上英文可能看起来还像“Total”,但参数类型契约已经不同,直到真实用户进入对应页面才会暴露。另一个常见问题是某种语言根本缺少一个资源 key,应用可能回退到默认语言。回退机制是可用性兜底,不等于发布检查可以静默忽略缺译。

LocaleLedger 用 Node.js 离线读取工程的 resources/base/element/string.json 与 resources/en_US/element/string.json,整理三类结果:key 完整且参数规格相同为 PASS;目标语种缺少该 key 为 MISSING;同名 key 的占位符位置或类型不同为 TYPE_DRIFT。后两类统一计入团队内部的 BLOCK。工具不向 AppGallery Connect 上传任何内容,也不宣称取得官方市场的审核状态;它只把工程构建前能发现的低成本问题提前暴露出来。

二、先固定一份所有人都看得懂的样本

本文使用演示批次 IL-207。默认语言有三条资源:order_total 值是“合计 %1 s ”, ‘ d e l i v e r y e t a ‘ 值是“预计 s”,`delivery_eta` 值是“预计 %1 s”,‘deliverye​ta‘值是“预计d 分钟”,button_retry 值是“重试”。英文资源里保留 order_total 和 button_retry,但前者写成“Total: %1$d”,后者写为“Retry”;delivery_eta 被故意删掉。人工推演便得到 PASS 1 / BLOCK 2:button_retry 通过,order_total 占位符类型不一致,delivery_eta 缺译。屏幕和开发图都必须使用相同的 key 与状态,不能为了展示好看随便换成其他报错字段。

资源文件里每一项都使用 name 和 value,形式上是字符串资源数组。示例只列出关键数据,真实项目可能还包含 plural、stringArray 和地区限定词资源,不能把所有资源当作同一种字符串处理。团队还需要明确“base 是否可允许作为某个语言的完整交付文案”;在本例中 base 只是比较基线,en_US 的缺项会触发内部阻断,不以系统最终显示何种回退语言作为放行依据。

{
  "string": [
    { "name": "order_total", "value": "合计 %1$s" },
    { "name": "delivery_eta", "value": "预计 %1$d 分钟" },
    { "name": "button_retry", "value": "重试" }
  ]
}

上述内容对应 resources/base/element/string.json。配套的 en_US 文件也放在 ZIP 的 demo/ 目录中,包含 order_total: Total: %1$d 与 button_retry: Retry 两条。把故意损坏的样例保留下来十分重要,只有保留明确的失败输入,脚本修改后才知道它是真的识别了问题,而不是始终把所有条目标为绿色。

三、从文案校对表变成一段能执行的检查脚本

读文件和比对 key 本身没有难度,难的是确认占位符的比较语义。一个字符串既可能有 %1$s,也可能出现 %2$d;只比较“有几个百分号”明显不够。LocaleLedger 把占位符解析为“参数位置:类型”并排序后再比较,意味着同两个参数在自然语言中的呈现先后顺序可以调整,而类型不能改变。双百分号 %% 视为普通百分号转义,不应参与参数列表。没有显式位置的写法按照顺序编号,但若产品同时混用显式和隐式位置,建议直接添加额外规则阻断,以免不同语言位置映射产生歧义。

// demo/tools/i18n-audit.mjs,使用 Node.js 内建模块,无需引入第三方包
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const root = process.argv[2] || process.cwd();
function readStrings(locale) {
  const p = resolve(root, `entry/src/main/resources/${locale}/element/string.json`);
  const rows = JSON.parse(readFileSync(p, 'utf8')).string;
  if (!Array.isArray(rows)) throw new Error(`${locale} 缺少 string 数组`);
  const map = new Map();
  for (const row of rows) {
    if (typeof row.name !== 'string' || typeof row.value !== 'string')
      throw new Error(`${locale} 资源项格式错误`);
    if (map.has(row.name)) throw new Error(`${locale} 重复 key: ${row.name}`);
    map.set(row.name, row.value);
  }
  return map;
}
function slots(value) {
  let sequence = 0;
  return Array.from(value.matchAll(/%(?:([1-9]\d*)\$)?([sdif%])/g))
    .filter(m => m[2] !== '%')
    .map(m => `${m[1] || ++sequence}:${m[2]}`).sort();
}
const base = readStrings('base');
const target = readStrings('en_US');
let pass = 0; let blocked = 0;
for (const [name, text] of base) {
  const localized = target.get(name);
  let status = 'PASS';
  if (localized === undefined) status = 'MISSING';
  else if (slots(text).join('|') !== slots(localized).join('|'))
    status = 'TYPE_DRIFT';
  if (status === 'PASS') pass++; else blocked++;
  console.log(`[IL-207] ${name} ${status}`);
}
console.log(`[IL-207] PASS ${pass} / BLOCK ${blocked}`);
if (blocked) process.exitCode = 1;

这是一份可以用 Node.js 单独执行的静态脚本,不依赖鸿蒙运行时,也没有伪造 getResourceByLocale 之类的 SDK 接口。通过运行 node demo/tools/i18n-audit.mjs demo 可以针对 ZIP 内附的样例得到退出码1;错误码1在这里就是内部检查不通过,并非华为审核错误码。修改 en_US,把 order_total 改成 %1$s 并补上 delivery_eta,再运行应得到全部 PASS。脚本故意只做格式字符串比较,不评估翻译准确性,也不能替代当地语言人员审校。

四、ArkUI 端如何消费同一套资源,而不是硬编码英文

静态校验之外,应用页面还要通过 ArkUI 资源引用渲染。资源名与代码中 $r('app.string.xxx') 的末段应该相对应;如果把中文和英文直接硬编码在 if (language === 'en') 分支里,离线脚本便无法看见这部分文案,漏检率会升高。这里示例的动态参数是已经格式化好的金额字符串,例如“¥128.00”,因此默认资源使用 %1$s;如果业务真正想传数值,应统一资源格式与调用参数的类型,不能只修改其中一个语言。

@Entry
@Component
struct CheckoutNote {
  @State amountText: string = '¥128.00';
  build() {
    Column({ space: 14 }) {
      Text($r('app.string.order_total', this.amountText))
      Text($r('app.string.button_retry'))
        .fontSize(15)
    }.padding(20)
  }
}

DevEco Studio 里应该先用资源编辑器确认文件位置与 key,随后在构建前运行 Node.js 工具,把它当成 CI 的一个独立步骤。不要用“应用英语界面看起来正常”替代静态检查,因为只有一条特定路径会使用 delivery_eta。若上线前存在多个模块,每个模块都可能有自己的资源目录,应由仓库配置枚举模块,而不是扫描一个模块后宣称全局通过。工程中对数字、日期、货币的本地化还可能交给 Intl 或 Localization Kit,单纯对字符串模板做比对无法验证这些格式化规则是否适用于每个地区。

五、把日志、屏幕与团队复核流程连在一起

对这份故意制造的坏数据,预期三条逐项日志依次为:[IL-207] order_total TYPE_DRIFT、[IL-207] delivery_eta MISSING、[IL-207] button_retry PASS,最后显示 [IL-207] PASS 1 / BLOCK 2。技术示意图中的颜色和标签与这个顺序保持一致:绿色只给 button_retry,两处红色问题分别标出占位符错误与缺少资源。下一个人接手时无需阅读长篇描述,便能根据 key 找到具体的 JSON 行。

请注意右侧手机图只是把脚本结果做成一张便于文章阅读的假想管理页面,不是系统上架助手,也不意味着 JavaScript 脚本可以自动出现在 DevEco 模拟器内。实际交付如果要把报告渲染进 App,需要另行设计导入数据文件、只读展示、权限和路径校验。本文为了让构造案例可复核,提供的是脚本与固定数据,并把“手机展示”明确限定为视觉说明。

六、还需要再查哪些问题,才能说发布资料准备充分

占位符一致只是第一关。第二关是目标语种有没有重复 key、空文案、未翻译标记或超长按钮文案;第三关是不同地区使用的货币、日期、地址与右到左布局是否合规;第四关才是应用市场侧实际填写的标题、描述和截图。应用商店的政策可能变更,也可能对不同应用类别提出不同要求,不能凭本文的自定义 BLOCK 宣称触发官方拒审条件。对涉及用户隐私、支付和合规的翻译,更应安排人工复核和真实设备验收。

还有两个实现边界要提前写到任务单里:其一,这个简单正则只处理常见 %s/%d/%i/%f 及显式位置写法,无法完整证明复杂格式字符串、转义与复数表达式语义等价;其二,缺少某个地区资源文件与缺少某条 key 的处理策略不同,后者可能被回退覆盖,前者可能要求整个语种的发布准备另行决策。脚本遇到文件不存在会抛出异常并失败,这种失败不能偷换成 PASS,也不意味着设备运行时没有回退能力。

七、结尾:将静态证据保留在每次发布记录里

真正有价值的不是多一个按钮,而是每次发布都保存输入样例、目标语言清单、脚本版本、退出码以及人工放行记录。IL-207 用三个资源 key 演示了一个足够小、可以查清楚的失败面;生产环境可以把同样原则扩展到更多地区,并增加差异报告与评审责任人。文章的演示输出是离线推导,不是已经运行后的机器测量;代码和配图为可审查的材料,而不是以视觉效果代替测试报告。上架前自动化越谨慎,真正提交市场审核时越容易把问题定位到具体责任环节。

官方资料与核对说明

Logo

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

更多推荐