fix(app-iced): M106 r2 —— 两处「改了行为、旧口径还在树里」(复审 p2-1 / p2-2)

p2-1(它在撒谎):`renderer/acceptance/locale.rs::Chain::line()` 还打 `ui_text=pinned-en`,
注释写着「pages/mod.rs::render 里下载页固定传 Lang::En」、`setting` 回退 `<未接线>`——M106 之后
两句同时为假,于是 `ULL_ACCEPTANCE=1` 时启动日志两行互相矛盾(旧行说「文案钉在英文」,新行说
「生效中文」)。修法:`Chain` 增 `machine`(只看平台源 + 环境变量,供 `[env] language=` 用)与
`detection`(含 `ui.language` 手选,与 App 的 `[i18n] choice=…` 同源);`host()` 真读
`SettingsBridge::load_ui_language()`,`line()` 删掉 `ui_text`、`setting` 打真实值。
实测三行现在一致:
  [i18n] setting=<未设置=跟随系统> platform=registry:zh-CN env=env:<未设置> -> lang=zh-CN locale=zh-CN source=registry
  [i18n] choice=system lang=zh-CN locale=zh-CN source=registry effective=zh-CN
`locale_tests.rs` 把钉假串的那条改成「不许出现 pinned-en」的回归绊线,并新增
`i18n_line_honours_the_explicit_setting_without_rewriting_the_machine_language`。

p2-2(doc-only):`pages/model.rs` 的「只是留口,产品路径先给 Lang::En」与 `pages/download.rs`
的「产品路径现传 Lang::En,接上探测结果时只改调用点」都已被实际代码推翻(`app/state.rs` +
`refresh_view` 在填、调用点已是 `model.lang`),按事实重写。顺带修掉 `fonts/cjk.rs` 里同样的
「设置层的读取点尚未接线」。

门禁:`cargo fmt --all -- --check` 退出码 0;`cargo test -p unlucky-app-iced --lib` 退出码 0
(409 passed, 0 failed)。全改动文件 ≤400 行(最大 269)。
This commit is contained in:
AskaEth committed 2026-11-05 01:17:36 +08:00
1 parent 89dcb6bdf3
commit 954a129e01
14 files changed
+286 -107

No files matched your search

@@ -7,6 +7,7 @@
//! [`AppleLanguages`] 先落在这里。**它该搬去 `platform/locale.rs`**——两处并存会漂(见交接报告)。
use unlucky_i18n::lang::{self, Detection, LocaleSource};
use unlucky_presenter::settings_bridge::SettingsBridge;
use crate::Probe;
@@ -83,21 +84,43 @@ pub fn tz() -> Option<String> {
/// 到底是「用户就是英文」还是「一个源都没取到」根本分不出来。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Chain {
/// `ui.language` 的显式值(当前没有读取点,恒 `None`)。
/// `ULL_LANG` 的显式覆盖(截图矩阵的注入值);`None` = 没设。
///
/// 它与 [`Chain::setting`] 是**两个不同的源**:一个是我们为了出图钉的值,一个是用户配置里存的。
/// 判定顺序里前者压后者(见 [`Chain::read`]),日志里分开打,读的人不用猜。
pub forced: Option<String>,
/// `ui.language` 的显式值(`None` = 文件里**没写过** = 跟随系统)。
///
/// 与 `App` 启动判定同一个读法(`SettingsBridge::load_ui_language`:只认显式值,不走 schema
/// 默认值),所以「没写过」在这里与 `ui::lang` 里是同一个意思。
pub setting: Option<String>,
/// (源自称的名字, 原始 locale)。
pub platform: (&'static str, Option<String>),
/// (源自称的名字, 原始 locale)。
pub env: (&'static str, Option<String>),
/// 结论(由 [`lang::resolve`] 算,不在本模块重写一遍优先级)。
/// 只按**平台源 + 环境变量**算出来的「这台机器说什么语言」(**不看** `ui.language`)。
///
/// `[env] language=` 用的是它:那一行回答的是「本机是什么」,用户手选英文不该改写机器的事实。
pub machine: Detection,
/// 最终结论:**与 `App` 启动日志的 `[i18n] choice=…` 用同一个判据函数**
/// (`ui::lang::choose_choice` + `LangChoice::resolve`),所以两行不可能说的是两种语言。
pub detection: Detection,
/// 这一次是不是**截图通道**(D30):`true` 时配置那一级按设计被忽略,日志里打出来,
/// 否则「配置写了英文、结论是中文」看起来像 bug。
pub screenshot: bool,
}
impl Chain {
/// 用三个源探测一次。**每个源只读一次**(`defaults` / `reg.exe` 都是子进程)。
/// 用全部四个源探测一次。**每个源只读一次**(`defaults` / `reg.exe` 都是子进程)。
///
/// 优先级**不在这里重写**:档位由 [`crate::ui::lang::choose_choice`] 出(与 `App` 启动判定
/// 同一个函数),结论由 `LangChoice::resolve` 出(与 `App` 同一个 `resolve`)。
/// 顺序即 **`ULL_LANG` > 配置(`screenshot == false` 才算) > 平台源 > 环境变量 > 英文**。
#[must_use]
pub fn read(
forced: Option<&str>,
setting: Option<&str>,
screenshot: bool,
platform: &dyn LocaleSource,
env: &dyn LocaleSource,
) -> Self {
@@ -109,45 +132,70 @@ impl Chain {
label: env.label(),
raw: env.locale(),
};
// 优先级仍由 `resolve` 单点决定:这里把**已读到的值**包成同标签的源喂回去,
// 不复制一遍 if/else(复制就会漂)。
let detection = lang::resolve(setting, &platform, &env);
// 「机器说什么语言」:与 `ui.language` 无关,`[env] language=` 用它。
let machine = lang::resolve(None, &platform, &env);
// 「这一次界面用什么语言」:**调产品路径那个函数**,不在这里复制一遍优先级表。
let detection =
crate::ui::lang::choose_choice(forced, setting, screenshot).resolve(&platform, &env);
Self {
forced: forced.map(str::to_owned),
setting: setting.map(str::to_owned),
platform: (platform.label, platform.raw),
env: (env.label, env.raw),
machine,
detection,
screenshot,
}
}
/// 本机(平台源 + 环境变量;配置未接线)。
/// 本机链路(`ULL_LANG` + 配置 + 平台源 + 环境变量)。
///
/// `screenshot` 由调用方从 `Options` 递进来(`report_boot` 手上有):**截图通道按 D30 忽略配置**,
/// 这一级判断必须与产品路径同源,否则又会出现「一行说 zh-CN、下一行说 effective=en」。
///
/// 诚实边界:这里 `SettingsBridge::open()` 走**默认数据根**;同一次运行若带了 `--data-dir`,
/// 配置可能来自另一份文件(此时以 `App` 那行 `[i18n] choice=…` 为准,它用的是 App 自己的桥)。
#[must_use]
pub fn host() -> Self {
Self::read(None, platform_locale_source(), &lang::EnvLocale)
pub fn host(screenshot: bool) -> Self {
let forced = std::env::var(crate::ui::lang::LANG_ENV)
.ok()
.filter(|value| !value.trim().is_empty());
let setting = SettingsBridge::open().load_ui_language();
Self::read(
forced.as_deref(),
setting.as_deref(),
screenshot,
platform_locale_source(),
&lang::EnvLocale,
)
}
/// 结论语言标签(`zh-CN` / `en`)。
/// **机器的**语言标签(`zh-CN` / `en`)——不含 `ui.language` 的手选。
#[must_use]
pub fn language(&self) -> &'static str {
self.detection.lang.tag()
self.machine.lang.tag()
}
/// `[i18n]` 一行:四级链 + 结论。
/// `[i18n]` 一行:四个源 + 结论。
///
/// `ui_text=pinned-en` 是**今天的事实**:`pages/mod.rs::render` 里下载页固定传 `Lang::En`、
/// 日志页传 `model.lang`(默认也是 `En`),也就是**文案还没接这条结论**。不写这一格的话,
/// 日志会让人以为「语言已经是中文了」——那是新的假绿。
/// 顺序即 `ULL_LANG` > 配置(`shot=false` 才参与) > 平台源 > 环境变量 > 英文;`->` 后面那一段
/// 就是**最终生效语言 + 它来自哪一级**,与 `App` 的 `[i18n] choice=…` **用同一个判据函数**算出来。
///
/// 为什么把 `forced=` 与 `shot=` 也打出来:`shot=true` 时配置按 D30 被忽略,不写这一格的话
/// 「配置里明明写了 en_us,结论却是 zh-CN」看起来像 bug(r4 复审 p2-1 的两组复现就是这两种)。
///
/// 曾经这里钉着 `ui_text=pinned-en`(那会儿 `pages/mod.rs::render` 真的把下载页写死成英文)。
#[must_use]
pub fn line(&self) -> String {
format!(
"[i18n] setting={} platform={}:{} env={}:{} -> {} ui_text=pinned-en",
// `<未接线>` 而不是 `<未设置>`:配置侧的读取点**还不存在**(M98 r1 p3-3)。
// 写「未设置」会让读的人以为用户没配过;真实情况是我们没读。
self.setting.as_deref().unwrap_or("<未接线>"),
"[i18n] forced={} setting={} platform={}:{} env={}:{} shot={} -> {}",
self.forced.as_deref().unwrap_or("<未设置>"),
self.setting.as_deref().unwrap_or("<未设置=跟随系统>"),
self.platform.0,
show(self.platform.1.as_deref()),
self.env.0,
show(self.env.1.as_deref()),
self.screenshot,
self.detection.describe()
)
}