From def4095e4e2202edef95e08b6cfdb12d93f1e1fe Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 30 Oct 2025 17:14:59 +0800 Subject: [PATCH] feat(i18n): add internationalization support for tray menu - Implement TrayTexts struct to manage multilingual tray menu text - Auto-refresh tray menu when language settings change - Add missing notification message translations - Format code for consistency --- src-tauri/src/commands/provider.rs | 10 ++------ src-tauri/src/lib.rs | 40 +++++++++++++++++++++++++----- src-tauri/tests/app_type_parse.rs | 5 +++- src/hooks/useSettings.ts | 8 +++++- src/i18n/locales/en.json | 16 ++++++++++-- src/i18n/locales/zh.json | 16 ++++++++++-- 6 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 6c76e19..23daba9 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -25,10 +25,7 @@ pub fn get_providers( /// 获取当前供应商ID #[tauri::command] -pub fn get_current_provider( - state: State<'_, AppState>, - app: String, -) -> Result { +pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::current(state.inner(), app_type).map_err(|e| e.to_string()) } @@ -108,10 +105,7 @@ pub fn import_default_config_test_hook( /// 导入当前配置为默认供应商 #[tauri::command] -pub fn import_default_config( - state: State<'_, AppState>, - app: String, -) -> Result { +pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; import_default_config_internal(&state, app_type) .map(|_| true) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9999b90..e9ff51c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,18 +35,46 @@ use tauri::{ use tauri::{ActivationPolicy, RunEvent}; use tauri::{Emitter, Manager}; +#[derive(Clone, Copy)] +struct TrayTexts { + show_main: &'static str, + no_provider_hint: &'static str, + quit: &'static str, +} + +impl TrayTexts { + fn from_language(language: &str) -> Self { + match language { + "en" => Self { + show_main: "Open main window", + no_provider_hint: " (No providers yet, please add them from the main window)", + quit: "Quit", + }, + _ => Self { + show_main: "打开主界面", + no_provider_hint: " (无供应商,请在主界面添加)", + quit: "退出", + }, + } + } +} + /// 创建动态托盘菜单 fn create_tray_menu( app: &tauri::AppHandle, 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 config = app_state.config.read().map_err(AppError::from)?; let mut menu_builder = MenuBuilder::new(app); // 顶部:打开主界面 - let show_main_item = MenuItem::with_id(app, "show_main", "打开主界面", true, None::<&str>) - .map_err(|e| AppError::Message(format!("创建打开主界面菜单失败: {}", e)))?; + let show_main_item = + MenuItem::with_id(app, "show_main", tray_texts.show_main, true, None::<&str>) + .map_err(|e| AppError::Message(format!("创建打开主界面菜单失败: {}", e)))?; menu_builder = menu_builder.item(&show_main_item).separator(); // 直接添加所有供应商到主菜单(扁平化结构,更简单可靠) @@ -97,7 +125,7 @@ fn create_tray_menu( let empty_hint = MenuItem::with_id( app, "claude_empty", - " (无供应商,请在主界面添加)", + tray_texts.no_provider_hint, false, None::<&str>, ) @@ -153,7 +181,7 @@ fn create_tray_menu( let empty_hint = MenuItem::with_id( app, "codex_empty", - " (无供应商,请在主界面添加)", + tray_texts.no_provider_hint, false, None::<&str>, ) @@ -163,7 +191,7 @@ fn create_tray_menu( } // 分隔符和退出菜单 - let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>) + let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>) .map_err(|e| AppError::Message(format!("创建退出菜单失败: {}", e)))?; menu_builder = menu_builder.separator().item(&quit_item); @@ -268,7 +296,7 @@ fn switch_provider_internal( let provider_id_clone = provider_id.clone(); crate::commands::switch_provider(app_state.clone(), app_type_str.clone(), provider_id) - .map_err(AppError::Message)?; + .map_err(AppError::Message)?; // 切换成功后重新创建托盘菜单 if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) { diff --git a/src-tauri/tests/app_type_parse.rs b/src-tauri/tests/app_type_parse.rs index c5f412a..4abd316 100644 --- a/src-tauri/tests/app_type_parse.rs +++ b/src-tauri/tests/app_type_parse.rs @@ -6,7 +6,10 @@ use cc_switch_lib::AppType; fn parse_known_apps_case_insensitive_and_trim() { assert!(matches!(AppType::from_str("claude"), Ok(AppType::Claude))); assert!(matches!(AppType::from_str("codex"), Ok(AppType::Codex))); - assert!(matches!(AppType::from_str(" ClAuDe \n"), Ok(AppType::Claude))); + assert!(matches!( + AppType::from_str(" ClAuDe \n"), + Ok(AppType::Claude) + )); assert!(matches!(AppType::from_str("\tcoDeX\t"), Ok(AppType::Codex))); } diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 7934aa6..c02a442 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { settingsApi, type AppId } from "@/lib/api"; +import { providersApi, settingsApi, type AppId } from "@/lib/api"; import { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query"; import type { Settings } from "@/types"; import { useSettingsForm, type SettingsFormState } from "./useSettingsForm"; @@ -161,6 +161,12 @@ export function useSettings(): UseSettingsResult { ); } + try { + await providersApi.updateTrayMenu(); + } catch (error) { + console.warn("[useSettings] Failed to refresh tray menu", error); + } + const appDirChanged = sanitizedAppDir !== (previousAppDir ?? undefined); setRequiresRestart(appDirChanged); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a8d5fa5..e7c62a7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -84,19 +84,30 @@ "configJsonHint": "Please fill in complete Claude Code configuration", "editCommonConfigTitle": "Edit common config snippet", "editCommonConfigHint": "Common config snippet will be merged into all providers that enable it", - "addProvider": "Add Provider" + "addProvider": "Add Provider", + "sortUpdated": "Sort order updated", + "usageSaved": "Usage query configuration saved", + "usageSaveFailed": "Failed to save usage query configuration" }, "notifications": { + "providerAdded": "Provider added", "providerSaved": "Provider configuration saved", "providerDeleted": "Provider deleted successfully", "switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect", "switchFailed": "Switch failed, please check configuration", "autoImported": "Default provider created from existing configuration", + "addFailed": "Failed to add provider: {{error}}", "saveFailed": "Save failed: {{error}}", "saveFailedGeneric": "Save failed, please try again", "appliedToClaudePlugin": "Applied to Claude plugin", "removedFromClaudePlugin": "Removed from Claude plugin", - "syncClaudePluginFailed": "Sync Claude plugin failed" + "syncClaudePluginFailed": "Sync Claude plugin failed", + "updateSuccess": "Provider updated successfully", + "updateFailed": "Failed to update provider: {{error}}", + "deleteSuccess": "Provider deleted", + "deleteFailed": "Failed to delete provider: {{error}}", + "settingsSaved": "Settings saved", + "settingsSaveFailed": "Failed to save settings: {{error}}" }, "confirm": { "deleteProvider": "Delete Provider", @@ -167,6 +178,7 @@ "releaseNotes": "Release Notes", "viewReleaseNotes": "View release notes for this version", "viewCurrentReleaseNotes": "View current version release notes", + "importFailedError": "Import config failed: {{message}}", "exportFailedError": "Export config failed:", "restartRequired": "Restart Required", "restartRequiredMessage": "Modifying the CC-Switch configuration directory requires restarting the application to take effect. Restart now?", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 3c74648..5d2cae4 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -84,19 +84,30 @@ "configJsonHint": "请填写完整的 Claude Code 配置", "editCommonConfigTitle": "编辑通用配置片段", "editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中", - "addProvider": "添加供应商" + "addProvider": "添加供应商", + "sortUpdated": "排序已更新", + "usageSaved": "用量查询配置已保存", + "usageSaveFailed": "用量查询配置保存失败" }, "notifications": { + "providerAdded": "供应商已添加", "providerSaved": "供应商配置已保存", "providerDeleted": "供应商删除成功", "switchSuccess": "切换成功!请重启 {{appName}} 终端以生效", "switchFailed": "切换失败,请检查配置", "autoImported": "已从现有配置创建默认供应商", + "addFailed": "添加供应商失败:{{error}}", "saveFailed": "保存失败:{{error}}", "saveFailedGeneric": "保存失败,请重试", "appliedToClaudePlugin": "已应用到 Claude 插件", "removedFromClaudePlugin": "已从 Claude 插件移除", - "syncClaudePluginFailed": "同步 Claude 插件失败" + "syncClaudePluginFailed": "同步 Claude 插件失败", + "updateSuccess": "供应商更新成功", + "updateFailed": "更新供应商失败:{{error}}", + "deleteSuccess": "供应商已删除", + "deleteFailed": "删除供应商失败:{{error}}", + "settingsSaved": "设置已保存", + "settingsSaveFailed": "保存设置失败:{{error}}" }, "confirm": { "deleteProvider": "删除供应商", @@ -167,6 +178,7 @@ "releaseNotes": "更新日志", "viewReleaseNotes": "查看该版本更新日志", "viewCurrentReleaseNotes": "查看当前版本更新日志", + "importFailedError": "导入配置失败:{{message}}", "exportFailedError": "导出配置失败:", "restartRequired": "需要重启应用", "restartRequiredMessage": "修改 CC-Switch 配置目录后需要重启应用才能生效,是否立即重启?",