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}
+
+ ),
+ })}
- 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") )}
- Or download the respective installer format directly: -
+{t("directDownloadHint")}
- 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 (
- We could not load combo data right now. Check your connection and try again. -
+{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")}- 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() {
) : (
- 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." + )}
{t("apiKeySecure")}
- {t("compatBlockedParamsHint") ?? "Blocked params (stripped from requests)"} + {providerText( + t, + "compatBlockedParamsHint", + "Blocked params (stripped from requests)" + )} {paramSaving && ` ● ${t("compatSaving")}`}
- {t("compatAllowedParamsHint") ?? "Allowed params (re-added after deny)"} + {providerText( + t, + "compatAllowedParamsHint", + "Allowed params (re-added after deny)" + )}
- 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")}- {t("backgroundDegradationDesc") || - "Auto-redirect background requests (titles, summaries) to cheaper models"} -
+{t("backgroundDegradationDesc")}
- {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")} />
- {t("degradationMap") || "Model Degradation Map"} + {t("degradationMap")}
{/* Add new mapping */} @@ -157,23 +147,19 @@ export default function BackgroundDegradationTab() {- When enabled, failed requests are retried through CLIProxyAPI (localhost:8317) -
+{t("cliproxyapiFallbackDescription")}
{t("cliproxyapiVersion")}
- {toolState.installedVersion ? `v${toolState.installedVersion}` : "Not installed"} + {toolState.installedVersion ? `v${toolState.installedVersion}` : t("notInstalled")}
- Connect to SkillsMP to discover and install skills from the marketplace. -
+{t("memorySkillsSkillsmpDescription")}
- 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() {- Choose which provider the Skills page uses for search and install. -
+{t("memorySkillsActiveProviderDescription")}
- 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("modelAliasesDesc") || "Auto-forward deprecated model IDs to their replacements"} -
+{t("modelAliasesDesc")}
{t("addCustomAlias") || "Add Custom Alias"}
+{t("addCustomAlias")}
- {t("customAliases") || "Custom Aliases"} + {t("customAliases")}
- Fetch and rotate free validated proxies from the 1proxy community platform -
+{t("oneproxyDescription")}
| {t("oneproxyAnonymity")} | -+ {t("oneproxyGoogle")} + | {t("oneproxyActions")} | @@ -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 ? ( <>{proxy.source} | @@ -64,16 +67,16 @@ export default function FreeProxyRow({
{proxy.inPool ? (
- in pool
+ {t("proxyFreePoolInPool")}
) : (
|
diff --git a/src/app/(dashboard)/dashboard/settings/error.tsx b/src/app/(dashboard)/dashboard/settings/error.tsx
index 4449ef5cb3..4c0adcd7f6 100644
--- a/src/app/(dashboard)/dashboard/settings/error.tsx
+++ b/src/app/(dashboard)/dashboard/settings/error.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useTranslations } from "next-intl";
+
export default function SettingsError({
error: _error,
reset,
@@ -7,6 +9,8 @@ export default function SettingsError({
error: Error & { digest?: string };
reset: () => void;
}) {
+ const t = useTranslations("settings");
+
return (
|---|