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
@@ -34,6 +34,7 @@
|
||||
//! | [`nav`] / [`instances`] / [`loader`] / [`launch`] / [`font`] / [`core_text`] | 页面与运行时文案 | 各自页面 |
|
||||
//! | [`download`] / [`logs`] | 下载页 / 日志页(M94 / M95) | 那两页 |
|
||||
//! | `settings/`(`general` / `appearance` / `java` / `download` / `ai`) | 设置页,**一个分组一个文件** | 设置页四条线 |
|
||||
//! | [`general`] | 设置页 General 分组的**真控件**文案(语言选择器三档 + 主题/启动页/关闭三行) | M106 `I18NFOLLOW`(挂在链尾,既有顺序不动) |
|
||||
//!
|
||||
//! ### 域文件的形状(加 key 时照抄)
|
||||
//!
|
||||
@@ -115,6 +116,7 @@ mod agent;
|
||||
mod core_text;
|
||||
mod download;
|
||||
mod font;
|
||||
mod general;
|
||||
mod instances;
|
||||
mod launch;
|
||||
mod loader;
|
||||
@@ -132,6 +134,7 @@ use agent::agent_entries;
|
||||
use core_text::core_text_entries;
|
||||
use download::download_entries;
|
||||
use font::font_entries;
|
||||
use general::general_entries;
|
||||
use instances::instances_entries;
|
||||
use launch::launch_entries;
|
||||
use loader::loader_entries;
|
||||
@@ -158,6 +161,7 @@ nav_entries! {
|
||||
download_entries,
|
||||
logs_entries,
|
||||
agent_entries,
|
||||
general_entries,
|
||||
finish,
|
||||
{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! 设置页 General 分组**真控件**的文案(M106 `I18NFOLLOW`):语言选择器 + 其余三行。
|
||||
//!
|
||||
//! ## 与 `catalog/settings/general.rs` 的分工
|
||||
//!
|
||||
//! `catalog/settings/general.rs`(M99 建的)只有两条**标题**([`SettingsLanguage`] /
|
||||
//! [`SettingsLanguageHint`]),是设置页五域装配的一部分;本文件是 M106 接真控件时新增的
|
||||
//! **选择器文案**:三档选项名、选中后的「界面当前使用」回显,以及 General 分组里另外三行
|
||||
//! (主题 / 启动页 / 关闭按钮)。
|
||||
//!
|
||||
//! 为什么要分两个文件:`catalog.rs` 的装配链是**追加**式的,新域挂在链尾就不会动到既有
|
||||
//! `Key` 的顺序(= `catalog/golden.rs` 那张逐字节金表)。两个文件同属 General 分组,读代码时
|
||||
//! 按「标题 vs 选项」区分即可;真要合并也是一个文件搬十行的活,不急着在接线的 mission 里做。
|
||||
//!
|
||||
//! ## 选项名为什么不是「语言自称」
|
||||
//!
|
||||
//! 常见做法是三档都写 `简体中文` / `English` 这样的自称。这里没有:`catalog/tests.rs` 有一条
|
||||
//! `chinese_catalog_is_actually_chinese`(英文表与中文表逐 key 必须不同),自称会让「中文表里
|
||||
//! 写着 English」被判成漏译。所以选项名**跟着界面语言走**:中文界面「跟随系统 / 简体中文 / 英文」,
|
||||
//! 英文界面 "Follow system / Chinese (Simplified) / English"——这也是多数启动器的做法。
|
||||
//!
|
||||
//! 域文件的形状见 [`crate::catalog`] 的「域装配」一节。
|
||||
|
||||
macro_rules! general_entries {
|
||||
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
|
||||
$next! { $($rest,)* {
|
||||
$($acc)*
|
||||
// ── General 分组(语言选择器 + 其余三行)
|
||||
SettingsLanguageFollowSystem => ("Follow system", "跟随系统"),
|
||||
SettingsLanguageZhCn => ("Chinese (Simplified)", "简体中文"),
|
||||
SettingsLanguageEn => ("English", "英文"),
|
||||
SettingsLanguageEffective => ("Interface right now: {language}", "界面当前使用:{language}"),
|
||||
SettingsTheme => ("Theme", "主题"),
|
||||
SettingsThemeFollowSystem => ("Follow system", "跟随系统"),
|
||||
SettingsStartupPage => ("Startup page", "启动页"),
|
||||
SettingsStartupInstances => ("Instances", "实例"),
|
||||
SettingsCloseButton => ("Close button", "关闭按钮"),
|
||||
SettingsCloseToTray => ("Exit to tray", "退出到托盘"),
|
||||
// 设置页页头(M106):标题复用 `NavSettings`,这三条是副标题与两个动作。
|
||||
SettingsSubtitle => ("Launcher behaviour, Java runtimes and download sources", "启动器行为、Java 运行时与下载源"),
|
||||
SettingsResetSection => ("Reset section", "重置本组"),
|
||||
SettingsSave => ("Save", "保存"),
|
||||
// 第五条导航项(M106 给导航轨补的最后一条)。放在本域而不是 `catalog/nav.rs`:
|
||||
// 金表按**位置**逐字节比,插进 nav 域会把既有 4 条导航行整体平移一位。
|
||||
NavAi => ("Assistant", "助手"),
|
||||
// 设置页左列的分组名(M106)。`Group.name` 是**id**(测试按它认组),显示名走这里。
|
||||
SettingsGroupGeneral => ("General", "通用"),
|
||||
SettingsGroupAppearance => ("Appearance", "外观"),
|
||||
SettingsGroupJava => ("Java", "Java 运行时"),
|
||||
SettingsGroupDownloads => ("Downloads", "下载"),
|
||||
// 逐实例覆盖的空态(M106)
|
||||
SettingsOverridesEmptyTitle => ("No per-instance overrides", "没有逐实例覆盖"),
|
||||
SettingsOverridesEmptyBody => ("Overrides let one instance keep its own Java runtime, memory or JVM flags while the rest of the launcher keeps the defaults above.", "逐实例覆盖让某个实例保留自己的 Java 运行时、内存或 JVM 参数,其余实例沿用上面的默认值。"),
|
||||
SettingsOverridesAdd => ("Add override", "新增覆盖"),
|
||||
// `Route::Agent` 的回退卡正文(M106:`app/view.rs` 里最后一条硬编码英文)
|
||||
AgentFallbackBody => ("The assistant page is not built yet. Chat lives in the docked panel once the AI mission lands; this screen is the M68 skeleton fallback.", "助手页还没做。AI 接线落地后对话会出现在右侧停靠面板;这一屏是 M68 的骨架回退。"),
|
||||
} }
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) use general_entries;
|
||||
@@ -124,4 +124,28 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
|
||||
("AgentRoleYou", "You", "你"),
|
||||
("AgentRoleAssistant", "Assistant", "助手"),
|
||||
("AgentRoleNotice", "Notice", "提示"),
|
||||
// ── M106 `I18NFOLLOW`:General 分组真控件(语言选择器 + 其余三行)。**只追加在链尾**,
|
||||
// 上一行到 `LogsEmptyAction` 的既有顺序一字未动(金表要证明的正是这件事)。
|
||||
("SettingsLanguageFollowSystem", "Follow system", "跟随系统"),
|
||||
("SettingsLanguageZhCn", "Chinese (Simplified)", "简体中文"),
|
||||
("SettingsLanguageEn", "English", "英文"),
|
||||
("SettingsLanguageEffective", "Interface right now: {language}", "界面当前使用:{language}"),
|
||||
("SettingsTheme", "Theme", "主题"),
|
||||
("SettingsThemeFollowSystem", "Follow system", "跟随系统"),
|
||||
("SettingsStartupPage", "Startup page", "启动页"),
|
||||
("SettingsStartupInstances", "Instances", "实例"),
|
||||
("SettingsCloseButton", "Close button", "关闭按钮"),
|
||||
("SettingsCloseToTray", "Exit to tray", "退出到托盘"),
|
||||
("SettingsSubtitle", "Launcher behaviour, Java runtimes and download sources", "启动器行为、Java 运行时与下载源"),
|
||||
("SettingsResetSection", "Reset section", "重置本组"),
|
||||
("SettingsSave", "Save", "保存"),
|
||||
("NavAi", "Assistant", "助手"),
|
||||
("SettingsGroupGeneral", "General", "通用"),
|
||||
("SettingsGroupAppearance", "Appearance", "外观"),
|
||||
("SettingsGroupJava", "Java", "Java 运行时"),
|
||||
("SettingsGroupDownloads", "Downloads", "下载"),
|
||||
("SettingsOverridesEmptyTitle", "No per-instance overrides", "没有逐实例覆盖"),
|
||||
("SettingsOverridesEmptyBody", "Overrides let one instance keep its own Java runtime, memory or JVM flags while the rest of the launcher keeps the defaults above.", "逐实例覆盖让某个实例保留自己的 Java 运行时、内存或 JVM 参数,其余实例沿用上面的默认值。"),
|
||||
("SettingsOverridesAdd", "Add override", "新增覆盖"),
|
||||
("AgentFallbackBody", "The assistant page is not built yet. Chat lives in the docked panel once the AI mission lands; this screen is the M68 skeleton fallback.", "助手页还没做。AI 接线落地后对话会出现在右侧停靠面板;这一屏是 M68 的骨架回退。"),
|
||||
];
|
||||
@@ -0,0 +1,258 @@
|
||||
//! 设置页的**界面语言档位**(M106 `I18NFOLLOW`):跟随系统 / 手选某一门。
|
||||
//!
|
||||
//! ## 为什么单独一个文件
|
||||
//!
|
||||
//! `lang.rs` 讲的是「系统说哪种语言」(locale → [`Lang`] 的探测,M92),本文件讲的是「用户要哪种
|
||||
//! 语言」——两者的判据不一样:探测只能尽量理解别人给的字符串,档位是我们自己写进配置的,写错就该
|
||||
//! 暴露。放在一起会让 `lang.rs` 顶破 400 行(任务书那条硬线要求按功能拆文件)。
|
||||
//!
|
||||
//! ## D28 的判定顺序(整条挂在 [`LangChoice::resolve`] 上)
|
||||
//!
|
||||
//! **手选 > 系统语言 > 环境变量 > 英文**。最后一级「英文」不是偷懒:纯英文环境不扫、不下载中文
|
||||
//! 字体(零额外体积 / 零网络),这条由 [`crate::font::decide`] 兜住,真值表见下面
|
||||
//! `english_never_needs_a_chinese_font_and_chinese_always_resolves_one`。
|
||||
//!
|
||||
//! ## 「跟随系统」怎么表达
|
||||
//!
|
||||
//! **不写 `ui.language` 这个键**([`LangChoice::setting_value`] 返回 `None`)。不写哨兵值是因为
|
||||
//! 两件事:① schema 的允许值只有 `zh_cn` / `en_us`(`core::setting::keys::LANGUAGES`),写别的
|
||||
//! 会被枚举校验拒掉;② 读的那一头(`SettingBridge::load_ui_language`)只认显式值,所以「键不存在」
|
||||
//! 与「用户选了跟随系统」本来就是同一件事。
|
||||
|
||||
use crate::catalog::Key;
|
||||
use crate::lang::{resolve, Detection, Lang, LocaleSource, ORIGIN_SETTING};
|
||||
|
||||
/// 设置页「界面语言」的三档选择。
|
||||
///
|
||||
/// 为什么不是 `Option<Lang>`:`None` 与「用户明确勾了跟随系统」在类型上分不开,而设置页要把那一档
|
||||
/// **画成选中**。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum LangChoice {
|
||||
/// 跟随系统(= 配置里没有 `ui.language`)。
|
||||
System,
|
||||
/// 用户手选的固定语言。
|
||||
Fixed(Lang),
|
||||
}
|
||||
|
||||
impl LangChoice {
|
||||
/// 全部档位(顺序即设置页语言行的按钮顺序)。
|
||||
pub const ALL: &'static [LangChoice] = &[
|
||||
LangChoice::System,
|
||||
LangChoice::Fixed(Lang::ZhCn),
|
||||
LangChoice::Fixed(Lang::En),
|
||||
];
|
||||
|
||||
/// 配置里 `ui.language` 的原文 → 档位。
|
||||
///
|
||||
/// `None`(键不存在)与**不认识的值**都算「跟随系统」——与 [`resolve`] 跳过坏值的行为同源:
|
||||
/// 配置被 AI / CLI 写脏时退回系统语言,而不是猜成某一种。
|
||||
#[must_use]
|
||||
pub fn from_setting(value: Option<&str>) -> LangChoice {
|
||||
value
|
||||
.and_then(Lang::from_setting)
|
||||
.map_or(LangChoice::System, LangChoice::Fixed)
|
||||
}
|
||||
|
||||
/// 落盘用:跟随系统 = `None`(**删掉那个键**,不写哨兵值——哨兵会让「跟随系统」永远失效)。
|
||||
#[must_use]
|
||||
pub const fn setting_value(self) -> Option<&'static str> {
|
||||
match self {
|
||||
LangChoice::System => None,
|
||||
LangChoice::Fixed(lang) => Some(lang.setting_value()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 这一档在设置页上的名字(key 在 `catalog/general.rs` 域)。
|
||||
#[must_use]
|
||||
pub const fn label_key(self) -> Key {
|
||||
match self {
|
||||
LangChoice::System => Key::SettingsLanguageFollowSystem,
|
||||
LangChoice::Fixed(Lang::ZhCn) => Key::SettingsLanguageZhCn,
|
||||
LangChoice::Fixed(Lang::En) => Key::SettingsLanguageEn,
|
||||
}
|
||||
}
|
||||
|
||||
/// 这一档最终生效的语言 + 来源(日志与设置页回显都用它)。
|
||||
///
|
||||
/// 跟随系统 → 走 [`resolve`] 的三级链;手选 → 直接赢,`origin = setting`
|
||||
/// (`raw` 写配置值,日志里能看出「这是用户手选的」)。
|
||||
#[must_use]
|
||||
pub fn resolve(self, platform: &dyn LocaleSource, env: &dyn LocaleSource) -> Detection {
|
||||
match self {
|
||||
LangChoice::System => resolve(None, platform, env),
|
||||
LangChoice::Fixed(lang) => Detection {
|
||||
lang,
|
||||
raw: Some(lang.setting_value().to_owned()),
|
||||
origin: ORIGIN_SETTING,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lang::ORIGIN_DEFAULT;
|
||||
|
||||
#[test]
|
||||
fn choice_round_trips_through_the_config_value() {
|
||||
// 抓得住:①「跟随系统」被写成哨兵值——那会让 `Lang::from_setting` 认不出、下次启动退回
|
||||
// 系统语言,「手选」这件事静默丢失;②档位顺序被调换(设置页按钮顺序)。
|
||||
// 抓不住:配置文件里到底有没有这个键(那是 `SettingsBridge` 的读写,见 presenter 侧测试)。
|
||||
assert_eq!(LangChoice::from_setting(None), LangChoice::System);
|
||||
assert_eq!(
|
||||
LangChoice::from_setting(Some("zh_cn")),
|
||||
LangChoice::Fixed(Lang::ZhCn)
|
||||
);
|
||||
assert_eq!(
|
||||
LangChoice::from_setting(Some("en_us")),
|
||||
LangChoice::Fixed(Lang::En)
|
||||
);
|
||||
for bogus in ["zh-CN", "system", "", "fr_fr"] {
|
||||
assert_eq!(
|
||||
LangChoice::from_setting(Some(bogus)),
|
||||
LangChoice::System,
|
||||
"{bogus}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(LangChoice::System.setting_value(), None, "跟随系统不写键");
|
||||
for choice in LangChoice::ALL {
|
||||
assert_eq!(
|
||||
LangChoice::from_setting(choice.setting_value()),
|
||||
*choice,
|
||||
"{} 落盘后读不回来",
|
||||
choice.setting_value().unwrap_or("<跟随系统>")
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
LangChoice::ALL,
|
||||
&[
|
||||
LangChoice::System,
|
||||
LangChoice::Fixed(Lang::ZhCn),
|
||||
LangChoice::Fixed(Lang::En)
|
||||
][..]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choice_resolution_truth_table() {
|
||||
// D28 的四级优先级,**逐档逐源**列出来:手选 > 系统 > 环境变量 > 英文。
|
||||
// 抓得住:优先级顺序被改(例如手选的英文被中文系统盖掉)、或「跟随系统」直接落英文。
|
||||
// 抓不住:这些源在真机上读到什么(`SystemLocale` 的真机行为在 `app-iced` 侧)。
|
||||
struct Named(&'static str, Option<&'static str>);
|
||||
|
||||
impl LocaleSource for Named {
|
||||
fn locale(&self) -> Option<String> {
|
||||
self.1.map(str::to_owned)
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
let registry_zh = Named("registry", Some("zh-CN"));
|
||||
let registry_en = Named("registry", Some("en-US"));
|
||||
let nothing = Named("registry", None);
|
||||
let env_zh = Named("env", Some("zh_CN.UTF-8"));
|
||||
let env_en = Named("env", Some("en_US.UTF-8"));
|
||||
let env_none = Named("env", None);
|
||||
|
||||
let follow = LangChoice::System;
|
||||
let en_choice = LangChoice::Fixed(Lang::En);
|
||||
let zh = LangChoice::Fixed(Lang::ZhCn);
|
||||
|
||||
// (档位, 平台源, 环境变量源) -> (生效语言, 来源)
|
||||
let table: &[(LangChoice, &dyn LocaleSource, &dyn LocaleSource, Lang, &str)] = &[
|
||||
// ① 手选压过一切(系统是中文、环境是中文,手选英文就画英文)
|
||||
(en_choice, ®istry_zh, &env_zh, Lang::En, ORIGIN_SETTING),
|
||||
(zh, ®istry_en, &env_en, Lang::ZhCn, ORIGIN_SETTING),
|
||||
// ② 跟随系统 → 系统源
|
||||
(follow, ®istry_zh, &env_en, Lang::ZhCn, "registry"),
|
||||
(follow, ®istry_en, &env_zh, Lang::En, "registry"),
|
||||
// ③ 系统源取不到 → 环境变量
|
||||
(follow, ¬hing, &env_zh, Lang::ZhCn, "env"),
|
||||
(follow, ¬hing, &env_en, Lang::En, "env"),
|
||||
// ④ 都没有 → 英文 + default(D28:纯英文环境不用管)
|
||||
(follow, ¬hing, &env_none, Lang::En, ORIGIN_DEFAULT),
|
||||
];
|
||||
|
||||
for (choice, platform, env, lang, origin) in table {
|
||||
let detection = choice.resolve(*platform, *env);
|
||||
assert_eq!(
|
||||
(detection.lang, detection.origin),
|
||||
(*lang, *origin),
|
||||
"{choice:?} platform={:?} env={:?}",
|
||||
platform.locale(),
|
||||
env.locale()
|
||||
);
|
||||
}
|
||||
|
||||
// 手选档的 `raw` 是配置值(日志一眼看出「用户手选的」),跟随系统档的 `raw` 是系统原文。
|
||||
assert_eq!(
|
||||
zh.resolve(®istry_en, &env_en).describe(),
|
||||
"lang=zh-CN locale=zh_cn source=setting"
|
||||
);
|
||||
assert_eq!(
|
||||
follow.resolve(®istry_zh, &env_en).describe(),
|
||||
"lang=zh-CN locale=zh-CN source=registry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_never_needs_a_chinese_font_and_chinese_always_resolves_one() {
|
||||
// D28 字体那一级的真值表(`font::decide` 是纯函数,这里把语言 × 系统字体库全组合扫一遍):
|
||||
// 抓得住:①英文环境被接上中文字体/下载(「零额外体积/零网络」的直接违约);②中文环境扫不到
|
||||
// 字体却不给下载信号(会白块);③扫到了却不用系统字体(会白下 20 MB)。
|
||||
// 抓不住:本机到底装没装这些族(`app-iced` 的 `fonts/cjk.rs` 把真字体库喂进来)。
|
||||
use crate::font::{self, Strategy, CJK_FAMILIES, NOTO_SANS_SC};
|
||||
|
||||
let has_font =
|
||||
|families: &'static [&'static str]| move |family: &str| families.contains(&family);
|
||||
|
||||
for installed in [
|
||||
has_font(&[]),
|
||||
has_font(&["Segoe UI"]),
|
||||
has_font(CJK_FAMILIES),
|
||||
] {
|
||||
assert_eq!(
|
||||
font::decide(Lang::En, &installed),
|
||||
Strategy::NotNeeded,
|
||||
"英文环境不许要中文字体"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
font::decide(Lang::ZhCn, &has_font(&["Segoe UI"])),
|
||||
Strategy::NeedsDownload(&NOTO_SANS_SC)
|
||||
);
|
||||
let first = CJK_FAMILIES[0];
|
||||
let installed = has_font(&["Segoe UI", "Microsoft YaHei UI"]);
|
||||
assert_eq!(first, "Microsoft YaHei UI");
|
||||
assert_eq!(
|
||||
font::decide(Lang::ZhCn, &installed),
|
||||
Strategy::System { family: first }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_choice_has_a_label_in_both_catalogs() {
|
||||
// 抓得住:设置页语言行接了一个 catalog 里不存在的 key(画出来是 `⟪missing:…⟫`)。
|
||||
// 抓不住:译文翻得对不对(金表 + `chinese_catalog_is_actually_chinese` 管那一层)。
|
||||
let mut keys = Vec::new();
|
||||
for choice in LangChoice::ALL {
|
||||
let key = choice.label_key();
|
||||
for lang in Lang::ALL {
|
||||
assert!(
|
||||
lang.catalog().get(key).is_some(),
|
||||
"{key:?} 在 {} 目录里缺词",
|
||||
lang.tag()
|
||||
);
|
||||
}
|
||||
keys.push(key);
|
||||
}
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
assert_eq!(keys.len(), LangChoice::ALL.len(), "两档用了同一个 key");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
//! | 模块 | 职责 | 不负责 |
|
||||
//! |---|---|---|
|
||||
//! | [`lang`] | `Lang`(en / zh-CN)+ locale 探测(源可注入)+ **四级优先级** [`lang::resolve`] | 读注册表 / 读配置 / spawn 进程 |
|
||||
//! | [`choice`] | `LangChoice`(跟随系统 / 手选):设置页那一档 + 它到生效语言的映射 | 落盘(那是 `presenter::settings_bridge`) |
|
||||
//! | [`catalog`] | `Key` 枚举 + 中英两套常量表 + 取词 / 占位符渲染 + 缺译诊断 | 排版、字号、断行 |
|
||||
//! | [`font`] | 中文环境扫系统 CJK 字体的**判据** + 缺字体时的「需下载」信号 + 注入式下载器接口 | 联网、写文件、装字体 |
|
||||
//! | [`bridge`] | core / presenter 的状态串 → `Key` 的**示例契约**(文案边界规则全文在这里) | 改 core 的产出 |
|
||||
@@ -35,8 +36,10 @@
|
||||
|
||||
pub mod bridge;
|
||||
pub mod catalog;
|
||||
pub mod choice;
|
||||
pub mod font;
|
||||
pub mod lang;
|
||||
|
||||
pub use catalog::{Catalog, Key};
|
||||
pub use choice::LangChoice;
|
||||
pub use lang::Lang;
|
||||
Reference in new issue
Block a user