feat: integrate i18next for internationalization support (#65)
* 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. * 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 --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
76
README_i18n.md
Normal file
76
README_i18n.md
Normal 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` 配置来更改默认语言
|
||||||
@@ -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
65
pnpm-lock.yaml
generated
@@ -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: {}
|
||||||
|
|||||||
67
src/App.tsx
67
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
31
src/components/LanguageSwitcher.tsx
Normal file
31
src/components/LanguageSwitcher.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
29
src/i18n/index.ts
Normal 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
111
src/i18n/locales/en.json
Normal 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
111
src/i18n/locales/zh.json
Normal 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": "打开更新日志失败:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user