From c5aa244d65bd8616717b4defbc8262933be3c60b Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 28 Sep 2025 22:23:49 +0800 Subject: [PATCH] feat: integrate language switcher into settings with modern segment control UI - Move language switcher from header to settings modal for better organization - Implement modern segment control UI instead of radio buttons for language selection - Add language preference persistence in localStorage and backend settings - Support instant language preview with cancel/revert functionality - Remove standalone LanguageSwitcher component - Improve initial language detection logic (localStorage -> browser -> default) - Add proper i18n keys for language settings UI text --- src-tauri/src/settings.rs | 10 +++ src/App.tsx | 2 - src/components/LanguageSwitcher.tsx | 31 -------- src/components/SettingsModal.tsx | 108 ++++++++++++++++++++++++++-- src/i18n/index.ts | 32 ++++++++- src/i18n/locales/en.json | 5 ++ src/i18n/locales/zh.json | 5 ++ src/types.ts | 2 + 8 files changed, 157 insertions(+), 38 deletions(-) delete mode 100644 src/components/LanguageSwitcher.tsx diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 3fbe938..defc88f 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -15,6 +15,8 @@ pub struct AppSettings { pub claude_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub codex_config_dir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub language: Option, } fn default_show_in_tray() -> bool { @@ -32,6 +34,7 @@ impl Default for AppSettings { minimize_to_tray_on_close: true, claude_config_dir: None, codex_config_dir: None, + language: None, } } } @@ -55,6 +58,13 @@ impl AppSettings { .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| s.to_string()); + + self.language = self + .language + .as_ref() + .map(|s| s.trim()) + .filter(|s| matches!(*s, "en" | "zh")) + .map(|s| s.to_string()); } pub fn load() -> Self { diff --git a/src/App.tsx b/src/App.tsx index f709f3d..b405166 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,6 @@ import { ConfirmDialog } from "./components/ConfirmDialog"; import { AppSwitcher } from "./components/AppSwitcher"; import SettingsModal from "./components/SettingsModal"; import { UpdateBadge } from "./components/UpdateBadge"; -import LanguageSwitcher from "./components/LanguageSwitcher"; import { Plus, Settings, Moon, Sun } from "lucide-react"; import { buttonStyles } from "./lib/styles"; import { useDarkMode } from "./hooks/useDarkMode"; @@ -308,7 +307,6 @@ function App() { > {isDarkMode ? : } -
- ); -}; - -export default LanguageSwitcher; diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 08e7a93..de7684a 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -25,13 +25,33 @@ interface SettingsModalProps { } export default function SettingsModal({ onClose }: SettingsModalProps) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + + const normalizeLanguage = (lang?: string | null): "zh" | "en" => + lang === "en" ? "en" : "zh"; + + const readPersistedLanguage = (): "zh" | "en" => { + if (typeof window !== "undefined") { + const stored = window.localStorage.getItem("language"); + if (stored === "en" || stored === "zh") { + return stored; + } + } + return normalizeLanguage(i18n.language); + }; + + const persistedLanguage = readPersistedLanguage(); + const [settings, setSettings] = useState({ showInTray: true, minimizeToTrayOnClose: true, claudeConfigDir: undefined, codexConfigDir: undefined, + language: persistedLanguage, }); + const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">( + persistedLanguage, + ); const [configPath, setConfigPath] = useState(""); const [version, setVersion] = useState(""); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); @@ -73,6 +93,12 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { (loadedSettings as any)?.minimizeToTrayOnClose ?? (loadedSettings as any)?.minimize_to_tray_on_close ?? true; + const storedLanguage = normalizeLanguage( + typeof (loadedSettings as any)?.language === "string" + ? (loadedSettings as any).language + : persistedLanguage, + ); + setSettings({ showInTray, minimizeToTrayOnClose, @@ -84,7 +110,12 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { typeof (loadedSettings as any)?.codexConfigDir === "string" ? (loadedSettings as any).codexConfigDir : undefined, + language: storedLanguage, }); + setInitialLanguage(storedLanguage); + if (i18n.language !== storedLanguage) { + void i18n.changeLanguage(storedLanguage); + } } catch (error) { console.error(t("console.loadSettingsFailed"), error); } @@ -125,6 +156,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const saveSettings = async () => { try { + const selectedLanguage = settings.language === "en" ? "en" : "zh"; const payload: Settings = { ...settings, claudeConfigDir: @@ -135,15 +167,42 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { settings.codexConfigDir && settings.codexConfigDir.trim() !== "" ? settings.codexConfigDir.trim() : undefined, + language: selectedLanguage, }; await window.api.saveSettings(payload); setSettings(payload); + try { + window.localStorage.setItem("language", selectedLanguage); + } catch (error) { + console.warn("[Settings] Failed to persist language preference", error); + } + setInitialLanguage(selectedLanguage); + if (i18n.language !== selectedLanguage) { + void i18n.changeLanguage(selectedLanguage); + } onClose(); } catch (error) { console.error(t("console.saveSettingsFailed"), error); } }; + const handleLanguageChange = (lang: "zh" | "en") => { + setSettings((prev) => ({ ...prev, language: lang })); + if (i18n.language !== lang) { + void i18n.changeLanguage(lang); + } + }; + + const handleCancel = () => { + if (settings.language !== initialLanguage) { + setSettings((prev) => ({ ...prev, language: initialLanguage })); + if (i18n.language !== initialLanguage) { + void i18n.changeLanguage(initialLanguage); + } + } + onClose(); + }; + const handleCheckUpdate = async () => { if (hasUpdate && updateHandle) { if (isPortable) { @@ -291,7 +350,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{ - if (e.target === e.currentTarget) onClose(); + if (e.target === e.currentTarget) handleCancel(); }} >
+ +
+
+
+ + {/* 窗口行为设置 */}

@@ -534,7 +634,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { {/* 底部按钮 */}