feat(i18n): 外观页 40 条文案进词表(M180 I18NAPPEAR)
- 卡片内 22 处硬编码英文(整窗/面板透明度、背景图开关+路径、填充、暗化、配色来源)全部改走 Key
- 运行时诊断改成 key + 参数:`background::Note`(读盘/解码/过大/小图平铺)、`state::backend_note`、
`palette::note` 全部返回 Key;`state.rs` 只留 key、不放文案表(390 → 391 行)
- 上限/下限常量走占位符 `{floor}` / `{max}` / `{limit}` / `{requested}`;`{:.2}` 在 Rust 侧格式化好
再套模板(`Catalog::render` 只认 `{name}`)
- 新增 40 条 key(中英各一条),按装配链锚点插在 `SettingsAppearanceCornerSuspended` 之后,
金表 `golden/tail.rs` 同位置插入 40 行(`rows() == len() == Key::ALL.len()`)
- 守卫三条:中文渲染全非 ASCII、卡片 key 全覆盖(改回写死英文即红)、外观 key 逐条真翻过
- probe 加第二条取证路(`record_rendered`),`take()` 签名不动(M179 的守卫在 scope 外)
This commit is contained in:
1 parent
65e7ccc3bf
commit
c4c3c15285
14 files changed
+858
-184
No files matched your search
@@ -0,0 +1,253 @@
|
||||
//! `pages::appearance` 的单测:`Change` 的落盘映射(M93)+ **中文界面文案的取证守卫**(M180)。
|
||||
//!
|
||||
//! 为什么是独立文件:`mod.rs` 已经 220 行上下,而 M180 要在模块里加三条取证用例。搬移口径与
|
||||
//! `background/tests.rs` 一致——既有两条用例**一字未动**,只是从 `mod.rs` 的行内 `mod tests` 挪到这里。
|
||||
//!
|
||||
//! ## M180 的两条取证守卫,它们「抓得住什么」
|
||||
//!
|
||||
//! iced 0.14 没有可遍历控件树的公开接口(见 [`super::probe`] 的模块文档),所以取证点只能是
|
||||
//! 取词口 `section::t` / `section::tr`。于是这一对守卫分工如下:
|
||||
//!
|
||||
//! | 守卫 | 抓得住 | 抓不住 |
|
||||
//! |---|---|---|
|
||||
//! | [`the_appearance_card_speaks_chinese`] | 中文界面下取到的每一条文案都是纯 ASCII(= 有人把某条 key 的英文写回中文表 / 中文表里放英文原句) | 硬编码字面量(它压根不过取词口) |
|
||||
//! | [`every_appearance_card_key_is_drawn`] | 某一处控件被改回**写死英文**:那条 key 不再被取,集合里就少一个元素 | 新加一条字面量(新 key 还没有,集合不看它) |
|
||||
//!
|
||||
//! 两条合起来才等于「中文界面下外观卡片里不再出现英文字面量」——单看任一条都有一个真会发生的
|
||||
//! 漏网口,所以这里刻意写两条。
|
||||
|
||||
use super::{Change, APPEARANCE_GROUP};
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::{keys, BgFill, PaletteSource};
|
||||
|
||||
/// 抓得住:每条改动都点名到自己那个 schema 键(写错键 = 静默写到别的配置项上)。
|
||||
/// 抓不住:这些键是否真的在 core 的 schema 白名单里(那由 presenter/core 的测试守)。
|
||||
#[test]
|
||||
fn every_change_names_its_own_schema_key() {
|
||||
assert_eq!(APPEARANCE_GROUP, 1, "左列第 1 项是 Appearance");
|
||||
let cases = [
|
||||
(Change::WindowOpacity(0.8), keys::WINDOW_OPACITY),
|
||||
(Change::PanelAlpha(0.6), keys::PANEL_ALPHA),
|
||||
(Change::BgEnabled(true), keys::BG_IMAGE_ENABLED),
|
||||
(Change::BgImage("D:/x.png".to_owned()), keys::BG_IMAGE),
|
||||
(Change::BgFill(BgFill::Tile), keys::BG_FILL),
|
||||
(Change::BgDim(0.5), keys::BG_DIM),
|
||||
(
|
||||
Change::PaletteSource(PaletteSource::Wallpaper),
|
||||
keys::PALETTE_SOURCE,
|
||||
),
|
||||
(
|
||||
Change::Corner(unlucky_presenter::prefs::WindowCorner::Square),
|
||||
keys::WINDOW_CORNER,
|
||||
),
|
||||
];
|
||||
for (change, key) in cases {
|
||||
assert_eq!(change.key(), Some(key), "{change:?}");
|
||||
assert!(change.value().is_some());
|
||||
}
|
||||
assert_eq!(Change::SelectGroup(2).key(), None, "切分组不落盘");
|
||||
assert_eq!(Change::SelectGroup(2).value(), None);
|
||||
}
|
||||
|
||||
/// 抓得住:落盘字符串必须是 schema 认识的词表(枚举档写错就整条配置读回默认值)。
|
||||
/// 抓不住:数值是否在范围内(钳位在 core 侧,越界写入会被 schema 拒绝)。
|
||||
#[test]
|
||||
fn persisted_values_use_the_schema_vocabulary() {
|
||||
assert_eq!(
|
||||
Change::BgFill(BgFill::Contain).value().as_deref(),
|
||||
Some("contain")
|
||||
);
|
||||
assert_eq!(
|
||||
Change::PaletteSource(PaletteSource::BackgroundImage)
|
||||
.value()
|
||||
.as_deref(),
|
||||
Some("background-image")
|
||||
);
|
||||
assert_eq!(Change::BgEnabled(false).value().as_deref(), Some("false"));
|
||||
assert_eq!(Change::WindowOpacity(0.8).value().as_deref(), Some("0.8"));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 文案(M180 I18NAPPEAR)
|
||||
|
||||
/// 画一帧外观卡片,取走这一帧从词表取到的**全部**字:`(key, 文案)`。
|
||||
///
|
||||
/// 三档状态各画一帧:
|
||||
/// - [`Case::Factory`]:auto 档、没设背景图——覆盖常驻的标签 / 选项 / 说明;
|
||||
/// - [`Case::Cpu`]:CPU 档——覆盖「强制 100%」那一行与软件渲染说明(这两条只在降级态出现);
|
||||
/// - [`Case::Dim`]:整窗 α 调低——覆盖「圆角暂时不生效」那条 notice。
|
||||
///
|
||||
/// 两条取词路径都要收:`t()` 的 `&'static str` 与 `tr()` 替换完占位符的整句(后者不进前者的记录)。
|
||||
fn appearance_texts(lang: Lang, case: Case) -> Vec<(Key, String)> {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||
|
||||
use super::probe;
|
||||
use crate::cli::Backend;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
let (backend, alpha) = match case {
|
||||
Case::Factory => (Backend::Auto, 1.0),
|
||||
Case::Cpu => (Backend::Cpu, 1.0),
|
||||
Case::Dim => (Backend::Gpu, 0.5),
|
||||
};
|
||||
let mut state = super::State::new(
|
||||
&AppearancePrefs::factory(),
|
||||
&AppearanceBase::factory(),
|
||||
backend,
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
if alpha < 1.0 {
|
||||
state.apply(Change::WindowOpacity(alpha));
|
||||
}
|
||||
|
||||
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||
// 前面几条用例可能留了记录:本线程的取证每次都从清空开始。
|
||||
probe::take();
|
||||
probe::take_rendered();
|
||||
|
||||
let _tree = super::section(&ui, &state, lang);
|
||||
|
||||
let mut texts: Vec<(Key, String)> = probe::take()
|
||||
.into_iter()
|
||||
.map(|(key, text)| (key, text.to_owned()))
|
||||
.collect();
|
||||
texts.extend(probe::take_rendered());
|
||||
texts
|
||||
}
|
||||
|
||||
/// 画哪一档状态。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Case {
|
||||
/// 工厂态(GPU / auto)。
|
||||
Factory,
|
||||
/// CPU 降级态(面板 α 与玻璃被强制)。
|
||||
Cpu,
|
||||
/// 整窗 α 调低(圆角暂时不生效)。
|
||||
Dim,
|
||||
}
|
||||
|
||||
/// 这一页**常驻**的 key(三档状态合起来必须一个不缺地取到)。
|
||||
///
|
||||
/// 为什么是手写清单而不是「`Key::ALL` 里所有 `SettingsAppearance*`」:诊断一族(读盘失败 / 配色回落 /
|
||||
/// 小图平铺)取决于运行时状态,单测里要让它们出现得造磁盘上的坏图;那些由
|
||||
/// `background/tests.rs` 与 `palette.rs` 的单测按 key 逐条钉(见各自的用例注释)。
|
||||
const CARD_KEYS: &[Key] = &[
|
||||
Key::SettingsAppearanceCorner,
|
||||
Key::SettingsAppearanceCornerSystem,
|
||||
Key::SettingsAppearanceCornerSquare,
|
||||
Key::SettingsAppearanceCornerHint,
|
||||
Key::SettingsAppearanceCornerSuspended,
|
||||
Key::SettingsAppearanceWindowOpacity,
|
||||
Key::SettingsAppearanceWindowOpacityHint,
|
||||
Key::SettingsAppearancePanelOpacity,
|
||||
Key::SettingsAppearancePanelOpacityHint,
|
||||
Key::SettingsAppearancePanelOpacityForcedValue,
|
||||
Key::SettingsAppearancePanelOpacityForcedHint,
|
||||
Key::SettingsAppearanceSoftwareRendererNote,
|
||||
Key::SettingsAppearanceBackgroundImage,
|
||||
Key::SettingsAppearanceOff,
|
||||
Key::SettingsAppearanceOn,
|
||||
Key::SettingsAppearanceImagePath,
|
||||
Key::SettingsAppearancePaletteLevel,
|
||||
Key::SettingsAppearanceBgFill,
|
||||
Key::SettingsAppearanceBgFillCover,
|
||||
Key::SettingsAppearanceBgFillContain,
|
||||
Key::SettingsAppearanceBgFillTile,
|
||||
Key::SettingsAppearanceBgDim,
|
||||
Key::SettingsAppearanceBgDimHint,
|
||||
Key::SettingsAppearancePaletteSource,
|
||||
Key::SettingsAppearancePaletteBackground,
|
||||
Key::SettingsAppearancePaletteWallpaper,
|
||||
Key::SettingsAppearancePalettePreset,
|
||||
];
|
||||
|
||||
/// **M180 守门 ①**:中文界面下,外观卡片里取到的每一条文案都不是纯 ASCII。
|
||||
///
|
||||
/// 抓得住:某条 key 的中文译文被换成英文原句(中文表里出现纯 ASCII 的一句);中文译文写成
|
||||
/// `PNG` / `Preset` 这种专名;`tr()` 那条路径把数字拼进英文模板(比如 `Colours: preset`)。
|
||||
/// 抓不住:硬编码字面量——它不过取词口,由 [`every_appearance_card_key_is_drawn`] 与
|
||||
/// `catalog/settings/appearance.rs` 的译文守卫合力守。
|
||||
///
|
||||
/// 变异证明:把 `SettingsAppearanceOff` 的中文改成 `"Off"` ⇒ 本条立刻红。
|
||||
#[test]
|
||||
fn the_appearance_card_speaks_chinese() {
|
||||
// 白名单:技术专名允许是纯 ASCII(现在一条都没有——`%` / 数字不在文案里,它们是参数)。
|
||||
const ASCII_OK: &[&str] = &[];
|
||||
|
||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||
let pairs = appearance_texts(Lang::ZhCn, case);
|
||||
assert!(
|
||||
pairs.len() >= 20,
|
||||
"{case:?}:取词口只记到 {} 条文案,渲染路径没走到底(语言管子断了?)",
|
||||
pairs.len()
|
||||
);
|
||||
for (key, text) in &pairs {
|
||||
if ASCII_OK.contains(&text.as_str()) {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
!text.is_ascii(),
|
||||
"{case:?}:中文界面下外观卡片里出现了纯 ASCII 的文案「{text}」({})\
|
||||
——写死英文,或中文表里放了英文原句",
|
||||
key.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **M180 守门 ②(漏项)**:这一页每一处控件都必须从词表取词。
|
||||
///
|
||||
/// 抓得住:把某处 `t(Key::X, lang)` / `tr(lang, Key::X, …)` 改回写死的英文字面量——那条 key 不再
|
||||
/// 出现在取词记录里,集合比较当场红(这正是 M172 口径「22 处硬编码」的复发形态)。
|
||||
/// 抓不住:新加一条写死英文(它没有 key,也就没有「少了哪条」)。
|
||||
///
|
||||
/// 变异证明:把 `bg_image.rs` 里 `t(Key::SettingsAppearanceOff, lang)` 换回 `"Off"` ⇒ 本条红。
|
||||
#[test]
|
||||
fn every_appearance_card_key_is_drawn() {
|
||||
let mut seen: Vec<Key> = Vec::new();
|
||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||
seen.extend(
|
||||
appearance_texts(Lang::ZhCn, case)
|
||||
.into_iter()
|
||||
.map(|(key, _)| key),
|
||||
);
|
||||
}
|
||||
|
||||
let missing: Vec<&str> = CARD_KEYS
|
||||
.iter()
|
||||
.filter(|key| !seen.contains(key))
|
||||
.map(|key| key.name())
|
||||
.collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"这些 key 一帧都没取到(控件被改回写死英文,或整段没画):{missing:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 英文界面下不许出现汉字(否则「含汉字」可能来自写死的中文,而不是词表)。
|
||||
#[test]
|
||||
fn the_appearance_card_keeps_its_english_baseline() {
|
||||
for case in [Case::Factory, Case::Cpu, Case::Dim] {
|
||||
let texts = appearance_texts(Lang::En, case);
|
||||
let with_han: Vec<&(Key, String)> = texts
|
||||
.iter()
|
||||
.filter(|(_, text)| text.chars().any(is_han))
|
||||
.collect();
|
||||
assert!(
|
||||
with_han.is_empty(),
|
||||
"{case:?}:英文界面下外观卡片出现了汉字:{with_han:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 有没有汉字(与 `pages/settings/tests/appearance_section.rs` 的 `is_han` 同一段码位)。
|
||||
fn is_han(ch: char) -> bool {
|
||||
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||
}
|
||||
Reference in new issue
Block a user