From b2203588782abb0f4a167796d527d644fa353ac8 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 22:52:45 +0800 Subject: [PATCH 1/3] Add Web internationalization foundation --- apps/web/package.json | 2 + apps/web/src/App.tsx | 107 +++++---- apps/web/src/components/ConnectionModal.tsx | 152 +++++------- apps/web/src/components/CreateMenu.tsx | 13 +- apps/web/src/components/ErrorState.tsx | 4 +- apps/web/src/components/LanguageMenu.tsx | 32 +++ apps/web/src/components/Modal.tsx | 4 +- apps/web/src/components/ProductNavigation.tsx | 22 +- apps/web/src/components/SystemNavigation.tsx | 15 +- apps/web/src/components/ThemeMenu.tsx | 8 +- apps/web/src/components/Toast.tsx | 4 +- apps/web/src/features/agents/AgentCatalog.tsx | 49 ++-- apps/web/src/features/agents/AgentForm.tsx | 124 +++++----- .../src/features/agents/AgentSetupView.tsx | 60 ++--- apps/web/src/features/agents/AgentsView.tsx | 100 ++++---- apps/web/src/features/agents/agent-form.ts | 65 ++--- .../src/features/agents/agent-templates.ts | 35 +-- .../src/features/agents/session-admission.ts | 47 ++-- .../features/dashboard/DashboardView.test.tsx | 2 +- .../src/features/dashboard/DashboardView.tsx | 155 ++++++------ .../dashboard/RuntimeObservabilityContent.tsx | 123 +++++----- .../features/dashboard/RuntimeTrendCharts.tsx | 66 ++--- .../features/dashboard/RuntimeTrendPanel.tsx | 52 ++-- .../src/features/dashboard/dashboard-model.ts | 13 +- .../EnvironmentTemplatesView.tsx | 53 ++-- .../environment-templates/TemplateForm.tsx | 21 +- .../environment-templates/template-editor.ts | 17 +- .../src/features/sandbox/NodeEnrollment.tsx | 5 +- apps/web/src/features/sandbox/NodeHealth.tsx | 5 +- .../features/sandbox/SandboxDiagnostic.tsx | 5 +- .../features/sandbox/SandboxManagerView.tsx | 10 +- .../src/features/sandbox/SandboxNodeCard.tsx | 5 +- .../features/sandbox/SandboxNodeSelector.tsx | 5 +- .../web/src/features/sandbox/SandboxSetup.tsx | 4 +- .../src/features/sandbox/SandboxTopology.tsx | 5 +- .../src/features/sandbox/SessionPlacement.tsx | 5 +- .../src/features/sessions/SessionsView.tsx | 205 ++++++++-------- .../sessions/actions/FunctionActionPanel.tsx | 62 ++--- .../sessions/actions/SessionActionsDialog.tsx | 92 +++---- .../sessions/actions/session-actions.ts | 66 ++--- .../create/SessionInitialInputEditor.tsx | 62 ++--- .../sessions/create/SessionStartDialog.tsx | 180 +++++++------- .../sessions/create/SessionToolsEditor.tsx | 57 ++--- .../sessions/create/session-environment.ts | 27 ++- .../sessions/create/session-initial-input.ts | 19 +- .../sessions/create/session-start-draft.ts | 20 +- .../environment/EnvironmentDialog.tsx | 6 +- .../EnvironmentFileCreatePanel.tsx | 33 ++- .../environment/EnvironmentFilesPanel.tsx | 65 +++-- .../sessions/environment/EnvironmentPanel.tsx | 149 ++++++------ .../sessions/items/ApplyPatchDiffViewer.tsx | 16 +- .../features/sessions/items/ItemRenderers.tsx | 28 ++- .../src/features/sessions/trace/TraceView.tsx | 219 +++++++++-------- .../features/sessions/turns/TurnTimeline.tsx | 71 +++--- .../src/features/system/SourceFilesPanel.tsx | 133 ++++++----- apps/web/src/features/system/SystemView.tsx | 74 +++--- .../src/features/vaults/CredentialDialog.tsx | 47 ++-- .../src/features/vaults/VaultCreateForm.tsx | 26 +- apps/web/src/features/vaults/VaultsView.tsx | 61 ++--- apps/web/src/features/vaults/vault-catalog.ts | 21 +- .../web/src/features/vaults/vault-metadata.ts | 12 +- .../src/features/vaults/vault-operations.ts | 3 +- apps/web/src/i18n/README.md | 23 ++ apps/web/src/i18n/i18n.test.tsx | 46 ++++ apps/web/src/i18n/i18next.d.ts | 10 + apps/web/src/i18n/index.ts | 63 +++++ apps/web/src/i18n/locales/en/agents.ts | 56 +++++ apps/web/src/i18n/locales/en/app.ts | 8 + apps/web/src/i18n/locales/en/common.ts | 16 ++ apps/web/src/i18n/locales/en/connection.ts | 81 +++++++ apps/web/src/i18n/locales/en/dashboard.ts | 34 +++ apps/web/src/i18n/locales/en/navigation.ts | 24 ++ apps/web/src/i18n/locales/en/pages.ts | 23 ++ apps/web/src/i18n/locales/en/sessions.ts | 208 ++++++++++++++++ apps/web/src/i18n/locales/en/system.ts | 63 +++++ apps/web/src/i18n/locales/en/templates.ts | 51 ++++ apps/web/src/i18n/locales/en/vaults.ts | 45 ++++ apps/web/src/i18n/locales/zh-CN/agents.ts | 29 +++ apps/web/src/i18n/locales/zh-CN/app.ts | 8 + apps/web/src/i18n/locales/zh-CN/common.ts | 16 ++ apps/web/src/i18n/locales/zh-CN/connection.ts | 19 ++ apps/web/src/i18n/locales/zh-CN/dashboard.ts | 34 +++ apps/web/src/i18n/locales/zh-CN/navigation.ts | 24 ++ apps/web/src/i18n/locales/zh-CN/pages.ts | 23 ++ apps/web/src/i18n/locales/zh-CN/sessions.ts | 226 ++++++++++++++++++ apps/web/src/i18n/locales/zh-CN/system.ts | 63 +++++ apps/web/src/i18n/locales/zh-CN/templates.ts | 16 ++ apps/web/src/i18n/locales/zh-CN/vaults.ts | 10 + apps/web/src/i18n/resources.ts | 60 +++++ apps/web/src/i18n/test-setup.ts | 3 + apps/web/src/lib/LocaleProvider.tsx | 17 -- apps/web/src/main.tsx | 4 +- apps/web/src/style.css | 55 ++++- apps/web/vite.config.ts | 1 + pnpm-lock.yaml | 65 +++++ 95 files changed, 3081 insertions(+), 1491 deletions(-) create mode 100644 apps/web/src/components/LanguageMenu.tsx create mode 100644 apps/web/src/i18n/README.md create mode 100644 apps/web/src/i18n/i18n.test.tsx create mode 100644 apps/web/src/i18n/i18next.d.ts create mode 100644 apps/web/src/i18n/index.ts create mode 100644 apps/web/src/i18n/locales/en/agents.ts create mode 100644 apps/web/src/i18n/locales/en/app.ts create mode 100644 apps/web/src/i18n/locales/en/common.ts create mode 100644 apps/web/src/i18n/locales/en/connection.ts create mode 100644 apps/web/src/i18n/locales/en/dashboard.ts create mode 100644 apps/web/src/i18n/locales/en/navigation.ts create mode 100644 apps/web/src/i18n/locales/en/pages.ts create mode 100644 apps/web/src/i18n/locales/en/sessions.ts create mode 100644 apps/web/src/i18n/locales/en/system.ts create mode 100644 apps/web/src/i18n/locales/en/templates.ts create mode 100644 apps/web/src/i18n/locales/en/vaults.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/agents.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/app.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/common.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/connection.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/dashboard.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/navigation.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/pages.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/sessions.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/system.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/templates.ts create mode 100644 apps/web/src/i18n/locales/zh-CN/vaults.ts create mode 100644 apps/web/src/i18n/resources.ts create mode 100644 apps/web/src/i18n/test-setup.ts delete mode 100644 apps/web/src/lib/LocaleProvider.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 47db10c42..6318e7dbb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,9 +12,11 @@ "dependencies": { "@agents-core-web/agents-client": "workspace:*", "@tanstack/react-table": "^8.21.3", + "i18next": "^26.4.2", "lucide-react": "^1.22.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-i18next": "^17.0.15", "react-markdown": "^10.1.0", "uplot": "^1.6.32" }, diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 42248d6e9..1ea7d2512 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,6 @@ import { Settings2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { AgentCoreError } from "@agents-core-web/agents-client"; import type { @@ -24,6 +25,7 @@ import { CreateMenu } from "./components/CreateMenu"; import { ProductNavigation, type ProductView } from "./components/ProductNavigation"; import { StatusIcon } from "./components/StatusIcon"; import { ThemeMenu } from "./components/ThemeMenu"; +import { LanguageMenu } from "./components/LanguageMenu"; import { useToast } from "./components/Toast"; import { AgentsView } from "./features/agents/AgentsView"; import { @@ -247,6 +249,7 @@ function projectTextEvent(current: SessionItem[], event: SessionEvent): SessionI } export function App() { + const { t } = useTranslation(["navigation", "app"]); const { show: showToast } = useToast(); const [view, setView] = useState(viewFromLocation); const [connection, setConnection] = useState(() => loadConnection()); @@ -452,7 +455,7 @@ export function App() { return false; } if (result.value === null) { - const message = "Agent data changed while the collection was loading. Refresh again to reconcile all loaded pages."; + const message = t("errors.agentChanged", { ns: "app" }); setAgentCollectionState("failed"); setAgentCollectionError(message); notify(message, "error"); @@ -465,7 +468,7 @@ export function App() { } finally { if (agentCollectionAbortRef.current === controller) agentCollectionAbortRef.current = null; } - }, [core, coreGeneration, notify]); + }, [core, coreGeneration, notify, t]); const refreshSessions = useCallback(async () => { if (coreGeneration !== connectionGenerationRef.current) return false; @@ -496,7 +499,7 @@ export function App() { return false; } if (result.value === null) { - const message = "Session data changed while the collection was loading. Refresh again to reconcile all loaded pages."; + const message = t("errors.sessionChanged", { ns: "app" }); setSessionCollectionState("failed"); setSessionCollectionError(message); notify(message, "error"); @@ -552,7 +555,7 @@ export function App() { } finally { if (sessionCollectionAbortRef.current === controller) sessionCollectionAbortRef.current = null; } - }, [core, coreGeneration, notify]); + }, [core, coreGeneration, notify, t]); const refreshRuntimeSnapshot = useCallback(async () => { if (coreGeneration !== connectionGenerationRef.current) return false; @@ -580,7 +583,7 @@ export function App() { if (result.status === "rejected") { if (isAbort(result.reason) && !timedOut) return false; const message = timedOut - ? "Runtime snapshot exceeded the 15 second Web refresh budget." + ? t("errors.runtimeBudget", { ns: "app" }) : errorMessage(result.reason); setRuntimeCollectionState("failed"); setRuntimeCollectionError(message); @@ -588,7 +591,7 @@ export function App() { } if (result.value === null) { setRuntimeCollectionState("failed"); - setRuntimeCollectionError("Session data changed while Runtime observations were loading. The previous complete snapshot was retained."); + setRuntimeCollectionError(t("errors.runtimeChanged", { ns: "app" })); return false; } setRuntimeSnapshot(result.value); @@ -600,7 +603,7 @@ export function App() { window.clearTimeout(timeout); if (runtimeCollectionAbortRef.current === controller) runtimeCollectionAbortRef.current = null; } - }, [core, coreGeneration]); + }, [core, coreGeneration, t]); const refreshFilteredSessions = useCallback(async (agentId: string) => { if ( @@ -635,7 +638,7 @@ export function App() { return false; } if (result.value === null) { - const message = "Filtered Session data changed while the collection was loading. Refresh again to reconcile all loaded pages."; + const message = t("errors.filteredChanged", { ns: "app" }); setFilteredSessionCollectionState("failed"); setFilteredSessionCollectionError(message); notify(message, "error"); @@ -643,7 +646,7 @@ export function App() { } const stableFilteredSessions = result.value; if (stableFilteredSessions.some((session) => session.agent.id !== agentId)) { - const message = "Agent Core returned a Session outside the requested Agent filter."; + const message = t("errors.outsideFilter", { ns: "app" }); setFilteredSessionCollectionState("failed"); setFilteredSessionCollectionError(message); notify(message, "error"); @@ -665,7 +668,7 @@ export function App() { filteredSessionCollectionAbortRef.current = null; } } - }, [core, coreGeneration, notify]); + }, [core, coreGeneration, notify, t]); const refreshVaults = useCallback(async () => { if (coreGeneration !== connectionGenerationRef.current) return false; @@ -1413,10 +1416,7 @@ export function App() { if (!shouldRetryStreamError(error)) { setCurrentStreamState("failed", errorMessage(error)); const status = error instanceof AgentCoreError ? ` (${error.status})` : ""; - notify( - `Live event stream was rejected by Agent Core${status}. Check the Core connection settings.`, - "error", - ); + notify(t("errors.streamRejected", { ns: "app", status }), "error"); return; } } @@ -1442,7 +1442,7 @@ export function App() { controller.abort(); if (selectedStreamAbortRef.current === controller) selectedStreamAbortRef.current = null; }; - }, [applyLiveSessionEvent, core, coreGeneration, notify, refreshSession, selectedId, streamRetryRevision]); + }, [applyLiveSessionEvent, core, coreGeneration, notify, refreshSession, selectedId, streamRetryRevision, t]); const retryCurrentStream = useCallback(() => { requestCurrentStreamRetry(selectedIdRef.current, (sessionId) => { @@ -1474,7 +1474,7 @@ export function App() { }; const createAgent = async (input: CreateAgentInput) => { - const agent = await run(() => core.createAgent(input), "Agent created."); + const agent = await run(() => core.createAgent(input), t("success.agentCreated", { ns: "app" })); if (!agent || coreGeneration !== connectionGenerationRef.current) return undefined; agentCollectionRevisionRef.current += 1; setAgents((current) => [agent, ...current]); @@ -1486,7 +1486,7 @@ export function App() { }; const updateAgent = async (agentId: string, input: UpdateAgentInput) => { - const agent = await run(() => requestAgentUpdate(core, agentId, input), "Agent updated."); + const agent = await run(() => requestAgentUpdate(core, agentId, input), t("success.agentUpdated", { ns: "app" })); if (!agent || coreGeneration !== connectionGenerationRef.current) return undefined; agentCollectionRevisionRef.current += 1; setAgents((current) => replaceSavedAgent(current, agent)); @@ -1494,7 +1494,7 @@ export function App() { }; const deleteAgent = async (agentId: string) => { - const deleted = await run(() => requestAgentDelete(core, agentId), "Agent deleted."); + const deleted = await run(() => requestAgentDelete(core, agentId), t("success.agentDeleted", { ns: "app" })); if (!deleted || coreGeneration !== connectionGenerationRef.current) return; agentCollectionRevisionRef.current += 1; setAgents((current) => removeSavedAgent(current, agentId)); @@ -1502,7 +1502,7 @@ export function App() { const runVaultMutation = async (operation: () => Promise): Promise => { if (coreGeneration !== connectionGenerationRef.current) { - throw new Error("The Core connection changed before the Vault operation started."); + throw new Error(t("errors.connectionChanged", { ns: "app" })); } const operationRequest = operationRequestRef.current + 1; operationRequestRef.current = operationRequest; @@ -1510,7 +1510,7 @@ export function App() { try { const result = await operation(); if (coreGeneration !== connectionGenerationRef.current) { - throw new Error("The Core connection changed before the Vault operation was confirmed."); + throw new Error(t("errors.connectionChangedConfirm", { ns: "app" })); } return result; } finally { @@ -1529,7 +1529,7 @@ export function App() { async createVault(name, metadata) { try { await runVaultMutation(() => requestVaultCreate(core, name, metadata)); - notify("Vault created.", "success"); + notify(t("success.vaultCreated", { ns: "app" }), "success"); } catch (error) { await refreshAfterVaultMutation(); throw error; @@ -1543,9 +1543,9 @@ export function App() { auth: { type: "static_bearer", mcp_server_url: serverURL, token }, })); if (created.vault_id !== vaultId || created.name !== name || created.auth.mcp_server_url !== serverURL) { - throw new Error("Agent Core returned mismatched Credential metadata."); + throw new Error(t("errors.credentialMismatch", { ns: "app" })); } - notify("Credential created. Token remains hidden.", "success"); + notify(t("success.credentialCreated", { ns: "app" }), "success"); } catch (error) { await refreshAfterVaultMutation(); throw error; @@ -1557,7 +1557,7 @@ export function App() { credential.vault_id === vaultId && credential.id === credentialId )); if (!baseline || vaultCollectionState !== "ready") { - throw new Error("The latest Credential metadata is unavailable. Refresh before replacing its token."); + throw new Error(t("errors.latestCredentialMissing", { ns: "app" })); } try { const updated = await runVaultMutation(() => core.replaceVaultCredentialToken(vaultId, credentialId, { @@ -1567,8 +1567,8 @@ export function App() { updated.id !== baseline.id || updated.vault_id !== baseline.vault_id || updated.name !== baseline.name || updated.auth.mcp_server_url !== baseline.auth.mcp_server_url || updated.created_at !== baseline.created_at || updated.updated_at < baseline.updated_at - ) throw new Error("Agent Core returned mismatched Credential metadata after replacement."); - notify("Credential token replaced. Running work may still hold the previous token.", "success"); + ) throw new Error(t("errors.credentialReplacementMismatch", { ns: "app" })); + notify(t("success.credentialReplaced", { ns: "app" }), "success"); } catch (error) { await refreshAfterVaultMutation(); throw error; @@ -1578,7 +1578,7 @@ export function App() { async deleteCredential(vaultId, credentialId) { try { await runVaultMutation(() => core.deleteVaultCredential(vaultId, credentialId)); - notify("Credential deleted. Provider-side token was not revoked.", "success"); + notify(t("success.credentialDeleted", { ns: "app" }), "success"); } catch (error) { let confirmedDeleted = false; try { @@ -1588,7 +1588,7 @@ export function App() { } await refreshAfterVaultMutation(); if (confirmedDeleted) { - notify("Credential deletion reconciled from Core.", "success"); + notify(t("success.credentialDeleteReconciled", { ns: "app" }), "success"); return; } throw error; @@ -1598,7 +1598,7 @@ export function App() { async deleteVault(vaultId) { try { await runVaultMutation(() => core.deleteVault(vaultId)); - notify("Vault and its Credentials deleted. Provider-side tokens were not revoked.", "success"); + notify(t("success.vaultDeleted", { ns: "app" }), "success"); } catch (error) { let confirmedDeleted = false; try { @@ -1608,7 +1608,7 @@ export function App() { } await refreshAfterVaultMutation(); if (confirmedDeleted) { - notify("Vault deletion reconciled from Core.", "success"); + notify(t("success.vaultDeleteReconciled", { ns: "app" }), "success"); return; } throw error; @@ -1622,12 +1622,12 @@ export function App() { const createEnvironmentTemplate = async (input: CreateEnvironmentTemplateInput) => { if (!__AGENTS_CORE_WEB_OPENAI_HOSTED_SESSIONS__) { - throw new Error("Managed Environment configuration is not enabled for this Web build."); + throw new Error(t("errors.managedDisabled", { ns: "app" })); } const template = await core.createEnvironmentTemplate(input); // Re-read the durable collection so selection uses Core's own listing. await refreshEnvironmentTemplates(); - notify("Environment Template saved. No Runtime was allocated.", "success"); + notify(t("success.templateSaved", { ns: "app" }), "success"); return template; }; @@ -1726,10 +1726,10 @@ export function App() { input.idempotencyKey, )); if (!session || coreGeneration !== connectionGenerationRef.current) { - throw new Error("The Session creation outcome could not be confirmed."); + throw new Error(t("errors.sessionOutcomeUnknown", { ns: "app" })); } openSession(session); - notify(input.input === undefined ? "Idle Session created. Opening live events…" : "Session opened. Connecting live events…", "success"); + notify(t(input.input === undefined ? "success.idleSessionOpened" : "success.sessionOpened", { ns: "app" }), "success"); return; } @@ -1844,14 +1844,14 @@ export function App() { try { await created; if (generation !== connectionGenerationRef.current) { - throw new Error("The Session creation outcome could not be confirmed after the Core connection changed."); + throw new Error(t("errors.sessionOutcomeChanged", { ns: "app" })); } notify( input.input === undefined ? input.environment.type === "openai_hosted" - ? "Managed hosted Session created. Live creation events are connected." - : "Idle Session created. Live creation events are connected." - : "Session created with initial input. Live creation events are connected.", + ? t("success.managedSessionConnected", { ns: "app" }) + : t("success.idleSessionConnected", { ns: "app" }) + : t("success.sessionInputConnected", { ns: "app" }), "success", ); } catch (error) { @@ -1907,7 +1907,7 @@ export function App() { throw error; } if (generation !== connectionGenerationRef.current) { - notify("The previous Core returned a Session update after the connection changed. The current Core view was not modified.", "error"); + notify(t("errors.previousSessionUpdate", { ns: "app" }), "error"); return undefined; } sessionCollectionRevisionRef.current += 1; @@ -1925,7 +1925,7 @@ export function App() { filteredSessionsRef.current = next; return next; }); - notify("Session metadata updated.", "success"); + notify(t("success.metadataUpdated", { ns: "app" }), "success"); return updated; }; @@ -2028,10 +2028,10 @@ export function App() { ); } if (generation !== connectionGenerationRef.current) { - notify("The previous Core confirmed Session deletion after the connection changed. The current Core view was not modified.", "error"); + notify(t("errors.previousSessionDelete", { ns: "app" }), "error"); return false; } - return removeSessionFromWorkspace(sessionId, "Session deleted from Agent Core."); + return removeSessionFromWorkspace(sessionId, t("success.sessionDeleted", { ns: "app" })); }; // Runs only after the user confirms Cancel work and delete for a Session that @@ -2048,7 +2048,7 @@ export function App() { ); } if (settled === "missing") { - return removeSessionFromWorkspace(sessionId, "Session is absent from Agent Core after cancellation."); + return removeSessionFromWorkspace(sessionId, t("success.sessionAbsentAfterCancel", { ns: "app" })); } return deleteSessionFromCore(sessionId); }; @@ -2057,7 +2057,7 @@ export function App() { const sessionId = selectedId; if (!sessionId) return; if (!streamReady) { - const message = "Wait for the live event stream to connect before sending."; + const message = t("errors.waitForStream", { ns: "app" }); notify(message, "error"); throw new Error(message); } @@ -2103,7 +2103,7 @@ export function App() { try { await run( () => core.cancelTurn(sessionId, pending.idempotencyKey), - "Cancellation requested.", + t("success.cancellationRequested", { ns: "app" }), ); } catch (error) { if ( @@ -2130,7 +2130,7 @@ export function App() { try { await run( () => core.submitFunctionResult(sessionId, input, pending.idempotencyKey), - "Function result submitted.", + t("success.functionSubmitted", { ns: "app" }), ); } catch (error) { if ( @@ -2255,7 +2255,7 @@ export function App() { const openSessionSetup = (agentId?: string) => { if (agentId && !agents.some((candidate) => candidate.id === agentId)) { - notify("Session setup could not open because the selected saved Agent is not loaded.", "error"); + notify(t("errors.setupAgentMissing", { ns: "app" }), "error"); return; } setView("sessions"); @@ -2273,7 +2273,7 @@ export function App() { return (
- Skip to main content + {t("skipToContent")} diff --git a/apps/web/src/components/ConnectionModal.tsx b/apps/web/src/components/ConnectionModal.tsx index a5a41f8c5..e9d0d71a0 100644 --- a/apps/web/src/components/ConnectionModal.tsx +++ b/apps/web/src/components/ConnectionModal.tsx @@ -1,5 +1,7 @@ import { Check, Container, Copy, ExternalLink, Info, KeyRound } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; import { isLocalProxyBaseUrl, @@ -37,6 +39,7 @@ const parsarContainerSetupUrl = "https://github.com/MiniMax-AI/parsar-core/blob/ const parsarDaemonSetupUrl = "https://github.com/MiniMax-AI/parsar-core/blob/main/services/agents-api/README.md#internal-execution-device-connection"; function DockerCommand({ label, command }: { label: string; command: string }) { + const { t } = useTranslation("connection"); const [copied, setCopied] = useState(false); const copy = async () => { @@ -53,48 +56,46 @@ function DockerCommand({ label, command }: { label: string; command: string }) { return (
{command} -
); } function DockerBackendGuide({ profile }: { profile: LocalDockerBackendGuideProfile | null }) { + const { t } = useTranslation("connection"); return (
{profile ? ( <>
- Already set up on this computer - Start the existing containers + {t("existingTitle")} + {t("existingSubtitle")}
  1. - Start the dedicated databaseWait until its Docker health status is healthy. + {t("databaseStart")}{t("databaseWait")}
  2. - Start Core API and daemonThe daemon reconnects to Core independently. + {t("coreDaemonStart")}{t("daemonReconnects")}
  3. - Verify Core process healthA successful health response is liveness only; use Test connection next. + {t("verifyHealth")}{t("healthBoundary")}
    - First time on this computer - Create Core before trying docker start + {t("firstTime")} + {t("firstTimeSubtitle")}
    -

    - Docker is assumed to be installed. Parsar Core still requires a dedicated PostgreSQL database, a private - caller principal, migrations, the Core API container, and a provisioned daemon profile. Core does not - publish a safe zero-input bootstrap, so Web will not invent credentials or - create containers with guessed settings. -

    +

    {t("dockerAssumption")}

    1. - Build the Core imageRun from the reviewed parsar-core checkout. + {t("buildImage")}{t("reviewedCheckout")}
    2. -
    3. Create the private Core stackPrepare the dedicated database, caller key files, migrations, and API container using the in-repository container guide.
    4. -
    5. Provision and connect the daemonIssue a separate device profile; caller keys and daemon credentials are not interchangeable.
    6. -
    7. Return here and testConfigure Web's server-side caller-key file, restart Web, then use Test connection.
    8. +
    9. {t("createStack")}{t("createStackHelp")}
    10. +
    11. {t("provisionDaemon")}{t("provisionDaemonHelp")}
    12. +
    13. {t("returnTest")}{t("returnTestHelp")}
    @@ -140,18 +136,11 @@ function DockerBackendGuide({ profile }: { profile: LocalDockerBackendGuideProfi ) : (
    - Docker startup guide is not configured for this Web build. -

    - Set the non-secret AGENTS_CORE_WEB_DOCKER_BACKEND_* values from .env.example, - then restart Web. Container names are operator configuration and are never guessed in the browser. -

    + {t("dockerUnconfigured")} +

    {t("dockerUnconfiguredHelp")}

    )} -

    - Existing-stack commands only start saved database/Core/daemon containers. First-time setup remains an - operator action because it creates durable state and credentials. A self-hosted executor is Session-specific - and must be connected from that Session's Environment instructions. -

    +

    {t("dockerBoundary")}

); } @@ -168,53 +157,47 @@ function initialAdvancedDraft(connection: CoreConnection): CoreConnection { }; } -function resultCopy(result: CoreProbeResult): { title: string; detail: string } { +function resultCopy(result: CoreProbeResult, t: TFunction<"connection">): { title: string; detail: string } { switch (result.kind) { case "authenticated": return { - title: "Core API authenticated", - detail: "The read-only Agents API request returned a valid collection.", + title: t("result.authenticatedTitle"), detail: t("result.authenticatedDetail"), }; case "invalid_configuration": return { - title: "Core URL blocked", - detail: "Remote Core URLs must use HTTPS. Plain HTTP is allowed only for an explicit loopback host.", + title: t("result.invalidTitle"), detail: t("result.invalidDetail"), }; case "unauthorized": return { - title: "Authentication failed", - detail: "Core returned 401 invalid_api_key. Check the server-managed caller key or current-tab token.", + title: t("result.unauthorizedTitle"), detail: t("result.unauthorizedDetail"), }; case "protocol_mismatch": return { - title: "Agents API protocol mismatch", - detail: "The endpoint did not accept the tested /v1/agents GET contract and OpenAI-Beta: agents=v1 header.", + title: t("result.mismatchTitle"), detail: t("result.mismatchDetail"), }; case "http_error": return { - title: `Core returned HTTP ${result.httpStatus ?? "error"}`, - detail: "The basic Agents API read did not succeed. No Core resource was changed.", + title: t("result.httpTitle", { status: result.httpStatus ?? "error" }), detail: t("result.httpDetail"), }; case "unreachable": return { - title: "Core unreachable", - detail: "The browser could not reach Core. Check the local proxy target, network, or direct-mode CORS policy.", + title: t("result.unreachableTitle"), detail: t("result.unreachableDetail"), }; } } export function ConnectionProbeStatus({ state }: { state: ConnectionProbeState }) { + const { t } = useTranslation("connection"); if (state.status === "idle") return null; if (state.status === "loading") { return (
- Testing Core connection… -

Sending one read-only Agents API GET request.

+ {t("testingTitle")}

{t("testingDetail")}

); } - const copy = resultCopy(state.result); + const copy = resultCopy(state.result, t); const failed = state.result.kind !== "authenticated"; return (
{copy.title}

{copy.detail}

- Chat uses the current Agents API contract. This read-only probe does not start a Turn or verify its runtime dependencies. + {t("probeBoundary")}
); } @@ -237,6 +220,8 @@ export function ConnectionModal({ onClose, onSave, }: ConnectionModalProps) { + const { t } = useTranslation("connection"); + const { t: translateCommon } = useTranslation("common"); const modeName = useId(); const [mode, setMode] = useState(() => initialMode(connection)); const [advancedDraft, setAdvancedDraft] = useState(() => initialAdvancedDraft(connection)); @@ -295,11 +280,11 @@ export function ConnectionModal({ } >
- Connection mode + {t("mode")}
@@ -348,24 +331,21 @@ export function ConnectionModal({
-
API base
+
{t("apiBase")}
/v1
-
Authentication
-
{proxyAuthEnabled ? "Server-managed key detected" : "Server-managed key not detected"}
+
{t("authentication")}
{proxyAuthEnabled ? t("keyDetected") : t("keyMissing")}

{proxyAuthEnabled - ? "The Vite proxy supplies its key server-side. No bearer credential is exposed to browser JavaScript." - : "Configure the local proxy token file and restart Web. Local mode will not request a browser token."} + ? t("proxySecure") : t("proxyConfigure")}

@@ -373,7 +353,7 @@ export function ConnectionModal({ ) : (
@@ -420,11 +397,10 @@ export function ConnectionModal({
- Connection test -

Checks authenticated Agents API access with one GET. It never creates an Agent, Session, Turn, or Item.

+ {t("testTitle")}

{t("testHelp")}

@@ -433,31 +409,27 @@ export function ConnectionModal({
); diff --git a/apps/web/src/components/CreateMenu.tsx b/apps/web/src/components/CreateMenu.tsx index ee6774568..97ffac8c6 100644 --- a/apps/web/src/components/CreateMenu.tsx +++ b/apps/web/src/components/CreateMenu.tsx @@ -5,6 +5,7 @@ import { Plus, } from "lucide-react"; import { useEffect, useRef, useState, type KeyboardEvent } from "react"; +import { useTranslation } from "react-i18next"; interface CreateMenuActions { canCreateAgent: boolean; @@ -19,17 +20,18 @@ export function CreateMenuContent({ onCreateAgent, onStartSession, }: CreateMenuActions) { + const { t } = useTranslation(); return ( -
+
@@ -37,6 +39,7 @@ export function CreateMenuContent({ } export function CreateMenu(actions: CreateMenuActions) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const rootRef = useRef(null); const triggerRef = useRef(null); @@ -99,7 +102,7 @@ export function CreateMenu(actions: CreateMenuActions) { }} >