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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user