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:
1 parent
89dcb6bdf3
commit
954a129e01
14 files changed
+286
-107
No files matched your search
@@ -64,7 +64,7 @@ pub(super) fn report_boot(options: &Options, probe: &Probe, decision: &Decision)
|
||||
}
|
||||
acceptance::report_backend_plan(decision, options.backend);
|
||||
if acceptance::enabled() {
|
||||
acceptance::report_startup(probe, options.scale_factor);
|
||||
acceptance::report_startup(probe, options.scale_factor, options.screenshot.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ use std::time::Instant;
|
||||
use iced::window;
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::backend::{build_backend, config_path, AppOptions};
|
||||
use unlucky_presenter::dto::Backend;
|
||||
use unlucky_presenter::prefs::ThemeMode;
|
||||
@@ -60,11 +59,6 @@ pub(super) const IDLE_TICK: std::time::Duration = std::time::Duration::from_secs
|
||||
/// 3 秒够真 host 把「命令已受理」的第一条事件交回来;之后由「有 Busy/Running 实例」这个条件续订。
|
||||
pub(super) const WATCH_WINDOW: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// 设置页左侧分组的取证开关:`ULL_SETTINGS_GROUP=0` 直达 General(语言行在那里)。
|
||||
///
|
||||
/// 只影响 `--screenshot` 那一次进程(见 [`App::with_presenter`]);交互模式按用户点的那一组。
|
||||
pub(super) const SETTINGS_GROUP_ENV: &str = "ULL_SETTINGS_GROUP";
|
||||
|
||||
/// 进程内「run() 开始」的时刻(首帧耗时基线)。
|
||||
pub(super) static STARTED: OnceLock<Instant> = OnceLock::new();
|
||||
/// 首帧日志只打一次。
|
||||
@@ -206,17 +200,11 @@ impl App {
|
||||
backend_tier,
|
||||
flavor,
|
||||
);
|
||||
// 取证开关(D30,与 `ULL_START_ROUTE` 同一条思路):`ULL_SETTINGS_GROUP=<下标>` 选设置页的
|
||||
// 左侧分组。设置页出厂停在「外观」(`APPEARANCE_GROUP`),而「语言」那一行在 General 组——
|
||||
// 出图必须能直达,否则验收图里根本没有用户抱怨的那一行。**只在截图通道生效**:交互模式
|
||||
// 一律按用户点的那一组。
|
||||
if options.screenshot.is_some() {
|
||||
if let Some(index) = std::env::var(SETTINGS_GROUP_ENV)
|
||||
.ok()
|
||||
.and_then(|raw| raw.trim().parse::<usize>().ok())
|
||||
{
|
||||
appearance.selected_group = index;
|
||||
}
|
||||
// 取证开关(D30):`ULL_SETTINGS_GROUP=<下标>` 直达设置页的某一组(出图要看见语言行)。
|
||||
if let Some(index) =
|
||||
crate::ui::lang::screenshot_settings_group(options.screenshot.is_some())
|
||||
{
|
||||
appearance.selected_group = index;
|
||||
}
|
||||
// 界面语言(M106):**M92 已经探测对了,问题是一直没人用**——`pages::Model.lang` 此前在
|
||||
// `view::model` 里被写死成 `Lang::En`,于是中文系统上开出来的是全英文界面。
|
||||
@@ -328,7 +316,8 @@ impl App {
|
||||
// 否则会出现「选项区英文、取消弹窗中文」的同屏两语(复审 P2-2)。
|
||||
if self.state.install_cancelled != self.install_cancelled_seen {
|
||||
self.install_cancelled_seen = self.state.install_cancelled;
|
||||
// M106:提示语跟着**生效语言**走(M105 留的 `view::ui_lang()` 占位到此收口)。
|
||||
// M106:提示语跟着**生效语言**走(M105 留的 `view::ui_lang()` 占位已删——它恒英文,
|
||||
// 中文界面会弹出英文提示,那正是 M105 r1 复审 P2-2 要堵的同屏两语)。
|
||||
let text = pages::cancelled_toast(self.lang());
|
||||
self.state.toasts.push(ToastKind::Info, text);
|
||||
}
|
||||
@@ -378,37 +367,6 @@ impl App {
|
||||
pub(super) fn ui(&self) -> &Ui {
|
||||
&self.ui
|
||||
}
|
||||
|
||||
/// 当前生效的界面语言(= 目录选择;页面文案一律由它派生)。
|
||||
pub(super) fn lang(&self) -> Lang {
|
||||
self.lang.lang()
|
||||
}
|
||||
|
||||
/// 设置页语言行要的两件:档位(画选中态)+ 生效语言(画回显)。
|
||||
pub(super) fn language(&self) -> pages::Language {
|
||||
self.lang.for_settings()
|
||||
}
|
||||
|
||||
/// 换界面语言(设置页语言行 → `pages::Message::Language`)。
|
||||
///
|
||||
/// 三件事缺一不可:①档位与结论换掉 ②视图模型重建(`view_dirty` + `refresh_view`,目录是
|
||||
/// 视图模型的一部分)③落盘(重启后保持所选语言)。字体不在这里重扫:中文档缺字体时的
|
||||
/// 判据/信号归 `i18n::font::decide` + `fonts/cjk.rs`(见报告「没验的部分」)。
|
||||
pub(super) fn set_language(&mut self, choice: unlucky_i18n::LangChoice) {
|
||||
if choice == self.lang.choice() {
|
||||
return;
|
||||
}
|
||||
self.lang = LangState::of(choice);
|
||||
self.lang.log();
|
||||
if !self.bridge.persist_ui_language(choice.setting_value()) {
|
||||
eprintln!(
|
||||
"[i18n] 语言偏好没落盘(配置只读或数据根解析不出来):本次生效,重启会回到旧值"
|
||||
);
|
||||
}
|
||||
self.lang.log_cjk();
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动参数 → presenter 的后端选项。
|
||||
|
||||
@@ -180,7 +180,8 @@ fn cancelling_gives_the_panel_back_to_the_install_options() {
|
||||
///
|
||||
/// 抓得住:谁把中文写回 presenter(类型上已经做不到——它没有 i18n 依赖)、
|
||||
/// 或者 app 层每次 `refresh_view` 都重复弹(下面的 `items().len() == 1`)。
|
||||
/// 抓不住:`Lang` 本身接得对不对(`view::ui_lang()` 现恒为英文,那是 M106 的范围)。
|
||||
/// 抓不住:`Lang` 本身接得对不对(M106 起语言来自 `App::lang()`,本测试**显式钉**成英文,
|
||||
/// 否则这条断言会跟着跑测试那台机器的 locale 变——那是假绿)。
|
||||
#[test]
|
||||
fn cancelling_announces_once_in_the_page_language() {
|
||||
let log: Rc<RefCell<Vec<AppCommand>>> = Rc::new(RefCell::new(Vec::new()));
|
||||
@@ -198,6 +199,11 @@ fn cancelling_announces_once_in_the_page_language() {
|
||||
);
|
||||
assert!(app.state.toasts.is_empty(), "出厂态不该有 Toast");
|
||||
|
||||
// M106:语言显式钉成英文。不钉的话「页面语言」就是跑测试那台机器的 locale(英文 CI 绿、
|
||||
// 中文机器红)——那种测试证明不了任何事。钉住之后期望值仍从目录取,守的是同一条原意:
|
||||
// **提示语来自 i18n 目录,不是 presenter 写死的中文**。
|
||||
app.set_language(unlucky_i18n::LangChoice::Fixed(unlucky_i18n::Lang::En));
|
||||
|
||||
app.state.apply(AppEvent::Core(
|
||||
unlucky_presenter::dto::CoreEvent::InstallCancelled,
|
||||
));
|
||||
@@ -207,7 +213,7 @@ fn cancelling_announces_once_in_the_page_language() {
|
||||
let text = items[0].text.clone();
|
||||
assert_eq!(
|
||||
text,
|
||||
pages::cancelled_toast(super::super::view::ui_lang()),
|
||||
pages::cancelled_toast(unlucky_i18n::Lang::En),
|
||||
"提示语必须来自 i18n 目录(不是 presenter 写死的中文)"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -19,8 +19,20 @@
|
||||
//! - 抓不住:加一个 `pages::Action` 变体 —— 它落进 `Action(action)` 的占位分支(打印一行
|
||||
//! `[page] 占位动作 …`),编译期不会报。这是占位期的既有设计,不是本文件能守的。
|
||||
|
||||
use unlucky_i18n::{Lang, LangChoice};
|
||||
|
||||
use crate::pages;
|
||||
use crate::ui::lang::LangState;
|
||||
|
||||
use super::*;
|
||||
|
||||
// --------------------------------------------------------------- 界面语言(M106)
|
||||
//
|
||||
// 这三个方法住在本文件而不是 `app/state.rs`:`state.rs` 贴着 400 行硬线(M104 的 `agent`、
|
||||
// M105 的 `download_draft` 都往那儿长),而 `set_language` 的唯一调用点就是本文件的
|
||||
// `pages::Message::Language` 分支——落点与调用点放一起,读的人不用来回跳。
|
||||
// 与 M93 把 appearance 那批 `App` 方法放进 `app/appearance.rs` 是同一手法。
|
||||
|
||||
impl App {
|
||||
/// 页面消息:导航走路由栈,真业务命令派发,纯视图态改本地呈现。
|
||||
pub(super) fn on_page(&mut self, message: pages::Message) -> Task<Message> {
|
||||
@@ -72,6 +84,39 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// 当前生效的界面语言(= 目录选择;页面文案一律由它派生)。
|
||||
pub(in crate::app) fn lang(&self) -> Lang {
|
||||
self.lang.lang()
|
||||
}
|
||||
|
||||
/// 设置页语言行要的两件:档位(画选中态)+ 生效语言(画回显)。
|
||||
pub(in crate::app) fn language(&self) -> pages::Language {
|
||||
self.lang.for_settings()
|
||||
}
|
||||
|
||||
/// 换界面语言(设置页语言行 → `pages::Message::Language`)。
|
||||
///
|
||||
/// 三件事缺一不可:①档位与结论换掉 ②视图模型重建(`view_dirty` + `refresh_view`,目录是
|
||||
/// 视图模型的一部分)③落盘(重启后保持所选语言)。字体不在这里重扫:中文档缺字体时的
|
||||
/// 判据/信号归 `i18n::font::decide` + `fonts/cjk.rs`(见报告「没验的部分」)。
|
||||
pub(in crate::app) fn set_language(&mut self, choice: LangChoice) {
|
||||
if choice == self.lang.choice() {
|
||||
return;
|
||||
}
|
||||
self.lang = LangState::of(choice);
|
||||
self.lang.log();
|
||||
if !self.bridge.persist_ui_language(choice.setting_value()) {
|
||||
eprintln!(
|
||||
"[i18n] 语言偏好没落盘(配置只读或数据根解析不出来):本次生效,重启会回到旧值"
|
||||
);
|
||||
}
|
||||
self.lang.log_cjk();
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
|
||||
@@ -97,16 +97,6 @@ impl App {
|
||||
|
||||
// --------------------------------------------------------------- 快照 → 视图模型
|
||||
|
||||
/// **测试用的英文基线**(M105 留的占位在这里收口)。
|
||||
///
|
||||
/// 产品路径的界面语言由 `App::lang()` 经 `model(…, lang)` 与 `pages::cancelled_toast(lang)`
|
||||
/// 递进去(M106),这个函数只剩测试还在用(`app/update/download_wiring.rs` 的取消提示断言要一个
|
||||
/// 与产品无关的语言常量)。`#[cfg(test)]`:非测试构建里它没有调用点,留着就是死代码。
|
||||
#[cfg(test)]
|
||||
pub(super) fn ui_lang() -> Lang {
|
||||
Lang::En
|
||||
}
|
||||
|
||||
/// 把 presenter 的快照转成页面视图模型(**唯一**的展示转换点)。
|
||||
///
|
||||
/// `forced` 是 `--force-state` 的落点(P3-1):它只影响**画出来的**状态层,不改任何状态机。
|
||||
@@ -289,15 +279,8 @@ mod tests {
|
||||
/// 也抓不住不消费 `model.forced` 的裸 `iced::button`(设置页「外观」区就是)。
|
||||
/// 测试用的薄包装:默认宽度 / 默认分类 / 空草稿。
|
||||
fn at(state: &AppState, forced: Option<crate::cli::ForcedState>) -> pages::Model {
|
||||
let draft = pages::InstallDraft::default();
|
||||
model(
|
||||
state,
|
||||
1180.0,
|
||||
forced,
|
||||
pages::Category::All,
|
||||
&draft,
|
||||
Lang::En,
|
||||
)
|
||||
let (draft, lang) = (pages::InstallDraft::default(), Lang::En);
|
||||
model(state, 1180.0, forced, pages::Category::All, &draft, lang)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -67,9 +67,9 @@ pub fn report(detection: Detection, faces: Option<&[Face]>) -> CjkReport {
|
||||
|
||||
/// 探测本机:优先级见 [`unlucky_i18n::lang::resolve`]。
|
||||
///
|
||||
/// `setting` 是配置里 `ui.language` 的原文(`zh_cn` / `en_us`);`None` = **还没读配置**
|
||||
/// (设置层的读取点尚未接线,见模块文档的诚实边界)。注意 `None` 与 `Some("不认识的值")`
|
||||
/// 效果相同:都不当显式选择,继续往下走。
|
||||
/// `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)
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ use super::{Action, Message};
|
||||
|
||||
/// 整页(**唯一**的真数据入口:`model` 由 `app/view.rs` 从 presenter 快照建)。
|
||||
///
|
||||
/// `lang` 是 i18n 的入参:`theme::Ui` 还没有 language 字段(那条在 M93 手里),产品路径现传
|
||||
/// [`Lang::En`](D28 的英文基线零中文负担);接上探测结果时只改调用点,页面不用动。
|
||||
/// `lang` 是 i18n 的入参,调用点传的是 `pages::Model.lang`(M106 起 = `App` 的生效语言,
|
||||
/// 见 `ui::lang::LangState`);页面自己不读全局、不猜语言。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
|
||||
@@ -89,10 +89,11 @@ pub struct Model {
|
||||
pub download: Download,
|
||||
/// 日志页的数据(M95:presenter 快照 → 日志行 + 诊断结论,见 [`super::logs`])。
|
||||
pub logs: Logs,
|
||||
/// 界面语言(M95):页面文案走 i18n key,语言由 app 层决定后灌进来。
|
||||
/// 界面语言(M106):页面文案走 i18n key,语言由 app 层决定后灌进来。
|
||||
///
|
||||
/// 这里只是**留口**:`theme.rs` 还没有「当前语言」的入口(那条线归 M93),
|
||||
/// 产品路径先给 `Lang::En`;将来 `Ui` 拿到语言后,这个字段改由它填。
|
||||
/// 谁填:`app/state.rs` 的 `App::refresh_view`(`LangState::lang()` → `view::model(..)`),
|
||||
/// 即「用户档位 / 系统 locale → 生效语言」这条链的结论。**不是**基线值:M95 以前这里是
|
||||
/// `Lang::En` 占位,M106 之后中文系统上灌进来的是 `Lang::ZhCn`。
|
||||
pub lang: Lang,
|
||||
/// 取证用的强制交互态(`--force-state`):`Some` 时页面里的控件按该态画。
|
||||
///
|
||||
|
||||
@@ -101,9 +101,12 @@
|
||||
//! `app/view.rs` 不用动 —— **推荐这个形状**。
|
||||
//!
|
||||
//! 顺带订正 r3 之前那句「在 `app/state.rs` 用真值初始化 `Groups.language_appearance`(1 个文件)」:
|
||||
//! 今天 `App` / `app/state.rs` 里**没有**任何语言真值源(`presenter::prefs::AppearancePrefs` 无
|
||||
//! language 字段;唯一的值是 `app/view.rs:128` 里硬编码的 `Lang::En`),要先有真值源才谈得上
|
||||
//! 初始化位置。两种形状都是「谁先撞谁付一次钱」的一次性代价,不是永久性四路撞车。
|
||||
//! **M106 起语言已经有真值源**——`App::lang: LangState`(`crates/app-iced/src/ui/lang.rs`,用户档位
|
||||
//! `LangChoice` + 探测结论 `Detection`),由 `App::refresh_view` 经 `view::model(…, lang)` 灌进
|
||||
//! `pages::Model.lang`,设置页再从 `settings::view` 的 `language` 形参拿到它。所以下一条线要给某一组
|
||||
//! 读「当前语言」时,**不需要**再造一层状态:要么走 `Model.lang`(给 `settings::view` 加形参,
|
||||
//! 与 M106 同形状),要么读 `Groups` 里本线的字段。两种形状都是「谁先撞谁付一次钱」的一次性代价,
|
||||
//! 不是永久性四路撞车。
|
||||
//! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是
|
||||
//! `fn(&Ui, States) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! → `App::view` **一路都返回 `Element`**(iced 0.14 的 `view` 也只返回 `Element`,`view(&self)`
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ fn i18n_line_shows_every_rung_and_the_resolved_language() {
|
||||
|
||||
let chain = Chain::read(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
&Fixed(Some("zh-Hans-CN"), Some("applelanguages")),
|
||||
&Fixed(None, Some("env")),
|
||||
);
|
||||
@@ -81,11 +83,115 @@ fn i18n_line_shows_every_rung_and_the_resolved_language() {
|
||||
"平台源原始值必须打出来:{line}"
|
||||
);
|
||||
assert!(line.contains("env=env:<未设置>"), "{line}");
|
||||
assert!(line.contains("forced=<未设置>"), "{line}");
|
||||
assert!(line.contains("setting=<未设置=跟随系统>"), "{line}");
|
||||
assert!(line.contains("shot=false"), "{line}");
|
||||
assert!(
|
||||
line.contains("-> lang=zh-CN locale=zh-Hans-CN source=applelanguages"),
|
||||
"{line}"
|
||||
);
|
||||
assert!(line.contains("ui_text=pinned-en"), "{line}");
|
||||
// **回归绊线(M106 r1 p2-1)**:这行曾经钉着 `ui_text=pinned-en`,在文案真的接了
|
||||
// `model.lang` 之后变成一句假话(「界面钉在英文」)。它不许回来。
|
||||
assert!(
|
||||
!line.contains("pinned-en"),
|
||||
"`ui_text=pinned-en` 是 M106 之前的旧事实,现在会与 App 的 [i18n] 行自相矛盾:{line}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 配置的手选必须**同时**体现在结论与 `[env]` 的分工上(M106 r1 p2-1 的修法证据)。
|
||||
///
|
||||
/// 抓得住:①`ui.language` 这一级被丢掉(那行 `->` 的结论会退回系统语言,与 `App` 的
|
||||
/// `[i18n] choice=…` 矛盾);②把「机器语言」也改成手选值(`[env] language=` 答的是「本机是什么」,
|
||||
/// 用户手选英文不该改写机器的事实)。
|
||||
#[test]
|
||||
fn i18n_line_honours_the_explicit_setting_without_rewriting_the_machine_language() {
|
||||
struct Fixed(Option<&'static str>);
|
||||
impl LocaleSource for Fixed {
|
||||
fn locale(&self) -> Option<String> {
|
||||
self.0.map(str::to_owned)
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
"registry"
|
||||
}
|
||||
}
|
||||
|
||||
let chain = Chain::read(
|
||||
None,
|
||||
Some("en_us"),
|
||||
false,
|
||||
&Fixed(Some("zh-CN")),
|
||||
&Fixed(None),
|
||||
);
|
||||
assert_eq!(chain.language(), "zh-CN", "[env] 报的是机器语言");
|
||||
assert_eq!(chain.detection.lang, unlucky_i18n::Lang::En, "手选压过系统");
|
||||
let line = chain.line();
|
||||
assert!(line.contains("setting=en_us"), "{line}");
|
||||
assert!(line.contains("source=setting"), "{line}");
|
||||
assert!(line.contains("lang=en"), "{line}");
|
||||
}
|
||||
|
||||
/// **两个 `[i18n]` 行不许打架**(r4 复审 p2-1 的回归绊线)。
|
||||
///
|
||||
/// 复审实测的两组复现,现在必须是同一个结论:
|
||||
/// - ① 只设 `ULL_LANG=en_us`、配置干净 ⇒ 结论 = en(`forced` 压过系统中文);
|
||||
/// - ② 配置 `ui.language=en_us` + **截图通道** ⇒ 结论 = zh-CN(D30:截图不读用户配置);
|
||||
/// - ③ 配置 `ui.language=en_us` + 交互通道 ⇒ 结论 = en(配置这一级只在非截图时参与)。
|
||||
///
|
||||
/// 抓得住:①`ULL_LANG` 这一级被丢掉;②截图通道又去读配置(「一行说 zh-CN、下一行说 effective=en」
|
||||
/// 就是这么来的);③acceptance 侧自己复制了一份优先级表 —— 每条都用
|
||||
/// `crate::ui::lang::choose_choice`(产品路径那张表)算出**期望结论**再与 `Chain` 比,谁复制谁红。
|
||||
/// 抓不住:真机上 `reg.exe` / `defaults` 读到什么(那是真机验收),以及 `App` 那行日志本身
|
||||
/// (它由 `ui/lang.rs` 的单测守)。
|
||||
#[test]
|
||||
fn i18n_line_and_the_app_agree_on_every_channel_and_override() {
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
struct Fixed(Option<&'static str>);
|
||||
impl LocaleSource for Fixed {
|
||||
fn locale(&self) -> Option<String> {
|
||||
self.0.map(str::to_owned)
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
"registry"
|
||||
}
|
||||
}
|
||||
|
||||
let registry_zh = Fixed(Some("zh-CN"));
|
||||
let no_env = Fixed(None);
|
||||
|
||||
/// 一行真值表:`(ULL_LANG, 配置, 截图通道) -> (期望语言, 期望来源)`。
|
||||
type Case<'a> = (Option<&'a str>, Option<&'a str>, bool, Lang, &'a str);
|
||||
let table: &[Case<'_>] = &[
|
||||
(Some("en_us"), None, false, Lang::En, "setting"),
|
||||
// 复审组合 ①:`ULL_LANG` 在截图通道里同样压过系统中文
|
||||
(Some("en_us"), None, true, Lang::En, "setting"),
|
||||
// 复审组合 ②:截图通道**忽略配置**,于是退回系统语言
|
||||
(None, Some("en_us"), true, Lang::ZhCn, "registry"),
|
||||
// 组合 ③:非截图通道,配置这一级生效
|
||||
(None, Some("en_us"), false, Lang::En, "setting"),
|
||||
(None, None, false, Lang::ZhCn, "registry"),
|
||||
];
|
||||
|
||||
for (forced, setting, screenshot, lang, origin) in table {
|
||||
let chain = Chain::read(*forced, *setting, *screenshot, ®istry_zh, &no_env);
|
||||
let line = chain.line();
|
||||
assert_eq!(
|
||||
(chain.detection.lang, chain.detection.origin),
|
||||
(*lang, *origin),
|
||||
"forced={forced:?} setting={setting:?} shot={screenshot} 结论不对:{line}"
|
||||
);
|
||||
// `[env] language=` 只报机器语言,任何手选/覆盖都不该改写它。
|
||||
assert_eq!(chain.language(), "zh-CN", "{line}");
|
||||
|
||||
// **同一张表**的结构性钉子:期望结论取自产品路径那个函数,谁在 acceptance 侧另写一份就红。
|
||||
let expected = crate::ui::lang::choose_choice(*forced, *setting, *screenshot)
|
||||
.resolve(®istry_zh, &no_env);
|
||||
assert_eq!(chain.detection, expected, "两侧的判据表漂了:{line}");
|
||||
assert!(
|
||||
line.contains(&format!("shot={screenshot}")),
|
||||
"截图通道必须在行里写出来:{line}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 抓得住 macOS 最典型的失败形态:**一个源都没有**(Finder 启动的 GUI 进程既没有 `LANG`
|
||||
@@ -102,7 +208,7 @@ fn i18n_line_distinguishes_no_source_from_english_source() {
|
||||
}
|
||||
}
|
||||
|
||||
let empty = Chain::read(None, &Nothing, &Nothing);
|
||||
let empty = Chain::read(None, None, false, &Nothing, &Nothing);
|
||||
assert_eq!(empty.language(), "en");
|
||||
assert!(empty.line().contains("source=default"), "{}", empty.line());
|
||||
|
||||
@@ -116,7 +222,7 @@ fn i18n_line_distinguishes_no_source_from_english_source() {
|
||||
}
|
||||
}
|
||||
|
||||
let env = Chain::read(None, &Nothing, &EnvEnglish);
|
||||
let env = Chain::read(None, None, false, &Nothing, &EnvEnglish);
|
||||
assert!(env.line().contains("source=env"), "{}", env.line());
|
||||
assert!(
|
||||
env.line().contains("locale=en_US.UTF-8"),
|
||||
|
||||
@@ -69,8 +69,10 @@ pub fn announce() {
|
||||
///
|
||||
/// 两行都吃**同一次**适配器枚举与**同一次**语言链(`defaults` / `reg.exe` 是子进程,读两次就是
|
||||
/// 两次开销,而且两次之间值可能变)。
|
||||
pub fn report_startup(probe: &Probe, scale: Option<f32>) {
|
||||
let chain = Chain::host();
|
||||
pub fn report_startup(probe: &Probe, scale: Option<f32>, screenshot: bool) {
|
||||
// `screenshot` 一路递到 `Chain`:截图通道按 D30 忽略配置,判定必须与产品路径同源
|
||||
// (两行日志用的都是 `ui::lang::choose_choice` 那一张表,见 `locale::Chain::read`)。
|
||||
let chain = Chain::host(screenshot);
|
||||
eprintln!(
|
||||
"{}",
|
||||
env_line(probe, chain.language(), scale, tz().as_deref())
|
||||
|
||||
@@ -51,6 +51,10 @@ use crate::theme::Ui;
|
||||
///
|
||||
/// `background` 由 `pages::appearance::layer` 产出(已与 `Message::Page` 映射过);垫在 `stack!`
|
||||
/// 最底层,于是「面板 α < 1」时能真的看见它——这就是 D34 在像素上的落点。
|
||||
// 9 个形参:M104 的 `right`/`overlay` 之后 M106 又加了 `lang`(导航轨标签要按语言取词)。
|
||||
// 拆成结构体要动 `app/view.rs` 的两处调用点,而那正是 M104/M105 反复设锚点的地方——
|
||||
// 等 M105 那轮 rebase(`draft` 与 `lang` 两个形参都要留)时一并结构体化,先就地 `allow`。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn view_with_background<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: NavItem,
|
||||
|
||||
@@ -50,6 +50,21 @@ use crate::platform::locale::SystemLocale;
|
||||
/// 一个环境变量就换语言(与 `ULL_APPEARANCE_*` 那批同一条做法)。
|
||||
pub(crate) const LANG_ENV: &str = "ULL_LANG";
|
||||
|
||||
/// 设置页左侧分组的取证开关:`ULL_SETTINGS_GROUP=0` 直达 General(语言行在那里)。
|
||||
///
|
||||
/// 只影响 `--screenshot` 那一次进程(设置页出厂停在「外观」,而用户抱怨的语言行在 General 组;
|
||||
/// 出图必须能直达)。交互模式一律按用户点的那一组,所以这里拿 `screenshot` 当闸门。
|
||||
/// 为什么住在 `ui/lang.rs`:它与 [`LANG_ENV`] 是同一族「出图取证开关」,一起放在语言入口层,
|
||||
/// `App` 那边只留一行调用(`app/state.rs` 贴着 400 行硬线)。
|
||||
pub(crate) fn screenshot_settings_group(screenshot: bool) -> Option<usize> {
|
||||
if !screenshot {
|
||||
return None;
|
||||
}
|
||||
std::env::var("ULL_SETTINGS_GROUP")
|
||||
.ok()
|
||||
.and_then(|raw| raw.trim().parse::<usize>().ok())
|
||||
}
|
||||
|
||||
/// 这一次进程的界面语言:用户选的档 + 探测出来的结论。
|
||||
///
|
||||
/// 不是 `Copy`:结论里带着探测到的原始 locale 字符串(`Detection.raw`),而它只在切档/启动时
|
||||
@@ -133,13 +148,21 @@ impl LangState {
|
||||
|
||||
/// 档位选择的**纯判据**(单测直接打这张表)。
|
||||
///
|
||||
/// **产品路径与验收路径共用它**:`LangState::boot` 用它决定目录,`renderer::acceptance::locale`
|
||||
/// 的 `[i18n]` 行也用它算结论——两行日志因此不可能再说出不同的语言(r4 复审 p2-1 就是
|
||||
/// 「两处各算一遍」造成的)。
|
||||
///
|
||||
/// | 输入 | 结果 |
|
||||
/// |---|---|
|
||||
/// | `ULL_LANG` 给了合法值 | 那一档(压过配置) |
|
||||
/// | 没给 + 配置写了 `zh_cn` / `en_us` | 那一档 |
|
||||
/// | 没给 + 配置没写(或写了坏值) | 跟随系统 |
|
||||
/// | **截图通道** + 配置写了值 | 跟随系统(除非 `ULL_LANG` 另给) |
|
||||
fn choose_choice(forced: Option<&str>, setting: Option<&str>, screenshot: bool) -> LangChoice {
|
||||
pub(crate) fn choose_choice(
|
||||
forced: Option<&str>,
|
||||
setting: Option<&str>,
|
||||
screenshot: bool,
|
||||
) -> LangChoice {
|
||||
let configured = if screenshot { None } else { setting };
|
||||
LangChoice::from_setting(forced.or(configured))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user