From 7ca73697b0f5b9b5645884817fab616b4608ebdc Mon Sep 17 00:00:00 2001 From: Alex Jordan <60003097+alex-jordan547@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:37:50 +0200 Subject: [PATCH] fix(i18n): localize hardcoded web UI copy (#9245) * fix(i18n): localize hardcoded web UI copy * test(i18n): cover hardcoded UI regressions * chore(changelog): add PR 9245 fragment --- .../fixes/9245-hardcoded-web-ui-i18n.md | 1 + .../(dashboard)/dashboard/BootstrapBanner.tsx | 23 +- .../(dashboard)/dashboard/HomePageClient.tsx | 136 +- .../analytics/AutoRoutingAnalyticsTab.tsx | 8 +- .../analytics/SearchAnalyticsTab.tsx | 32 +- .../dashboard/audit/McpAuditTab.tsx | 8 +- .../dashboard/batch/BatchDetailModal.tsx | 128 +- .../dashboard/batch/FileDetailModal.tsx | 62 +- .../(dashboard)/dashboard/chaos/chaosI18n.ts | 16 + .../components/ChaosConfigActionsBar.tsx | 5 +- .../components/ChaosTestResultsPanel.tsx | 25 +- src/app/(dashboard)/dashboard/chaos/page.tsx | 8 +- .../chaos/useChaosConfigPersistence.ts | 22 +- .../dashboard/chaos/useChaosTestRun.ts | 20 +- .../components/CliproxyapiToolCard.tsx | 70 +- .../combos/IntelligentComboPanel.tsx | 15 +- .../(dashboard)/dashboard/combos/error.tsx | 16 +- .../dashboard/compression/page.tsx | 12 +- .../dashboard/endpoint/EndpointPageClient.tsx | 33 +- .../endpoint/components/A2ADashboard.tsx | 2 +- .../endpoint/components/NotionSourceCard.tsx | 71 +- .../[id]/components/ConnectionRow.tsx | 26 +- .../components/CoolingConnectionsPanel.tsx | 18 +- .../[id]/components/CustomModelsSection.tsx | 30 +- .../components/KimiCodeAuthMethodModal.tsx | 6 +- .../[id]/components/ModelCompatPopover.tsx | 15 +- .../providers/[id]/components/ModelRow.tsx | 2 +- .../components/NoAuthProviderControls.tsx | 6 +- .../[id]/components/PassthroughModelRow.tsx | 2 +- .../components/ProviderPlaygroundPanel.tsx | 8 +- .../modals/EditCompatibleNodeModal.tsx | 6 +- .../[id]/hooks/useCommandCodeAuth.ts | 75 +- .../[id]/hooks/useConnectionDeleteConfirm.ts | 11 +- .../[id]/hooks/useModelVisibilityHandlers.ts | 6 +- .../[id]/hooks/useProviderConnections.ts | 83 +- .../providers/[id]/hooks/useProviderModels.ts | 21 +- .../[id]/hooks/useProviderSettings.ts | 42 +- .../components/AddCompatibleProviderModal.tsx | 6 +- .../providers/components/ProviderCard.tsx | 32 +- .../components/ProviderSummaryCard.tsx | 41 +- .../(dashboard)/dashboard/providers/error.tsx | 38 +- .../providers/hooks/useProviderModels.ts | 6 +- .../(dashboard)/dashboard/providers/page.tsx | 28 +- .../CliproxyProviderExposureCard.tsx | 1 - src/app/(dashboard)/dashboard/relay/page.tsx | 12 +- .../settings/components/AuthzSection.tsx | 2 +- .../settings/components/AutoDisableCard.tsx | 2 +- .../components/BackgroundDegradationTab.tsx | 41 +- .../components/CliproxyapiSettingsTab.tsx | 85 +- .../settings/components/MemorySkillsTab.tsx | 22 +- .../settings/components/ModelAliasesTab.tsx | 21 +- .../settings/components/ModelsDevSyncTab.tsx | 6 +- .../settings/components/OneproxyTab.tsx | 37 +- .../settings/components/ProxyStatusBadge.tsx | 5 +- .../settings/components/ResilienceTab.tsx | 35 +- .../settings/components/RoutingTab.tsx | 117 +- .../settings/components/SecurityTab.tsx | 12 +- .../settings/components/SystemStorageTab.tsx | 18 +- .../components/proxy/DocumentationTab.tsx | 11 +- .../components/proxy/FreeProxyRow.tsx | 11 +- .../(dashboard)/dashboard/settings/error.tsx | 16 +- .../tools/traffic-inspector/page.tsx | 12 +- .../components/advanced/PipelineView.tsx | 27 +- src/app/400/page.tsx | 20 +- src/app/401/page.tsx | 20 +- src/app/403/page.tsx | 17 +- src/app/408/page.tsx | 17 +- src/app/429/page.tsx | 17 +- src/app/500/page.tsx | 17 +- src/app/502/page.tsx | 20 +- src/app/503/page.tsx | 17 +- src/app/authorize/route.ts | 27 +- .../codex/[token]/CodexConnectClient.tsx | 62 +- src/app/docs/[...slug]/page.tsx | 6 +- src/app/docs/api-explorer/page.tsx | 21 +- src/app/docs/components/ApiExplorerClient.tsx | 33 +- src/app/docs/components/FeedbackWidget.tsx | 11 +- src/app/docs/layout.tsx | 72 +- src/app/docs/page.tsx | 99 +- src/app/error.tsx | 26 +- src/app/global-error.tsx | 141 +- src/app/login/page.tsx | 2 +- src/app/maintenance/page.tsx | 18 +- src/app/not-found.tsx | 16 +- src/app/offline/page.tsx | 16 +- src/app/status/page.tsx | 73 +- src/i18n/messages/en.json | 1206 ++++++++++++++-- src/i18n/messages/fr.json | 1231 ++++++++++++++-- src/i18n/messages/pt-BR.json | 1242 ++++++++++++++--- src/i18n/messages/vi.json | 1226 ++++++++++++++-- src/shared/components/CommandPalette.tsx | 12 +- .../components/DistributeProxiesButton.tsx | 16 +- src/shared/components/Footer.tsx | 48 +- src/shared/components/KiroAuthModal.tsx | 111 +- .../components/KiroSocialOAuthModal.tsx | 40 +- src/shared/components/ModelRoutingSection.tsx | 7 +- src/shared/components/ModelSelectField.tsx | 8 +- .../components/NoAuthProviderToggle.tsx | 8 +- src/shared/components/OAuthModal.tsx | 110 +- .../components/ProviderTestSlideOver.tsx | 92 +- src/shared/components/ProxyLogDetail.tsx | 44 +- src/shared/components/RequestLoggerDetail.tsx | 172 +-- src/shared/components/RequestLoggerV2.tsx | 34 +- src/shared/components/RequestTimeline.tsx | 59 +- src/shared/components/SystemMonitor.tsx | 40 +- src/shared/components/TokenHealthBadge.tsx | 10 +- src/shared/components/TraeAuthModal.tsx | 65 +- src/shared/components/UsageStats.tsx | 74 +- .../ComboCompressionModeSelect.tsx | 26 +- src/shared/components/docs/APIReference.tsx | 14 +- .../components/docs/DocsBreadcrumbs.tsx | 6 +- .../components/oauthModal/GheConfigStep.tsx | 16 +- .../oauthModal/GitlabDuoSetupStep.tsx | 30 +- .../i18n-hardcoded-ui-regressions.test.ts | 81 ++ 114 files changed, 6465 insertions(+), 2045 deletions(-) create mode 100644 changelog.d/fixes/9245-hardcoded-web-ui-i18n.md create mode 100644 src/app/(dashboard)/dashboard/chaos/chaosI18n.ts create mode 100644 tests/unit/i18n-hardcoded-ui-regressions.test.ts diff --git a/changelog.d/fixes/9245-hardcoded-web-ui-i18n.md b/changelog.d/fixes/9245-hardcoded-web-ui-i18n.md new file mode 100644 index 0000000000..a2c0157189 --- /dev/null +++ b/changelog.d/fixes/9245-hardcoded-web-ui-i18n.md @@ -0,0 +1 @@ +- **fix(i18n):** localized hardcoded web UI copy across public pages, dashboard views, and shared components, with complete French and Vietnamese coverage ([#9245](https://github.com/diegosouzapw/OmniRoute/pull/9245)) — thanks @alex-jordan547 diff --git a/src/app/(dashboard)/dashboard/BootstrapBanner.tsx b/src/app/(dashboard)/dashboard/BootstrapBanner.tsx index a819d82d1d..f771ae34a7 100644 --- a/src/app/(dashboard)/dashboard/BootstrapBanner.tsx +++ b/src/app/(dashboard)/dashboard/BootstrapBanner.tsx @@ -27,22 +27,17 @@ export default function BootstrapBanner() { ⚠️

- Running in zero-config mode + {t("zeroConfigBannerTitle")}

- OmniRoute auto-generated secure encryption keys on first launch. They are persisted to{" "} - - {dataDir} - - . No action is required — your data is encrypted and safe. To use custom keys, add{" "} - - JWT_SECRET - {" "} - and{" "} - - STORAGE_ENCRYPTION_KEY - {" "} - to that file. + {t.rich("zeroConfigBannerBody", { + dataDir, + code: (chunks) => ( + + {chunks} + + ), + })}

{updatePhase === "failed" && ( )} @@ -924,30 +924,32 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {

- Update Available: v{versionInfo.latest} {isElectron && "(Desktop App)"} + {t("updateAvailableTitle", { + version: versionInfo.latest, + desktop: isElectron ? ` ${t("desktopAppLabel")}` : "", + })}

{isElectron ? ( <> - {electronUpdateStatus.status === "checking" && "Checking for updates..."} + {electronUpdateStatus.status === "checking" && t("checkingForUpdates")} {electronUpdateStatus.status === "available" && - `Version v${versionInfo.latest} is available for download.`} + t("versionAvailableForDownload", { version: versionInfo.latest })} {electronUpdateStatus.status === "downloading" && - `Downloading update... ${electronUpdateStatus.percent || 0}% complete.`} - {electronUpdateStatus.status === "downloaded" && - "Update downloaded successfully! Click Restart & Install to apply."} + t("downloadingUpdate", { percent: electronUpdateStatus.percent || 0 })} + {electronUpdateStatus.status === "downloaded" && t("updateDownloaded")} {electronUpdateStatus.status === "error" && - `Auto-update failed: ${electronUpdateStatus.message || "Unknown error"}.`} + t("autoUpdateFailed", { + reason: electronUpdateStatus.message || t("unknownUpdateError"), + })} {(electronUpdateStatus.status === "idle" || electronUpdateStatus.status === "not-available") && - `Version v${versionInfo.latest} is available for the desktop app.`} + t("versionAvailableDesktop", { version: versionInfo.latest })} ) : versionInfo.autoUpdateSupported ? ( - t("updateAvailableDesc") || - `You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.` + t("updateAvailableDesc") ) : ( - versionInfo.autoUpdateError || - "Manual update required for this installation type." + versionInfo.autoUpdateError || t("manualUpdateRequired") )}

@@ -961,7 +963,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { onClick={() => globalThis.window.electronAPI?.downloadUpdate()} className="font-semibold" > - Download Update + {t("downloadUpdate")} )} {electronUpdateStatus.status === "downloading" && ( @@ -980,7 +982,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { onClick={() => globalThis.window.electronAPI?.installUpdate()} className="font-semibold animate-pulse" > - Restart & Install + {t("restartAndInstall")} )} {(electronUpdateStatus.status === "error" || @@ -996,7 +998,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { }} className="font-semibold" > - Check for Update + {t("checkForUpdate")} )} @@ -1008,9 +1010,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { className="ml-4 shrink-0 font-semibold" title={versionInfo.autoUpdateError || ""} > - {versionInfo.autoUpdateSupported - ? t("updateNow") || "Update Now" - : "Manual Update"} + {versionInfo.autoUpdateSupported ? t("updateNow") : t("manualUpdate")} )} @@ -1022,9 +1022,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { electronUpdateStatus.status === "available" || electronUpdateStatus.status === "not-available") && (
-

- Or download the respective installer format directly: -

+

{t("directDownloadHint")}

)}
@@ -259,7 +279,7 @@ export default function FileDetailModal({ {isTruncated && (
warning - Showing first 1000 lines ({lineCount} total lines) + {t("batchFilePreviewTruncated", { shown: 1000, total: lineCount })}
)}
@@ -281,7 +301,7 @@ export default function FileDetailModal({ className="flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-[var(--color-accent)] text-white hover:opacity-90 transition-opacity" > download - Download Full File + {t("batchFileDownloadFull")} diff --git a/src/app/(dashboard)/dashboard/chaos/chaosI18n.ts b/src/app/(dashboard)/dashboard/chaos/chaosI18n.ts new file mode 100644 index 0000000000..528621b435 --- /dev/null +++ b/src/app/(dashboard)/dashboard/chaos/chaosI18n.ts @@ -0,0 +1,16 @@ +export type ChaosTranslator = (( + key: string, + values?: Record +) => string) & { + has?: (key: string) => boolean; +}; + +export function chaosText( + t: ChaosTranslator, + key: string, + fallback: string, + values?: Record +): string { + if (typeof t.has !== "function" || !t.has(key)) return fallback; + return values ? t(key, values) : t(key); +} diff --git a/src/app/(dashboard)/dashboard/chaos/components/ChaosConfigActionsBar.tsx b/src/app/(dashboard)/dashboard/chaos/components/ChaosConfigActionsBar.tsx index 8cbc3d3960..54242757db 100644 --- a/src/app/(dashboard)/dashboard/chaos/components/ChaosConfigActionsBar.tsx +++ b/src/app/(dashboard)/dashboard/chaos/components/ChaosConfigActionsBar.tsx @@ -1,6 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; +import { chaosText, type ChaosTranslator } from "../chaosI18n"; /** * Save/Reset + Test-run action buttons for the Chaos Mode config page. @@ -22,7 +23,7 @@ export function ChaosConfigActionsBar({ onReset: () => void; onTest: () => void; }) { - const t = useTranslations("chaosConfig"); + const t = useTranslations("chaosConfig") as ChaosTranslator; return ( <> @@ -66,7 +67,7 @@ export function ChaosConfigActionsBar({ ) : ( play_arrow )} - {testing ? "Running..." : t("testButton")} + {testing ? chaosText(t, "running", "Running...") : t("testButton")} diff --git a/src/app/(dashboard)/dashboard/chaos/components/ChaosTestResultsPanel.tsx b/src/app/(dashboard)/dashboard/chaos/components/ChaosTestResultsPanel.tsx index 38a0d54d2b..63d822f6d3 100644 --- a/src/app/(dashboard)/dashboard/chaos/components/ChaosTestResultsPanel.tsx +++ b/src/app/(dashboard)/dashboard/chaos/components/ChaosTestResultsPanel.tsx @@ -1,5 +1,8 @@ "use client"; +import { useTranslations } from "next-intl"; +import { chaosText, type ChaosTranslator } from "../chaosI18n"; + export interface ChaosModelResult { providerId: string; providerName: string; @@ -26,14 +29,24 @@ export interface ChaosTestResult { * complexity/size ratchet (config/quality/complexity-baseline.json). */ export function ChaosTestResultsPanel({ result }: { result: ChaosTestResult }) { + const t = useTranslations("chaosConfig") as ChaosTranslator; + const resultsTitle = chaosText( + t, + "testResults", + `Test Results — ${result.mode} mode (${result.totalProviders} providers)`, + { mode: result.mode, count: result.totalProviders } + ); + const startedLabel = chaosText( + t, + "started", + `Started: ${new Date(result.startedAt).toLocaleTimeString()}`, + { time: new Date(result.startedAt).toLocaleTimeString() } + ); + return (
-

- Test Results — {result.mode} mode ({result.totalProviders} providers) -

-
- Started: {new Date(result.startedAt).toLocaleTimeString()} -
+

{resultsTitle}

+
{startedLabel}
{result.models.map((model, idx) => (
; diff --git a/src/app/(dashboard)/dashboard/chaos/useChaosConfigPersistence.ts b/src/app/(dashboard)/dashboard/chaos/useChaosConfigPersistence.ts index a3616b544b..1cd2e85417 100644 --- a/src/app/(dashboard)/dashboard/chaos/useChaosConfigPersistence.ts +++ b/src/app/(dashboard)/dashboard/chaos/useChaosConfigPersistence.ts @@ -2,6 +2,7 @@ import { useCallback, useState, type Dispatch, type SetStateAction } from "react"; import { useTranslations } from "next-intl"; +import { chaosText, type ChaosTranslator } from "./chaosI18n"; import type { ChaosPageConfig, ChaosPageMessage } from "./chaosPageTypes"; /** @@ -13,7 +14,7 @@ export function useChaosConfigPersistence( config: ChaosPageConfig, setConfig: Dispatch> ) { - const t = useTranslations("chaosConfig"); + const t = useTranslations("chaosConfig") as ChaosTranslator; const [saving, setSaving] = useState(false); const [message, setMessage] = useState(null); @@ -49,17 +50,26 @@ export function useChaosConfigPersistence( if (res.ok) { const data = await res.json(); setConfig(data.config); - setMessage({ type: "success", text: "Config reset to defaults" }); + setMessage({ + type: "success", + text: chaosText(t, "resetSuccess", "Config reset to defaults"), + }); } else { - const err = await res.json().catch(() => ({ error: "Reset failed" })); - setMessage({ type: "error", text: err.error || "Reset failed" }); + const err = await res.json().catch(() => ({ error: null })); + setMessage({ + type: "error", + text: err.error || chaosText(t, "resetFailed", "Reset failed"), + }); } } catch { - setMessage({ type: "error", text: "Failed to reset config" }); + setMessage({ + type: "error", + text: chaosText(t, "resetFailed", "Failed to reset config"), + }); } finally { setSaving(false); } - }, [setConfig]); + }, [setConfig, t]); return { t, saving, message, setMessage, saveConfig, resetConfig }; } diff --git a/src/app/(dashboard)/dashboard/chaos/useChaosTestRun.ts b/src/app/(dashboard)/dashboard/chaos/useChaosTestRun.ts index 092c9620f4..5b9baa2bb0 100644 --- a/src/app/(dashboard)/dashboard/chaos/useChaosTestRun.ts +++ b/src/app/(dashboard)/dashboard/chaos/useChaosTestRun.ts @@ -2,6 +2,7 @@ import { useCallback, useState } from "react"; import { useTranslations } from "next-intl"; +import { chaosText, type ChaosTranslator } from "./chaosI18n"; import type { ChaosTestResult } from "./components/ChaosTestResultsPanel"; import type { ChaosPageConfig, ChaosPageMessage } from "./chaosPageTypes"; @@ -10,8 +11,11 @@ import type { ChaosPageConfig, ChaosPageMessage } from "./chaosPageTypes"; * of the page component to keep it under the complexity/size ratchet * (config/quality/complexity-baseline.json). */ -export function useChaosTestRun(config: ChaosPageConfig, setMessage: (message: ChaosPageMessage) => void) { - const t = useTranslations("chaosConfig"); +export function useChaosTestRun( + config: ChaosPageConfig, + setMessage: (message: ChaosPageMessage) => void +) { + const t = useTranslations("chaosConfig") as ChaosTranslator; const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); @@ -34,11 +38,17 @@ export function useChaosTestRun(config: ChaosPageConfig, setMessage: (message: C const data: ChaosTestResult = await res.json(); setTestResult(data); } else { - const err = await res.json().catch(() => ({ error: "Unknown error" })); - setMessage({ type: "error", text: err.error || "Test failed" }); + const err = await res.json().catch(() => ({ error: null })); + setMessage({ + type: "error", + text: err.error || chaosText(t, "testFailed", "Test failed"), + }); } } catch (err: any) { - setMessage({ type: "error", text: err.message || "Test failed" }); + setMessage({ + type: "error", + text: err.message || chaosText(t, "testFailed", "Test failed"), + }); } finally { setTesting(false); } diff --git a/src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard.tsx index 68733f13b2..f3c58fe5d1 100644 --- a/src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import { useTranslations } from "next-intl"; import { Card, Button } from "@/shared/components"; interface ToolState { @@ -24,6 +25,7 @@ interface UpdateInfo { } export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () => {} }) { + const t = useTranslations("cliTools"); const [toolState, setToolState] = useState(null); const [updateInfo, setUpdateInfo] = useState(null); const [loading, setLoading] = useState(null); @@ -71,7 +73,7 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () }); const data = await res.json(); if (res.ok) { - setMessage({ type: "success", text: data.message || `${action} succeeded` }); + setMessage({ type: "success", text: data.message || t("cliproxyapiActionSucceeded") }); await fetchStatus(); if (action === "install" || action === "restart") await fetchUpdateInfo(); } else { @@ -79,11 +81,14 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || - `${action} failed`, + t("cliproxyapiActionFailed"), }); } } catch (err) { - setMessage({ type: "error", text: err instanceof Error ? err.message : "Request failed" }); + setMessage({ + type: "error", + text: err instanceof Error ? err.message : t("cliproxyapiRequestFailed"), + }); } finally { setLoading(null); } @@ -93,14 +98,26 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () if (!toolState) return null; const s = toolState.status; const map: Record = { - running: { label: "Running", color: "bg-green-500/10 text-green-600 dark:text-green-400" }, - stopped: { label: "Stopped", color: "bg-zinc-500/10 text-zinc-500 dark:text-zinc-400" }, + running: { + label: t("cliproxyapiStatusRunning"), + color: "bg-green-500/10 text-green-600 dark:text-green-400", + }, + stopped: { + label: t("cliproxyapiStatusStopped"), + color: "bg-zinc-500/10 text-zinc-500 dark:text-zinc-400", + }, not_installed: { - label: "Not Installed", + label: t("cliproxyapiStatusNotInstalled"), color: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400", }, - installed: { label: "Installed", color: "bg-blue-500/10 text-blue-600 dark:text-blue-400" }, - error: { label: "Error", color: "bg-red-500/10 text-red-600 dark:text-red-400" }, + installed: { + label: t("cliproxyapiStatusInstalled"), + color: "bg-blue-500/10 text-blue-600 dark:text-blue-400", + }, + error: { + label: t("cliproxyapiStatusError"), + color: "bg-red-500/10 text-red-600 dark:text-red-400", + }, }; const badge = map[s] || map.not_installed; return ( @@ -125,9 +142,7 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = ()

CLIProxyAPI

{statusBadge()}
-

- Upstream proxy fallback (Go-based OAuth) -

+

{t("cliproxyapiDescription")}

- Update available: v{updateInfo.current} → v{updateInfo.latest} + {t("cliproxyapiUpdateAvailable", { + current: updateInfo.current, + latest: updateInfo.latest, + })} )}
-

Version

+

{t("cliproxyapiVersion")}

- {toolState?.installedVersion ? `v${toolState.installedVersion}` : "Not installed"} + {toolState?.installedVersion + ? `v${toolState.installedVersion}` + : t("cliproxyapiNotInstalledValue")}

-

Health

+

{t("cliproxyapiHealth")}

{toolState?.healthStatus === "healthy" - ? `Healthy` + ? t("cliproxyapiHealthy") : toolState?.healthStatus === "unhealthy" - ? "Unhealthy" - : "Unknown"} + ? t("cliproxyapiUnhealthy") + : t("cliproxyapiUnknown")}

-

Port

+

{t("cliproxyapiPort")}

{toolState?.port || 8317}

@@ -209,7 +229,7 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () loading={loading === "install"} > download - Install + {t("cliproxyapiInstall")} )} {toolState?.status === "running" ? ( @@ -220,7 +240,7 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () loading={loading === "stop"} > stop - Stop + {t("cliproxyapiStop")} ) : toolState?.installedVersion ? ( ) : null} {toolState?.status === "running" && ( @@ -241,7 +261,7 @@ export default function CliproxyapiToolCard({ isExpanded = false, onToggle = () loading={loading === "restart"} > restart_alt - Restart + {t("cliproxyapiRestart")} )} diff --git a/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx index 6063081e87..07551683f6 100644 --- a/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx +++ b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx @@ -133,8 +133,8 @@ export default function IntelligentComboPanel({ {combo?.name} - {allCombos.length} intelligent combo(s) - {providerScopeCount} providers in scope + {t("intelligentComboCount", { count: allCombos.length })} + {t("providersInScope", { count: providerScopeCount })} @@ -161,7 +161,7 @@ export default function IntelligentComboPanel({

- Candidate Pool + {getI18nOrFallback(t, "candidatePoolLabel", "Candidate Pool")}

{providerScopeCount}

@@ -183,7 +183,12 @@ export default function IntelligentComboPanel({

{savingModePack && ( - Saving {savingModePack}… + + {getI18nOrFallback(t, "savingModePack", "Saving {pack}…").replace( + "{pack}", + savingModePack + )} + )} @@ -308,7 +313,7 @@ export default function IntelligentComboPanel({

- Exploration Rate + {getI18nOrFallback(t, "explorationRateLabel", "Exploration Rate")}

{Math.round(normalizedConfig.explorationRate * 100)}% diff --git a/src/app/(dashboard)/dashboard/combos/error.tsx b/src/app/(dashboard)/dashboard/combos/error.tsx index 4071cb243d..4f7fa2a903 100644 --- a/src/app/(dashboard)/dashboard/combos/error.tsx +++ b/src/app/(dashboard)/dashboard/combos/error.tsx @@ -1,5 +1,7 @@ "use client"; +import { useTranslations } from "next-intl"; + export default function CombosError({ error: _error, reset, @@ -7,6 +9,8 @@ export default function CombosError({ error: Error & { digest?: string }; reset: () => void; }) { + const t = useTranslations("combos"); + return (

-

- Failed to load combos -

-

- We could not load combo data right now. Check your connection and try again. -

+

{t("errorTitle")}

+

{t("errorDescription")}

{_error?.digest && ( -

Error ID: {_error.digest}

+

{t("errorId", { id: _error.digest })}

)} {process.env.NODE_ENV === "development" && _error?.message && (

{_error.message}

@@ -30,7 +30,7 @@ export default function CombosError({ onClick={reset} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors focus:outline-2 focus:outline-offset-2 focus:outline-primary" > - Try Again + {t("errorRetry")}
diff --git a/src/app/(dashboard)/dashboard/compression/page.tsx b/src/app/(dashboard)/dashboard/compression/page.tsx index 63d5b50ca1..af2b1b19a3 100644 --- a/src/app/(dashboard)/dashboard/compression/page.tsx +++ b/src/app/(dashboard)/dashboard/compression/page.tsx @@ -1,9 +1,13 @@ import { redirect } from "next/navigation"; +import { getTranslations } from "next-intl/server"; -export const metadata = { - title: "Compression", - description: "Configure context compression settings to reduce token usage and costs.", -}; +export async function generateMetadata() { + const t = await getTranslations("metadata"); + return { + title: t("compressionTitle"), + description: t("compressionDescription"), + }; +} export default function CompressionPage() { redirect("/dashboard/context/caveman"); diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx index d9f4d8cc4f..f7785f1306 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx @@ -534,7 +534,10 @@ export default function APIPageClient({ machineId }: Readonly hub

- {t("categoryCore") || "Core APIs"} + {t("categoryCore")}

@@ -1843,7 +1846,7 @@ export default function APIPageClient({ machineId }: Readonly perm_media

- {t("categoryMedia") || "Media & Multi-Modal"} + {t("categoryMedia")}

@@ -1916,7 +1919,7 @@ export default function APIPageClient({ machineId }: Readonly

- {t("categorySearch") || "Search & Discovery"} + {t("categorySearch")}

@@ -1992,7 +1995,7 @@ export default function APIPageClient({ machineId }: Readonly ({ id: p.id, owned_by: p.id, type: "search" }))} copy={copy} @@ -2008,7 +2011,7 @@ export default function APIPageClient({ machineId }: Readonly build

- {t("categoryUtility") || "Utility & Management"} + {t("categoryUtility")}

@@ -2041,7 +2044,7 @@ export default function APIPageClient({ machineId }: Readonly{t("tableTask")} {t("tableSkill")} {t("tableState")} - {t("tablePhase") || "FSM Status"} + {t("tablePhase")} {t("tableUpdated")} {t("tableActions")} diff --git a/src/app/(dashboard)/dashboard/endpoint/components/NotionSourceCard.tsx b/src/app/(dashboard)/dashboard/endpoint/components/NotionSourceCard.tsx index b95fac1b2c..fca809af6e 100644 --- a/src/app/(dashboard)/dashboard/endpoint/components/NotionSourceCard.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/components/NotionSourceCard.tsx @@ -12,6 +12,18 @@ export default function NotionSourceCard() { const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const [expanded, setExpanded] = useState(false); + const translateOrFallback = (key: string, fallback: string) => { + try { + const translated = t(key as never); + if (!translated || translated === key || translated === `endpoint.${key}`) { + return fallback; + } + return translated; + } catch { + return fallback; + } + }; + const fetchConfig = useCallback(async () => { try { const res = await fetch("/api/settings/notion"); @@ -37,7 +49,10 @@ export default function NotionSourceCard() { const handleSaveToken = async () => { if (!token.trim()) { - setMessage({ type: "error", text: "Please enter a Notion integration token" }); + setMessage({ + type: "error", + text: translateOrFallback("notionEnterToken", "Please enter a Notion integration token"), + }); return; } setBusy(true); @@ -53,11 +68,20 @@ export default function NotionSourceCard() { setConnected(true); setMessage({ type: "success", text: data.message }); } else { - setMessage({ type: "error", text: data.error ?? "Failed to connect" }); + setMessage({ + type: "error", + text: data.error ?? translateOrFallback("notionConnectFailed", "Failed to connect"), + }); setConnected(false); } } catch (err) { - setMessage({ type: "error", text: err instanceof Error ? err.message : "Connection failed" }); + setMessage({ + type: "error", + text: + err instanceof Error + ? err.message + : translateOrFallback("notionConnectionFailed", "Connection failed"), + }); } finally { setBusy(false); } @@ -74,10 +98,19 @@ export default function NotionSourceCard() { setToken(""); setMessage({ type: "success", text: data.message }); } else { - setMessage({ type: "error", text: data.error ?? "Failed to disconnect" }); + setMessage({ + type: "error", + text: data.error ?? translateOrFallback("notionDisconnectFailed", "Failed to disconnect"), + }); } } catch (err) { - setMessage({ type: "error", text: err instanceof Error ? err.message : "Disconnect failed" }); + setMessage({ + type: "error", + text: + err instanceof Error + ? err.message + : translateOrFallback("notionDisconnectFailed", "Disconnect failed"), + }); } finally { setBusy(false); } @@ -97,11 +130,16 @@ export default function NotionSourceCard() {
Notion - {connected ? "Connected" : "Not connected"} + {connected + ? translateOrFallback("notionConnected", "Connected") + : translateOrFallback("notionNotConnected", "Not connected")}

- Search, read, query, and write to Notion through routed AI models + {translateOrFallback( + "notionDescription", + "Search, read, query, and write to Notion through routed AI models" + )}

- Create an Internal Integration at{" "} + {translateOrFallback( + "notionIntegrationHelp", + "Create an Internal Integration at" + )}{" "} https://www.notion.so/profile/integrations @@ -156,7 +200,10 @@ export default function NotionSourceCard() { ) : (

- Token configured. Notion tools are available via MCP. + {translateOrFallback( + "notionTokenConfigured", + "Token configured. Notion tools are available via MCP." + )}
)} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx index 5da7bf3d0a..02f1d27267 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx @@ -250,7 +250,7 @@ function getStatusPresentation( if (errorType === "account_deactivated") { return { statusVariant: "error", - statusLabel: t("statusDeactivated", "Deactivated"), + statusLabel: providerText(t, "statusDeactivated", "Deactivated"), errorType, errorBadge, errorTextClass: "text-red-600 font-bold", @@ -305,7 +305,7 @@ function getStatusPresentation( if (errorType === "banned") { return { statusVariant: "error", - statusLabel: t("statusBanned", "Banned (403)"), + statusLabel: providerText(t, "statusBanned", "Banned (403)"), errorType, errorBadge, errorTextClass: "text-red-600 font-bold", @@ -315,7 +315,7 @@ function getStatusPresentation( if (errorType === "credits_exhausted") { return { statusVariant: "warning", - statusLabel: t("statusCreditsExhausted", "Out of Credits"), + statusLabel: providerText(t, "statusCreditsExhausted", "Out of Credits"), errorType, errorBadge, errorTextClass: "text-amber-500", @@ -400,22 +400,10 @@ export default function ConnectionRow({ t("oauthAccount") ) : connection.name; - const applyCodexAuthLabel = - typeof t.has === "function" && t.has("applyCodexAuthLocal") - ? t("applyCodexAuthLocal") - : "Apply auth"; - const exportCodexAuthLabel = - typeof t.has === "function" && t.has("exportCodexAuthFile") - ? t("exportCodexAuthFile") - : "Export auth"; - const applyClaudeAuthLabel = - typeof t.has === "function" && t.has("applyClaudeAuthLocal") - ? t("applyClaudeAuthLocal") - : "Apply auth"; - const exportClaudeAuthLabel = - typeof t.has === "function" && t.has("exportClaudeAuthFile") - ? t("exportClaudeAuthFile") - : "Export auth"; + const applyCodexAuthLabel = providerText(t, "applyCodexAuthLocal", "Apply auth"); + const exportCodexAuthLabel = providerText(t, "exportCodexAuthFile", "Export auth"); + const applyClaudeAuthLabel = providerText(t, "applyClaudeAuthLocal", "Apply auth"); + const exportClaudeAuthLabel = providerText(t, "exportClaudeAuthFile", "Export auth"); // Use useState + useEffect for impure Date.now() to avoid calling during render const [isCooldown, setIsCooldown] = useState(false); // T12: token expiry status — lazy init avoids calling Date.now() during render; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CoolingConnectionsPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CoolingConnectionsPanel.tsx index 63df0643f3..eeac716437 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/CoolingConnectionsPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CoolingConnectionsPanel.tsx @@ -22,8 +22,10 @@ */ import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; import { formatResetCountdown } from "@/shared/utils/formatting"; import type { ConnectionRowConnection } from "./ConnectionRow"; +import { providerText } from "../providerPageHelpers"; export interface CoolingConnectionsPanelProps { readonly connections: readonly ConnectionRowConnection[]; @@ -37,6 +39,7 @@ function isCoolingNow(connection: ConnectionRowConnection, now: number): boolean export default function CoolingConnectionsPanel(props: CoolingConnectionsPanelProps) { const { connections } = props; + const t = useTranslations("providers"); // Tick once per second so the human-readable countdown updates. const [now, setNow] = useState(() => Date.now()); useEffect(() => { @@ -58,12 +61,17 @@ export default function CoolingConnectionsPanel(props: CoolingConnectionsPanelPr className="inline-block h-2 w-2 animate-pulse rounded-full bg-amber-500" />

- Currently cooling ({cooling.length}) + {providerText(t, "coolingConnectionsTitle", "Currently cooling ({count})", { + count: cooling.length, + })}

- These connections returned a 429 (rate-limit) on their last request. OmniRoute will skip - them until the timer expires — no manual disable required. + {providerText( + t, + "coolingConnectionsDescription", + "These connections returned a 429 (rate-limit) on their last request. OmniRoute will skip them until the timer expires — no manual disable required." + )}

    {cooling.map((c) => { @@ -72,7 +80,9 @@ export default function CoolingConnectionsPanel(props: CoolingConnectionsPanelPr c.displayName || c.name || c.email || - (c.id ? `connection ${c.id.slice(0, 8)}` : "connection"); + (c.id + ? `${providerText(t, "connectionFallback", "connection")} ${c.id.slice(0, 8)}` + : providerText(t, "connectionFallback", "connection")); return (
  • { if (!editingModelId || editingModelId !== modelId) return; if (!editingEndpoints.length) { - notify.error("Select at least one supported endpoint"); + notify.error( + providerText(t, "selectSupportedEndpoint", "Select at least one supported endpoint") + ); return; } @@ -429,7 +447,7 @@ export default function CustomModelsSection({ : ep === "embeddings" ? `📐 ${t("supportedEndpointEmbeddings")}` : ep === "rerank" - ? "Rerank" + ? providerText(t, "rerankEndpoint", "Rerank") : ep === "images" ? `🖼️ ${t("supportedEndpointImages")}` : `🔊 ${t("supportedEndpointAudio")}`} @@ -661,7 +679,7 @@ export default function CustomModelsSection({ : ep === "embeddings" ? `📐 ${t("supportedEndpointEmbeddings")}` : ep === "rerank" - ? "Rerank" + ? providerText(t, "rerankEndpoint", "Rerank") : ep === "images" ? `🖼️ ${t("supportedEndpointImages")}` : `🔊 ${t("supportedEndpointAudio")}`} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/KimiCodeAuthMethodModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/KimiCodeAuthMethodModal.tsx index 0480a1147b..323ec2a2c6 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/KimiCodeAuthMethodModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/KimiCodeAuthMethodModal.tsx @@ -1,7 +1,7 @@ "use client"; import { Modal } from "@/shared/components"; -import type { ProviderMessageTranslator } from "../providerPageHelpers"; +import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers"; type KimiCodeAuthMethodModalProps = { isOpen: boolean; @@ -50,7 +50,9 @@ export default function KimiCodeAuthMethodModal({
    key
    -

    Kimi Code API Key

    +

    + {providerText(t, "kimiCodeApiKeyLabel", "Kimi Code API Key")} +

    {t("apiKeySecure")}

    diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx index 44c0708325..f85be293de 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx @@ -13,6 +13,7 @@ import { UPSTREAM_HEADERS_UI_MAX, headerRowsToRecord, compatProtocolLabelKey, + providerText, type HeaderDraftRow, } from "../providerPageHelpers"; @@ -353,7 +354,7 @@ export default function ModelCompatPopover({ {/* Param filters — model-level block/allow (#6625) */}

    - {t("compatBlockedParamsHint") ?? "Blocked params (stripped from requests)"} + {providerText( + t, + "compatBlockedParamsHint", + "Blocked params (stripped from requests)" + )} {paramSaving && ` ● ${t("compatSaving")}`}

    @@ -387,7 +392,11 @@ export default function ModelCompatPopover({ className="mb-1 w-full rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-[11px] font-mono text-text-main placeholder:text-text-muted focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 dark:border-zinc-600 dark:bg-zinc-900" />

    - {t("compatAllowedParamsHint") ?? "Allowed params (re-added after deny)"} + {providerText( + t, + "compatAllowedParamsHint", + "Allowed params (re-added after deny)" + )}

    diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx index 2c453971d5..e8f1521721 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx @@ -413,7 +413,7 @@ export default function ModelRow({ : testStatus === "quota" ? t("modelTestQuotaTooltip") : testStatus === "error" - ? "Error" + ? providerText(t, "errorShort", "Error") : t("testModel") } > diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/NoAuthProviderControls.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/NoAuthProviderControls.tsx index 148dee8ad4..f2b33f73df 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/NoAuthProviderControls.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/NoAuthProviderControls.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { NoAuthAccountCard, NoAuthProviderCard } from "@/shared/components"; import { getProviderAlias, supportsNoAuthProviderProxy } from "@/shared/constants/providers"; import { useNotificationStore } from "@/store/notificationStore"; +import { providerText } from "../providerPageHelpers"; const ACCOUNT_PROVIDER_NAMES: Record = { mimocode: "MiMoCode", @@ -153,7 +154,10 @@ export default function NoAuthProviderControls({ const res = await fetch("/api/dahl/tokens", { method: "POST" }); const data = await res.json(); if (!res.ok || !data.token) { - throw new Error(data?.error || "Failed to create Dahl token"); + throw new Error( + data?.error || + providerText(t, "createDahlTokenFailed", "Failed to create Dahl token") + ); } return data.token as string; } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx index 32d2d044ed..66a3bfd8ea 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx @@ -204,7 +204,7 @@ export default function PassthroughModelRow({ : testStatus === "quota" ? t("modelTestQuotaTooltip") : testStatus === "error" - ? "Error" + ? providerText(t, "errorShort", "Error") : t("testModel") } > diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx index a77bb6cd06..c7bb85f4f2 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx @@ -6,6 +6,7 @@ // a single-kind panel or the LlmChatCard for standard LLM providers. import { useState } from "react"; +import { useTranslations } from "next-intl"; import { LlmChatCard } from "@/app/(dashboard)/dashboard/media-providers/components/LlmChatCard"; import { ServiceKindTabs } from "@/app/(dashboard)/dashboard/media-providers/components/ServiceKindTabs"; import { EmbeddingExampleCard } from "@/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard"; @@ -18,6 +19,7 @@ import { VideoExampleCard } from "@/app/(dashboard)/dashboard/media-providers/co import { MusicExampleCard } from "@/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard"; import type { ServiceKind } from "@/shared/constants/providers"; import { AI_PROVIDERS } from "@/shared/constants/providers"; +import { providerText } from "../providerPageHelpers"; export const MEDIA_SERVICE_KINDS: ServiceKind[] = [ "embedding", @@ -56,12 +58,12 @@ export function renderKindPanel(kind: ServiceKind, providerId: string): JSX.Elem } export default function ProviderPlaygroundPanel({ providerId }: { providerId: string }) { + const t = useTranslations("providers"); // Resolve serviceKinds from AI_PROVIDERS. // For providers without explicit serviceKinds (most LLM providers), we infer // "llm" as the default. const providerEntry = AI_PROVIDERS[providerId as keyof typeof AI_PROVIDERS] as - | (Record & { serviceKinds?: string[] }) - | undefined; + (Record & { serviceKinds?: string[] }) | undefined; const rawKinds: string[] = providerEntry?.serviceKinds ?? []; @@ -93,7 +95,7 @@ export default function ProviderPlaygroundPanel({ providerId }: { providerId: st return (
    -

    Playground

    +

    {providerText(t, "playgroundTitle", "Playground")}

    ({ phase: "idle", state: "", @@ -62,7 +64,7 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "applying", - message: "Applying browser-approved key…", + message: providerText(t, "commandCodeApplyingKey", "Applying browser-approved key…"), })); try { @@ -74,7 +76,9 @@ export function useCommandCodeAuth({ const data = await res.json().catch(() => ({})); if (!res.ok) { - const errorMessage = data.error || "Failed to apply Command Code auth"; + const errorMessage = + data.error || + providerText(t, "commandCodeApplyFailed", "Failed to apply Command Code auth"); setCommandCodeAuthState((current) => ({ ...current, phase: "error", @@ -87,26 +91,30 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "applied", - message: "Command Code connected", + message: providerText(t, "commandCodeConnected", "Command Code connected"), })); commandCodeAuthWindowRef.current?.close?.(); commandCodeAuthWindowRef.current = null; await fetchConnections(); handleCloseAddApiKeyModal(); - notify.success("Command Code connection added"); + notify.success( + providerText(t, "commandCodeConnectionAdded", "Command Code connection added") + ); return true; } catch (error) { console.error("Error applying Command Code auth:", error); setCommandCodeAuthState((current) => ({ ...current, phase: "error", - message: "Failed to apply Command Code auth", + message: providerText(t, "commandCodeApplyFailed", "Failed to apply Command Code auth"), })); - notify.error("Failed to apply Command Code auth"); + notify.error( + providerText(t, "commandCodeApplyFailed", "Failed to apply Command Code auth") + ); return false; } }, - [fetchConnections, handleCloseAddApiKeyModal, notify] + [fetchConnections, handleCloseAddApiKeyModal, notify, t] ); const handleStartCommandCodeAuth = useCallback(async () => { @@ -124,7 +132,7 @@ export function useCommandCodeAuth({ authUrl: "", callbackUrl: "", expiresAt: null, - message: "Opening Command Code Studio…", + message: providerText(t, "commandCodeOpeningStudio", "Opening Command Code Studio…"), }); try { @@ -135,7 +143,9 @@ export function useCommandCodeAuth({ const data = await res.json().catch(() => ({})); if (!res.ok || !data.state || !data.authUrl) { - const errorMessage = data.error || "Failed to start Command Code auth"; + const errorMessage = + data.error || + providerText(t, "commandCodeStartFailed", "Failed to start Command Code auth"); setCommandCodeAuthState((current) => ({ ...current, phase: "error", @@ -152,7 +162,11 @@ export function useCommandCodeAuth({ authUrl: data.authUrl, callbackUrl: data.callbackUrl || "", expiresAt: data.expiresAt || null, - message: "Open the auth URL, approve access, then paste the returned key/JSON/URL below…", + message: providerText( + t, + "commandCodeApprovalInstructions", + "Open the auth URL, approve access, then paste the returned key/JSON/URL below…" + ), }); if (popup) { @@ -169,9 +183,19 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "error", - message: "Popup blocked. Please allow popups and try Command Code Connect again.", + message: providerText( + t, + "commandCodePopupBlocked", + "Popup blocked. Please allow popups and try Command Code Connect again." + ), })); - notify.error("Popup blocked. Please allow popups and try Command Code Connect again."); + notify.error( + providerText( + t, + "commandCodePopupBlocked", + "Popup blocked. Please allow popups and try Command Code Connect again." + ) + ); return; } commandCodeAuthWindowRef.current = fallbackPopup; @@ -183,11 +207,11 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "expired", - message: "Command Code link expired", + message: providerText(t, "commandCodeLinkExpired", "Command Code link expired"), })); commandCodeAuthWindowRef.current?.close?.(); commandCodeAuthWindowRef.current = null; - notify.error("Command Code auth expired"); + notify.error(providerText(t, "commandCodeAuthExpired", "Command Code auth expired")); clearCommandCodeAuthTimer(); return; } @@ -206,11 +230,11 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "expired", - message: "Command Code link expired", + message: providerText(t, "commandCodeLinkExpired", "Command Code link expired"), })); commandCodeAuthWindowRef.current?.close?.(); commandCodeAuthWindowRef.current = null; - notify.error("Command Code auth expired"); + notify.error(providerText(t, "commandCodeAuthExpired", "Command Code auth expired")); clearCommandCodeAuthTimer(); return; } @@ -219,13 +243,15 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "applied", - message: "Command Code connected", + message: providerText(t, "commandCodeConnected", "Command Code connected"), })); commandCodeAuthWindowRef.current?.close?.(); commandCodeAuthWindowRef.current = null; await fetchConnections(); handleCloseAddApiKeyModal(); - notify.success("Command Code connection added"); + notify.success( + providerText(t, "commandCodeConnectionAdded", "Command Code connection added") + ); clearCommandCodeAuthTimer(); return; } @@ -234,7 +260,11 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "received", - message: "Browser approved, applying…", + message: providerText( + t, + "commandCodeApplyingApproval", + "Browser approved, applying…" + ), })); clearCommandCodeAuthTimer(); await handleCommandCodeAuthApply( @@ -258,9 +288,9 @@ export function useCommandCodeAuth({ setCommandCodeAuthState((current) => ({ ...current, phase: "error", - message: "Failed to start Command Code auth", + message: providerText(t, "commandCodeStartFailed", "Failed to start Command Code auth"), })); - notify.error("Failed to start Command Code auth"); + notify.error(providerText(t, "commandCodeStartFailed", "Failed to start Command Code auth")); popup?.close?.(); commandCodeAuthWindowRef.current = null; clearCommandCodeAuthTimer(); @@ -272,6 +302,7 @@ export function useCommandCodeAuth({ fetchConnections, handleCommandCodeAuthApply, notify, + t, ]); const handleOpenCommandCodeConnect = useCallback(() => { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionDeleteConfirm.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionDeleteConfirm.ts index 001679d69e..724762fcd4 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionDeleteConfirm.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionDeleteConfirm.ts @@ -11,6 +11,8 @@ */ import { useCallback, useState } from "react"; +import { useTranslations } from "next-intl"; +import { providerText } from "../providerPageHelpers"; export interface ConnectionDeleteConfirmTarget { id: string; @@ -34,6 +36,7 @@ export function useConnectionDeleteConfirm( fetchConnections: () => Promise, notify: NotifyLike ): ConnectionDeleteConfirmState { + const t = useTranslations("providers"); const [connection, setConnection] = useState(null); const [deleting, setDeleting] = useState(false); @@ -56,24 +59,24 @@ export function useConnectionDeleteConfirm( try { const res = await fetch(`/api/providers/${connectionId}`, { method: "DELETE" }); if (res.ok) { - notify.success("Connection deleted"); + notify.success(providerText(t, "connectionDeleted", "Connection deleted")); await fetchConnections(); } else { const data = await res.json().catch(() => ({})); const message = (typeof data?.error === "string" && data.error) || data?.error?.message || - "Failed to delete connection"; + providerText(t, "failedDeleteConnection", "Failed to delete connection"); notify.error(message); } } catch (error) { console.error("Error deleting connection:", error); - notify.error("Failed to delete connection"); + notify.error(providerText(t, "failedDeleteConnection", "Failed to delete connection")); } finally { setDeleting(false); setConnection(null); } - }, [connection, fetchConnections, notify]); + }, [connection, fetchConnections, notify, t]); return { connection, deleting, request, confirm, cancel }; } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts index f364112247..d49bbc989f 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts @@ -316,11 +316,13 @@ export function useModelVisibilityHandlers({ // extractApiErrorMessage coerces any object-shaped `error` (e.g. a Zod // format object) to a string so notify.error never hands the toast a // non-string child (React #31 → frozen page). - notify.error(extractApiErrorMessage(data, "Model test failed")); + notify.error( + extractApiErrorMessage(data, providerText(t, "modelTestFailed", "Model test failed")) + ); setModelTestStatus((prev) => ({ ...prev, [modelId]: "error" })); } } catch (err) { - notify.error("Network error testing model"); + notify.error(providerText(t, "modelTestNetworkError", "Network error testing model")); setModelTestStatus((prev) => ({ ...prev, [modelId]: "error" })); } finally { setTestingModelId(null); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts index 1cc953002e..14d9be2bbc 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts @@ -30,7 +30,7 @@ import { connectionBelongsToProviderPage, getProviderConnectionsRequestUrl, } from "../../providerPageUtils"; -import { normalizeCodexLimitPolicy } from "../providerPageHelpers"; +import { normalizeCodexLimitPolicy, providerText } from "../providerPageHelpers"; import { useProviderQuotaVisibility } from "./useProviderQuotaVisibility"; import { useReorderByAvailability } from "./useReorderByAvailability"; import { @@ -399,7 +399,14 @@ export function useProviderConnections( if (!res.ok) { const data = await res.json().catch(() => ({})); - notify.error(data.error || "Failed to update Claude extra-usage policy"); + notify.error( + data.error || + providerText( + t, + "failedUpdateClaudeExtraUsagePolicy", + "Failed to update Claude extra-usage policy" + ) + ); return; } @@ -429,12 +436,26 @@ export function useProviderConnections( ); notify.success( enabled - ? "Claude extra-usage blocking enabled (extra usage will be blocked)" - : "Claude extra-usage blocking disabled (extra usage is allowed)" + ? providerText( + t, + "claudeExtraUsageBlockingEnabled", + "Claude extra-usage blocking enabled (extra usage will be blocked)" + ) + : providerText( + t, + "claudeExtraUsageBlockingDisabled", + "Claude extra-usage blocking disabled (extra usage is allowed)" + ) ); } catch (error) { console.error("Error toggling Claude extra-usage policy:", error); - notify.error("Failed to update Claude extra-usage policy"); + notify.error( + providerText( + t, + "failedUpdateClaudeExtraUsagePolicy", + "Failed to update Claude extra-usage policy" + ) + ); } }; @@ -468,7 +489,10 @@ export function useProviderConnections( if (!res.ok) { const data = await res.json().catch(() => ({})); - notify.error(data.error || "Failed to update Codex limit policy"); + notify.error( + data.error || + providerText(t, "failedUpdateCodexLimitPolicy", "Failed to update Codex limit policy") + ); return; } @@ -485,10 +509,12 @@ export function useProviderConnections( : connection ) ); - notify.success("Codex limit policy updated"); + notify.success(providerText(t, "codexLimitPolicyUpdated", "Codex limit policy updated")); } catch (error) { console.error("Error toggling Codex quota policy:", error); - notify.error("Failed to update Codex limit policy"); + notify.error( + providerText(t, "failedUpdateCodexLimitPolicy", "Failed to update Codex limit policy") + ); } }; @@ -518,7 +544,10 @@ export function useProviderConnections( if (!res.ok) { const data = await res.json().catch(() => ({})); - notify.error(data.error || "Failed to update upstream proxy routing"); + notify.error( + data.error || + providerText(t, "failedUpdateCliproxyRouting", "Failed to update upstream proxy routing") + ); return; } @@ -528,7 +557,9 @@ export function useProviderConnections( } notify.success(UPSTREAM_PROXY_MODE_MESSAGES[mode]); } catch { - notify.error("Failed to update upstream proxy routing"); + notify.error( + providerText(t, "failedUpdateCliproxyRouting", "Failed to update upstream proxy routing") + ); } }; @@ -718,10 +749,10 @@ export function useProviderConnections( if (onAfter) await onAfter(); } else { const data = await res.json(); - notify.error(data.error || "Batch delete failed"); + notify.error(data.error || providerText(t, "batchDeleteFailed", "Batch delete failed")); } } catch { - notify.error("Network error during batch delete"); + notify.error(providerText(t, "batchDeleteNetworkError", "Network error during batch delete")); } finally { setBatchDeleting(false); } @@ -743,7 +774,11 @@ export function useProviderConnections( }); if (!res.ok) { const data = await res.json().catch(() => ({})); - throw new Error(data.error?.message || data.error || "Batch update failed"); + throw new Error( + data.error?.message || + data.error || + providerText(t, "batchUpdateFailed", "Batch update failed") + ); } const data = await res.json(); updated += data.updated ?? 0; @@ -764,7 +799,10 @@ export function useProviderConnections( ); } } catch (error: any) { - notify.error(error?.message || "Network error during batch update"); + notify.error( + error?.message || + providerText(t, "batchUpdateNetworkError", "Network error during batch update") + ); } finally { setBatchUpdating(null); } @@ -859,7 +897,13 @@ export function useProviderConnections( const proxiesData = await proxiesRes.json(); const savedProxies = (proxiesData?.items || []).filter((p: any) => p.status === "active"); if (savedProxies.length === 0) { - notify.error("No saved proxies found. Add proxies in Settings → Proxy first."); + notify.error( + providerText( + t, + "noSavedProxies", + "No saved proxies found. Add proxies in Settings → Proxy first." + ) + ); return; } @@ -910,11 +954,16 @@ export function useProviderConnections( await fetchConnections(); const tagLabel = tagFilter ? `"${tagFilter}" ` : ""; notify.success( - `Distributed ${assigned} proxy assignment(s) across ${tagLabel}${sorted.length} connection(s).` + providerText( + t, + "proxiesDistributed", + "Distributed {assigned} proxy assignment(s) across {tagLabel}{total} connection(s).", + { assigned, tagLabel, total: sorted.length } + ) ); } catch (err) { console.error("Error distributing proxies:", err); - notify.error("Failed to distribute proxies."); + notify.error(providerText(t, "failedDistributeProxies", "Failed to distribute proxies.")); } finally { setDistributingProxies(false); } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts index 0128a2ee9f..05d0221fcd 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts @@ -16,7 +16,7 @@ import { useState, useCallback } from "react"; import { useTranslations } from "next-intl"; import { useNotificationStore } from "@/store/notificationStore"; -import type { CompatModelRow } from "../providerPageHelpers"; +import { providerText, type CompatModelRow } from "../providerPageHelpers"; // ──── types ───────────────────────────────────────────────────────────────── @@ -79,11 +79,13 @@ export function useProviderModels( notify.success(t("setAliasSuccess", { alias })); } else { const data = await res.json().catch(() => ({})); - notify.error(data?.error?.message || "Failed to set alias"); + notify.error( + data?.error?.message || providerText(t, "failedSetAlias", "Failed to set alias") + ); } } catch (error) { console.log("Error setting alias:", error); - notify.error("Network error setting alias"); + notify.error(providerText(t, "networkErrorSettingAlias", "Network error setting alias")); } }, [fetchAliases, t, notify] @@ -100,11 +102,13 @@ export function useProviderModels( notify.success(t("deleteAliasSuccess", { alias })); } else { const data = await res.json().catch(() => ({})); - notify.error(data?.error?.message || "Failed to delete alias"); + notify.error( + data?.error?.message || providerText(t, "failedDeleteAlias", "Failed to delete alias") + ); } } catch (error) { console.log("Error deleting alias:", error); - notify.error("Network error deleting alias"); + notify.error(providerText(t, "networkErrorDeletingAlias", "Network error deleting alias")); } }, [fetchAliases, t, notify] @@ -113,10 +117,9 @@ export function useProviderModels( const fetchProviderModelMeta = useCallback(async () => { if (isSearchProvider) return; try { - const res = await fetch( - `/api/provider-models?provider=${encodeURIComponent(providerId)}`, - { cache: "no-store" } - ); + const res = await fetch(`/api/provider-models?provider=${encodeURIComponent(providerId)}`, { + cache: "no-store", + }); if (!res.ok) return; const data = await res.json(); setModelMeta({ diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts index 6a9248b64d..0c3da0c719 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts @@ -115,9 +115,7 @@ export function useProviderSettings(providerId: string): UseProviderSettingsRetu } catch (error) { if (!isCurrentRequest()) return; setCodexSettingsLoaded(false); - setCodexSettingsLoadError( - error instanceof Error ? error.message : "Failed to load settings" - ); + setCodexSettingsLoadError(error instanceof Error ? error.message : "Failed to load settings"); } }, [providerId]); @@ -184,15 +182,20 @@ export function useProviderSettings(providerId: string): UseProviderSettingsRetu if (!res.ok) { const data = await res.json().catch(() => ({})); setCodexGlobalServiceMode(previousMode); - notify.error(data.error || "Failed to update Codex service mode"); + notify.error( + data.error || + providerText(t, "failedUpdateCodexServiceMode", "Failed to update Codex service mode") + ); return; } - notify.success("Codex service mode updated"); + notify.success(providerText(t, "codexServiceModeUpdated", "Codex service mode updated")); } catch (error) { setCodexGlobalServiceMode(previousMode); console.error("Error updating Codex service mode:", error); - notify.error("Failed to update Codex service mode"); + notify.error( + providerText(t, "failedUpdateCodexServiceMode", "Failed to update Codex service mode") + ); } finally { setSavingCodexGlobalServiceMode(false); } @@ -215,7 +218,14 @@ export function useProviderSettings(providerId: string): UseProviderSettingsRetu if (!res.ok) { const data = await res.json().catch(() => ({})); setPreferClaudeCodeForUnprefixedClaudeModels(previous); - notify.error(data.error || "Failed to update Claude Code routing preference"); + notify.error( + data.error || + providerText( + t, + "failedUpdateClaudeRoutingPreference", + "Failed to update Claude Code routing preference" + ) + ); return; } @@ -227,14 +237,26 @@ export function useProviderSettings(providerId: string): UseProviderSettingsRetu } notify.success( enabled - ? "Unprefixed Claude models now prefer Claude Code" - : "Unprefixed Claude models no longer prefer Claude Code" + ? providerText( + t, + "claudeRoutingPreferenceEnabled", + "Unprefixed Claude models now prefer Claude Code" + ) + : providerText( + t, + "claudeRoutingPreferenceDisabled", + "Unprefixed Claude models no longer prefer Claude Code" + ) ); } catch (error) { setPreferClaudeCodeForUnprefixedClaudeModels(previous); console.error("Error updating Claude Code routing preference:", error); notify.error( - providerText(t, "failedUpdateClaudeRoutingPreference", "Failed to update Claude Code routing preference") + providerText( + t, + "failedUpdateClaudeRoutingPreference", + "Failed to update Claude Code routing preference" + ) ); } finally { setSavingClaudeRoutingPreference(false); diff --git a/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx b/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx index bdbd751767..867393f0c6 100644 --- a/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx @@ -10,6 +10,7 @@ import { getClientIdentityProfileHeaders, } from "@/shared/constants/clientIdentityProfiles"; import NewApiAggregatorFields from "../[id]/components/modals/NewApiAggregatorFields"; +import { providerText } from "../[id]/providerPageHelpers"; type CompatibleMode = "openai" | "anthropic" | "cc"; type CompatibleProviderNode = { id: string } & Record; @@ -282,7 +283,10 @@ export default function AddCompatibleProviderModal({ method: data.method ?? null, }); } catch { - setValidationResult({ valid: false, error: "Network error" }); + setValidationResult({ + valid: false, + error: providerText(t, "networkError", "Network error"), + }); } finally { setValidating(false); } diff --git a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx index f8da4b9f9f..83a504bd14 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx @@ -31,17 +31,17 @@ interface ProviderStats { codexServiceTier?: "default" | "priority" | "flex" | null; } -const KIND_LABEL: Record = { - llm: "Chat", - embedding: "Embed", - image: "Image", - imageToText: "I→T", - tts: "TTS", - stt: "STT", - webSearch: "Search", - webFetch: "Fetch", - video: "Video", - music: "Music", +const KIND_LABEL_KEYS: Record = { + llm: { key: "serviceKindChat", fallback: "Chat" }, + embedding: { key: "serviceKindEmbedding", fallback: "Embed" }, + image: { key: "serviceKindImage", fallback: "Image" }, + imageToText: { key: "serviceKindImageToText", fallback: "I→T" }, + tts: { key: "serviceKindTts", fallback: "TTS" }, + stt: { key: "serviceKindStt", fallback: "STT" }, + webSearch: { key: "serviceKindWebSearch", fallback: "Search" }, + webFetch: { key: "serviceKindWebFetch", fallback: "Fetch" }, + video: { key: "serviceKindVideo", fallback: "Video" }, + music: { key: "serviceKindMusic", fallback: "Music" }, }; /** Maps a compatible-provider `apiType` to its `KIND_LABEL` key (#6936: non-chat @@ -167,6 +167,10 @@ const ProviderCard = forwardRef(function const t = useTranslations("providers"); const tc = useTranslations("common"); const tp = useTranslations("miniPlayground"); + const kindLabel = (kind: string) => { + const entry = KIND_LABEL_KEYS[kind]; + return entry ? providerText(t, entry.key, entry.fallback) : kind; + }; const [testExpanded, setTestExpanded] = useState(false); const innerRef = useRef(null); const linkElementRef = useRef(null); @@ -463,15 +467,15 @@ const ProviderCard = forwardRef(function key={k} className="text-[10px] px-1.5 py-0.5 rounded bg-bg-subtle border border-border text-text-muted leading-none" > - {KIND_LABEL[k] ?? k} + {kindLabel(k)} ))} {isCompatible && ( {provider.apiType === "responses" ? t("responses") - : (KIND_LABEL[COMPATIBLE_API_TYPE_KIND[provider.apiType ?? ""] ?? ""] ?? - t("chat"))} + : kindLabel(COMPATIBLE_API_TYPE_KIND[provider.apiType ?? ""] ?? "") || + t("chat")} )} {isCcCompatible && ( diff --git a/src/app/(dashboard)/dashboard/providers/components/ProviderSummaryCard.tsx b/src/app/(dashboard)/dashboard/providers/components/ProviderSummaryCard.tsx index a4be582732..2bc5f10c44 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ProviderSummaryCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ProviderSummaryCard.tsx @@ -110,7 +110,12 @@ export default function ProviderSummaryCard({ const categories = [ { key: null, color: null, label: t("providerSummaryAll"), stat: summaryStats.all }, { key: "oauth", color: "bg-blue-500", label: t("oauthLabel"), stat: summaryStats.oauth }, - { key: "ide", color: "bg-cyan-500", label: "IDE", stat: summaryStats.ide }, + { + key: "ide", + color: "bg-cyan-500", + label: providerText(t, "categoryIde", "IDE"), + stat: summaryStats.ide, + }, { key: "free", color: "bg-green-500", @@ -132,8 +137,18 @@ export default function ProviderSummaryCard({ label: t("compatibleLabel"), stat: summaryStats.compatible, }, - { key: "webcookie", color: "bg-purple-500", label: "Web Cookie", stat: summaryStats.webcookie }, - { key: "search", color: "bg-teal-500", label: "Search", stat: summaryStats.search }, + { + key: "webcookie", + color: "bg-purple-500", + label: providerText(t, "categoryWebCookie", "Web Cookie"), + stat: summaryStats.webcookie, + }, + { + key: "search", + color: "bg-teal-500", + label: providerText(t, "categorySearch", "Search"), + stat: summaryStats.search, + }, { key: "webfetch", color: "bg-orange-500", @@ -141,12 +156,22 @@ export default function ProviderSummaryCard({ stat: summaryStats.webfetch, title: t("webFetchTooltip"), }, - { key: "audio", color: "bg-rose-500", label: "Audio", stat: summaryStats.audio }, - { key: "local", color: "bg-emerald-500", label: "Local", stat: summaryStats.local }, + { + key: "audio", + color: "bg-rose-500", + label: providerText(t, "categoryAudio", "Audio"), + stat: summaryStats.audio, + }, + { + key: "local", + color: "bg-emerald-500", + label: providerText(t, "categoryLocal", "Local"), + stat: summaryStats.local, + }, { key: "cloudagent", color: "bg-violet-500", - label: "Cloud Agent", + label: providerText(t, "categoryCloudAgent", "Cloud Agent"), stat: summaryStats.cloudagent, }, ].filter((category) => category.key !== "no-auth" || category.stat.total > 0); @@ -178,8 +203,8 @@ export default function ProviderSummaryCard({ setModelSearchQuery(e.target.value)} - placeholder={t("searchByModel") || "Search by model…"} - aria-label={t("searchByModel") || "Search by model"} + placeholder={providerText(t, "searchByModel", "Search by model…")} + aria-label={providerText(t, "searchByModelAria", "Search by model")} icon="psychology" inputClassName={modelSearchQuery ? "pr-9" : ""} /> diff --git a/src/app/(dashboard)/dashboard/providers/error.tsx b/src/app/(dashboard)/dashboard/providers/error.tsx index 086ef9d5b6..0f24a06d49 100644 --- a/src/app/(dashboard)/dashboard/providers/error.tsx +++ b/src/app/(dashboard)/dashboard/providers/error.tsx @@ -1,5 +1,27 @@ "use client"; +import { useTranslations } from "next-intl"; + +type ProviderMessageTranslator = ((key: string, values?: Record) => string) & { + has?: (key: string) => boolean; +}; + +function providerText( + t: ProviderMessageTranslator, + key: string, + fallback: string, + values?: Record +): string { + if (typeof t.has === "function" && t.has(key)) return t(key, values); + if (values) { + return Object.entries(values).reduce( + (acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)), + fallback + ); + } + return fallback; +} + export default function ProvidersError({ error: _error, reset, @@ -7,6 +29,8 @@ export default function ProvidersError({ error: Error & { digest?: string }; reset: () => void; }) { + const t = useTranslations("providers"); + return (

    - Failed to load providers + {providerText(t, "pageLoadErrorTitle", "Failed to load providers")}

    - We could not load provider data right now. Check your connection and try again. + {providerText( + t, + "pageLoadErrorDescription", + "We could not load provider data right now. Check your connection and try again." + )}

    {_error?.digest && ( -

    Error ID: {_error.digest}

    +

    + {providerText(t, "pageLoadErrorId", "Error ID: {id}", { id: _error.digest })} +

    )} {process.env.NODE_ENV === "development" && _error?.message && (

    {_error.message}

    @@ -30,7 +60,7 @@ export default function ProvidersError({ onClick={reset} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors focus:outline-2 focus:outline-offset-2 focus:outline-primary" > - Try Again + {providerText(t, "pageLoadErrorRetry", "Try Again")}
    diff --git a/src/app/(dashboard)/dashboard/providers/hooks/useProviderModels.ts b/src/app/(dashboard)/dashboard/providers/hooks/useProviderModels.ts index c3997f813d..61f540b3e4 100644 --- a/src/app/(dashboard)/dashboard/providers/hooks/useProviderModels.ts +++ b/src/app/(dashboard)/dashboard/providers/hooks/useProviderModels.ts @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslations } from "next-intl"; export interface ProviderModel { id: string; @@ -31,6 +32,7 @@ interface UseProviderModelsResult { * `providerId` changes). */ export function useProviderModels(providerId: string): UseProviderModelsResult { + const t = useTranslations("providers"); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -50,7 +52,7 @@ export function useProviderModels(providerId: string): UseProviderModelsResult { const body = (await res.json().catch(() => null)) as { error?: { message?: string }; } | null; - const msg = body?.error?.message ?? `HTTP ${res.status}`; + const msg = body?.error?.message ?? `${t("providerTestFailed")} (HTTP ${res.status})`; if (!cancelled) setError(msg); return; } @@ -116,7 +118,7 @@ export function useProviderModels(providerId: string): UseProviderModelsResult { }; cleanupRef.current = cleanup; return cleanup; - }, [providerId]); + }, [providerId, t]); useEffect(() => { if (!providerId) { diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index a0984d1c9a..4402b83404 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -134,22 +134,28 @@ type ProviderBatchTestResults = { error?: string | { message?: string }; }; -function getConnectionErrorTag(connection) { +function getConnectionErrorTag(connection, t: ProviderMessageTranslator) { if (!connection) return null; const explicitType = connection.lastErrorType; - if (explicitType === "runtime_error") return "Runtime"; + if (explicitType === "runtime_error") return providerText(t, "errorTypeRuntime", "Runtime"); if ( explicitType === "upstream_auth_error" || explicitType === "auth_missing" || explicitType === "token_refresh_failed" || explicitType === "token_expired" ) { - return "Auth"; + return providerText(t, "errorTypeUpstreamAuth", "Auth"); + } + if (explicitType === "upstream_rate_limited") { + return providerText(t, "errorTypeRateLimited", "Rate limited"); + } + if (explicitType === "upstream_unavailable") { + return providerText(t, "errorTypeUpstreamUnavailable", "Server error"); + } + if (explicitType === "network_error") { + return providerText(t, "errorTypeNetworkError", "Network"); } - if (explicitType === "upstream_rate_limited") return "Rate limited"; - if (explicitType === "upstream_unavailable") return "Server error"; - if (explicitType === "network_error") return "Network"; const numericCode = Number(connection.errorCode); if (Number.isFinite(numericCode) && numericCode >= 400) { @@ -157,19 +163,21 @@ function getConnectionErrorTag(connection) { } const fromMessage = getErrorCode(connection.lastError); - if (fromMessage === "401" || fromMessage === "403") return "Auth"; + if (fromMessage === "401" || fromMessage === "403") { + return providerText(t, "errorTypeUpstreamAuth", "Auth"); + } if (fromMessage && fromMessage !== "ERR") return fromMessage; const msg = (connection.lastError || "").toLowerCase(); if (msg.includes("runtime") || msg.includes("not runnable") || msg.includes("not installed")) - return "Runtime"; + return providerText(t, "errorTypeRuntime", "Runtime"); if ( msg.includes("invalid api key") || msg.includes("token invalid") || msg.includes("revoked") || msg.includes("unauthorized") ) - return "Auth"; + return providerText(t, "errorTypeUpstreamAuth", "Auth"); return "ERR"; } @@ -355,7 +363,7 @@ export default function ProvidersPage() { (a: any, b: any) => (new Date(b.lastErrorAt || 0) as any) - (new Date(a.lastErrorAt || 0) as any) )[0]; - const errorCode = latestError ? getConnectionErrorTag(latestError) : null; + const errorCode = latestError ? getConnectionErrorTag(latestError, t) : null; const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null; // Check expirations diff --git a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx index fabcb7e616..6c508ca6b2 100644 --- a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx @@ -47,7 +47,6 @@ export function CliproxyProviderExposureCard() { mutate(); } catch { setMsg({ ok: false, text: t("networkError") }); - } finally { setPending(false); } } diff --git a/src/app/(dashboard)/dashboard/relay/page.tsx b/src/app/(dashboard)/dashboard/relay/page.tsx index f9cc786e3f..c58db08816 100644 --- a/src/app/(dashboard)/dashboard/relay/page.tsx +++ b/src/app/(dashboard)/dashboard/relay/page.tsx @@ -1,10 +1,14 @@ import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; import RelayProxyClient from "./RelayProxyClient"; -export const metadata: Metadata = { - title: "OmniRoute — Relay Proxies", - description: "Serverless relay proxy endpoints for your AI infrastructure", -}; +export async function generateMetadata(): Promise { + const t = await getTranslations("metadata"); + return { + title: t("relayTitle"), + description: t("relayDescription"), + }; +} export default function RelayProxyPage() { return ; diff --git a/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx b/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx index a77d252c44..f05e4a7c92 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx @@ -320,7 +320,7 @@ export default function AuthzSection() { onClick={() => handleRemovePrefix(prefix)} disabled={locked || submitting} > - Remove + {t("authz.remove")}
  • ); diff --git a/src/app/(dashboard)/dashboard/settings/components/AutoDisableCard.tsx b/src/app/(dashboard)/dashboard/settings/components/AutoDisableCard.tsx index 98bd920293..fb75847983 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AutoDisableCard.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AutoDisableCard.tsx @@ -38,7 +38,7 @@ export default function AutoDisableCard() { const savedData = await res.json(); setData(savedData); setEditMode(false); - notify.success(t("savedSuccessfully") || "Saved successfully"); + notify.success(t("savedSuccessfully")); } catch (err) { notify.error(err instanceof Error ? err.message : "Error saving"); } finally { diff --git a/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx b/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx index f23e91f65d..651ae22714 100644 --- a/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx @@ -93,18 +93,12 @@ export default function BackgroundDegradationTab() {
    -

    - {t("backgroundDegradationTitle") || "Background Task Degradation"} -

    -

    - {t("backgroundDegradationDesc") || - "Auto-redirect background requests (titles, summaries) to cheaper models"} -

    +

    {t("backgroundDegradationTitle")}

    +

    {t("backgroundDegradationDesc")}

    {status === "saved" && ( - check_circle{" "} - {t("saved") || "Saved"} + check_circle {t("saved")} )} @@ -112,9 +106,7 @@ export default function BackgroundDegradationTab() { {/* Toggle */}
    -

    - {t("enableDegradation") || "Enable Background Degradation"} -

    +

    {t("enableDegradation")}

    {t("enableDegradationHint") || "Automatically use cheaper models for background utility tasks"} @@ -124,7 +116,7 @@ export default function BackgroundDegradationTab() { checked={config.enabled} onChange={(enabled) => save({ enabled })} disabled={loading || saving} - ariaLabel={t("enableDegradation") || "Enable Background Degradation"} + ariaLabel={t("enableDegradation")} />

    @@ -133,9 +125,7 @@ export default function BackgroundDegradationTab() {
    analytics - - {t("tasksDetected") || "Tasks detected"}: - + {t("tasksDetected")}: {config.stats.detected} @@ -148,7 +138,7 @@ export default function BackgroundDegradationTab() { {/* Degradation Map */}

    - {t("degradationMap") || "Model Degradation Map"} + {t("degradationMap")}

    {/* Add new mapping */} @@ -157,23 +147,19 @@ export default function BackgroundDegradationTab() {
    - +
    @@ -206,15 +192,14 @@ export default function BackgroundDegradationTab() { chevron_right - {t("detectionPatterns") || "Detection Patterns"} ( - {config.detectionPatterns?.length || 0}) + {t("detectionPatterns")} ({config.detectionPatterns?.length || 0}) {/* Add new pattern */}
    setNewPattern(e.target.value)} className="flex-1 px-3 py-2 rounded-lg text-sm bg-surface border border-border/50 focus:border-sky-500/50 focus:outline-none" @@ -224,7 +209,7 @@ export default function BackgroundDegradationTab() { disabled={saving || !newPattern.trim()} className="px-3 py-2 rounded-lg text-sm font-medium bg-sky-500/10 text-sky-500 hover:bg-sky-500/20 disabled:opacity-50 transition-all" > - {t("add") || "Add"} + {t("add")}
    diff --git a/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx index b1c984e61c..513147da39 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx @@ -49,17 +49,21 @@ export default function CliproxyapiSettingsTab() { const data = await res.json(); if (res.ok) { setImportResult( - `Imported ${data.imported ?? 0} account(s) (scanned ${data.scanned ?? 0}, skipped ${data.skipped ?? 0}).` + t("cliproxyapiImportResult", { + imported: data.imported ?? 0, + scanned: data.scanned ?? 0, + skipped: data.skipped ?? 0, + }) ); } else { - setImportResult(data.error || "Import failed."); + setImportResult(data.error || t("cliproxyapiImportFailed")); } } catch { - setImportResult("Import failed."); + setImportResult(t("cliproxyapiImportFailed")); } finally { setImporting(false); } - }, []); + }, [t]); useEffect(() => { fetch("/api/settings") @@ -95,34 +99,37 @@ export default function CliproxyapiSettingsTab() { }); }, []); - const updateSetting = useCallback(async (key: string, value: boolean | string) => { - if (key === "cliproxyapi_url" && typeof value === "string" && value.trim() !== "") { - if (!isValidUrl(value)) { - setMessage({ type: "error", text: "Invalid URL format. Use http:// or https://" }); - return; + const updateSetting = useCallback( + async (key: string, value: boolean | string) => { + if (key === "cliproxyapi_url" && typeof value === "string" && value.trim() !== "") { + if (!isValidUrl(value)) { + setMessage({ type: "error", text: t("cliproxyapiInvalidUrl") }); + return; + } } - } - setSaving(true); - setMessage(null); - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ [key]: value }), - }); - if (!res.ok) { - throw new Error(`Server returned ${res.status}`); + setSaving(true); + setMessage(null); + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ [key]: value }), + }); + if (!res.ok) { + throw new Error(`Server returned ${res.status}`); + } + await res.json(); + setSettings((prev) => ({ ...prev, [key]: value })); + setMessage({ type: "success", text: t("settingSaved") }); + } catch { + setMessage({ type: "error", text: t("settingSaveFailed") }); + } finally { + setSaving(false); } - await res.json(); - setSettings((prev) => ({ ...prev, [key]: value })); - setMessage({ type: "success", text: "Setting saved" }); - } catch { - setMessage({ type: "error", text: "Failed to save setting" }); - } finally { - setSaving(false); - } - }, []); + }, + [t] + ); const cpaEnabled = settings.cliproxyapi_fallback_enabled === true; const cpaUrl = settings.cliproxyapi_url || "http://127.0.0.1:8317"; @@ -148,14 +155,14 @@ export default function CliproxyapiSettingsTab() {
    info - CLIProxyAPI lifecycle management (install, start, stop) has moved to{" "} + {t("cliproxyapiLifecycleNoticeBefore")}{" "} - Providers → Services + {t("cliproxyapiLifecycleNoticeLink")} - . Fallback routing settings below remain here. + {t("cliproxyapiLifecycleNoticeAfter")}
    @@ -181,9 +188,7 @@ export default function CliproxyapiSettingsTab() {

    {t("cliproxyapiFallback")}

    -

    - When enabled, failed requests are retried through CLIProxyAPI (localhost:8317) -

    +

    {t("cliproxyapiFallbackDescription")}

    @@ -212,7 +217,7 @@ export default function CliproxyapiSettingsTab() {

    {t("cliproxyapiVersion")}

    - {toolState.installedVersion ? `v${toolState.installedVersion}` : "Not installed"} + {toolState.installedVersion ? `v${toolState.installedVersion}` : t("notInstalled")}

    @@ -268,10 +273,10 @@ export default function CliproxyapiSettingsTab() { }`} > {toolState.healthStatus === "healthy" - ? "Healthy" + ? t("healthy") : toolState.healthStatus === "unhealthy" - ? "Unhealthy" - : "Unknown"} + ? t("unhealthy") + : t("unknown")}

    diff --git a/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx index 048d9a47e8..4ae49ee40c 100644 --- a/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx @@ -784,9 +784,7 @@ export default function MemorySkillsTab() {

    {t("memorySkillsSkillsmpMarketplace")}

    -

    - Connect to SkillsMP to discover and install skills from the marketplace. -

    +

    {t("memorySkillsSkillsmpDescription")}

    {skillsmpStatus === "saved" && ( @@ -816,12 +814,12 @@ export default function MemorySkillsTab() { disabled={skillsmpSaving} className="px-4 py-2 text-sm font-medium rounded-lg bg-violet-500 text-white hover:bg-violet-600 disabled:opacity-50 transition-colors" > - {skillsmpSaving ? "Saving..." : "Save"} + {skillsmpSaving ? t("saving") : t("save")}

    - Get your API key from skillsmp.com. Rate limit: - 500 requests/day. + {t("skillsmpApiKeyHintBefore")} skillsmp.com + {t("skillsmpApiKeyHintAfter", { limit: 500 })}

    @@ -836,9 +834,7 @@ export default function MemorySkillsTab() {

    {t("memorySkillsActiveSkillsProvider")}

    -

    - Choose which provider the Skills page uses for search and install. -

    +

    {t("memorySkillsActiveProviderDescription")}

    {skillsProviderStatus === "saved" && ( @@ -867,10 +863,10 @@ export default function MemorySkillsTab() {

    - SkillsMP Marketplace + {t("memorySkillsSkillsmpProviderTitle")}

    - Authenticated marketplace (uses your SkillsMP API key). + {t("memorySkillsSkillsmpProviderDescription")}

    @@ -887,10 +883,10 @@ export default function MemorySkillsTab() {

    - skills.sh Directory + {t("memorySkillsSkillsshProviderTitle")}

    - Public directory provider (no API key required). + {t("memorySkillsSkillsshProviderDescription")}

    diff --git a/src/app/(dashboard)/dashboard/settings/components/ModelAliasesTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ModelAliasesTab.tsx index 126704f1f2..68177a313a 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ModelAliasesTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ModelAliasesTab.tsx @@ -82,26 +82,23 @@ export default function ModelAliasesTab() {
    -

    {t("modelAliasesTitle") || "Model Aliases"}

    -

    - {t("modelAliasesDesc") || "Auto-forward deprecated model IDs to their replacements"} -

    +

    {t("modelAliasesTitle")}

    +

    {t("modelAliasesDesc")}

    {status === "saved" && ( - check_circle{" "} - {t("saved") || "Saved"} + check_circle {t("saved")} )} {/* Add custom alias */}
    -

    {t("addCustomAlias") || "Add Custom Alias"}

    +

    {t("addCustomAlias")}

    setNewFrom(e.target.value)} className="flex-1 px-3 py-2 rounded-lg text-sm bg-surface border border-border/50 focus:border-amber-500/50 focus:outline-none" @@ -109,7 +106,7 @@ export default function ModelAliasesTab() { setNewTo(e.target.value)} className="flex-1 px-3 py-2 rounded-lg text-sm bg-surface border border-border/50 focus:border-amber-500/50 focus:outline-none" @@ -119,7 +116,7 @@ export default function ModelAliasesTab() { disabled={saving || !newFrom.trim() || !newTo.trim()} className="px-4 py-2 rounded-lg text-sm font-medium bg-amber-500/10 text-amber-500 hover:bg-amber-500/20 disabled:opacity-50 transition-all" > - {t("add") || "Add"} + {t("add")}
    @@ -128,7 +125,7 @@ export default function ModelAliasesTab() { {customEntries.length > 0 && (

    - {t("customAliases") || "Custom Aliases"} + {t("customAliases")}

    {customEntries.map(([from, to]) => ( @@ -157,7 +154,7 @@ export default function ModelAliasesTab() { chevron_right - {t("builtInAliases") || "Built-in Aliases"} ({builtInEntries.length}) + {t("builtInAliases")} ({builtInEntries.length})
    {builtInEntries.map(([from, to]) => ( diff --git a/src/app/(dashboard)/dashboard/settings/components/ModelsDevSyncTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ModelsDevSyncTab.tsx index 117e6d168d..c5a60fbad1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ModelsDevSyncTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ModelsDevSyncTab.tsx @@ -85,7 +85,7 @@ export default function ModelsDevSyncTab() { }); fetchStatus(); } else { - setFeedback({ type: "error", message: result.error || "Sync failed" }); + setFeedback({ type: "error", message: result.error || t("syncFailed") }); } } else { setFeedback({ type: "error", message: "Sync request failed" }); @@ -110,7 +110,7 @@ export default function ModelsDevSyncTab() { }); if (!res.ok) { setEnabled(!newVal); - setFeedback({ type: "error", message: t("enableSyncError") || "Failed to update" }); + setFeedback({ type: "error", message: t("enableSyncError") }); } else { setFeedback({ type: "success", message: "Settings saved" }); } @@ -136,7 +136,7 @@ export default function ModelsDevSyncTab() { if (!res.ok) { setIntervalHours(oldInterval); setDraftIntervalHours(oldInterval); - setFeedback({ type: "error", message: t("enableSyncError") || "Failed to update" }); + setFeedback({ type: "error", message: t("enableSyncError") }); } else { setFeedback({ type: "success", message: "Interval updated" }); } diff --git a/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx b/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx index 35ec7f7319..52465ba0ae 100644 --- a/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/OneproxyTab.tsx @@ -44,6 +44,7 @@ export default function OneproxyTab() { const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [syncResult, setSyncResult] = useState(null); + const [syncSucceeded, setSyncSucceeded] = useState(false); const [filterProtocol, setFilterProtocol] = useState(""); const [filterCountry, setFilterCountry] = useState(""); const [minQuality, setMinQuality] = useState(""); @@ -83,24 +84,32 @@ export default function OneproxyTab() { const handleSync = async () => { setSyncing(true); setSyncResult(null); + setSyncSucceeded(false); try { const res = await fetch("/api/settings/oneproxy", { method: "POST" }); const data = await res.json(); if (data.success) { - setSyncResult(`Synced ${data.total} proxies (${data.added} new, ${data.updated} updated)`); + setSyncSucceeded(true); + setSyncResult( + t("oneproxySyncSuccess", { + total: data.total, + added: data.added, + updated: data.updated, + }) + ); } else { - setSyncResult(`Sync failed: ${data.error}`); + setSyncResult(t("oneproxySyncFailed", { error: data.error })); } await loadData(); } catch (err) { - setSyncResult(`Sync failed: ${err}`); + setSyncResult(t("oneproxySyncFailed", { error: String(err) })); } finally { setSyncing(false); } }; const handleClearAll = async () => { - if (!confirm("Clear all 1proxy proxies?")) return; + if (!confirm(t("oneproxyClearAllConfirm"))) return; try { await fetch("/api/settings/oneproxy?clearAll=1", { method: "DELETE" }); await loadData(); @@ -141,17 +150,15 @@ export default function OneproxyTab() {

    {t("oneproxyTitle")}

    -

    - Fetch and rotate free validated proxies from the 1proxy community platform -

    +

    {t("oneproxyDescription")}

    {proxies.length > 0 && ( )}
    @@ -160,7 +167,7 @@ export default function OneproxyTab() { {syncResult && (
    {t("oneproxyLoadingProxies")}
    ) : proxies.length === 0 ? ( -
    - No 1proxy proxies found. Click "Sync Now" to fetch free proxies. -
    +
    {t("oneproxyEmpty")}
    ) : (
    @@ -256,7 +261,9 @@ export default function OneproxyTab() { - + @@ -303,7 +310,7 @@ export default function OneproxyTab() { onClick={() => handleDelete(proxy.id)} className="text-red-500 hover:text-red-700 text-xs" > - Delete + {t("oneproxyDelete")} diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyStatusBadge.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyStatusBadge.tsx index 4f1287bf4f..58a39d8519 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyStatusBadge.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyStatusBadge.tsx @@ -1,10 +1,13 @@ "use client"; +import { useTranslations } from "next-intl"; + interface ProxyStatusBadgeProps { status?: string; } export function ProxyStatusBadge({ status }: ProxyStatusBadgeProps) { + const t = useTranslations("settings"); const isInactive = status === "inactive"; return ( - {isInactive ? "Inactive" : "Active"} + {isInactive ? t("proxyStatusInactive") : t("proxyStatusActive")} ); } diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx index f96d479654..f2c8c7a114 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx @@ -702,7 +702,7 @@ function ComboCooldownWaitCard({ setDraft(value); }, [value]); - const title = t("resilienceComboCooldownWaitTitle") || "Combo cooldown wait"; + const title = t("resilienceComboCooldownWaitTitle"); const desc = t("resilienceComboCooldownWaitDesc") || "For all combo strategies: wait out a short transient cooldown and re-dispatch instead of returning a 429 immediately. Never waits on quota_exhausted."; @@ -735,7 +735,7 @@ function ComboCooldownWaitCard({ {editing ? ( <> setDraft((prev) => ({ ...prev, enabled }))} /> setDraft((prev) => ({ ...prev, maxWaitMs }))} /> setDraft((prev) => ({ ...prev, maxAttempts }))} />
    -
    - {t("resilienceEnableServerWait") || "Enabled"} -
    +
    {t("resilienceEnableServerWait")}
    {value.enabled ? t("statusEnabled") : t("statusDisabled")}
    -
    - {t("resilienceComboCooldownMaxWaitMs") || "Max wait per attempt"} -
    +
    {t("resilienceComboCooldownMaxWaitMs")}
    {formatMs(value.maxWaitMs)}
    -
    - {t("resilienceMaxAttempts") || "Max attempts"} -
    +
    {t("resilienceMaxAttempts")}
    {value.maxAttempts}
    -
    - {t("resilienceComboCooldownBudgetMs") || "Total wait budget"} -
    +
    {t("resilienceComboCooldownBudgetMs")}
    {formatMs(value.budgetMs)}
    @@ -820,8 +812,7 @@ function QuotaShareConcurrencyLimitCard({ setDraft(value); }, [value]); - const title = - t("resilienceQuotaShareConcurrencyTitle") || "Quota-share per-connection concurrency"; + const title = t("resilienceQuotaShareConcurrencyTitle"); const desc = t("resilienceQuotaShareConcurrencyDesc") || "For quota-share combos only: when a connection sets a Max Concurrent cap, serialize concurrent requests to that subscription account so it is never flooded past its ceiling — excess requests wait in the queue instead of getting a 429. The cap comes from each connection's Max Concurrent field; this switch only enables/disables honoring it."; @@ -853,7 +844,7 @@ function QuotaShareConcurrencyLimitCard({
    {editing ? ( ) : (
    -
    - {t("resilienceEnableServerWait") || "Enabled"} -
    +
    {t("resilienceEnableServerWait")}
    {value.enabled ? t("statusEnabled") : t("statusDisabled")}
    diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index fe7f3eaaa6..22462ce866 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -180,17 +180,17 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = { const PROVIDER_TILE_DISPLAY: Record< string, - { name: string; description: string; icon: string; tone: string } + { nameKey: string; descriptionKey: string; icon: string; tone: string } > = { [PROVIDER_CLAUDE]: { - name: "Claude (OAuth)", - description: "Native Claude provider with OAuth-issued tokens.", + nameKey: "routingClaudeProviderName", + descriptionKey: "routingClaudeProviderDescription", icon: "anthropic", tone: "indigo", }, [PROVIDER_CC_BRIDGE]: { - name: "Claude-Code Bridge", - description: "Relay endpoints using API keys (anthropic-compatible-cc-*).", + nameKey: "routingCcBridgeName", + descriptionKey: "routingCcBridgeDescription", icon: "hub", tone: "purple", }, @@ -335,7 +335,7 @@ function StringListEditor({ onClick={() => onChange([...items, ""])} className="self-start" > - {tCommon("add") || "Add entry"} + {t("routingAddEntry")}
    ); @@ -567,7 +567,11 @@ function OpEditor({
    ); default: - return

    Unknown op kind: {op?.kind}

    ; + return ( +

    + {t("routingUnknownOpKind", { kind: String(op?.kind ?? "") })} +

    + ); } } @@ -625,16 +629,16 @@ function summarizeTransformOp(op: any, t: any): string { // Client-side validator — light shape check before we PATCH; the server // re-validates with the full zod schema in settingsSchemas.ts. -function validateProviderTransformsConfig(value: unknown): string | null { - if (!value || typeof value !== "object") return "Config must be a JSON object"; +function validateProviderTransformsConfig(value: unknown, t: any): string | null { + if (!value || typeof value !== "object") return t("routingConfigMustBeObject"); const cfg = value as { enabled?: unknown; pipeline?: unknown }; - if (typeof cfg.enabled !== "boolean") return "`enabled` must be true or false"; - if (!Array.isArray(cfg.pipeline)) return "`pipeline` must be an array of ops"; - if (cfg.pipeline.length > 50) return "Pipeline cannot exceed 50 ops"; + if (typeof cfg.enabled !== "boolean") return t("routingEnabledMustBeBoolean"); + if (!Array.isArray(cfg.pipeline)) return t("routingPipelineMustBeArray"); + if (cfg.pipeline.length > 50) return t("routingPipelineTooLong"); for (let i = 0; i < cfg.pipeline.length; i++) { const op = cfg.pipeline[i] as { kind?: unknown }; if (!op || typeof op !== "object" || typeof op.kind !== "string") { - return `Op #${i + 1}: missing or invalid \`kind\``; + return t("routingOpMissingKind", { index: i + 1 }); } const validKinds = [ "drop_paragraph_if_contains", @@ -648,7 +652,7 @@ function validateProviderTransformsConfig(value: unknown): string | null { "obfuscate_words", ]; if (!validKinds.includes(op.kind)) { - return `Op #${i + 1}: unknown kind "${op.kind}"`; + return t("routingOpUnknownKind", { index: i + 1, kind: op.kind }); } } return null; @@ -813,11 +817,11 @@ export default function RoutingTab() { } catch (err) { setJsonErrors((prev) => ({ ...prev, - [providerId]: `Invalid JSON: ${(err as Error).message}`, + [providerId]: t("routingInvalidJson", { error: (err as Error).message }), })); return; } - const validationError = validateProviderTransformsConfig(parsed); + const validationError = validateProviderTransformsConfig(parsed, t); if (validationError) { setJsonErrors((prev) => ({ ...prev, [providerId]: validationError })); return; @@ -1017,7 +1021,7 @@ export default function RoutingTab() { {availableProvidersToAdd.map((p) => ( ))} @@ -1039,12 +1043,11 @@ export default function RoutingTab() {
    {Object.entries(systemTransforms.providers).map(([providerId, providerCfg]) => { const isBuiltin = BUILTIN_PROVIDERS.has(providerId); - const display = PROVIDER_TILE_DISPLAY[providerId] ?? { - name: providerId, - description: "Custom provider.", - icon: "extension", - tone: "purple", - }; + const display = PROVIDER_TILE_DISPLAY[providerId]; + const displayName = display ? t(display.nameKey) : providerId; + const displayDescription = display + ? t(display.descriptionKey) + : t("routingCustomProviderDescription"); const draft = jsonDrafts[providerId] ?? JSON.stringify(providerCfg, null, 2); const errorMsg = jsonErrors[providerId] ?? null; const opCount = Array.isArray(providerCfg.pipeline) ? providerCfg.pipeline.length : 0; @@ -1066,7 +1069,7 @@ export default function RoutingTab() { {providerId} - {display.name} + {displayName}
    } subtitle={ @@ -1081,7 +1084,7 @@ export default function RoutingTab() { onChange={(checked) => toggleProviderEnabled(providerId, checked)} disabled={loading} ariaLabel={ - tCommon("enable") + " " + display.name + " " + t("systemTransforms") + tCommon("enable") + " " + displayName + " " + t("systemTransforms") } /> {!isBuiltin && ( @@ -1098,7 +1101,7 @@ export default function RoutingTab() { } > -

    {display.description}

    +

    {displayDescription}

    {providerSaveErrors[providerId] && (
    {isJsonOpen - ? "▾ " + tCommon("hide") + " JSON editor" - : "▸ Import / export JSON"} + ? `▾ ${t("routingJsonEditorHide")}` + : `▸ ${t("routingJsonEditorImportExport")}`} {isJsonOpen && (
    {t("oneproxyAnonymity")} Google + {t("oneproxyGoogle")} + {t("oneproxyActions")}