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 已另记)。
This commit is contained in:
AskaEth committed 2026-11-02 16:58:09 +08:00
1 parent e77a5df6e8
commit ce2c42032b
8 files changed
+513 -39

No files matched your search

+51 -11
View File
@@ -4,6 +4,8 @@
//!
//! - **判据**(要不要中文字体、哪些族算候选、缺了要不要下载)全在 `unlucky_i18n::font`,是纯函数,
//! 零 UI 依赖、脱机可测;
//! - **语言**从 [`unlucky_i18n::lang::resolve`] 拿:`ui.language` 显式值 > [`SystemLocale`]
//! (Windows 读注册表,零 FFI)> 环境变量 > 英文;
//! - **本模块只做适配**:把 [`super::Face`] 列表变成一个「某族装没装」的查询,并处理
//! **读不到字体库**这种情况。
//!
@@ -15,26 +17,28 @@
//!
//! ## 诚实边界(抓得住什么 / 抓不住什么)
//!
//! - **抓得住**:语言 + 字体库族名 → 策略(含 `Unprobeable` 这一支);
//! - **抓不住**:族名存在 ≠ 字形覆盖(`SimSun` 也可能是个残缺子集)。真判据要开 `ttf-parser` 查 cmap,
//! 本 mission 不引依赖(M60 Q3 spike 做过,结论见 `DevDocs`)。
//! - **抓得住**:语言 + 字体库族名 → 策略(含 `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};
use unlucky_i18n::lang::{self, Detection, EnvLocale};
use super::Face;
use crate::platform::locale::SystemLocale;
/// 一次中文探测的结论(启动日志的第二段)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CjkReport {
/// 语言探测(含原始 locale,便于解释「为什么是英文」)。
/// 语言探测(含原始 locale 与来源,便于解释「为什么是英文」)。
pub detection: Detection,
/// 字体策略结论。
pub strategy: Strategy,
}
impl CjkReport {
/// 一行日志片段(`lang=zh-CN locale=zh_CN.UTF-8 cjk=system:"Microsoft YaHei"`)。
/// 一行日志片段(`lang=zh-CN locale=zh-CN source=registry cjk=system:"Microsoft YaHei"`)。
pub fn describe(&self) -> String {
format!("{} {}", self.detection.describe(), self.strategy.describe())
}
@@ -55,9 +59,18 @@ pub fn report(detection: Detection, faces: Option<&[Face]>) -> CjkReport {
}
}
/// 探测本机(locale 走环境变量;换 Win32 真值源是后续 mission 的注入点)。
/// 探测本机:优先级见 [`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 {
report(lang::detect_env(), faces)
probe_with(None, faces)
}
#[cfg(test)]
@@ -77,14 +90,16 @@ mod tests {
fn zh() -> Detection {
Detection {
lang: unlucky_i18n::Lang::ZhCn,
raw: Some("zh_CN.UTF-8".to_owned()),
raw: Some("zh-CN".to_owned()),
origin: "injected",
}
}
fn en() -> Detection {
Detection {
lang: unlucky_i18n::Lang::En,
raw: Some("en_US.UTF-8".to_owned()),
raw: Some("en-US".to_owned()),
origin: "injected",
}
}
@@ -102,7 +117,7 @@ mod tests {
);
assert_eq!(
report.describe(),
"lang=zh-CN locale=zh_CN.UTF-8 cjk=system:\"Microsoft YaHei\""
"lang=zh-CN locale=zh-CN source=injected cjk=system:\"Microsoft YaHei\""
);
}
@@ -144,4 +159,29 @@ mod tests {
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"));
}
}
+1
View File
@@ -146,6 +146,7 @@ fn describe_is_one_line_and_mentions_all_three_stacks() {
detection: Detection {
lang: unlucky_i18n::Lang::En,
raw: None,
origin: unlucky_i18n::lang::ORIGIN_DEFAULT,
},
strategy: Strategy::NotNeeded,
},