- 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

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