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:
TinsFox
2025-09-28 20:47:44 +08:00
committed by GitHub
parent fd0e83ebd5
commit 5f3caa1484
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-updater": "^2.0.0",
"codemirror": "^6.0.2",
"i18next": "^25.5.2",
"jsonc-parser": "^3.2.1",
"lucide-react": "^0.542.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^16.0.0",
"tailwindcss": "^4.1.13"
}
}

65
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
codemirror:
specifier: ^6.0.2
version: 6.0.2
i18next:
specifier: ^25.5.2
version: 25.5.2(typescript@5.9.2)
jsonc-parser:
specifier: ^3.2.1
version: 3.3.1
@@ -53,6 +56,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
react-i18next:
specifier: ^16.0.0
version: 16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
tailwindcss:
specifier: ^4.1.13
version: 4.1.13
@@ -159,6 +165,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -750,6 +760,17 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
i18next@25.5.2:
resolution: {integrity: sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
jiti@2.5.1:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true
@@ -890,6 +911,22 @@ packages:
peerDependencies:
react: ^18.3.1
react-i18next@16.0.0:
resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==}
peerDependencies:
i18next: '>= 25.5.2'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -973,6 +1010,10 @@ packages:
terser:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -1079,6 +1120,8 @@ snapshots:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -1591,6 +1634,16 @@ snapshots:
graceful-fs@4.2.11: {}
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
i18next@25.5.2(typescript@5.9.2):
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
typescript: 5.9.2
jiti@2.5.1: {}
js-tokens@4.0.0: {}
@@ -1692,6 +1745,16 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-i18next@16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2):
dependencies:
'@babel/runtime': 7.28.4
html-parse-stringify: 3.0.1
i18next: 25.5.2(typescript@5.9.2)
react: 18.3.1
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
typescript: 5.9.2
react-refresh@0.17.0: {}
react@18.3.1:
@@ -1767,6 +1830,8 @@ snapshots:
fsevents: 2.3.3
lightningcss: 1.30.1
void-elements@3.1.0: {}
w3c-keyname@2.2.8: {}
yallist@3.1.1: {}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { AlertTriangle, X } from "lucide-react";
import { isLinux } from "../lib/platform";
@@ -16,11 +17,13 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
confirmText = "确定",
cancelText = "取消",
confirmText,
cancelText,
onConfirm,
onCancel,
}) => {
const { t } = useTranslation();
if (!isOpen) return null;
return (
@@ -65,13 +68,13 @@ export const ConfirmDialog: React.FC<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"
autoFocus
>
{cancelText}
{cancelText || t("common.cancel")}
</button>
<button
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"
>
{confirmText}
{confirmText || t("common.confirm")}
</button>
</div>
</div>

View File

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

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
X,
RefreshCw,
@@ -24,6 +25,7 @@ interface SettingsModalProps {
}
export default function SettingsModal({ onClose }: SettingsModalProps) {
const { t } = useTranslation();
const [settings, setSettings] = useState<Settings>({
showInTray: true,
minimizeToTrayOnClose: true,
@@ -54,9 +56,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const appVersion = await getVersion();
setVersion(appVersion);
} catch (error) {
console.error("获取版本信息失败:", error);
console.error(t("console.getVersionFailed"), error);
// 失败时不硬编码版本号,显示为未知
setVersion("未知");
setVersion(t("common.unknown"));
}
};
@@ -84,7 +86,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
: undefined,
});
} catch (error) {
console.error("加载设置失败:", error);
console.error(t("console.loadSettingsFailed"), error);
}
};
@@ -95,7 +97,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setConfigPath(path);
}
} catch (error) {
console.error("获取配置路径失败:", error);
console.error(t("console.getConfigPathFailed"), error);
}
};
@@ -108,7 +110,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setResolvedClaudeDir(claudeDir || "");
setResolvedCodexDir(codexDir || "");
} catch (error) {
console.error("获取配置目录失败:", error);
console.error(t("console.getConfigDirFailed"), error);
}
};
@@ -117,7 +119,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const portable = await window.api.isPortable();
setIsPortable(portable);
} catch (error) {
console.error("检测便携模式失败:", error);
console.error(t("console.detectPortableFailed"), error);
}
};
@@ -138,7 +140,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setSettings(payload);
onClose();
} catch (error) {
console.error("保存设置失败:", error);
console.error(t("console.saveSettingsFailed"), error);
}
};
@@ -155,7 +157,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
await updateHandle.downloadAndInstall();
await relaunchApp();
} catch (error) {
console.error("更新失败:", error);
console.error(t("console.updateFailed"), error);
// 更新失败时回退到打开 Releases 页面
await window.api.checkForUpdates();
} finally {
@@ -176,7 +178,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
}, 3000);
}
} catch (error) {
console.error("检查更新失败:", error);
console.error(t("console.checkUpdateFailed"), error);
// 在开发模式下,模拟已是最新版本的响应
if (import.meta.env.DEV) {
setShowUpToDate(true);
@@ -197,7 +199,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
try {
await window.api.openAppConfigFolder();
} catch (error) {
console.error("打开配置文件夹失败:", error);
console.error(t("console.openConfigFolderFailed"), error);
}
};
@@ -228,7 +230,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setResolvedCodexDir(sanitized);
}
} catch (error) {
console.error("选择配置目录失败:", error);
console.error(t("console.selectConfigDirFailed"), error);
}
};
@@ -238,7 +240,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const folder = app === "claude" ? ".claude" : ".codex";
return await join(home, folder);
} catch (error) {
console.error("获取默认配置目录失败:", error);
console.error(t("console.getDefaultConfigDirFailed"), error);
return "";
}
};
@@ -266,8 +268,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const handleOpenReleaseNotes = async () => {
try {
const targetVersion = updateInfo?.availableVersion || version;
const unknownLabel = t("common.unknown");
// 如果未知或为空,回退到 releases 首页
if (!targetVersion || targetVersion === "未知") {
if (!targetVersion || targetVersion === unknownLabel) {
await window.api.openExternal(
"https://github.com/farion1231/cc-switch/releases"
);
@@ -280,7 +283,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
`https://github.com/farion1231/cc-switch/releases/tag/${tag}`
);
} catch (error) {
console.error("打开更新日志失败:", error);
console.error(t("console.openReleaseNotesFailed"), error);
}
};
@@ -300,7 +303,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 标题栏 */}
<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">
{t("settings.title")}
</h2>
<button
onClick={onClose}
@@ -315,16 +318,16 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 窗口行为设置 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("settings.windowBehavior")}
</h3>
<div className="space-y-3">
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-900 dark:text-gray-100">
{t("settings.minimizeToTray")}
</span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
退
{t("settings.minimizeToTrayDescription")}
</p>
</div>
<input
@@ -347,18 +350,18 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 配置文件位置 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("settings.configFileLocation")}
</h3>
<div className="flex items-center gap-2">
<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">
{configPath || "加载中..."}
{configPath || t("common.loading")}
</span>
</div>
<button
onClick={handleOpenConfigFolder}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="打开文件夹"
title={t("settings.openFolder")}
>
<FolderOpen
size={18}
@@ -371,16 +374,15 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 配置目录覆盖 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("settings.configDirectoryOverride")}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed">
WSL 使 Claude Code Codex WSL
{t("settings.configDirectoryDescription")}
</p>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Claude Code
{t("settings.claudeConfigDir")}
</label>
<div className="flex gap-2">
<input
@@ -392,14 +394,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
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"
/>
<button
type="button"
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"
title="浏览目录"
title={t("settings.browseDirectory")}
>
<FolderSearch size={16} />
</button>
@@ -407,7 +409,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
type="button"
onClick={() => handleResetConfigDir("claude")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="恢复默认目录(需保存后生效)"
title={t("settings.resetDefault")}
>
<Undo2 size={16} />
</button>
@@ -416,7 +418,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Codex
{t("settings.codexConfigDir")}
</label>
<div className="flex gap-2">
<input
@@ -428,14 +430,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
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"
/>
<button
type="button"
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"
title="浏览目录"
title={t("settings.browseDirectory")}
>
<FolderSearch size={16} />
</button>
@@ -443,7 +445,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
type="button"
onClick={() => handleResetConfigDir("codex")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="恢复默认目录(需保存后生效)"
title={t("settings.resetDefault")}
>
<Undo2 size={16} />
</button>
@@ -455,7 +457,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{/* 关于 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("common.about")}
</h3>
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
<div className="flex items-start justify-between">
@@ -465,7 +467,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
CC Switch
</p>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{version}
{t("common.version")} {version}
</p>
</div>
</div>
@@ -474,12 +476,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
onClick={handleOpenReleaseNotes}
className="px-2 py-1 text-xs font-medium text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 rounded-lg hover:bg-blue-500/10 transition-colors"
title={
hasUpdate ? "查看该版本更新日志" : "查看当前版本更新日志"
hasUpdate
? t("settings.viewReleaseNotes")
: t("settings.viewCurrentReleaseNotes")
}
>
<span className="inline-flex items-center gap-1">
<ExternalLink size={12} />
{t("settings.releaseNotes")}
</span>
</button>
<button
@@ -498,25 +502,27 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
{isDownloading ? (
<span className="flex items-center gap-1">
<Download size={12} className="animate-pulse" />
...
{t("settings.updating")}
</span>
) : isCheckingUpdate ? (
<span className="flex items-center gap-1">
<RefreshCw size={12} className="animate-spin" />
...
{t("settings.checking")}
</span>
) : hasUpdate ? (
<span className="flex items-center gap-1">
<Download size={12} />
v{updateInfo?.availableVersion}
{t("settings.updateTo", {
version: updateInfo?.availableVersion ?? "",
})}
</span>
) : showUpToDate ? (
<span className="flex items-center gap-1">
<Check size={12} />
{t("settings.upToDate")}
</span>
) : (
"检查更新"
t("settings.checkForUpdates")
)}
</button>
</div>
@@ -531,14 +537,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
{t("common.cancel")}
</button>
<button
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"
>
<Save size={16} />
{t("common.save")}
</button>
</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";
// 导入 Tauri API自动绑定到 window.api
import "./lib/tauri-api";
// 导入国际化配置
import "./i18n";
// 根据平台添加 body class便于平台特定样式
try {
@@ -23,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<UpdateProvider>
<App />
</UpdateProvider>
</React.StrictMode>,
</React.StrictMode>
);