HarmonyOS 组合限定词覆盖规则(连载04)【共创稿事节】
多语资源已经按模块归属校对,但只检查en_US和zh_CN仍会漏掉一种问题:横屏平板的深色界面需要更短的提示文案,指定组合目录却没有提供notice。普通英文目录中存在同名键,页面可以获得通用文本,发布前却仍需确认专用文案是否按计划交付。LocaleReview因此增加组合限定词覆盖合同,把语言齐全和场景直接覆盖分成两项要求。

组合目录先遵守命名顺序
本篇使用语言、方向、设备、颜色模式四个维度,组合为en_US-horizontal-tablet-dark。语言与地区内部使用下划线,维度之间使用中划线,排列顺序保持一致。工程还可能存在文字、网络和密度等限定词,新增维度时应按资源目录规范扩展合同,不能随意拼接一个看起来可读的文件夹名。
覆盖合同定义的是产品要求:指定目录直接提供某些资源。普通语言目录中出现同名键,仅作为定位信息,不计入这项直接覆盖条件。系统资源匹配服务于运行时取值,发布合同服务于交付完整性,二者承担不同职责;不能把目录覆盖检查写成一套未经定义的资源选择规则。
继续保留前篇entry与lexicon作用域、引用绑定以及模块内语言检查。新增profile限定在某个模块中,防止入口包的组合资源被拿去满足共享包要求。每项规则只列需要专用文案的键,不强迫组合目录复制全部默认资源。
把专用覆盖要求写进合同
在前篇resource_contract.json已有modules和references之外,增加下面的profiles字段。此处展示新增字段的完整对象,合入现有根对象即可。entry原有四个键及各语言翻译不变,lexicon继续保留自己的notice。
{
"profiles": [
{
"id": "tablet-dark-notice",
"module": "entry",
"locale": "en_US",
"orientation": "horizontal",
"device": "tablet",
"theme": "dark",
"requiredKeys": ["notice"]
}
]
}
entry的组合目录需要新建element/string.json,内容为一个string数组,其中notice的value是“%1$s: %2$d items”。它比通用英文文案更紧凑,参数位置和类型仍与默认资源一致。若目录暂缺,报告应同时列出缺少直接覆盖的notice,以及普通英文目录已有同名键,帮助维护者判断应该新增哪份文件。
组合目录中的完整资源内容如下,存放路径为 entry/src/main/resources/en_US-horizontal-tablet-dark/element/string.json。
{
"string": [
{ "name": "notice", "value": "%1$s: %2$d items" }
]
}
从结构化维度生成目录名
新建 qualifier_contract.py。当前项目仅开放两个语言、两种方向、两类设备和两种颜色模式,空字符串表示该维度未指定。取值和所需键都在读取阶段校验,避免将拼错的维度传到文件系统后再表现为一串缺键。
from pathlib import Path
from dataclasses import dataclass
import json
@dataclass(frozen=True)
class Profile:
id: str
module: str
locale: str
orientation: str
device: str
theme: str
required_keys: tuple[str, ...]
def folder(self) -> str:
return "-".join(value for value in (
self.locale, self.orientation, self.device, self.theme) if value)
def load_profiles(contract: Path) -> list[Profile]:
data = json.loads(contract.read_text(encoding="utf-8"))
rows = data.get("profiles") if isinstance(data, dict) else None
if not isinstance(rows, list) or not rows:
raise ValueError("需要非空profiles数组")
profiles: list[Profile] = []
ids: set[str] = set()
allowed = {
"locale": {"en_US", "zh_CN"},
"orientation": {"", "horizontal", "vertical"},
"device": {"", "phone", "tablet"},
"theme": {"", "dark", "light"}
}
for row in rows:
if not isinstance(row, dict):
raise ValueError("覆盖规则应为对象")
name, module = row.get("id"), row.get("module")
if not isinstance(name, str) or not name.strip() or name in ids:
raise ValueError("覆盖规则身份为空或重复")
if not isinstance(module, str) or not module.strip():
raise ValueError("覆盖规则缺少模块")
values: dict[str, str] = {}
for field, choices in allowed.items():
value = row.get(field, "")
if not isinstance(value, str) or value not in choices:
raise ValueError(f"限定词取值错误: {field}")
values[field] = value
keys = row.get("requiredKeys")
if not isinstance(keys, list) or not keys or not all(
isinstance(key, str) and key.strip() == key and key for key in keys
) or len(set(keys)) != len(keys):
raise ValueError("requiredKeys为空、重复或字段错误")
profiles.append(Profile(name, module, values["locale"],
values["orientation"], values["device"], values["theme"], tuple(keys)))
ids.add(name)
return profiles
合同以维度字段保存规则,目录名由同一方法生成,能够减少不同脚本各自拼接带来的差异。需要检查完整目录名时,可以直接查看folder输出,再对照工程结构。不要在报告里为了排版把横线换成其他符号,路径表达应和文件系统保持一致。
将专用覆盖与参数检查接起来
新增 profile_audit.py。它继续调用前篇的load_scopes、audit_workspace和参数解析器,先完成既有模块校对,再检查指定组合目录。默认集合之外的键会被标出,组合文案的参数也需要保持契约,避免只补齐名称却改变调用类型。
from pathlib import Path
import argparse
import json
import sys
from resource_scope import Scope, load_scopes
from workspace_audit import audit_workspace, has_issues
from locale_audit import load_strings
from placeholder_signature import placeholder_signature
from qualifier_contract import Profile, load_profiles
def audit_profiles(scopes: list[Scope], profiles: list[Profile]) -> list[dict[str, object]]:
modules = {scope.name: scope for scope in scopes}
reports: list[dict[str, object]] = []
for profile in profiles:
if profile.module not in modules:
raise ValueError(f"覆盖规则模块不存在: {profile.module}")
resources = modules[profile.module].resources
base = load_strings(resources / "base" / "element")
if any(key not in base for key in profile.required_keys):
raise ValueError(f"覆盖合同引用未知默认键: {profile.id}")
signatures = {key: placeholder_signature(value) for key, value in base.items()}
element = resources / profile.folder() / "element"
direct = load_strings(element) if element.is_dir() else {}
language_path = resources / profile.locale / "element"
language = load_strings(language_path) if language_path.is_dir() else {}
parameter_issues: list[dict[str, str]] = []
for key in sorted(set(base) & set(direct)):
try:
if placeholder_signature(direct[key]) != signatures[key]:
parameter_issues.append({"key": key, "reason": "参数签名不同"})
except ValueError as error:
parameter_issues.append({"key": key, "reason": str(error)})
reports.append({
"profile": profile.id, "module": profile.module,
"folder": profile.folder(), "directDirectoryMissing": not element.is_dir(),
"missing": sorted(set(profile.required_keys) - set(direct)),
"unknown": sorted(set(direct) - set(base)),
"localeKeysPresent": sorted(set(profile.required_keys) & set(language)),
"parameterIssues": parameter_issues
})
return reports
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("root", type=Path)
parser.add_argument("contract", type=Path)
args = parser.parse_args()
try:
scopes, refs = load_scopes(args.root, args.contract)
workspace = audit_workspace(scopes, refs)
profiles = audit_profiles(scopes, load_profiles(args.contract))
except (OSError, ValueError) as error:
print(str(error), file=sys.stderr)
return 2
print(json.dumps({"workspace": workspace, "profiles": profiles},
ensure_ascii=False, indent=2))
failed = has_issues(workspace) or any(
row["directDirectoryMissing"] or row["missing"] or row["unknown"]
or row["parameterIssues"] for row in profiles)
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
所需文件均来自当前连载正文:locale_audit.py和placeholder_signature.py沿用第二篇,resource_scope.py和workspace_audit.py沿用第三篇,再加入本篇两个文件。运行 python3 profile_audit.py 工程根目录 resource_contract.json,退出码零表示合同检查通过,一表示资源问题,二表示输入或合同结构错误。
把组合目录补齐后,notice的缺失列表应清空;将其中的第二个参数改成字符串类型,应只在对应profile里出现参数问题。若把notice误写成notcie,报告会同时指出所需键缺失和额外未知键。两条信息指向同一次拼写问题,修复时应更正资源名,而不是新增一份重复文案。

页面明确说明是哪一种缺失
下面替换 pages/Index.ets,展示组合目录尚缺notice、语言目录已有同名键的固定校对样例。页面的规则按钮展开和收起合同要求,扫描仍由上述工具链负责。将两种存在性分别表达,可以避免维护者看到“英文已有文案”后误以为不需要处理。
@Entry
@Component
struct Index {
@State showRule: boolean = true;
build() {
Scroll() {
Column({ space: 16 }) {
Text('多语发布校对').fontSize(26).fontWeight(FontWeight.Bold)
Text('组合限定词覆盖').fontSize(18)
Column({ space: 12 }) {
Text('entry').fontSize(20)
Text('en_US-horizontal-tablet-dark').fontSize(20).maxLines(3)
Text('notice').fontSize(22).fontWeight(FontWeight.Bold)
Text('缺少直接覆盖').fontSize(18).fontColor('#9A5B00')
}.width('100%').padding(20).backgroundColor('#FFF3DD').borderRadius(16)
Text('语言目录已有同名键').fontSize(18).fontColor('#245AC7')
if (this.showRule) {
Text('要求:组合目录直接提供notice').fontSize(18)
.width('100%').padding(20).backgroundColor('#EDF3FF')
}
Button('查看覆盖规则').width('100%').backgroundColor('#245AC7')
.onClick(() => { this.showRule = !this.showRule; })
}.width('100%').padding(20)
}.width('100%').height('100%').backgroundColor('#FFFFFF')
}
}

让产品要求与运行时匹配各有位置
资源目录可以只覆盖部分键,其他键继续沿用普通资源。发布合同也只应要求确有专用表达的内容,避免把每个语言、方向和颜色组合都变成一份完整拷贝。复制越多,后续参数变更越容易遗漏,维护成本也会随组合数量增加。
直接覆盖只针对明确交付的内容
并非每个限定词组合都需要一份完整字符串文件。本例要求横屏平板深色场景提供notice,是因为该场景采用更紧凑的提示表达;app_name、publish和cancel仍可继续使用通用语言资源。合同列出的键越明确,报告越容易解释,维护者也不需要通过复制全部默认资源来消除检查提示。
同一个组合目录可以承担多项产品要求,不同profile也可以针对不同键检查同一目录。规则身份用于区分这些交付要求,不能直接拿文件夹名作为唯一结果键。反过来,如果同一规则身份重复出现,读取阶段就应停止,防止报告覆盖前一项要求,让某些缺失内容从结果中消失。
普通语言目录中的同名键只说明通用资源存在。它有助于定位当前已有哪份文案,却没有表达专用文案是否被审阅和交付。把localeKeysPresent保留为独立字段,页面便能同时呈现两项事实,而不必用一个模糊的“资源存在”标记掩盖差异。
目录名称错误要回到维度配置
如果报告持续提示组合目录缺失,而工程中确实能看到一个相近文件夹,应逐项比对语言地区、方向、设备与颜色模式,再比对连接符和顺序。不要给检查器增加一个“忽略目录顺序”的开关。资源目录有自己的命名合同,容忍错误拼写会让源码检查和系统加载看到不同的路径。
当前代码从结构化字段生成路径,因此合同里填写错误的颜色模式或设备类型会立即失败。若工程需要加入其他设备或语言,应在允许集合和样本中一起增加,避免只放开输入却没有核对相应目录规范。规则扩展应保持可追溯的含义,而不是把任意字符串都视为有效限定词。
组合目录中的未知键也不能被静默忽略。它可能是资源名称拼写错误,也可能是默认集合还没有同步更新。先核对键的归属和引用,再决定改名、补默认资源或移除无用项。无论采取哪种修复,都应保持模块作用域,不能因为另一个模块存在同名键就自动放行。
用成对样本防止检查规则退化
校对时保留一对相反样本:普通英文目录已有notice而组合目录缺失,应仍报告直接覆盖不足;组合目录补齐notice后,再把第二个参数改错,应报告参数问题。这两步分别检验覆盖要求和字符串合同,能够防止后续重构只保留其中一项检查。
再加入一个无效profile模块,以及一个不在默认集合中的requiredKeys,二者都属于合同输入问题。它们与目标目录缺文件不同,修复位置也不同。把输入错误保留为退出码二,可以让流水线明确提示维护检查配置,而不是把问题错误地分配给翻译或设计人员。
如果产品取消某个专用场景,应同时移除对应profile并审视现有组合资源是否还被使用。删除合同只意味着不再要求专用交付,并不会自动清理资源文件;删除资源也不会自动取消交付要求。两类变更需要在同一发布修订中核对,避免留下长期无人维护的组合目录。
从模块内语言校对推进到组合覆盖后,工具链仍然保留原有边界:模块决定归属,语言检查保证通用翻译,profile检查保证指定交付。将这些要求结构化后,新增场景只需增加明确规则,再为新维度补齐校验与样例,而不必依靠人工逐个打开目录猜测资源是否齐全。
相关资料:华为资源分类与访问、OpenHarmony资源目录规则。
更多推荐

所有评论(0)