多语言问题最尴尬的一种情况,是中文页面完全正常,英文也能显示,只有某个小语种的取件提示少了后半句。开发时容易把它当成翻译漏词;上架前集中检查才发现,原文里明明有两个动态参数,翻译资源只保留了一个 %s。字符串资源能够被找到,不代表运行时表达一定完整。更隐蔽的是一条隐私说明在某种语言的目录里压根没有定义,界面可能发生回退,让检查人员误以为这个地区已经翻译过。这篇把这个环节从人工对照表中拆出来,用 Node.js 写一个可在开发机运行的静态巡检脚本,配一个名为 LocaleProof 的展示页面。样例任务 LP-809,计划检查 3 组语言目录、4 个业务键,准备两种明确的问题:日语的 pickup_eta 少一个占位符、privacy_hint 缺少翻译资源,结论为 2 项待修复。这些缺陷是本文故意写入的样例数据,不是任何真实产品曾被应用市场拒审的记录。## 一、先分清“资源存在”和“文案可用”HarmonyOS 通常把字符串资源放在模块 resources 目录下,以 string.json 管理 name 和 value。基准目录的资源及其他语言地区限定目录,应按项目实际支持的语言分别维护。华为本地化指南强调避免把需要翻译的内容硬编码在组件里,也建议在完整句子中使用占位标识,而不是将几段翻译字串生硬拼接。这里的脚本只是顺着这个工程目标做一致性检查,不对翻译质量、法律含义或审核是否通过下结论。这次选的业务文本相当朴素:pickup_eta 表示“预计到站时间 + 取件码”,中文基准形如“预计%s到站,取件码%s”;privacy_hint 是数据使用说明;另两项是 submit_label 和 cancel_label。英文版的第一条同样含两个 %s。日语演示版故意只写一个 %s,还遗漏了 privacy_hint。如果只判断 key 是否存在,就会漏掉第一种;如果只对比占位数量,又会漏掉第二种。## 二、读资源时先拒绝静默覆盖小脚本有一个容易被忽略的陷阱:把 string 数组转成对象时,如果出现两个相同的 name,后者可能覆盖前者。即便报表写着“全部通过”,原始数据已经存在歧义。这里先让读取函数对 JSON 格式错误、缺少数组、重复键和空资源名明确报错,不在展示层偷偷容错。当前工程使用 base、en_US、ja_JP 三个资源目录,各自含 element/string.json。下面一段是运行在开发机上的 Node.js 代码,依赖标准库,不涉及 HarmonyOS 端侧系统接口,也不能代替 DevEco Studio 的资源编译校验。jsconst fs = require('node:fs');const path = require('node:path');const LOCALES = ['base', 'en_US', 'ja_JP'];const KEYS = ['pickup_eta', 'privacy_hint', 'submit_label', 'cancel_label'];function readStrings(root, locale) { const file = path.join(root, locale, 'element', 'string.json'); const payload = JSON.parse(fs.readFileSync(file, 'utf8')); if (!Array.isArray(payload.string)) throw new Error(`${locale}: invalid string array`); const values = new Map(); for (const item of payload.string) { if (typeof item.name !== 'string' || !item.name.trim() || typeof item.value !== 'string' || values.has(item.name)) { throw new Error(`${locale}: duplicate or invalid resource`); } values.set(item.name, item.value); } return values;}这一步保护的是检查输入本身的可信度,不是翻译是否准确。脚本不认识 plural 资源,也不处理本地化图片、从右向左文字或无障碍描述;它只为当前四个普通字符串建立严格的基线。某个语言目录可以根据系统的资源选择机制回退到其他资源,但本文的发布规则要求三个目标语言都必须覆盖这四个键,因此缺项会被自定义门禁拦住。不要把这种项目策略写成 HarmonyOS 对全部应用的强制要求。## 三、占位符比较要和显示文本分开另一种看似简单的写法是检查字符串是否包含百分号。它无法发现少了第二个参数,也不能区别字符串参数与数字参数。我在这个演示工具里只支持百分号加 s、百分号加 d,以及两个百分号表示字面百分号,把参数类别按出现顺序形成签名,再与 base 版本比较。范围窄一点,反而便于确认什么被检查过,什么还没有。接着为每个语言建立对账规则。缺少键记为 MISSING_KEY;格式参数签名不同记为 TOKEN_DRIFT。翻译文字可以完全不同,只要基础参数签名符合这份工具的约定就不会报错。jsfunction tokens(value) { return (value.match(/%%|%s|%d/g) || []) .filter(token => token !== '%%').join(',');}function compareLocale(base, current, locale) { const issues = []; for (const key of KEYS) { const standard = base.get(key); if (standard === undefined) throw new Error(`missing base key: ${key}`); const translated = current.get(key); if (translated === undefined) { issues.push({ locale, key, type: 'MISSING_KEY' }); } else if (tokens(standard) !== tokens(translated)) { issues.push({ locale, key, type: 'TOKEN_DRIFT' }); } } return issues;}比较的是参数形式而不是语义身份。比如两段文本都包含两个字符串参数,脚本就认为签名一致;可如果翻译人员把时间和取件码的语义位置写反,工具发现不了。更复杂的格式化资源可能允许位置参数换序,这段正则甚至可能产生误报。门禁通过只意味着本文的窄范围内没有发现差异,不能取消人工翻译校验、格式化单元测试和真实布局检查。图中的 DevEco Studio 风格画面为开发流程演示:左边是多语言资源目录,中间是检查脚本,右侧显示任务 LP-809 的报告,底部采用 HiLog 样式承载模拟输出。真正运行脚本时应在开发机终端执行,不是让手机 HiLog 直接执行 Node.js。它也不是任何一次应用市场审核或真机编译的截图。## 四、把问题落到文件和键上,而不是只给一个失败状态只把静态脚本的退出码设为非零,内容人员仍要重新逐行查看资源文件。更合理的输出是哪个语言、哪个 key、什么错误类型,并保留任务号方便在代码审查中追踪。下面将入口与报告汇总起来,使用 process.exitCode 标识门禁结果。这样终端仍能完整打印问题清单,而不必提前终止进程。jsfunction main(root) { const base = readStrings(root, 'base'); const issues = []; for (const locale of LOCALES.filter(v => v !== 'base')) { issues.push(...compareLocale(base, readStrings(root, locale), locale)); } console.log(`[DEMO] LP-809 locales=${LOCALES.length} keys=${KEYS.length} issues=${issues.length}`); for (const issue of issues) { console.log(`[DEMO] ${issue.locale} ${issue.key} ${issue.type}`); } process.exitCode = issues.length > 0 ? 1 : 0;}main(process.argv[2] || 'entry/src/main/resources');用本篇固定测试资源运行,应该看到 locales=3、keys=4、issues=2。两条问题分别是 ja_JP 的 pickup_eta 参数签名不一致,以及 ja_JP 的 privacy_hint 缺项。这些不是推测应用市场后台会如何反馈,而是可以在开发机通过 Node.js 针对样例 fixture 复现的静态结果。如果文件路径不存在、JSON 解析失败,程序会抛出异常;绝不能把脚本启动失败误判成零问题。CI 流程应完整保存标准错误,并阻止本轮自动放行。手机效果图采用 ReportPage 的设计样式:三组语言、四个检查键、两条待修复,业务状态为 HOLD。这里故意不写“平台审核未通过”,因为脚本没有向应用市场提交任何包,也不具备判断法律合规性的能力。页面上的重新巡检属于规划按钮;实际产品需要由开发机或可信构建流程触发,输出与当前资源版本绑定的报告,然后再同步给应用端查看。## 五、自动检查覆盖不到的部分更要明确这个版本只处理 string.json 中的简单字符串,忽略非法百分号的深层语法语义,也不识别数值、日期和货币本地化。即使每条译文都有两个占位符,上到 UI 后仍可能因文字长度、阅读方向或行高产生截断。它只能减少漏项,不能取代多语言设备界面检查,更无法判断翻译内容是否符合当地文化与法律要求。另外,不应把静态检查失败与系统运行失败混为一谈。缺项有时会通过资源回退显示出别的语言,界面视觉上似乎正常,但可能不符合产品的语言覆盖目标。反过来,某些业务本就允许使用统一的品牌短句,如果项目规定允许回退,也不应被这套自定义门禁拦截。具体规则应该由开发、翻译、产品和发布负责人共同确认。还有一个容易踩的边界:脚本把参数顺序当成强约束,但自然语言可能把语序完全翻转。如果计划长期使用,应增加位置参数语法分析,或者把有业务含义的参数映射成明确的模型字段,再分别做模板验证。不要为了少报一次错,简单把所有占位符数量相加;那会漏掉数字与字符串参数错位。对于复杂项目,脚本需要测试集来支撑,而不是靠一个万能正则。我更愿意把 LocaleProof 当成提审之前的一张可复现对账单。它能在代码审查阶段把两处缺陷变成有名字、有位置、有退出码的待办;至于译文是否专业、隐私说明是否充分、应用是否符合当时的发布要求,仍需要人工和官方审核流程处理。工程工具有价值的地方不是写出“百分百合规”,而是让散落在多份语言资源中的小差异更早显形。资料参考:华为开发者联盟国际化和本地化概述、避免硬编码与拼接以及当前文档中心。本文的任务号、检查键和失败策略属于自定义项目示例,不是任何 SDK 版本或上架后台的官方规则。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐