HarmonyOS 多语发布资源缺键与默认回退(连载01)【共创稿事节】
英文页面里的一个按钮仍显示中文,未必是语言切换失效。它也可能没有英文资源,系统沿着资源匹配规则取到了默认目录中的文本。界面可以继续显示,缺失项却被回退行为掩盖。多语发布校对台从这里切入:把“页面有字可读”和“目标语言资源已补齐”分成两个检查目标。

发布清单需要明确承诺覆盖哪些语言
这一工程以应用发布前的语言资源检查为主线,先建立资源键集合,再逐步扩展占位参数、跨文件归属、限定词组合、修订差异和发布清单。检查输入是源码资源目录,输出是开发者能够定位到文件和键名的问题列表。校对结果进入发布流程前,还需要和实际界面校阅配合。
默认目录承担兜底职责,目标语言目录承担产品明确承诺的语言覆盖。两者并不冲突。对于明确要求完整翻译的按钮键,英文目录缺失就应该被标出;对于允许沿用名称的品牌词,可以后续建立显式例外。不要让“系统能够找到一段文字”代替产品对语言覆盖的定义。
本篇约定只检查单个模块中element目录下的字符串资源文件,目标语言固定为en_US和zh_CN。基本键集合来自base。其他资源类型、复合限定词和模块间引用另设规则,避免扫描器偷偷替整个资源系统做未经定义的推断。开始时把范围说清楚,后面的错误信息才具有可操作性。
在读入阶段拦住重复键
下面脚本保存为工程外的 locale_audit.py,要求Python三点十或更高版本,仅使用标准库。字符串资源文件采用标准JSON读取。遍历element下全部JSON文件后合并string数组,重复定义的键会在转成字典之前报错,否则后一个值覆盖前一个值,会把资源冲突隐藏起来。
from pathlib import Path
import json
def load_strings(element: Path) -> dict[str, str]:
if not element.is_dir():
raise ValueError(f"资源目录不存在: {element}")
result: dict[str, str] = {}
for source in sorted(element.glob("*.json")):
payload = json.loads(source.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"资源根节点应为对象: {source}")
rows = payload.get("string", [])
if not isinstance(rows, list):
raise ValueError(f"string应为数组: {source}")
for row in rows:
if not isinstance(row, dict):
raise ValueError(f"资源项应为对象: {source}")
name = row.get("name")
value = row.get("value")
if not isinstance(name, str) or not name.strip():
raise ValueError(f"资源名称为空或类型错误: {source}")
if name != name.strip() or not isinstance(value, str):
raise ValueError(f"资源字段类型或空白错误: {source}")
if name in result:
raise ValueError(f"资源键重复: {source}: {name}")
result[name] = value
return result
空字符串和缺少资源是两种情况。这个读取器允许空字符串进入集合,因此本篇的缺键检查不会把空值当成不存在。若某类文案禁止为空,应再增加值质量检查,并给出独立的问题类型。把所有问题都塞进“翻译缺失”,会导致维护者难以判断应该新增键还是修改值。
其他JSON资源文件没有string数组时会贡献空集合,这使颜色、尺寸等文件可以和字符串文件并存。如果某个本应包含字符串的文件拼错了根字段,项目还需要更严格的资源结构规则;这里的集合检查只针对已经识别出的字符串项,不通过猜测文件名擅自解释内容。
用集合差说明问题属于哪一侧
同一脚本继续加入下面代码。缺失集合是默认键减目标键;额外集合则反过来。结果排序后输出,便于版本比较。目标目录整个缺失时,也必须列出全部默认键,同时给出目录问题,不能因为路径不存在就跳过这一门语言。
import argparse
import sys
def audit(resources: Path, locales: list[str]) -> dict[str, object]:
base = load_strings(resources / "base" / "element")
if not base:
raise ValueError("默认字符串集合为空")
reports: list[dict[str, object]] = []
for locale in locales:
element = resources / locale / "element"
exists = element.is_dir()
target = load_strings(element) if exists else {}
missing = sorted(set(base) - set(target))
extra = sorted(set(target) - set(base))
reports.append({
"locale": locale,
"missing": missing,
"extra": extra,
"directoryMissing": not exists
})
return {"baseCount": len(base), "locales": reports}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("resources", type=Path)
parser.add_argument("--locales", nargs="+", default=["en_US", "zh_CN"])
args = parser.parse_args()
try:
report = audit(args.resources, args.locales)
except (OSError, ValueError) as error:
print(str(error), file=sys.stderr)
return 2
print(json.dumps(report, ensure_ascii=False, indent=2))
rows = report["locales"]
failed = any(row["missing"] or row["extra"] or row["directoryMissing"]
for row in rows)
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
退出码零表示本篇定义的键集合检查通过,一表示发现集合差异,二表示输入损坏或读取失败。构建流水线应该分别呈现这两类失败:翻译同事可以补齐缺键,却无法处理工作目录传错导致的路径错误。错误分类清楚,才能减少来回询问。
额外键按当前产品规则阻止通过,是为了避免废弃文案残留和键名拼错。如果工程允许语言专属字符串,应将这些键加入明确的允许集合,再从额外集合中扣除,而不是简单删掉整项检查。例外要有归属、用途和复核期限,不能靠某次构建参数长期绕开。
用最小资源集确认扫描结果
在base的string.json写入app_name、publish、cancel,值分别为“校对台”“发布”“取消”;在en_US保留app_name和publish,值为Review和Publish;在zh_CN保留全部三个中文键。资源文件均采用下面的结构,按上述清单补齐数组项即可。
{
"string": [
{ "name": "app_name", "value": "Review" },
{ "name": "publish", "value": "Publish" }
]
}
运行 python3 locale_audit.py entry/src/main/resources,依据集合差应得到baseCount为三、英文缺少cancel、中文没有缺失项,进程退出码为一。把英文cancel补成Cancel后,三个键集合相等,检查即可通过。这里比较的是键的存在性,翻译是否准确仍需阅读文案,不能从集合相等推导语义正确。

让页面承载结果而不是重新解释结果
下面是 entry/src/main/ets/pages/Index.ets 的完整页面。为便于核对展示逻辑,直接嵌入上面资源样例的键集合,重新核对按钮根据这些集合计算缺失项。此处属于开发者校对界面,卡片显示目标语言和缺失键;它并未读取电脑上的脚本文件。接入自动结果时,应通过明确的数据入口替换该数组,并校验字段类型。
interface LocaleSource {
locale: string;
keys: string[];
}
interface LocaleRow {
locale: string;
missing: string[];
}
@Entry
@Component
struct Index {
private baseKeys: string[] = ['app_name', 'publish', 'cancel'];
private sources: LocaleSource[] = [
{ locale: 'en_US', keys: ['app_name', 'publish'] },
{ locale: 'zh_CN', keys: ['app_name', 'publish', 'cancel'] }
];
@State rows: LocaleRow[] = [];
aboutToAppear(): void {
this.recheck();
}
private recheck(): void {
this.rows = this.sources.map((source: LocaleSource): LocaleRow => {
const row: LocaleRow = {
locale: source.locale,
missing: this.baseKeys.filter((key: string) => source.keys.indexOf(key) < 0)
};
return row;
});
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('多语发布校对').fontSize(26).fontWeight(FontWeight.Bold)
Text('缺键检测与默认回退').fontSize(16)
Text('base 3 keys').fontSize(18)
ForEach(this.rows, (row: LocaleRow) => {
Column({ space: 10 }) {
Text(row.locale).fontSize(22).fontWeight(FontWeight.Bold)
Text(`缺失${row.missing.length}项`).fontSize(18)
.fontColor(row.missing.length > 0 ? '#9A5B00' : '#16734B')
if (row.missing.length > 0) {
Text(row.missing.join(', ')).fontSize(18)
}
}
.width('100%').padding(20).backgroundColor('#F0F5FF')
.borderRadius(16).alignItems(HorizontalAlign.Start)
}, (row: LocaleRow) => row.locale)
Text('发布检查:请补齐缺失资源').fontSize(18).fontColor('#9A5B00')
Button('重新核对').width('100%').backgroundColor('#245AC7')
.onClick(() => { this.recheck(); })
}.width('100%').padding(20)
}.width('100%').height('100%').backgroundColor('#FFFFFF')
}
}

页面使用语言名作为行身份,避免按数组下标复用卡片后出现语言与问题串位。结果刷新应整体替换一次集合,不能先清空再逐行追加,让界面短暂显示“没有缺失”。后续若加入加载状态,应将“尚未检查”和“检查通过”设计成不同状态,空数组本身不能表达检查已经结束。
回退负责可用性,检查负责覆盖率
当英文按钮回退到默认文本时,排查顺序应是目标限定词目录、键是否存在、资源归属模块,再检查页面引用。不要立刻在组件里写死英文,也不要把默认资源删掉逼迫错误暴露。默认资源仍应完整,检查器则负责把语言覆盖要求转换成可比较的集合。
这份工具链从一开始就把输入问题、缺键问题和页面展示分开。它的价值是让发布前的语言校对拥有稳定入口和确定的差异表达:某个语言缺了哪个键、哪个文件不能解析、哪一种差异阻止继续打包。占位参数和限定词组合可以在这个结果模型上继续增加,而不需要推翻已经建立的资源归属关系。
定位顺序决定修复是否准确
把缺失键补进错误模块,是语言问题经常反复出现的原因。扫描命令的路径应指向当前发布模块的resources,新增资源也应落在同一个模块的语言目录中。提交前核对命令路径、目标文件和页面引用的资源归属,三者一致后再讨论文字是否正确。对于多个模块的工程,可以逐个调用检查器,并在外围保留模块名,不要把所有键合成一张没有归属的大表。
缺键与重复键应该采用不同的修复方式。缺键需要补齐目标语言资源,重复键需要确定唯一维护位置;把重复项随意改名,会迫使页面引用一起变化,还可能留下无法解释的额外键。资源名称具有引用关系,应将名称变更视为代码变更,而不是单纯的翻译调整。
还应检查结果是否来自当前修订。旧版本的检查输出即使显示没有缺失,也无法解释新增加的按钮。将扫描安排在资源变更后的打包前步骤,并由构建过程直接读取退出码,可以减少人工复制结果造成的错配。后续增加修订标识时,标识应进入内部结果数据,与用户可见文案保持分离。
扫描范围有意保持明确:默认键和两个目标语言目录形成可重复比较的输入。新增地区变体时先确认产品需要独立覆盖,还是允许沿用语言级资源,再把这一选择写入检查配置。否则盲目要求每个限定词目录包含全部默认键,反而会制造大量无意义的重复资源,增加后续维护成本。
相关资料:华为资源分类与访问、OpenHarmony资源分类与访问。
更多推荐

所有评论(0)