Files
Unlucky-Launcher/crates/i18n/src/catalog/tests.rs
T
AskaEth 0522a41a52
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.5
2026-11-12 12:06:59 +08:00

253 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `Catalog` 的守门测试(从 `catalog.rs` 原样搬出来:那边已经贴着 400 行硬上限,
//! 而页面每加一批 key 都要在这个模块里占一行)。
//!
//! **零改动搬运**:用例、注释、断言一字未动;`use super::*` 指向的仍是 `crate::catalog`。
//! M94(下载页)与 M95(日志页)几乎同时撞到这堵墙、各自搬了一次:两条 branch 的**用例部分
//! 逐字相同**(解冲突时按 M94 那份合并,测试数量 7 条不变)。
use super::*;
/// 手写一套**故意缺条**的目录(不经过 `catalog!`,模拟后续 mission 的注入表)。
const PARTIAL_ZH: &[(Key, &str)] = &[
(Key::NavInstances, "实例"),
(Key::NavLogs, "日志"),
(Key::NavSettings, "设置"),
(Key::NavDownload, "下载"),
(Key::InstancesNew, "新建实例"),
(Key::InstancesImport, "导入"),
(Key::InstancesBack, "返回实例列表"),
(Key::InstancesPlay, "启动"),
(Key::InstancesEmptyTitle, "未选择实例"),
(
Key::InstancesEmptyBody,
"在左侧选择一个实例,查看它的版本、加载器与启动选项。",
),
(Key::LoaderVanilla, "原版"),
(Key::LaunchOutcomeNormal, "正常结束"),
(Key::LaunchOutcomeCrash, "游戏崩溃"),
// 缺:LaunchOutcomeSilent / LaunchFailedWithCode / FontCjk* / Settings* / CoreTextUntranslated
];
#[test]
fn built_in_catalogs_have_every_key() {
// 抓得住:`catalog!` 的成对生成被绕开(有人改回「两套各写一遍」并漏了一条 / 抄重了一条)。
// 判据是**集合相等**:`Key` 枚举拦住了「表里有、枚举没有」,这里拦反方向。
// 抓不住:译文对不对(这只能是人工 / 后续的语言审查)。
for lang in Lang::ALL {
let catalog = Catalog::of(*lang);
assert_eq!(catalog.missing(), Vec::new(), "{}", catalog.lang().tag());
assert_eq!(catalog.diagnose(), None);
let mut keys = catalog
.entries()
.iter()
.map(|(key, _)| *key)
.collect::<Vec<_>>();
let total = keys.len();
keys.sort_unstable();
keys.dedup();
assert_eq!(keys.len(), total, "{} 目录里有重复条目", lang.tag());
assert_eq!(
keys.len(),
Key::ALL.len(),
"{} 条目数与 key 数不等",
lang.tag()
);
for key in Key::ALL {
assert!(
catalog.get(*key).is_some(),
"{} 缺 {}",
lang.tag(),
key.name()
);
}
}
assert!(!Key::ALL.is_empty());
}
#[test]
fn key_names_are_unique_and_stable() {
// 抓得住:同一个 key 被写成两个变体(名字重复会让日志/诊断指认失准)。
// 抓不住:重命名变体对下游源码的影响(那是编译错误,本来就跑不过)。
let mut names = Key::ALL.iter().map(|key| key.name()).collect::<Vec<_>>();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total);
assert_eq!(Key::NavInstances.name(), "NavInstances");
}
#[test]
fn chinese_catalog_is_actually_chinese() {
// 抓得住:整句漏译(英文被抄进中文表);一条都没有说明这张表是真翻过的。
// 抓不住:语义翻错、语气不合(机器能查的只有「与英文不同」这一层)。
let en = Catalog::of(Lang::En);
let zh = Catalog::of(Lang::ZhCn);
for key in Key::ALL {
let english = en.text(*key);
assert_ne!(english, zh.text(*key), "{} 未翻译", key.name());
assert!(!english.is_empty() && !zh.text(*key).is_empty());
}
}
#[test]
fn placeholder_sets_match_across_languages() {
// 抓得住:一处文案的 `{code}` / `{when}` 在另一套译文里丢了或写错名——
// 那会让 render() 输出半截句子,或让参数静默失效。
// 抓不住:占位符本身是不是正确的变量(那是调用方的职责,由页面侧测试守)。
let en = Catalog::of(Lang::En);
let zh = Catalog::of(Lang::ZhCn);
for key in Key::ALL {
let mut left = placeholders(en.text(*key));
let mut right = placeholders(zh.text(*key));
left.sort_unstable();
right.sort_unstable();
assert_eq!(left, right, "{}", key.name());
}
assert_eq!(placeholders("exit {code} at {when}"), vec!["code", "when"]);
assert_eq!(placeholders("没有占位符"), Vec::<&str>::new());
}
#[test]
fn missing_rows_are_diagnosed_in_plain_words() {
// 抓得住:缺译诊断的**可读性**(必须点名语言、key 名与英文原文)——注入目录缺条时靠它定位。
// 抓不住:内置目录真的缺译(内置表由 catalog! 结构保证,见上一条测试)。
let partial = Catalog::from_entries(Lang::ZhCn, PARTIAL_ZH);
assert_eq!(partial.missing().len(), Key::ALL.len() - PARTIAL_ZH.len());
let report = partial.diagnose().expect("应当报缺译");
assert!(report.starts_with("zh-CN 缺 "), "{report}");
assert!(report.contains("LaunchOutcomeSilent"), "{report}");
assert!(
report.contains("Suspected silent failure"),
"诊断要带上英文原文当参考:{report}"
);
assert!(
!report.contains("NavInstances"),
"已译的 key 不该出现在报告里:{report}"
);
}
#[test]
fn missing_translation_never_falls_back_silently() {
// 抓得住:缺译被静默顶替(回落英文 / 回落中文 / 返回空串)——那会产出「半句中文半句英文」的界面。
// 抓不住:MISSING 标记真的被画到屏幕上(那是 UI 侧截图验收的事)。
let partial = Catalog::from_entries(Lang::ZhCn, PARTIAL_ZH);
assert_eq!(partial.get(Key::LaunchOutcomeSilent), None);
assert_eq!(partial.text(Key::LaunchOutcomeSilent), MISSING);
assert_eq!(
partial.render(
Key::LaunchFailedWithCode,
&[("code", "0"), ("reason", "JVM 拒绝启动")]
),
"⟪missing:LaunchFailedWithCode⟫"
);
}
#[test]
fn render_substitutes_and_keeps_unfilled_placeholders_visible() {
// 抓得住:占位符替换的回归(漏参时**保留** `{code}` 而不是清空、不 panic)。
// 抓不住:调用方传没传对值(`("code", "0")` 是不是真的退出码)。
let zh = Catalog::of(Lang::ZhCn);
assert_eq!(
zh.render(
Key::LaunchFailedWithCode,
&[("code", "0"), ("reason", "mod 冲突")]
),
"启动失败(退出码 0):mod 冲突"
);
assert_eq!(
zh.render(Key::LaunchFailedWithCode, &[("code", "0")]),
"启动失败(退出码 0):{reason}"
);
assert_eq!(zh.text(Key::InstancesNew), "新建实例");
assert_eq!(zh.lang(), Lang::ZhCn);
}
/// M99:把单张条目表按域拆到 `catalog/<域>.rs` 之后,`key -> (en, zh-CN)` 必须**逐条逐字节**不变。
///
/// **抓得住**:① 某个 key 整条被搬丢(金表有、现在没有 ⇒ 条数或 key 名对不上);② 两条 key 的译文
/// 互抄 / 同 key 被覆盖成别人的译文(该行 en 或 zh 不等);③ 某条译文改了一个字符(逐字节比,
/// **不是**数条数——「A 丢了一条、B 复制成两条」条数照旧);④ 域的顺序被调换(`Key` 的 `Ord` 与
/// `entries()` 的声明顺序都靠它,金表按原顺序比)。
/// **抓不住**:译文本身翻得对不对——金表是「拆分前那一份」的快照,只能证明没变,不能证明翻得好。
///
/// 金表怎么来的:拆分前在这里临时加过一条 `println!("GOLD\t{}\t{}\t{}", …)` 的用例,把
/// `Key::ALL` ×(`Catalog::of(En)`, `Catalog::of(ZhCn)`)导出成 72 行 TSV,再原样抄成
/// `catalog/golden.rs`;那条导出用例随后已删除(不在提交里)。
#[test]
fn split_keeps_every_translation() {
let en = Catalog::of(Lang::En);
let zh = Catalog::of(Lang::ZhCn);
let actual = Key::ALL
.iter()
.map(|key| (key.name(), en.text(*key), zh.text(*key)))
.collect::<Vec<_>>();
assert_eq!(
actual.len(),
super::golden::len(),
"key 条数与拆分前不一致(某条被搬丢,或多出来一条)"
);
for (index, expected) in super::golden::rows().enumerate() {
let got = actual[index];
assert_eq!(
got.0, expected.0,
"第 {index} 条的 key 变了:拆分的域顺序被打乱,或某条被搬到了别处"
);
assert_eq!(got.1, expected.1, "{} 的英文译文变了", expected.0);
assert_eq!(got.2, expected.2, "{} 的中文译文变了", expected.0);
}
}
/// 用户可见文案里**不许**出现内部决策编号(`(D13)` / `(M175)` 这类)。
///
/// 为什么要在目录这一层守:`(D13)` 对用户没有任何意义(他不知道 D13 是什么),而这类串一旦
/// 混进来就再没人记得删。M197 出图时实测到下载目录的基准版本徽标写着 `基准版本(D13)`。
///
/// 变异证明:把 `SettingsJavaDownloadBaseline` 的译文改回 `Baseline (D13)` / `基准版本(D13)`
/// ⇒ 本用例红(中英两侧都扫)。判据是 `(`/`(` + `D`/`M` + 数字,不误伤 `(404)` / `(5xx / 429)`
/// 这类真对用户有意义的括号短语。
#[test]
fn no_user_visible_string_mentions_an_internal_decision_id() {
for (key, en, zh) in super::golden::rows().copied() {
for (side, text) in [("en", en), ("zh-CN", zh)] {
assert!(
!mentions_decision_id(text),
"{key} 的{side}译文里出现了内部决策编号:{text}"
);
}
}
}
/// 用户可见文案里**不许**出现 markdown 反引号。
///
/// 渲染层没有内联代码通道(M175 r3 复审已核实 `RichText|markdown|Span::|spans(` 全树只命中
/// 进度条与注释),反引号会原样上屏。M175 修过三处、M197 又修了三处;M168 的
/// `FontCjkStatusHint` 是最后一条(M197 复审 r1 指出的),这里一并钉住。
///
/// 变异证明:给任意一条译文加一对反引号 ⇒ 本用例红。
#[test]
fn no_user_visible_string_carries_markdown_backticks() {
for (key, en, zh) in super::golden::rows().copied() {
for (side, text) in [("en", en), ("zh-CN", zh)] {
assert!(
!text.contains('`'),
"{key} 的{side}译文里还有 markdown 反引号:{text}"
);
}
}
}
/// 串里有没有「(D13)」「(M175)」这类内部编号(全角半角括号都算)。
fn mentions_decision_id(text: &str) -> bool {
let chars: Vec<char> = text.chars().collect();
chars.windows(3).any(|window| {
(window[0] == '(' || window[0] == '(')
&& (window[1] == 'D' || window[1] == 'M')
&& window[2].is_ascii_digit()
})
}