mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
i18n: complete internationalization for v3.8+ features
- Add health status translations (operational, degraded, failed, circuitOpen) - Add proxy panel translations (serviceAddress, stats, stopped state) - Add usage filter translations (appType, statusCode, searchPlaceholder) - Add providerIcon click hints (clickToChange, clickToSelect) - Add config load error translations for main.tsx - Complete Japanese proxy section (failoverQueue, autoFailover) - Fix date/time locale in usage charts and tables - Use t() function in all hardcoded UI strings
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HealthStatus } from "@/lib/api/model-test";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HealthStatusIndicatorProps {
|
||||
status: HealthStatus;
|
||||
@@ -11,17 +12,20 @@ interface HealthStatusIndicatorProps {
|
||||
const statusConfig = {
|
||||
operational: {
|
||||
color: "bg-emerald-500",
|
||||
label: "正常",
|
||||
labelKey: "health.operational",
|
||||
labelFallback: "正常",
|
||||
textColor: "text-emerald-600 dark:text-emerald-400",
|
||||
},
|
||||
degraded: {
|
||||
color: "bg-yellow-500",
|
||||
label: "降级",
|
||||
labelKey: "health.degraded",
|
||||
labelFallback: "降级",
|
||||
textColor: "text-yellow-600 dark:text-yellow-400",
|
||||
},
|
||||
failed: {
|
||||
color: "bg-red-500",
|
||||
label: "失败",
|
||||
labelKey: "health.failed",
|
||||
labelFallback: "失败",
|
||||
textColor: "text-red-600 dark:text-red-400",
|
||||
},
|
||||
};
|
||||
@@ -31,13 +35,15 @@ export const HealthStatusIndicator: React.FC<HealthStatusIndicatorProps> = ({
|
||||
responseTimeMs,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const config = statusConfig[status];
|
||||
const label = t(config.labelKey, { defaultValue: config.labelFallback });
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className={cn("w-2 h-2 rounded-full", config.color)} />
|
||||
<span className={cn("text-xs font-medium", config.textColor)}>
|
||||
{config.label}
|
||||
{label}
|
||||
{responseTimeMs !== undefined && ` (${responseTimeMs}ms)`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ProviderHealthStatus } from "@/types/proxy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ProviderHealthBadgeProps {
|
||||
consecutiveFailures: number;
|
||||
@@ -14,11 +15,14 @@ export function ProviderHealthBadge({
|
||||
consecutiveFailures,
|
||||
className,
|
||||
}: ProviderHealthBadgeProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 根据失败次数计算状态
|
||||
const getStatus = () => {
|
||||
if (consecutiveFailures === 0) {
|
||||
return {
|
||||
label: "正常",
|
||||
labelKey: "health.operational",
|
||||
labelFallback: "正常",
|
||||
status: ProviderHealthStatus.Healthy,
|
||||
color: "bg-green-500",
|
||||
// 使用更深/柔和的背景色,去除可能的白色内容感
|
||||
@@ -27,7 +31,8 @@ export function ProviderHealthBadge({
|
||||
};
|
||||
} else if (consecutiveFailures < 5) {
|
||||
return {
|
||||
label: "降级",
|
||||
labelKey: "health.degraded",
|
||||
labelFallback: "降级",
|
||||
status: ProviderHealthStatus.Degraded,
|
||||
color: "bg-yellow-500",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
@@ -35,7 +40,8 @@ export function ProviderHealthBadge({
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: "熔断",
|
||||
labelKey: "health.circuitOpen",
|
||||
labelFallback: "熔断",
|
||||
status: ProviderHealthStatus.Failed,
|
||||
color: "bg-red-500",
|
||||
bgColor: "bg-red-500/10",
|
||||
@@ -45,6 +51,9 @@ export function ProviderHealthBadge({
|
||||
};
|
||||
|
||||
const statusConfig = getStatus();
|
||||
const label = t(statusConfig.labelKey, {
|
||||
defaultValue: statusConfig.labelFallback,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -54,10 +63,13 @@ export function ProviderHealthBadge({
|
||||
statusConfig.textColor,
|
||||
className,
|
||||
)}
|
||||
title={`连续失败 ${consecutiveFailures} 次`}
|
||||
title={t("health.consecutiveFailures", {
|
||||
count: consecutiveFailures,
|
||||
defaultValue: `连续失败 ${consecutiveFailures} 次`,
|
||||
})}
|
||||
>
|
||||
<div className={cn("w-2 h-2 rounded-full", statusConfig.color)} />
|
||||
<span>{statusConfig.label}</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,15 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
||||
<button
|
||||
type="button"
|
||||
className="w-20 h-20 p-3 rounded-xl border-2 border-muted hover:border-primary transition-colors cursor-pointer bg-muted/30 hover:bg-muted/50 flex items-center justify-center"
|
||||
title={currentIcon ? "点击更换图标" : "点击选择图标"}
|
||||
title={
|
||||
currentIcon
|
||||
? t("providerIcon.clickToChange", {
|
||||
defaultValue: "点击更换图标",
|
||||
})
|
||||
: t("providerIcon.clickToSelect", {
|
||||
defaultValue: "点击选择图标",
|
||||
})
|
||||
}
|
||||
>
|
||||
<ProviderIcon
|
||||
icon={currentIcon}
|
||||
@@ -145,7 +153,7 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
||||
<FormItem>
|
||||
<FormLabel>{t("provider.websiteUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://" />
|
||||
<Input {...field} placeholder={t("providerForm.websiteUrlPlaceholder")} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -150,9 +150,7 @@ export function ProviderPresetSelector({
|
||||
style={getPresetButtonStyle(isSelected, entry.preset)}
|
||||
title={
|
||||
presetCategoryLabels[category] ??
|
||||
t("providerPreset.categoryOther", {
|
||||
defaultValue: "其他",
|
||||
})
|
||||
t("providerPreset.other")
|
||||
}
|
||||
>
|
||||
{renderPresetIcon(entry.preset)}
|
||||
|
||||
@@ -15,8 +15,10 @@ import { useFailoverQueue } from "@/lib/query/failover";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import type { ProxyStatus } from "@/types/proxy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ProxyPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { status, isRunning } = useProxyStatus();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
@@ -48,7 +50,9 @@ export function ProxyPanel() {
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-muted-foreground">服务地址</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.panel.serviceAddress", { defaultValue: "服务地址" })}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -56,7 +60,7 @@ export function ProxyPanel() {
|
||||
className="h-7 gap-1.5 text-xs"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
配置
|
||||
{t("common.settings")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
@@ -70,16 +74,23 @@ export function ProxyPanel() {
|
||||
navigator.clipboard.writeText(
|
||||
`http://${status.address}:${status.port}`,
|
||||
);
|
||||
toast.success("地址已复制", { closeButton: true });
|
||||
toast.success(
|
||||
t("proxy.panel.addressCopied", {
|
||||
defaultValue: "地址已复制",
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
}}
|
||||
>
|
||||
复制
|
||||
{t("common.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-border space-y-2">
|
||||
<p className="text-xs text-muted-foreground">使用中</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("provider.inUse")}
|
||||
</p>
|
||||
{status.active_targets && status.active_targets.length > 0 ? (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{status.active_targets.map((target) => (
|
||||
@@ -101,14 +112,18 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
) : status.current_provider ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
当前 Provider:{" "}
|
||||
{t("proxy.panel.currentProvider", {
|
||||
defaultValue: "当前 Provider:",
|
||||
})}{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{status.current_provider}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
当前 Provider:等待首次请求…
|
||||
{t("proxy.panel.waitingFirstRequest", {
|
||||
defaultValue: "当前 Provider:等待首次请求…",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -121,7 +136,7 @@ export function ProxyPanel() {
|
||||
<div className="flex items-center gap-2">
|
||||
<ListOrdered className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
故障转移队列
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -179,23 +194,31 @@ export function ProxyPanel() {
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<StatCard
|
||||
icon={<Activity className="h-4 w-4" />}
|
||||
label="活跃连接"
|
||||
label={t("proxy.panel.stats.activeConnections", {
|
||||
defaultValue: "活跃连接",
|
||||
})}
|
||||
value={status.active_connections}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<TrendingUp className="h-4 w-4" />}
|
||||
label="总请求数"
|
||||
label={t("proxy.panel.stats.totalRequests", {
|
||||
defaultValue: "总请求数",
|
||||
})}
|
||||
value={status.total_requests}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
label="成功率"
|
||||
label={t("proxy.panel.stats.successRate", {
|
||||
defaultValue: "成功率",
|
||||
})}
|
||||
value={`${status.success_rate.toFixed(1)}%`}
|
||||
variant={status.success_rate > 90 ? "success" : "warning"}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
label="运行时间"
|
||||
label={t("proxy.panel.stats.uptime", {
|
||||
defaultValue: "运行时间",
|
||||
})}
|
||||
value={formatUptime(status.uptime_seconds)}
|
||||
/>
|
||||
</div>
|
||||
@@ -206,10 +229,12 @@ export function ProxyPanel() {
|
||||
<Server className="h-8 w-8" />
|
||||
</div>
|
||||
<p className="text-base font-medium text-foreground mb-1">
|
||||
代理服务已停止
|
||||
{t("proxy.panel.stoppedTitle", { defaultValue: "代理服务已停止" })}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
使用右上角开关即可启动服务
|
||||
{t("proxy.panel.stoppedDescription", {
|
||||
defaultValue: "使用右上角开关即可启动服务",
|
||||
})}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -218,7 +243,9 @@ export function ProxyPanel() {
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
配置代理服务
|
||||
{t("proxy.panel.openSettings", {
|
||||
defaultValue: "配置代理服务",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -319,6 +346,7 @@ function ProviderQueueItem({
|
||||
appType,
|
||||
isCurrent,
|
||||
}: ProviderQueueItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: health } = useProviderHealth(provider.id, appType);
|
||||
|
||||
return (
|
||||
@@ -344,7 +372,7 @@ function ProviderQueueItem({
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-primary/20 text-primary">
|
||||
使用中
|
||||
{t("provider.inUse")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function ImportExportSection({
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
className="absolute -top-2 -right-2 h-6 w-6 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center shadow-lg transition-colors z-10"
|
||||
aria-label="Clear selection"
|
||||
aria-label={t("common.clear")}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -106,7 +106,9 @@ export function PricingEditModal({
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, modelId: e.target.value })
|
||||
}
|
||||
placeholder="例如: claude-3-5-sonnet-20241022"
|
||||
placeholder={t("usage.modelIdPlaceholder", {
|
||||
defaultValue: "例如: claude-3-5-sonnet-20241022",
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -122,7 +124,9 @@ export function PricingEditModal({
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, displayName: e.target.value })
|
||||
}
|
||||
placeholder="例如: Claude 3.5 Sonnet"
|
||||
placeholder={t("usage.displayNamePlaceholder", {
|
||||
defaultValue: "例如: Claude 3.5 Sonnet",
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,14 @@ export function RequestDetailPanel({
|
||||
requestId,
|
||||
onClose,
|
||||
}: RequestDetailPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { data: request, isLoading } = useRequestDetail(requestId);
|
||||
const dateLocale =
|
||||
i18n.language === "zh"
|
||||
? "zh-CN"
|
||||
: i18n.language === "ja"
|
||||
? "ja-JP"
|
||||
: "en-US";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -69,7 +75,9 @@ export function RequestDetailPanel({
|
||||
{t("usage.time", "时间")}
|
||||
</dt>
|
||||
<dd>
|
||||
{new Date(request.createdAt * 1000).toLocaleString("zh-CN")}
|
||||
{new Date(request.createdAt * 1000).toLocaleString(
|
||||
dateLocale,
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { LogFilters } from "@/types/usage";
|
||||
import { ChevronLeft, ChevronRight, RefreshCw, Search, X } from "lucide-react";
|
||||
|
||||
export function RequestLogTable() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// 默认时间范围:过去24小时
|
||||
@@ -62,6 +62,13 @@ export function RequestLogTable() {
|
||||
});
|
||||
};
|
||||
|
||||
const dateLocale =
|
||||
i18n.language === "zh"
|
||||
? "zh-CN"
|
||||
: i18n.language === "ja"
|
||||
? "ja-JP"
|
||||
: "en-US";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 筛选栏 */}
|
||||
@@ -77,10 +84,12 @@ export function RequestLogTable() {
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] bg-background">
|
||||
<SelectValue placeholder={t("usage.endpoint", "端点")} />
|
||||
<SelectValue placeholder={t("usage.appType", "应用类型")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("common.all", "全部端点")}</SelectItem>
|
||||
<SelectItem value="all">
|
||||
{t("usage.allApps", { defaultValue: "全部应用" })}
|
||||
</SelectItem>
|
||||
<SelectItem value="claude">Claude</SelectItem>
|
||||
<SelectItem value="codex">Codex</SelectItem>
|
||||
<SelectItem value="gemini">Gemini</SelectItem>
|
||||
@@ -97,7 +106,7 @@ export function RequestLogTable() {
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] bg-background">
|
||||
<SelectValue placeholder={t("usage.status", "状态码")} />
|
||||
<SelectValue placeholder={t("usage.statusCode", "状态码")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("common.all", "全部状态")}</SelectItem>
|
||||
@@ -113,7 +122,10 @@ export function RequestLogTable() {
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("usage.provider", "搜索供应商...")}
|
||||
placeholder={t(
|
||||
"usage.searchProviderPlaceholder",
|
||||
"搜索供应商...",
|
||||
)}
|
||||
className="pl-9 bg-background"
|
||||
value={tempFilters.providerName || ""}
|
||||
onChange={(e) =>
|
||||
@@ -125,7 +137,7 @@ export function RequestLogTable() {
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("usage.model", "搜索模型...")}
|
||||
placeholder={t("usage.searchModelPlaceholder", "搜索模型...")}
|
||||
className="w-[180px] bg-background"
|
||||
value={tempFilters.model || ""}
|
||||
onChange={(e) =>
|
||||
@@ -140,7 +152,9 @@ export function RequestLogTable() {
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="whitespace-nowrap">时间范围:</span>
|
||||
<span className="whitespace-nowrap">
|
||||
{t("usage.timeRange", { defaultValue: "时间范围" })}:
|
||||
</span>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
className="h-8 w-[200px] bg-background"
|
||||
@@ -267,7 +281,9 @@ export function RequestLogTable() {
|
||||
logs.map((log) => (
|
||||
<TableRow key={log.requestId}>
|
||||
<TableCell>
|
||||
{new Date(log.createdAt * 1000).toLocaleString("zh-CN")}
|
||||
{new Date(log.createdAt * 1000).toLocaleString(
|
||||
dateLocale,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.providerName ||
|
||||
|
||||
@@ -56,15 +56,15 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
|
||||
color: "text-purple-500",
|
||||
bg: "bg-purple-500/10",
|
||||
subValue: (
|
||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
|
||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Input</span>
|
||||
<span>{t("usage.input", { defaultValue: "Input" })}</span>
|
||||
<span className="text-foreground/80">
|
||||
{(inputTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Output</span>
|
||||
<span>{t("usage.output", { defaultValue: "Output" })}</span>
|
||||
<span className="text-foreground/80">
|
||||
{(outputTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
@@ -78,21 +78,21 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
|
||||
icon: Database,
|
||||
color: "text-orange-500",
|
||||
bg: "bg-orange-500/10",
|
||||
subValue: (
|
||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Write</span>
|
||||
<span className="text-foreground/80">
|
||||
{(cacheWriteTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
subValue: (
|
||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{t("usage.cacheWrite", { defaultValue: "Write" })}</span>
|
||||
<span className="text-foreground/80">
|
||||
{(cacheWriteTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{t("usage.cacheRead", { defaultValue: "Read" })}</span>
|
||||
<span className="text-foreground/80">
|
||||
{(cacheReadTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Read</span>
|
||||
<span className="text-foreground/80">
|
||||
{(cacheReadTokens / 1000).toFixed(1)}k
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -17,7 +17,7 @@ interface UsageTrendChartProps {
|
||||
}
|
||||
|
||||
export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { data: trends, isLoading } = useUsageTrends(days);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -29,14 +29,20 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
||||
}
|
||||
|
||||
const isToday = days === 1;
|
||||
const dateLocale =
|
||||
i18n.language === "zh"
|
||||
? "zh-CN"
|
||||
: i18n.language === "ja"
|
||||
? "ja-JP"
|
||||
: "en-US";
|
||||
const chartData =
|
||||
trends?.map((stat) => {
|
||||
const pointDate = new Date(stat.date);
|
||||
return {
|
||||
rawDate: stat.date,
|
||||
label: isToday
|
||||
? pointDate.toLocaleTimeString("zh-CN", { hour: "2-digit" })
|
||||
: pointDate.toLocaleDateString("zh-CN", {
|
||||
? pointDate.toLocaleTimeString(dateLocale, { hour: "2-digit" })
|
||||
: pointDate.toLocaleDateString(dateLocale, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ProxyConfig } from "@/types/proxy";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import type { ProxyConfig } from "@/types/proxy";
|
||||
*/
|
||||
export function useProxyConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 查询配置
|
||||
const { data: config, isLoading } = useQuery({
|
||||
@@ -24,12 +26,16 @@ export function useProxyConfig() {
|
||||
mutationFn: (newConfig: ProxyConfig) =>
|
||||
invoke("update_proxy_config", { config: newConfig }),
|
||||
onSuccess: () => {
|
||||
toast.success("代理配置已保存", { closeButton: true });
|
||||
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`保存失败: ${error.message}`);
|
||||
toast.error(
|
||||
t("proxy.settings.toast.saveFailed", {
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ export function useProxyStatus() {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
const detail =
|
||||
extractErrorMessage(error) || t("common.unknown", { defaultValue: "未知错误" });
|
||||
toast.error(
|
||||
t("proxy.server.startFailed", {
|
||||
defaultValue: `启动代理服务失败: ${detail}`,
|
||||
@@ -75,7 +76,8 @@ export function useProxyStatus() {
|
||||
queryClient.invalidateQueries({ queryKey: ["providerHealth"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
const detail =
|
||||
extractErrorMessage(error) || t("common.unknown", { defaultValue: "未知错误" });
|
||||
toast.error(
|
||||
t("proxy.stopWithRestoreFailed", {
|
||||
defaultValue: `停止失败: ${detail}`,
|
||||
@@ -111,7 +113,8 @@ export function useProxyStatus() {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
const detail =
|
||||
extractErrorMessage(error) || t("common.unknown", { defaultValue: "未知错误" });
|
||||
toast.error(
|
||||
t("proxy.takeover.failed", {
|
||||
defaultValue: `操作失败: ${detail}`,
|
||||
@@ -133,8 +136,14 @@ export function useProxyStatus() {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
toast.error(`切换失败: ${detail}`);
|
||||
const detail =
|
||||
extractErrorMessage(error) || t("common.unknown", { defaultValue: "未知错误" });
|
||||
toast.error(
|
||||
t("proxy.switchFailed", {
|
||||
error: detail,
|
||||
defaultValue: `切换失败: ${detail}`,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -378,7 +378,19 @@
|
||||
"daysAgo": "{{count}} day ago",
|
||||
"multiplePlans": "{{count}} plans",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse"
|
||||
"collapse": "Collapse",
|
||||
"modelIdPlaceholder": "e.g., claude-3-5-sonnet-20241022",
|
||||
"displayNamePlaceholder": "e.g., Claude 3.5 Sonnet",
|
||||
"appType": "App Type",
|
||||
"allApps": "All Apps",
|
||||
"statusCode": "Status Code",
|
||||
"searchProviderPlaceholder": "Search provider...",
|
||||
"searchModelPlaceholder": "Search model...",
|
||||
"timeRange": "Time Range",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"cacheWrite": "Write",
|
||||
"cacheRead": "Read"
|
||||
},
|
||||
"usageScript": {
|
||||
"title": "Configure Usage Query",
|
||||
@@ -448,7 +460,9 @@
|
||||
"tip3": "• Entire config must be wrapped in () to form object literal expression"
|
||||
},
|
||||
"errors": {
|
||||
"usage_query_failed": "Usage query failed"
|
||||
"usage_query_failed": "Usage query failed",
|
||||
"configLoadFailedTitle": "Configuration Load Failed",
|
||||
"configLoadFailedMessage": "Unable to read configuration file:\n{{path}}\n\nError details:\n{{detail}}\n\nPlease check if the JSON is valid, or restore from a backup file (e.g., config.json.bak) in the same directory.\n\nThe app will exit so you can fix this."
|
||||
},
|
||||
"presetSelector": {
|
||||
"title": "Select Configuration Type",
|
||||
@@ -847,7 +861,9 @@
|
||||
"label": "Icon",
|
||||
"colorLabel": "Icon Color",
|
||||
"selectIcon": "Select Icon",
|
||||
"preview": "Preview"
|
||||
"preview": "Preview",
|
||||
"clickToChange": "Click to change icon",
|
||||
"clickToSelect": "Click to select icon"
|
||||
},
|
||||
"migration": {
|
||||
"success": "Configuration migrated successfully"
|
||||
@@ -855,7 +871,36 @@
|
||||
"agents": {
|
||||
"title": "Agents"
|
||||
},
|
||||
"health": {
|
||||
"operational": "Operational",
|
||||
"degraded": "Degraded",
|
||||
"failed": "Failed",
|
||||
"circuitOpen": "Circuit Open",
|
||||
"consecutiveFailures": "{{count}} consecutive failures"
|
||||
},
|
||||
"proxy": {
|
||||
"panel": {
|
||||
"serviceAddress": "Service Address",
|
||||
"addressCopied": "Address copied",
|
||||
"currentProvider": "Current Provider:",
|
||||
"waitingFirstRequest": "Current Provider: Waiting for first request...",
|
||||
"stoppedTitle": "Proxy Service Stopped",
|
||||
"stoppedDescription": "Use the toggle in the top right to start the service",
|
||||
"openSettings": "Configure Proxy Service",
|
||||
"stats": {
|
||||
"activeConnections": "Active Connections",
|
||||
"totalRequests": "Total Requests",
|
||||
"successRate": "Success Rate",
|
||||
"uptime": "Uptime"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"toast": {
|
||||
"saved": "Proxy configuration saved",
|
||||
"saveFailed": "Save failed: {{error}}"
|
||||
}
|
||||
},
|
||||
"switchFailed": "Switch failed: {{error}}",
|
||||
"failoverQueue": {
|
||||
"title": "Failover Queue",
|
||||
"description": "Manage failover order for each app's providers",
|
||||
|
||||
@@ -378,7 +378,19 @@
|
||||
"daysAgo": "{{count}} 日前",
|
||||
"multiplePlans": "{{count}} プラン",
|
||||
"expand": "展開",
|
||||
"collapse": "折りたたむ"
|
||||
"collapse": "折りたたむ",
|
||||
"modelIdPlaceholder": "例: claude-3-5-sonnet-20241022",
|
||||
"displayNamePlaceholder": "例: Claude 3.5 Sonnet",
|
||||
"appType": "アプリ種別",
|
||||
"allApps": "すべてのアプリ",
|
||||
"statusCode": "ステータスコード",
|
||||
"searchProviderPlaceholder": "プロバイダーを検索...",
|
||||
"searchModelPlaceholder": "モデルを検索...",
|
||||
"timeRange": "期間",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"cacheWrite": "Write",
|
||||
"cacheRead": "Read"
|
||||
},
|
||||
"usageScript": {
|
||||
"title": "利用状況を設定",
|
||||
@@ -448,7 +460,9 @@
|
||||
"tip3": "• 全体を () で囲み、オブジェクトリテラル式にしてください"
|
||||
},
|
||||
"errors": {
|
||||
"usage_query_failed": "利用状況の取得に失敗しました"
|
||||
"usage_query_failed": "利用状況の取得に失敗しました",
|
||||
"configLoadFailedTitle": "設定の読み込みに失敗しました",
|
||||
"configLoadFailedMessage": "設定ファイルを読み込めません:\n{{path}}\n\nエラー詳細:\n{{detail}}\n\nJSON が正しいか確認するか、同じディレクトリのバックアップファイル(config.json.bak など)から復元してください。\n\nアプリを終了して修正してください。"
|
||||
},
|
||||
"presetSelector": {
|
||||
"title": "設定タイプを選択",
|
||||
@@ -847,12 +861,88 @@
|
||||
"label": "アイコン",
|
||||
"colorLabel": "アイコンカラー",
|
||||
"selectIcon": "アイコンを選択",
|
||||
"preview": "プレビュー"
|
||||
"preview": "プレビュー",
|
||||
"clickToChange": "クリックでアイコンを変更",
|
||||
"clickToSelect": "クリックでアイコンを選択"
|
||||
},
|
||||
"migration": {
|
||||
"success": "設定の移行が完了しました"
|
||||
},
|
||||
"agents": {
|
||||
"title": "エージェント"
|
||||
},
|
||||
"health": {
|
||||
"operational": "正常",
|
||||
"degraded": "低下",
|
||||
"failed": "失敗",
|
||||
"circuitOpen": "サーキットオープン",
|
||||
"consecutiveFailures": "{{count}} 回連続失敗"
|
||||
},
|
||||
"proxy": {
|
||||
"panel": {
|
||||
"serviceAddress": "サービスアドレス",
|
||||
"addressCopied": "アドレスをコピーしました",
|
||||
"currentProvider": "現在のプロバイダー:",
|
||||
"waitingFirstRequest": "現在のプロバイダー: 最初のリクエスト待ち...",
|
||||
"stoppedTitle": "プロキシサービス停止中",
|
||||
"stoppedDescription": "右上のトグルでサービスを開始できます",
|
||||
"openSettings": "プロキシサービスを設定",
|
||||
"stats": {
|
||||
"activeConnections": "アクティブ接続",
|
||||
"totalRequests": "総リクエスト数",
|
||||
"successRate": "成功率",
|
||||
"uptime": "稼働時間"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"toast": {
|
||||
"saved": "プロキシ設定を保存しました",
|
||||
"saveFailed": "保存に失敗しました: {{error}}"
|
||||
}
|
||||
},
|
||||
"switchFailed": "切り替えに失敗しました: {{error}}",
|
||||
"failoverQueue": {
|
||||
"title": "フェイルオーバーキュー",
|
||||
"description": "各アプリのプロバイダーのフェイルオーバー順序を管理します",
|
||||
"info": "現在アクティブなプロバイダーが常に優先されます。リクエストが失敗すると、システムはキュー順に他のプロバイダーを試行します。",
|
||||
"selectProvider": "キューに追加するプロバイダーを選択",
|
||||
"noAvailableProviders": "追加できるプロバイダーがありません",
|
||||
"empty": "フェイルオーバーキューが空です。自動フェイルオーバーを有効にするにはプロバイダーを追加してください。",
|
||||
"dragHint": "ドラッグでフェイルオーバー順序を調整します。番号が小さいほど優先度が高くなります。",
|
||||
"toggleEnabled": "有効/無効",
|
||||
"addSuccess": "フェイルオーバーキューに追加しました",
|
||||
"addFailed": "追加に失敗しました",
|
||||
"removeSuccess": "フェイルオーバーキューから削除しました",
|
||||
"removeFailed": "削除に失敗しました",
|
||||
"reorderSuccess": "キュー順序を更新しました",
|
||||
"reorderFailed": "順序の更新に失敗しました",
|
||||
"toggleFailed": "状態の更新に失敗しました"
|
||||
},
|
||||
"autoFailover": {
|
||||
"info": "フェイルオーバーキューに複数のプロバイダーが設定されている場合、リクエストが失敗すると優先度順に試行します。プロバイダーが連続失敗のしきい値に達すると、サーキットブレーカーが開き、一時的にスキップされます。",
|
||||
"configSaved": "自動フェイルオーバー設定を保存しました",
|
||||
"configSaveFailed": "保存に失敗しました",
|
||||
"retrySettings": "リトライとタイムアウト設定",
|
||||
"failureThreshold": "失敗しきい値",
|
||||
"failureThresholdHint": "この回数連続で失敗するとサーキットブレーカーが開きます(推奨: 3-10)",
|
||||
"timeout": "回復待ち時間(秒)",
|
||||
"timeoutHint": "サーキットが開いた後、回復を試みるまでの待ち時間(推奨: 30-120)",
|
||||
"circuitBreakerSettings": "サーキットブレーカー詳細設定",
|
||||
"successThreshold": "回復成功しきい値",
|
||||
"successThresholdHint": "半開状態でこの回数成功するとサーキットブレーカーが閉じます",
|
||||
"errorRate": "エラー率しきい値 (%)",
|
||||
"errorRateHint": "この値を超えるとサーキットブレーカーが開きます",
|
||||
"minRequests": "最小リクエスト数",
|
||||
"minRequestsHint": "エラー率を計算する前の最小リクエスト数",
|
||||
"explanationTitle": "仕組み",
|
||||
"failureThresholdLabel": "失敗しきい値",
|
||||
"failureThresholdExplain": "この回数連続で失敗すると、サーキットブレーカーが開き、プロバイダーは一時的に利用不可になります",
|
||||
"timeoutLabel": "回復待ち時間",
|
||||
"timeoutExplain": "サーキットが開いた後、半開状態を試みるまでの待ち時間",
|
||||
"successThresholdLabel": "回復成功しきい値",
|
||||
"successThresholdExplain": "半開状態でこの回数成功するとサーキットブレーカーが閉じ、プロバイダーが再び利用可能になります",
|
||||
"errorRateLabel": "エラー率しきい値",
|
||||
"errorRateExplain": "失敗しきい値に達していなくても、エラー率がこの値を超えるとサーキットブレーカーが開きます"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +378,19 @@
|
||||
"daysAgo": "{{count}} 天前",
|
||||
"multiplePlans": "{{count}} 个套餐",
|
||||
"expand": "展开",
|
||||
"collapse": "收起"
|
||||
"collapse": "收起",
|
||||
"modelIdPlaceholder": "例如: claude-3-5-sonnet-20241022",
|
||||
"displayNamePlaceholder": "例如: Claude 3.5 Sonnet",
|
||||
"appType": "应用类型",
|
||||
"allApps": "全部应用",
|
||||
"statusCode": "状态码",
|
||||
"searchProviderPlaceholder": "搜索供应商...",
|
||||
"searchModelPlaceholder": "搜索模型...",
|
||||
"timeRange": "时间范围",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"cacheWrite": "Write",
|
||||
"cacheRead": "Read"
|
||||
},
|
||||
"usageScript": {
|
||||
"title": "配置用量查询",
|
||||
@@ -448,7 +460,9 @@
|
||||
"tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式"
|
||||
},
|
||||
"errors": {
|
||||
"usage_query_failed": "用量查询失败"
|
||||
"usage_query_failed": "用量查询失败",
|
||||
"configLoadFailedTitle": "配置加载失败",
|
||||
"configLoadFailedMessage": "无法读取配置文件:\n{{path}}\n\n错误详情:\n{{detail}}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak)恢复。\n\n应用将退出以便您进行修复。"
|
||||
},
|
||||
"presetSelector": {
|
||||
"title": "选择配置类型",
|
||||
@@ -847,7 +861,9 @@
|
||||
"label": "图标",
|
||||
"colorLabel": "图标颜色",
|
||||
"selectIcon": "选择图标",
|
||||
"preview": "预览"
|
||||
"preview": "预览",
|
||||
"clickToChange": "点击更换图标",
|
||||
"clickToSelect": "点击选择图标"
|
||||
},
|
||||
"migration": {
|
||||
"success": "配置迁移成功"
|
||||
@@ -855,7 +871,36 @@
|
||||
"agents": {
|
||||
"title": "智能体"
|
||||
},
|
||||
"health": {
|
||||
"operational": "正常",
|
||||
"degraded": "降级",
|
||||
"failed": "失败",
|
||||
"circuitOpen": "熔断",
|
||||
"consecutiveFailures": "连续失败 {{count}} 次"
|
||||
},
|
||||
"proxy": {
|
||||
"panel": {
|
||||
"serviceAddress": "服务地址",
|
||||
"addressCopied": "地址已复制",
|
||||
"currentProvider": "当前 Provider:",
|
||||
"waitingFirstRequest": "当前 Provider:等待首次请求…",
|
||||
"stoppedTitle": "代理服务已停止",
|
||||
"stoppedDescription": "使用右上角开关即可启动服务",
|
||||
"openSettings": "配置代理服务",
|
||||
"stats": {
|
||||
"activeConnections": "活跃连接",
|
||||
"totalRequests": "总请求数",
|
||||
"successRate": "成功率",
|
||||
"uptime": "运行时间"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"toast": {
|
||||
"saved": "代理配置已保存",
|
||||
"saveFailed": "保存失败: {{error}}"
|
||||
}
|
||||
},
|
||||
"switchFailed": "切换失败: {{error}}",
|
||||
"failoverQueue": {
|
||||
"title": "故障转移队列",
|
||||
"description": "管理各应用的供应商故障转移顺序",
|
||||
|
||||
+13
-3
@@ -4,7 +4,7 @@ import App from "./App";
|
||||
import { UpdateProvider } from "./contexts/UpdateContext";
|
||||
import "./index.css";
|
||||
// 导入国际化配置
|
||||
import "./i18n";
|
||||
import i18n from "./i18n";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { queryClient } from "@/lib/query";
|
||||
@@ -43,8 +43,18 @@ async function handleConfigLoadError(
|
||||
const detail = payload?.error ?? "Unknown error";
|
||||
|
||||
await message(
|
||||
`无法读取配置文件:\n${path}\n\n错误详情:\n${detail}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak)恢复。\n\n应用将退出以便您进行修复。`,
|
||||
{ title: "配置加载失败", kind: "error" },
|
||||
i18n.t("errors.configLoadFailedMessage", {
|
||||
path,
|
||||
detail,
|
||||
defaultValue:
|
||||
"无法读取配置文件:\n{{path}}\n\n错误详情:\n{{detail}}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak)恢复。\n\n应用将退出以便您进行修复。",
|
||||
}),
|
||||
{
|
||||
title: i18n.t("errors.configLoadFailedTitle", {
|
||||
defaultValue: "配置加载失败",
|
||||
}),
|
||||
kind: "error",
|
||||
},
|
||||
);
|
||||
|
||||
await exit(1);
|
||||
|
||||
Reference in New Issue
Block a user