复审 p1:`detect_env()` 在 Windows 上恒为 `en`(`LC_ALL/LC_MESSAGES/LANG` 都不存在), D28 整条中文链(探测 → 扫 CJK → 需下载)在基线平台永不触发。复审在本机(zh-CN)实测 `lang=en raw=None`,这条我认。 **p1 修法(tower 定路线:子进程读注册表,不碰 FFI、不加依赖)** - 新增 `crates/app-iced/src/platform/locale.rs`:`SystemLocale` 实现 `unlucky_i18n::lang::LocaleSource`,内部 `reg.exe query "HKCU\Control Panel\International" /v LocaleName`(本机实测 `REG_SZ zh-CN`),读不到时退 `/v Locale` 的十六进制 LCID (本机 `00000804`),用一张 20 条的短表翻成标签,表外语言返回 `None`(宁可英文,不猜语种)。 先例同 M73 壁纸探测(`core::platform::wallpaper` 的 `reg.exe` 路线)。 - 子进程走注入的 `RegIo` 接口,单测用 `FakeReg`(不 spawn);夹具是**本机真机抓的原文** (含 CRLF / 前导空白),另有一条 `#[cfg(windows)]` 真机测试证明 `reg.exe` 这条路真的通。 - i18n 仍是纯函数:`lang::resolve(setting, platform, env)` 是四级优先级的唯一实现—— **`ui.language` 显式值 > 平台源 > POSIX 环境变量 > 英文**,`Detection` 新增 `origin` (`setting` / 源自报的 `label()` / `env` / `default`),启动日志因此能写清 `source=registry`。 配置值不认识时**跳过**继续往下走,不当成任何一种语言。 - `fonts::cjk::probe_with(setting, faces)` 把 `ui.language` 那一级的位置先留好(目前传 `None`: 设置层还没有读取点,写在模块文档的诚实边界里);`probe()` 走全链。 - `platform::mod` 的模块表补一行说明 locale 也是「零 unsafe」那一类。 **本机(zh-CN)实跑证据**(`--software-renderer --screenshot …`,exit 0): `[font] … | lang=zh-CN locale=zh-CN source=registry cjk=system:"Microsoft YaHei UI"` ——探测出中文、且走的是「扫 CJK 字体」而不是 `not-needed`。 **p2**:删掉 `catalog` 那条 `compile_fail` 守卫(复审两个变异仍全绿 ⇒ 它守的是「这段代码 编译不过」,不是「编译期校验还在」)。改为陈述事实:「`Key` 是枚举,拼错 `E0599`」, 并写明这里**没有**守卫测试及原因。 未动 `ui.language` 默认值(`core` schema 不在本 mission scope,tower 已另记)。
188 lines
7.5 KiB
Rust
188 lines
7.5 KiB
Rust
//! 中文环境的 CJK 字体探测(D28 后半):把 iced 系统字体库里的族名喂给 `unlucky_i18n::font::decide`。
|
||
//!
|
||
//! ## 分工
|
||
//!
|
||
//! - **判据**(要不要中文字体、哪些族算候选、缺了要不要下载)全在 `unlucky_i18n::font`,是纯函数,
|
||
//! 零 UI 依赖、脱机可测;
|
||
//! - **语言**从 [`unlucky_i18n::lang::resolve`] 拿:`ui.language` 显式值 > [`SystemLocale`]
|
||
//! (Windows 读注册表,零 FFI)> 环境变量 > 英文;
|
||
//! - **本模块只做适配**:把 [`super::Face`] 列表变成一个「某族装没装」的查询,并处理
|
||
//! **读不到字体库**这种情况。
|
||
//!
|
||
//! ## 为什么「读不到」不等于「需下载」
|
||
//!
|
||
//! `system_faces()` 拿不到字体库时返回 `None`。把它当成「没有中文字体」会直接触发一次 CDN 下载
|
||
//! (D28 的自动下载路径),而真相是**我们不知道**。所以这一路走 [`Strategy::Unprobeable`]:
|
||
//! 日志里留痕,不下载、不弹提示。
|
||
//!
|
||
//! ## 诚实边界(抓得住什么 / 抓不住什么)
|
||
//!
|
||
//! - **抓得住**:语言 + 字体库族名 → 策略(含 `Unprobeable` 这一支);优先级顺序(注入的假源可测);
|
||
//! - **抓不住**:①族名存在 ≠ 字形覆盖(`SimSun` 也可能是个残缺子集)。真判据要开 `ttf-parser`
|
||
//! 查 cmap,本 mission 不引依赖(M60 Q3 spike 做过,结论见 `DevDocs`);
|
||
//! ②`ui.language` 这一级目前**没有接线**(设置层还没有读取点),调用点传 `None`。
|
||
|
||
use unlucky_i18n::font::{self, Strategy};
|
||
use unlucky_i18n::lang::{self, Detection, EnvLocale};
|
||
|
||
use super::Face;
|
||
use crate::platform::locale::SystemLocale;
|
||
|
||
/// 一次中文探测的结论(启动日志的第二段)。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct CjkReport {
|
||
/// 语言探测(含原始 locale 与来源,便于解释「为什么是英文」)。
|
||
pub detection: Detection,
|
||
/// 字体策略结论。
|
||
pub strategy: Strategy,
|
||
}
|
||
|
||
impl CjkReport {
|
||
/// 一行日志片段(`lang=zh-CN locale=zh-CN source=registry cjk=system:"Microsoft YaHei"`)。
|
||
pub fn describe(&self) -> String {
|
||
format!("{} {}", self.detection.describe(), self.strategy.describe())
|
||
}
|
||
}
|
||
|
||
/// 用给定语言 + 字体库内容做判定。
|
||
///
|
||
/// `faces` 为 `None` = 字体库读不出来(→ [`Strategy::Unprobeable`],**不是**「需下载」)。
|
||
pub fn report(detection: Detection, faces: Option<&[Face]>) -> CjkReport {
|
||
let strategy = faces.map_or(Strategy::Unprobeable, |faces| {
|
||
let is_installed = |family: &str| faces.iter().any(|face| face.family == family);
|
||
font::decide(detection.lang, &is_installed)
|
||
});
|
||
|
||
CjkReport {
|
||
detection,
|
||
strategy,
|
||
}
|
||
}
|
||
|
||
/// 探测本机:优先级见 [`unlucky_i18n::lang::resolve`]。
|
||
///
|
||
/// `setting` 是配置里 `ui.language` 的原文(`zh_cn` / `en_us`);`None` = **还没读配置**
|
||
/// (设置层的读取点尚未接线,见模块文档的诚实边界)。注意 `None` 与 `Some("不认识的值")`
|
||
/// 效果相同:都不当显式选择,继续往下走。
|
||
pub fn probe_with(setting: Option<&str>, faces: Option<&[Face]>) -> CjkReport {
|
||
report(lang::resolve(setting, &SystemLocale, &EnvLocale), faces)
|
||
}
|
||
|
||
/// 不读配置时的入口(启动路径用)。
|
||
pub fn probe(faces: Option<&[Face]>) -> CjkReport {
|
||
probe_with(None, faces)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn faces(families: &[&str]) -> Vec<Face> {
|
||
families
|
||
.iter()
|
||
.map(|family| Face {
|
||
family: (*family).to_owned(),
|
||
weight: 400,
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn zh() -> Detection {
|
||
Detection {
|
||
lang: unlucky_i18n::Lang::ZhCn,
|
||
raw: Some("zh-CN".to_owned()),
|
||
origin: "injected",
|
||
}
|
||
}
|
||
|
||
fn en() -> Detection {
|
||
Detection {
|
||
lang: unlucky_i18n::Lang::En,
|
||
raw: Some("en-US".to_owned()),
|
||
origin: "injected",
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn chinese_host_with_a_cjk_font_uses_the_system_one() {
|
||
// 抓得住:适配层把字体库查反/查漏(例如只按第一个族判断、或把 `Segoe UI` 当候选)。
|
||
// 抓不住:这台机器实测有没有这个族(测试用的是注入的假字面表)。
|
||
let installed = faces(&["Segoe UI", "Microsoft YaHei", "Cascadia Mono"]);
|
||
let report = report(zh(), Some(&installed));
|
||
assert_eq!(
|
||
report.strategy,
|
||
Strategy::System {
|
||
family: "Microsoft YaHei"
|
||
}
|
||
);
|
||
assert_eq!(
|
||
report.describe(),
|
||
"lang=zh-CN locale=zh-CN source=injected cjk=system:\"Microsoft YaHei\""
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn chinese_host_without_cjk_fonts_gets_the_download_signal() {
|
||
// 抓得住:D28 的「中文环境扫不到 → 要下载」这条路(信号必须能被日志看出来)。
|
||
// 抓不住:信号之后的下载与提示(本 mission 不联网,页面也没接)。
|
||
let latin_only = faces(&["Segoe UI", "Roboto"]);
|
||
let report = report(zh(), Some(&latin_only));
|
||
assert_eq!(
|
||
report.strategy,
|
||
Strategy::NeedsDownload(&unlucky_i18n::font::NOTO_SANS_SC)
|
||
);
|
||
assert!(report
|
||
.describe()
|
||
.contains("cjk=needs-download(Noto Sans SC)"));
|
||
}
|
||
|
||
#[test]
|
||
fn english_host_never_asks_for_a_chinese_font() {
|
||
// 抓得住:英文环境的「零字体负担」(D28)——系统里有没有中文字体都不影响结论。
|
||
// 抓不住:程序真的没去读那些字体文件(那是 iced/cosmic-text 的加载行为)。
|
||
assert_eq!(report(en(), Some(&[])).strategy, Strategy::NotNeeded);
|
||
assert_eq!(
|
||
report(en(), Some(&faces(&["Microsoft YaHei"]))).strategy,
|
||
Strategy::NotNeeded
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn unreadable_font_database_is_unknown_not_missing() {
|
||
// 抓得住:把「读不到字体库」错判成「缺字体」——那会在探测失败时触发一次多余的 CDN 下载。
|
||
// 抓不住:真的读不到字体库时的运行行为(那条路径要 poisoned lock,测试造不出来)。
|
||
let report = report(zh(), None);
|
||
assert_eq!(report.strategy, Strategy::Unprobeable);
|
||
assert!(
|
||
report.describe().ends_with("cjk=unprobeable"),
|
||
"{}",
|
||
report.describe()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn explicit_setting_overrides_the_platform_source() {
|
||
// 抓得住:`ui.language` 显式值被平台源盖掉(用户手选中文/英文必须赢过系统语言);
|
||
// 也让「平台源真的接在链路上」有个不依赖本机语言的证据。
|
||
// 抓不住:`ui.language` 在真机上读到什么(设置层还没接线,调用点传 None)。
|
||
let installed = faces(&["Microsoft YaHei"]);
|
||
|
||
let forced_english = probe_with(Some("en_us"), Some(&installed));
|
||
assert_eq!(forced_english.detection.lang, unlucky_i18n::Lang::En);
|
||
assert_eq!(
|
||
forced_english.detection.origin,
|
||
unlucky_i18n::lang::ORIGIN_SETTING
|
||
);
|
||
assert_eq!(forced_english.strategy, Strategy::NotNeeded);
|
||
|
||
let forced_chinese = probe_with(Some("zh_cn"), Some(&installed));
|
||
assert_eq!(
|
||
forced_chinese.strategy,
|
||
Strategy::System {
|
||
family: "Microsoft YaHei"
|
||
}
|
||
);
|
||
assert!(forced_chinese.describe().contains("source=setting"));
|
||
}
|
||
}
|