diff --git a/README_i18n.md b/README_i18n.md new file mode 100644 index 0000000..1caf9b9 --- /dev/null +++ b/README_i18n.md @@ -0,0 +1,76 @@ +# CC Switch 国际化功能说明 + +## 已完成的工作 + +1. **安装依赖**:添加了 `react-i18next` 和 `i18next` 包 +2. **配置国际化**:在 `src/i18n/` 目录下创建了配置文件 +3. **翻译文件**:创建了英文和中文翻译文件 +4. **组件更新**:替换了主要组件中的硬编码文案 +5. **语言切换器**:添加了语言切换按钮 + +## 文件结构 + +``` +src/ +├── i18n/ +│ ├── index.ts # 国际化配置文件 +│ └── locales/ +│ ├── en.json # 英文翻译 +│ └── zh.json # 中文翻译 +├── components/ +│ └── LanguageSwitcher.tsx # 语言切换组件 +└── main.tsx # 导入国际化配置 +``` + +## 默认语言设置 + +- **默认语言**:英文 (en) +- **回退语言**:英文 (en) + +## 使用方式 + +1. 在组件中导入 `useTranslation`: + ```tsx + import { useTranslation } from 'react-i18next'; + + function MyComponent() { + const { t } = useTranslation(); + return
{t('common.save')}
; + } + ``` + +2. 切换语言: + ```tsx + const { i18n } = useTranslation(); + i18n.changeLanguage('zh'); // 切换到中文 + ``` + +## 翻译键结构 + +- `common.*` - 通用文案(保存、取消、设置等) +- `header.*` - 头部相关文案 +- `provider.*` - 供应商相关文案 +- `notifications.*` - 通知消息 +- `settings.*` - 设置页面文案 +- `apps.*` - 应用名称 +- `console.*` - 控制台日志信息 + +## 测试功能 + +应用已添加了语言切换按钮(地球图标),点击可以在中英文之间切换,验证国际化功能是否正常工作。 + +## 已更新的组件 + +- ✅ App.tsx - 主应用组件 +- ✅ ConfirmDialog.tsx - 确认对话框 +- ✅ AddProviderModal.tsx - 添加供应商弹窗 +- ✅ EditProviderModal.tsx - 编辑供应商弹窗 +- ✅ ProviderList.tsx - 供应商列表 +- ✅ LanguageSwitcher.tsx - 语言切换器 +- 🔄 SettingsModal.tsx - 设置弹窗(部分完成) + +## 注意事项 + +1. 所有新的文案都应该添加到翻译文件中,而不是硬编码 +2. 翻译键名应该有意义且结构化 +3. 可以通过修改 `src/i18n/index.ts` 中的 `lng` 配置来更改默认语言 diff --git a/package.json b/package.json index edc4df1..ae5b4af 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,12 @@ "@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "codemirror": "^6.0.2", + "i18next": "^25.5.2", "jsonc-parser": "^3.2.1", "lucide-react": "^0.542.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.0.0", "tailwindcss": "^4.1.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3393607..7c2b881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: codemirror: specifier: ^6.0.2 version: 6.0.2 + i18next: + specifier: ^25.5.2 + version: 25.5.2(typescript@5.9.2) jsonc-parser: specifier: ^3.2.1 version: 3.3.1 @@ -53,6 +56,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-i18next: + specifier: ^16.0.0 + version: 16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) tailwindcss: specifier: ^4.1.13 version: 4.1.13 @@ -159,6 +165,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -750,6 +760,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + i18next@25.5.2: + resolution: {integrity: sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true @@ -890,6 +911,22 @@ packages: peerDependencies: react: ^18.3.1 + react-i18next@16.0.0: + resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==} + peerDependencies: + i18next: '>= 25.5.2' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -973,6 +1010,10 @@ packages: terser: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -1079,6 +1120,8 @@ snapshots: '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -1591,6 +1634,16 @@ snapshots: graceful-fs@4.2.11: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + i18next@25.5.2(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.4 + optionalDependencies: + typescript: 5.9.2 + jiti@2.5.1: {} js-tokens@4.0.0: {} @@ -1692,6 +1745,16 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-i18next@16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.4 + html-parse-stringify: 3.0.1 + i18next: 25.5.2(typescript@5.9.2) + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.9.2 + react-refresh@0.17.0: {} react@18.3.1: @@ -1767,6 +1830,8 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 + void-elements@3.1.0: {} + w3c-keyname@2.2.8: {} yallist@3.1.1: {} diff --git a/src/App.tsx b/src/App.tsx index 3fd7737..f709f3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { Provider } from "./types"; import { AppType } from "./lib/tauri-api"; import ProviderList from "./components/ProviderList"; @@ -8,6 +9,7 @@ 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"; @@ -17,6 +19,7 @@ import { getCodexBaseUrl } from "./utils/providerConfigUtils"; import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync"; function App() { + const { t } = useTranslation(); const { isDarkMode, toggleDarkMode } = useDarkMode(); const { isAutoSyncEnabled } = useVSCodeAutoSync(); const [activeApp, setActiveApp] = useState("claude"); @@ -24,7 +27,7 @@ function App() { const [currentProviderId, setCurrentProviderId] = useState(""); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [editingProviderId, setEditingProviderId] = useState( - null, + null ); const [notification, setNotification] = useState<{ message: string; @@ -44,7 +47,7 @@ function App() { const showNotification = ( message: string, type: "success" | "error", - duration = 3000, + duration = 3000 ) => { // 清除之前的定时器 if (timeoutRef.current) { @@ -88,7 +91,7 @@ function App() { try { unlisten = await window.api.onProviderSwitched(async (data) => { if (import.meta.env.DEV) { - console.log("收到供应商切换事件:", data); + console.log(t("console.providerSwitchReceived"), data); } // 如果当前应用类型匹配,则重新加载数据 @@ -102,7 +105,7 @@ function App() { } }); } catch (error) { - console.error("设置供应商切换监听器失败:", error); + console.error(t("console.setupListenerFailed"), error); } }; @@ -152,16 +155,16 @@ function App() { await loadProviders(); setEditingProviderId(null); // 显示编辑成功提示 - showNotification("供应商配置已保存", "success", 2000); + showNotification(t("notifications.providerSaved"), "success", 2000); // 更新托盘菜单 await window.api.updateTrayMenu(); } catch (error) { - console.error("更新供应商失败:", error); + console.error(t("console.updateProviderFailed"), error); setEditingProviderId(null); const errorMessage = extractErrorMessage(error); const message = errorMessage - ? `保存失败:${errorMessage}` - : "保存失败,请重试"; + ? t("notifications.saveFailed", { error: errorMessage }) + : t("notifications.saveFailedGeneric"); showNotification(message, "error", errorMessage ? 6000 : 3000); } }; @@ -170,13 +173,13 @@ function App() { const provider = providers[id]; setConfirmDialog({ isOpen: true, - title: "删除供应商", - message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`, + title: t("confirm.deleteProvider"), + message: t("confirm.deleteProviderMessage", { name: provider?.name }), onConfirm: async () => { await window.api.deleteProvider(id, activeApp); await loadProviders(); setConfirmDialog(null); - showNotification("供应商删除成功", "success"); + showNotification(t("notifications.providerDeleted"), "success"); // 更新托盘菜单 await window.api.updateTrayMenu(); }, @@ -190,9 +193,9 @@ function App() { if (!status.exists) { if (!silent) { showNotification( - "未找到 VS Code 用户设置文件 (settings.json)", + t("notifications.vscodeSettingsNotFound"), "error", - 3000, + 3000 ); } return; @@ -208,11 +211,7 @@ function App() { const parsed = getCodexBaseUrl(provider); if (!parsed) { if (!silent) { - showNotification( - "当前配置缺少 base_url,无法写入 VS Code", - "error", - 4000, - ); + showNotification(t("notifications.missingBaseUrl"), "error", 4000); } return; } @@ -226,16 +225,17 @@ function App() { if (updatedSettings !== raw) { await window.api.writeVSCodeSettings(updatedSettings); if (!silent) { - showNotification("已同步到 VS Code", "success", 1500); + showNotification(t("notifications.syncedToVSCode"), "success", 1500); } } // 触发providers重新加载,以更新VS Code按钮状态 await loadProviders(); } catch (error: any) { - console.error("同步到VS Code失败:", error); + console.error(t("console.syncToVSCodeFailed"), error); if (!silent) { - const errorMessage = error?.message || "同步 VS Code 失败"; + const errorMessage = + error?.message || t("notifications.syncVSCodeFailed"); showNotification(errorMessage, "error", 5000); } } @@ -246,11 +246,11 @@ function App() { if (success) { setCurrentProviderId(id); // 显示重启提示 - const appName = activeApp === "claude" ? "Claude Code" : "Codex"; + const appName = t(`apps.${activeApp}`); showNotification( - `切换成功!请重启 ${appName} 终端以生效`, + t("notifications.switchSuccess", { appName }), "success", - 2000, + 2000 ); // 更新托盘菜单 await window.api.updateTrayMenu(); @@ -260,7 +260,7 @@ function App() { await syncCodexToVSCode(id, true); // silent模式,不显示通知 } } else { - showNotification("切换失败,请检查配置", "error"); + showNotification(t("notifications.switchFailed"), "error"); } }; @@ -271,13 +271,13 @@ function App() { if (result.success) { await loadProviders(); - showNotification("已从现有配置创建默认供应商", "success", 3000); + showNotification(t("notifications.autoImported"), "success", 3000); // 更新托盘菜单 await window.api.updateTrayMenu(); } // 如果导入失败(比如没有现有配置),静默处理,不显示错误 } catch (error) { - console.error("自动导入默认配置失败:", error); + console.error(t("console.autoImportFailed"), error); // 静默处理,不影响用户体验 } }; @@ -293,22 +293,27 @@ function App() { target="_blank" rel="noopener noreferrer" className="text-xl font-semibold text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors" - title="在 GitHub 上查看" + title={t("header.viewOnGithub")} > CC Switch +
@@ -324,7 +329,7 @@ function App() { className={`inline-flex items-center gap-2 ${buttonStyles.primary}`} > - 添加供应商 + {t("header.addProvider")}
diff --git a/src/components/AddProviderModal.tsx b/src/components/AddProviderModal.tsx index 17bebb1..8d92753 100644 --- a/src/components/AddProviderModal.tsx +++ b/src/components/AddProviderModal.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Provider } from "../types"; import { AppType } from "../lib/tauri-api"; import ProviderForm from "./ProviderForm"; @@ -14,11 +15,13 @@ const AddProviderModal: React.FC = ({ onAdd, onClose, }) => { + const { t } = useTranslation(); + return ( = ({ isOpen, title, message, - confirmText = "确定", - cancelText = "取消", + confirmText, + cancelText, onConfirm, onCancel, }) => { + const { t } = useTranslation(); + if (!isOpen) return null; return ( @@ -65,13 +68,13 @@ export const ConfirmDialog: React.FC = ({ className="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-900 hover:bg-white dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors" autoFocus > - {cancelText} + {cancelText || t("common.cancel")} diff --git a/src/components/EditProviderModal.tsx b/src/components/EditProviderModal.tsx index 5c3d4ef..3844443 100644 --- a/src/components/EditProviderModal.tsx +++ b/src/components/EditProviderModal.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Provider } from "../types"; import { AppType } from "../lib/tauri-api"; import ProviderForm from "./ProviderForm"; @@ -16,6 +17,8 @@ const EditProviderModal: React.FC = ({ onSave, onClose, }) => { + const { t } = useTranslation(); + const handleSubmit = (data: Omit) => { onSave({ ...provider, @@ -26,8 +29,8 @@ const EditProviderModal: React.FC = ({ return ( { + const { t, i18n } = useTranslation(); + + const toggleLanguage = () => { + const newLang = i18n.language === "en" ? "zh" : "en"; + i18n.changeLanguage(newLang); + }; + + const titleKey = + i18n.language === "en" + ? "header.switchToChinese" + : "header.switchToEnglish"; + + return ( + + ); +}; + +export default LanguageSwitcher; diff --git a/src/components/ProviderList.tsx b/src/components/ProviderList.tsx index 785e7f1..fd725b0 100644 --- a/src/components/ProviderList.tsx +++ b/src/components/ProviderList.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Provider } from "../types"; import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react"; import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles"; @@ -35,6 +36,7 @@ const ProviderList: React.FC = ({ appType, onNotify, }) => { + const { t } = useTranslation(); // 提取API地址(兼容不同供应商配置:Claude env / Codex TOML) const getApiUrl = (provider: Provider): string => { try { @@ -49,9 +51,9 @@ const ProviderList: React.FC = ({ const match = cfg.config.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); if (match && match[2]) return match[2]; } - return "未配置官网地址"; + return t("provider.notConfigured"); } catch { - return "配置错误"; + return t("provider.configError"); } }; @@ -59,7 +61,7 @@ const ProviderList: React.FC = ({ try { await window.api.openExternal(url); } catch (error) { - console.error("打开链接失败:", error); + console.error(t("console.openLinkFailed"), error); } }; @@ -106,11 +108,7 @@ const ProviderList: React.FC = ({ try { const status = await window.api.getVSCodeSettingsStatus(); if (!status.exists) { - onNotify?.( - "未找到 VS Code 用户设置文件 (settings.json)", - "error", - 3000 - ); + onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000); return; } @@ -121,7 +119,7 @@ const ProviderList: React.FC = ({ if (!isOfficial) { const parsed = getCodexBaseUrl(provider); if (!parsed) { - onNotify?.("当前配置缺少 base_url,无法写入 VS Code", "error", 4000); + onNotify?.(t("notifications.missingBaseUrl"), "error", 4000); return; } } @@ -131,7 +129,7 @@ const ProviderList: React.FC = ({ if (next === raw) { // 幂等:没有变化也提示成功 - onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000); + onNotify?.(t("notifications.appliedToVSCode"), "success", 3000); setVscodeAppliedFor(provider.id); // 用户手动应用时,启用自动同步 enableAutoSync(); @@ -139,13 +137,14 @@ const ProviderList: React.FC = ({ } await window.api.writeVSCodeSettings(next); - onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000); + onNotify?.(t("notifications.appliedToVSCode"), "success", 3000); setVscodeAppliedFor(provider.id); // 用户手动应用时,启用自动同步 enableAutoSync(); } catch (e: any) { console.error(e); - const msg = e && e.message ? e.message : "应用到 VS Code 失败"; + const msg = + e && e.message ? e.message : t("notifications.syncVSCodeFailed"); onNotify?.(msg, "error", 5000); } }; @@ -154,11 +153,7 @@ const ProviderList: React.FC = ({ try { const status = await window.api.getVSCodeSettingsStatus(); if (!status.exists) { - onNotify?.( - "未找到 VS Code 用户设置文件 (settings.json)", - "error", - 3000 - ); + onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000); return; } const raw = await window.api.readVSCodeSettings(); @@ -167,20 +162,21 @@ const ProviderList: React.FC = ({ isOfficial: true, }); if (next === raw) { - onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000); + onNotify?.(t("notifications.removedFromVSCode"), "success", 3000); setVscodeAppliedFor(null); // 用户手动移除时,禁用自动同步 disableAutoSync(); return; } await window.api.writeVSCodeSettings(next); - onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000); + onNotify?.(t("notifications.removedFromVSCode"), "success", 3000); setVscodeAppliedFor(null); // 用户手动移除时,禁用自动同步 disableAutoSync(); } catch (e: any) { console.error(e); - const msg = e && e.message ? e.message : "移除失败"; + const msg = + e && e.message ? e.message : t("notifications.syncVSCodeFailed"); onNotify?.(msg, "error", 5000); } }; @@ -214,10 +210,10 @@ const ProviderList: React.FC = ({

- 还没有添加任何供应商 + {t("provider.noProviders")}

- 点击右上角的"添加供应商"按钮开始配置您的第一个API供应商 + {t("provider.noProvidersDescription")}

) : ( @@ -247,7 +243,7 @@ const ProviderList: React.FC = ({ )} > - 当前使用 + {t("provider.currentlyUsing")} @@ -292,13 +288,13 @@ const ProviderList: React.FC = ({ )} title={ vscodeAppliedFor === provider.id - ? "从 VS Code 移除我们写入的配置" - : "将当前供应商应用到 VS Code" + ? t("provider.removeFromVSCode") + : t("provider.applyToVSCode") } > {vscodeAppliedFor === provider.id - ? "从 VS Code 移除" - : "应用到 VS Code"} + ? t("provider.removeFromVSCode") + : t("provider.applyToVSCode")} )} @@ -332,7 +328,7 @@ const ProviderList: React.FC = ({ ? "text-gray-400 cursor-not-allowed" : "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10" )} - title="删除供应商" + title={t("provider.deleteProvider")} > diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 87c06a8..08e7a93 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { X, RefreshCw, @@ -24,6 +25,7 @@ interface SettingsModalProps { } export default function SettingsModal({ onClose }: SettingsModalProps) { + const { t } = useTranslation(); const [settings, setSettings] = useState({ showInTray: true, minimizeToTrayOnClose: true, @@ -54,9 +56,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const appVersion = await getVersion(); setVersion(appVersion); } catch (error) { - console.error("获取版本信息失败:", error); + console.error(t("console.getVersionFailed"), error); // 失败时不硬编码版本号,显示为未知 - setVersion("未知"); + setVersion(t("common.unknown")); } }; @@ -84,7 +86,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { : undefined, }); } catch (error) { - console.error("加载设置失败:", error); + console.error(t("console.loadSettingsFailed"), error); } }; @@ -95,7 +97,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setConfigPath(path); } } catch (error) { - console.error("获取配置路径失败:", error); + console.error(t("console.getConfigPathFailed"), error); } }; @@ -108,7 +110,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setResolvedClaudeDir(claudeDir || ""); setResolvedCodexDir(codexDir || ""); } catch (error) { - console.error("获取配置目录失败:", error); + console.error(t("console.getConfigDirFailed"), error); } }; @@ -117,7 +119,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const portable = await window.api.isPortable(); setIsPortable(portable); } catch (error) { - console.error("检测便携模式失败:", error); + console.error(t("console.detectPortableFailed"), error); } }; @@ -138,7 +140,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setSettings(payload); onClose(); } catch (error) { - console.error("保存设置失败:", error); + console.error(t("console.saveSettingsFailed"), error); } }; @@ -155,7 +157,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { await updateHandle.downloadAndInstall(); await relaunchApp(); } catch (error) { - console.error("更新失败:", error); + console.error(t("console.updateFailed"), error); // 更新失败时回退到打开 Releases 页面 await window.api.checkForUpdates(); } finally { @@ -176,7 +178,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { }, 3000); } } catch (error) { - console.error("检查更新失败:", error); + console.error(t("console.checkUpdateFailed"), error); // 在开发模式下,模拟已是最新版本的响应 if (import.meta.env.DEV) { setShowUpToDate(true); @@ -197,7 +199,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { try { await window.api.openAppConfigFolder(); } catch (error) { - console.error("打开配置文件夹失败:", error); + console.error(t("console.openConfigFolderFailed"), error); } }; @@ -228,7 +230,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setResolvedCodexDir(sanitized); } } catch (error) { - console.error("选择配置目录失败:", error); + console.error(t("console.selectConfigDirFailed"), error); } }; @@ -238,7 +240,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const folder = app === "claude" ? ".claude" : ".codex"; return await join(home, folder); } catch (error) { - console.error("获取默认配置目录失败:", error); + console.error(t("console.getDefaultConfigDirFailed"), error); return ""; } }; @@ -266,8 +268,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const handleOpenReleaseNotes = async () => { try { const targetVersion = updateInfo?.availableVersion || version; + const unknownLabel = t("common.unknown"); // 如果未知或为空,回退到 releases 首页 - if (!targetVersion || targetVersion === "未知") { + if (!targetVersion || targetVersion === unknownLabel) { await window.api.openExternal( "https://github.com/farion1231/cc-switch/releases" ); @@ -280,7 +283,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { `https://github.com/farion1231/cc-switch/releases/tag/${tag}` ); } catch (error) { - console.error("打开更新日志失败:", error); + console.error(t("console.openReleaseNotesFailed"), error); } }; @@ -300,7 +303,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { {/* 标题栏 */}

- 设置 + {t("settings.title")}

@@ -407,7 +409,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { type="button" onClick={() => handleResetConfigDir("claude")} className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" - title="恢复默认目录(需保存后生效)" + title={t("settings.resetDefault")} > @@ -416,7 +418,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
@@ -443,7 +445,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { type="button" onClick={() => handleResetConfigDir("codex")} className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" - title="恢复默认目录(需保存后生效)" + title={t("settings.resetDefault")} > @@ -455,7 +457,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { {/* 关于 */}

- 关于 + {t("common.about")}

@@ -465,7 +467,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { CC Switch

- 版本 {version} + {t("common.version")} {version}

@@ -474,12 +476,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { onClick={handleOpenReleaseNotes} className="px-2 py-1 text-xs font-medium text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 rounded-lg hover:bg-blue-500/10 transition-colors" title={ - hasUpdate ? "查看该版本更新日志" : "查看当前版本更新日志" + hasUpdate + ? t("settings.viewReleaseNotes") + : t("settings.viewCurrentReleaseNotes") } > - 更新日志 + {t("settings.releaseNotes")}
@@ -531,14 +537,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { onClick={onClose} className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" > - 取消 + {t("common.cancel")}
diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..8c7d3e2 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,29 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +import en from "./locales/en.json"; +import zh from "./locales/zh.json"; + +const resources = { + en: { + translation: en, + }, + zh: { + translation: zh, + }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: "en", // 默认语言设置为英文 + fallbackLng: "en", // 回退语言也设置为英文 + + interpolation: { + escapeValue: false, // React 已经默认转义 + }, + + // 开发模式下显示调试信息 + debug: false, +}); + +export default i18n; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..c7b6b29 --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,111 @@ +{ + "app": { + "title": "CC Switch", + "description": "Claude Code & Codex Provider Switching Tool" + }, + "common": { + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "close": "Close", + "settings": "Settings", + "about": "About", + "version": "Version", + "loading": "Loading...", + "success": "Success", + "error": "Error", + "unknown": "Unknown" + }, + "header": { + "viewOnGithub": "View on GitHub", + "toggleDarkMode": "Switch to Dark Mode", + "toggleLightMode": "Switch to Light Mode", + "addProvider": "Add Provider", + "switchToChinese": "Switch to Chinese", + "switchToEnglish": "Switch to English" + }, + "provider": { + "noProviders": "No providers added yet", + "noProvidersDescription": "Click the \"Add Provider\" button in the top right to configure your first API provider", + "currentlyUsing": "Currently Using", + "enable": "Enable", + "inUse": "In Use", + "editProvider": "Edit Provider", + "deleteProvider": "Delete Provider", + "addNewProvider": "Add New Provider", + "configError": "Configuration Error", + "notConfigured": "Not configured for official website", + "applyToVSCode": "Apply to VS Code", + "removeFromVSCode": "Remove from VS Code" + }, + "notifications": { + "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", + "appliedToVSCode": "Applied to VS Code, restart Codex plugin to take effect", + "removedFromVSCode": "Removed from VS Code, restart Codex plugin to take effect", + "syncedToVSCode": "Synced to VS Code", + "vscodeSettingsNotFound": "VS Code user settings file (settings.json) not found", + "missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code", + "saveFailed": "Save failed: {{error}}", + "saveFailedGeneric": "Save failed, please try again", + "syncVSCodeFailed": "Sync to VS Code failed" + }, + "confirm": { + "deleteProvider": "Delete Provider", + "deleteProviderMessage": "Are you sure you want to delete provider \"{{name}}\"? This action cannot be undone." + }, + "settings": { + "title": "Settings", + "windowBehavior": "Window Behavior", + "minimizeToTray": "Minimize to tray on close", + "minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.", + "configFileLocation": "Configuration File Location", + "openFolder": "Open Folder", + "configDirectoryOverride": "Configuration Directory Override (Advanced)", + "configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory in WSL to keep provider data consistent with the main environment.", + "claudeConfigDir": "Claude Code Configuration Directory", + "codexConfigDir": "Codex Configuration Directory", + "browsePlaceholderClaude": "e.g., /home//.claude", + "browsePlaceholderCodex": "e.g., /home//.codex", + "browseDirectory": "Browse Directory", + "resetDefault": "Reset to default directory (takes effect after saving)", + "checkForUpdates": "Check for Updates", + "updateTo": "Update to v{{version}}", + "updating": "Updating...", + "checking": "Checking...", + "upToDate": "Up to Date", + "releaseNotes": "Release Notes", + "viewReleaseNotes": "View release notes for this version", + "viewCurrentReleaseNotes": "View current version release notes" + }, + "apps": { + "claude": "Claude Code", + "codex": "Codex" + }, + "console": { + "providerSwitchReceived": "Received provider switch event:", + "setupListenerFailed": "Failed to setup provider switch listener:", + "updateProviderFailed": "Update provider failed:", + "syncToVSCodeFailed": "Sync to VS Code failed:", + "autoImportFailed": "Auto import default configuration failed:", + "openLinkFailed": "Failed to open link:", + "getVersionFailed": "Failed to get version info:", + "loadSettingsFailed": "Failed to load settings:", + "getConfigPathFailed": "Failed to get config path:", + "getConfigDirFailed": "Failed to get config directory:", + "detectPortableFailed": "Failed to detect portable mode:", + "saveSettingsFailed": "Failed to save settings:", + "updateFailed": "Update failed:", + "checkUpdateFailed": "Check for updates failed:", + "openConfigFolderFailed": "Failed to open config folder:", + "selectConfigDirFailed": "Failed to select config directory:", + "getDefaultConfigDirFailed": "Failed to get default config directory:", + "openReleaseNotesFailed": "Failed to open release notes:" + } +} diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json new file mode 100644 index 0000000..8cf5767 --- /dev/null +++ b/src/i18n/locales/zh.json @@ -0,0 +1,111 @@ +{ + "app": { + "title": "CC Switch", + "description": "Claude Code & Codex 供应商切换工具" + }, + "common": { + "add": "添加", + "edit": "编辑", + "delete": "删除", + "save": "保存", + "cancel": "取消", + "confirm": "确定", + "close": "关闭", + "settings": "设置", + "about": "关于", + "version": "版本", + "loading": "加载中...", + "success": "成功", + "error": "错误", + "unknown": "未知" + }, + "header": { + "viewOnGithub": "在 GitHub 上查看", + "toggleDarkMode": "切换到暗色模式", + "toggleLightMode": "切换到亮色模式", + "addProvider": "添加供应商", + "switchToChinese": "切换到中文", + "switchToEnglish": "切换到英文" + }, + "provider": { + "noProviders": "还没有添加任何供应商", + "noProvidersDescription": "点击右上角的\"添加供应商\"按钮开始配置您的第一个API供应商", + "currentlyUsing": "当前使用", + "enable": "启用", + "inUse": "使用中", + "editProvider": "编辑供应商", + "deleteProvider": "删除供应商", + "addNewProvider": "添加新供应商", + "configError": "配置错误", + "notConfigured": "未配置官网地址", + "applyToVSCode": "应用到 VS Code", + "removeFromVSCode": "从 VS Code 移除" + }, + "notifications": { + "providerSaved": "供应商配置已保存", + "providerDeleted": "供应商删除成功", + "switchSuccess": "切换成功!请重启 {{appName}} 终端以生效", + "switchFailed": "切换失败,请检查配置", + "autoImported": "已从现有配置创建默认供应商", + "appliedToVSCode": "已应用到 VS Code,重启 Codex 插件以生效", + "removedFromVSCode": "已从 VS Code 移除,重启 Codex 插件以生效", + "syncedToVSCode": "已同步到 VS Code", + "vscodeSettingsNotFound": "未找到 VS Code 用户设置文件 (settings.json)", + "missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code", + "saveFailed": "保存失败:{{error}}", + "saveFailedGeneric": "保存失败,请重试", + "syncVSCodeFailed": "同步 VS Code 失败" + }, + "confirm": { + "deleteProvider": "删除供应商", + "deleteProviderMessage": "确定要删除供应商 \"{{name}}\" 吗?此操作无法撤销。" + }, + "settings": { + "title": "设置", + "windowBehavior": "窗口行为", + "minimizeToTray": "关闭时最小化到托盘", + "minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。", + "configFileLocation": "配置文件位置", + "openFolder": "打开文件夹", + "configDirectoryOverride": "配置目录覆盖(高级)", + "configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL 里的配置目录,供应商数据与主环境保持一致。", + "claudeConfigDir": "Claude Code 配置目录", + "codexConfigDir": "Codex 配置目录", + "browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude", + "browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex", + "browseDirectory": "浏览目录", + "resetDefault": "恢复默认目录(需保存后生效)", + "checkForUpdates": "检查更新", + "updateTo": "更新到 v{{version}}", + "updating": "更新中...", + "checking": "检查中...", + "upToDate": "已是最新", + "releaseNotes": "更新日志", + "viewReleaseNotes": "查看该版本更新日志", + "viewCurrentReleaseNotes": "查看当前版本更新日志" + }, + "apps": { + "claude": "Claude Code", + "codex": "Codex" + }, + "console": { + "providerSwitchReceived": "收到供应商切换事件:", + "setupListenerFailed": "设置供应商切换监听器失败:", + "updateProviderFailed": "更新供应商失败:", + "syncToVSCodeFailed": "同步到VS Code失败:", + "autoImportFailed": "自动导入默认配置失败:", + "openLinkFailed": "打开链接失败:", + "getVersionFailed": "获取版本信息失败:", + "loadSettingsFailed": "加载设置失败:", + "getConfigPathFailed": "获取配置路径失败:", + "getConfigDirFailed": "获取配置目录失败:", + "detectPortableFailed": "检测便携模式失败:", + "saveSettingsFailed": "保存设置失败:", + "updateFailed": "更新失败:", + "checkUpdateFailed": "检查更新失败:", + "openConfigFolderFailed": "打开配置文件夹失败:", + "selectConfigDirFailed": "选择配置目录失败:", + "getDefaultConfigDirFailed": "获取默认配置目录失败:", + "openReleaseNotesFailed": "打开更新日志失败:" + } +} diff --git a/src/main.tsx b/src/main.tsx index 5d77b49..8bdc674 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,8 @@ import { UpdateProvider } from "./contexts/UpdateContext"; import "./index.css"; // 导入 Tauri API(自动绑定到 window.api) import "./lib/tauri-api"; +// 导入国际化配置 +import "./i18n"; // 根据平台添加 body class,便于平台特定样式 try { @@ -23,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - , + );