
作者李游多语言资源最棘手的错误往往不是“翻译得不够好”而是构建能通过、页面也能打开直到某个带参数的文案在特定语言下才暴露类型错位。%d被译成%s、复数分支缺少other、关键文案依赖默认资源回退这些问题靠逐页肉眼检查很难稳定发现。本文把检查器收敛为一个可重复执行的构建前门禁。示例工程叫LocaleGuard页面为LocaleAuditPage任务 ID 为L10N-0049。资源集合包含base、zh_CN、en_GB、ar四个目录和 126 个 key。首轮扫描得到 6 个问题缺失 key 2 个、占位符签名不一致 2 个、复数缺少other1 个、发布文案硬编码 1 个修复后结果为 0。所有统计用于展示检查逻辑不是某个真实项目的审核结果。一、先定义“签名”再谈字符串相等资源文件里的两条文案可以完全不同却仍然拥有相同的参数契约。例如基础资源已完成%d%%剩余%s和英文Completed %d%%, %s left文本不同但占位符顺序都是%d,%s。检查器真正要比较的是这个有序签名而不是翻译文本。示例里故意放入错误版本Completed %s%%, %s left。它看起来像正常英文运行时却把第一个整数参数当成字符串。若业务层仍按基础资源传入数字轻则格式异常重则让某些分支在运行期失败。1. 为什么不能只数占位符数量错误版本与正确版本都有两个占位符只比较数量会放过问题。类型和顺序同样重要。对带位置索引或精度修饰的格式规则还要先归一化再比较语义签名。检查器的第一版应支持项目实际使用的格式集合不要一上来写一个“匹配所有 printf 语法”的巨大正则。这段代码解决什么问题从资源文本中提取有序占位符签名让%d,%s与%s,%s的差异可被稳定识别。exporttypePlaceholderToken%d|%s|%f;exportfunctionplaceholderSignature(text:string):PlaceholderToken[]{constescapedPercent__PERCENT_LITERAL__;constnormalizedtext.replace(/%%/g,escapedPercent);constmatchesnormalized.match(/%(?:\d\$)?[dsf]/g)??[];returnmatches.map((item){consttypeitem[item.length-1];return(%${type})asPlaceholderToken;});}exportfunctionsameSignature(base:string,translated:string):boolean{constleftplaceholderSignature(base);constrightplaceholderSignature(translated);returnleft.lengthright.lengthleft.every((token,index)tokenright[index]);}先把%%替换掉是为了避免把百分号字面量误当成参数。位置索引被归一化只保留最终类型如果项目允许翻译调整参数顺序就不能丢掉位置索引而应解析后按索引比较。规则必须与调用方式一致不存在一套适合所有工程的万能签名。状态在这里还没有进入 UI。函数只输出确定结果扫描器再负责把差异变成PLACEHOLDER_SIGNATURE_MISMATCH。拆开后规则可以用小样本单测页面只消费报告不参与判断。二、默认资源回退是运行能力不是发布质量标准HarmonyOS 资源系统会根据设备语言和限定词选择匹配资源没有匹配项时可以回到默认资源。这个能力保证应用不至于因为某个翻译缺失而完全无文案但它不意味着项目应允许所有 key 随意回退。LocaleGuard把规则分成两层平台层确认资源结构合法、默认资源存在项目层对支付、权限、隐私、导出等高风险文案要求每个目标语言显式提供。这样不会把团队策略误写成系统规则也不会把系统回退误当成翻译完成。1. 四个目录采用同一份索引扫描器先读取base形成基准 key 集合再逐个读取zh_CN、en_GB、ar。每条记录带上目录、key、规则、期望值和实际值。报告使用稳定排序先 locale再 key再规则。否则同一批问题每次输出顺序不同很难在代码评审里看清新增与消失。这段代码解决什么问题把四个资源目录解析为统一索引并明确区分缺失、回退与高风险阻断。exportinterfaceLocaleIndex{locale:string;strings:Mapstring,string;plurals:Mapstring,Setstring;}exportinterfaceAuditIssue{locale:string;key:string;rule:MISSING_KEY|HIGH_RISK_FALLBACK|PLACEHOLDER_SIGNATURE_MISMATCH;expected?:string;actual?:string;}consthighRiskKeysnewSet([privacy_collect_location,export_delete_source,payment_confirm_amount]);exportfunctioncompareLocale(base:LocaleIndex,target:LocaleIndex):AuditIssue[]{constissues:AuditIssue[][];for(const[key,baseText]ofbase.strings){consttargetTexttarget.strings.get(key);if(targetTextundefined){issues.push({locale:target.locale,key,rule:highRiskKeys.has(key)?HIGH_RISK_FALLBACK:MISSING_KEY});continue;}if(!sameSignature(baseText,targetText)){issues.push({locale:target.locale,key,rule:PLACEHOLDER_SIGNATURE_MISMATCH,expected:placeholderSignature(baseText).join(,),actual:placeholderSignature(targetText).join(,)});}}returnissues;}为什么缺失 key 还要分普通与高风险因为两者运行时都可能走回退但发布决策不同。普通缺失可以在开发分支先记录关键文案则应阻断。实际项目要把高风险清单放进版本控制并要求业务负责人评审不要让扫描脚本作者独自决定所有业务优先级。易错点是把zh_CN当作默认资源。默认目录与某个语言限定目录职责不同应以官方资源目录规则为准。另一个易错点是只扫描一份 JSON字符串、复数、媒体资源可能位于不同文件解析器要按项目实际结构扩展。三、复数other与硬编码要分别处理复数规则不是把数字拼进字符串那么简单。不同语言的分类并不相同项目若使用复数资源至少要确认目标集合拥有兜底分支。LocaleGuard把缺少other作为项目发布门禁因为它能显著减少未覆盖数量落到错误文本的风险这是一条工程策略不应被描述为应用市场对所有项目的一刀切拒审条件。硬编码检查也要克制。扫描所有中文或英文字符会产生大量误报包括日志、测试数据和无障碍标识。示例只扫描发布构建中可到达的页面目录排除测试与调试文件并允许对确有理由的文本加带责任人的白名单。这段代码解决什么问题把复数兜底、硬编码与占位符差异合并成一份稳定报告并保持规则来源可解释。exportinterfaceAuditSummary{taskId:L10N-0049;locales:number;keys:number;issues:AuditIssue[];}exportfunctionauditAll(base:LocaleIndex,targets:LocaleIndex[]):AuditSummary{constissues:AuditIssue[][];for(consttargetoftargets){issues.push(...compareLocale(base,target));for(const[key,quantities]oftarget.plurals){if(!quantities.has(other)){issues.push({locale:target.locale,key,rule:MISSING_KEY,actual:plural:other});}}}return{taskId:L10N-0049,locales:4,keys:base.strings.size,issues:issues.sort((a,b)${a.locale}/${a.key}/${a.rule}.localeCompare(${b.locale}/${b.key}/${b.rule}))};}这里为了聚焦主线把硬编码扫描结果也转换为同一种AuditIssue后再汇总生产代码应给它独立规则名和源文件位置。keys取基础索引大小示例固定为 126locales包括 base 在内共 4 个。若资源解析失败不能返回“0 问题”而应让任务进入 FAILED。扫描失败与扫描通过是完全不同的状态。图中的工程目录、代码、模拟器与日志使用同一组数据L10N-0049、4 locales、126 keys、6 issues。画面是演示配图不冒充真实 DevEco Studio 执行证据。四、把检查器放在 Hvigor 之前而不是藏在某个人电脑里一个只能手动运行的脚本很快会变成“发布前记得点一下”。更可靠的做法是让它成为构建入口的一部分先执行 TypeScript 检查器并输出 JSON 报告退出码非零时不启动 Hvigor通过后再执行hvigorw assembleHap。这种外部门禁不依赖未核实的 Hvigor 插件接口同时仍然把检查放进标准构建链路。在本地可以封装为 npm script在 CI 中则直接执行两个命令。关键不是命令放在哪里而是保证所有发布构建走同一入口不能让“快捷构建”绕过资源审计。这段代码解决什么问题用明确退出码把 LocaleGuard 与 Hvigor 构建串联避免报告有问题时仍继续产出发布包。import{writeFileSync}fromnode:fs;import{spawnSync}fromnode:child_process;constsummaryauditAll(baseIndex,localeIndexes);writeFileSync(build/reports/locale-audit.json,JSON.stringify(summary,null,2));if(summary.issues.length0){console.error([LocaleGuard] taskL10N-0049 issues${summary.issues.length});process.exit(2);}constresultspawnSync(./hvigorw,[assembleHap],{stdio:inherit});process.exit(result.status??1);状态变化很清楚开始时是 SCANNING发现 6 个问题进入 REVIEW_REQUIRED修改资源后标记 FIXED 并重新扫描只有第二次issues.length 0才进入 PASS 并启动 Hvigor。不要在 FIXED 状态直接放行因为“改过了”不等于“规则已经重新验证”。生产环境还要处理 Windows 命令名、工作目录和超时并保留 JSON 报告作为构建产物。脚本本身异常时使用不同退出码便于 CI 区分“资源问题”和“工具故障”。如果团队后来把规则集做成 Hvigor 插件应先依据当前版本官方扩展文档验证接口而不是复制未经确认的示例。五、报告页面只展示能支持决策的数据LocaleAuditPage不试图成为翻译平台。它只回答本次构建能否继续任务 ID、资源目录数、基准 key 数、问题总数和状态链。首轮页面为 REVIEW_REQUIRED修复后显示6 → 0与 PASS。运行页显示时间00:49、四个 locale、126 个 key、回退阻断 0、硬编码 0。这里的“回退 0”指项目高风险回退规则没有命中并不代表系统资源解析永远不会回退。把指标命名写清能避免一个绿色数字掩盖真实含义。详情页保留修复轨迹缺失 key 2、占位符不一致 2、复数other1、硬编码 1总计 6。重点样本export_progress同时展示期望%d,%s、发现%s,%s与修复后%d,%s这样评审者不用打开资源文件也能理解阻断原因。红色标注只圈出签名错位和6 → 0不为每个字段都加箭头。诊断图承担的是规则解释它告诉开发者哪种差异会失败而不是仅仅展示一个漂亮的 PASS 页面。六、资源扫描的边界比正则表达式更重要占位符规则很容易继续膨胀位置索引、浮点精度、富文本标签、双向文本、资源引用、复数类别都可能加入。正确的扩展顺序是先收集工程实际格式再为每种格式增加测试样例。一个看似完美却没有样本约束的正则往往会在下一种语言上制造更多误报。还要区分三类结论平台事实资源限定目录与默认资源存在匹配、回退关系字符串资源支持格式参数。项目策略高风险 key 不允许依赖回退复数必须包含other发布页面不得出现未豁免硬编码。示例结果L10N-0049从 6 个问题修复为 0126 个 key 全部通过。第一类由官方文档约束第二类由团队质量门槛决定第三类只是本文 Demo 数据。把它们混写会让读者误以为项目策略是系统硬性政策或误以为示例数字来自真实审核。扫描报告还应保持可追溯记录规则版本、资源提交号与目标语言集合但不要收集翻译人员身份或把业务文案上传到无关服务。报告用于定位资源契约不应变成新的数据外泄入口。若构建使用缓存缓存键必须包含规则版本和资源摘要否则旧的 PASS 可能错误复用到新的资源提交。本文核对的官方一手资料包括 HarmonyOS 资源分类与访问、多语言资源、Localization Kit 与 Hvigor 工具说明https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-accesshttps://developer.huawei.com/consumer/cn/doc/harmonyos-guides/i18n-l10nhttps://developer.huawei.com/consumer/cn/doc/harmonyos-guides/localization-kithttps://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hvigor具体目录名、资源 JSON 结构和构建命令应以项目使用的 HarmonyOS SDK、DevEco Studio 与 Hvigor 版本为准。若官方文档的页面路径调整应从开发者文档中心检索同名章节不要依赖第三方转载来决定发布规则。七、从“能显示”提升到“契约一致”多语言资源的质量门槛不该停在“页面上有字”。真正稳定的本地化链路要保证 key 可达、参数契约一致、复数分支可兜底、关键文案不依赖意外回退并且每次发布都执行同一套检查。LocaleGuard的价值不在 6 个问题本身而在于把隐性的语言差异变成可比较、可阻断、可复查的构建数据。SCANNING → REVIEW_REQUIRED → FIXED → PASS不是为了多做一个页面而是迫使状态从“已经修改”走到“已经重新验证”。当export_progress的签名重新回到%d,%s构建才继续这条边界比发布前临时扫一眼资源文件可靠得多。