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
@@ -1,7 +1,6 @@
|
||||
//! `app/view.rs` 的守门测试(rebase 到 5adac23 之后 view.rs 贴 400 行上限,按「测试独立成文件」拆出)。
|
||||
|
||||
use super::*;
|
||||
// 拆分后 `view.rs` 不再 import 这三个(它们只被搬走的实例页转换用),用到的地方自己引
|
||||
use unlucky_presenter::dto::{
|
||||
InstanceRuntime, InstanceSummary, LaunchStage, LoaderInfo, LoaderKind, Progress,
|
||||
};
|
||||
@@ -31,9 +30,9 @@ fn instance(runtime: InstanceRuntime) -> InstanceSummary {
|
||||
/// 与「取证密闭」正好相反,而且图看起来完全正常);② 加了变体却映射到别的态这种复制粘贴错。
|
||||
/// 抓不住:`shell::overridden`(标题栏 / 导航轨按钮走的**另一处** match,断言在 `shell/tests.rs`),
|
||||
/// 也抓不住不消费 `model.forced` 的裸 `iced::button`(设置页「外观」区就是)。
|
||||
/// 测试用的薄包装:默认宽度 / 默认分类 / 空草稿。
|
||||
fn at(state: &AppState, forced: Option<crate::cli::ForcedState>) -> pages::Model {
|
||||
let (draft, lang) = (pages::InstallDraft::default(), Lang::En);
|
||||
/// 测试用的薄包装:默认宽度 / 默认分类 / 空草稿 / **给定语言**。
|
||||
fn at_lang(state: &AppState, forced: Option<crate::cli::ForcedState>, lang: Lang) -> pages::Model {
|
||||
let draft = pages::InstallDraft::default();
|
||||
let page = pages::content::PageState::default();
|
||||
model(
|
||||
state,
|
||||
@@ -46,6 +45,11 @@ fn at(state: &AppState, forced: Option<crate::cli::ForcedState>) -> pages::Model
|
||||
)
|
||||
}
|
||||
|
||||
/// 同 [`at_lang`],语言取英文(绝大多数断言只关心「快照 → 模型」的映射,不关心译文)。
|
||||
fn at(state: &AppState, forced: Option<crate::cli::ForcedState>) -> pages::Model {
|
||||
at_lang(state, forced, Lang::En)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_forced_state_maps_to_the_expected_interaction() {
|
||||
let state = AppState::new();
|
||||
@@ -122,22 +126,131 @@ fn failed_instances_can_be_retried() {
|
||||
reason: "exit 1".to_string(),
|
||||
});
|
||||
assert_eq!(
|
||||
runtime_status(&failed.runtime).as_deref(),
|
||||
runtime_status(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||||
Some("Launch failed · exit 1")
|
||||
);
|
||||
assert!(detail_of(&failed).can_launch, "失败后要能重试");
|
||||
assert!(
|
||||
detail_of(&failed, Lang::En.catalog()).can_launch,
|
||||
"失败后要能重试"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamps_are_sliced_not_guessed() {
|
||||
let en = Lang::En.catalog();
|
||||
assert_eq!(
|
||||
last_played(Some("2026-10-29T19:22:00Z")),
|
||||
last_played(Some("2026-10-29T19:22:00Z"), en),
|
||||
"2026-10-29 19:22 UTC"
|
||||
);
|
||||
assert_eq!(last_played(None), "Never");
|
||||
assert_eq!(last_played(None, en), "Never");
|
||||
// 短串 / 非 ASCII 原样返回(不 panic)
|
||||
assert_eq!(last_played(Some("2026-10")), "2026-10");
|
||||
assert_eq!(last_played(Some("昨天")), "昨天");
|
||||
assert_eq!(last_played(Some("2026-10"), en), "2026-10");
|
||||
assert_eq!(last_played(Some("昨天"), en), "昨天");
|
||||
// 「从未游玩」是唯一走词表的那一支
|
||||
assert_eq!(last_played(None, Lang::ZhCn.catalog()), "从未游玩");
|
||||
}
|
||||
|
||||
/// **快照 → 页面文案跟着 `App::lang` 走**(M120 的核心接线断言,模型这一层)。
|
||||
///
|
||||
/// 抓得住:`view::model` 又把语言写死成 `Lang::En`(M106 之前的原样)、阶段/元信息标签还留着
|
||||
/// 英文基线字面量、原版徽标直接画 presenter 的中文「原版」。这三种都不会让任何别的测试红。
|
||||
/// 抓不住:页面把 key 画没画上屏(那是 `pages/instances/tests/i18n.rs` 的取样口 + 出图)。
|
||||
#[test]
|
||||
fn the_runtime_lines_and_meta_labels_follow_the_language() {
|
||||
let mut state = AppState::new();
|
||||
state.apply(unlucky_presenter::dto::AppEvent::Core(
|
||||
unlucky_presenter::dto::CoreEvent::InstancesChanged(vec![
|
||||
instance(InstanceRuntime::Running { pid: 1234 }),
|
||||
instance(InstanceRuntime::Busy {
|
||||
stage: LaunchStage::Verify,
|
||||
progress: Progress {
|
||||
done: 750,
|
||||
total: 1_500,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
));
|
||||
state.navigate(Route::Detail("survival".to_string()));
|
||||
|
||||
let en = at_lang(&state, None, Lang::En);
|
||||
let zh = at_lang(&state, None, Lang::ZhCn);
|
||||
|
||||
// 列表 overline:中英各一份,数字与分隔符不动
|
||||
assert_eq!(
|
||||
en.instances.rows[0].status.as_deref(),
|
||||
Some("Running · pid 1234")
|
||||
);
|
||||
assert_eq!(
|
||||
zh.instances.rows[0].status.as_deref(),
|
||||
Some("运行中 · pid 1234")
|
||||
);
|
||||
assert_eq!(
|
||||
en.instances.rows[1].status.as_deref(),
|
||||
Some("Verifying · 50%")
|
||||
);
|
||||
assert_eq!(zh.instances.rows[1].status.as_deref(), Some("校验中 · 50%"));
|
||||
|
||||
// 元信息卡的标签与值
|
||||
let (en_detail, zh_detail) = (
|
||||
en.instances.detail.expect("有详情"),
|
||||
zh.instances.detail.expect("有详情"),
|
||||
);
|
||||
let labels = |detail: &pages::Detail| -> Vec<String> {
|
||||
detail.meta.iter().map(|cell| cell.label.clone()).collect()
|
||||
};
|
||||
assert_eq!(
|
||||
labels(&en_detail),
|
||||
vec![
|
||||
"VERSION",
|
||||
"LOADER",
|
||||
"ISOLATION",
|
||||
"VERSION ID",
|
||||
"FILES",
|
||||
"LAST PLAYED"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
labels(&zh_detail),
|
||||
vec!["版本", "加载器", "隔离方式", "版本标识", "文件", "上次游玩"]
|
||||
);
|
||||
|
||||
// 值里只有「是否在磁盘上」与「上次游玩」是可翻译的;版本号 / 隔离方式 / 版本标识是数据
|
||||
// 隔离档位是 core 的稳定 id(`isolated` / `shared`),也要换成人话;认不出来的原样上屏
|
||||
assert_eq!(en_detail.meta[2].value, "Isolated");
|
||||
assert_eq!(zh_detail.meta[2].value, "独立目录");
|
||||
assert_eq!(en_detail.meta[4].value, "Present on disk");
|
||||
assert_eq!(zh_detail.meta[4].value, "已在磁盘上");
|
||||
assert_eq!(
|
||||
en_detail.meta[5].value, "2026-10-29 19:22 UTC",
|
||||
"时间戳是数据:只切片,不随语言变形"
|
||||
);
|
||||
assert_eq!(zh_detail.meta[5].value, "2026-10-29 19:22 UTC");
|
||||
|
||||
// 详情状态行与列表 overline 同源
|
||||
assert_eq!(zh_detail.status.as_deref(), Some("运行中 · pid 1234"));
|
||||
}
|
||||
|
||||
/// 原版徽标:`LoaderInfo::badge()` 对原版返回**中文**「原版」,英文界面里必须换回 `Vanilla`。
|
||||
///
|
||||
/// 抓得住:`loader_badge` 退回直接调 `badge()`(M90 ~ M119 的形状)——那样英文界面会漏中文,
|
||||
/// 而中文界面又是对的,所以只有这条双语断言抓得住它。
|
||||
#[test]
|
||||
fn the_vanilla_badge_follows_the_language() {
|
||||
let mut bare = instance(InstanceRuntime::Idle);
|
||||
bare.loader = LoaderInfo {
|
||||
kind: LoaderKind::Vanilla,
|
||||
version: String::new(),
|
||||
};
|
||||
let mut state = AppState::new();
|
||||
state.apply(unlucky_presenter::dto::AppEvent::Core(
|
||||
unlucky_presenter::dto::CoreEvent::InstancesChanged(vec![bare]),
|
||||
));
|
||||
state.navigate(Route::Detail("survival".to_string()));
|
||||
|
||||
let en = at_lang(&state, None, Lang::En);
|
||||
let zh = at_lang(&state, None, Lang::ZhCn);
|
||||
assert_eq!(en.instances.rows[0].supporting, "1.20.4 · Vanilla");
|
||||
assert_eq!(zh.instances.rows[0].supporting, "1.20.4 · 原版");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user