- i18n: complete remaining internationalization across the UI

- Locales: add and align keys (common.enterValidValue, apiKeyInput.*, jsonEditor.*, claudeConfig.*); fix zh common.unknown mapping
- ProviderForm: localize labels/placeholders/hints/errors; unify JSON/auth validation to providerForm.*; add wizard CTA for Codex custom with i18n; cancel uses common.cancel
- CodexConfigEditor: i18n for quick wizard, labels/placeholders/hints, common config modal (title/help/buttons)
- ClaudeConfigEditor: i18n for main label, common-config toggle/button, modal title/help, footer buttons
- EndpointSpeedTest: localize failed/noEndpoints/done and aria labels
- ApiKeyInput: i18n for placeholder and show/hide aria
- JsonEditor: i18n linter messages
- PresetSelector: remove hardcoded defaults, use i18n keys
- UpdateBadge: i18n close aria
- Build/typecheck: pass; scan shows no visible hardcoded Chinese strings outside locales
This commit is contained in:
Jason
2025-10-07 23:31:00 +08:00
parent 420a4234de
commit 01da9a1eac
13 changed files with 425 additions and 154 deletions

View File

@@ -5,6 +5,7 @@ import { oneDark } from "@codemirror/theme-one-dark";
import { EditorState } from "@codemirror/state"; import { EditorState } from "@codemirror/state";
import { placeholder } from "@codemirror/view"; import { placeholder } from "@codemirror/view";
import { linter, Diagnostic } from "@codemirror/lint"; import { linter, Diagnostic } from "@codemirror/lint";
import { useTranslation } from "react-i18next";
interface JsonEditorProps { interface JsonEditorProps {
value: string; value: string;
@@ -23,6 +24,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
rows = 12, rows = 12,
showValidation = true, showValidation = true,
}) => { }) => {
const { t } = useTranslation();
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null); const viewRef = useRef<EditorView | null>(null);
@@ -46,12 +48,12 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
from: 0, from: 0,
to: doc.length, to: doc.length,
severity: "error", severity: "error",
message: "配置必须是JSON对象不能是数组或其他类型", message: t("jsonEditor.mustBeObject"),
}); });
} }
} catch (e) { } catch (e) {
// 简单处理JSON解析错误 // 简单处理JSON解析错误
const message = e instanceof SyntaxError ? e.message : "JSON格式错误"; const message = e instanceof SyntaxError ? e.message : t("jsonEditor.invalidJson");
diagnostics.push({ diagnostics.push({
from: 0, from: 0,
to: doc.length, to: doc.length,
@@ -62,7 +64,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
return diagnostics; return diagnostics;
}), }),
[showValidation], [showValidation, t],
); );
useEffect(() => { useEffect(() => {

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef, useMemo } from "react"; import React, { useState, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Provider, ProviderCategory, CustomEndpoint } from "../types"; import { Provider, ProviderCategory, CustomEndpoint } from "../types";
import { AppType } from "../lib/tauri-api"; import { AppType } from "../lib/tauri-api";
import { import {
@@ -190,6 +191,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onSubmit, onSubmit,
onClose, onClose,
}) => { }) => {
const { t } = useTranslation();
// 对于 Codex需要分离 auth 和 config // 对于 Codex需要分离 auth 和 config
const isCodex = appType === "codex"; const isCodex = appType === "codex";
@@ -331,21 +333,20 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useState(""); useState("");
const validateSettingsConfig = (value: string): string => { const validateSettingsConfig = (value: string): string => {
return validateJsonConfig(value, "配置内容"); const err = validateJsonConfig(value, "配置内容");
return err ? t("providerForm.configJsonError") : "";
}; };
const validateCodexAuth = (value: string): string => { const validateCodexAuth = (value: string): string => {
if (!value.trim()) { if (!value.trim()) return "";
return "";
}
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return "auth.json 必须是 JSON 对象"; return t("providerForm.authJsonRequired");
} }
return ""; return "";
} catch { } catch {
return "auth.json 格式错误请检查JSON语法"; return t("providerForm.authJsonError");
} }
}; };
@@ -520,7 +521,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setError(""); setError("");
if (!formData.name) { if (!formData.name) {
setError("请填写供应商名称"); setError(t("providerForm.fillSupplierName"));
return; return;
} }
@@ -535,7 +536,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
} }
// Codex: 仅要求 auth.json 必填config.toml 可为空 // Codex: 仅要求 auth.json 必填config.toml 可为空
if (!codexAuth.trim()) { if (!codexAuth.trim()) {
setError("请填写 auth.json 配置"); setError(t("providerForm.fillAuthJson"));
return; return;
} }
@@ -552,7 +553,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
? authJson.OPENAI_API_KEY.trim() ? authJson.OPENAI_API_KEY.trim()
: ""; : "";
if (!key) { if (!key) {
setError("请填写 OPENAI_API_KEY"); setError(t("providerForm.fillApiKey"));
return; return;
} }
} }
@@ -563,7 +564,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
config: codexConfig ?? "", config: codexConfig ?? "",
}; };
} catch (err) { } catch (err) {
setError("auth.json 格式错误请检查JSON语法"); setError(t("providerForm.authJsonError"));
return; return;
} }
} else { } else {
@@ -572,7 +573,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
); );
setSettingsConfigError(currentSettingsError); setSettingsConfigError(currentSettingsError);
if (currentSettingsError) { if (currentSettingsError) {
setError(currentSettingsError); setError(t("providerForm.configJsonError"));
return; return;
} }
@@ -586,21 +587,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
"" ""
).trim(); ).trim();
if (!resolvedValue) { if (!resolvedValue) {
setError(`请填写 ${config.label}`); setError(t("providerForm.fillParameter", { label: config.label }));
return; return;
} }
} }
} }
// Claude: 原有逻辑 // Claude: 原有逻辑
if (!formData.settingsConfig.trim()) { if (!formData.settingsConfig.trim()) {
setError("请填写配置内容"); setError(t("providerForm.fillConfigContent"));
return; return;
} }
try { try {
settingsConfig = JSON.parse(formData.settingsConfig); settingsConfig = JSON.parse(formData.settingsConfig);
} catch (err) { } catch (err) {
setError("配置JSON格式错误请检查语法"); setError(t("providerForm.configJsonError"));
return; return;
} }
} }
@@ -669,7 +670,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (snippetError) { if (snippetError) {
setCommonConfigError(snippetError); setCommonConfigError(snippetError);
if (snippetError.includes("配置 JSON 解析失败")) { if (snippetError.includes("配置 JSON 解析失败")) {
setSettingsConfigError("配置JSON格式错误请检查语法"); setSettingsConfigError(t("providerForm.configJsonError"));
} }
setUseCommonConfig(false); setUseCommonConfig(false);
return; return;
@@ -723,7 +724,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (removeResult.error) { if (removeResult.error) {
setCommonConfigError(removeResult.error); setCommonConfigError(removeResult.error);
if (removeResult.error.includes("配置 JSON 解析失败")) { if (removeResult.error.includes("配置 JSON 解析失败")) {
setSettingsConfigError("配置JSON格式错误请检查语法"); setSettingsConfigError(t("providerForm.configJsonError"));
} }
return; return;
} }
@@ -736,7 +737,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (addResult.error) { if (addResult.error) {
setCommonConfigError(addResult.error); setCommonConfigError(addResult.error);
if (addResult.error.includes("配置 JSON 解析失败")) { if (addResult.error.includes("配置 JSON 解析失败")) {
setSettingsConfigError("配置JSON格式错误请检查语法"); setSettingsConfigError(t("providerForm.configJsonError"));
} }
return; return;
} }
@@ -1456,13 +1457,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onCustomClick={handleCodexCustomClick} onCustomClick={handleCodexCustomClick}
renderCustomDescription={() => ( renderCustomDescription={() => (
<> <>
{t("providerForm.manualConfig")}
<button <button
type="button" type="button"
onClick={() => setIsCodexTemplateModalOpen(true)} onClick={() => setIsCodexTemplateModalOpen(true)}
className="text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors ml-1" className="text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors ml-1"
> >
使 {t("providerForm.useConfigWizard")}
</button> </button>
</> </>
)} )}
@@ -1474,7 +1475,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="name" htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
* {t("providerForm.supplierNameRequired")}
</label> </label>
<input <input
type="text" type="text"
@@ -1482,7 +1483,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
placeholder="例如Anthropic 官方" placeholder={t("providerForm.supplierNamePlaceholder")}
required required
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
@@ -1494,7 +1495,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="websiteUrl" htmlFor="websiteUrl"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
{t("providerForm.websiteLabel")}
</label> </label>
<input <input
type="url" type="url"
@@ -1502,7 +1503,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
name="websiteUrl" name="websiteUrl"
value={formData.websiteUrl} value={formData.websiteUrl}
onChange={handleChange} onChange={handleChange}
placeholder="https://example.com可选" placeholder={t("providerForm.websiteUrlPlaceholder")}
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
/> />
@@ -1516,10 +1517,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
required={!isOfficialPreset} required={!isOfficialPreset}
placeholder={ placeholder={
isOfficialPreset isOfficialPreset
? "官方登录无需填写 API Key直接保存即可" ? t("providerForm.officialNoApiKey")
: shouldShowKimiSelector : shouldShowKimiSelector
? "填写后可获取模型列表" ? t("providerForm.kimiApiKeyHint")
: "只需要填这里,下方配置会自动填充" : t("providerForm.apiKeyAutoFill")
} }
disabled={isOfficialPreset} disabled={isOfficialPreset}
/> />
@@ -1531,7 +1532,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors" className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
> >
API Key {t("providerForm.getApiKey")}
</a> </a>
</div> </div>
)} )}
@@ -1543,7 +1544,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
templateValueEntries.length > 0 && ( templateValueEntries.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {selectedTemplatePreset.name.trim()} * {t("providerForm.parameterConfig", { name: selectedTemplatePreset.name.trim() })}
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{templateValueEntries.map(([key, config]) => ( {templateValueEntries.map(([key, config]) => (
@@ -1616,7 +1617,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="baseUrl" htmlFor="baseUrl"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
{t("providerForm.apiEndpoint")}
</label> </label>
<button <button
type="button" type="button"
@@ -1624,7 +1625,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors" className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
> >
<Zap className="h-3.5 w-3.5" /> <Zap className="h-3.5 w-3.5" />
{t("providerForm.manageAndTest")}
</button> </button>
</div> </div>
<input <input
@@ -1632,13 +1633,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
id="baseUrl" id="baseUrl"
value={baseUrl} value={baseUrl}
onChange={(e) => handleBaseUrlChange(e.target.value)} onChange={(e) => handleBaseUrlChange(e.target.value)}
placeholder="https://your-api-endpoint.com" placeholder={t("providerForm.apiEndpointPlaceholder")}
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
/> />
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg"> <div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400"> <p className="text-xs text-amber-600 dark:text-amber-400">
💡 Claude API {t("providerForm.apiHint")}
</p> </p>
</div> </div>
</div> </div>
@@ -1677,8 +1678,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onChange={handleCodexApiKeyChange} onChange={handleCodexApiKeyChange}
placeholder={ placeholder={
isCodexOfficialPreset isCodexOfficialPreset
? "官方无需填写 API Key直接保存即可" ? t("codexConfig.codexOfficialNoApiKey")
: "只需要填这里,下方 auth.json 会自动填充" : t("codexConfig.codexApiKeyAutoFill")
} }
disabled={isCodexOfficialPreset} disabled={isCodexOfficialPreset}
required={ required={
@@ -1695,7 +1696,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors" className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
> >
API Key {t("providerForm.getApiKey")}
</a> </a>
</div> </div>
)} )}
@@ -1709,7 +1710,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="codexBaseUrl" htmlFor="codexBaseUrl"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
{t("codexConfig.apiUrlLabel")}
</label> </label>
<button <button
type="button" type="button"
@@ -1717,7 +1718,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors" className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
> >
<Zap className="h-3.5 w-3.5" /> <Zap className="h-3.5 w-3.5" />
{t("providerForm.manageAndTest")}
</button> </button>
</div> </div>
<input <input
@@ -1725,7 +1726,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
id="codexBaseUrl" id="codexBaseUrl"
value={codexBaseUrl} value={codexBaseUrl}
onChange={(e) => handleCodexBaseUrlChange(e.target.value)} onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
placeholder="https://your-api-endpoint.com/v1" placeholder={t("providerForm.codexApiEndpointPlaceholder")}
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
/> />
@@ -1800,7 +1801,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="anthropicModel" htmlFor="anthropicModel"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
() {t("providerForm.mainModel")}
</label> </label>
<input <input
type="text" type="text"
@@ -1809,7 +1810,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onChange={(e) => onChange={(e) =>
handleModelChange("ANTHROPIC_MODEL", e.target.value) handleModelChange("ANTHROPIC_MODEL", e.target.value)
} }
placeholder="例如: GLM-4.5" placeholder={t("providerForm.mainModelPlaceholder")}
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
/> />
@@ -1820,7 +1821,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
htmlFor="anthropicSmallFastModel" htmlFor="anthropicSmallFastModel"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
() {t("providerForm.fastModel")}
</label> </label>
<input <input
type="text" type="text"
@@ -1832,7 +1833,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
e.target.value e.target.value
) )
} }
placeholder="例如: GLM-4.5-Air" placeholder={t("providerForm.fastModelPlaceholder")}
autoComplete="off" autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
/> />
@@ -1841,7 +1842,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg"> <div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400"> <p className="text-xs text-amber-600 dark:text-amber-400">
💡 使 {t("providerForm.modelHint")}
</p> </p>
</div> </div>
</div> </div>
@@ -1872,7 +1873,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
> >
{t("common.cancel")}
</button> </button>
<button <button
type="submit" type="submit"

View File

@@ -1,5 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useTranslation } from "react-i18next";
interface ApiKeyInputProps { interface ApiKeyInputProps {
value: string; value: string;
@@ -14,12 +15,13 @@ interface ApiKeyInputProps {
const ApiKeyInput: React.FC<ApiKeyInputProps> = ({ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
value, value,
onChange, onChange,
placeholder = "请输入API Key", placeholder,
disabled = false, disabled = false,
required = false, required = false,
label = "API Key", label = "API Key",
id = "apiKey", id = "apiKey",
}) => { }) => {
const { t } = useTranslation();
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const toggleShowKey = () => { const toggleShowKey = () => {
@@ -46,7 +48,7 @@ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
id={id} id={id}
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder={placeholder} placeholder={placeholder ?? t("apiKeyInput.placeholder")}
disabled={disabled} disabled={disabled}
required={required} required={required}
autoComplete="off" autoComplete="off"
@@ -57,7 +59,7 @@ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
type="button" type="button"
onClick={toggleShowKey} onClick={toggleShowKey}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors" className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
aria-label={showKey ? "隐藏API Key" : "显示API Key"} aria-label={showKey ? t("apiKeyInput.hide") : t("apiKeyInput.show")}
> >
{showKey ? <EyeOff size={16} /> : <Eye size={16} />} {showKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button> </button>

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import JsonEditor from "../JsonEditor"; import JsonEditor from "../JsonEditor";
import { X, Save } from "lucide-react"; import { X, Save } from "lucide-react";
import { isLinux } from "../../lib/platform"; import { isLinux } from "../../lib/platform";
import { useTranslation } from "react-i18next";
interface ClaudeConfigEditorProps { interface ClaudeConfigEditorProps {
value: string; value: string;
@@ -24,6 +25,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
commonConfigError, commonConfigError,
configError, configError,
}) => { }) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false);
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -82,7 +84,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
htmlFor="settingsConfig" htmlFor="settingsConfig"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
Claude Code (JSON) * {t("claudeConfig.configLabel")}
</label> </label>
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"> <label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
<input <input
@@ -91,7 +93,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
onChange={(e) => onCommonConfigToggle(e.target.checked)} onChange={(e) => onCommonConfigToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2" className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/> />
{t("claudeConfig.writeCommonConfig")}
</label> </label>
</div> </div>
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
@@ -100,7 +102,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
onClick={() => setIsCommonConfigModalOpen(true)} onClick={() => setIsCommonConfigModalOpen(true)}
className="text-xs text-blue-500 dark:text-blue-400 hover:underline" className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
> >
{t("claudeConfig.editCommonConfig")}
</button> </button>
</div> </div>
{commonConfigError && !isCommonConfigModalOpen && ( {commonConfigError && !isCommonConfigModalOpen && (
@@ -124,7 +126,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p> <p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
)} )}
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
Claude Code settings.json {t("claudeConfig.fullSettingsHint")}
</p> </p>
{isCommonConfigModalOpen && ( {isCommonConfigModalOpen && (
<div <div
@@ -145,13 +147,13 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
{/* Header - 统一标题栏样式 */} {/* Header - 统一标题栏样式 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800"> <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{t("claudeConfig.editCommonConfigTitle")}
</h2> </h2>
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors" className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
aria-label="关闭" aria-label={t("common.close")}
> >
<X size={18} /> <X size={18} />
</button> </button>
@@ -160,7 +162,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
{/* Content - 统一内容区域样式 */} {/* Content - 统一内容区域样式 */}
<div className="flex-1 overflow-auto p-6 space-y-4"> <div className="flex-1 overflow-auto p-6 space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
"写入通用配置" settings.json {t("claudeConfig.commonConfigHint")}
</p> </p>
<JsonEditor <JsonEditor
value={commonConfigSnippet} value={commonConfigSnippet}
@@ -182,7 +184,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
onClick={closeModal} onClick={closeModal}
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
> >
{t("common.cancel")}
</button> </button>
<button <button
type="button" type="button"
@@ -190,7 +192,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2" className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
> >
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
{t("common.save")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from "react";
import { X, Save } from "lucide-react"; import { X, Save } from "lucide-react";
import { isLinux } from "../../lib/platform"; import { isLinux } from "../../lib/platform";
import { useTranslation } from "react-i18next";
import { import {
generateThirdPartyAuth, generateThirdPartyAuth,
@@ -74,6 +75,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
setIsTemplateModalOpen: externalSetTemplateModalOpen, setIsTemplateModalOpen: externalSetTemplateModalOpen,
}) => { }) => {
const { t } = useTranslation();
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// 使用内部状态或外部状态 // 使用内部状态或外部状态
@@ -236,7 +238,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
htmlFor="codexAuth" htmlFor="codexAuth"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
auth.json (JSON) * {t("codexConfig.authJson")}
</label> </label>
<textarea <textarea
@@ -244,9 +246,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
value={authValue} value={authValue}
onChange={(e) => handleAuthChange(e.target.value)} onChange={(e) => handleAuthChange(e.target.value)}
onBlur={onAuthBlur} onBlur={onAuthBlur}
placeholder={`{ placeholder={t("codexConfig.authJsonPlaceholder")}
"OPENAI_API_KEY": "sk-your-api-key-here"
}`}
rows={6} rows={6}
required required
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-y min-h-[8rem]" className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-y min-h-[8rem]"
@@ -266,7 +266,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
)} )}
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
Codex auth.json {t("codexConfig.authJsonHint")}
</p> </p>
</div> </div>
@@ -276,7 +276,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
htmlFor="codexConfig" htmlFor="codexConfig"
className="block text-sm font-medium text-gray-900 dark:text-gray-100" className="block text-sm font-medium text-gray-900 dark:text-gray-100"
> >
config.toml (TOML) {t("codexConfig.configToml")}
</label> </label>
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"> <label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
@@ -286,7 +286,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onChange={(e) => onCommonConfigToggle(e.target.checked)} onChange={(e) => onCommonConfigToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2" className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/> />
{t("codexConfig.writeCommonConfig")}
</label> </label>
</div> </div>
@@ -296,7 +296,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onClick={() => setIsCommonConfigModalOpen(true)} onClick={() => setIsCommonConfigModalOpen(true)}
className="text-xs text-blue-500 dark:text-blue-400 hover:underline" className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
> >
{t("codexConfig.editCommonConfig")}
</button> </button>
</div> </div>
@@ -325,7 +325,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
/> />
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
Codex config.toml {t("codexConfig.configTomlHint")}
</p> </p>
</div> </div>
@@ -348,14 +348,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div className="flex h-full min-h-0 flex-col" role="form"> <div className="flex h-full min-h-0 flex-col" role="form">
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800"> <div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{t("codexConfig.quickWizard")}
</h2> </h2>
<button <button
type="button" type="button"
onClick={closeTemplateModal} onClick={closeTemplateModal}
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100" className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
aria-label="关闭" aria-label={t("common.close")}
> >
<X size={18} /> <X size={18} />
</button> </button>
@@ -364,15 +364,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6"> <div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20"> <div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200"> <p className="text-sm text-blue-800 dark:text-blue-200">
auth.json config.toml {t("codexConfig.wizardHint")}
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
API * {t("codexConfig.apiKeyLabel")}
</label> </label>
<input <input
@@ -382,8 +381,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onChange={(e) => setTemplateApiKey(e.target.value)} onChange={(e) => setTemplateApiKey(e.target.value)}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
pattern=".*\S.*" pattern=".*\S.*"
title="请输入有效的内容" title={t("common.enterValidValue")}
placeholder="sk-your-api-key-here" placeholder={t("codexConfig.apiKeyPlaceholder")}
required required
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
@@ -391,7 +390,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
* {t("codexConfig.supplierNameLabel")}
</label> </label>
<input <input
@@ -405,21 +404,21 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
} }
}} }}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
placeholder="例如Codex 官方" placeholder={t("codexConfig.supplierNamePlaceholder")}
required required
pattern=".*\S.*" pattern=".*\S.*"
title="请输入有效的内容" title={t("common.enterValidValue")}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
使 {t("codexConfig.supplierNameHint")}
</p> </p>
</div> </div>
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.supplierCodeLabel")}
</label> </label>
<input <input
@@ -427,18 +426,18 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
value={templateProviderName} value={templateProviderName}
onChange={(e) => setTemplateProviderName(e.target.value)} onChange={(e) => setTemplateProviderName(e.target.value)}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
placeholder="custom可选" placeholder={t("codexConfig.supplierCodePlaceholder")}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
custom {t("codexConfig.supplierCodeHint")}
</p> </p>
</div> </div>
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
API * {t("codexConfig.apiUrlLabel")}
</label> </label>
<input <input
@@ -447,7 +446,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
ref={baseUrlInputRef} ref={baseUrlInputRef}
onChange={(e) => setTemplateBaseUrl(e.target.value)} onChange={(e) => setTemplateBaseUrl(e.target.value)}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
placeholder="https://your-api-endpoint.com/v1" placeholder={t("codexConfig.apiUrlPlaceholder")}
required required
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
@@ -455,7 +454,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.websiteLabel")}
</label> </label>
<input <input
@@ -463,18 +462,18 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
value={templateWebsiteUrl} value={templateWebsiteUrl}
onChange={(e) => setTemplateWebsiteUrl(e.target.value)} onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
placeholder="https://example.com" placeholder={t("codexConfig.websitePlaceholder")}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.websiteHint")}
</p> </p>
</div> </div>
<div> <div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100"> <label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
* {t("codexConfig.modelNameLabel")}
</label> </label>
<input <input
@@ -484,8 +483,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onChange={(e) => setTemplateModelName(e.target.value)} onChange={(e) => setTemplateModelName(e.target.value)}
onKeyDown={handleTemplateInputKeyDown} onKeyDown={handleTemplateInputKeyDown}
pattern=".*\S.*" pattern=".*\S.*"
title="请输入有效的内容" title={t("common.enterValidValue")}
placeholder="gpt-5-codex" placeholder={t("codexConfig.modelNamePlaceholder")}
required required
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/> />
@@ -497,7 +496,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
templateBaseUrl) && ( templateBaseUrl) && (
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700"> <div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.configPreview")}
</h3> </h3>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
@@ -543,7 +542,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onClick={closeTemplateModal} onClick={closeTemplateModal}
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100" className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
> >
{t("common.cancel")}
</button> </button>
<button <button
@@ -558,7 +557,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700" className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
> >
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
{t("codexConfig.applyConfig")}
</button> </button>
</div> </div>
</div> </div>
@@ -588,14 +587,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800"> <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Codex {t("codexConfig.editCommonConfigTitle")}
</h2> </h2>
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors" className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
aria-label="关闭" aria-label={t("common.close")}
> >
<X size={18} /> <X size={18} />
</button> </button>
@@ -605,7 +604,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
<div className="flex-1 overflow-auto p-6 space-y-4"> <div className="flex-1 overflow-auto p-6 space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
"写入通用配置" config.toml {t("codexConfig.commonConfigHint")}
</p> </p>
<textarea <textarea
@@ -646,7 +645,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onClick={closeModal} onClick={closeModal}
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors" className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
> >
{t("common.cancel")}
</button> </button>
<button <button
@@ -655,7 +654,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2" className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
> >
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
{t("common.save")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react"; import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react";
import { isLinux } from "../../lib/platform"; import { isLinux } from "../../lib/platform";
@@ -74,6 +75,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
onClose, onClose,
onCustomEndpointsChange, onCustomEndpointsChange,
}) => { }) => {
const { t } = useTranslation();
const [entries, setEntries] = useState<EndpointEntry[]>(() => const [entries, setEntries] = useState<EndpointEntry[]>(() =>
buildInitialEntries(initialEndpoints, value), buildInitialEntries(initialEndpoints, value),
); );
@@ -127,14 +129,14 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
return Array.from(map.values()); return Array.from(map.values());
}); });
} catch (error) { } catch (error) {
console.error("加载自定义端点失败:", error); console.error(t("endpointTest.loadEndpointsFailed"), error);
} }
}; };
if (visible) { if (visible) {
loadCustomEndpoints(); loadCustomEndpoints();
} }
}, [appType, visible, providerId]); }, [appType, visible, providerId, t]);
useEffect(() => { useEffect(() => {
setEntries((prev) => { setEntries((prev) => {
@@ -214,7 +216,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
let errorMsg: string | null = null; let errorMsg: string | null = null;
if (!candidate) { if (!candidate) {
errorMsg = "请输入有效的 URL"; errorMsg = t("endpointTest.enterValidUrl");
} }
let parsed: URL | null = null; let parsed: URL | null = null;
@@ -222,12 +224,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
try { try {
parsed = new URL(candidate); parsed = new URL(candidate);
} catch { } catch {
errorMsg = "URL 格式不正确"; errorMsg = t("endpointTest.invalidUrlFormat");
} }
} }
if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) { if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
errorMsg = "仅支持 HTTP/HTTPS"; errorMsg = t("endpointTest.onlyHttps");
} }
let sanitized = ""; let sanitized = "";
@@ -236,7 +238,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError // 使用当前 entries 做去重校验,避免依赖可能过期的 addError
const isDuplicate = entries.some((entry) => entry.url === sanitized); const isDuplicate = entries.some((entry) => entry.url === sanitized);
if (isDuplicate) { if (isDuplicate) {
errorMsg = "该地址已存在"; errorMsg = t("endpointTest.urlExists");
} }
} }
@@ -277,11 +279,11 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
setAddError(message || "保存失败,请重试"); setAddError(message || t("endpointTest.saveFailed"));
console.error("添加自定义端点失败:", error); console.error(t("endpointTest.addEndpointFailed"), error);
} }
}, },
[customUrl, entries, normalizedSelected, onChange, appType, providerId], [customUrl, entries, normalizedSelected, onChange, appType, providerId, t],
); );
const handleRemoveEndpoint = useCallback( const handleRemoveEndpoint = useCallback(
@@ -291,7 +293,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
try { try {
await window.api.removeCustomEndpoint(appType, providerId, entry.url); await window.api.removeCustomEndpoint(appType, providerId, entry.url);
} catch (error) { } catch (error) {
console.error("删除自定义端点失败:", error); console.error(t("endpointTest.removeEndpointFailed"), error);
return; return;
} }
} }
@@ -306,18 +308,18 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
return next; return next;
}); });
}, },
[normalizedSelected, onChange, appType, providerId], [normalizedSelected, onChange, appType, providerId, t],
); );
const runSpeedTest = useCallback(async () => { const runSpeedTest = useCallback(async () => {
const urls = entries.map((entry) => entry.url); const urls = entries.map((entry) => entry.url);
if (urls.length === 0) { if (urls.length === 0) {
setLastError("请先添加端点"); setLastError(t("endpointTest.pleaseAddEndpoint"));
return; return;
} }
if (typeof window === "undefined" || !window.api?.testApiEndpoints) { if (typeof window === "undefined" || !window.api?.testApiEndpoints) {
setLastError("测速功能不可用"); setLastError(t("endpointTest.testUnavailable"));
return; return;
} }
@@ -350,7 +352,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
...entry, ...entry,
latency: null, latency: null,
status: undefined, status: undefined,
error: "未返回结果", error: t("endpointTest.noResult"),
}; };
} }
return { return {
@@ -374,12 +376,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : `测速失败: ${String(error)}`; error instanceof Error ? error.message : `${t("endpointTest.testFailed", { error: String(error) })}`;
setLastError(message); setLastError(message);
} finally { } finally {
setIsTesting(false); setIsTesting(false);
} }
}, [entries, autoSelect, appType, normalizedSelected, onChange]); }, [entries, autoSelect, appType, normalizedSelected, onChange, t]);
const handleSelect = useCallback( const handleSelect = useCallback(
async (url: string) => { async (url: string) => {
@@ -431,13 +433,13 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800"> <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100"> <h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
{t("endpointTest.title")}
</h3> </h3>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors" className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
aria-label="关闭" aria-label={t("common.close")}
> >
<X size={16} /> <X size={16} />
</button> </button>
@@ -448,7 +450,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
{/* 测速控制栏 */} {/* 测速控制栏 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
{entries.length} {entries.length} {t("endpointTest.endpoints")}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400"> <label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
@@ -458,7 +460,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
onChange={(event) => setAutoSelect(event.target.checked)} onChange={(event) => setAutoSelect(event.target.checked)}
className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600" className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600"
/> />
{t("endpointTest.autoSelect")}
</label> </label>
<button <button
type="button" type="button"
@@ -469,12 +471,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
{isTesting ? ( {isTesting ? (
<> <>
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("endpointTest.testing")}
</> </>
) : ( ) : (
<> <>
<Zap className="h-3.5 w-3.5" /> <Zap className="h-3.5 w-3.5" />
{t("endpointTest.testSpeed")}
</> </>
)} )}
</button> </button>
@@ -487,7 +489,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
<input <input
type="url" type="url"
value={customUrl} value={customUrl}
placeholder="https://api.example.com" placeholder={t("endpointTest.addEndpointPlaceholder")}
onChange={(event) => setCustomUrl(event.target.value)} onChange={(event) => setCustomUrl(event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === "Enter") { if (event.key === "Enter") {
@@ -567,7 +569,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
) : isTesting ? ( ) : isTesting ? (
<Loader2 className="h-4 w-4 animate-spin text-gray-400" /> <Loader2 className="h-4 w-4 animate-spin text-gray-400" />
) : entry.error ? ( ) : entry.error ? (
<div className="text-xs text-gray-400"></div> <div className="text-xs text-gray-400">{t("endpointTest.failed")}</div>
) : ( ) : (
<div className="text-xs text-gray-400"></div> <div className="text-xs text-gray-400"></div>
)} )}
@@ -589,7 +591,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
</div> </div>
) : ( ) : (
<div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400"> <div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400">
{t("endpointTest.noEndpoints")}
</div> </div>
)} )}
@@ -609,7 +611,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
onClick={onClose} onClick={onClose}
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium" className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium"
> >
{t("endpointTest.done")}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, RefreshCw, AlertCircle } from "lucide-react"; import { ChevronDown, RefreshCw, AlertCircle } from "lucide-react";
interface KimiModel { interface KimiModel {
@@ -26,6 +27,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
onModelChange, onModelChange,
disabled = false, disabled = false,
}) => { }) => {
const { t } = useTranslation();
const [models, setModels] = useState<KimiModel[]>([]); const [models, setModels] = useState<KimiModel[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -34,7 +36,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
// 获取模型列表 // 获取模型列表
const fetchModelsWithKey = async (key: string) => { const fetchModelsWithKey = async (key: string) => {
if (!key) { if (!key) {
setError("请先填写 API Key"); setError(t("kimiSelector.fillApiKeyFirst"));
return; return;
} }
@@ -50,7 +52,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`请求失败: ${response.status} ${response.statusText}`); throw new Error(t("kimiSelector.requestFailed", { error: `${response.status} ${response.statusText}` }));
} }
const data = await response.json(); const data = await response.json();
@@ -58,11 +60,11 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
if (data.data && Array.isArray(data.data)) { if (data.data && Array.isArray(data.data)) {
setModels(data.data); setModels(data.data);
} else { } else {
throw new Error("返回数据格式错误"); throw new Error(t("kimiSelector.invalidData"));
} }
} catch (err) { } catch (err) {
console.error("获取模型列表失败:", err); console.error(t("kimiSelector.fetchModelsFailed") + ":", err);
setError(err instanceof Error ? err.message : "获取模型列表失败"); setError(err instanceof Error ? err.message : t("kimiSelector.fetchModelsFailed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -110,10 +112,10 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
> >
<option value=""> <option value="">
{loading {loading
? "加载中..." ? t("common.loading")
: models.length === 0 : models.length === 0
? "暂无模型" ? t("kimiSelector.noModels")
: "请选择模型"} : t("kimiSelector.pleaseSelectModel")}
</option> </option>
{models.map((model) => ( {models.map((model) => (
<option key={model.id} value={model.id}> <option key={model.id} value={model.id}>
@@ -133,7 +135,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("kimiSelector.modelConfig")}
</h3> </h3>
<button <button
type="button" type="button"
@@ -142,7 +144,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<RefreshCw size={12} className={loading ? "animate-spin" : ""} /> <RefreshCw size={12} className={loading ? "animate-spin" : ""} />
{t("kimiSelector.refreshModels")}
</button> </button>
</div> </div>
@@ -158,12 +160,12 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ModelSelect <ModelSelect
label="主模型" label={t("kimiSelector.mainModel")}
value={anthropicModel} value={anthropicModel}
onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)} onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)}
/> />
<ModelSelect <ModelSelect
label="快速模型" label={t("kimiSelector.fastModel")}
value={anthropicSmallFastModel} value={anthropicSmallFastModel}
onChange={(value) => onChange={(value) =>
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value) onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value)
@@ -174,7 +176,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
{!apiKey.trim() && ( {!apiKey.trim() && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg"> <div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400"> <p className="text-xs text-amber-600 dark:text-amber-400">
💡 API Key {t("kimiSelector.apiKeyHint")}
</p> </p>
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { Zap } from "lucide-react"; import { Zap } from "lucide-react";
import { ProviderCategory } from "../../types"; import { ProviderCategory } from "../../types";
import { ClaudeIcon, CodexIcon } from "../BrandIcons"; import { ClaudeIcon, CodexIcon } from "../BrandIcons";
@@ -20,14 +21,16 @@ interface PresetSelectorProps {
} }
const PresetSelector: React.FC<PresetSelectorProps> = ({ const PresetSelector: React.FC<PresetSelectorProps> = ({
title = "选择配置类型", title,
presets, presets,
selectedIndex, selectedIndex,
onSelectPreset, onSelectPreset,
onCustomClick, onCustomClick,
customLabel = "自定义", customLabel,
renderCustomDescription, renderCustomDescription,
}) => { }) => {
const { t } = useTranslation();
const getButtonClass = (index: number, preset?: Preset) => { const getButtonClass = (index: number, preset?: Preset) => {
const isSelected = selectedIndex === index; const isSelected = selectedIndex === index;
const baseClass = const baseClass =
@@ -54,14 +57,14 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
if (renderCustomDescription) { if (renderCustomDescription) {
return renderCustomDescription(); return renderCustomDescription();
} }
return "手动配置供应商,需要填写完整的配置信息"; return t("presetSelector.customDescription");
} }
if (selectedIndex !== null && selectedIndex >= 0) { if (selectedIndex !== null && selectedIndex >= 0) {
const preset = presets[selectedIndex]; const preset = presets[selectedIndex];
return preset?.isOfficial || preset?.category === "official" return preset?.isOfficial || preset?.category === "official"
? "官方登录,不需要填写 API Key" ? t("presetSelector.officialDescription")
: "使用预设配置,只需填写 API Key"; : t("presetSelector.presetDescription");
} }
return null; return null;
@@ -71,7 +74,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3"> <label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{title} {title || t("presetSelector.title")}
</label> </label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button <button
@@ -79,7 +82,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
className={`${getButtonClass(-1)} ${selectedIndex === -1 ? "" : ""}`} className={`${getButtonClass(-1)} ${selectedIndex === -1 ? "" : ""}`}
onClick={onCustomClick} onClick={onCustomClick}
> >
{customLabel} {customLabel || t("presetSelector.custom")}
</button> </button>
{presets.map((preset, index) => ( {presets.map((preset, index) => (
<button <button

View File

@@ -71,12 +71,12 @@ const ProviderList: React.FC<ProviderListProps> = ({
const applied = await window.api.isClaudePluginApplied(); const applied = await window.api.isClaudePluginApplied();
setClaudeApplied(applied); setClaudeApplied(applied);
} catch (error) { } catch (error) {
console.error("检测 Claude 插件配置失败:", error); console.error(t("console.setupListenerFailed"), error);
setClaudeApplied(false); setClaudeApplied(false);
} }
}; };
checkClaude(); checkClaude();
}, [appType, currentProviderId, providers]); }, [appType, currentProviderId, providers, t]);
const handleApplyToClaudePlugin = async () => { const handleApplyToClaudePlugin = async () => {
try { try {
@@ -182,7 +182,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
handleUrlClick(provider.websiteUrl!); handleUrlClick(provider.websiteUrl!);
}} }}
className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors" className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors"
title={`访问 ${provider.websiteUrl}`} title={t("providerForm.visitWebsite", { url: provider.websiteUrl })}
> >
{provider.websiteUrl} {provider.websiteUrl}
</button> </button>

View File

@@ -369,7 +369,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
alert(`${t("settings.configExported")}\n${result.filePath}`); alert(`${t("settings.configExported")}\n${result.filePath}`);
} }
} catch (error) { } catch (error) {
console.error("导出配置失败:", error); console.error(t("settings.exportFailedError"), error);
alert(`${t("settings.exportFailed")}: ${error}`); alert(`${t("settings.exportFailed")}: ${error}`);
} }
}; };
@@ -384,7 +384,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
setImportError(''); setImportError('');
} }
} catch (error) { } catch (error) {
console.error('选择文件失败:', error); console.error(t("settings.selectFileFailed") + ":", error);
alert(`${t("settings.selectFileFailed")}: ${error}`); alert(`${t("settings.selectFileFailed")}: ${error}`);
} }
}; };

View File

@@ -1,5 +1,6 @@
import { X, Download } from "lucide-react"; import { X, Download } from "lucide-react";
import { useUpdate } from "../contexts/UpdateContext"; import { useUpdate } from "../contexts/UpdateContext";
import { useTranslation } from "react-i18next";
interface UpdateBadgeProps { interface UpdateBadgeProps {
className?: string; className?: string;
@@ -8,6 +9,7 @@ interface UpdateBadgeProps {
export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) { export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate(); const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate();
const { t } = useTranslation();
// 如果没有更新或已关闭,不显示 // 如果没有更新或已关闭,不显示
if (!hasUpdate || isDismissed || !updateInfo) { if (!hasUpdate || isDismissed || !updateInfo) {
@@ -52,7 +54,7 @@ export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
transition-colors transition-colors
focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:outline-none focus:ring-2 focus:ring-blue-500/20
" "
aria-label="关闭更新提醒" aria-label={t("common.close")}
> >
<X className="w-3 h-3 text-gray-400 dark:text-gray-500" /> <X className="w-3 h-3 text-gray-400 dark:text-gray-500" />
</button> </button>

View File

@@ -17,7 +17,25 @@
"loading": "Loading...", "loading": "Loading...",
"success": "Success", "success": "Success",
"error": "Error", "error": "Error",
"unknown": "Unknown" "unknown": "Unknown",
"enterValidValue": "Please enter a valid value"
},
"apiKeyInput": {
"placeholder": "Enter API Key",
"show": "Show API Key",
"hide": "Hide API Key"
},
"jsonEditor": {
"mustBeObject": "Configuration must be a JSON object, not an array or other type",
"invalidJson": "Invalid JSON format"
},
"claudeConfig": {
"configLabel": "Claude Code settings.json (JSON) *",
"writeCommonConfig": "Write Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Common Config Snippet",
"commonConfigHint": "This snippet will be merged into settings.json when 'Write Common Config' is checked",
"fullSettingsHint": "Full Claude Code settings.json content"
}, },
"header": { "header": {
"viewOnGithub": "View on GitHub", "viewOnGithub": "View on GitHub",
@@ -96,7 +114,8 @@
"upToDate": "Up to Date", "upToDate": "Up to Date",
"releaseNotes": "Release Notes", "releaseNotes": "Release Notes",
"viewReleaseNotes": "View release notes for this version", "viewReleaseNotes": "View release notes for this version",
"viewCurrentReleaseNotes": "View current version release notes" "viewCurrentReleaseNotes": "View current version release notes",
"exportFailedError": "Export config failed:"
}, },
"apps": { "apps": {
"claude": "Claude Code", "claude": "Claude Code",
@@ -120,5 +139,114 @@
"selectConfigDirFailed": "Failed to select config directory:", "selectConfigDirFailed": "Failed to select config directory:",
"getDefaultConfigDirFailed": "Failed to get default config directory:", "getDefaultConfigDirFailed": "Failed to get default config directory:",
"openReleaseNotesFailed": "Failed to open release notes:" "openReleaseNotesFailed": "Failed to open release notes:"
},
"providerForm": {
"supplierName": "Provider Name",
"supplierNameRequired": "Provider Name *",
"supplierNamePlaceholder": "e.g., Anthropic Official",
"websiteUrl": "Website URL",
"websiteUrlPlaceholder": "https://example.com (optional)",
"apiEndpoint": "API Endpoint",
"apiEndpointPlaceholder": "https://your-api-endpoint.com",
"codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1",
"manageAndTest": "Manage & Test",
"configContent": "Config Content",
"useConfigWizard": "Use Configuration Wizard",
"manualConfig": "Manually configure provider, requires complete configuration, or",
"officialNoApiKey": "Official login does not require API Key, save directly",
"codexOfficialNoApiKey": "Official does not require API Key, save directly",
"kimiApiKeyHint": "Fill in to get model list",
"apiKeyAutoFill": "Just fill in here, config below will be auto-filled",
"codexApiKeyAutoFill": "Just fill in here, auth.json below will be auto-filled",
"getApiKey": "Get API Key",
"parameterConfig": "Parameter Config - {{name}} *",
"mainModel": "Main Model (optional)",
"mainModelPlaceholder": "e.g., GLM-4.5",
"fastModel": "Fast Model (optional)",
"fastModelPlaceholder": "e.g., GLM-4.5-Air",
"modelHint": "💡 Leave blank to use provider's default model",
"apiHint": "💡 Fill in Claude API compatible service endpoint",
"fillSupplierName": "Please fill in provider name",
"fillConfigContent": "Please fill in configuration content",
"fillParameter": "Please fill in {{label}}",
"configJsonError": "Config JSON format error, please check syntax",
"authJsonRequired": "auth.json must be a JSON object",
"authJsonError": "auth.json format error, please check JSON syntax",
"fillAuthJson": "Please fill in auth.json configuration",
"fillApiKey": "Please fill in OPENAI_API_KEY",
"visitWebsite": "Visit {{url}}"
},
"endpointTest": {
"title": "API Endpoint Management",
"endpoints": "endpoints",
"autoSelect": "Auto Select",
"testSpeed": "Test",
"testing": "Testing",
"addEndpointPlaceholder": "https://api.example.com",
"done": "Done",
"noEndpoints": "No endpoints",
"failed": "Failed",
"enterValidUrl": "Please enter a valid URL",
"invalidUrlFormat": "Invalid URL format",
"onlyHttps": "Only HTTP/HTTPS supported",
"urlExists": "This URL already exists",
"saveFailed": "Save failed, please try again",
"loadEndpointsFailed": "Failed to load custom endpoints:",
"addEndpointFailed": "Failed to add custom endpoint:",
"removeEndpointFailed": "Failed to remove custom endpoint:",
"pleaseAddEndpoint": "Please add an endpoint first",
"testUnavailable": "Speed test unavailable",
"noResult": "No result returned",
"testFailed": "Speed test failed: {{error}}"
},
"codexConfig": {
"quickWizard": "Quick Configuration Wizard",
"authJson": "auth.json (JSON) *",
"authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}",
"authJsonHint": "Codex auth.json configuration content",
"configToml": "config.toml (TOML)",
"configTomlHint": "Codex config.toml configuration content",
"writeCommonConfig": "Write Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Codex Common Config Snippet",
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
"wizardHint": "Enter key parameters, the system will automatically generate standard auth.json and config.toml configuration.",
"apiKeyLabel": "API Key *",
"apiKeyPlaceholder": "sk-your-api-key-here",
"supplierNameLabel": "Provider Name *",
"supplierNamePlaceholder": "e.g., Codex Official",
"supplierNameHint": "Will be displayed in the provider list, can use Chinese",
"supplierCodeLabel": "Provider Code (English)",
"supplierCodePlaceholder": "custom (optional)",
"supplierCodeHint": "Will be used as identifier in config file, defaults to custom",
"apiUrlLabel": "API Request URL *",
"apiUrlPlaceholder": "https://your-api-endpoint.com/v1",
"websiteLabel": "Website URL",
"websitePlaceholder": "https://example.com",
"websiteHint": "Official website address (optional)",
"modelNameLabel": "Model Name *",
"modelNamePlaceholder": "gpt-5-codex",
"configPreview": "Configuration Preview",
"applyConfig": "Apply Configuration"
},
"kimiSelector": {
"modelConfig": "Model Configuration",
"mainModel": "Main Model",
"fastModel": "Fast Model",
"refreshModels": "Refresh Model List",
"pleaseSelectModel": "Please select a model",
"noModels": "No models available",
"fillApiKeyFirst": "Please fill in API Key first",
"requestFailed": "Request failed: {{error}}",
"invalidData": "Invalid response data format",
"fetchModelsFailed": "Failed to fetch model list",
"apiKeyHint": "💡 Fill in API Key to automatically fetch available model list"
},
"presetSelector": {
"title": "Select Configuration Type",
"custom": "Custom",
"customDescription": "Manually configure provider, requires complete configuration",
"officialDescription": "Official login, no API Key required",
"presetDescription": "Use preset configuration, only API Key required"
} }
} }

View File

@@ -17,7 +17,25 @@
"loading": "加载中...", "loading": "加载中...",
"success": "成功", "success": "成功",
"error": "错误", "error": "错误",
"unknown": "未知" "unknown": "未知",
"enterValidValue": "请输入有效的内容"
},
"apiKeyInput": {
"placeholder": "请输入API Key",
"show": "显示API Key",
"hide": "隐藏API Key"
},
"jsonEditor": {
"mustBeObject": "配置必须是JSON对象不能是数组或其他类型",
"invalidJson": "JSON格式错误"
},
"claudeConfig": {
"configLabel": "Claude Code 配置 (JSON) *",
"writeCommonConfig": "写入通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑通用配置片段",
"commonConfigHint": "该片段会在勾选\"写入通用配置\"时合并到 settings.json 中",
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容"
}, },
"header": { "header": {
"viewOnGithub": "在 GitHub 上查看", "viewOnGithub": "在 GitHub 上查看",
@@ -96,7 +114,8 @@
"upToDate": "已是最新", "upToDate": "已是最新",
"releaseNotes": "更新日志", "releaseNotes": "更新日志",
"viewReleaseNotes": "查看该版本更新日志", "viewReleaseNotes": "查看该版本更新日志",
"viewCurrentReleaseNotes": "查看当前版本更新日志" "viewCurrentReleaseNotes": "查看当前版本更新日志",
"exportFailedError": "导出配置失败:"
}, },
"apps": { "apps": {
"claude": "Claude Code", "claude": "Claude Code",
@@ -120,5 +139,114 @@
"selectConfigDirFailed": "选择配置目录失败:", "selectConfigDirFailed": "选择配置目录失败:",
"getDefaultConfigDirFailed": "获取默认配置目录失败:", "getDefaultConfigDirFailed": "获取默认配置目录失败:",
"openReleaseNotesFailed": "打开更新日志失败:" "openReleaseNotesFailed": "打开更新日志失败:"
},
"providerForm": {
"supplierName": "供应商名称",
"supplierNameRequired": "供应商名称 *",
"supplierNamePlaceholder": "例如Anthropic 官方",
"websiteUrl": "官网地址",
"websiteUrlPlaceholder": "https://example.com可选",
"apiEndpoint": "请求地址",
"apiEndpointPlaceholder": "https://your-api-endpoint.com",
"codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1",
"manageAndTest": "管理与测速",
"configContent": "配置内容",
"useConfigWizard": "使用配置向导",
"manualConfig": "手动配置供应商,需要填写完整的配置信息,或者",
"officialNoApiKey": "官方登录无需填写 API Key直接保存即可",
"codexOfficialNoApiKey": "官方无需填写 API Key直接保存即可",
"kimiApiKeyHint": "填写后可获取模型列表",
"apiKeyAutoFill": "只需要填这里,下方配置会自动填充",
"codexApiKeyAutoFill": "只需要填这里,下方 auth.json 会自动填充",
"getApiKey": "获取 API Key",
"parameterConfig": "参数配置 - {{name}} *",
"mainModel": "主模型 (可选)",
"mainModelPlaceholder": "例如: GLM-4.5",
"fastModel": "快速模型 (可选)",
"fastModelPlaceholder": "例如: GLM-4.5-Air",
"modelHint": "💡 留空将使用供应商的默认模型",
"apiHint": "💡 填写兼容 Claude API 的服务端点地址",
"fillSupplierName": "请填写供应商名称",
"fillConfigContent": "请填写配置内容",
"fillParameter": "请填写 {{label}}",
"configJsonError": "配置JSON格式错误请检查语法",
"authJsonRequired": "auth.json 必须是 JSON 对象",
"authJsonError": "auth.json 格式错误请检查JSON语法",
"fillAuthJson": "请填写 auth.json 配置",
"fillApiKey": "请填写 OPENAI_API_KEY",
"visitWebsite": "访问 {{url}}"
},
"endpointTest": {
"title": "请求地址管理",
"endpoints": "个端点",
"autoSelect": "自动选择",
"testSpeed": "测速",
"testing": "测速中",
"addEndpointPlaceholder": "https://api.example.com",
"done": "完成",
"noEndpoints": "暂无端点",
"failed": "失败",
"enterValidUrl": "请输入有效的 URL",
"invalidUrlFormat": "URL 格式不正确",
"onlyHttps": "仅支持 HTTP/HTTPS",
"urlExists": "该地址已存在",
"saveFailed": "保存失败,请重试",
"loadEndpointsFailed": "加载自定义端点失败:",
"addEndpointFailed": "添加自定义端点失败:",
"removeEndpointFailed": "删除自定义端点失败:",
"pleaseAddEndpoint": "请先添加端点",
"testUnavailable": "测速功能不可用",
"noResult": "未返回结果",
"testFailed": "测速失败: {{error}}"
},
"codexConfig": {
"quickWizard": "快速配置向导",
"authJson": "auth.json (JSON) *",
"authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}",
"authJsonHint": "Codex auth.json 配置内容",
"configToml": "config.toml (TOML)",
"configTomlHint": "Codex config.toml 配置内容",
"writeCommonConfig": "写入通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
"wizardHint": "输入关键参数,系统将自动生成标准的 auth.json 和 config.toml 配置。",
"apiKeyLabel": "API 密钥 *",
"apiKeyPlaceholder": "sk-your-api-key-here",
"supplierNameLabel": "供应商名称 *",
"supplierNamePlaceholder": "例如Codex 官方",
"supplierNameHint": "将显示在供应商列表中,可使用中文",
"supplierCodeLabel": "供应商代号(英文)",
"supplierCodePlaceholder": "custom可选",
"supplierCodeHint": "将用作配置文件中的标识符,默认为 custom",
"apiUrlLabel": "API 请求地址 *",
"apiUrlPlaceholder": "https://your-api-endpoint.com/v1",
"websiteLabel": "官网地址",
"websitePlaceholder": "https://example.com",
"websiteHint": "官方网站地址(可选)",
"modelNameLabel": "模型名称 *",
"modelNamePlaceholder": "gpt-5-codex",
"configPreview": "配置预览",
"applyConfig": "应用配置"
},
"kimiSelector": {
"modelConfig": "模型配置",
"mainModel": "主模型",
"fastModel": "快速模型",
"refreshModels": "刷新模型列表",
"pleaseSelectModel": "请选择模型",
"noModels": "暂无模型",
"fillApiKeyFirst": "请先填写 API Key",
"requestFailed": "请求失败: {{error}}",
"invalidData": "返回数据格式错误",
"fetchModelsFailed": "获取模型列表失败",
"apiKeyHint": "💡 填写 API Key 后将自动获取可用模型列表"
},
"presetSelector": {
"title": "选择配置类型",
"custom": "自定义",
"customDescription": "手动配置供应商,需要填写完整的配置信息",
"officialDescription": "官方登录,不需要填写 API Key",
"presetDescription": "使用预设配置,只需填写 API Key"
} }
} }