feat(app-iced,i18n): M106 —— 界面跟随系统语言 + 设置页真实语言行
用户原话:「我系统语言是中文 为什么打开软件是英文 并且设置页里面显示的是 English(system)」。
两处根源:①M92 的探测链一直是对的,但 `pages::Model.lang` 在 `app/view.rs` 里写死成
`Lang::En`(`pages::render` 还把下载页写死成英文),探测结果没人用;②设置页语言行是硬编码
字面量 `("Language", "English (System)")`。
新增/改动(按层):
- i18n:新域 `catalog/general.rs`(24 条中英双份 key,装配链尾追加)+ `catalog/golden.rs`
只追加不重排;`choice.rs`(`LangChoice`:跟随系统/手选 + D28 四级优先级真值表 + 字体优先级真值表)。
- app-iced 语言入口 `src/ui.rs` + `src/ui/lang.rs`:`LangState` = 用户档位 + 探测结论;
启动判定 `ULL_LANG` > 配置 `ui.language` > 系统 locale > 英文;两行日志(探测/生效 + 中文策略)。
- `app/state.rs`:`App` 持有 `LangState`,切语言 = 换目录 + 落盘 + 重建视图模型;
`app/view.rs` 的 `model()` 收 `lang`,不再写死英文。
- 设置页:语言行换真控件(跟随系统 / 简体中文 / 英文,回显「界面当前使用:简体中文」);
页头、左列分组名、逐实例空态、导航轨标签全部走 catalog。
- presenter:`settings_bridge_lang.rs`(`load_ui_language` / `persist_ui_language`)。
「跟随系统」= **删键**(schema 允许值只有 zh_cn/en_us),读的是 `doc().get()` 而不是
`effective_str`(后者会把默认值 `zh_cn` 当成用户手选,英文机器一启动就被切成中文)。
验证:408 条 app-iced lib 测试 + 28 条 i18n 测试 + 全仓 `cargo test --workspace --exclude
unlucky-app` 全绿;fmt / clippy `-D warnings` 干净;出图 6 张(en/zh × 浅/深 + 下载页对照)
在 `DevDocs/artifacts/m106/`(该目录被 .gitignore 忽略,按仓内惯例作为本地验收产物)。
This commit is contained in:
1 parent
777d02e7cb
commit
89dcb6bdf3
25 files changed
+1456
-92
No files matched your search
@@ -20,6 +20,7 @@ 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;
|
||||
@@ -30,6 +31,7 @@ use unlucky_presenter::toast::ToastKind;
|
||||
use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::{Families, Ui};
|
||||
use crate::ui::lang::LangState;
|
||||
|
||||
use super::WINDOW_SIZE;
|
||||
|
||||
@@ -58,6 +60,11 @@ 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();
|
||||
/// 首帧日志只打一次。
|
||||
@@ -86,6 +93,11 @@ pub struct App {
|
||||
/// 用来定优先级:`--theme` > 配置文件 > 系统主题。没有它,`Message::SystemTheme` 会无条件
|
||||
/// 盖掉配置文件的显式主题(M90 r3 修)。
|
||||
pub(super) theme_mode: ThemeMode,
|
||||
/// 界面语言(M106):用户档位 + 探测结论 + 目录选择。见 [`crate::ui::lang`]。
|
||||
///
|
||||
/// 为什么不在 `Ui` 里:`theme.rs` 是令牌求值器(颜色/字号),而语言是**运行期状态**——
|
||||
/// 随设置变、要落盘、要重建视图模型(切主题只重建 Style)。
|
||||
pub(super) lang: LangState,
|
||||
/// 上一次推进节拍的时刻(节拍由帧事件驱动,按真实流逝时间累加)。
|
||||
pub(super) last_tick: Instant,
|
||||
/// 窗口宽度(布局档位的判据;交互模式跟着 resize 事件走)。
|
||||
@@ -188,17 +200,33 @@ impl App {
|
||||
// 面板 α / 玻璃的降级判据要的是**实际**后端,不是 `--renderer` 的请求值(复审 r1 P2-2):
|
||||
// 默认 `Auto` + 无独显时请求是 Auto、实际是 tiny-skia,只看请求值就会漏掉 D36 的强制项。
|
||||
let backend_tier = super::backend::effective_backend(options.backend);
|
||||
let appearance = pages::appearance::State::new(
|
||||
let mut appearance = pages::appearance::State::new(
|
||||
&appearance_prefs,
|
||||
&appearance_base,
|
||||
backend_tier,
|
||||
flavor,
|
||||
);
|
||||
let view = pages::Model::new(
|
||||
// 取证开关(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;
|
||||
}
|
||||
}
|
||||
// 界面语言(M106):**M92 已经探测对了,问题是一直没人用**——`pages::Model.lang` 此前在
|
||||
// `view::model` 里被写死成 `Lang::En`,于是中文系统上开出来的是全英文界面。
|
||||
let lang = LangState::boot(&bridge, options.screenshot.is_some());
|
||||
let mut view = pages::Model::new(
|
||||
pages::Screen::Instances,
|
||||
width,
|
||||
Some(pages::Screen::Instances.nav()),
|
||||
);
|
||||
view.lang = lang.lang();
|
||||
|
||||
let mut app = Self {
|
||||
ui: Ui::new(flavor, families),
|
||||
@@ -210,6 +238,7 @@ impl App {
|
||||
ticks: 0,
|
||||
config_due_at: unlucky_presenter::theme::CONFIG_POLL_TICKS,
|
||||
theme_mode,
|
||||
lang,
|
||||
last_tick: Instant::now(),
|
||||
width,
|
||||
view,
|
||||
@@ -232,6 +261,10 @@ impl App {
|
||||
};
|
||||
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是 baseline。
|
||||
app.rebuild_ui();
|
||||
// 两行日志:①档位 + 探测结论 + 生效语言 ②中文策略(D28 缺字体不许静默)。
|
||||
// 下一个人据此分得清「探测错了」还是「探测对了但没用上」。
|
||||
app.lang.log();
|
||||
app.lang.log_cjk();
|
||||
app
|
||||
}
|
||||
|
||||
@@ -295,7 +328,8 @@ impl App {
|
||||
// 否则会出现「选项区英文、取消弹窗中文」的同屏两语(复审 P2-2)。
|
||||
if self.state.install_cancelled != self.install_cancelled_seen {
|
||||
self.install_cancelled_seen = self.state.install_cancelled;
|
||||
let text = pages::cancelled_toast(super::view::ui_lang());
|
||||
// M106:提示语跟着**生效语言**走(M105 留的 `view::ui_lang()` 占位到此收口)。
|
||||
let text = pages::cancelled_toast(self.lang());
|
||||
self.state.toasts.push(ToastKind::Info, text);
|
||||
}
|
||||
|
||||
@@ -307,12 +341,14 @@ impl App {
|
||||
let draft = self.download_draft.clone();
|
||||
// 右栗占掉的宽度先扣掉:面板展开时页面按更窄的宽度排版(「收缩而不是溢出」,M104)
|
||||
let width = self.agent.content_width(self.width);
|
||||
// M106:语言形参跟着 `LangState` 走(`draft` 那一位是 M105 的安装草稿)。
|
||||
self.view = super::view::model(
|
||||
&self.state,
|
||||
width,
|
||||
self.options.force_state,
|
||||
category,
|
||||
&draft,
|
||||
self.lang(),
|
||||
);
|
||||
self.view_dirty = false;
|
||||
self.refreshes += 1;
|
||||
@@ -342,6 +378,37 @@ 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 的后端选项。
|
||||
|
||||
Reference in new issue
Block a user