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
@@ -40,6 +40,8 @@ use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::cli::ForcedState;
|
||||
use crate::icons;
|
||||
@@ -58,10 +60,12 @@ pub fn view_with_background<'a>(
|
||||
right: Option<Element<'a, Message>>,
|
||||
overlay: Option<Element<'a, Message>>,
|
||||
forced: Option<ForcedState>,
|
||||
lang: Lang,
|
||||
) -> Element<'a, Message> {
|
||||
// 右栗(M104)是 `row` 的**第三个兄弟**而不是叠层:它是布局宽度的一部分,压在上面会让
|
||||
// 中栗以为自己还有那么宽(那就是任务里禁止的「溢出」)。它的宽度由 `agent::docked` 自己声明。
|
||||
let mut work = row![nav::rail(ui, selected, forced), content]
|
||||
// M106:导航轨标签按当前界面语言取词(`lang` 一路从 `App::lang()` 递进来)。
|
||||
let mut work = row![nav::rail(ui, selected, forced, lang), content]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
if let Some(right) = right {
|
||||
|
||||
@@ -21,6 +21,7 @@ use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::{LayoutMetrics, Space};
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Key, Lang};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::cli::ForcedState;
|
||||
@@ -53,17 +54,25 @@ impl NavItem {
|
||||
NavItem::Ai,
|
||||
];
|
||||
|
||||
/// 显示名(i18n 是后续 mission;本 mission 用英文基线文案,避免干净 CI 上缺中文变豆腐块)。
|
||||
pub const fn label(self) -> &'static str {
|
||||
/// 显示名的 key(M106 起走 catalog;两套目录都有词由 `catalog` 的完整性测试守)。
|
||||
pub const fn label_key(self) -> Key {
|
||||
match self {
|
||||
NavItem::Instances => "Instances",
|
||||
NavItem::Download => "Download",
|
||||
NavItem::Logs => "Logs",
|
||||
NavItem::Settings => "Settings",
|
||||
NavItem::Ai => "Assistant",
|
||||
NavItem::Instances => Key::NavInstances,
|
||||
NavItem::Download => Key::NavDownload,
|
||||
NavItem::Logs => Key::NavLogs,
|
||||
NavItem::Settings => Key::NavSettings,
|
||||
NavItem::Ai => Key::NavAi,
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示名(按当前界面语言)。
|
||||
///
|
||||
/// 为什么带 `lang` 而不是读全局:这个函数要被单测与 harness 调,全局状态会让「同一份代码在
|
||||
/// 不同语言下画什么」不可断言。
|
||||
pub fn label(self, lang: Lang) -> &'static str {
|
||||
lang.catalog().text(self.label_key())
|
||||
}
|
||||
|
||||
/// 令牌图标。
|
||||
pub const fn icon(self) -> IconKind {
|
||||
match self {
|
||||
@@ -81,13 +90,14 @@ pub(super) fn rail<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: NavItem,
|
||||
forced: Option<ForcedState>,
|
||||
lang: Lang,
|
||||
) -> Element<'a, Message> {
|
||||
let metrics = ui.metrics();
|
||||
let indicator = indicator_size(metrics);
|
||||
|
||||
let items = NavItem::ALL
|
||||
.iter()
|
||||
.map(|item| rail_item(ui, *item, *item == selected, indicator, forced))
|
||||
.map(|item| rail_item(ui, *item, *item == selected, indicator, forced, lang))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 轨顶留白走 container padding,而不是「先塞一个 spacer 元素」:后者会被 column 的
|
||||
@@ -108,6 +118,7 @@ fn rail_item<'a>(
|
||||
selected: bool,
|
||||
indicator: (f32, f32),
|
||||
forced: Option<ForcedState>,
|
||||
lang: Lang,
|
||||
) -> Element<'a, Message> {
|
||||
let (indicator_width, indicator_height) = indicator;
|
||||
|
||||
@@ -145,7 +156,13 @@ fn rail_item<'a>(
|
||||
// 药丸会贴左缘(x=0..55 而不是 16..71)——r1 实测踩到的第二个坑。
|
||||
let stack = column![
|
||||
pill,
|
||||
label(ui, TypeRole::LabelMedium, false, item.label(), label_color),
|
||||
label(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
item.label(lang),
|
||||
label_color
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(nav_rail_width(ui.metrics()))
|
||||
@@ -253,7 +270,7 @@ mod tests {
|
||||
fn every_nav_item_has_a_label_and_an_icon() {
|
||||
assert!((4..=6).contains(&NavItem::ALL.len()));
|
||||
for item in NavItem::ALL {
|
||||
assert!(!item.label().is_empty(), "{item:?} 没有文案");
|
||||
assert!(!item.label(Lang::En).is_empty(), "{item:?} 没有文案");
|
||||
assert!(!item.icon().icon().path.is_empty(), "{item:?} 没有图标");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user