2 Commits

Author SHA1 Message Date
Jason
ea81c3d839 feat: improve i18n implementation with better translations and accessibility
- Add proper i18n keys for language switcher tooltips and aria-labels
- Replace hardcoded Chinese console error messages with i18n keys
- Add missing translation keys for new UI elements
- Improve accessibility with proper aria-label attributes
2025-09-28 20:35:14 +08:00
TinsFox
aa05a8475f feat: integrate i18next for internationalization support
- Added i18next and react-i18next dependencies for localization.
- Updated various components to utilize translation functions for user-facing text.
- Enhanced user experience by providing multilingual support across the application.
2025-09-28 18:23:23 +08:00
14 changed files with 556 additions and 113 deletions

76
README_i18n.md Normal file
View File

@@ -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 <div>{t('common.save')}</div>;
}
```
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` 配置来更改默认语言

View File

@@ -37,10 +37,12 @@
"@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"i18next": "^25.5.2",
"jsonc-parser": "^3.2.1", "jsonc-parser": "^3.2.1",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^16.0.0",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
} }
} }

65
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
codemirror: codemirror:
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2 version: 6.0.2
i18next:
specifier: ^25.5.2
version: 25.5.2(typescript@5.9.2)
jsonc-parser: jsonc-parser:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.3.1 version: 3.3.1
@@ -53,6 +56,9 @@ importers:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.3.1(react@18.3.1) 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: tailwindcss:
specifier: ^4.1.13 specifier: ^4.1.13
version: 4.1.13 version: 4.1.13
@@ -159,6 +165,10 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@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': '@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -750,6 +760,17 @@ packages:
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 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: jiti@2.5.1:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true hasBin: true
@@ -890,6 +911,22 @@ packages:
peerDependencies: peerDependencies:
react: ^18.3.1 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: react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -973,6 +1010,10 @@ packages:
terser: terser:
optional: true 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: w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -1079,6 +1120,8 @@ snapshots:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1 '@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.27.2': '@babel/template@7.27.2':
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
@@ -1591,6 +1634,16 @@ snapshots:
graceful-fs@4.2.11: {} 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: {} jiti@2.5.1: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -1692,6 +1745,16 @@ snapshots:
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 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-refresh@0.17.0: {}
react@18.3.1: react@18.3.1:
@@ -1767,6 +1830,8 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
lightningcss: 1.30.1 lightningcss: 1.30.1
void-elements@3.1.0: {}
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}
yallist@3.1.1: {} yallist@3.1.1: {}

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "./types"; import { Provider } from "./types";
import { AppType } from "./lib/tauri-api"; import { AppType } from "./lib/tauri-api";
import ProviderList from "./components/ProviderList"; import ProviderList from "./components/ProviderList";
@@ -8,6 +9,7 @@ import { ConfirmDialog } from "./components/ConfirmDialog";
import { AppSwitcher } from "./components/AppSwitcher"; import { AppSwitcher } from "./components/AppSwitcher";
import SettingsModal from "./components/SettingsModal"; import SettingsModal from "./components/SettingsModal";
import { UpdateBadge } from "./components/UpdateBadge"; import { UpdateBadge } from "./components/UpdateBadge";
import LanguageSwitcher from "./components/LanguageSwitcher";
import { Plus, Settings, Moon, Sun } from "lucide-react"; import { Plus, Settings, Moon, Sun } from "lucide-react";
import { buttonStyles } from "./lib/styles"; import { buttonStyles } from "./lib/styles";
import { useDarkMode } from "./hooks/useDarkMode"; import { useDarkMode } from "./hooks/useDarkMode";
@@ -17,6 +19,7 @@ import { getCodexBaseUrl } from "./utils/providerConfigUtils";
import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync"; import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync";
function App() { function App() {
const { t } = useTranslation();
const { isDarkMode, toggleDarkMode } = useDarkMode(); const { isDarkMode, toggleDarkMode } = useDarkMode();
const { isAutoSyncEnabled } = useVSCodeAutoSync(); const { isAutoSyncEnabled } = useVSCodeAutoSync();
const [activeApp, setActiveApp] = useState<AppType>("claude"); const [activeApp, setActiveApp] = useState<AppType>("claude");
@@ -24,7 +27,7 @@ function App() {
const [currentProviderId, setCurrentProviderId] = useState<string>(""); const [currentProviderId, setCurrentProviderId] = useState<string>("");
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [editingProviderId, setEditingProviderId] = useState<string | null>( const [editingProviderId, setEditingProviderId] = useState<string | null>(
null, null
); );
const [notification, setNotification] = useState<{ const [notification, setNotification] = useState<{
message: string; message: string;
@@ -44,7 +47,7 @@ function App() {
const showNotification = ( const showNotification = (
message: string, message: string,
type: "success" | "error", type: "success" | "error",
duration = 3000, duration = 3000
) => { ) => {
// 清除之前的定时器 // 清除之前的定时器
if (timeoutRef.current) { if (timeoutRef.current) {
@@ -88,7 +91,7 @@ function App() {
try { try {
unlisten = await window.api.onProviderSwitched(async (data) => { unlisten = await window.api.onProviderSwitched(async (data) => {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log("收到供应商切换事件:", data); console.log(t("console.providerSwitchReceived"), data);
} }
// 如果当前应用类型匹配,则重新加载数据 // 如果当前应用类型匹配,则重新加载数据
@@ -102,7 +105,7 @@ function App() {
} }
}); });
} catch (error) { } catch (error) {
console.error("设置供应商切换监听器失败:", error); console.error(t("console.setupListenerFailed"), error);
} }
}; };
@@ -152,16 +155,16 @@ function App() {
await loadProviders(); await loadProviders();
setEditingProviderId(null); setEditingProviderId(null);
// 显示编辑成功提示 // 显示编辑成功提示
showNotification("供应商配置已保存", "success", 2000); showNotification(t("notifications.providerSaved"), "success", 2000);
// 更新托盘菜单 // 更新托盘菜单
await window.api.updateTrayMenu(); await window.api.updateTrayMenu();
} catch (error) { } catch (error) {
console.error("更新供应商失败:", error); console.error(t("console.updateProviderFailed"), error);
setEditingProviderId(null); setEditingProviderId(null);
const errorMessage = extractErrorMessage(error); const errorMessage = extractErrorMessage(error);
const message = errorMessage const message = errorMessage
? `保存失败:${errorMessage}` ? t("notifications.saveFailed", { error: errorMessage })
: "保存失败,请重试"; : t("notifications.saveFailedGeneric");
showNotification(message, "error", errorMessage ? 6000 : 3000); showNotification(message, "error", errorMessage ? 6000 : 3000);
} }
}; };
@@ -170,13 +173,13 @@ function App() {
const provider = providers[id]; const provider = providers[id];
setConfirmDialog({ setConfirmDialog({
isOpen: true, isOpen: true,
title: "删除供应商", title: t("confirm.deleteProvider"),
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`, message: t("confirm.deleteProviderMessage", { name: provider?.name }),
onConfirm: async () => { onConfirm: async () => {
await window.api.deleteProvider(id, activeApp); await window.api.deleteProvider(id, activeApp);
await loadProviders(); await loadProviders();
setConfirmDialog(null); setConfirmDialog(null);
showNotification("供应商删除成功", "success"); showNotification(t("notifications.providerDeleted"), "success");
// 更新托盘菜单 // 更新托盘菜单
await window.api.updateTrayMenu(); await window.api.updateTrayMenu();
}, },
@@ -190,9 +193,9 @@ function App() {
if (!status.exists) { if (!status.exists) {
if (!silent) { if (!silent) {
showNotification( showNotification(
"未找到 VS Code 用户设置文件 (settings.json)", t("notifications.vscodeSettingsNotFound"),
"error", "error",
3000, 3000
); );
} }
return; return;
@@ -208,11 +211,7 @@ function App() {
const parsed = getCodexBaseUrl(provider); const parsed = getCodexBaseUrl(provider);
if (!parsed) { if (!parsed) {
if (!silent) { if (!silent) {
showNotification( showNotification(t("notifications.missingBaseUrl"), "error", 4000);
"当前配置缺少 base_url无法写入 VS Code",
"error",
4000,
);
} }
return; return;
} }
@@ -226,16 +225,17 @@ function App() {
if (updatedSettings !== raw) { if (updatedSettings !== raw) {
await window.api.writeVSCodeSettings(updatedSettings); await window.api.writeVSCodeSettings(updatedSettings);
if (!silent) { if (!silent) {
showNotification("已同步到 VS Code", "success", 1500); showNotification(t("notifications.syncedToVSCode"), "success", 1500);
} }
} }
// 触发providers重新加载以更新VS Code按钮状态 // 触发providers重新加载以更新VS Code按钮状态
await loadProviders(); await loadProviders();
} catch (error: any) { } catch (error: any) {
console.error("同步到VS Code失败:", error); console.error(t("console.syncToVSCodeFailed"), error);
if (!silent) { if (!silent) {
const errorMessage = error?.message || "同步 VS Code 失败"; const errorMessage =
error?.message || t("notifications.syncVSCodeFailed");
showNotification(errorMessage, "error", 5000); showNotification(errorMessage, "error", 5000);
} }
} }
@@ -246,11 +246,11 @@ function App() {
if (success) { if (success) {
setCurrentProviderId(id); setCurrentProviderId(id);
// 显示重启提示 // 显示重启提示
const appName = activeApp === "claude" ? "Claude Code" : "Codex"; const appName = t(`apps.${activeApp}`);
showNotification( showNotification(
`切换成功!请重启 ${appName} 终端以生效`, t("notifications.switchSuccess", { appName }),
"success", "success",
2000, 2000
); );
// 更新托盘菜单 // 更新托盘菜单
await window.api.updateTrayMenu(); await window.api.updateTrayMenu();
@@ -260,7 +260,7 @@ function App() {
await syncCodexToVSCode(id, true); // silent模式不显示通知 await syncCodexToVSCode(id, true); // silent模式不显示通知
} }
} else { } else {
showNotification("切换失败,请检查配置", "error"); showNotification(t("notifications.switchFailed"), "error");
} }
}; };
@@ -271,13 +271,13 @@ function App() {
if (result.success) { if (result.success) {
await loadProviders(); await loadProviders();
showNotification("已从现有配置创建默认供应商", "success", 3000); showNotification(t("notifications.autoImported"), "success", 3000);
// 更新托盘菜单 // 更新托盘菜单
await window.api.updateTrayMenu(); await window.api.updateTrayMenu();
} }
// 如果导入失败(比如没有现有配置),静默处理,不显示错误 // 如果导入失败(比如没有现有配置),静默处理,不显示错误
} catch (error) { } catch (error) {
console.error("自动导入默认配置失败:", error); console.error(t("console.autoImportFailed"), error);
// 静默处理,不影响用户体验 // 静默处理,不影响用户体验
} }
}; };
@@ -293,22 +293,27 @@ function App() {
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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 CC Switch
</a> </a>
<button <button
onClick={toggleDarkMode} onClick={toggleDarkMode}
className={buttonStyles.icon} className={buttonStyles.icon}
title={isDarkMode ? "切换到亮色模式" : "切换到暗色模式"} title={
isDarkMode
? t("header.toggleLightMode")
: t("header.toggleDarkMode")
}
> >
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />} {isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
</button> </button>
<LanguageSwitcher />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setIsSettingsOpen(true)} onClick={() => setIsSettingsOpen(true)}
className={buttonStyles.icon} className={buttonStyles.icon}
title="设置" title={t("common.settings")}
> >
<Settings size={18} /> <Settings size={18} />
</button> </button>
@@ -324,7 +329,7 @@ function App() {
className={`inline-flex items-center gap-2 ${buttonStyles.primary}`} className={`inline-flex items-center gap-2 ${buttonStyles.primary}`}
> >
<Plus size={16} /> <Plus size={16} />
{t("header.addProvider")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "../types"; import { Provider } from "../types";
import { AppType } from "../lib/tauri-api"; import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm"; import ProviderForm from "./ProviderForm";
@@ -14,11 +15,13 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({
onAdd, onAdd,
onClose, onClose,
}) => { }) => {
const { t } = useTranslation();
return ( return (
<ProviderForm <ProviderForm
appType={appType} appType={appType}
title="添加新供应商" title={t("provider.addNewProvider")}
submitText="添加" submitText={t("common.add")}
showPresets={true} showPresets={true}
onSubmit={onAdd} onSubmit={onAdd}
onClose={onClose} onClose={onClose}

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { AlertTriangle, X } from "lucide-react"; import { AlertTriangle, X } from "lucide-react";
import { isLinux } from "../lib/platform"; import { isLinux } from "../lib/platform";
@@ -16,11 +17,13 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen, isOpen,
title, title,
message, message,
confirmText = "确定", confirmText,
cancelText = "取消", cancelText,
onConfirm, onConfirm,
onCancel, onCancel,
}) => { }) => {
const { t } = useTranslation();
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@@ -65,13 +68,13 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
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" 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 autoFocus
> >
{cancelText} {cancelText || t("common.cancel")}
</button> </button>
<button <button
onClick={onConfirm} onClick={onConfirm}
className="px-4 py-2 text-sm font-medium bg-red-500 text-white hover:bg-red-500/90 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium bg-red-500 text-white hover:bg-red-500/90 rounded-md transition-colors"
> >
{confirmText} {confirmText || t("common.confirm")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "../types"; import { Provider } from "../types";
import { AppType } from "../lib/tauri-api"; import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm"; import ProviderForm from "./ProviderForm";
@@ -16,6 +17,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
onSave, onSave,
onClose, onClose,
}) => { }) => {
const { t } = useTranslation();
const handleSubmit = (data: Omit<Provider, "id">) => { const handleSubmit = (data: Omit<Provider, "id">) => {
onSave({ onSave({
...provider, ...provider,
@@ -26,8 +29,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
return ( return (
<ProviderForm <ProviderForm
appType={appType} appType={appType}
title="编辑供应商" title={t("common.edit")}
submitText="保存" submitText={t("common.save")}
initialData={provider} initialData={provider}
showPresets={false} showPresets={false}
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@@ -0,0 +1,31 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Globe } from "lucide-react";
import { buttonStyles } from "../lib/styles";
const LanguageSwitcher: React.FC = () => {
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 (
<button
onClick={toggleLanguage}
className={buttonStyles.icon}
title={t(titleKey)}
aria-label={t(titleKey)}
>
<Globe size={18} />
</button>
);
};
export default LanguageSwitcher;

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "../types"; import { Provider } from "../types";
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react"; import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles"; import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
@@ -35,6 +36,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
appType, appType,
onNotify, onNotify,
}) => { }) => {
const { t } = useTranslation();
// 提取API地址兼容不同供应商配置Claude env / Codex TOML // 提取API地址兼容不同供应商配置Claude env / Codex TOML
const getApiUrl = (provider: Provider): string => { const getApiUrl = (provider: Provider): string => {
try { try {
@@ -49,9 +51,9 @@ const ProviderList: React.FC<ProviderListProps> = ({
const match = cfg.config.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); const match = cfg.config.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
if (match && match[2]) return match[2]; if (match && match[2]) return match[2];
} }
return "未配置官网地址"; return t("provider.notConfigured");
} catch { } catch {
return "配置错误"; return t("provider.configError");
} }
}; };
@@ -59,7 +61,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
try { try {
await window.api.openExternal(url); await window.api.openExternal(url);
} catch (error) { } catch (error) {
console.error("打开链接失败:", error); console.error(t("console.openLinkFailed"), error);
} }
}; };
@@ -106,11 +108,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
try { try {
const status = await window.api.getVSCodeSettingsStatus(); const status = await window.api.getVSCodeSettingsStatus();
if (!status.exists) { if (!status.exists) {
onNotify?.( onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
"未找到 VS Code 用户设置文件 (settings.json)",
"error",
3000
);
return; return;
} }
@@ -121,7 +119,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
if (!isOfficial) { if (!isOfficial) {
const parsed = getCodexBaseUrl(provider); const parsed = getCodexBaseUrl(provider);
if (!parsed) { if (!parsed) {
onNotify?.("当前配置缺少 base_url无法写入 VS Code", "error", 4000); onNotify?.(t("notifications.missingBaseUrl"), "error", 4000);
return; return;
} }
} }
@@ -131,7 +129,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
if (next === raw) { if (next === raw) {
// 幂等:没有变化也提示成功 // 幂等:没有变化也提示成功
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000); onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
setVscodeAppliedFor(provider.id); setVscodeAppliedFor(provider.id);
// 用户手动应用时,启用自动同步 // 用户手动应用时,启用自动同步
enableAutoSync(); enableAutoSync();
@@ -139,13 +137,14 @@ const ProviderList: React.FC<ProviderListProps> = ({
} }
await window.api.writeVSCodeSettings(next); await window.api.writeVSCodeSettings(next);
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000); onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
setVscodeAppliedFor(provider.id); setVscodeAppliedFor(provider.id);
// 用户手动应用时,启用自动同步 // 用户手动应用时,启用自动同步
enableAutoSync(); enableAutoSync();
} catch (e: any) { } catch (e: any) {
console.error(e); 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); onNotify?.(msg, "error", 5000);
} }
}; };
@@ -154,11 +153,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
try { try {
const status = await window.api.getVSCodeSettingsStatus(); const status = await window.api.getVSCodeSettingsStatus();
if (!status.exists) { if (!status.exists) {
onNotify?.( onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
"未找到 VS Code 用户设置文件 (settings.json)",
"error",
3000
);
return; return;
} }
const raw = await window.api.readVSCodeSettings(); const raw = await window.api.readVSCodeSettings();
@@ -167,20 +162,21 @@ const ProviderList: React.FC<ProviderListProps> = ({
isOfficial: true, isOfficial: true,
}); });
if (next === raw) { if (next === raw) {
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000); onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
setVscodeAppliedFor(null); setVscodeAppliedFor(null);
// 用户手动移除时,禁用自动同步 // 用户手动移除时,禁用自动同步
disableAutoSync(); disableAutoSync();
return; return;
} }
await window.api.writeVSCodeSettings(next); await window.api.writeVSCodeSettings(next);
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000); onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
setVscodeAppliedFor(null); setVscodeAppliedFor(null);
// 用户手动移除时,禁用自动同步 // 用户手动移除时,禁用自动同步
disableAutoSync(); disableAutoSync();
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
const msg = e && e.message ? e.message : "移除失败"; const msg =
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
onNotify?.(msg, "error", 5000); onNotify?.(msg, "error", 5000);
} }
}; };
@@ -214,10 +210,10 @@ const ProviderList: React.FC<ProviderListProps> = ({
<Users size={24} className="text-gray-400" /> <Users size={24} className="text-gray-400" />
</div> </div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("provider.noProviders")}
</h3> </h3>
<p className="text-gray-500 dark:text-gray-400 text-sm"> <p className="text-gray-500 dark:text-gray-400 text-sm">
"添加供应商"API供应商 {t("provider.noProvidersDescription")}
</p> </p>
</div> </div>
) : ( ) : (
@@ -247,7 +243,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
)} )}
> >
<CheckCircle2 size={12} /> <CheckCircle2 size={12} />
使 {t("provider.currentlyUsing")}
</div> </div>
</div> </div>
@@ -292,13 +288,13 @@ const ProviderList: React.FC<ProviderListProps> = ({
)} )}
title={ title={
vscodeAppliedFor === provider.id vscodeAppliedFor === provider.id
? "从 VS Code 移除我们写入的配置" ? t("provider.removeFromVSCode")
: "将当前供应商应用到 VS Code" : t("provider.applyToVSCode")
} }
> >
{vscodeAppliedFor === provider.id {vscodeAppliedFor === provider.id
? "从 VS Code 移除" ? t("provider.removeFromVSCode")
: "应用到 VS Code"} : t("provider.applyToVSCode")}
</button> </button>
)} )}
<button <button
@@ -312,13 +308,13 @@ const ProviderList: React.FC<ProviderListProps> = ({
)} )}
> >
{!isCurrent && <Play size={14} />} {!isCurrent && <Play size={14} />}
{isCurrent ? "使用中" : "启用"} {isCurrent ? t("provider.inUse") : t("provider.enable")}
</button> </button>
<button <button
onClick={() => onEdit(provider.id)} onClick={() => onEdit(provider.id)}
className={buttonStyles.icon} className={buttonStyles.icon}
title="编辑供应商" title={t("provider.editProvider")}
> >
<Edit3 size={16} /> <Edit3 size={16} />
</button> </button>
@@ -332,7 +328,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
? "text-gray-400 cursor-not-allowed" ? "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" : "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")}
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { import {
X, X,
RefreshCw, RefreshCw,
@@ -24,6 +25,7 @@ interface SettingsModalProps {
} }
export default function SettingsModal({ onClose }: SettingsModalProps) { export default function SettingsModal({ onClose }: SettingsModalProps) {
const { t } = useTranslation();
const [settings, setSettings] = useState<Settings>({ const [settings, setSettings] = useState<Settings>({
showInTray: true, showInTray: true,
minimizeToTrayOnClose: true, minimizeToTrayOnClose: true,
@@ -54,9 +56,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const appVersion = await getVersion(); const appVersion = await getVersion();
setVersion(appVersion); setVersion(appVersion);
} catch (error) { } 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, : undefined,
}); });
} catch (error) { } catch (error) {
console.error("加载设置失败:", error); console.error(t("console.loadSettingsFailed"), error);
} }
}; };
@@ -95,7 +97,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setConfigPath(path); setConfigPath(path);
} }
} catch (error) { } catch (error) {
console.error("获取配置路径失败:", error); console.error(t("console.getConfigPathFailed"), error);
} }
}; };
@@ -108,7 +110,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setResolvedClaudeDir(claudeDir || ""); setResolvedClaudeDir(claudeDir || "");
setResolvedCodexDir(codexDir || ""); setResolvedCodexDir(codexDir || "");
} catch (error) { } 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(); const portable = await window.api.isPortable();
setIsPortable(portable); setIsPortable(portable);
} catch (error) { } catch (error) {
console.error("检测便携模式失败:", error); console.error(t("console.detectPortableFailed"), error);
} }
}; };
@@ -138,7 +140,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setSettings(payload); setSettings(payload);
onClose(); onClose();
} catch (error) { } 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 updateHandle.downloadAndInstall();
await relaunchApp(); await relaunchApp();
} catch (error) { } catch (error) {
console.error("更新失败:", error); console.error(t("console.updateFailed"), error);
// 更新失败时回退到打开 Releases 页面 // 更新失败时回退到打开 Releases 页面
await window.api.checkForUpdates(); await window.api.checkForUpdates();
} finally { } finally {
@@ -176,7 +178,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
}, 3000); }, 3000);
} }
} catch (error) { } catch (error) {
console.error("检查更新失败:", error); console.error(t("console.checkUpdateFailed"), error);
// 在开发模式下,模拟已是最新版本的响应 // 在开发模式下,模拟已是最新版本的响应
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
setShowUpToDate(true); setShowUpToDate(true);
@@ -197,7 +199,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
try { try {
await window.api.openAppConfigFolder(); await window.api.openAppConfigFolder();
} catch (error) { } catch (error) {
console.error("打开配置文件夹失败:", error); console.error(t("console.openConfigFolderFailed"), error);
} }
}; };
@@ -228,7 +230,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setResolvedCodexDir(sanitized); setResolvedCodexDir(sanitized);
} }
} catch (error) { } 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"; const folder = app === "claude" ? ".claude" : ".codex";
return await join(home, folder); return await join(home, folder);
} catch (error) { } catch (error) {
console.error("获取默认配置目录失败:", error); console.error(t("console.getDefaultConfigDirFailed"), error);
return ""; return "";
} }
}; };
@@ -266,8 +268,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const handleOpenReleaseNotes = async () => { const handleOpenReleaseNotes = async () => {
try { try {
const targetVersion = updateInfo?.availableVersion || version; const targetVersion = updateInfo?.availableVersion || version;
const unknownLabel = t("common.unknown");
// 如果未知或为空,回退到 releases 首页 // 如果未知或为空,回退到 releases 首页
if (!targetVersion || targetVersion === "未知") { if (!targetVersion || targetVersion === unknownLabel) {
await window.api.openExternal( await window.api.openExternal(
"https://github.com/farion1231/cc-switch/releases" "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}` `https://github.com/farion1231/cc-switch/releases/tag/${tag}`
); );
} catch (error) { } catch (error) {
console.error("打开更新日志失败:", error); console.error(t("console.openReleaseNotesFailed"), error);
} }
}; };
@@ -300,7 +303,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 标题栏 */} {/* 标题栏 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800"> <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
<h2 className="text-lg font-semibold text-blue-500 dark:text-blue-400"> <h2 className="text-lg font-semibold text-blue-500 dark:text-blue-400">
{t("settings.title")}
</h2> </h2>
<button <button
onClick={onClose} onClick={onClose}
@@ -315,16 +318,16 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 窗口行为设置 */} {/* 窗口行为设置 */}
<div> <div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("settings.windowBehavior")}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<label className="flex items-center justify-between"> <label className="flex items-center justify-between">
<div> <div>
<span className="text-sm text-gray-900 dark:text-gray-100"> <span className="text-sm text-gray-900 dark:text-gray-100">
{t("settings.minimizeToTray")}
</span> </span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
退 {t("settings.minimizeToTrayDescription")}
</p> </p>
</div> </div>
<input <input
@@ -347,18 +350,18 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 配置文件位置 */} {/* 配置文件位置 */}
<div> <div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("settings.configFileLocation")}
</h3> </h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 px-3 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg"> <div className="flex-1 px-3 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg">
<span className="text-xs font-mono text-gray-500 dark:text-gray-400"> <span className="text-xs font-mono text-gray-500 dark:text-gray-400">
{configPath || "加载中..."} {configPath || t("common.loading")}
</span> </span>
</div> </div>
<button <button
onClick={handleOpenConfigFolder} onClick={handleOpenConfigFolder}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="打开文件夹" title={t("settings.openFolder")}
> >
<FolderOpen <FolderOpen
size={18} size={18}
@@ -371,16 +374,15 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 配置目录覆盖 */} {/* 配置目录覆盖 */}
<div> <div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("settings.configDirectoryOverride")}
</h3> </h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed"> <p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed">
WSL 使 Claude Code Codex WSL {t("settings.configDirectoryDescription")}
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"> <label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Claude Code {t("settings.claudeConfigDir")}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -392,14 +394,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
claudeConfigDir: e.target.value, claudeConfigDir: e.target.value,
}) })
} }
placeholder="例如:/home/<你的用户名>/.claude" placeholder={t("settings.browsePlaceholderClaude")}
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
/> />
<button <button
type="button" type="button"
onClick={() => handleBrowseConfigDir("claude")} onClick={() => handleBrowseConfigDir("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" 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.browseDirectory")}
> >
<FolderSearch size={16} /> <FolderSearch size={16} />
</button> </button>
@@ -407,7 +409,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
type="button" type="button"
onClick={() => handleResetConfigDir("claude")} 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" 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")}
> >
<Undo2 size={16} /> <Undo2 size={16} />
</button> </button>
@@ -416,7 +418,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
<div> <div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"> <label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Codex {t("settings.codexConfigDir")}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -428,14 +430,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
codexConfigDir: e.target.value, codexConfigDir: e.target.value,
}) })
} }
placeholder="例如:/home/<你的用户名>/.codex" placeholder={t("settings.browsePlaceholderCodex")}
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
/> />
<button <button
type="button" type="button"
onClick={() => handleBrowseConfigDir("codex")} onClick={() => handleBrowseConfigDir("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" 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.browseDirectory")}
> >
<FolderSearch size={16} /> <FolderSearch size={16} />
</button> </button>
@@ -443,7 +445,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
type="button" type="button"
onClick={() => handleResetConfigDir("codex")} 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" 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")}
> >
<Undo2 size={16} /> <Undo2 size={16} />
</button> </button>
@@ -455,7 +457,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 关于 */} {/* 关于 */}
<div> <div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("common.about")}
</h3> </h3>
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg"> <div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@@ -465,7 +467,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
CC Switch CC Switch
</p> </p>
<p className="mt-1 text-gray-500 dark:text-gray-400"> <p className="mt-1 text-gray-500 dark:text-gray-400">
{version} {t("common.version")} {version}
</p> </p>
</div> </div>
</div> </div>
@@ -474,12 +476,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
onClick={handleOpenReleaseNotes} 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" 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={ title={
hasUpdate ? "查看该版本更新日志" : "查看当前版本更新日志" hasUpdate
? t("settings.viewReleaseNotes")
: t("settings.viewCurrentReleaseNotes")
} }
> >
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<ExternalLink size={12} /> <ExternalLink size={12} />
{t("settings.releaseNotes")}
</span> </span>
</button> </button>
<button <button
@@ -498,25 +502,27 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{isDownloading ? ( {isDownloading ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Download size={12} className="animate-pulse" /> <Download size={12} className="animate-pulse" />
... {t("settings.updating")}
</span> </span>
) : isCheckingUpdate ? ( ) : isCheckingUpdate ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<RefreshCw size={12} className="animate-spin" /> <RefreshCw size={12} className="animate-spin" />
... {t("settings.checking")}
</span> </span>
) : hasUpdate ? ( ) : hasUpdate ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Download size={12} /> <Download size={12} />
v{updateInfo?.availableVersion} {t("settings.updateTo", {
version: updateInfo?.availableVersion ?? "",
})}
</span> </span>
) : showUpToDate ? ( ) : showUpToDate ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Check size={12} /> <Check size={12} />
{t("settings.upToDate")}
</span> </span>
) : ( ) : (
"检查更新" t("settings.checkForUpdates")
)} )}
</button> </button>
</div> </div>
@@ -531,14 +537,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
onClick={onClose} 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" 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")}
</button> </button>
<button <button
onClick={saveSettings} onClick={saveSettings}
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2" className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2"
> >
<Save size={16} /> <Save size={16} />
{t("common.save")}
</button> </button>
</div> </div>
</div> </div>

29
src/i18n/index.ts Normal file
View File

@@ -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;

111
src/i18n/locales/en.json Normal file
View File

@@ -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/<your-username>/.claude",
"browsePlaceholderCodex": "e.g., /home/<your-username>/.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:"
}
}

111
src/i18n/locales/zh.json Normal file
View File

@@ -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": "打开更新日志失败:"
}
}

View File

@@ -5,6 +5,8 @@ import { UpdateProvider } from "./contexts/UpdateContext";
import "./index.css"; import "./index.css";
// 导入 Tauri API自动绑定到 window.api // 导入 Tauri API自动绑定到 window.api
import "./lib/tauri-api"; import "./lib/tauri-api";
// 导入国际化配置
import "./i18n";
// 根据平台添加 body class便于平台特定样式 // 根据平台添加 body class便于平台特定样式
try { try {
@@ -23,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<UpdateProvider> <UpdateProvider>
<App /> <App />
</UpdateProvider> </UpdateProvider>
</React.StrictMode>, </React.StrictMode>
); );