From 997be22bfa5d14161a6f5b1f805631054368cdb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BB=85=C3=BC?= <53787985+LaiYueTing@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:35:10 +0800 Subject: [PATCH] fix(tray): detect system locale for first-run language instead of hardcoding zh (#4355) The tray menu used a hardcoded zh (Simplified Chinese) fallback whenever settings.language was unset (i.e. first install). On systems whose UI language resolves to Traditional Chinese / Japanese / English via the frontend's navigator-based detection, the tray therefore showed Simplified Chinese until the user manually switched language once. Detect the OS locale via the sys-locale crate and map it to a supported tray language, mirroring the frontend getInitialLanguage precedence (zh-TW/HK/MO/Hant -> zh-TW, other zh -> zh, ja -> ja, en -> en, otherwise zh). This keeps the tray consistent with the UI from the first launch with no timing window. An explicitly stored settings.language still takes precedence, so user choices are never overridden. Adds unit tests for the locale mapping. Co-authored-by: LaiYueTing Co-authored-by: Jason --- src-tauri/Cargo.lock | 10 +++++ src-tauri/Cargo.toml | 1 + src-tauri/src/tray.rs | 96 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index eb60f4441..9d3f77a00 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -799,6 +799,7 @@ dependencies = [ "serde_yaml", "serial_test", "sha2", + "sys-locale", "tauri", "tauri-build", "tauri-plugin-deep-link", @@ -5438,6 +5439,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.7.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 93934cf8c..91599b0ea 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -81,6 +81,7 @@ sha2 = "0.10" hmac = "0.12" json5 = "0.4" json-five = "0.3.1" +sys-locale = "0.3" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index b33a00c54..741a5ebb6 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -58,6 +58,41 @@ pub struct TrayTexts { pub no_project_label: &'static str, } +/// 将系统区域标识映射为托盘支持的语言码。 +/// +/// 镜像前端 `i18n/getInitialLanguage` 的判定顺序,确保首次安装 +/// (`settings.language` 尚未写入)时托盘语言与界面语言一致: +/// 繁中系统(zh-TW/HK/MO/Hant)→ `zh-TW`,其余 zh → `zh`, +/// 日文 → `ja`,英文 → `en`,未知区域回退到 `zh`(与前端默认一致)。 +fn map_locale_to_tray_language(locale: &str) -> &'static str { + let locale = locale.to_lowercase(); + if locale == "zh" { + "zh" + } else if locale.starts_with("zh-tw") + || locale.starts_with("zh-hk") + || locale.starts_with("zh-mo") + || locale.starts_with("zh-hant") + { + "zh-TW" + } else if locale.starts_with("zh") { + "zh" + } else if locale.starts_with("ja") { + "ja" + } else if locale.starts_with("en") { + "en" + } else { + "zh" + } +} + +/// 读取系统区域并映射为托盘语言码;取不到区域时回退到 `zh`。 +fn detect_system_tray_language() -> &'static str { + sys_locale::get_locale() + .as_deref() + .map(map_locale_to_tray_language) + .unwrap_or("zh") +} + impl TrayTexts { pub fn from_language(language: &str) -> Self { match language { @@ -604,7 +639,13 @@ pub fn create_tray_menu( app_state: &AppState, ) -> Result, AppError> { let app_settings = crate::settings::get_settings(); - let tray_texts = TrayTexts::from_language(app_settings.language.as_deref().unwrap_or("zh")); + // 用户未显式设置语言(首次安装)时,按系统区域回退而非硬编码简体, + // 否则繁中系统的托盘会固定显示简体直到用户手动切换一次。 + let language: &str = match app_settings.language.as_deref() { + Some(lang) => lang, + None => detect_system_tray_language(), + }; + let tray_texts = TrayTexts::from_language(language); // Get visible apps setting, default to all visible let visible_apps = app_settings.visible_apps.unwrap_or_default(); @@ -1094,6 +1135,59 @@ mod tests { assert_ne!(TRAY_ID, "main"); } + #[test] + fn locale_maps_traditional_chinese_variants_to_zh_tw() { + use super::map_locale_to_tray_language; + for locale in [ + "zh-TW", + "zh-HK", + "zh-MO", + "zh-Hant", + "zh-Hant-TW", + "zh-hant-hk", + ] { + assert_eq!( + map_locale_to_tray_language(locale), + "zh-TW", + "expected {locale} -> zh-TW" + ); + } + } + + #[test] + fn locale_maps_simplified_chinese_variants_to_zh() { + use super::map_locale_to_tray_language; + for locale in ["zh", "zh-CN", "zh-SG", "zh-Hans", "zh-Hans-CN"] { + assert_eq!( + map_locale_to_tray_language(locale), + "zh", + "expected {locale} -> zh" + ); + } + } + + #[test] + fn locale_maps_japanese_and_english() { + use super::map_locale_to_tray_language; + assert_eq!(map_locale_to_tray_language("ja-JP"), "ja"); + assert_eq!(map_locale_to_tray_language("ja"), "ja"); + assert_eq!(map_locale_to_tray_language("en-US"), "en"); + assert_eq!(map_locale_to_tray_language("en"), "en"); + } + + #[test] + fn locale_unknown_falls_back_to_zh() { + use super::map_locale_to_tray_language; + // 与前端 getInitialLanguage 的默认值保持一致。 + for locale in ["de-DE", "fr", "ko-KR", ""] { + assert_eq!( + map_locale_to_tray_language(locale), + "zh", + "expected {locale} -> zh (default)" + ); + } + } + #[test] fn tray_sections_include_grokbuild_provider_switching() { let section = TRAY_SECTIONS