//! `[env]` 与 `[i18n]`:**本机是什么**(系统 / arch / 核数 / GPU / 时区)与**为什么是这个语言**。 //! //! ## 语言链为什么在这里而不是 `platform/` //! //! `platform::locale::SystemLocale` 是 **Windows 注册表**那一路(`reg.exe`,零 FFI)。macOS 的 //! 真值源是 `defaults read -g AppleLanguages`,而 M98 的 scope 没有 `platform/**`,所以 //! [`AppleLanguages`] 先落在这里。**它该搬去 `platform/locale.rs`**——两处并存会漂(见交接报告)。 use unlucky_i18n::lang::{self, Detection, LocaleSource}; use crate::Probe; // ---------------------------------------------------------------- [env] /// `[env]`:系统 / arch / 核数 / 语言 / 时区 / 缩放请求 / GPU。 /// /// `gpu` 与 `hardware` **来自真实适配器枚举**([`Probe`]),不是请求值;枚举不到就写 /// `<无适配器>` 并把 `hardware=false` 一起打出来——「没有适配器」与「没枚举」是两件事。 /// /// `display=` 恒为 `needs-window`:显示器尺寸只有拿到窗口句柄才问得到,而这一行在开窗之前就打了。 /// 真值由验收脚本用 `system_profiler SPDisplaysDataType` 采进 `logs/00-env.txt`——**写个猜的值 /// 比留白更坏**(那会让「Monitor 4K / 无外接」这种结论变成猜测)。 #[must_use] pub fn env_line(probe: &Probe, language: &str, scale: Option, tz: Option<&str>) -> String { format!( "[env] os={} arch={} cores={} language={language} tz={} scale_requested={} display=needs-window \ adapters={} hardware={} gpu=\"{}\"", std::env::consts::OS, std::env::consts::ARCH, std::thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get), tz.unwrap_or("<未设置>"), scale.map_or_else(|| "system".to_owned(), |value| value.to_string()), probe.adapters.len(), probe.hardware, gpu_summary(probe), ) } /// 枚举结果里**最该被当成 GPU** 的那个适配器的摘要:`DiscreteGpu > IntegratedGpu > 其它`。 /// /// 为什么不是「第一个非 Cpu/Other」(M98 r1 p3-2):本机枚举顺序是核显在前,于是 `[env] gpu=` /// 报的是 `AMD Radeon(TM) Graphics [IntegratedGpu]`,而 iced 实际选的是 /// `AMD Radeon RX 6600 XT [DiscreteGpu]`。一行「机器有什么显卡」的日志报错型号,比不报更坏。 /// 判据仍然只管**型号名称**:真正的「iced 选了谁」只有 `[gpu] …Selected: AdapterInfo` 那行有。 #[must_use] pub fn gpu_summary(probe: &Probe) -> String { probe .adapters .iter() .max_by_key(|adapter| gpu_rank(adapter.device_type)) .map_or_else( || "<无适配器>".to_owned(), |adapter| format!("{} [{}]", adapter.name, adapter.device_type), ) } /// 适配器的「该不该被当成这台机器的显卡」排序权重(越大越该)。 fn gpu_rank(device_type: &str) -> u8 { match device_type { "DiscreteGpu" => 3, "IntegratedGpu" => 2, "VirtualGpu" => 1, "Cpu" | "Other" => 0, _ => 1, } } /// 时区(`TZ` 环境变量)。`std` 拿不到系统时区,所以这里**只报环境变量**,没有就写未设置。 /// /// macOS 上的真值时区由脚本用 `date +%Z%z` 采(那份是观测值,这个只是进程视角)。 #[must_use] pub fn tz() -> Option { std::env::var("TZ") .ok() .filter(|value| !value.trim().is_empty()) } // ---------------------------------------------------------------- [i18n] /// 语言决策链的四级(顺序即优先级:显式值 > 平台源 > 环境变量 > 英文)。 /// /// 每一级的**原始值**都留着,日志才能解释「为什么是英文」——只打结论的话,macOS 上「英文」 /// 到底是「用户就是英文」还是「一个源都没取到」根本分不出来。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct Chain { /// `ui.language` 的显式值(当前没有读取点,恒 `None`)。 pub setting: Option, /// (源自称的名字, 原始 locale)。 pub platform: (&'static str, Option), /// (源自称的名字, 原始 locale)。 pub env: (&'static str, Option), /// 结论(由 [`lang::resolve`] 算,不在本模块重写一遍优先级)。 pub detection: Detection, } impl Chain { /// 用三个源探测一次。**每个源只读一次**(`defaults` / `reg.exe` 都是子进程)。 #[must_use] pub fn read( setting: Option<&str>, platform: &dyn LocaleSource, env: &dyn LocaleSource, ) -> Self { let platform = Captured { label: platform.label(), raw: platform.locale(), }; let env = Captured { label: env.label(), raw: env.locale(), }; // 优先级仍由 `resolve` 单点决定:这里把**已读到的值**包成同标签的源喂回去, // 不复制一遍 if/else(复制就会漂)。 let detection = lang::resolve(setting, &platform, &env); Self { setting: setting.map(str::to_owned), platform: (platform.label, platform.raw), env: (env.label, env.raw), detection, } } /// 本机(平台源 + 环境变量;配置未接线)。 #[must_use] pub fn host() -> Self { Self::read(None, platform_locale_source(), &lang::EnvLocale) } /// 结论语言标签(`zh-CN` / `en`)。 #[must_use] pub fn language(&self) -> &'static str { self.detection.lang.tag() } /// `[i18n]` 一行:四级链 + 结论。 /// /// `ui_text=pinned-en` 是**今天的事实**:`pages/mod.rs::render` 里下载页固定传 `Lang::En`、 /// 日志页传 `model.lang`(默认也是 `En`),也就是**文案还没接这条结论**。不写这一格的话, /// 日志会让人以为「语言已经是中文了」——那是新的假绿。 #[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("<未接线>"), self.platform.0, show(self.platform.1.as_deref()), self.env.0, show(self.env.1.as_deref()), self.detection.describe() ) } } fn show(value: Option<&str>) -> String { value.map_or_else(|| "<未设置>".to_owned(), |raw| raw.to_owned()) } /// 把值钉住的源(为了「读一次、按真优先级算」)。 struct Captured { label: &'static str, raw: Option, } impl LocaleSource for Captured { fn locale(&self) -> Option { self.raw.clone() } fn label(&self) -> &'static str { self.label } } /// 本平台的「系统语言」真值源。 /// /// - Windows:`reg.exe` 读注册表([`crate::platform::locale::SystemLocale`]); /// - macOS:[`AppleLanguages`](`defaults read -g AppleLanguages`); /// - 其它:没有平台源(`resolve` 会继续往环境变量走)。 /// /// 为什么 macOS 不复用 `SystemLocale`:它 `cfg!(windows)` 之外恒返回 `None`,在 Mac 上等于 /// 「没有平台源」——于是语言只剩环境变量一级,而 GUI 程序从 Finder 启动时 `LANG` 通常是空的。 #[must_use] pub fn platform_locale_source() -> &'static dyn LocaleSource { #[cfg(target_os = "macos")] { &AppleLanguages } #[cfg(not(target_os = "macos"))] { &crate::platform::locale::SystemLocale } } /// macOS 的系统语言源:`defaults read -g AppleLanguages` 的第一项。 #[derive(Debug, Clone, Copy, Default)] pub struct AppleLanguages; impl LocaleSource for AppleLanguages { fn locale(&self) -> Option { #[cfg(target_os = "macos")] { let output = std::process::Command::new("defaults") .args(["read", "-g", "AppleLanguages"]) .output() .ok()?; if !output.status.success() { return None; } parse_apple_languages(&String::from_utf8_lossy(&output.stdout)) } #[cfg(not(target_os = "macos"))] { None } } fn label(&self) -> &'static str { "applelanguages" } } /// 解析 `defaults` 的输出,取**第一条**语言(真机格式见测试夹具)。 /// /// ```text /// ( /// "zh-Hans-CN", /// "en-US" /// ) /// ``` /// /// 首项是用户的**首选语言**;后面几项是「语言与地区」里的备选,与本 crate「只看主语言子标签」 /// 的口径无关。 #[must_use] pub fn parse_apple_languages(text: &str) -> Option { let start = text.find('"')?; let rest = &text[start + 1..]; let end = rest.find('"')?; let value = rest[..end].trim(); (!value.is_empty()).then(|| value.to_owned()) } #[cfg(test)] #[path = "locale_tests.rs"] mod tests;