Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/features/fleet/DiagnosticTip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { sandboxDiagnosticMessage } from "../../lib/sandbox-diagnostic";
/** The specific reason behind a node's status, in the help tip beside it: what is wrong, then how to fix it. */
export function DiagnosticTip({ code }: { code: string }) {
const { i18n } = useTranslation();
const message = sandboxDiagnosticMessage(code, i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en");
const message = sandboxDiagnosticMessage(code, i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en");
if (!message) return null;
return (
<HelpTip label={message.label}>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/NodeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function nodeDiagnostic(node: SandboxNode): string {

function Diagnostic({ value }: { value: string }) {
const { i18n } = useTranslation("sandbox");
const message = sandboxDiagnosticMessage(value, i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en");
const message = sandboxDiagnosticMessage(value, i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en");
if (!message) return <>{MISSING}</>;
return <span className="node-diagnostic">{message.label}<HelpTip label={message.label}>{message.advice}</HelpTip></span>;
}
Expand Down Expand Up @@ -49,7 +49,7 @@ export function NodeDetail({ node, allocations, coreUrl, targetGeneration, stale
}) {
const { t, i18n } = useTranslation("sandbox");
const locale = i18n.resolvedLanguage;
const shortLocale = locale?.startsWith("zh") ? "zh" : "en";
const shortLocale = locale?.startsWith("zh") ? "zh-CN" : "en";
const now = Math.floor(Date.now() / 1000);
const own = allocations.filter((allocation) => allocation.node_id === node.id);
const reporting = !stale && node.online;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/NodeEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function NodeEditDialog({ client, node, size, suspends, onClose, onSaved
onSaved: () => void;
}) {
const { t, i18n } = useTranslation("sandbox");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const { t: tCommon } = useTranslation("common");
const id = useId();
const [name, setName] = useState(node?.name ?? "");
Expand Down Expand Up @@ -61,7 +61,7 @@ export function NodeEditDialog({ client, node, size, suspends, onClose, onSaved
t("Host: {{host}}.", { host: measure(hostCpus, hostMemory) }),
...(size ? [t("Each sandbox: {{size}}.", { size: measure(size.cpus, size.memory_mib * 2 ** 20) })] : []),
...(fit !== null && fit > 0 ? [t("Suggested: at most {{count}} at once.", { count: fit })] : []),
].join(locale === "zh" ? "" : " ") : null;
].join(locale === "zh-CN" ? "" : " ") : null;

async function save() {
if (!ready || !node) return;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/NodeEnrollment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function NodeEnrollment({ client, consoleConfig, deployment, nodes, open,
onRefresh: () => Promise<SandboxNode[] | null>;
}) {
const { t, i18n } = useTranslation("sandbox");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const { t: tCommon } = useTranslation("common");
const id = useId();
const [active, setActive] = useState(DEFAULT_ACTIVE);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/NodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { HelpTip, StatusDot, type Tone } from "../../components/console-ui";
import { NameCell, RowActions } from "../../components/list-ui";
import { epochSeconds, formatDateTime, formatRelative } from "../../lib/format";
import type { MessageKey } from "../../lib/locale-strings";
import type { ParseKeys } from "i18next";
import { nodeProviderDiagnostic } from "../../lib/sandbox-diagnostic";
import { DiagnosticTip } from "../fleet/DiagnosticTip";
import { nodeHealth, suspendedSandboxes } from "../fleet/fleet-model";
Expand Down Expand Up @@ -36,7 +36,7 @@ export function nodeState(node: SandboxNode, allocations: readonly SandboxAlloca
}

const stateTone: Record<NodeState, Tone> = { unconfirmed: "neutral", old_address: "warning", offline: "danger", degraded: "warning", attention: "warning", available: "ok" };
const stateLabel: Record<NodeState, MessageKey> = {
const stateLabel: Record<NodeState, ParseKeys<"sandbox">> = {
unconfirmed: "Status unconfirmed",
old_address: "Old address",
offline: "Offline",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/NodeRolloutStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { SandboxNode, SandboxNodeRollout } from "@oac/agents-client";
import { useTranslation } from "react-i18next";
import { HelpTip, StatusDot, type Tone } from "../../components/console-ui";
import type { MessageKey } from "../../lib/locale-strings";
import type { ParseKeys } from "i18next";
import { DiagnosticTip } from "../fleet/DiagnosticTip";

const labels: Record<SandboxNodeRollout["state"], MessageKey> = { ready: "Ready for target", preparing: "Preparing target", failed: "Preparation failed", update_required: "Node software incompatible", unknown: "Target readiness unknown" };
const labels: Record<SandboxNodeRollout["state"], ParseKeys<"sandbox">> = { ready: "Ready for target", preparing: "Preparing target", failed: "Preparation failed", update_required: "Node software incompatible", unknown: "Target readiness unknown" };
const tones: Record<SandboxNodeRollout["state"], Tone> = { ready: "ok", preparing: "neutral", failed: "warning", update_required: "warning", unknown: "neutral" };

/** A serving pin is historical ownership, not proof of a live connection or capacity. */
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/SandboxDeploymentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ function DeploymentHeader({ actions }: { actions?: ReactNode }) {
/** System's secondary page is the sole owner of deployment configuration controls. */
export function SandboxDeploymentPage() {
const { i18n } = useTranslation("sandbox");
return <section className="page-section console-page sandbox-manager sandbox-manager-page" lang={i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en"}>
return <section className="page-section console-page sandbox-manager sandbox-manager-page" lang={i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en"}>
<DeploymentConfiguration />
</section>;
}

function DeploymentConfiguration() {
const { t, i18n } = useTranslation("sandbox");
const { t: tNavigation } = useTranslation("sandboxNavigation");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const { navigate } = useConsoleNavigation();
const queryClient = useQueryClient();
const { deploymentQuery, snapshot, installation, loading, busy, setupNeedsRefresh, confirmed, fresh, refresh, configurationKey } = useSandboxPageState();
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/sandbox/SandboxDeploymentSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next";
import { Modal } from "../../components/Modal";
import { HelpTip, RefreshButton } from "../../components/console-ui";
import { formatBytes, formatPeriod, MISSING } from "../../lib/format";
import type { MessageKey } from "../../lib/locale-strings";
import type { ParseKeys } from "i18next";
import { sandboxProviderLabel } from "../../lib/sandbox-labels";
import { sandboxSize, templateBuildSize, templateBuildStatus } from "./deployment-specification";
import { SandboxRolloutSummary } from "./SandboxRolloutSummary";
import { SandboxResetControls } from "./SandboxResetControls";
import { SandboxSetupWizard } from "./SandboxSetupWizard";

const MIB = 2 ** 20;
const buildStatusLabel: Record<ReturnType<typeof templateBuildStatus>, MessageKey> = { ready: "Ready", notReady: "Not ready", unknown: "Unknown state" };
const buildStatusLabel: Record<ReturnType<typeof templateBuildStatus>, ParseKeys<"sandbox">> = { ready: "Ready", notReady: "Not ready", unknown: "Unknown state" };

export function SandboxDeploymentSettings({ deployment, disabled, fresh, onReset, onCancelReset, onUpdate, writeFailure, onRefresh, refreshing, pending }: {
deployment: SandboxDeployment;
Expand All @@ -28,7 +28,7 @@ export function SandboxDeploymentSettings({ deployment, disabled, fresh, onReset
}) {
const { t, i18n } = useTranslation("sandbox");
const { t: tNavigation } = useTranslation("sandboxNavigation");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const [changing, setChanging] = useState(false);
const [editKey, setEditKey] = useState(0);
const spec = deployment.specification;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/SandboxManagerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import "./SandboxManagerView.css";
/** Nodes owns node enrollment, the list and individual node management. */
export function SandboxManagerView() {
const { t, i18n } = useTranslation("sandbox");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const { data: config, isError, isFetching, refetch } = useQuery(sandboxConsoleConfigQuery);
return <section className="page-section console-page sandbox-manager sandbox-manager-page" lang={locale}>
{config ? <SandboxManager consoleConfig={config} /> : <>
Expand Down Expand Up @@ -53,7 +53,7 @@ function NodesPageHeader({ title, count, back, actions, headingRef }: { title?:

function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig }) {
const { t, i18n } = useTranslation("sandbox");
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const { params, navigate, back: goBack } = useConsoleNavigation();
const client = sandboxAdmin;
const queryClient = useQueryClient();
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/features/sandbox/SandboxSetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useConsoleNavigation } from "../../lib/console-navigation";
import { coreFieldError } from "../../lib/core-error";
import { formatBytes } from "../../lib/format";
import { installationQuery } from "../../lib/installation";
import type { MessageKey } from "../../lib/locale-strings";
import type { ParseKeys } from "i18next";
import { sandboxConfigurationRejection, sandboxProviderLabel } from "../../lib/sandbox-labels";
import { defaultSandboxResources, distributionRuntime, isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS, savedSpecification, validSandboxResources } from "./deployment-specification";
import { e2bKeyReady, e2bUpdateSelection } from "./sandbox-update";
Expand Down Expand Up @@ -81,7 +81,7 @@ function presets(provider: SandboxProvider): Record<Preset, SandboxResources> |
return { small: scale(0.5), standard, large: scale(2) };
}

const releaseLabels: Record<keyof SandboxRuntimeRelease, MessageKey> = {
const releaseLabels: Record<keyof SandboxRuntimeRelease, ParseKeys<"sandbox">> = {
source_commit: "Source commit",
image_id: "Image ID",
image_manifest_digest: "Image manifest digest",
Expand Down Expand Up @@ -125,7 +125,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
const { t, i18n } = useTranslation("sandbox");
const { t: tCommon } = useTranslation("common");
const id = useId();
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en";
const locale = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
const currentMode: Where | null = current ? deploymentContract.providers[current.provider].mode : null;
const [step, setStep] = useState<Step>(editing ? currentMode === "direct" ? "e2b" : "size" : "where");
const [where, setWhere] = useState<Where | null>(currentMode);
Expand Down Expand Up @@ -542,7 +542,7 @@ function NumberField({ id, label, value, onChange, error }: { error?: string | n
);
}

function Nav({ onBack, onNext, nextDisabled, t }: { onBack: () => void; onNext?: () => void; nextDisabled?: boolean; t: (key: MessageKey) => string }) {
function Nav({ onBack, onNext, nextDisabled, t }: { onBack: () => void; onNext?: () => void; nextDisabled?: boolean; t: (key: ParseKeys<"sandbox">) => string }) {
return (
<div className="wizard-nav">
<button className="button ghost" type="button" onClick={onBack}><ArrowLeft size={14} aria-hidden="true" />{t("Back")}</button>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/node-enrollment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SandboxEnrollment, SandboxNode } from "@oac/agents-client";

import type { MessageKey } from "../../lib/locale-strings";
import type { ParseKeys } from "i18next";
import { nodeProviderDiagnostic } from "../../lib/sandbox-diagnostic";

/**
Expand All @@ -11,7 +11,7 @@ import { nodeProviderDiagnostic } from "../../lib/sandbox-diagnostic";
export const NODE_READY_WAIT_MS = 60_000;

export interface HostPrerequisite {
label: MessageKey;
label: ParseKeys<"sandbox">;
}

/**
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ The console uses `i18next` and `react-i18next`. English is the fallback language
Translations are split by namespace, registered in `resources.ts`:

- `locales/en/*.ts` and `locales/zh-CN/*.ts` hold one file per feature namespace: `common` (reusable actions and labels, with the Core error messages of `core-errors.ts` nested inside it), `navigation` (the shell), `pages`, `agents`, `templates`, `vaults`, `files`, `skills`, `keys`, `sessions`, `diagnostics`, `dashboard`, `overview`, `metrics`, `system`, `sandbox-navigation` (registered as `sandboxNavigation`) and `onboarding`.
- The `sandbox` and `firstRun` namespaces come from `src/lib/locale-strings.ts` and `src/lib/console-auth-strings.ts`. Their keys are the English text and their values the Chinese translation. Sandbox status and node diagnostic formatting in `src/lib/sandbox-labels.ts` and `src/lib/sandbox-diagnostic.ts` reads the same strings.
- `sandbox` and `firstRun` use English text as keys in `locales/zh-CN/sandbox.ts` and `locales/zh-CN/first-run.ts`; `resources.ts` projects those keys into the English resources. Keep these keys unchanged when moving copy.

Add a namespace when a feature grows beyond page-level labels; do not grow one application-wide translation object.

When adding or changing copy:

1. Add the English key and the `zh-CN` translation in matching namespace files.
2. Consume the key with `useTranslation(namespace)` in React components.
3. Use interpolation for dynamic values instead of concatenating translated text.
4. Keep API values, identifiers, paths, commands and user-provided content out of translation resources.
5. Run the Web tests. The resource parity test rejects keys missing from either language.
3. Outside React, use `i18n.getFixedT(language, namespace)` with `SupportedLanguage` for an explicit language without changing the console language; use i18next’s `ParseKeys` for typed key maps.
4. Use interpolation for dynamic values instead of concatenating translated text.
5. Keep API values, identifiers, paths, commands and user-provided content out of translation resources.
6. Run the Web tests. The resource parity test rejects keys missing from either language.

The initial language follows the browser preference (`zh*` selects `zh-CN`) unless the user has chosen a language in the console menu. The choice is stored in local storage; the console still works when browser storage is unavailable.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const consoleAuthChinese = {
export const firstRun = {
"Try again": "重试",
"Sign in to OpenAgentCore": "登录 OpenAgentCore",
"Core key": "Core Key",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { consoleAuthChinese } from "./console-auth-strings";
export const chinese = {
import { firstRun } from "./first-run";
export const sandbox = {
"E2B provider": "E2B 服务商",
"Other E2B-compatible provider": "其他兼容 E2B 的服务商",
"Loading templates…": "正在读取模板…",
Expand All @@ -13,7 +13,7 @@ export const chinese = {
"Select a ready build": "选择 ready 构建",
"Core validates the exact ready build again when you save.": "保存时 Core 会再次校验所选的 ready 构建。",
"The selected provider supplies a default. You can edit it for a compatible endpoint.": "服务商已带出默认地址,也可以修改为兼容接口。",
...consoleAuthChinese,
...firstRun,
"Hosted Sessions will be archived permanently. Deployment configuration and node registrations will be cleared. Unsaved workspace contents may be lost.": "托管 Session 将归档且无法恢复,部署配置与节点注册将清除。未保存的工作区内容可能丢失。",
"What reset affects": "重置影响范围",
"How automatic reset works": "自动清理规则",
Expand Down Expand Up @@ -421,4 +421,3 @@ export const chinese = {
"{{name}} is still bound to an old Core address. Remove it and add it again.": "{{name}} 仍绑定在旧的 Core 地址上,需要移除后重新添加。",
"{{count}} nodes are still bound to an old Core address: {{names}}. Remove them and add them again.": "有 {{count}} 个节点仍绑定在旧的 Core 地址上:{{names}}。需要移除后重新添加。",
} as const;
export type MessageKey = keyof typeof chinese;
8 changes: 2 additions & 6 deletions apps/web/src/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,13 @@ import { skills as zhCNSkills } from "./locales/zh-CN/skills";
import { keys as zhCNKeys } from "./locales/zh-CN/keys";
import { system as zhCNSystem } from "./locales/zh-CN/system";
import { onboarding as zhCNOnboarding } from "./locales/zh-CN/onboarding";
import { consoleAuthChinese } from "../lib/console-auth-strings";
import { chinese as zhCNSandbox } from "../lib/locale-strings";
import { firstRun as zhCNFirstRun } from "./locales/zh-CN/first-run";
import { sandbox as zhCNSandbox } from "./locales/zh-CN/sandbox";

const enSandbox = Object.fromEntries(
Object.keys(zhCNSandbox).map((key) => [key, key]),
) as { [K in keyof typeof zhCNSandbox]: K };

const zhCNFirstRun = {
...consoleAuthChinese,
} as const;

const enFirstRun = Object.fromEntries(
Object.keys(zhCNFirstRun).map((key) => [key, key]),
) as { [K in keyof typeof zhCNFirstRun]: K };
Expand Down
Loading
Loading