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:
@@ -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)
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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?",
|
||||||
|
|||||||
@@ -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 配置目录后需要重启应用才能生效,是否立即重启?",
|
||||||
|
|||||||
Reference in New Issue
Block a user