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:
AskaEth committed 2026-11-05 13:36:44 +08:00
1 parent 2f491c31f9
commit 1a4e1fee12
17 files changed
+924 -119

No files matched your search

+16 -14
View File
@@ -13,11 +13,13 @@ use crate::dto::{
AgentEvent, AppCommand, AppEvent, Backend, ChatMessage, ChatRole, CoreEvent, InstanceId,
InstanceRuntime, LaunchStage, Progress,
};
use crate::mock_data::{mock_instances, mock_log_lines, mock_log_sessions, mock_versions};
use crate::mock_data::{mock_log_lines, mock_log_sessions, mock_versions};
use crate::toast::{Toast, ToastKind};
#[path = "mock/accounts.rs"]
mod accounts_screen;
#[path = "mock/instances.rs"]
mod instances_screen;
/// 启动脚本:按时间推进 download → verify → launch → running。
#[derive(Debug)]
@@ -55,19 +57,19 @@ impl MockBackend {
}
fn seed(&mut self) {
self.events.push_back(AppEvent::Core(
CoreEvent::InstancesChanged(mock_instances()),
));
self.events
.push_back(AppEvent::Core(CoreEvent::VersionListChanged(
mock_versions(),
)));
self.events
.push_back(AppEvent::Core(CoreEvent::LogSessionsChanged(
mock_log_sessions(),
)));
self.events
.push_back(AppEvent::Core(CoreEvent::LogLinesChanged(mock_log_lines())));
// 实例的截图矩阵(`ULL_INSTANCE_STATE=empty|demo|running`,见 `mock/instances.rs`)。
// 缺省与 `mock_data::mock_instances()` 逐字段相同——这里的开关只给出图用。
self.emit(AppEvent::Core(CoreEvent::InstancesChanged(
instances_screen::instances(&instances_screen::scenario()),
)));
// 三条都走 `emit`(同一个队尾),与上面那条实例事件的形状一致
self.emit(AppEvent::Core(CoreEvent::VersionListChanged(
mock_versions(),
)));
self.emit(AppEvent::Core(CoreEvent::LogSessionsChanged(
mock_log_sessions(),
)));
self.emit(AppEvent::Core(CoreEvent::LogLinesChanged(mock_log_lines())));
self.seed_accounts();
}
+122
View File
@@ -0,0 +1,122 @@
//! 截图/演示用的**实例场景**(只在 mock 后端里被调用)。
//!
//! D30 要求实例页出状态矩阵:空态 / 有实例 / 运行中(带 pid)/ 启动中(带百分比)。
//! 截图通道强制 mock 后端(`AppOptions::mock`),所以这四态的**数据**必须由 mock 造:
//! 走 `ULL_INSTANCE_STATE` 选场景,与 `ULL_ACCOUNT_STATE`(M114)/ `ULL_CONTENT_STATE`(M116)
//! 同一套取证开关。
//!
//! ## 三个约束(塔台 M120 批的)
//!
//! 1. **不设变量 = 今天那 4 条**(Idle×3 + Failed×1)——[`crate::mock_data::mock_instances`]
//! 原样返回,产品/单测的行为一字未变;
//! 2. **这个开关只影响 mock 通道**:真 host 的实例来自 `core::game` 的快照,读不到这里;
//! 3. `running` 的 pid 是**写死的 mock 值**(`1234`),不是真进程 pid——它证明的是
//! 「`运行中 · pid {pid}` 这条文案的数字与分隔符不随语言变形」,不是「我能读到真 pid」。
//!
//! ```text
//! ULL_INSTANCE_STATE=empty ULL_START_ROUTE=instances ULL_LANG=zh_cn \
//! unlucky-launcher-iced --screenshot=instances-empty-zh.png --software-renderer
//! ```
use crate::dto::{InstanceRuntime, InstanceSummary, LaunchStage, Progress};
use crate::mock_data::mock_instances;
/// 挑演示场景的环境变量。
pub(crate) const STATE_ENV: &str = "ULL_INSTANCE_STATE";
/// 场景名(未知值按 `demo` 处理,与 `ULL_ACCOUNT_STATE` 同一条兜底口径)。
pub(super) fn scenario() -> String {
std::env::var(STATE_ENV)
.unwrap_or_else(|_| "demo".to_string())
.trim()
.to_lowercase()
}
/// 场景对应的实例列表。
///
/// | 场景 | 列表 | 页面看起来 |
/// |---|---|---|
/// | `empty` | 空表 | 空列表 + 「未选择实例」空态(`detail: None`) |
/// | `demo`(缺省 / 未知值) | 4 条 Idle×3 + Failed×1 | 今天的样子(M90 起的基线) |
/// | `running` | 4 条,其中 1 条 `Running{pid}`、1 条 `Busy{42%}` | 列表 overline 与详情状态行都带运行态 |
pub(super) fn instances(scenario: &str) -> Vec<InstanceSummary> {
match scenario {
"empty" => Vec::new(),
"running" => running(),
_ => mock_instances(),
}
}
/// 运行态矩阵:同一批实例换 runtime(名字/版本/加载器都还是 `mock_data` 那一份)。
///
/// 三种态各留一条,是为了**一屏看完**状态文案的三种形状:正在跑(`pid`)、启动中(百分比)、
/// 启动失败(原因)。少一条就得再出一张图,而图多一张就多一处可以撒谎的地方。
fn running() -> Vec<InstanceSummary> {
let mut list = mock_instances();
for instance in &mut list {
match instance.id.as_str() {
"survival" => instance.runtime = InstanceRuntime::Running { pid: 1234 },
"vanilla-latest" => {
instance.runtime = InstanceRuntime::Busy {
stage: LaunchStage::Download,
progress: Progress {
done: 630,
total: 1_500,
},
};
}
_ => {}
}
}
list
}
#[cfg(test)]
mod tests {
use super::*;
/// **默认行为不变**(塔台三条约束的第 1 条):不设变量就是 `mock_data` 那一份。
///
/// 抓得住:有人把 `demo` 分支改成了「顺手多造几条」——那会让所有既有截图与断言漂。
#[test]
fn the_demo_scenario_is_exactly_the_old_mock_list() {
for unknown in ["demo", "", "bogus"] {
let got = instances(unknown);
let want = mock_instances();
assert_eq!(got.len(), want.len(), "{unknown:?}");
assert_eq!(got, want, "{unknown:?} 必须与 mock_data 逐字段相同");
}
}
#[test]
fn the_empty_scenario_has_no_instances() {
assert!(instances("empty").is_empty());
}
/// 运行态矩阵:`Running` / `Busy` 各一条,且**名字与版本没被动手脚**(它们不是场景的一部分)。
#[test]
fn the_running_scenario_covers_running_and_busy() {
let list = instances("running");
assert_eq!(list.len(), mock_instances().len());
assert!(
list.iter().any(
|item| matches!(item.runtime, InstanceRuntime::Running { pid } if pid == 1234)
),
"要有一条带固定 pid 的运行中实例"
);
assert!(list.iter().any(|item| matches!(
item.runtime,
InstanceRuntime::Busy {
stage: LaunchStage::Download,
..
}
)));
assert_eq!(list[0].name, mock_instances()[0].name);
}
/// 环境变量名是**契约**(出图脚本与报告都写它,改名等于让文档说谎)。
#[test]
fn the_scenario_variable_is_named() {
assert_eq!(STATE_ENV, "ULL_INSTANCE_STATE");
}
}