Files
Unlucky-Launcher/crates/app-iced/src/pages/account/tests_text.rs
T
AskaEth af39efd23b fix(presenter,app-iced,i18n): M132 缺 MSA client id 上屏成「未配置 + 怎么补」
缺 client id 原来整条链路只给 `LoginFailure::Other`:界面画「Sign-in failed /
完整原因在启动器日志里 / Code: other」,用户得翻日志才知道要设 `ULL_MSA_CLIENT_ID`。

落点定在**账户页登录面板**,不是启动流程:这个失败只发生在 `microsoft_auth()`(两条
登录流的入口);没配 id 的发布版建不出微软账户,`identity::resolve` 那条启动路径用户
撞不到(只有「有 id 的构建里登录过、之后 id 被摘掉」才可达)。

- presenter:新增 `LoginFailure::MsaNotConfigured`(`msa_not_configured`)。`microsoft_auth`
  改为回 `AuthResult`,`AuthError::Unsupported`(`from_env` 唯一的产出点,零网络)单独成码,
  其余失败照旧走 `classify`。诊断原文仍只进日志。
- i18n:账户域新增 `AccountLoginNotConfigured`(标题)/ `AccountFailureMsaNotConfigured`
  (正文:设 `ULL_MSA_CLIENT_ID=<id>` 或构建时注入,改完重启),golden 同步。
- app-iced:`failure_title_key` 给这一类专属标题;正文按码查表;这一类**不给「重试」**
  (进程内的环境变量改不了,重试必然是同一个错)。
- 测试:`a_missing_client_id_tells_the_user_what_to_configure` 断言 UI 模型里出现「未配置」
  + 变量名 + `<id>` 占位 + `msa_not_configured`,且没有那句「去翻日志」;反向锚
  `a_network_failure_does_not_look_like_a_missing_config` 与
  `only_a_missing_client_id_becomes_the_config_gap_code` 钉住别的失败不许跟着变。

为什么这一类可以上屏:上屏的是 catalog 固定文案(无凭据、无 token、无内部细节),
`DevicePanel::Failed` 里没有 `message`/`detail` 字段,原文在类型上就画不出来;
理由是它的原因本身就是用户的下一步。M117/M119 的「诊断不上屏」约定不动。
2026-11-05 20:08:52 +08:00

99 lines
4.6 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.
//! 「**模型这一层会给出什么**」的取样器:视图模型字段 + **按码做的 i18n 查表**,拼成一串。
//!
//! ## 边界(复审 r2 P2-2 订正,别把这条读成「视图覆盖」)
//!
//! 这是**平行实现**:它自己调 `catalog` 查表,**不调用** `login::panel` / `forms::status_line`,
//! 也不遍历 iced 的元素树。所以它证明的是「模型 + i18n 表里没有中文」,**不证明**「视图真的照它画」。
//!
//! 视图层靠两道别的闸:
//! 1. **类型**:`DevicePanel` / `AuthlibPanel` 里**没有** `message`/`detail` 字段(r3 删掉了),
//! 想画后端原文是编译错误,不是测试问题;
//! 2. **出图**:`DevDocs/artifacts/m114/after/*` 由人(tower/复审)逐张看。
//!
//! 为什么单独一个文件:`pages/account/tests.rs` 贴着 400 行硬线,而这段是**纯取样**
//! (不给任何断言)——断言都在 `tests.rs` 里。
use unlucky_i18n::{Key, Lang};
use unlucky_presenter::dto::AccountNoticeCode;
use super::login::{failure_body_key, failure_title_key};
use super::model::{AuthlibPanel, DevicePanel, Hint, Page};
/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。
pub(super) fn on_screen(page: &Page) -> String {
let catalog = Lang::En.catalog();
let mut out = String::new();
for row in &page.rows {
out.push_str(&row.name);
out.push_str(&row.monogram);
out.push_str(&row.kind_label);
out.push_str(&row.added);
out.push_str(&row.detail);
}
out.push_str(&page.form.offline_name);
out.push_str(&page.form.server);
if let Some(notice) = &page.notice {
// 与 `widgets::banner` 同源:码 → i18n(主题名拼进去)
out.push_str(match notice.code {
AccountNoticeCode::OfflineNameInvalid => {
catalog.text(Key::AccountNoticeOfflineNameInvalid)
}
AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists),
AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed),
AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound),
AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther),
AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded),
AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved),
AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched),
});
out.push_str(&notice.subject);
}
if let Some(hint) = page.hint {
out.push_str(match hint {
Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth),
Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin),
});
}
match &page.device {
DevicePanel::Hidden => {}
DevicePanel::Requesting => out.push_str(catalog.text(Key::AccountDeviceRequesting)),
DevicePanel::Code {
code,
link,
remaining,
expired,
} => {
out.push_str(code);
out.push_str(link);
if *expired {
out.push_str(catalog.text(Key::AccountDeviceExpired));
} else if let Some(secs) = remaining {
let clock = format!("{:02}:{:02}", secs / 60, secs % 60);
out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)]));
}
}
DevicePanel::Browser { link, .. } => out.push_str(link),
DevicePanel::Failed { failure } => {
out.push_str(catalog.text(failure_title_key(*failure)));
out.push_str(catalog.text(failure_body_key(*failure)));
out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())]));
}
DevicePanel::Succeeded { name } => {
out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)]));
}
}
match &page.authlib {
AuthlibPanel::Idle => {}
AuthlibPanel::Running => out.push_str(catalog.text(Key::AccountServerChecking)),
AuthlibPanel::Ready { server_name } => out.push_str(server_name),
AuthlibPanel::Failed { failure } => out.push_str(failure.code()),
AuthlibPanel::Succeeded { name } => out.push_str(name),
}
out
}
/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。
pub(super) fn has_cjk(text: &str) -> bool {
text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}'))
}