feat(app-iced): M120 实例页与启动阶段标签跟着界面语言走
来源:M106 r1 复审点名——中文机器上首屏(实例页)仍是英中混排(页头 / New instance / Import / ALL INSTANCES / VERSION / Present on disk / Running · pid / Launch failed)。 用户的原话是「我系统语言是中文 为什么打开软件是英文」。 改了什么 - i18n:catalog/instances.rs 从 6 条死 key 补到 34 条(页头 / 列表列头 / 详情动作 / 窄窗说明 / 元信息标签与值 / 隔离档位 / 运行状态 / 四个启动阶段)。族在装配链第 2 位, 新 key 追加在本族末尾,golden 对应位置插入(不是文件末尾追加)。 - 页面:新增 pages/instances/text.rs(Texts)——页面唯一的取词入口,实例页里不再有 任何可见英文字面量;detail / demo 也改从它取。 - 阶段标签:view/instances.rs 的 Running / Launch failed / 四阶段全部走目录; **数字与分隔符不随语言变形**(运行中 · pid 1234 与 Running · pid 1234 只换前半句)。 - 顺手修掉两处「语言反向泄漏」:原版徽标此前直接画 presenter 的中文「原版」(英文界面 会漏中文),现在走 LoaderVanilla;隔离档位 isolated/shared 也从 core 的稳定 id 换成人话。 - 删掉 InstancesLastPlayed:M117 加的整句 key,从未接线,形状与「标签 + 值」的元信息卡 不符,由 InstancesMetaLastPlayed + InstancesNeverPlayed 取代。 守门(pages/instances/tests/i18n.rs + app/view/tests.rs) - 中文态无 ASCII 英文单词(品牌 Java / 技术标识 pid 白名单); - 每一条 Instances* 的两套译文必须不同(切语言时真的变); - Running 的数字与 · pid 分隔符在两套语言里逐字保留; - 页面源码里 29 条旧英文基线不许回来;每条 key 都必须被页面引用(死 key)。 出图:DevDocs/artifacts/m120/01~12(有实例 / 空态 / 运行中 × 浅深 × 中英)。
This commit is contained in:
1 parent
2f491c31f9
commit
1a4e1fee12
17 files changed
+924
-119
No files matched your search
@@ -0,0 +1,263 @@
|
||||
//! 实例页的**文案守门**(M120):中文态不许漏英文、切语言必须真的变、运行状态的数字不许变形。
|
||||
//!
|
||||
//! | 用例 | 抓得住什么 |
|
||||
//! |---|---|
|
||||
//! | [`the_chinese_page_has_no_english_words`] | 目录里某条中文忘了翻(或翻完还留着英文词) |
|
||||
//! | [`every_instances_text_changes_with_the_language`] | 两套语言写成一样(= 切换语言时那一处不动,看着像「已 i18n」其实是死文案) |
|
||||
//! | [`the_running_line_keeps_its_number_and_separator`] | `Running · pid 1234` 的数字 / `pid` / `·` 被翻译走样 |
|
||||
//! | [`the_page_sources_have_no_english_baseline_left`] | 页面里又写回旧英文基线字面量(M120 之前的形状) |
|
||||
//! | [`every_instances_key_is_referenced_by_the_page`] | 目录里加了 key、页面没人用(死 key,M117 那七条就是这么来的) |
|
||||
//!
|
||||
//! ## 边界(别把这张表读成「视图覆盖」)
|
||||
//!
|
||||
//! iced 0.14 的 `Element` **没有内省接口**(`instances/tests.rs` 里那条「为什么读源码」写的就是它),
|
||||
//! 所以「上屏文本」取样只能从页面取词的那**一个入口** [`Texts`] 拿:页面用它画、测试用同一个
|
||||
//! `Texts` 断言,不是另写一份平行实现。仍然抓不住的是「页面又冒出一条**新**硬编码字面量」——
|
||||
//! 那由 [`the_page_sources_have_no_english_baseline_left`] 兜已知的那批 + 出图人工看。
|
||||
//! 「模型这一层(快照 → 状态行 / 元信息)跟着 `Lang` 变」由 `app/view/tests.rs` 守。
|
||||
|
||||
use unlucky_i18n::catalog::MISSING;
|
||||
use unlucky_i18n::{Catalog, Key, Lang};
|
||||
|
||||
use super::super::Texts;
|
||||
|
||||
/// 中文界面里允许出现的 ASCII 词:**品牌名与技术标识**。
|
||||
///
|
||||
/// 每一条都要说清为什么:
|
||||
///
|
||||
/// - `Java`:运行时品牌名(`JAVA` 那格的值域里也全是 `Temurin 21` 这类品牌串),翻成「爪哇」是错的;
|
||||
/// - `pid`:进程号的技术标识。用户拿它去任务管理器里搜进程,翻成「进程」就丢了这条可操作性。
|
||||
const ALLOWED: &[&str] = &["Java", "pid"];
|
||||
|
||||
/// 页面取词的五个源码文件(死 key 扫描与旧串扫描共用)。
|
||||
const PAGE_SOURCES: [(&str, &str); 5] = [
|
||||
("instances.rs", include_str!("../../instances.rs")),
|
||||
("instances/detail.rs", include_str!("../detail.rs")),
|
||||
("instances/demo.rs", include_str!("../demo.rs")),
|
||||
("instances/text.rs", include_str!("../text.rs")),
|
||||
(
|
||||
"app/view/instances.rs",
|
||||
include_str!("../../../app/view/instances.rs"),
|
||||
),
|
||||
];
|
||||
|
||||
/// M120 之前页面里那些**英文基线字面量**。它们现在只许活在目录里。
|
||||
///
|
||||
/// 页头标题 `Instances` 不在清单里:它在后面由结构断言「`Header::new` 的实参必须是
|
||||
/// `texts.title()`」单独守(`"Instances"` 这三个字在 `text.rs` 里是**族名前缀**,进清单会假红)。
|
||||
const OLD_LITERALS: [&str; 29] = [
|
||||
"ALL INSTANCES",
|
||||
"Local versions and their mod loaders",
|
||||
"New instance",
|
||||
"Import",
|
||||
"Back to instances",
|
||||
"Play",
|
||||
"Stop",
|
||||
"Duplicate",
|
||||
"Open folder",
|
||||
"Delete",
|
||||
"No instance selected",
|
||||
"Present on disk",
|
||||
"Missing",
|
||||
"Never",
|
||||
"Running · pid ",
|
||||
"Launch failed · ",
|
||||
"Downloading",
|
||||
"Verifying",
|
||||
"Starting",
|
||||
"Waiting for window",
|
||||
"VERSION",
|
||||
"VERSION ID",
|
||||
"LOADER",
|
||||
"ISOLATION",
|
||||
"FILES",
|
||||
"LAST PLAYED",
|
||||
"MEMORY",
|
||||
"JAVA",
|
||||
"MODS",
|
||||
];
|
||||
|
||||
/// 把 `{占位符}` 抹掉再切 ASCII 词:占位符是**运行时数据**(pid / 原因 / 时间),不是文案。
|
||||
fn ascii_words(text: &str) -> Vec<String> {
|
||||
let mut stripped = String::new();
|
||||
let mut depth = 0usize;
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'{' => depth += 1,
|
||||
'}' if depth > 0 => depth -= 1,
|
||||
_ if depth > 0 => {}
|
||||
_ => stripped.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
let mut words = Vec::new();
|
||||
let mut current = String::new();
|
||||
for ch in stripped.chars() {
|
||||
if ch.is_ascii_alphabetic() {
|
||||
current.push(ch);
|
||||
} else if !current.is_empty() {
|
||||
words.push(std::mem::take(&mut current));
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
words.push(current);
|
||||
}
|
||||
words
|
||||
}
|
||||
|
||||
/// 有没有中日韩统一表意文字(「这条文案真的翻成中文了吗」的最直接判据)。
|
||||
fn has_cjk(text: &str) -> bool {
|
||||
text.chars().any(|c| {
|
||||
matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}')
|
||||
})
|
||||
}
|
||||
|
||||
/// **中文界面无英文词**:实例页这一族的每一条中文译文,除了 [`ALLOWED`] 之外不许出现 ASCII 单词。
|
||||
///
|
||||
/// 抓得住:漏翻(中英写成一样)、翻了一半(`Present on disk` → `已在磁盘上 Present`)、
|
||||
/// 新加 key 时中文那栏顺手抄了英文。
|
||||
/// 抓不住:译文本身读着别扭;也抓不住「页面另写了一条字面量」(见模块文档的边界一节)。
|
||||
///
|
||||
/// **变异实测**(M120 交付时跑过):把 `InstancesPresentOnDisk` 的中文改回 `Present on disk`,
|
||||
/// 这条立刻红并指出 key 名。
|
||||
#[test]
|
||||
fn the_chinese_page_has_no_english_words() {
|
||||
let zh = Lang::ZhCn.catalog();
|
||||
let mut offenders = Vec::new();
|
||||
let mut checked = 0;
|
||||
|
||||
for key in Texts::keys() {
|
||||
let text = zh.text(key);
|
||||
assert_ne!(text, MISSING, "{} 没有中文译文", key.name());
|
||||
checked += 1;
|
||||
for word in ascii_words(text) {
|
||||
if !ALLOWED.contains(&word.as_str()) {
|
||||
offenders.push(format!("{} · 出现 {word:?} ← 「{text}」", key.name()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
offenders.is_empty(),
|
||||
"中文界面上这些英文词会漏出来(品牌名 / 技术标识请加进 ALLOWED 并写明理由):\n{}",
|
||||
offenders.join("\n")
|
||||
);
|
||||
assert!(
|
||||
checked >= 30,
|
||||
"实例页的 key 不该被成批删掉(只查到 {checked} 条)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **切换语言,文本真的变**:这一族的每一条中英译文都必须不同。
|
||||
///
|
||||
/// 抓得住:翻完没改(两栏抄同一句)——那种 key 在切语言时**一个字都不动**,界面上看起来
|
||||
/// 「已经国际化」,其实那一处永远是英文。今夜反复出现的「测试绿着但什么都没守住」就是这一类。
|
||||
/// 抓不住:某条译文翻错了词。
|
||||
#[test]
|
||||
fn every_instances_text_changes_with_the_language() {
|
||||
let en = Lang::En.catalog();
|
||||
let zh = Lang::ZhCn.catalog();
|
||||
|
||||
for key in Texts::keys() {
|
||||
let (a, b) = (en.text(key), zh.text(key));
|
||||
assert_ne!(a, MISSING, "{} 没有英文译文", key.name());
|
||||
assert_ne!(b, MISSING, "{} 没有中文译文", key.name());
|
||||
assert_ne!(
|
||||
a,
|
||||
b,
|
||||
"{} 的两套语言写成了一样({a:?})——切语言时这一处不会变",
|
||||
key.name()
|
||||
);
|
||||
// 中文那栏必须真的有汉字——**整句只有品牌名时例外**(`InstancesMetaJava` = "Java":
|
||||
// 那一格的值域是 `Temurin 21` 这种品牌串,翻成「爪哇」是错的)。
|
||||
let brand_only = {
|
||||
let words = ascii_words(b);
|
||||
!words.is_empty() && words.iter().all(|word| ALLOWED.contains(&word.as_str()))
|
||||
};
|
||||
assert!(
|
||||
has_cjk(b) || brand_only,
|
||||
"{} 的中文译文里一个汉字都没有:{b:?}",
|
||||
key.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **数字与分隔符不随语言变形**:`Running · pid 1234` 的两套语言只换前半句。
|
||||
///
|
||||
/// 抓得住:把 `pid` 翻成「进程」、把 `·` 换成 `-`、把 `{pid}` 写成固定文字、少一个空格。
|
||||
/// 抓不住:真实 pid 的取值(那是 core 的事,出图那台机器上也没真跑过)。
|
||||
#[test]
|
||||
fn the_running_line_keeps_its_number_and_separator() {
|
||||
let en = Catalog::of(Lang::En).render(Key::InstancesRunning, &[("pid", "1234")]);
|
||||
let zh = Catalog::of(Lang::ZhCn).render(Key::InstancesRunning, &[("pid", "1234")]);
|
||||
|
||||
assert_eq!(en, "Running · pid 1234");
|
||||
assert_eq!(zh, "运行中 · pid 1234");
|
||||
assert_ne!(en, zh, "切换语言时这一行要变(只换标签本体)");
|
||||
for text in [en, zh] {
|
||||
assert!(text.contains("1234"), "数字丢了:{text}");
|
||||
assert!(text.contains(" · pid "), "分隔符或 pid 变形了:{text}");
|
||||
}
|
||||
|
||||
// 失败那一行同理:core 给的原因原文原样保留,只翻前半句
|
||||
let failed_en = Catalog::of(Lang::En).render(Key::InstancesFailed, &[("reason", "exit 1")]);
|
||||
let failed_zh = Catalog::of(Lang::ZhCn).render(Key::InstancesFailed, &[("reason", "exit 1")]);
|
||||
assert_eq!(failed_en, "Launch failed · exit 1");
|
||||
assert_eq!(failed_zh, "启动失败 · exit 1");
|
||||
}
|
||||
|
||||
/// **旧串不许回来**:M120 之前页面里的英文基线字面量,现在只许活在目录里。
|
||||
///
|
||||
/// 为什么用「已知清单」而不是「扫所有英文串」:合法写法太多了(`Action::Import` 这类枚举、
|
||||
/// 品牌名、文档里的反引号代码),扫宽了会假红。清单里这 29 条是**这次真的搬走的那批**,
|
||||
/// 谁把它们抄回页面,这条就红——那正是「页面又变成中英混排」的唯一来源。
|
||||
///
|
||||
/// **变异实测**(M120 交付时跑过):把 `"ALL INSTANCES"` 写回 `instances.rs` 的页头,立刻红
|
||||
/// 并指出是哪个文件、哪一句。
|
||||
#[test]
|
||||
fn the_page_sources_have_no_english_baseline_left() {
|
||||
for (name, source) in PAGE_SOURCES {
|
||||
for literal in OLD_LITERALS {
|
||||
let needle = format!("\"{literal}\"");
|
||||
assert!(
|
||||
!source.contains(&needle),
|
||||
"{name} 里又写回了英文基线字面量 {literal:?}(走 Texts / 目录,别写死)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 反向锚:这些来源**必须**真的在用 Texts / 目录,否则上面那条是空转
|
||||
assert!(
|
||||
PAGE_SOURCES[0].1.contains("Texts::of("),
|
||||
"实例页没有从 Texts 取词"
|
||||
);
|
||||
assert!(
|
||||
PAGE_SOURCES[0].1.contains("Header::new(texts.title())"),
|
||||
"页头标题必须是词表里的那条(`Header::new` 里写死字符串 = 中文界面上顶着英文标题)"
|
||||
);
|
||||
assert!(
|
||||
PAGE_SOURCES[3].1.contains("Key::Instances"),
|
||||
"text.rs 没有引用本族的 key"
|
||||
);
|
||||
assert!(
|
||||
PAGE_SOURCES[4].1.contains("Key::InstancesStage"),
|
||||
"阶段标签没有走目录"
|
||||
);
|
||||
}
|
||||
|
||||
/// **死 key 守门**:目录里每一条 `Instances*` 都必须被页面(或产品快照转换)真的引用。
|
||||
///
|
||||
/// M117 加的那七条就是这么沉的:目录里写着,页面上一个都没用。扫描是字面出现的
|
||||
/// `Key::<名字>`,所以用宏拼 key 名能骗过它——这个局限写在这里,不假装它更强。
|
||||
#[test]
|
||||
fn every_instances_key_is_referenced_by_the_page() {
|
||||
for key in Texts::keys() {
|
||||
let needle = format!("Key::{}", key.name());
|
||||
assert!(
|
||||
PAGE_SOURCES
|
||||
.iter()
|
||||
.any(|(_, source)| source.contains(&needle)),
|
||||
"{needle} 在目录里定义了,但实例页没有任何地方用它(死 key,删掉或接上)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user