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
This commit is contained in:
Jason
2025-10-30 17:14:59 +08:00
parent b3e14b3985
commit def4095e4e
6 changed files with 75 additions and 20 deletions

View File

@@ -25,10 +25,7 @@ pub fn get_providers(
/// 获取当前供应商ID /// 获取当前供应商ID
#[tauri::command] #[tauri::command]
pub fn get_current_provider( pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result<String, String> {
state: State<'_, AppState>,
app: String,
) -> Result<String, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; 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()) 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] #[tauri::command]
pub fn import_default_config( pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
state: State<'_, AppState>,
app: String,
) -> Result<bool, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
import_default_config_internal(&state, app_type) import_default_config_internal(&state, app_type)
.map(|_| true) .map(|_| true)

View File

@@ -35,18 +35,46 @@ use tauri::{
use tauri::{ActivationPolicy, RunEvent}; use tauri::{ActivationPolicy, RunEvent};
use tauri::{Emitter, Manager}; 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( fn create_tray_menu(
app: &tauri::AppHandle, app: &tauri::AppHandle,
app_state: &AppState, app_state: &AppState,
) -> Result<Menu<tauri::Wry>, AppError> { ) -> Result<Menu<tauri::Wry>, 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 config = app_state.config.read().map_err(AppError::from)?;
let mut menu_builder = MenuBuilder::new(app); let mut menu_builder = MenuBuilder::new(app);
// 顶部:打开主界面 // 顶部:打开主界面
let show_main_item = MenuItem::with_id(app, "show_main", "打开主界面", true, None::<&str>) let show_main_item =
.map_err(|e| AppError::Message(format!("创建打开主界面菜单失败: {}", e)))?; 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(); menu_builder = menu_builder.item(&show_main_item).separator();
// 直接添加所有供应商到主菜单(扁平化结构,更简单可靠) // 直接添加所有供应商到主菜单(扁平化结构,更简单可靠)
@@ -97,7 +125,7 @@ fn create_tray_menu(
let empty_hint = MenuItem::with_id( let empty_hint = MenuItem::with_id(
app, app,
"claude_empty", "claude_empty",
" (无供应商,请在主界面添加)", tray_texts.no_provider_hint,
false, false,
None::<&str>, None::<&str>,
) )
@@ -153,7 +181,7 @@ fn create_tray_menu(
let empty_hint = MenuItem::with_id( let empty_hint = MenuItem::with_id(
app, app,
"codex_empty", "codex_empty",
" (无供应商,请在主界面添加)", tray_texts.no_provider_hint,
false, false,
None::<&str>, 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)))?; .map_err(|e| AppError::Message(format!("创建退出菜单失败: {}", e)))?;
menu_builder = menu_builder.separator().item(&quit_item); menu_builder = menu_builder.separator().item(&quit_item);
@@ -268,7 +296,7 @@ fn switch_provider_internal(
let provider_id_clone = provider_id.clone(); let provider_id_clone = provider_id.clone();
crate::commands::switch_provider(app_state.clone(), app_type_str.clone(), provider_id) 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()) { if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) {

View File

@@ -6,7 +6,10 @@ use cc_switch_lib::AppType;
fn parse_known_apps_case_insensitive_and_trim() { fn parse_known_apps_case_insensitive_and_trim() {
assert!(matches!(AppType::from_str("claude"), Ok(AppType::Claude))); assert!(matches!(AppType::from_str("claude"), Ok(AppType::Claude)));
assert!(matches!(AppType::from_str("codex"), Ok(AppType::Codex))); 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))); assert!(matches!(AppType::from_str("\tcoDeX\t"), Ok(AppType::Codex)));
} }

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; 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 { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query";
import type { Settings } from "@/types"; import type { Settings } from "@/types";
import { useSettingsForm, type SettingsFormState } from "./useSettingsForm"; 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); const appDirChanged = sanitizedAppDir !== (previousAppDir ?? undefined);
setRequiresRestart(appDirChanged); setRequiresRestart(appDirChanged);

View File

@@ -84,19 +84,30 @@
"configJsonHint": "Please fill in complete Claude Code configuration", "configJsonHint": "Please fill in complete Claude Code configuration",
"editCommonConfigTitle": "Edit common config snippet", "editCommonConfigTitle": "Edit common config snippet",
"editCommonConfigHint": "Common config snippet will be merged into all providers that enable it", "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": { "notifications": {
"providerAdded": "Provider added",
"providerSaved": "Provider configuration saved", "providerSaved": "Provider configuration saved",
"providerDeleted": "Provider deleted successfully", "providerDeleted": "Provider deleted successfully",
"switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect", "switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect",
"switchFailed": "Switch failed, please check configuration", "switchFailed": "Switch failed, please check configuration",
"autoImported": "Default provider created from existing configuration", "autoImported": "Default provider created from existing configuration",
"addFailed": "Failed to add provider: {{error}}",
"saveFailed": "Save failed: {{error}}", "saveFailed": "Save failed: {{error}}",
"saveFailedGeneric": "Save failed, please try again", "saveFailedGeneric": "Save failed, please try again",
"appliedToClaudePlugin": "Applied to Claude plugin", "appliedToClaudePlugin": "Applied to Claude plugin",
"removedFromClaudePlugin": "Removed from 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": { "confirm": {
"deleteProvider": "Delete Provider", "deleteProvider": "Delete Provider",
@@ -167,6 +178,7 @@
"releaseNotes": "Release Notes", "releaseNotes": "Release Notes",
"viewReleaseNotes": "View release notes for this version", "viewReleaseNotes": "View release notes for this version",
"viewCurrentReleaseNotes": "View current version release notes", "viewCurrentReleaseNotes": "View current version release notes",
"importFailedError": "Import config failed: {{message}}",
"exportFailedError": "Export config failed:", "exportFailedError": "Export config failed:",
"restartRequired": "Restart Required", "restartRequired": "Restart Required",
"restartRequiredMessage": "Modifying the CC-Switch configuration directory requires restarting the application to take effect. Restart now?", "restartRequiredMessage": "Modifying the CC-Switch configuration directory requires restarting the application to take effect. Restart now?",

View File

@@ -84,19 +84,30 @@
"configJsonHint": "请填写完整的 Claude Code 配置", "configJsonHint": "请填写完整的 Claude Code 配置",
"editCommonConfigTitle": "编辑通用配置片段", "editCommonConfigTitle": "编辑通用配置片段",
"editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中", "editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中",
"addProvider": "添加供应商" "addProvider": "添加供应商",
"sortUpdated": "排序已更新",
"usageSaved": "用量查询配置已保存",
"usageSaveFailed": "用量查询配置保存失败"
}, },
"notifications": { "notifications": {
"providerAdded": "供应商已添加",
"providerSaved": "供应商配置已保存", "providerSaved": "供应商配置已保存",
"providerDeleted": "供应商删除成功", "providerDeleted": "供应商删除成功",
"switchSuccess": "切换成功!请重启 {{appName}} 终端以生效", "switchSuccess": "切换成功!请重启 {{appName}} 终端以生效",
"switchFailed": "切换失败,请检查配置", "switchFailed": "切换失败,请检查配置",
"autoImported": "已从现有配置创建默认供应商", "autoImported": "已从现有配置创建默认供应商",
"addFailed": "添加供应商失败:{{error}}",
"saveFailed": "保存失败:{{error}}", "saveFailed": "保存失败:{{error}}",
"saveFailedGeneric": "保存失败,请重试", "saveFailedGeneric": "保存失败,请重试",
"appliedToClaudePlugin": "已应用到 Claude 插件", "appliedToClaudePlugin": "已应用到 Claude 插件",
"removedFromClaudePlugin": "已从 Claude 插件移除", "removedFromClaudePlugin": "已从 Claude 插件移除",
"syncClaudePluginFailed": "同步 Claude 插件失败" "syncClaudePluginFailed": "同步 Claude 插件失败",
"updateSuccess": "供应商更新成功",
"updateFailed": "更新供应商失败:{{error}}",
"deleteSuccess": "供应商已删除",
"deleteFailed": "删除供应商失败:{{error}}",
"settingsSaved": "设置已保存",
"settingsSaveFailed": "保存设置失败:{{error}}"
}, },
"confirm": { "confirm": {
"deleteProvider": "删除供应商", "deleteProvider": "删除供应商",
@@ -167,6 +178,7 @@
"releaseNotes": "更新日志", "releaseNotes": "更新日志",
"viewReleaseNotes": "查看该版本更新日志", "viewReleaseNotes": "查看该版本更新日志",
"viewCurrentReleaseNotes": "查看当前版本更新日志", "viewCurrentReleaseNotes": "查看当前版本更新日志",
"importFailedError": "导入配置失败:{{message}}",
"exportFailedError": "导出配置失败:", "exportFailedError": "导出配置失败:",
"restartRequired": "需要重启应用", "restartRequired": "需要重启应用",
"restartRequiredMessage": "修改 CC-Switch 配置目录后需要重启应用才能生效,是否立即重启?", "restartRequiredMessage": "修改 CC-Switch 配置目录后需要重启应用才能生效,是否立即重启?",