From e2eba05e5cf4ea0b796d67d5efed9530df6182a9 Mon Sep 17 00:00:00 2001 From: Alex Jordan <60003097+alex-jordan547@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:23:12 +0200 Subject: [PATCH] fix(i18n): localize hardcoded web UI copy --- .../(dashboard)/dashboard/BootstrapBanner.tsx | 23 +- .../(dashboard)/dashboard/HomePageClient.tsx | 136 ++- .../analytics/AutoRoutingAnalyticsTab.tsx | 14 +- .../analytics/SearchAnalyticsTab.tsx | 37 +- .../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 | 32 +- .../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 | 14 +- .../[id]/hooks/useCommandCodeAuth.ts | 75 +- .../[id]/hooks/useConnectionDeleteConfirm.ts | 11 +- .../[id]/hooks/useModelVisibilityHandlers.ts | 6 +- .../[id]/hooks/useProviderConnections.ts | 91 +- .../providers/[id]/hooks/useProviderModels.ts | 21 +- .../[id]/hooks/useProviderSettings.ts | 42 +- .../components/AddCompatibleProviderModal.tsx | 14 +- .../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 | 52 +- .../CliproxyProviderExposureCard.tsx | 68 +- 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 | 97 +- .../settings/components/MemorySkillsTab.tsx | 31 +- .../settings/components/ModelAliasesTab.tsx | 21 +- .../settings/components/ModelsDevSyncTab.tsx | 6 +- .../settings/components/OneproxyTab.tsx | 67 +- .../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 +- .../components/proxy/SubscriptionTab.tsx | 320 ++--- .../(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/forgot-password/page.tsx | 24 +- 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 | 994 +++++++++++++++- src/i18n/messages/fr.json | 994 +++++++++++++++- src/i18n/messages/pt-BR.json | 1036 ++++++++++++++++- src/i18n/messages/vi.json | 994 +++++++++++++++- src/shared/components/CommandPalette.tsx | 16 +- src/shared/components/DegradationBadge.tsx | 2 +- .../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 | 78 +- .../ComboCompressionModeSelect.tsx | 26 +- src/shared/components/docs/APIReference.tsx | 14 +- .../components/docs/DocsBreadcrumbs.tsx | 6 +- .../components/oauthModal/GheConfigStep.tsx | 16 +- .../oauthModal/GitlabDuoSetupStep.tsx | 30 +- 115 files changed, 6305 insertions(+), 1773 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/chaos/chaosI18n.ts 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" && ( )} @@ -917,30 +917,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") )}

@@ -954,7 +956,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { onClick={() => globalThis.window.electronAPI?.downloadUpdate()} className="font-semibold" > - Download Update + {t("downloadUpdate")} )} {electronUpdateStatus.status === "downloading" && ( @@ -973,7 +975,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { onClick={() => globalThis.window.electronAPI?.installUpdate()} className="font-semibold animate-pulse" > - Restart & Install + {t("restartAndInstall")} )} {(electronUpdateStatus.status === "error" || @@ -989,7 +991,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { }} className="font-semibold" > - Check for Update + {t("checkForUpdate")} )} @@ -1001,9 +1003,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")} )} @@ -1015,9 +1015,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 0b389e974d..09837dd5c9 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx @@ -15,11 +15,7 @@ import { getCodexEffectiveServiceTier, type CodexGlobalServiceMode, } from "@/lib/providers/codexFastTier"; -import { - normalizeCodexLimitPolicy, - providerText, - ERROR_TYPE_LABELS, -} from "../providerPageHelpers"; +import { normalizeCodexLimitPolicy, providerText, ERROR_TYPE_LABELS } from "../providerPageHelpers"; import { getCodexPlanLabel } from "../codexPlanLabel"; import ProviderQuotaVisibilityToggle from "./ProviderQuotaVisibilityToggle"; @@ -245,7 +241,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", @@ -300,7 +296,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", @@ -310,7 +306,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", @@ -391,22 +387,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 8580acccc8..74f9550c2a 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx @@ -411,7 +411,7 @@ export default function ModelRow({ : testStatus === "ok" ? "OK" : 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 bccea2c617..514575ea96 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", @@ -123,7 +124,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 edbc55f93d..e1cb0882ca 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow.tsx @@ -190,7 +190,7 @@ export default function PassthroughModelRow({ : testStatus === "ok" ? "OK" : 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")}

    (null); + const [validationResult, setValidationResult] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); useEffect(() => { @@ -130,7 +133,10 @@ export default function EditCompatibleNodeModal({ 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/[id]/hooks/useCommandCodeAuth.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useCommandCodeAuth.ts index ea1a6809d4..a6baeec4db 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useCommandCodeAuth.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useCommandCodeAuth.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { type CommandCodeAuthFlowState } from "../providerPageHelpers"; +import { useTranslations } from "next-intl"; +import { providerText, type CommandCodeAuthFlowState } from "../providerPageHelpers"; export type UseCommandCodeAuthParams = { providerId: string; @@ -15,6 +16,7 @@ export function useCommandCodeAuth({ setShowAddApiKeyModal, notify, }: UseCommandCodeAuthParams) { + const t = useTranslations("providers"); const [commandCodeAuthState, setCommandCodeAuthState] = useState({ 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 66ccee8c2d..5227089d1c 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 47bec6b1fd..d325e71341 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts @@ -27,7 +27,7 @@ import { useNotificationStore } from "@/store/notificationStore"; import { isClaudeCodeCompatibleProvider } from "@/shared/constants/providers"; import type { ConnectionRowConnection } from "../components/ConnectionRow"; import { connectionBelongsToProviderPage } from "../../providerPageUtils"; -import { normalizeCodexLimitPolicy } from "../providerPageHelpers"; +import { normalizeCodexLimitPolicy, providerText } from "../providerPageHelpers"; import { useProviderQuotaVisibility } from "./useProviderQuotaVisibility"; import { useReorderByAvailability } from "./useReorderByAvailability"; import { @@ -378,7 +378,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; } @@ -408,12 +415,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" + ) + ); } }; @@ -447,7 +468,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; } @@ -464,10 +488,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") + ); } }; @@ -481,18 +507,27 @@ export function useProviderConnections( if (!res.ok) { const data = await res.json().catch(() => ({})); - notify.error(data.error || "Failed to update CLIProxyAPI routing"); + notify.error( + data.error || + providerText(t, "failedUpdateCliproxyRouting", "Failed to update CLIProxyAPI routing") + ); return; } setCpaProviderEnabled(enabled); notify.success( enabled - ? "Requests now route through CLIProxyAPI (deeper emulation)" - : "Requests now use native OmniRoute (direct)" + ? providerText( + t, + "cliproxyRoutingEnabled", + "Requests now route through CLIProxyAPI (deeper emulation)" + ) + : providerText(t, "cliproxyRoutingDisabled", "Requests now use native OmniRoute (direct)") ); } catch { - notify.error("Failed to update CLIProxyAPI routing"); + notify.error( + providerText(t, "failedUpdateCliproxyRouting", "Failed to update CLIProxyAPI routing") + ); } }; @@ -664,10 +699,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); } @@ -689,7 +724,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; @@ -710,7 +749,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); } @@ -805,7 +847,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; } @@ -856,11 +904,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 ee107c63b6..0de6f273b7 100644 --- a/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx @@ -8,6 +8,7 @@ import { CLIENT_IDENTITY_PROFILE_OPTIONS, getClientIdentityProfileHeaders, } from "@/shared/constants/clientIdentityProfiles"; +import { providerText } from "../[id]/providerPageHelpers"; type CompatibleMode = "openai" | "anthropic" | "cc"; type CompatibleProviderNode = { id: string } & Record; @@ -100,9 +101,11 @@ export default function AddCompatibleProviderModal({ const [checkKey, setCheckKey] = useState(""); const [checkModelId, setCheckModelId] = useState(""); const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState< - null | { valid: boolean; error?: string | null; method?: string | null } - >(null); + const [validationResult, setValidationResult] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const apiTypeOptions = useMemo( @@ -242,7 +245,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 a1efc70ec6..75a1b0d9ca 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx @@ -30,17 +30,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 @@ -166,6 +166,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); @@ -426,15 +430,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 b2b2ec62d4..9dbcadb104 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 } from "react"; +import { useTranslations } from "next-intl"; export interface ProviderModel { id: string; @@ -29,6 +30,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); @@ -49,7 +51,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; } @@ -113,7 +115,7 @@ export function useProviderModels(providerId: string): UseProviderModelsResult { return () => { cancelled = true; }; - }, [providerId]); + }, [providerId, t]); return { models, loading, error }; } diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index cac4648d3b..176a7f5a6e 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"; } @@ -352,7 +360,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 @@ -822,11 +830,14 @@ export default function ProvidersPage() { dns

    - {t("addFirstProvider") || "Add your first provider"} + {providerText(t, "addFirstProvider", "Add your first provider")}

    - {t("addFirstProviderDesc") || - "Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts."} + {providerText( + t, + "addFirstProviderDesc", + "Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts." + )}

    @@ -1094,10 +1105,10 @@ export default function ProvidersPage() {

    - {t("ideProviders") || "IDE Providers"}{" "} + {providerText(t, "ideProviders", "IDE Providers")}{" "}

    @@ -1121,12 +1132,15 @@ export default function ProvidersPage() {

    - {t("ideProvidersDesc") || - "Editors with built-in AI subscription. Use the provider page to import credentials directly from the IDE's keychain."} + {providerText( + t, + "ideProvidersDesc", + "Editors with built-in AI subscription. Use the provider page to import credentials directly from the IDE's keychain." + )}

    {ideProviderEntries.length === 0 ? (
    - {t("noIdeProviders") || "No IDE providers match the current filters."} + {providerText(t, "noIdeProviders", "No IDE providers match the current filters.")}
    ) : (
    diff --git a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx index d3e406747a..21654cea45 100644 --- a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx @@ -6,12 +6,34 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { Card, Toggle } from "@/shared/components"; import { useServiceStatus } from "../hooks/useServiceStatus"; +type ServiceMessageTranslator = ((key: string, values?: Record) => string) & { + has?: (key: string) => boolean; +}; + +function serviceText( + t: ServiceMessageTranslator, + 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; +} + const NAME = "cliproxy"; export function CliproxyProviderExposureCard() { + const t = useTranslations("embeddedServices"); const { data, mutate } = useServiceStatus(NAME); const [pending, setPending] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); @@ -28,14 +50,30 @@ export function CliproxyProviderExposureCard() { if (!res.ok) { const body = await res.json().catch(() => ({})); const errorMsg = - body?.error?.message ?? body?.message ?? `Failed to update (HTTP ${res.status})`; + body?.error?.message ?? + body?.message ?? + serviceText( + t, + "cliproxyProviderExposureUpdateFailed", + "Failed to update (HTTP {status})", + { + status: res.status, + } + ); setMsg({ ok: false, text: errorMsg }); return; } setMsg(null); mutate(); } catch { - setMsg({ ok: false, text: "Network error — could not update provider exposure setting" }); + setMsg({ + ok: false, + text: serviceText( + t, + "cliproxyProviderExposureNetworkFailed", + "Network error — could not update provider exposure setting" + ), + }); } finally { setPending(false); } @@ -48,10 +86,15 @@ export function CliproxyProviderExposureCard() { hub
    -

    Provider Exposure

    +

    + {serviceText(t, "cliproxyProviderExposureTitle", "Provider Exposure")} +

    - Expose CLIProxyAPI models as a routing target under the{" "} - cliproxyapi/ prefix. + {serviceText( + t, + "cliproxyProviderExposureDescription", + "Expose CLIProxyAPI models as a routing target under the cliproxyapi/ prefix." + )}

    @@ -74,16 +117,23 @@ export function CliproxyProviderExposureCard() {

    - Expose as{" "} + {serviceText(t, "cliproxyProviderExposureLabel", "Expose as")}{" "} cliproxyapi/...

    - When enabled, discovered models appear in provider selects across OmniRoute. + {serviceText( + t, + "cliproxyProviderExposureHint", + "When enabled, discovered models appear in provider selects across OmniRoute." + )}

    - +
    ); } - 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 6c431afaf1..6fc8ed43b8 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() {
    progress_activity - Loading... + {t("loading")}
    ) : toolStateError ? (

    {toolStateError}

    ) : toolState ? (
    -

    Status

    +

    {t("cliproxyapiStatusLabel")}

    {statusIcon}

    - {toolState.status?.replace("_", " ") || "Unknown"} + {toolState.status?.replace("_", " ") || t("unknown")}

    -

    Version

    +

    {t("cliproxyapiVersionLabel")}

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

    -

    Health

    +

    {t("cliproxyapiHealthLabel")}

    {toolState.healthStatus === "healthy" - ? "Healthy" + ? t("healthy") : toolState.healthStatus === "unhealthy" - ? "Unhealthy" - : "Unknown"} + ? t("unhealthy") + : t("unknown")}

    -

    Port

    +

    {t("cliproxyapiPortLabel")}

    {toolState.port || 8317}

    diff --git a/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx index 1cb2319159..8f6f2882de 100644 --- a/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx @@ -369,7 +369,10 @@ export default function MemorySkillsTab() { role="note" data-testid="memory-token-cost-warning" > -