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:
AskaEth committed 2026-11-04 22:50:09 +08:00
1 parent 777d02e7cb
commit 89dcb6bdf3
25 files changed
+1456 -92

No files matched your search

+37 -13
View File
@@ -22,6 +22,7 @@ use unlucky_design::icons::IconKind;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::{Key, Lang};
use crate::pages::appearance::Change;
use crate::pages::{Action, Message};
@@ -33,22 +34,40 @@ use super::Group;
const EMPTY_SLOT_HEIGHT: f32 = 320.0;
/// 页头:标题 + 副标题 + 两个占位动作。
pub(super) fn header(ui: &Ui) -> Header<'_> {
Header::new("Settings")
.subtitle("Launcher behaviour, Java runtimes and download sources")
.action(text_action(ui, "Reset section", Action::ResetSection))
.action(text_action(ui, "Save", Action::SaveSettings))
///
/// M106:三条文案走 catalog(`lang` 由 `settings::view` 传下来)。页标题复用 `NavSettings`——
/// 左列与页头说的是同一件事,不该有第二条译文。
pub(super) fn header<'a>(ui: &'a Ui, lang: Lang) -> Header<'a> {
let catalog = lang.catalog();
Header::new(catalog.text(Key::NavSettings))
.subtitle(catalog.text(Key::SettingsSubtitle))
.action(text_action(
ui,
catalog.text(Key::SettingsResetSection),
Action::ResetSection,
))
.action(text_action(
ui,
catalog.text(Key::SettingsSave),
Action::SaveSettings,
))
}
// --------------------------------------------------------------- 侧列
/// 左列分组导航(supporting-pane 的辅助列,宽 = 令牌 `nav_drawer_width` = 240)。
pub(super) fn group_nav<'a>(ui: &'a Ui, selected: usize) -> Element<'a, Message> {
pub(super) fn group_nav<'a>(ui: &'a Ui, selected: usize, lang: Lang) -> Element<'a, Message> {
let catalog = lang.catalog();
let items = column(
super::GROUPS
.iter()
.enumerate()
.map(|(index, group)| group_item(ui, group, index, index == selected))
.map(|(index, group)| {
let label = super::GROUP_LABELS
.get(index)
.map_or(group.name, |key| catalog.text(*key));
group_item(ui, group, label, index, index == selected)
})
.collect::<Vec<_>>(),
)
.spacing(ui.space(Space::X1))
@@ -72,6 +91,7 @@ pub(super) fn group_nav<'a>(ui: &'a Ui, selected: usize) -> Element<'a, Message>
fn group_item<'a>(
ui: &'a Ui,
group: &'a Group,
label: &'a str,
index: usize,
selected: bool,
) -> Element<'a, Message> {
@@ -86,7 +106,7 @@ fn group_item<'a>(
let inner = row![
glyph_box(group.icon, content_color, ICON_PX),
text_role(ui, spec.text, false, group.name, content_color),
text_role(ui, spec.text, false, label, content_color),
]
.spacing(ui.space(Space::X3))
.width(Length::Fill)
@@ -180,14 +200,18 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
}
/// 空态位:没有逐实例覆盖时的样子(12b §15 的口径)。
pub(super) fn overrides_empty<'a>(ui: &'a Ui) -> Element<'a, Message> {
pub(super) fn overrides_empty<'a>(ui: &'a Ui, lang: Lang) -> Element<'a, Message> {
let catalog = lang.catalog();
container(empty_state(
ui,
IconKind::Instances,
"No per-instance overrides",
"Overrides let one instance keep its own Java runtime, memory or JVM flags while the rest \
of the launcher keeps the defaults above.",
vec![text_action(ui, "Add override", Action::AddOverride)],
catalog.text(Key::SettingsOverridesEmptyTitle),
catalog.text(Key::SettingsOverridesEmptyBody),
vec![text_action(
ui,
catalog.text(Key::SettingsOverridesAdd),
Action::AddOverride,
)],
))
.width(Length::Fill)
.height(Length::Fixed(EMPTY_SLOT_HEIGHT))