i18n: complete internationalization for provider and usage query panels

- Add 45+ new translation keys for usage query and usage script features
- Fix duplicate provider object in translation files that caused missing translations
- Remove all hardcoded Chinese text and defaultValue fallbacks from components
- Add proper translations for:
  * Usage footer (query status, plan usage display)
  * Usage script modal (script editor, validation, test controls)
  * Provider forms (basic fields, endpoints, model selectors)
  * Provider dialogs (add/edit hints and titles)

Modified 16 files:
- 2 translation files (zh.json, en.json)
- 14 component files (removed defaultValue, added t() calls)

All UI text now properly supports Chinese/English switching.
This commit is contained in:
Jason
2025-10-19 11:55:46 +08:00
parent bae6a1cf55
commit eb6948a562
16 changed files with 176 additions and 125 deletions

View File

@@ -142,7 +142,7 @@ function App() {
</Button> </Button>
<Button onClick={() => setIsAddOpen(true)}> <Button onClick={() => setIsAddOpen(true)}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{t("header.addProvider", { defaultValue: "添加供应商" })} {t("header.addProvider")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -198,12 +198,11 @@ function App() {
<ConfirmDialog <ConfirmDialog
isOpen={Boolean(confirmDelete)} isOpen={Boolean(confirmDelete)}
title={t("confirm.deleteProvider", { defaultValue: "删除供应商" })} title={t("confirm.deleteProvider")}
message={ message={
confirmDelete confirmDelete
? t("confirm.deleteProviderMessage", { ? t("confirm.deleteProviderMessage", {
name: confirmDelete.name, name: confirmDelete.name,
defaultValue: `确定删除 ${confirmDelete.name} 吗?`,
}) })
: "" : ""
} }

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { RefreshCw, AlertCircle } from "lucide-react"; import { RefreshCw, AlertCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { type AppType } from "@/lib/api"; import { type AppType } from "@/lib/api";
import { useUsageQuery } from "@/lib/query/queries"; import { useUsageQuery } from "@/lib/query/queries";
import { UsageData } from "../types"; import { UsageData } from "../types";
@@ -15,6 +16,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
appType, appType,
usageEnabled, usageEnabled,
}) => { }) => {
const { t } = useTranslation();
const { const {
data: usage, data: usage,
isLoading: loading, isLoading: loading,
@@ -31,7 +33,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2 text-red-500 dark:text-red-400"> <div className="flex items-center gap-2 text-red-500 dark:text-red-400">
<AlertCircle size={14} /> <AlertCircle size={14} />
<span>{usage.error || "查询失败"}</span> <span>{usage.error || t("usage.queryFailed")}</span>
</div> </div>
{/* 刷新按钮 */} {/* 刷新按钮 */}
@@ -39,7 +41,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
onClick={() => refetch()} onClick={() => refetch()}
disabled={loading} disabled={loading}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0" className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0"
title="刷新用量" title={t("usage.refreshUsage")}
> >
<RefreshCw size={12} className={loading ? "animate-spin" : ""} /> <RefreshCw size={12} className={loading ? "animate-spin" : ""} />
</button> </button>
@@ -58,13 +60,13 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
{/* 标题行:包含刷新按钮 */} {/* 标题行:包含刷新按钮 */}
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium"> <span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
{t("usage.planUsage")}
</span> </span>
<button <button
onClick={() => refetch()} onClick={() => refetch()}
disabled={loading} disabled={loading}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50" className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50"
title="刷新用量" title={t("usage.refreshUsage")}
> >
<RefreshCw size={12} className={loading ? "animate-spin" : ""} /> <RefreshCw size={12} className={loading ? "animate-spin" : ""} />
</button> </button>
@@ -82,6 +84,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
// 单个套餐数据展示组件 // 单个套餐数据展示组件
const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => { const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
const { t } = useTranslation();
const { const {
planName, planName,
extra, extra,
@@ -130,7 +133,7 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
)} )}
{isExpired && ( {isExpired && (
<span className="text-red-500 dark:text-red-400 font-medium text-[10px] px-1.5 py-0.5 bg-red-50 dark:bg-red-900/20 rounded flex-shrink-0"> <span className="text-red-500 dark:text-red-400 font-medium text-[10px] px-1.5 py-0.5 bg-red-50 dark:bg-red-900/20 rounded flex-shrink-0">
{invalidMessage || "已失效"} {invalidMessage || t("usage.invalid")}
</span> </span>
)} )}
</div> </div>
@@ -143,7 +146,7 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
{/* 总额度 */} {/* 总额度 */}
{total !== undefined && ( {total !== undefined && (
<> <>
<span className="text-gray-500 dark:text-gray-400"></span> <span className="text-gray-500 dark:text-gray-400">{t("usage.total")}</span>
<span className="tabular-nums text-gray-600 dark:text-gray-400"> <span className="tabular-nums text-gray-600 dark:text-gray-400">
{total === -1 ? "∞" : total.toFixed(2)} {total === -1 ? "∞" : total.toFixed(2)}
</span> </span>
@@ -154,7 +157,7 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
{/* 已用额度 */} {/* 已用额度 */}
{used !== undefined && ( {used !== undefined && (
<> <>
<span className="text-gray-500 dark:text-gray-400">使</span> <span className="text-gray-500 dark:text-gray-400">{t("usage.used")}</span>
<span className="tabular-nums text-gray-600 dark:text-gray-400"> <span className="tabular-nums text-gray-600 dark:text-gray-400">
{used.toFixed(2)} {used.toFixed(2)}
</span> </span>
@@ -165,7 +168,7 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
{/* 剩余额度 - 突出显示 */} {/* 剩余额度 - 突出显示 */}
{remaining !== undefined && ( {remaining !== undefined && (
<> <>
<span className="text-gray-500 dark:text-gray-400"></span> <span className="text-gray-500 dark:text-gray-400">{t("usage.remaining")}</span>
<span <span
className={`font-semibold tabular-nums ${ className={`font-semibold tabular-nums ${
isExpired isExpired

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Play, Wand2 } from "lucide-react"; import { Play, Wand2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { Provider, UsageScript } from "../types"; import { Provider, UsageScript } from "../types";
import { usageApi, type AppType } from "@/lib/api"; import { usageApi, type AppType } from "@/lib/api";
import JsonEditor from "./JsonEditor"; import JsonEditor from "./JsonEditor";
@@ -88,12 +89,13 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
onClose, onClose,
onSave, onSave,
}) => { }) => {
const { t } = useTranslation();
const [script, setScript] = useState<UsageScript>(() => { const [script, setScript] = useState<UsageScript>(() => {
return ( return (
provider.meta?.usage_script || { provider.meta?.usage_script || {
enabled: false, enabled: false,
language: "javascript", language: "javascript",
code: PRESET_TEMPLATES["通用模板"], code: PRESET_TEMPLATES[t("usageScript.presetTemplate") === "预设模板" ? "通用模板" : "General"],
timeout: 10, timeout: 10,
} }
); );
@@ -104,13 +106,13 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
const handleSave = () => { const handleSave = () => {
// 验证脚本格式 // 验证脚本格式
if (script.enabled && !script.code.trim()) { if (script.enabled && !script.code.trim()) {
toast.error("脚本配置不能为空"); toast.error(t("usageScript.scriptEmpty"));
return; return;
} }
// 基本的 JS 语法检查(检查是否包含 return 语句) // 基本的 JS 语法检查(检查是否包含 return 语句)
if (script.enabled && !script.code.includes("return")) { if (script.enabled && !script.code.includes("return")) {
toast.error("脚本必须包含 return 语句", { duration: 5000 }); toast.error(t("usageScript.mustHaveReturn"), { duration: 5000 });
return; return;
} }
@@ -127,17 +129,17 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
const summary = result.data const summary = result.data
.map((plan) => { .map((plan) => {
const planInfo = plan.planName ? `[${plan.planName}]` : ""; const planInfo = plan.planName ? `[${plan.planName}]` : "";
return `${planInfo} 剩余: ${plan.remaining} ${plan.unit}`; return `${planInfo} ${t("usage.remaining")} ${plan.remaining} ${plan.unit}`;
}) })
.join(", "); .join(", ");
toast.success(`测试成功!${summary}`, { duration: 3000 }); toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000 });
} else { } else {
toast.error(`测试失败: ${result.error || "无数据返回"}`, { toast.error(`${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`, {
duration: 5000, duration: 5000,
}); });
} }
} catch (error: any) { } catch (error: any) {
toast.error(`测试失败: ${error?.message || "未知错误"}`, { toast.error(`${t("usageScript.testFailed")}: ${error?.message || t("common.unknown")}`, {
duration: 5000, duration: 5000,
}); });
} finally { } finally {
@@ -156,9 +158,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
printWidth: 80, printWidth: 80,
}); });
setScript({ ...script, code: formatted.trim() }); setScript({ ...script, code: formatted.trim() });
toast.success("格式化成功", { duration: 1000 }); toast.success(t("usageScript.formatSuccess"), { duration: 1000 });
} catch (error: any) { } catch (error: any) {
toast.error(`格式化失败: ${error?.message || "语法错误"}`, { toast.error(`${t("usageScript.formatFailed")}: ${error?.message || t("jsonEditor.invalidJson")}`, {
duration: 3000, duration: 3000,
}); });
} }
@@ -175,7 +177,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col"> <DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle> - {provider.name}</DialogTitle> <DialogTitle>{t("usageScript.title")} - {provider.name}</DialogTitle>
</DialogHeader> </DialogHeader>
{/* Content - Scrollable */} {/* Content - Scrollable */}
@@ -191,7 +193,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
className="w-4 h-4" className="w-4 h-4"
/> />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("usageScript.enableUsageQuery")}
</span> </span>
</label> </label>
@@ -200,7 +202,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{/* 预设模板选择 */} {/* 预设模板选择 */}
<div> <div>
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100"> <label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100">
{t("usageScript.presetTemplate")}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
{Object.keys(PRESET_TEMPLATES).map((name) => ( {Object.keys(PRESET_TEMPLATES).map((name) => (
@@ -218,7 +220,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{/* 脚本编辑器 */} {/* 脚本编辑器 */}
<div> <div>
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100"> <label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100">
JavaScript {t("usageScript.queryScript")}
</label> </label>
<JsonEditor <JsonEditor
value={script.code} value={script.code}
@@ -227,9 +229,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
language="javascript" language="javascript"
/> />
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
: <code>{"{{apiKey}}"}</code>,{" "} {t("usageScript.variablesHint", {
<code>{"{{baseUrl}}"}</code> | extractor API apiKey: "{{apiKey}}",
JSON baseUrl: "{{baseUrl}}"
})}
</p> </p>
</div> </div>
@@ -237,7 +240,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<label className="block"> <label className="block">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("usageScript.timeoutSeconds")}
</span> </span>
<input <input
type="number" type="number"
@@ -257,10 +260,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{/* 脚本说明 */} {/* 脚本说明 */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm text-gray-700 dark:text-gray-300"> <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm text-gray-700 dark:text-gray-300">
<h4 className="font-medium mb-2"></h4> <h4 className="font-medium mb-2">{t("usageScript.scriptHelp")}</h4>
<div className="space-y-3 text-xs"> <div className="space-y-3 text-xs">
<div> <div>
<strong></strong> <strong>{t("usageScript.configFormat")}</strong>
<pre className="mt-1 p-2 bg-white/50 dark:bg-black/20 rounded text-[10px] overflow-x-auto"> <pre className="mt-1 p-2 bg-white/50 dark:bg-black/20 rounded text-[10px] overflow-x-auto">
{`({ {`({
request: { request: {
@@ -285,51 +288,25 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</div> </div>
<div> <div>
<strong>extractor </strong> <strong>{t("usageScript.extractorFormat")}</strong>
<ul className="mt-1 space-y-0.5 ml-2"> <ul className="mt-1 space-y-0.5 ml-2">
<li> <li>{t("usageScript.fieldIsValid")}</li>
<code>isValid</code>: <li>{t("usageScript.fieldInvalidMessage")}</li>
</li> <li>{t("usageScript.fieldRemaining")}</li>
<li> <li>{t("usageScript.fieldUnit")}</li>
<code>invalidMessage</code>: <li>{t("usageScript.fieldPlanName")}</li>
isValid false <li>{t("usageScript.fieldTotal")}</li>
</li> <li>{t("usageScript.fieldUsed")}</li>
<li> <li>{t("usageScript.fieldExtra")}</li>
<code>remaining</code>:
</li>
<li>
<code>unit</code>: "USD"
</li>
<li>
<code>planName</code>:
</li>
<li>
<code>total</code>:
</li>
<li>
<code>used</code>:
</li>
<li>
<code>extra</code>:
</li>
</ul> </ul>
</div> </div>
<div className="text-gray-600 dark:text-gray-400"> <div className="text-gray-600 dark:text-gray-400">
<strong>💡 </strong> <strong>{t("usageScript.tips")}</strong>
<ul className="mt-1 space-y-0.5 ml-2"> <ul className="mt-1 space-y-0.5 ml-2">
<li> <li>{t("usageScript.tip1", { apiKey: "{{apiKey}}", baseUrl: "{{baseUrl}}" })}</li>
<code>{"{{apiKey}}"}</code> {" "} <li>{t("usageScript.tip2")}</li>
<code>{"{{baseUrl}}"}</code> <li>{t("usageScript.tip3")}</li>
</li>
<li>
extractor ES2020+
</li>
<li>
<code>()</code>{" "}
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -349,27 +326,27 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
disabled={!script.enabled || testing} disabled={!script.enabled || testing}
> >
<Play size={14} /> <Play size={14} />
{testing ? "测试中..." : "测试脚本"} {testing ? t("usageScript.testing") : t("usageScript.testScript")}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleFormat} onClick={handleFormat}
disabled={!script.enabled} disabled={!script.enabled}
title="格式化代码 (Prettier)" title={t("usageScript.format")}
> >
<Wand2 size={14} /> <Wand2 size={14} />
{t("usageScript.format")}
</Button> </Button>
</div> </div>
{/* Right side - Cancel and Save buttons */} {/* Right side - Cancel and Save buttons */}
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={onClose}> <Button variant="ghost" size="sm" onClick={onClose}>
{t("common.cancel")}
</Button> </Button>
<Button variant="default" size="sm" onClick={handleSave}> <Button variant="default" size="sm" onClick={handleSave}>
{t("usageScript.saveConfig")}
</Button> </Button>
</div> </div>
</DialogFooter> </DialogFooter>

View File

@@ -135,8 +135,8 @@ export function AddProviderDialog({
const submitLabel = const submitLabel =
appType === "claude" appType === "claude"
? t("provider.addClaudeProvider", { defaultValue: "添加 Claude 供应商" }) ? t("provider.addClaudeProvider")
: t("provider.addCodexProvider", { defaultValue: "添加 Codex 供应商" }); : t("provider.addCodexProvider");
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -144,16 +144,14 @@ export function AddProviderDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>{submitLabel}</DialogTitle> <DialogTitle>{submitLabel}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("provider.addDescription", { {t("provider.addProviderHint")}
defaultValue: "填写信息后即可在列表中快速切换供应商。",
})}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4"> <div className="flex-1 overflow-y-auto px-6 py-4">
<ProviderForm <ProviderForm
appType={appType} appType={appType}
submitLabel={t("common.add", { defaultValue: "添加" })} submitLabel={t("common.add")}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
showButtons={false} showButtons={false}
@@ -162,11 +160,11 @@ export function AddProviderDialog({
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)}>
{t("common.cancel", { defaultValue: "取消" })} {t("common.cancel")}
</Button> </Button>
<Button type="submit" form="provider-form"> <Button type="submit" form="provider-form">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{t("common.add", { defaultValue: "添加" })} {t("common.add")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -66,19 +66,17 @@ export function EditProviderDialog({
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col"> <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{t("provider.editProvider", { defaultValue: "编辑供应商" })} {t("provider.editProvider")}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{t("provider.editDescription", { {t("provider.editProviderHint")}
defaultValue: "更新配置后将立即应用到当前供应商。",
})}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4"> <div className="flex-1 overflow-y-auto px-6 py-4">
<ProviderForm <ProviderForm
appType={appType} appType={appType}
submitLabel={t("common.save", { defaultValue: "保存" })} submitLabel={t("common.save")}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
initialData={{ initialData={{
@@ -92,11 +90,11 @@ export function EditProviderDialog({
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)}>
{t("common.cancel", { defaultValue: "取消" })} {t("common.cancel")}
</Button> </Button>
<Button type="submit" form="provider-form"> <Button type="submit" form="provider-form">
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
{t("common.save", { defaultValue: "保存" })} {t("common.save")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -35,25 +35,25 @@ export function ProviderActions({
{isCurrent ? ( {isCurrent ? (
<> <>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
{t("provider.inUse", { defaultValue: "已启用" })} {t("provider.inUse")}
</> </>
) : ( ) : (
<> <>
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
{t("provider.enable", { defaultValue: "启用" })} {t("provider.enable")}
</> </>
)} )}
</Button> </Button>
<Button size="sm" variant="outline" onClick={onEdit}> <Button size="sm" variant="outline" onClick={onEdit}>
{t("common.edit", { defaultValue: "编辑" })} {t("common.edit")}
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={onConfigureUsage} onClick={onConfigureUsage}
title={t("provider.configureUsage", { defaultValue: "配置用量查询" })} title={t("provider.configureUsage")}
> >
<BarChart3 className="h-4 w-4" /> <BarChart3 className="h-4 w-4" />
</Button> </Button>

View File

@@ -104,7 +104,7 @@ export function ProviderCard({
"mt-1 flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors hover:border-muted hover:text-foreground", "mt-1 flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors hover:border-muted hover:text-foreground",
dragHandleProps?.isDragging && "border-primary text-primary", dragHandleProps?.isDragging && "border-primary text-primary",
)} )}
aria-label={t("provider.dragHandle", { defaultValue: "拖拽排序" })} aria-label={t("provider.dragHandle")}
{...(dragHandleProps?.attributes ?? {})} {...(dragHandleProps?.attributes ?? {})}
{...(dragHandleProps?.listeners ?? {})} {...(dragHandleProps?.listeners ?? {})}
> >
@@ -118,7 +118,7 @@ export function ProviderCard({
</h3> </h3>
{isCurrent && ( {isCurrent && (
<span className="rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400"> <span className="rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400">
{t("provider.currentlyUsing", { defaultValue: "当前使用" })} {t("provider.currentlyUsing")}
</span> </span>
)} )}
</div> </div>

View File

@@ -15,16 +15,14 @@ export function ProviderEmptyState({ onCreate }: ProviderEmptyStateProps) {
<Users className="h-7 w-7 text-muted-foreground" /> <Users className="h-7 w-7 text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">
{t("provider.noProviders", { defaultValue: "暂无供应商" })} {t("provider.noProviders")}
</h3> </h3>
<p className="mt-2 max-w-sm text-sm text-muted-foreground"> <p className="mt-2 max-w-sm text-sm text-muted-foreground">
{t("provider.noProvidersDescription", { {t("provider.noProvidersDescription")}
defaultValue: "开始添加一个供应商以快速完成切换。",
})}
</p> </p>
{onCreate && ( {onCreate && (
<Button className="mt-6" onClick={onCreate}> <Button className="mt-6" onClick={onCreate}>
{t("provider.addProvider", { defaultValue: "添加供应商" })} {t("provider.addProvider")}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -25,14 +25,12 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t("provider.name", { defaultValue: "供应商名称" })} {t("provider.name")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
placeholder={t("provider.namePlaceholder", { placeholder={t("provider.namePlaceholder")}
defaultValue: "例如Claude 官方",
})}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -46,7 +44,7 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t("provider.websiteUrl", { defaultValue: "官网链接" })} {t("provider.websiteUrl")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} placeholder="https://" /> <Input {...field} placeholder="https://" />

View File

@@ -137,15 +137,11 @@ export function ClaudeFormFields({
{shouldShowSpeedTest && ( {shouldShowSpeedTest && (
<EndpointField <EndpointField
id="baseUrl" id="baseUrl"
label={t("providerForm.apiEndpoint", { defaultValue: "API 端点" })} label={t("providerForm.apiEndpoint")}
value={baseUrl} value={baseUrl}
onChange={onBaseUrlChange} onChange={onBaseUrlChange}
placeholder={t("providerForm.apiEndpointPlaceholder", { placeholder={t("providerForm.apiEndpointPlaceholder")}
defaultValue: "https://api.example.com", hint={t("providerForm.apiHint")}
})}
hint={t("providerForm.apiHint", {
defaultValue: "API 端点地址用于连接服务器",
})}
onManageClick={() => onEndpointModalToggle(true)} onManageClick={() => onEndpointModalToggle(true)}
/> />
)} )}

View File

@@ -68,15 +68,11 @@ export function CodexFormFields({
{shouldShowSpeedTest && ( {shouldShowSpeedTest && (
<EndpointField <EndpointField
id="codexBaseUrl" id="codexBaseUrl"
label={t("codexConfig.apiUrlLabel", { defaultValue: "API 端点" })} label={t("codexConfig.apiUrlLabel")}
value={codexBaseUrl} value={codexBaseUrl}
onChange={onBaseUrlChange} onChange={onBaseUrlChange}
placeholder={t("providerForm.codexApiEndpointPlaceholder", { placeholder={t("providerForm.codexApiEndpointPlaceholder")}
defaultValue: "https://api.example.com/v1", hint={t("providerForm.codexApiHint")}
})}
hint={t("providerForm.codexApiHint", {
defaultValue: "Codex API 端点地址",
})}
onManageClick={() => onEndpointModalToggle(true)} onManageClick={() => onEndpointModalToggle(true)}
/> />
)} )}

View File

@@ -51,7 +51,7 @@ export function CommonConfigEditor({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="settingsConfig"> <Label htmlFor="settingsConfig">
{t("provider.configJson", { defaultValue: "配置 JSON" })} {t("provider.configJson")}
</Label> </Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer"> <label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">

View File

@@ -554,7 +554,7 @@ export function ProviderForm({
{showButtons && ( {showButtons && (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" type="button" onClick={onCancel}> <Button variant="outline" type="button" onClick={onCancel}>
{t("common.cancel", { defaultValue: "取消" })} {t("common.cancel")}
</Button> </Button>
<Button type="submit">{submitLabel}</Button> <Button type="submit">{submitLabel}</Button>
</div> </div>

View File

@@ -28,7 +28,7 @@ export function ProviderPresetSelector({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<FormLabel> <FormLabel>
{t("providerPreset.label", { defaultValue: "预设供应商" })} {t("providerPreset.label")}
</FormLabel> </FormLabel>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{/* 自定义按钮 */} {/* 自定义按钮 */}
@@ -41,7 +41,7 @@ export function ProviderPresetSelector({
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700" : "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`} }`}
> >
{t("providerPreset.custom", { defaultValue: "自定义配置" })} {t("providerPreset.custom")}
</button> </button>
{/* 预设按钮 */} {/* 预设按钮 */}

View File

@@ -73,13 +73,15 @@
"sortUpdateFailed": "Failed to update sort order", "sortUpdateFailed": "Failed to update sort order",
"configureUsage": "Configure usage query", "configureUsage": "Configure usage query",
"name": "Provider Name", "name": "Provider Name",
"namePlaceholder": "e.g., Claude Official",
"websiteUrl": "Website URL", "websiteUrl": "Website URL",
"configJson": "Config JSON", "configJson": "Config JSON",
"writeCommonConfig": "Write common config", "writeCommonConfig": "Write common config",
"editCommonConfigButton": "Edit common config", "editCommonConfigButton": "Edit common config",
"configJsonHint": "Please fill in complete Claude Code configuration", "configJsonHint": "Please fill in complete Claude Code configuration",
"editCommonConfigTitle": "Edit common config snippet", "editCommonConfigTitle": "Edit common config snippet",
"editCommonConfigHint": "Common config snippet will be merged into all providers that enable it" "editCommonConfigHint": "Common config snippet will be merged into all providers that enable it",
"addProvider": "Add Provider"
}, },
"notifications": { "notifications": {
"providerSaved": "Provider configuration saved", "providerSaved": "Provider configuration saved",
@@ -297,6 +299,48 @@
"other": "Other", "other": "Other",
"hint": "You can continue to adjust the fields below after selecting a preset." "hint": "You can continue to adjust the fields below after selecting a preset."
}, },
"usage": {
"queryFailed": "Query failed",
"refreshUsage": "Refresh usage",
"planUsage": "Plan usage",
"invalid": "Expired",
"total": "Total:",
"used": "Used:",
"remaining": "Remaining:"
},
"usageScript": {
"title": "Configure Usage Query",
"enableUsageQuery": "Enable usage query",
"presetTemplate": "Preset template",
"queryScript": "Query script (JavaScript)",
"timeoutSeconds": "Timeout (seconds)",
"scriptHelp": "Script writing instructions:",
"configFormat": "Configuration format:",
"extractorFormat": "Extractor return format (all fields optional):",
"tips": "💡 Tips:",
"testing": "Testing...",
"testScript": "Test script",
"format": "Format",
"saveConfig": "Save config",
"scriptEmpty": "Script configuration cannot be empty",
"mustHaveReturn": "Script must contain return statement",
"testSuccess": "Test successful!",
"testFailed": "Test failed",
"formatSuccess": "Format successful",
"formatFailed": "Format failed",
"variablesHint": "Supported variables: {{apiKey}}, {{baseUrl}} | extractor function receives API response JSON object",
"fieldIsValid": "• isValid: Boolean, whether plan is valid",
"fieldInvalidMessage": "• invalidMessage: String, reason for expiration (shown when isValid is false)",
"fieldRemaining": "• remaining: Number, remaining quota",
"fieldUnit": "• unit: String, unit (e.g., \"USD\")",
"fieldPlanName": "• planName: String, plan name",
"fieldTotal": "• total: Number, total quota",
"fieldUsed": "• used: Number, used quota",
"fieldExtra": "• extra: String, custom display text",
"tip1": "• Variables {{apiKey}} and {{baseUrl}} are automatically replaced",
"tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax",
"tip3": "• Entire config must be wrapped in () to form object literal expression"
},
"kimiSelector": { "kimiSelector": {
"modelConfig": "Model Configuration", "modelConfig": "Model Configuration",
"mainModel": "Main Model", "mainModel": "Main Model",

View File

@@ -73,13 +73,15 @@
"sortUpdateFailed": "排序更新失败", "sortUpdateFailed": "排序更新失败",
"configureUsage": "配置用量查询", "configureUsage": "配置用量查询",
"name": "供应商名称", "name": "供应商名称",
"namePlaceholder": "例如Claude 官方",
"websiteUrl": "官网链接", "websiteUrl": "官网链接",
"configJson": "配置 JSON", "configJson": "配置 JSON",
"writeCommonConfig": "写入通用配置", "writeCommonConfig": "写入通用配置",
"editCommonConfigButton": "编辑通用配置", "editCommonConfigButton": "编辑通用配置",
"configJsonHint": "请填写完整的 Claude Code 配置", "configJsonHint": "请填写完整的 Claude Code 配置",
"editCommonConfigTitle": "编辑通用配置片段", "editCommonConfigTitle": "编辑通用配置片段",
"editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中" "editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中",
"addProvider": "添加供应商"
}, },
"notifications": { "notifications": {
"providerSaved": "供应商配置已保存", "providerSaved": "供应商配置已保存",
@@ -297,6 +299,48 @@
"other": "其他", "other": "其他",
"hint": "选择预设后可继续调整下方字段。" "hint": "选择预设后可继续调整下方字段。"
}, },
"usage": {
"queryFailed": "查询失败",
"refreshUsage": "刷新用量",
"planUsage": "套餐用量",
"invalid": "已失效",
"total": "总:",
"used": "使用:",
"remaining": "剩余:"
},
"usageScript": {
"title": "配置用量查询",
"enableUsageQuery": "启用用量查询",
"presetTemplate": "预设模板",
"queryScript": "查询脚本JavaScript",
"timeoutSeconds": "超时时间(秒)",
"scriptHelp": "脚本编写说明:",
"configFormat": "配置格式:",
"extractorFormat": "extractor 返回格式(所有字段均为可选):",
"tips": "💡 提示:",
"testing": "测试中...",
"testScript": "测试脚本",
"format": "格式化",
"saveConfig": "保存配置",
"scriptEmpty": "脚本配置不能为空",
"mustHaveReturn": "脚本必须包含 return 语句",
"testSuccess": "测试成功!",
"testFailed": "测试失败",
"formatSuccess": "格式化成功",
"formatFailed": "格式化失败",
"variablesHint": "支持变量: {{apiKey}}, {{baseUrl}} | extractor 函数接收 API 响应的 JSON 对象",
"fieldIsValid": "• isValid: 布尔值,套餐是否有效",
"fieldInvalidMessage": "• invalidMessage: 字符串,失效原因说明(当 isValid 为 false 时显示)",
"fieldRemaining": "• remaining: 数字,剩余额度",
"fieldUnit": "• unit: 字符串,单位(如 \"USD\"",
"fieldPlanName": "• planName: 字符串,套餐名称",
"fieldTotal": "• total: 数字,总额度",
"fieldUsed": "• used: 数字,已用额度",
"fieldExtra": "• extra: 字符串,扩展字段,可自由补充需要展示的文本",
"tip1": "• 变量 {{apiKey}} 和 {{baseUrl}} 会自动替换",
"tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法",
"tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式"
},
"kimiSelector": { "kimiSelector": {
"modelConfig": "模型配置", "modelConfig": "模型配置",
"mainModel": "主模型", "mainModel": "主模型",