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

多语言资源缺键与默认回退

发布清单需要明确承诺覆盖哪些语言

这一工程以应用发布前的语言资源检查为主线,先建立资源键集合,再逐步扩展占位参数、跨文件归属、限定词组合、修订差异和发布清单。检查输入是源码资源目录,输出是开发者能够定位到文件和键名的问题列表。校对结果进入发布流程前,还需要和实际界面校阅配合。

默认目录承担兜底职责,目标语言目录承担产品明确承诺的语言覆盖。两者并不冲突。对于明确要求完整翻译的按钮键,英文目录缺失就应该被标出;对于允许沿用名称的品牌词,可以后续建立显式例外。不要让“系统能够找到一段文字”代替产品对语言覆盖的定义。

本篇约定只检查单个模块中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资源分类与访问。

Logo

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

更多推荐