Files
Unlucky-Launcher/crates/app-iced/src/fonts/cjk.rs
T
AskaEth ce2c42032b fix(i18n): M92 r1 —— Windows 真 locale 走注册表子进程(零 FFI)+ 删掉假守卫
复审 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 已另记)。
2026-11-02 16:58:09 +08:00

188 lines
7.5 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.
//! 中文环境的 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"));
}
}