From 649e6f096d3102ff229cf9e72348cbc59c87ddd5 Mon Sep 17 00:00:00 2001 From: lizhihua <275091674@qq.com> Date: Tue, 17 Feb 2026 15:08:27 +0800 Subject: [PATCH] feat: add i18n text --- .../session/artifact-markdown-editor.tsx | 55 +- .../components/session/artifacts-panel.tsx | 9 +- .../src/app/components/session/composer.tsx | 73 +- .../app/components/session/context-panel.tsx | 37 +- .../app/components/session/inbox-panel.tsx | 49 +- .../app/components/session/message-list.tsx | 11 +- .../src/app/components/session/minimap.tsx | 5 +- .../src/app/components/session/sidebar.tsx | 55 +- .../app/src/app/components/status-bar.tsx | 35 +- packages/app/src/app/pages/config.tsx | 136 ++-- packages/app/src/app/pages/dashboard.tsx | 141 ++-- packages/app/src/app/pages/extensions.tsx | 21 +- packages/app/src/app/pages/identities.tsx | 231 +++--- packages/app/src/app/pages/onboarding.tsx | 8 +- packages/app/src/app/pages/plugins.tsx | 39 +- packages/app/src/app/pages/scheduled.tsx | 228 +++--- packages/app/src/app/pages/session.tsx | 262 ++++--- packages/app/src/app/pages/settings.tsx | 434 +++++----- packages/app/src/app/pages/skills.tsx | 86 +- packages/app/src/i18n/locales/en.ts | 686 +++++++++++++++- packages/app/src/i18n/locales/zh.ts | 739 +++++++++++++++++- 21 files changed, 2381 insertions(+), 959 deletions(-) diff --git a/packages/app/src/app/components/session/artifact-markdown-editor.tsx b/packages/app/src/app/components/session/artifact-markdown-editor.tsx index 1370194bc..96dea4a73 100644 --- a/packages/app/src/app/components/session/artifact-markdown-editor.tsx +++ b/packages/app/src/app/components/session/artifact-markdown-editor.tsx @@ -1,4 +1,5 @@ import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"; +import { t } from "../../../i18n"; import { FileText, RefreshCcw, Save, X } from "lucide-solid"; import Button from "../button"; @@ -35,13 +36,13 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp const [pendingReason, setPendingReason] = createSignal<"switch" | null>(null); const path = createMemo(() => props.path?.trim() ?? ""); - const title = createMemo(() => (path() ? basename(path()) : "Artifact")); + const title = createMemo(() => (path() ? basename(path()) : t("session.editor_default_title"))); const dirty = createMemo(() => draft() !== original()); const canWrite = createMemo(() => Boolean(props.client && props.workspaceId)); const canSave = createMemo(() => dirty() && !saving() && canWrite()); const writeDisabledReason = createMemo(() => { if (canWrite()) return null; - return "Connect to an OpenWork server worker to edit files."; + return t("session.editor_connect_hint"); }); const resetState = () => { @@ -69,7 +70,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp } if (!target) return; if (!isMarkdown(target)) { - setError("Only markdown files are supported."); + setError(t("session.editor_toast_markdown_only")); return; } @@ -100,7 +101,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp result = (await client.readWorkspaceFile(workspaceId, actualPath)) as OpenworkWorkspaceFileContent; } catch (second) { if (second instanceof OpenworkServerError && second.status === 404) { - throw new OpenworkServerError(404, "file_not_found", "File not found (workspace root or outbox)."); + throw new OpenworkServerError(404, "file_not_found", t("session.editor_toast_not_found")); } throw second; } @@ -112,7 +113,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp setResolvedPath(actualPath); setBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null); } catch (err) { - const message = err instanceof Error ? err.message : "Failed to load file"; + const message = err instanceof Error ? err.message : t("session.editor_toast_load_failed"); setError(message); setLoadedPath(target); } finally { @@ -125,11 +126,11 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp const workspaceId = props.workspaceId; const target = resolvedPath() ?? path(); if (!client || !workspaceId || !target) { - props.onToast?.("Cannot save: OpenWork server not connected"); + props.onToast?.(t("session.editor_toast_save_connect")); return; } if (!isMarkdown(target)) { - props.onToast?.("Only markdown files are supported"); + props.onToast?.(t("session.editor_toast_markdown_only")); return; } if (!dirty()) return; @@ -158,7 +159,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp setConfirmOverwrite(true); return; } - const message = err instanceof Error ? err.message : "Failed to save"; + const message = err instanceof Error ? err.message : t("session.editor_toast_save_failed"); setError(message); props.onToast?.(message); } finally { @@ -183,7 +184,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp return; } // Reload is destructive; reuse the close-discard banner semantics. - setError("Discard changes to reload from disk (close and reopen), or save first."); + setError(t("session.editor_toast_reload_discard")); }; createEffect(() => { @@ -240,7 +241,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
@@ -318,9 +320,9 @@ export default function ConfigView(props: ConfigViewProps) {
- OpenWork server sharing
+ {t("config.sharing_title")}
- Share these details with a trusted device. Keep the server on the same network for the fastest setup.
+ {t("config.sharing_subtitle")}
@@ -331,13 +333,13 @@ export default function ConfigView(props: ConfigViewProps) {
- OpenWork Server URL
- {hostConnectUrl() || "Starting server…"}
+ {t("config.server_url_label")}
+ {hostConnectUrl() || t("config.starting_server")}
{hostConnectUrlUsesMdns()
- ? ".local names are easier to remember but may not resolve on all networks."
- : "Use your local IP on the same Wi-Fi for the fastest connection."}
+ ? t("config.mdns_hint")
+ : t("config.ip_hint")}
@@ -347,13 +349,13 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => handleCopy(hostConnectUrl(), "host-url")}
disabled={!hostConnectUrl()}
>
- {copyingField() === "host-url" ? "Copied" : "Copy"}
+ {copyingField() === "host-url" ? t("config.copied") : t("config.copy")}
- Access token
+ {t("config.access_token_label")}
{clientTokenVisible()
? hostInfo()?.clientToken || "—"
@@ -361,7 +363,7 @@ export default function ConfigView(props: ConfigViewProps) {
? "••••••••••••"
: "—"}
- Use on phones or laptops connecting to this server.
+ {t("config.access_token_hint")}
setClientTokenVisible((prev) => !prev)}
disabled={!hostInfo()?.clientToken}
>
- {clientTokenVisible() ? "Hide" : "Show"}
+ {clientTokenVisible() ? t("config.hide") : t("config.show")}
handleCopy(hostInfo()?.clientToken ?? "", "client-token")}
disabled={!hostInfo()?.clientToken}
>
- {copyingField() === "client-token" ? "Copied" : "Copy"}
+ {copyingField() === "client-token" ? t("config.copied") : t("config.copy")}
- Server token
+ {t("config.server_token_label")}
{hostTokenVisible()
? hostInfo()?.hostToken || "—"
@@ -393,7 +395,7 @@ export default function ConfigView(props: ConfigViewProps) {
? "••••••••••••"
: "—"}
- Keep private. Required for approval actions.
+ {t("config.server_token_hint")}
setHostTokenVisible((prev) => !prev)}
disabled={!hostInfo()?.hostToken}
>
- {hostTokenVisible() ? "Hide" : "Show"}
+ {hostTokenVisible() ? t("config.hide") : t("config.show")}
handleCopy(hostInfo()?.hostToken ?? "", "host-token")}
disabled={!hostInfo()?.hostToken}
>
- {copyingField() === "host-token" ? "Copied" : "Copy"}
+ {copyingField() === "host-token" ? t("config.copied") : t("config.copy")}
-
- For per-workspace sharing links, use Share... in the workspace menu.
-
+
- OpenWork server
+ {t("config.openwork_server_title")}
- Connect to an OpenWork server. Use the URL and access token from your server admin.
+ {t("config.openwork_server_subtitle")}
{openworkStatusLabel()}
@@ -435,22 +435,22 @@ export default function ConfigView(props: ConfigViewProps) {
setOpenworkUrl(event.currentTarget.value)}
- placeholder="http://127.0.0.1:8787"
- hint="Use the URL shared by your OpenWork server."
+ placeholder={t("dashboard.remote_base_url_placeholder")}
+ hint={t("dashboard.openwork_host_hint")}
disabled={props.busy}
/>
- Resolved worker URL: {resolvedWorkspaceUrl() || "Not set"}
- Worker ID: {resolvedWorkspaceId() || "Unavailable"}
+ {t("config.resolved_worker_url")}{resolvedWorkspaceUrl() || t("config.not_set")}
+ {t("config.worker_id")}{resolvedWorkspaceId() || t("config.unavailable")}
@@ -485,27 +485,27 @@ export default function ConfigView(props: ConfigViewProps) {
const ok = await props.testOpenworkServerConnection(next);
setOpenworkTestState(ok ? "success" : "error");
setOpenworkTestMessage(
- ok ? "Connection successful." : "Connection failed. Check the host URL and token.",
+ ok ? t("config.connection_success") : t("config.connection_failed")
);
} catch (error) {
- const message = error instanceof Error ? error.message : "Connection failed.";
+ const message = error instanceof Error ? error.message : t("config.connection_failed_generic");
setOpenworkTestState("error");
setOpenworkTestMessage(message);
}
}}
disabled={props.busy || openworkTestState() === "testing"}
>
- {openworkTestState() === "testing" ? "Testing..." : "Test connection"}
+ {openworkTestState() === "testing" ? t("config.testing") : t("config.test_connection")}
props.updateOpenworkServerSettings(buildOpenworkSettings())}
disabled={props.busy || !hasOpenworkChanges()}
>
- Save
+ {t("config.save")}
- Reset
+ {t("config.reset")}
@@ -521,25 +521,23 @@ export default function ConfigView(props: ConfigViewProps) {
role="status"
aria-live="polite"
>
- {openworkTestState() === "testing" ? "Testing connection..." : openworkTestMessage() ?? "Connection status updated."}
+ {openworkTestState() === "testing" ? t("config.testing_connection") : openworkTestMessage() ?? t("config.connection_status_updated")}
-
- OpenWork server connection needed to sync skills, plugins, and commands.
+
+ {t("config.sync_hint")}
- Messaging identities
-
- Manage Telegram/Slack identities and routing in the Identities tab.
-
+ {t("config.identities_title")}
+
- Some config features (local server sharing + messaging bridge) require the desktop app.
+ {t("config.desktop_only_hint")}
diff --git a/packages/app/src/app/pages/dashboard.tsx b/packages/app/src/app/pages/dashboard.tsx
index 9bd58ded5..d3fa75a2a 100644
--- a/packages/app/src/app/pages/dashboard.tsx
+++ b/packages/app/src/app/pages/dashboard.tsx
@@ -14,6 +14,7 @@ import type {
WorkspaceSessionGroup,
View,
} from "../types";
+import { t } from "../../i18n";
import type { McpDirectoryInfo } from "../constants";
import { formatRelativeTime, isTauriRuntime, normalizeDirectoryPath } from "../utils";
import { buildOpenworkWorkspaceBaseUrl, createOpenworkServerClient } from "../lib/openwork-server";
@@ -265,21 +266,21 @@ export default function DashboardView(props: DashboardViewProps) {
const title = createMemo(() => {
switch (props.tab) {
case "scheduled":
- return "Automations";
+ return t("dashboard.tab_automations");
case "skills":
- return "Skills";
+ return t("dashboard.tab_skills");
case "plugins":
- return "Extensions";
+ return t("dashboard.tab_extensions");
case "mcp":
- return "Extensions";
+ return t("dashboard.tab_extensions");
case "identities":
- return "Identities";
+ return t("dashboard.tab_identities");
case "config":
- return "Advanced";
+ return t("dashboard.tab_advanced");
case "settings":
- return "Settings";
+ return t("dashboard.tab_settings");
default:
- return "Automations";
+ return t("dashboard.tab_automations");
}
});
@@ -288,15 +289,15 @@ export default function DashboardView(props: DashboardViewProps) {
workspace.openworkWorkspaceName?.trim() ||
workspace.name?.trim() ||
workspace.path?.trim() ||
- "Worker";
+ t("dashboard.workspace_worker");
const workspaceKindLabel = (workspace: WorkspaceInfo) =>
workspace.workspaceType === "remote"
? workspace.sandboxBackend === "docker" ||
Boolean(workspace.sandboxRunId?.trim()) ||
Boolean(workspace.sandboxContainerName?.trim())
- ? "Sandbox"
- : "Remote"
- : "Local";
+ ? t("dashboard.workspace_sandbox")
+ : t("dashboard.workspace_remote")
+ : t("dashboard.workspace_local");
const openSessionFromList = (workspaceId: string, sessionId: string) => {
// For same-workspace clicks, just select the session without workspace activation
@@ -390,7 +391,7 @@ export default function DashboardView(props: DashboardViewProps) {
const showMoreLabel = (workspaceId: string, total: number) => {
const remaining = Math.max(0, total - previewCount(workspaceId));
const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining);
- return nextCount > 0 ? `Show ${nextCount} more` : "Show more";
+ return nextCount > 0 ? t("dashboard.show_more_count").replace("{count}", nextCount.toString()) : t("dashboard.show_more");
};
const [workspaceMenuId, setWorkspaceMenuId] = createSignal(null);
let workspaceMenuRef: HTMLDivElement | undefined;
@@ -605,23 +606,23 @@ export default function DashboardView(props: DashboardViewProps) {
const token = props.openworkServerHostInfo?.clientToken?.trim() || "";
return [
{
- label: "OpenWork worker URL",
+ label: t("dashboard.share_worker_url"),
value: url,
- placeholder: !isTauriRuntime() ? "Desktop app required" : "Starting server...",
+ placeholder: !isTauriRuntime() ? t("dashboard.share_desktop_required") : t("dashboard.share_starting_server"),
hint: mountedUrl
- ? "Use on phones or laptops connecting to this worker."
+ ? t("dashboard.share_worker_hint")
: hostUrl
- ? "Worker URL is resolving; host URL shown as fallback."
+ ? t("dashboard.share_resolving_hint")
: undefined,
},
{
- label: "Access token",
+ label: t("config.access_token_label"),
value: token,
secret: true,
- placeholder: isTauriRuntime() ? "-" : "Desktop app required",
+ placeholder: isTauriRuntime() ? "-" : t("dashboard.share_desktop_required"),
hint: mountedUrl
- ? "Use on phones or laptops connecting to this worker."
- : "Use on phones or laptops connecting to this host.",
+ ? t("dashboard.share_worker_hint")
+ : t("dashboard.share_host_hint"),
},
];
}
@@ -635,15 +636,15 @@ export default function DashboardView(props: DashboardViewProps) {
"";
return [
{
- label: "OpenWork worker URL",
+ label: t("dashboard.share_worker_url"),
value: url,
},
{
- label: "Access token",
+ label: t("config.access_token_label"),
value: token,
secret: true,
- placeholder: token ? undefined : "Set token in Advanced",
- hint: "This token grants access to the worker on that host.",
+ placeholder: token ? undefined : t("dashboard.share_token_advanced_hint"),
+ hint: t("dashboard.share_token_grant_hint"),
},
];
}
@@ -652,13 +653,13 @@ export default function DashboardView(props: DashboardViewProps) {
const directory = ws.directory?.trim() || "";
return [
{
- label: "OpenCode base URL",
+ label: t("dashboard.share_opencode_url"),
value: baseUrl,
},
{
- label: "Directory",
+ label: t("dashboard.share_directory"),
value: directory,
- placeholder: "(auto)",
+ placeholder: t("dashboard.share_auto"),
},
];
});
@@ -667,17 +668,17 @@ export default function DashboardView(props: DashboardViewProps) {
const ws = shareWorkspace();
if (!ws) return null;
if (ws.workspaceType === "local" && props.engineInfo?.runtime === "direct") {
- return "Engine runtime is set to Direct. Switching local workers can restart the host and disconnect clients. The token may change after a restart.";
+ return t("dashboard.share_direct_warning");
}
return null;
});
const exportDisabledReason = createMemo(() => {
const ws = shareWorkspace();
- if (!ws) return "Export is available for local workers in the desktop app.";
- if (ws.workspaceType === "remote") return "Export is only supported for local workers.";
- if (!isTauriRuntime()) return "Export is available in the desktop app.";
- if (props.exportWorkspaceBusy) return "Export is already running.";
+ if (!ws) return t("dashboard.export_local_desktop");
+ if (ws.workspaceType === "remote") return t("dashboard.export_local_only");
+ if (!isTauriRuntime()) return t("dashboard.export_desktop_only");
+ if (props.exportWorkspaceBusy) return t("dashboard.export_running");
return null;
});
@@ -698,13 +699,13 @@ export default function DashboardView(props: DashboardViewProps) {
const updatePillLabel = createMemo(() => {
const state = props.updateStatus?.state;
if (state === "ready") {
- return props.anyActiveRuns ? "Update ready" : "Install update";
+ return props.anyActiveRuns ? t("dashboard.update_ready_btn") : t("dashboard.install_update_btn");
}
if (state === "downloading") {
const percent = updateDownloadPercent();
- return percent == null ? "Downloading" : `Downloading ${percent}%`;
+ return percent == null ? t("dashboard.downloading_btn") : t("dashboard.downloading_percent_btn").replace("{percent}", percent.toString());
}
- return "Update available";
+ return t("dashboard.update_available_btn");
});
const updatePillTone = createMemo(() => {
@@ -720,11 +721,11 @@ export default function DashboardView(props: DashboardViewProps) {
const state = props.updateStatus?.state;
if (state === "ready") {
return props.anyActiveRuns
- ? `Update ready ${version}. Stop active runs to restart.`
- : `Restart to apply update ${version}`;
+ ? t("dashboard.update_ready_tooltip").replace("{version}", version)
+ : t("dashboard.update_restart_tooltip").replace("{version}", version);
}
- if (state === "downloading") return `Downloading update ${version}`;
- return `Update available ${version}`;
+ if (state === "downloading") return t("dashboard.update_downloading_tooltip").replace("{version}", version);
+ return t("dashboard.update_available_tooltip").replace("{version}", version);
});
const handleUpdatePillClick = () => {
@@ -765,7 +766,7 @@ export default function DashboardView(props: DashboardViewProps) {
- Tasks
+ {t("dashboard.tasks_header")}
@@ -822,9 +823,9 @@ export default function DashboardView(props: DashboardViewProps) {
- Error
+ {t("dashboard.tasks_error_badge")}
{/* Session count intentionally hidden (not a useful signal and it can crowd the header actions). */}
@@ -873,7 +874,7 @@ export default function DashboardView(props: DashboardViewProps) {
setWorkspaceMenuId(null);
}}
>
- Edit name
+ {t("dashboard.menu_edit_name")}
- Share...
+ {t("dashboard.menu_share")}
- Test connection
+ {t("dashboard.menu_test_connection")}
- Edit connection
+ {t("dashboard.menu_edit_connection")}
@@ -918,7 +919,7 @@ export default function DashboardView(props: DashboardViewProps) {
setWorkspaceMenuId(null);
}}
>
- Stop sandbox
+ {t("dashboard.menu_stop_sandbox")}
- Remove worker
+ {t("dashboard.menu_remove_worker")}
@@ -982,9 +983,9 @@ export default function DashboardView(props: DashboardViewProps) {
- Failed to load tasks
+ {t("dashboard.tasks_error")}
}
@@ -1027,8 +1028,8 @@ export default function DashboardView(props: DashboardViewProps) {
onClick={() => createTaskInWorkspace(workspace().id)}
disabled={props.newTaskDisabled}
>
- No tasks yet.
- + New task
+ {t("dashboard.tasks_empty")}
+ {t("dashboard.tasks_new")}
@@ -1045,7 +1046,7 @@ export default function DashboardView(props: DashboardViewProps) {
}
>
- Loading tasks...
+ {t("dashboard.loading_tasks")}
@@ -1063,7 +1064,7 @@ export default function DashboardView(props: DashboardViewProps) {
onClick={() => setAddWorkspaceMenuOpen((prev) => !prev)}
>
- Add a worker
+ {t("dashboard.add_worker")}
@@ -1076,7 +1077,7 @@ export default function DashboardView(props: DashboardViewProps) {
}}
>
- New worker
+ {t("dashboard.new_worker")}
- Connect remote
+ {t("dashboard.connect_remote")}
- Import config
+ {t("dashboard.import_config")}
@@ -1370,7 +1371,7 @@ export default function DashboardView(props: DashboardViewProps) {
onClick={props.repairOpencodeCache}
disabled={props.cacheRepairBusy || !props.developerMode}
>
- {props.cacheRepairBusy ? "Repairing cache" : "Repair cache"}
+ {props.cacheRepairBusy ? t("dashboard.repairing_cache_btn") : t("dashboard.repair_cache_btn")}
- Retry
+ {t("dashboard.retry")}
@@ -1445,7 +1446,7 @@ export default function DashboardView(props: DashboardViewProps) {
onClick={() => props.setTab("scheduled")}
>
- Automations
+ {t("dashboard.tab_automations")}
props.setTab("skills")}
>
- Skills
+ {t("dashboard.tab_skills")}
props.setTab("mcp")}
>
- Extensions
+ {t("dashboard.tab_extensions")}
props.setTab("identities")}
>
- IDs
+ {t("dashboard.nav_ids")}
props.setTab("config")}
>
- Advanced
+ {t("dashboard.tab_advanced")}
@@ -1489,11 +1490,11 @@ export default function DashboardView(props: DashboardViewProps) {
diff --git a/packages/app/src/app/pages/extensions.tsx b/packages/app/src/app/pages/extensions.tsx
index 5fb458d8a..0e78de113 100644
--- a/packages/app/src/app/pages/extensions.tsx
+++ b/packages/app/src/app/pages/extensions.tsx
@@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, createSignal } from "solid-js";
import { Box, Cpu } from "lucide-solid";
+import { t } from "../../i18n";
import Button from "../components/button";
import McpView, { type McpViewProps } from "./mcp";
import PluginsView, { type PluginsViewProps } from "./plugins";
@@ -46,16 +47,16 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
- Extensions
+ {t("extensions.title")}
- Apps (MCP) and OpenCode plugins live in one place.
+ {t("extensions.description")}
0}>
- {connectedAppsCount()} app{connectedAppsCount() === 1 ? "" : "s"} connected
+ {t("extensions.apps_connected").replace("{count}", String(connectedAppsCount())).replace("{plural}", connectedAppsCount() === 1 ? "" : "s")}
@@ -63,7 +64,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
- {pluginCount()} plugin{pluginCount() === 1 ? "" : "s"}
+ {t("extensions.plugins_count").replace("{count}", String(pluginCount())).replace("{plural}", pluginCount() === 1 ? "" : "s")}
@@ -78,7 +79,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
aria-pressed={section() === "all"}
onClick={() => setSection("all")}
>
- All
+ {t("extensions.tab_all")}
setSection("mcp")}
>
- Apps
+ {t("extensions.tab_apps")}
setSection("plugins")}
>
- Plugins
+ {t("extensions.tab_plugins")}
- Refresh
+ {t("extensions.refresh")}
@@ -109,7 +110,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
- Apps (MCP)
+ {t("extensions.section_apps")}
- Plugins (OpenCode)
+ {t("extensions.section_plugins")}
props.openworkServerStatus === "connected" && Boolean(openworkServerClient()));
const scopedWorkspaceReady = createMemo(() => Boolean(workspaceId()));
- const defaultRoutingDirectory = createMemo(() => props.activeWorkspaceRoot.trim() || "Not set");
+ const defaultRoutingDirectory = createMemo(() => props.activeWorkspaceRoot.trim() || t("identities.not_set"));
let lastResetKey = "";
const statusLabel = createMemo(() => {
- if (healthError()) return "Unavailable";
+ if (healthError()) return t("identities.status_unavailable");
const snapshot = health();
- if (!snapshot) return "Unknown";
- return snapshot.ok ? "Running" : "Offline";
+ if (!snapshot) return t("identities.status_unknown");
+ return snapshot.ok ? t("identities.status_running") : t("identities.status_offline");
});
const isWorkerOnline = createMemo(() => {
@@ -239,13 +240,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const ts = lastActivityAt();
if (!ts) return "\u2014";
const elapsedMs = Math.max(0, Date.now() - ts);
- if (elapsedMs < 60_000) return "Just now";
+ if (elapsedMs < 60_000) return t("identities.time_just_now");
const minutes = Math.floor(elapsedMs / 60_000);
- if (minutes < 60) return `${minutes}m ago`;
+ if (minutes < 60) return t("identities.time_minutes_ago").replace("{minutes}", String(minutes));
const hours = Math.floor(minutes / 60);
- if (hours < 24) return `${hours}h ago`;
+ if (hours < 24) return t("identities.time_hours_ago").replace("{hours}", String(hours));
const days = Math.floor(hours / 24);
- return `${days}d ago`;
+ return t("identities.time_days_ago").replace("{days}", String(days));
});
const workspaceAgentStatus = createMemo(() => {
@@ -324,7 +325,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setAgentContent(OPENCODE_ROUTER_AGENT_FILE_TEMPLATE);
setAgentDraft(OPENCODE_ROUTER_AGENT_FILE_TEMPLATE);
setAgentBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null);
- setAgentStatus("Created default messaging agent file.");
+ setAgentStatus(t("identities.msg_agent_created"));
} catch (error) {
setAgentError(formatRequestError(error));
} finally {
@@ -352,10 +353,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setAgentExists(true);
setAgentContent(agentDraft());
setAgentBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null);
- setAgentStatus("Saved messaging behavior.");
+ setAgentStatus(t("identities.msg_agent_saved"));
} catch (error) {
if (error instanceof OpenworkServerError && error.status === 409) {
- setAgentError("File changed remotely. Reload and save again.");
+ setAgentError(t("identities.error_file_changed"));
} else {
setAgentError(formatRequestError(error));
}
@@ -387,7 +388,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
...(sendAutoBind() ? { autoBind: true } : {}),
});
setSendResult(result);
- const base = `Dispatched ${result.sent}/${result.attempted} messages.`;
+ const base = t("identities.msg_dispatched").replace("{sent}", String(result.sent)).replace("{attempted}", String(result.attempted));
setSendStatus(result.reason?.trim() ? `${base} ${result.reason.trim()}` : base);
} catch (error) {
setSendError(formatRequestError(error));
@@ -414,9 +415,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setTelegramIdentities([]);
setTelegramBotUsername(null);
setSlackIdentities([]);
- setHealthError("Worker scope unavailable. Reconnect using a worker URL or switch to a known worker.");
- setTelegramIdentitiesError("Worker scope unavailable.");
- setSlackIdentitiesError("Worker scope unavailable.");
+ setHealthError(t("identities.error_worker_scope_unavailable_long"));
+ setTelegramIdentitiesError(t("identities.error_worker_scope_unavailable"));
+ setSlackIdentitiesError(t("identities.error_worker_scope_unavailable"));
resetAgentState();
setSendStatus(null);
setSendError(null);
@@ -441,7 +442,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const message =
(healthRes.json && typeof (healthRes.json as any).message === "string")
? String((healthRes.json as any).message)
- : `OpenCodeRouter health unavailable (${healthRes.status})`;
+ : t("identities.error_health_unavailable").replace("{status}", String(healthRes.status));
setHealthError(message);
}
}
@@ -450,14 +451,14 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setTelegramIdentities(tgRes.items ?? []);
} else {
setTelegramIdentities([]);
- setTelegramIdentitiesError("Telegram identities unavailable.");
+ setTelegramIdentitiesError(t("identities.error_telegram_unavailable"));
}
if (isOpenCodeRouterIdentities(slackRes)) {
setSlackIdentities(slackRes.items ?? []);
} else {
setSlackIdentities([]);
- setSlackIdentitiesError("Slack identities unavailable.");
+ setSlackIdentitiesError(t("identities.error_slack_unavailable"));
}
if (!agentDirty() && !agentSaving()) {
@@ -484,13 +485,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const ok = await props.reconnectOpenworkServer();
if (!ok) {
- setReconnectError("Reconnect failed. Check OpenWork URL/token and try again.");
+ setReconnectError(t("identities.error_reconnect_failed"));
return;
}
- setReconnectStatus("Reconnected. Refreshing worker state...");
+ setReconnectStatus(t("identities.msg_reconnected_refreshing"));
await refreshAll({ force: true });
- setReconnectStatus("Reconnected.");
+ setReconnectStatus(t("identities.msg_reconnected"));
};
const upsertTelegram = async () => {
@@ -514,12 +515,12 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
if (username) {
const normalized = String(username).trim().replace(/^@+/, "");
setTelegramBotUsername(normalized || null);
- setTelegramStatus(`Saved (@${normalized || String(username)})`);
+ setTelegramStatus(t("identities.msg_saved_username").replace("{username}", normalized || String(username)));
} else {
- setTelegramStatus(result.applied === false ? "Saved (pending apply)." : "Saved.");
+ setTelegramStatus(result.applied === false ? t("identities.msg_saved_pending") : t("identities.msg_saved"));
}
} else {
- setTelegramError("Failed to save.");
+ setTelegramError(t("identities.error_save_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setTelegramError(result.applyError.trim());
@@ -549,9 +550,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const result = await client.deleteOpenCodeRouterTelegramIdentity(id, identityId);
if (result.ok) {
setTelegramBotUsername(null);
- setTelegramStatus(result.applied === false ? "Deleted (pending apply)." : "Deleted.");
+ setTelegramStatus(result.applied === false ? t("identities.msg_deleted_pending") : t("identities.msg_deleted"));
} else {
- setTelegramError("Failed to delete.");
+ setTelegramError(t("identities.error_delete_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setTelegramError(result.applyError.trim());
@@ -582,9 +583,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
try {
const result = await client.upsertOpenCodeRouterSlackIdentity(id, { botToken, appToken, enabled: slackEnabled() });
if (result.ok) {
- setSlackStatus(result.applied === false ? "Saved (pending apply)." : "Saved.");
+ setSlackStatus(result.applied === false ? t("identities.msg_saved_pending") : t("identities.msg_saved"));
} else {
- setSlackError("Failed to save.");
+ setSlackError(t("identities.error_save_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setSlackError(result.applyError.trim());
@@ -614,9 +615,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
try {
const result = await client.deleteOpenCodeRouterSlackIdentity(id, identityId);
if (result.ok) {
- setSlackStatus(result.applied === false ? "Deleted (pending apply)." : "Deleted.");
+ setSlackStatus(result.applied === false ? t("identities.msg_deleted_pending") : t("identities.msg_deleted"));
} else {
- setSlackError("Failed to delete.");
+ setSlackError(t("identities.error_delete_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setSlackError(result.applyError.trim());
@@ -668,7 +669,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Header ---- */}
- Messaging channels
+ {t("identities.title")}
- Repair & reconnect
+ {t("identities.repair_reconnect")}
- Refresh
+ {t("identities.refresh")}
- Let people reach your worker through messaging apps. Connect a channel and
- your worker will automatically read and respond to messages.
+ {t("identities.description")}
- Workspace scope: {scopedOpenworkBaseUrl().trim() || props.openworkServerUrl.trim() || "Not set"}
+ {t("identities.workspace_scope").replace("{url}", scopedOpenworkBaseUrl().trim() || props.openworkServerUrl.trim() || t("identities.not_set"))}
{(value) => {value()}}
@@ -708,18 +708,14 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Not connected to server ---- */}
- Connect to an OpenWork server
-
- Identities are available when you are connected to an OpenWork host (openwork).
-
+ {t("identities.connect_host")}
+
-
- Workspace ID is required to manage identities. Reconnect with a workspace URL (for example: /w/<workspace-id>) or select a workspace mapped on this host.
-
+
@@ -731,7 +727,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
}`}
onClick={() => setActiveTab("general")}
>
- General
+ {t("identities.tab_general")}
setActiveTab("advanced")}
>
- Advanced
+ {t("identities.tab_advanced")}
@@ -760,7 +756,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
- {isWorkerOnline() ? "Worker online" : healthError() ? "Worker unavailable" : "Worker offline"}
+ {isWorkerOnline() ? t("identities.worker_online") : healthError() ? t("identities.worker_unavailable") : t("identities.worker_offline")}
0}
/>
0}
/>
@@ -804,7 +800,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Available channels ---- */}
- Available channels
+ {t("identities.available_channels")}
@@ -825,15 +821,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
- Telegram
+ {t("identities.telegram")}
- Connected
+ {t("identities.connected")}
- Create a Telegram bot that anyone can message. Great for personal automations and external contacts.
+ {t("identities.telegram_description")}
- {item.enabled ? "Enabled" : "Disabled"} · {item.running ? "Running" : "Stopped"}
+ {item.enabled ? t("identities.enabled") : t("mcp.disabled")} · {item.running ? t("status.running") : t("status.stopped")}
@@ -877,7 +873,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={telegramSaving() || item.id === "env" || !workspaceId()}
onClick={() => void deleteTelegram(item.id)}
>
- Disconnect
+ {t("identities.disconnect")}
@@ -888,7 +884,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* Connected stats summary */}
- Status
+ {t("identities.status_label")}
i.running) ? "bg-emerald-9" : "bg-gray-8"
@@ -896,18 +892,18 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
i.running) ? "text-emerald-11" : "text-gray-10"
}`}>
- {telegramIdentities().some((i) => i.running) ? "Active" : "Stopped"}
+ {telegramIdentities().some((i) => i.running) ? t("identities.status_active") : t("identities.status_stopped")}
- Identities
- {telegramIdentities().length} configured
+ {t("identities.identities_label")}
+ {telegramIdentities().length} {t("identities.configured")}
- Channel
+ {t("identities.status_channels")}
- {health()?.channels.telegram ? "On" : "Off"}
+ {health()?.channels.telegram ? t("identities.channel_on") : t("identities.channel_off")}
@@ -924,31 +920,29 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
- Quick setup
+ {t("identities.quick_setup")}
-
1
-
- Open @BotFather and run
/newbot.
-
+
-
2
- Copy the bot token and paste it below.
+ {t("identities.telegram_step2")}
-
3
- Connect, then send
/start to your bot to activate the chat.
+
-
+
setTelegramToken(e.currentTarget.value)}
@@ -961,7 +955,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
checked={telegramEnabled()}
onChange={(e) => setTelegramEnabled(e.currentTarget.checked)}
/>
- Enabled
+ {t("identities.enabled")}
- {telegramSaving() ? "Connecting..." : "Connect Telegram"}
+ {telegramSaving() ? t("identities.connecting") : t("identities.connect_telegram")}
@@ -994,7 +988,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
class="inline-flex items-center gap-2 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2 text-[12px] font-medium text-gray-11 hover:bg-gray-2"
>
- Open @{telegramBotUsername()} in Telegram
+ {t("identities.open_telegram_bot").replace("{username}", telegramBotUsername() || "")}
)}
@@ -1028,15 +1022,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
- Slack
+ {t("identities.slack")}
- Connected
+ {t("identities.connected")}
- Your worker appears as a bot in Slack channels. Team members can message it directly or mention it in threads.
+ {t("identities.slack_description")}
- {item.enabled ? "Enabled" : "Disabled"} · {item.running ? "Running" : "Stopped"}
+ {item.enabled ? t("identities.enabled") : t("mcp.disabled")} · {item.running ? t("status.running") : t("status.stopped")}
@@ -1080,7 +1074,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={slackSaving() || item.id === "env" || !workspaceId()}
onClick={() => void deleteSlack(item.id)}
>
- Disconnect
+ {t("identities.disconnect")}
@@ -1091,7 +1085,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* Connected stats summary */}
- Status
+ {t("identities.status_label")}
i.running) ? "bg-emerald-9" : "bg-gray-8"
@@ -1099,18 +1093,18 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
i.running) ? "text-emerald-11" : "text-gray-10"
}`}>
- {slackIdentities().some((i) => i.running) ? "Active" : "Stopped"}
+ {slackIdentities().some((i) => i.running) ? t("identities.status_active") : t("identities.status_stopped")}
- Identities
- {slackIdentities().length} configured
+ {t("identities.identities_label")}
+ {slackIdentities().length} {t("identities.configured")}
- Channel
+ {t("identities.status_channels")}
- {health()?.channels.slack ? "On" : "Off"}
+ {health()?.channels.slack ? t("identities.channel_on") : t("identities.channel_off")}
@@ -1127,13 +1121,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
- Connect your Slack workspace to let team members interact with this worker in channels and DMs.
+ {t("identities.slack_hint")}
-
+
-
+
setSlackEnabled(e.currentTarget.checked)}
/>
- Enabled
+ {t("identities.enabled")}
- {slackSaving() ? "Connecting..." : "Connect Slack"}
+ {slackSaving() ? t("identities.connecting") : t("identities.connect_slack")}
@@ -1206,21 +1200,20 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Message routing ---- */}
- Message routing
+ {t("identities.message_routing")}
- Control which conversations go to which workspace folder. Messages are
- routed to the worker's default folder unless you set up rules here.
+ {t("identities.message_routing_description")}
- Default routing
+ {t("identities.default_routing")}
- All channels
+ {t("identities.all_channels")}
@@ -1229,19 +1222,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
-
- Advanced: reply with /dir <path> in Slack/Telegram to override the directory for a specific chat (limited to this workspace root).
-
+
{/* ---- Messaging agent behavior ---- */}
- Messaging agent behavior
-
- One file per workspace. Add optional first line @agent <id> to route via a specific OpenCode agent.
-
+ {t("identities.agent_behavior")}
+
{OPENCODE_ROUTER_AGENT_FILE_PATH}
@@ -1251,24 +1240,24 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{(value) => (
- Active scope: workspace · status: {value().loaded ? "loaded" : "missing"} · selected agent: {value().selected || "(none)"}
+ {t("identities.active_scope").replace("{loaded}", value().loaded ? "loaded" : "missing").replace("{selected}", value().selected || "(none)")}
)}
- Loading agent file…
+ {t("identities.loading_agent")}
- Agent file not found in this workspace yet.
+ {t("identities.agent_not_found")}
@@ -1316,29 +1305,29 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Outbound send test ---- */}
- Send test message
+ {t("identities.send_test_message")}
- Validate outbound wiring. Use a peer ID for direct send, or leave peer ID empty to fan out by bindings in a directory.
+ {t("identities.send_test_hint")}
-
+
-
+
setSendPeerId(e.currentTarget.value)}
/>
@@ -1347,7 +1336,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
-
+
setSendAutoBind(e.currentTarget.checked)}
/>
- Auto-bind peer to directory on direct send
+ {t("identities.auto_bind")}
-
+
@@ -387,7 +392,7 @@ const AutomationJobCard = (props: {
- Run context
+ {t("scheduled.run_context")}
@@ -404,7 +409,7 @@ const AutomationJobCard = (props: {
- Source: {props.job.source}
+ {t("scheduled.source").replace("{source}", props.job.source)}
@@ -413,14 +418,14 @@ const AutomationJobCard = (props: {
- Last run {toRelative(props.job.lastRunAt)}
+ {t("scheduled.last_run").replace("{time}", toRelative(props.job.lastRunAt))}
- Created {toRelative(props.job.createdAt)}
+ {t("scheduled.created").replace("{time}", toRelative(props.job.createdAt))}
- Agent {props.job.run?.agent}
+ {t("scheduled.agent").replace("{agent}", props.job.run?.agent ?? "")}
- Model {props.job.run?.model}
+ {t("scheduled.model").replace("{model}", props.job.run?.model ?? "")}
@@ -435,35 +440,35 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
});
const supportNote = createMemo(() => {
if (props.source === "remote") {
- return props.sourceReady ? null : "OpenWork server unavailable. Connect to sync scheduled tasks.";
+ return props.sourceReady ? null : t("scheduled.server_unavailable");
}
- if (!isTauriRuntime()) return "Scheduled tasks require the desktop app.";
- if (props.isWindows) return "Scheduler is not supported on Windows yet.";
+ if (!isTauriRuntime()) return t("scheduled.desktop_required");
+ if (props.isWindows) return t("scheduled.windows_not_supported");
return null;
});
const sourceDescription = createMemo(() =>
props.source === "remote"
- ? "Automations that run on a schedule from the connected OpenWork server."
- : "Automations that run on a schedule from this device."
+ ? t("scheduled.desc_remote")
+ : t("scheduled.desc_local")
);
const sourceLabel = createMemo(() =>
- props.source === "remote" ? "From OpenWork server" : "From local scheduler"
+ props.source === "remote" ? t("scheduled.from_server") : t("scheduled.from_local")
);
- const schedulerLabel = createMemo(() => (props.source === "remote" ? "OpenWork server" : "Local"));
+ const schedulerLabel = createMemo(() => (props.source === "remote" ? t("scheduled.server") : t("scheduled.local")));
const schedulerHint = createMemo(() =>
- props.source === "remote" ? "Remote instance" : "Launchd or systemd"
+ props.source === "remote" ? t("scheduled.remote_instance") : t("scheduled.launchd_systemd")
);
const schedulerUnavailableHint = createMemo(() =>
- props.source === "remote" ? "OpenWork server unavailable" : "Desktop-only"
+ props.source === "remote" ? t("scheduled.server_unavailable_short") : t("scheduled.desktop_only")
);
const deleteDescription = createMemo(() =>
props.source === "remote"
- ? "This removes the schedule and deletes the job definition from the connected OpenWork server."
- : "This removes the schedule and deletes the job definition from your machine."
+ ? t("scheduled.delete_desc_remote")
+ : t("scheduled.delete_desc_local")
);
const lastUpdatedLabel = createMemo(() => {
- if (!props.lastUpdatedAt) return "Not synced yet";
+ if (!props.lastUpdatedAt) return t("scheduled.not_synced");
return formatRelativeTime(props.lastUpdatedAt);
});
@@ -471,10 +476,10 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
const [deleteBusy, setDeleteBusy] = createSignal(false);
const [deleteError, setDeleteError] = createSignal(null);
const [createModalOpen, setCreateModalOpen] = createSignal(false);
- const [automationName, setAutomationName] = createSignal("Daily bug scan");
+ const [automationName, setAutomationName] = createSignal(t("scheduled.template_daily_planning"));
const [automationProject, setAutomationProject] = createSignal(props.activeWorkspaceRoot);
const [automationPrompt, setAutomationPrompt] = createSignal(
- "Scan recent commits and flag riskier diffs."
+ t("scheduled.template_daily_planning_prompt")
);
const [scheduleMode, setScheduleMode] = createSignal<"daily" | "interval">("daily");
const [scheduleTime, setScheduleTime] = createSignal("09:00");
@@ -491,7 +496,7 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
setDeleteTarget(null);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
- setDeleteError(message || "Failed to delete job.");
+ setDeleteError(message || t("scheduled.delete_failed"));
} finally {
setDeleteBusy(false);
}
@@ -532,8 +537,8 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
if (root) {
setAutomationProject(root);
}
- setAutomationName(template.name);
- setAutomationPrompt(template.prompt);
+ setAutomationName(t(template.nameKey));
+ setAutomationPrompt(t(template.promptKey));
setScheduleMode(template.scheduleMode);
if (template.scheduleMode === "interval") {
setIntervalHours(template.intervalHours ?? 6);
@@ -559,8 +564,8 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
if (run?.prompt || job.prompt) {
const promptBody = (run?.prompt ?? job.prompt ?? "").trim();
- const workdirHint = workdir ? `\n\nRun from ${workdir}.` : "";
- props.setPrompt(`Run this automation now: ${job.name}.\nSchedule: ${schedule}.\n\n${promptBody}${workdirHint}`.trim());
+ const workdirHint = workdir ? t("scheduled.run_from").replace("{workdir}", workdir) : "";
+ props.setPrompt(t("scheduled.run_now_prompt").replace("{name}", job.name).replace("{schedule}", schedule) + `\n\n${promptBody}${workdirHint}`.trim());
props.createSessionAndOpen();
return;
}
@@ -568,15 +573,15 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
if (run?.command) {
const args = run.arguments ? ` ${run.arguments}` : "";
const cmd = `${run.command}${args}`.trim();
- const workdirHint = workdir ? `\n\nRun from ${workdir}.` : "";
+ const workdirHint = workdir ? t("scheduled.run_from").replace("{workdir}", workdir) : "";
props.setPrompt(
- `Run this automation now: ${job.name}.\nSchedule: ${schedule}.\n\nRun the following command:\n${cmd}${workdirHint}`.trim()
+ t("scheduled.run_command_prompt").replace("{name}", job.name).replace("{schedule}", schedule).replace("{cmd}", cmd).replace("{workdirHint}", workdirHint)
);
props.createSessionAndOpen();
return;
}
- props.setPrompt(`Run this automation now: ${job.name}.\nSchedule: ${schedule}.`);
+ props.setPrompt(t("scheduled.run_now_prompt").replace("{name}", job.name).replace("{schedule}", schedule));
props.createSessionAndOpen();
};
@@ -607,7 +612,7 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
onClick={openSchedulerDocs}
class="text-xs font-medium text-gray-9 transition-colors hover:text-gray-12"
>
- Learn more
+ {t("scheduled.explore_more")}
- {props.busy ? "Refreshing" : "Refresh"}
+ {props.busy ? t("scheduled.refreshing") : t("scheduled.refresh")}
- New automation
+ {t("scheduled.new_automation")}
@@ -642,9 +647,9 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
- Automations
+ {t("scheduled.automations")}
- Beta
+ {t("scheduled.beta")}
{sourceDescription()}
@@ -673,14 +678,14 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
fallback={
- No automations yet. Pick a template or create your own automation prompt.
+ {t("scheduled.no_automations")}
{(card) => (
openCreateModalFromTemplate(card)}
disabled={props.newTaskDisabled}
@@ -693,7 +698,7 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
onClick={openSchedulerDocs}
class="mx-auto block text-xs text-gray-9 transition-colors hover:text-gray-12"
>
- Explore more
+ {t("scheduled.explore_more")}
}
@@ -719,7 +724,7 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
- Delete automation?
+ {t("scheduled.delete_confirm_title")}
{deleteDescription()}
@@ -728,10 +733,10 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
setDeleteTarget(null)} disabled={deleteBusy()}>
- Cancel
+ {t("scheduled.cancel")}
- {deleteBusy() ? "Deleting" : "Delete"}
+ {deleteBusy() ? t("scheduled.deleting") : t("scheduled.delete")}
@@ -745,10 +750,9 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
- Create automation
+ {t("scheduled.create_modal_title")}
- Automations are scheduled by running a prompt in a new thread. We’ll prefill
- a prompt for you to send.
+ {t("scheduled.create_modal_desc")}
setAutomationProject(event.currentTarget.value)}
- placeholder="Choose a folder"
+ placeholder={t("scheduled.choose_folder")}
class="w-full rounded-xl border border-gray-6 bg-gray-2 px-3 py-2 text-sm text-gray-12 focus:outline-none focus:ring-1 focus:ring-blue-9/20 focus:border-blue-7"
/>
diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx
index f3a87c2a6..cc5735e13 100644
--- a/packages/app/src/app/pages/session.tsx
+++ b/packages/app/src/app/pages/session.tsx
@@ -1,5 +1,6 @@
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js";
import type { Agent, Part } from "@opencode-ai/sdk/v2/client";
+import { t } from "../../i18n";
import type {
ArtifactItem,
DashboardTab,
@@ -249,21 +250,21 @@ export default function SessionView(props: SessionViewProps) {
// navigation-only. Avoid showing any tab as "selected" to reduce confusion.
const showRightSidebarSelection = createMemo(() => !props.selectedSessionId);
- const agentLabel = createMemo(() => props.selectedSessionAgent ?? "Default agent");
+ const agentLabel = createMemo(() => props.selectedSessionAgent ?? t("session.default_agent"));
const workspaceLabel = (workspace: WorkspaceInfo) =>
workspace.displayName?.trim() ||
workspace.openworkWorkspaceName?.trim() ||
workspace.name?.trim() ||
workspace.path?.trim() ||
- "Worker";
+ t("dashboard.workspace_worker");
const workspaceKindLabel = (workspace: WorkspaceInfo) =>
workspace.workspaceType === "remote"
? workspace.sandboxBackend === "docker" ||
Boolean(workspace.sandboxRunId?.trim()) ||
Boolean(workspace.sandboxContainerName?.trim())
- ? "Sandbox"
- : "Remote"
- : "Local";
+ ? t("dashboard.workspace_sandbox")
+ : t("dashboard.workspace_remote")
+ : t("dashboard.workspace_local");
const todoList = createMemo(() => props.todos.filter((todo) => todo.content.trim()));
const todoCount = createMemo(() => todoList().length);
const todoCompletedCount = createMemo(() =>
@@ -346,7 +347,7 @@ export default function SessionView(props: SessionViewProps) {
const activeSearchPositionLabel = createMemo(() => {
const hits = searchHits();
- if (!hits.length) return "No matches";
+ if (!hits.length) return t("session.search_no_matches");
const size = hits.length;
const raw = activeSearchHitIndex();
const index = ((raw % size) + size) % size;
@@ -459,11 +460,11 @@ export default function SessionView(props: SessionViewProps) {
const openMarkdownEditor = (file: string) => {
const relative = toWorkspaceRelativeForApi(file);
if (!relative) {
- setToastMessage("Only worker-relative files can be opened here.");
+ setToastMessage(t("session.toast_relative_only"));
return;
}
if (!/\.(md|mdx|markdown)$/i.test(relative)) {
- setToastMessage("Only markdown files can be edited here right now.");
+ setToastMessage(t("session.toast_markdown_only"));
return;
}
setMarkdownEditorPath(relative);
@@ -477,7 +478,9 @@ export default function SessionView(props: SessionViewProps) {
const todoLabel = createMemo(() => {
const total = todoCount();
if (!total) return "";
- return `${todoCompletedCount()} out of ${total} tasks completed`;
+ return t("session.tasks_progress")
+ .replace("{completed}", String(todoCompletedCount()))
+ .replace("{total}", String(total));
});
const MAX_SESSIONS_PREVIEW = 3;
const COLLAPSED_SESSIONS_PREVIEW = 1;
@@ -540,7 +543,9 @@ export default function SessionView(props: SessionViewProps) {
const showMoreLabel = (workspaceId: string, total: number) => {
const remaining = Math.max(0, total - previewCount(workspaceId));
const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining);
- return nextCount > 0 ? `Show ${nextCount} more` : "Show more";
+ return nextCount > 0
+ ? t("dashboard.show_more_count").replace("{count}", String(nextCount))
+ : t("dashboard.show_more");
};
const [workspaceMenuId, setWorkspaceMenuId] = createSignal(null);
let workspaceMenuRef: HTMLDivElement | undefined;
@@ -565,9 +570,9 @@ export default function SessionView(props: SessionViewProps) {
const attachmentsDisabledReason = createMemo(() => {
if (attachmentsEnabled()) return null;
if (props.openworkServerStatus === "limited") {
- return "Add a server token to attach files.";
+ return t("session.attach_token_hint");
}
- return "Connect to OpenWork server to attach files.";
+ return t("session.attach_connect_hint");
});
createEffect(() => {
@@ -652,12 +657,12 @@ export default function SessionView(props: SessionViewProps) {
if (!trimmed) return;
if (props.activeWorkspaceDisplay.workspaceType === "remote") {
- setToastMessage("File open is unavailable for remote workers.");
+ setToastMessage(t("session.toast_file_open_unavailable"));
return;
}
if (!isTauriRuntime()) {
- setToastMessage("File open is available in the desktop app.");
+ setToastMessage(t("session.toast_file_open_desktop"));
return;
}
@@ -665,13 +670,13 @@ export default function SessionView(props: SessionViewProps) {
const { openPath } = await import("@tauri-apps/plugin-opener");
const root = props.activeWorkspaceRoot.trim();
if (!isAbsolutePath(trimmed) && !root) {
- setToastMessage("Pick a worker to open files.");
+ setToastMessage(t("session.toast_pick_worker"));
return;
}
const target = !isAbsolutePath(trimmed) && root ? await join(root, trimmed) : trimmed;
await openPath(target);
} catch (error) {
- const message = error instanceof Error ? error.message : "Unable to open file";
+ const message = error instanceof Error ? error.message : t("session.toast_open_failed");
setToastMessage(message);
}
};
@@ -688,7 +693,7 @@ export default function SessionView(props: SessionViewProps) {
setAgentPickerReady(true);
return sorted;
} catch (error) {
- const message = error instanceof Error ? error.message : "Failed to load agents";
+ const message = error instanceof Error ? error.message : t("session.toast_load_agents_failed");
setAgentPickerError(message);
setAgentOptions([]);
return [];
@@ -796,36 +801,36 @@ export default function SessionView(props: SessionViewProps) {
const tool = typeof record.tool === "string" ? record.tool : "";
switch (tool) {
case "task":
- return "Delegating";
+ return t("session.status_delegating");
case "todowrite":
case "todoread":
- return "Planning";
+ return t("session.status_planning");
case "read":
- return "Gathering context";
+ return t("session.status_gathering_context");
case "list":
case "grep":
case "glob":
- return "Searching codebase";
+ return t("session.status_searching_codebase");
case "webfetch":
- return "Searching the web";
+ return t("session.status_searching_web");
case "edit":
case "write":
case "apply_patch":
- return "Writing file";
+ return t("session.status_writing_file");
case "bash":
- return "Running shell";
+ return t("session.status_running_shell");
default:
- return "Working";
+ return t("session.status_working");
}
}
if (part.type === "reasoning") {
const text = typeof (part as any).text === "string" ? (part as any).text : "";
const match = text.trimStart().match(/^\*\*(.+?)\*\*/);
- if (match) return `Thinking about ${match[1].trim()}`;
- return "Thinking";
+ if (match) return t("session.status_thinking_about").replace("{subject}", match[1].trim());
+ return t("session.status_thinking");
}
if (part.type === "text") {
- return "Gathering thoughts";
+ return t("session.status_gathering_thoughts");
}
return null;
};
@@ -851,7 +856,7 @@ export default function SessionView(props: SessionViewProps) {
const record = part as any;
const state = record.state ?? {};
const title =
- typeof state.title === "string" && state.title.trim() ? state.title.trim() : String(record.tool ?? "Tool");
+ typeof state.title === "string" && state.title.trim() ? state.title.trim() : String(record.tool ?? t("session.status_tool"));
const output = typeof state.output === "string" ? truncateDetail(state.output) : null;
const error = typeof state.error === "string" ? truncateDetail(state.error) : null;
return { title, detail: output ?? error ?? undefined };
@@ -859,12 +864,12 @@ export default function SessionView(props: SessionViewProps) {
if (part.type === "reasoning") {
const text = typeof (part as any).text === "string" ? (part as any).text : "";
const detail = truncateDetail(text);
- return detail ? { title: "Reasoning", detail } : { title: "Reasoning" };
+ return detail ? { title: t("session.status_reasoning"), detail } : { title: t("session.status_reasoning") };
}
if (part.type === "text") {
const text = typeof (part as any).text === "string" ? (part as any).text : "";
const detail = truncateDetail(text);
- return detail ? { title: "Draft", detail } : { title: "Draft" };
+ return detail ? { title: t("session.status_draft"), detail } : { title: t("session.status_draft") };
}
return null;
});
@@ -872,15 +877,15 @@ export default function SessionView(props: SessionViewProps) {
const runLabel = createMemo(() => {
switch (runPhase()) {
case "sending":
- return "Sending";
+ return t("session.run_status_sending");
case "retrying":
- return "Retrying";
+ return t("session.run_status_retrying");
case "responding":
- return "Responding";
+ return t("session.run_status_responding");
case "thinking":
- return "Thinking";
+ return t("session.run_status_thinking");
case "error":
- return "Run failed";
+ return t("session.run_status_failed");
default:
return "";
}
@@ -1067,15 +1072,15 @@ export default function SessionView(props: SessionViewProps) {
const cancelRun = async () => {
if (abortBusy()) return;
if (!props.selectedSessionId) {
- setToastMessage("No session selected");
+ setToastMessage(t("session.toast_no_session"));
return;
}
setAbortBusy(true);
- setToastMessage("Stopping the run...");
+ setToastMessage(t("session.toast_stopping"));
try {
await props.abortSession(props.selectedSessionId);
- setToastMessage("Stopped.");
+ setToastMessage(t("session.toast_stopped"));
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to stop";
setToastMessage(message);
@@ -1087,13 +1092,13 @@ export default function SessionView(props: SessionViewProps) {
const retryRun = async () => {
const text = props.lastPromptSent.trim();
if (!text) {
- setToastMessage("Nothing to retry yet");
+ setToastMessage(t("session.toast_no_retry"));
return;
}
if (abortBusy()) return;
setAbortBusy(true);
- setToastMessage("Trying again...");
+ setToastMessage(t("session.toast_retrying"));
try {
if (showRunIndicator() && props.selectedSessionId) {
await props.abortSession(props.selectedSessionId);
@@ -1135,17 +1140,17 @@ export default function SessionView(props: SessionViewProps) {
const undoLastMessage = async () => {
if (historyActionBusy()) return;
if (!canUndoLastMessage()) {
- setToastMessage("Nothing to undo yet.");
+ setToastMessage(t("session.toast_no_undo"));
return;
}
setHistoryActionBusy("undo");
try {
await props.undoLastUserMessage();
- setToastMessage("Reverted the last user message.");
+ setToastMessage(t("session.toast_undo_success"));
} catch (error) {
const message = error instanceof Error ? error.message : props.safeStringify(error);
- setToastMessage(message || "Failed to undo");
+ setToastMessage(message || t("session.toast_undo_failed"));
} finally {
setHistoryActionBusy(null);
}
@@ -1154,17 +1159,17 @@ export default function SessionView(props: SessionViewProps) {
const redoLastMessage = async () => {
if (historyActionBusy()) return;
if (!canRedoLastMessage()) {
- setToastMessage("Nothing to redo.");
+ setToastMessage(t("session.toast_no_redo"));
return;
}
setHistoryActionBusy("redo");
try {
await props.redoLastUserMessage();
- setToastMessage("Restored the reverted message.");
+ setToastMessage(t("session.toast_redo_success"));
} catch (error) {
const message = error instanceof Error ? error.message : props.safeStringify(error);
- setToastMessage(message || "Failed to redo");
+ setToastMessage(message || t("session.toast_redo_failed"));
} finally {
setHistoryActionBusy(null);
}
@@ -1206,7 +1211,7 @@ export default function SessionView(props: SessionViewProps) {
const prev = prevTodoCount();
if (count > prev && prev > 0) {
const lastMsg = chatContainerEl?.querySelector('[data-message-role="assistant"]:last-child');
- triggerFlyout(lastMsg ?? null, "sidebar-progress", "New Task", "check");
+ triggerFlyout(lastMsg ?? null, "sidebar-progress", t("session.flyout_new_task"), "check");
}
setPrevTodoCount(count);
});
@@ -1217,7 +1222,7 @@ export default function SessionView(props: SessionViewProps) {
const prev = prevFileCount();
if (count > prev && prev > 0) {
const lastMsg = chatContainerEl?.querySelector('[data-message-role="assistant"]:last-child');
- triggerFlyout(lastMsg ?? null, "sidebar-context", "File Modified", "folder");
+ triggerFlyout(lastMsg ?? null, "sidebar-context", t("session.flyout_file_modified"), "folder");
}
setPrevFileCount(count);
});
@@ -1248,7 +1253,7 @@ export default function SessionView(props: SessionViewProps) {
const openRenameModal = () => {
setSessionMenuOpen(false);
if (!props.selectedSessionId) {
- setToastMessage("No session selected");
+ setToastMessage(t("session.toast_no_session"));
return;
}
setRenameTitle(selectedSessionTitle());
@@ -1280,7 +1285,7 @@ export default function SessionView(props: SessionViewProps) {
const openDeleteSessionModal = () => {
setSessionMenuOpen(false);
if (!props.selectedSessionId) {
- setToastMessage("No session selected");
+ setToastMessage(t("session.toast_no_session"));
return;
}
setDeleteSessionOpen(true);
@@ -1299,12 +1304,12 @@ export default function SessionView(props: SessionViewProps) {
try {
await props.deleteSession(sessionId);
setDeleteSessionOpen(false);
- setToastMessage("Session deleted");
+ setToastMessage(t("session.toast_session_deleted"));
// Route away from the deleted session id.
props.setView("session");
} catch (error) {
const message = error instanceof Error ? error.message : props.safeStringify(error);
- setToastMessage(message || "Failed to delete session");
+ setToastMessage(message || t("session.toast_delete_failed"));
} finally {
setDeleteSessionBusy(false);
}
@@ -1313,7 +1318,7 @@ export default function SessionView(props: SessionViewProps) {
const requireSessionId = () => {
const sessionId = props.selectedSessionId;
if (!sessionId) {
- setToastMessage("No session selected");
+ setToastMessage(t("session.toast_no_session"));
return null;
}
return sessionId;
@@ -1363,10 +1368,10 @@ export default function SessionView(props: SessionViewProps) {
setProviderAuthActionBusy(true);
try {
const message = await props.startProviderAuth(providerId);
- setToastMessage(message || "Auth flow started");
+ setToastMessage(message || t("session.toast_auth_started"));
props.closeProviderAuthModal();
} catch (error) {
- const message = error instanceof Error ? error.message : "Auth failed";
+ const message = error instanceof Error ? error.message : t("session.toast_auth_failed");
setToastMessage(message);
} finally {
setProviderAuthActionBusy(false);
@@ -1378,10 +1383,10 @@ export default function SessionView(props: SessionViewProps) {
setProviderAuthActionBusy(true);
try {
const message = await props.submitProviderApiKey(providerId, apiKey);
- setToastMessage(message || "API key saved");
+ setToastMessage(message || t("session.toast_api_key_saved"));
props.closeProviderAuthModal();
} catch (error) {
- const message = error instanceof Error ? error.message : "Failed to save API key";
+ const message = error instanceof Error ? error.message : t("session.toast_api_key_failed");
setToastMessage(message);
} finally {
setProviderAuthActionBusy(false);
@@ -1536,17 +1541,17 @@ export default function SessionView(props: SessionViewProps) {
const ws = shareWorkspace();
if (!ws) return null;
if (ws.workspaceType === "local" && props.engineInfo?.runtime === "direct") {
- return "Engine runtime is set to Direct. Switching local workers can restart the host and disconnect clients. The token may change after a restart.";
+ return t("dashboard.share_direct_warning");
}
return null;
});
const exportDisabledReason = createMemo(() => {
const ws = shareWorkspace();
- if (!ws) return "Export is available for local workers in the desktop app.";
- if (ws.workspaceType === "remote") return "Export is only supported for local workers.";
- if (!isTauriRuntime()) return "Export is available in the desktop app.";
- if (props.exportWorkspaceBusy) return "Export is already running.";
+ if (!ws) return t("dashboard.export_local_desktop");
+ if (ws.workspaceType === "remote") return t("dashboard.export_local_only");
+ if (!isTauriRuntime()) return t("dashboard.export_desktop_only");
+ if (props.exportWorkspaceBusy) return t("dashboard.export_running");
return null;
});
@@ -1623,22 +1628,22 @@ export default function SessionView(props: SessionViewProps) {
const client = props.openworkServerClient;
const workspaceId = props.openworkServerWorkspaceId?.trim() ?? "";
if (!client || !workspaceId) {
- setToastMessage("Connect to the OpenWork server to upload inbox files.");
+ setToastMessage(t("session.toast_upload_connect"));
return;
}
if (!files.length) return;
const label = files.length === 1 ? files[0]?.name ?? "file" : `${files.length} files`;
- setToastMessage(`Uploading ${label} to inbox...`);
+ setToastMessage(t("session.toast_uploading").replace("{label}", label));
try {
for (const file of files) {
await client.uploadInbox(workspaceId, file);
}
const summary = files.map((file) => file.name).filter(Boolean).join(", ");
- setToastMessage(summary ? `Uploaded to inbox: ${summary}` : "Uploaded to inbox.");
+ setToastMessage(summary ? t("session.toast_uploaded_summary").replace("{summary}", summary) : t("session.toast_uploaded"));
} catch (error) {
- const message = error instanceof Error ? error.message : "Inbox upload failed";
+ const message = error instanceof Error ? error.message : t("session.toast_upload_failed");
setToastMessage(message);
}
};
@@ -1784,7 +1789,7 @@ export default function SessionView(props: SessionViewProps) {
- Tasks
+ {t("dashboard.tasks_header")}
@@ -1841,9 +1846,9 @@ export default function SessionView(props: SessionViewProps) {
- Error
+ {t("dashboard.tasks_error_badge")}
@@ -1859,7 +1864,7 @@ export default function SessionView(props: SessionViewProps) {
createTaskInWorkspace(workspace().id);
}}
disabled={props.newTaskDisabled}
- aria-label="New task"
+ aria-label={t("dashboard.tasks_new")}
>
@@ -1891,7 +1896,7 @@ export default function SessionView(props: SessionViewProps) {
setWorkspaceMenuId(null);
}}
>
- Edit name
+ {t("dashboard.menu_edit_name")}
- Share...
+ {t("dashboard.menu_share")}
- Test connection
+ {t("dashboard.menu_test_connection")}
- Edit connection
+ {t("dashboard.menu_edit_connection")}
- Remove workspace
+ {t("dashboard.menu_remove_worker")}
@@ -1990,9 +1995,9 @@ export default function SessionView(props: SessionViewProps) {
- Failed to load tasks
+ {t("dashboard.tasks_error")}
}
@@ -2037,8 +2042,8 @@ export default function SessionView(props: SessionViewProps) {
onClick={() => createTaskInWorkspace(workspace().id)}
disabled={props.newTaskDisabled}
>
- No tasks yet.
- + New task
+ {t("dashboard.tasks_empty")}
+ {t("dashboard.tasks_new")}
@@ -2055,7 +2060,7 @@ export default function SessionView(props: SessionViewProps) {
}
>
- Loading tasks...
+ {t("dashboard.loading_tasks")}
@@ -2073,7 +2078,7 @@ export default function SessionView(props: SessionViewProps) {
onClick={() => setAddWorkspaceMenuOpen((prev) => !prev)}
>
- Add a worker
+ {t("dashboard.add_worker")}
@@ -2086,7 +2091,7 @@ export default function SessionView(props: SessionViewProps) {
}}
>
- New worker
+ {t("dashboard.new_worker")}
- Connect remote
+ {t("dashboard.connect_remote")}
- Import config
+ {t("dashboard.import_config")}
@@ -2146,7 +2151,7 @@ export default function SessionView(props: SessionViewProps) {
- {selectedSessionTitle() || "New task"}
+ {selectedSessionTitle() || t("session.new_task")}
{props.headerStatus}
@@ -2170,8 +2175,8 @@ export default function SessionView(props: SessionViewProps) {
}
openSearch();
}}
- title="Search conversation (Ctrl/Cmd+F)"
- aria-label="Search conversation"
+ title={t("session.search_tooltip")}
+ aria-label={t("session.search_aria")}
>
@@ -2180,8 +2185,8 @@ export default function SessionView(props: SessionViewProps) {
class="h-9 w-9 flex items-center justify-center rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
onClick={undoLastMessage}
disabled={!canUndoLastMessage() || historyActionBusy() !== null}
- title="Undo last message"
- aria-label="Undo last message"
+ title={t("session.undo_tooltip")}
+ aria-label={t("session.undo_tooltip")}
>
}>
@@ -2192,8 +2197,8 @@ export default function SessionView(props: SessionViewProps) {
class="h-9 w-9 flex items-center justify-center rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
onClick={redoLastMessage}
disabled={!canRedoLastMessage() || historyActionBusy() !== null}
- title="Redo last reverted message"
- aria-label="Redo last reverted message"
+ title={t("session.redo_tooltip")}
+ aria-label={t("session.redo_tooltip")}
>
}>
@@ -2204,8 +2209,8 @@ export default function SessionView(props: SessionViewProps) {
type="button"
class="h-9 w-9 flex items-center justify-center rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
disabled={!props.selectedSessionId}
- title={props.selectedSessionId ? "Session actions" : "Select a session to manage it"}
- aria-label={props.selectedSessionId ? "Session actions" : "Select a session to manage it"}
+ title={props.selectedSessionId ? t("session.actions_tooltip") : t("session.select_session_tooltip")}
+ aria-label={props.selectedSessionId ? t("session.actions_tooltip") : t("session.select_session_tooltip")}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -2225,14 +2230,14 @@ export default function SessionView(props: SessionViewProps) {
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover"
onClick={openRenameModal}
>
- Rename session
+ {t("session.rename_title")}
- Delete session
+ {t("session.delete_title")}
@@ -2264,8 +2269,8 @@ export default function SessionView(props: SessionViewProps) {
}
}}
class="min-w-0 flex-1 bg-transparent text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none"
- placeholder="Search in this chat"
- aria-label="Search in this chat"
+ placeholder={t("session.search_placeholder")}
+ aria-label={t("session.search_placeholder")}
/>
{activeSearchPositionLabel()}
moveSearchHit(-1)}
aria-label="Previous match"
>
- Prev
+ {t("session.search_prev")}
moveSearchHit(1)}
aria-label="Next match"
>
- Next
+ {t("session.search_next")}
- What do you want to do?
+ {t("session.hero_title")}
- Pick a starting point or just type below.
+ {t("session.hero_description")}
@@ -2332,9 +2337,9 @@ export default function SessionView(props: SessionViewProps) {
void handleBrowserAutomationQuickstart();
}}
>
- Automate your browser
+ {t("dashboard.quick_start_browser")}
- Set up browser actions and run reliable web tasks from OpenWork.
+ {t("dashboard.quick_start_browser_desc")}
- Give me a soul
+ {t("dashboard.quick_start_soul")}
- Keep your goals and preferences across sessions with light scheduled check-ins.
- Tradeoff: more autonomy can create extra background runs, but revert is one command.
+ {t("dashboard.quick_start_soul_desc")}
+ {t("dashboard.quick_start_soul_note")}
@@ -2364,8 +2369,9 @@ export default function SessionView(props: SessionViewProps) {
setMessageWindowStart(0);
}}
>
- Show {hiddenMessageCount().toLocaleString()} earlier message
- {hiddenMessageCount() === 1 ? "" : "s"}
+ {t("session.show_earlier_messages")
+ .replace("{count}", hiddenMessageCount().toLocaleString())
+ .replace("{plural}", hiddenMessageCount() === 1 ? "" : "s")}
@@ -2414,7 +2420,7 @@ export default function SessionView(props: SessionViewProps) {
class="pointer-events-auto rounded-full border border-gray-6 bg-gray-1/90 px-4 py-2 text-xs text-gray-11 shadow-lg shadow-gray-12/5 backdrop-blur-md hover:bg-gray-2 transition-colors"
onClick={() => scrollToLatest("smooth")}
>
- Jump to latest
+ {t("session.jump_to_latest")}
@@ -2568,7 +2574,7 @@ export default function SessionView(props: SessionViewProps) {
}}
>
- Automations
+ {t("dashboard.tab_automations")}
- Skills
+ {t("dashboard.tab_skills")}
- Extensions
+ {t("dashboard.tab_extensions")}
- Identities
+ {t("dashboard.tab_identities")}
- Advanced
+ {t("dashboard.tab_advanced")}
@@ -2670,14 +2676,14 @@ export default function SessionView(props: SessionViewProps) {
- Permission Required
- OpenCode is requesting permission to continue.
+ {t("session.permission_required")}
+ {t("session.permission_description")}
{props.safeStringify(props.activePermission?.metadata)}
@@ -2747,7 +2753,7 @@ export default function SessionView(props: SessionViewProps) {
disabled={props.permissionReplyBusy}
>
- Deny
+ {t("session.deny")}
- This clears your saved preference and shows the connection choice on next launch. + {tr("settings.reset_startup_hint")}
{engineStdout()}
{engineStderr()}
@@ -1309,8 +1313,8 @@ export default function SettingsView(props: SettingsViewProps) {
{props.orchestratorStatus?.lastError}
@@ -1352,8 +1356,8 @@ export default function SettingsView(props: SettingsViewProps) {
{props.opencodeConnectStatus?.error}
@@ -1409,8 +1413,8 @@ export default function SettingsView(props: SettingsViewProps) {
{openworkStdout()}
{openworkStderr()}
@@ -1441,8 +1445,8 @@ export default function SettingsView(props: SettingsViewProps) {
{opencodeRouterStdout()}
{opencodeRouterStderr()}
@@ -1502,27 +1506,27 @@ export default function SettingsView(props: SettingsViewProps) {
{props.safeStringify(props.pendingPermissions)}
{props.safeStringify(props.events)}
@@ -1592,14 +1596,14 @@ export default function SettingsView(props: SettingsViewProps) {
@@ -1609,7 +1613,7 @@ export default function SettingsView(props: SettingsViewProps) {
- Audit log
+ {tr("settings.devtools.audit_log")}
{openworkAuditStatusLabel()}
@@ -1619,7 +1623,7 @@ export default function SettingsView(props: SettingsViewProps) {
0}
- fallback={No audit entries yet.}
+ fallback={{tr("settings.devtools.no_audit_entries")}}
>
@@ -1648,4 +1652,4 @@ export default function SettingsView(props: SettingsViewProps) {
);
-}
+}
\ No newline at end of file
diff --git a/packages/app/src/app/pages/skills.tsx b/packages/app/src/app/pages/skills.tsx
index b16e51f0f..aae69cfa4 100644
--- a/packages/app/src/app/pages/skills.tsx
+++ b/packages/app/src/app/pages/skills.tsx
@@ -491,10 +491,10 @@ export default function SkillsView(props: SkillsViewProps) {
- Worker profile
+ {translate("skills.worker_profile")}
{workspaceLabel()}
- Skills are the core abilities of this worker. Add from Hub or create new ones directly in chat.
+ {translate("skills.worker_profile_desc")}
- Create skill in chat
+ {translate("skills.create_in_chat")}
- Installed
+ {translate("skills.stat_installed")}
{props.skills.length}
- Hub available
+ {translate("skills.stat_hub_available")}
{availableHubSkills().length}
- Skill creator
+ {translate("skills.stat_skill_creator")}
- {skillCreatorInstalled() ? "Installed" : "Not installed"}
+ {skillCreatorInstalled() ? translate("skills.stat_installed") : translate("skills.stat_not_installed")}
- Mode
+ {translate("skills.stat_mode")}
- {props.canUseDesktopTools ? "Local" : "Server"}
+ {props.canUseDesktopTools ? translate("skills.mode_local") : translate("skills.mode_server")}
@@ -556,7 +556,7 @@ export default function SkillsView(props: SkillsViewProps) {
type="text"
value={searchQuery()}
onInput={(event) => setSearchQuery(event.currentTarget.value)}
- placeholder="Search installed or hub skills"
+ placeholder={translate("skills.search_placeholder")}
class="bg-dls-hover border border-dls-border rounded-lg py-1.5 pl-9 pr-4 text-xs w-56 focus:w-72 focus:outline-none transition-all"
/>
@@ -571,7 +571,7 @@ export default function SkillsView(props: SkillsViewProps) {
}`}
>
- New skill
+ {translate("skills.new_skill")}
- Install from link
+ {translate("skills.install_link")}
@@ -663,7 +663,7 @@ export default function SkillsView(props: SkillsViewProps) {
openShareLink(skill);
}}
disabled={props.busy}
- title="Share link"
+ title={translate("skills.share_link_title")}
>
@@ -676,7 +676,7 @@ export default function SkillsView(props: SkillsViewProps) {
void openSkill(skill);
}}
disabled={props.busy}
- title="Edit"
+ title={translate("skills.edit")}
>
@@ -708,7 +708,7 @@ export default function SkillsView(props: SkillsViewProps) {
- Install skills
+ {translate("skills.install_title")}
props.refreshHubSkills({ force: true })}
@@ -718,10 +718,10 @@ export default function SkillsView(props: SkillsViewProps) {
? "text-dls-secondary"
: "text-dls-secondary hover:text-dls-text"
}`}
- title="Refresh hub catalog"
+ title={translate("skills.refresh_hub_hint")}
>
- Refresh hub
+ {translate("skills.refresh_hub")}
@@ -735,7 +735,7 @@ export default function SkillsView(props: SkillsViewProps) {
when={filteredHubSkills().length}
fallback={
- No hub skills available.
+ {translate("skills.no_hub_skills")}
}
>
@@ -751,7 +751,7 @@ export default function SkillsView(props: SkillsViewProps) {
{skill.name}
- From openwork-hub}>
+ {translate("skills.from_hub")}}>
{skill.description}
@@ -760,7 +760,7 @@ export default function SkillsView(props: SkillsViewProps) {
- Trigger: {skill.trigger}
+ {translate("skills.trigger_label").replace("{trigger}", skill.trigger ?? "")}
@@ -779,7 +779,7 @@ export default function SkillsView(props: SkillsViewProps) {
void installFromHub(skill);
}}
disabled={props.busy || installingHubSkill() === skill.name}
- title={`Install ${skill.name}`}
+ title={`${translate("skills.install_btn")} ${skill.name}`}
>
- {installingHubSkill() === skill.name ? "Installing" : "Add"}
+ {installingHubSkill() === skill.name ? translate("skills.installing") : translate("skills.add")}
)}
@@ -797,7 +797,7 @@ export default function SkillsView(props: SkillsViewProps) {
- Capability setup
+ {translate("skills.capability_setup")}
{(item) => (
@@ -896,14 +896,14 @@ export default function SkillsView(props: SkillsViewProps) {
disabled={!selectedDirty() || props.busy}
onClick={() => void saveSelectedSkill()}
>
- Save
+ {translate("skills.save")}
- Close
+ {translate("skills.close")}
@@ -916,7 +916,7 @@ export default function SkillsView(props: SkillsViewProps) {
Loading… }
+ fallback={{translate("skills.loading")}}
>
Paste a skill bundle URL, preview it, then install.
+{translate("skills.install_link_desc")}
openwork).",
+ "identities.workspace_id_required": "Workspace ID is required to manage identities. Reconnect with a workspace URL (for example: /w/<workspace-id>) or select a workspace mapped on this host.",
+ "identities.tab_general": "General",
+ "identities.tab_advanced": "Advanced",
+ "identities.worker_online": "Worker online",
+ "identities.worker_unavailable": "Worker unavailable",
+ "identities.worker_offline": "Worker offline",
+ "identities.status_channels": "Channels",
+ "identities.status_messages_today": "Messages today",
+ "identities.status_last_activity": "Last activity",
+ "identities.available_channels": "Available channels",
+ "identities.telegram": "Telegram",
+ "identities.telegram_description": "Create a Telegram bot that anyone can message. Great for personal automations and external contacts.",
+ "identities.slack": "Slack",
+ "identities.slack_description": "Your worker appears as a bot in Slack channels. Team members can message it directly or mention it in threads.",
+ "identities.connected": "Connected",
+ "identities.status_active": "Active",
+ "identities.status_stopped": "Stopped",
+ "identities.configured": "configured",
+ "identities.channel_on": "On",
+ "identities.channel_off": "Off",
+ "identities.quick_setup": "Quick setup",
+ "identities.telegram_step1": "Open @BotFather and run /newbot.",
+ "identities.telegram_step2": "Copy the bot token and paste it below.",
+ "identities.telegram_step3": "Connect, then send /start to your bot to activate the chat.",
+ "identities.bot_token": "Bot token",
+ "identities.telegram_token_placeholder": "Paste Telegram bot token from @BotFather",
+ "identities.enabled": "Enabled",
+ "identities.connect_telegram": "Connect Telegram",
+ "identities.connecting": "Connecting...",
+ "identities.open_telegram_bot": "Open @{username} in Telegram",
+ "identities.slack_hint": "Connect your Slack workspace to let team members interact with this worker in channels and DMs.",
+ "identities.app_token": "App token",
+ "identities.connect_slack": "Connect Slack",
+ "identities.message_routing": "Message routing",
+ "identities.message_routing_description": "Control which conversations go to which workspace folder. Messages are routed to the worker's default folder unless you set up rules here.",
+ "identities.default_routing": "Default routing",
+ "identities.all_channels": "All channels",
+ "identities.routing_advanced_hint": "Advanced: reply with /dir <path> in Slack/Telegram to override the directory for a specific chat (limited to this workspace root).",
+ "identities.agent_behavior": "Messaging agent behavior",
+ "identities.agent_behavior_hint": "One file per workspace. Add optional first line @agent <id> to route via a specific OpenCode agent.",
+ "identities.active_scope": "Active scope: workspace · status: {loaded} · selected agent: {selected}",
+ "identities.loading_agent": "Loading agent file...",
+ "identities.agent_not_found": "Agent file not found in this workspace yet.",
+ "identities.agent_placeholder": "Add messaging behavior instructions for opencodeRouter here...",
+ "identities.reload": "Reload",
+ "identities.create_default_file": "Create default file",
+ "identities.save_behavior": "Save behavior",
+ "identities.saving": "Saving...",
+ "identities.unsaved_changes": "Unsaved changes",
+ "identities.send_test_message": "Send test message",
+ "identities.send_test_hint": "Validate outbound wiring. Use a peer ID for direct send, or leave peer ID empty to fan out by bindings in a directory.",
+ "identities.peer_id": "Peer ID (optional)",
+ "identities.directory": "Directory (optional)",
+ "identities.auto_bind": "Auto-bind peer to directory on direct send",
+ "identities.message": "Message",
+ "identities.test_message_placeholder": "Test message content",
+ "identities.sending": "Sending...",
+ "identities.disconnect": "Disconnect",
+ "identities.status_label": "Status",
+ "identities.identities_label": "Identities",
+ "identities.status_unavailable": "Unavailable",
+ "identities.status_unknown": "Unknown",
+ "identities.status_running": "Running",
+ "identities.status_offline": "Offline",
+ "identities.time_just_now": "Just now",
+ "identities.time_minutes_ago": "{minutes}m ago",
+ "identities.time_hours_ago": "{hours}h ago",
+ "identities.time_days_ago": "{days}d ago",
+ "identities.msg_agent_created": "Created default messaging agent file.",
+ "identities.msg_agent_saved": "Saved messaging behavior.",
+ "identities.error_file_changed": "File changed remotely. Reload and save again.",
+ "identities.msg_dispatched": "Dispatched {sent}/{attempted} messages.",
+ "identities.error_worker_scope_unavailable": "Worker scope unavailable.",
+ "identities.error_worker_scope_unavailable_long": "Worker scope unavailable. Reconnect using a worker URL or switch to a known worker.",
+ "identities.error_reconnect_failed": "Reconnect failed. Check OpenWork URL/token and try again.",
+ "identities.msg_reconnected_refreshing": "Reconnected. Refreshing worker state...",
+ "identities.msg_reconnected": "Reconnected.",
+ "identities.msg_saved_pending": "Saved (pending apply).",
+ "identities.msg_saved": "Saved.",
+ "identities.error_save_failed": "Failed to save.",
+ "identities.msg_deleted_pending": "Deleted (pending apply).",
+ "identities.msg_deleted": "Deleted.",
+ "identities.error_delete_failed": "Failed to delete.",
+ "identities.msg_saved_username": "Saved (@{username})",
+ "identities.error_health_unavailable": "OpenCodeRouter health unavailable ({status})",
+ "identities.error_telegram_unavailable": "Telegram identities unavailable.",
+ "identities.error_slack_unavailable": "Slack identities unavailable.",
+
+ "scheduled.never": "Never",
+ "scheduled.custom_schedule": "Custom schedule",
+ "scheduled.every_hour": "Every hour",
+ "scheduled.every_n_hours": "Every {n} hours",
+ "scheduled.every_day_at": "Every day at {time}",
+ "scheduled.weekdays_at": "Weekdays at {time}",
+ "scheduled.weekends_at": "Weekends at {time}",
+ "scheduled.at_time": "At {time}",
+ "scheduled.days_at_time": "{days} at {time}",
+ "scheduled.day_sun": "Sun",
+ "scheduled.day_mon": "Mon",
+ "scheduled.day_tue": "Tue",
+ "scheduled.day_wed": "Wed",
+ "scheduled.day_thu": "Thu",
+ "scheduled.day_fri": "Fri",
+ "scheduled.day_sat": "Sat",
+ "scheduled.label_command": "Command",
+ "scheduled.label_prompt": "Prompt",
+ "scheduled.label_task": "Task",
+ "scheduled.no_prompt_command": "No prompt or command found.",
+ "scheduled.status_not_run": "Not run yet",
+ "scheduled.status_running": "Running",
+ "scheduled.status_success": "Success",
+ "scheduled.status_failed": "Failed",
+ "scheduled.template_daily_planning": "Daily planning brief",
+ "scheduled.template_daily_planning_desc": "Build a focused plan from your tasks and calendar.",
+ "scheduled.template_daily_planning_prompt": "Review my pending tasks and calendar, then draft a practical plan for today with top priorities and one follow-up reminder.",
+ "scheduled.template_inbox_zero": "Inbox zero helper",
+ "scheduled.template_inbox_zero_desc": "Summarize unread messages and draft short replies.",
+ "scheduled.template_inbox_zero_prompt": "Summarize unread inbox messages, suggest priority order, and draft concise reply options for the top conversations.",
+ "scheduled.template_meeting_prep": "Meeting prep notes",
+ "scheduled.template_meeting_prep_desc": "Generate prep bullets for tomorrow's meetings.",
+ "scheduled.template_meeting_prep_prompt": "Prepare meeting briefs for tomorrow with context, talking points, and questions to unblock decisions.",
+ "scheduled.template_weekly_wins": "Weekly wins recap",
+ "scheduled.template_weekly_wins_desc": "Create a Friday recap of wins, blockers, and next steps.",
+ "scheduled.template_weekly_wins_prompt": "Summarize the week into wins, blockers, and clear next steps I can share with the team.",
+ "scheduled.template_learning_digest": "Learning digest",
+ "scheduled.template_learning_digest_desc": "Turn saved links and notes into a weekly digest.",
+ "scheduled.template_learning_digest_prompt": "Collect my saved links and notes, then draft a weekly learning digest with key ideas and follow-up actions.",
+ "scheduled.template_habit_checkin": "Habit check-in",
+ "scheduled.template_habit_checkin_desc": "Run a quick accountability check through the day.",
+ "scheduled.template_habit_checkin_prompt": "Ask me for a quick progress check-in, capture blockers, and suggest one concrete next action.",
+ "scheduled.run": "Run",
+ "scheduled.delete": "Delete",
+ "scheduled.run_context": "Run context",
+ "scheduled.source": "Source: {source}",
+ "scheduled.last_run": "Last run {time}",
+ "scheduled.created": "Created {time}",
+ "scheduled.agent": "Agent {agent}",
+ "scheduled.model": "Model {model}",
+ "scheduled.server_unavailable": "OpenWork server unavailable. Connect to sync scheduled tasks.",
+ "scheduled.desktop_required": "Scheduled tasks require the desktop app.",
+ "scheduled.windows_not_supported": "Scheduler is not supported on Windows yet.",
+ "scheduled.desc_remote": "Automations that run on a schedule from the connected OpenWork server.",
+ "scheduled.desc_local": "Automations that run on a schedule from this device.",
+ "scheduled.from_server": "From OpenWork server",
+ "scheduled.from_local": "From local scheduler",
+ "scheduled.server": "OpenWork server",
+ "scheduled.local": "Local",
+ "scheduled.remote_instance": "Remote instance",
+ "scheduled.launchd_systemd": "Launchd or systemd",
+ "scheduled.server_unavailable_short": "OpenWork server unavailable",
+ "scheduled.desktop_only": "Desktop-only",
+ "scheduled.delete_desc_remote": "This removes the schedule and deletes the job definition from the connected OpenWork server.",
+ "scheduled.delete_desc_local": "This removes the schedule and deletes the job definition from your machine.",
+ "scheduled.not_synced": "Not synced yet",
+ "scheduled.delete_failed": "Failed to delete job.",
+ "scheduled.refreshing": "Refreshing",
+ "scheduled.refresh": "Refresh",
+ "scheduled.new_automation": "New automation",
+ "scheduled.automations": "Automations",
+ "scheduled.beta": "Beta",
+ "scheduled.no_automations": "No automations yet. Pick a template or create your own automation prompt.",
+ "scheduled.explore_more": "Explore more",
+ "scheduled.delete_confirm_title": "Delete automation?",
+ "scheduled.deleting": "Deleting",
+ "scheduled.create_modal_title": "Create automation",
+ "scheduled.create_modal_desc": "Automations are scheduled by running a prompt in a new thread. We’ll prefill a prompt for you to send.",
+ "scheduled.name": "Name",
+ "scheduled.projects": "Projects",
+ "scheduled.choose_folder": "Choose a folder",
+ "scheduled.schedule": "Schedule",
+ "scheduled.daily": "Daily",
+ "scheduled.interval": "Interval",
+ "scheduled.every": "Every",
+ "scheduled.hours": "hours",
+ "scheduled.view_docs": "View scheduler docs",
+ "scheduled.cancel": "Cancel",
+ "scheduled.create": "Create",
+ "scheduled.run_now_prompt": "Run this automation now: {name}.\nSchedule: {schedule}.",
+ "scheduled.run_command_prompt": "Run this automation now: {name}.\nSchedule: {schedule}.\n\nRun the following command:\n{cmd}{workdirHint}",
+ "scheduled.prompt_construct": "Schedule a job{name} with cron \"{schedule}\" to {prompt}{workdir}",
+ "scheduled.run_from": " Run from {workdir}.",
+ "scheduled.named": " named \"{name}\"",
+
+ // ==================== Settings New ====================
+ "settings.messaging.title": "Messaging",
+ "settings.messaging.description": "Manage Telegram/Slack identities and bindings in the",
+ "settings.messaging.identities_link": "Identities",
+ "settings.messaging.tab": "tab.",
+ "settings.providers.title": "Providers",
+ "settings.providers.description": "Connect services for models and tools.",
+ "settings.providers.connect": "Connect provider",
+ "settings.providers.loading": "Loading providers...",
+ "settings.providers.summary.unavailable": "Connect to OpenCode to load providers.",
+ "settings.providers.summary.available": "{available} available",
+ "settings.providers.summary.connected": "{connected} connected · {available} available",
+ "settings.providers.api_keys_hint": "API keys are stored locally by OpenCode. Use /models to pick a default.",
+ "settings.appearance.title": "Appearance",
+ "settings.appearance.description": "Customize window appearance.",
+ "settings.appearance.hide_titlebar": "Hide titlebar",
+ "settings.appearance.hide_titlebar_hint": "Hide the window titlebar. Useful for tiling window managers on Linux (Hyprland, i3, sway).",
+ "settings.devtools.title": "Devtools",
+ "settings.devtools.description": "Sidecar health, capabilities, and audit trail.",
+ "settings.devtools.versions": "Versions",
+ "settings.devtools.versions_description": "Sidecar + desktop build info.",
+ "settings.devtools.opencode_engine": "OpenCode engine",
+ "settings.devtools.opencode_engine_description": "Local execution sidecar.",
+ "settings.devtools.orchestrator": "Orchestrator daemon",
+ "settings.devtools.orchestrator_description": "Workspace orchestration layer.",
+ "settings.devtools.opencode_sdk": "OpenCode SDK",
+ "settings.devtools.opencode_sdk_description": "UI connection diagnostics.",
+ "settings.devtools.openwork_server": "OpenWork server",
+ "settings.devtools.openwork_server_description": "Config and approvals sidecar.",
+ "settings.devtools.opencode_router": "OpenCodeRouter sidecar",
+ "settings.devtools.opencode_router_description": "Messaging bridge service.",
+ "settings.devtools.diagnostics": "OpenWork server diagnostics",
+ "settings.devtools.capabilities": "OpenWork server capabilities",
+ "settings.devtools.audit_log": "Audit log",
+ "settings.devtools.no_audit_entries": "No audit entries yet.",
+ "settings.engine.title": "Engine",
+ "settings.engine.description": "Choose how OpenCode runs locally.",
+ "settings.engine.source": "Engine source",
+ "settings.engine.bundled": "Bundled (recommended)",
+ "settings.engine.system": "System install (PATH)",
+ "settings.engine.custom": "Custom binary",
+ "settings.engine.bundled_hint": "Bundled engine is the most reliable option. Use System install only if you manage OpenCode yourself.",
+ "settings.engine.custom_label": "Custom OpenCode binary",
+ "settings.engine.custom_choose": "Choose",
+ "settings.engine.custom_clear": "Clear",
+ "settings.engine.custom_hint": "Use this to point OpenWork at a local OpenCode build (e.g. your fork). Applies next time the engine starts or reloads.",
+ "settings.engine.runtime": "Engine runtime",
+ "settings.engine.runtime_direct": "Direct (OpenCode)",
+ "settings.engine.runtime_orchestrator": "OpenWork Orchestrator",
+ "settings.engine.runtime_hint": "Applies the next time the engine starts or reloads.",
+ "settings.reset_recovery.title": "Reset & Recovery",
+ "settings.reset_recovery.description": "Clear data or restart the setup flow.",
+ "settings.tab.general": "General",
+ "settings.tab.model": "Model",
+ "settings.tab.advanced": "Advanced",
+ "settings.tab.debug": "Debug",
+ "settings.stop_local_server": "Stop local server",
+ "settings.disconnect_server": "Disconnect server",
+ "settings.reconnect_server": "Reconnect server",
+ "settings.reconnecting": "Reconnecting...",
+ "settings.restart": "Restart",
+ "settings.restarting": "Restarting...",
+ "settings.stop": "Stop",
+ "settings.clear": "Clear",
+ "settings.last_stdout": "Last stdout",
+ "settings.last_stderr": "Last stderr",
+ "settings.last_error": "Last error",
+ "settings.last_attempt": "Last attempt: {time}",
+ "settings.reason": "Reason: {reason}",
+ "settings.metrics.healthy": "Healthy: {ms}ms",
+ "settings.metrics.load_sessions": "Load sessions: {ms}ms",
+ "settings.metrics.pending_permissions": "Pending permissions: {ms}ms",
+ "settings.metrics.providers": "Providers: {ms}ms",
+ "settings.metrics.total": "Total: {ms}ms",
+ "settings.diagnostics_unavailable": "Diagnostics unavailable.",
+ "settings.capabilities_unavailable": "Capabilities unavailable. Connect with a client token.",
+ "settings.started": "Started: {time}",
+ "settings.read_only": "Read-only: {value}",
+ "settings.approval": "Approval: {mode} ({ms}ms)",
+ "settings.workspaces_count": "Workspaces: {count}",
+ "settings.active_workspace": "Active workspace: {id}",
+ "settings.config_path": "Config path: {path}",
+ "settings.token_source_client": "Token source: {source}",
+ "settings.token_source_host": "Host token source: {source}",
+ "settings.updates_auto_download": "Auto-update",
+ "settings.updates_auto_download_hint": "Download updates automatically (prompts to restart)",
+ "settings.docker_containers": "OpenWork Docker containers",
+ "settings.docker_containers_hint": "Force-remove Docker containers launched by OpenWork (sandbox + local dev stacks).",
+ "settings.workspace_debug_events": "Workspace debug events",
+ "settings.delete_containers": "Delete containers",
+ "settings.removing_containers": "Removing containers...",
+
+ // ==================== Session New ====================
+ "session.search_tooltip": "Search conversation (Ctrl/Cmd+F)",
+ "session.search_aria": "Search conversation",
+ "session.undo_tooltip": "Undo last message",
+ "session.redo_tooltip": "Redo last reverted message",
+ "session.actions_tooltip": "Session actions",
+ "session.select_session_tooltip": "Select a session to manage it",
+ "session.delete_title": "Delete session",
+ "session.delete_modal_title": "Delete session?",
+ "session.delete_modal_message": "This will permanently delete \"{title}\" and its messages.",
+ "session.delete_modal_message_default": "This will permanently delete the selected session and its messages.",
+ "session.show_earlier_messages": "Show {count} earlier message{plural}",
+ "session.jump_to_latest": "Jump to latest",
+ "session.toast_no_undo": "Nothing to undo yet.",
+ "session.toast_undo_success": "Reverted the last user message.",
+ "session.toast_no_redo": "Nothing to redo.",
+ "session.toast_redo_success": "Restored the reverted message.",
+ "session.toast_undo_failed": "Failed to undo",
+ "session.toast_redo_failed": "Failed to redo",
+ "session.toast_stopping": "Stopping the run...",
+ "session.toast_stopped": "Stopped.",
+ "session.toast_retrying": "Trying again...",
+ "session.toast_no_retry": "Nothing to retry yet",
+ "session.toast_no_session": "No session selected",
+ "session.toast_session_deleted": "Session deleted",
+ "session.toast_delete_failed": "Failed to delete session",
+ "session.toast_file_open_unavailable": "File open is unavailable for remote workers.",
+ "session.toast_file_open_desktop": "File open is available in the desktop app.",
+ "session.toast_pick_worker": "Pick a worker to open files.",
+ "session.toast_auth_started": "Auth flow started",
+ "session.toast_auth_failed": "Auth failed",
+ "session.toast_api_key_saved": "API key saved",
+ "session.toast_api_key_failed": "Failed to save API key",
+ "session.toast_upload_connect": "Connect to the OpenWork server to upload inbox files.",
+ "session.toast_uploading": "Uploading {label} to inbox...",
+ "session.toast_uploaded": "Uploaded to inbox.",
+ "session.toast_uploaded_summary": "Uploaded to inbox: {summary}",
+ "session.toast_upload_failed": "Inbox upload failed",
+ "session.toast_open_failed": "Unable to open file",
+ "session.toast_load_agents_failed": "Failed to load agents",
+ "session.toast_relative_only": "Only worker-relative files can be opened here.",
+ "session.toast_markdown_only": "Only markdown files can be edited here right now.",
+ "session.toast_connect_failed": "Connect failed",
+ "session.run_status_sending": "Sending",
+ "session.run_status_retrying": "Retrying",
+ "session.run_status_responding": "Responding",
+ "session.run_status_thinking": "Thinking",
+ "session.run_status_failed": "Run failed",
+ "session.flyout_new_task": "New Task",
+ "session.flyout_file_modified": "File Modified",
+ "session.hero_title": "What do you want to do?",
+ "session.hero_description": "Pick a starting point or just type below.",
+ "session.search_placeholder": "Search in this chat",
+ "session.search_prev": "Prev",
+ "session.search_next": "Next",
+ "session.tasks_progress": "{completed} out of {total} tasks completed",
+ "session.attach_token_hint": "Add a server token to attach files.",
+ "session.attach_connect_hint": "Connect to OpenWork server to attach files.",
+ "session.status_delegating": "Delegating",
+ "session.status_planning": "Planning",
+ "session.status_gathering_context": "Gathering context",
+ "session.status_searching_codebase": "Searching codebase",
+ "session.status_searching_web": "Searching the web",
+ "session.status_writing_file": "Writing file",
+ "session.status_running_shell": "Running shell",
+ "session.status_working": "Working",
+ "session.status_thinking_about": "Thinking about {subject}",
+ "session.status_gathering_thoughts": "Gathering thoughts",
+ "session.status_tool": "Tool",
+ "session.status_reasoning": "Reasoning",
+ "session.status_draft": "Draft",
+
+ "dashboard.quick_start_browser": "Automate your browser",
+ "dashboard.quick_start_browser_desc": "Set up browser actions and run reliable web tasks from OpenWork.",
+ "dashboard.quick_start_soul": "Give me a soul",
+ "dashboard.quick_start_soul_desc": "Keep your goals and preferences across sessions with light scheduled check-ins.",
+ "dashboard.quick_start_soul_note": "Tradeoff: more autonomy can create extra background runs, but revert is one command.",
+ "session.search_no_matches": "No matches",
+ "session.composer_loading_commands": "Loading commands...",
+ "session.composer_no_commands": "No commands found.",
+ "session.attachment_image": "Image",
+ "session.attachment_file": "File",
+ "session.upload_to_inbox": "Upload to inbox",
+ "session.composer_remote_workspace": "Remote workspace",
+ "session.attachments_unavailable": "Attachments are unavailable.",
+ "session.attach_files": "Attach files",
+ "session.agent_label": "Agent",
+ "session.loading_agents": "Loading agents...",
+ "session.default_agent": "Default agent",
+ "session.thinking_effort": "Thinking effort",
+ "session.active_variant": "Active",
+ "session.send_button": "Send",
+ "session.stop_button": "Stop",
+ "session.toast_unsupported_type": "Unsupported attachment type.",
+ "session.toast_file_too_large": "{name} exceeds the 8MB limit.",
+ "session.toast_image_too_large": "{name} is too large after encoding. Try a smaller image.",
+ "session.toast_read_failed": "Failed to read attachment",
+ "session.toast_remote_drop_hint": "This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Inbox in the sidebar.",
+ "session.show_steps": "Show {count} step{plural}",
+ "session.step_running": "running",
+ "session.copy_message": "Copy message",
+ "session.skill_badge": "skill",
+ "session.artifacts_title": "Artifacts",
+ "session.artifacts_empty": "No artifacts yet.",
+ "session.artifacts_image_preview_soon": "(image preview coming soon)",
+ "session.artifacts_show_fewer": "Show fewer",
+ "session.artifacts_show_more": "Show {count} more",
+ // Context Panel
+ "session.context_title": "Context",
+ "session.working_files_empty": "None yet.",
+ "session.plugins_title": "Plugins",
+ "session.plugins_empty": "No plugins loaded.",
+ "session.mcp_title": "MCP",
+ "session.mcp_empty": "No MCP servers loaded.",
+ "session.mcp_status_connected": "Connected",
+ "session.mcp_status_disconnected": "Disconnected",
+ "session.mcp_status_needs_auth": "Needs auth",
+ "session.mcp_status_register": "Register client",
+ "session.mcp_status_failed": "Failed",
+ "session.mcp_status_disabled": "Disabled",
+ "session.skills_title": "Skills",
+ "session.skills_empty": "No skills loaded.",
+ "session.authorized_folders_title": "Authorized folders",
+
+ // Inbox Panel
+ "session.inbox_title": "Inbox",
+ "session.inbox_refresh_tooltip": "Refresh inbox",
+ "session.inbox_drop_hint": "Drop files here to upload",
+ "session.inbox_upload_disabled": "Connect to a worker to upload",
+ "session.inbox_uploading": "Uploading...",
+ "session.inbox_upload_cta": "Drop files or click to upload",
+ "session.inbox_helper_text": "Share files with your remote worker.",
+ "session.inbox_connect_hint": "Connect to see inbox files.",
+ "session.inbox_empty": "No inbox files yet.",
+ "session.inbox_copy_tooltip": "Copy inbox path",
+ "session.inbox_download_tooltip": "Download",
+ "session.inbox_showing_first": "Showing first {count}.",
+ "session.inbox_toast_connect_upload": "Connect to a worker to upload inbox files.",
+ "session.inbox_toast_uploading": "Uploading {label}...",
+ "session.inbox_toast_uploaded": "Uploaded to worker inbox.",
+ "session.inbox_toast_upload_failed": "Inbox upload failed",
+ "session.inbox_toast_copied": "Copied: {path}",
+ "session.inbox_toast_copy_failed": "Copy failed. Your browser may block clipboard access.",
+ "session.inbox_toast_connect_download": "Connect to a worker to download inbox files.",
+ "session.inbox_toast_missing_id": "Missing inbox item id.",
+ "session.inbox_toast_download_failed": "Download failed",
+ "session.inbox_toast_load_failed": "Failed to load inbox",
+
+ // Editor
+ "session.editor_default_title": "Artifact",
+ "session.editor_unsaved": "Unsaved",
+ "session.editor_reload_tooltip": "Reload from disk",
+ "session.editor_reload": "Reload",
+ "session.editor_save_tooltip": "Save (Ctrl/Cmd+S)",
+ "session.editor_saving": "Saving...",
+ "session.editor_save": "Save",
+ "session.editor_close": "Close",
+ "session.editor_connect_hint": "Connect to an OpenWork server worker to edit files.",
+ "session.editor_overwrite_prompt": "File changed since load. Overwrite anyway?",
+ "session.editor_overwrite": "Overwrite",
+ "session.editor_discard_prompt": "Discard unsaved changes and close?",
+ "session.editor_keep": "Keep",
+ "session.editor_discard": "Discard",
+ "session.editor_switch_prompt": "Switch to {path}",
+ "session.editor_discard_switch": "Discard & switch",
+ "session.editor_save_switch": "Save & switch",
+ "session.editor_aria_label": "Artifact editor",
+ "session.editor_toast_save_connect": "Cannot save: OpenWork server not connected",
+ "session.editor_toast_markdown_only": "Only markdown files are supported",
+ "session.editor_toast_save_failed": "Failed to save",
+ "session.editor_toast_reload_discard": "Discard changes to reload from disk (close and reopen), or save first.",
+ "session.editor_toast_load_failed": "Failed to load file",
+ "session.editor_toast_not_found": "File not found (workspace root or outbox).",
+
+ // Minimap
+ "session.minimap_user_msg": "User message {index}",
+ "session.minimap_agent_msg": "Agent message {index}",
+ "session.minimap_user": "User",
+ "session.minimap_agent": "Agent",
+ "sidebar.needs_attention": "Needs attention",
+ "sidebar.expand": "Expand",
+ "sidebar.collapse": "Collapse",
+ "sidebar.drag_to_reorder": "Drag to reorder",
+ "sidebar.add_new_workspace": "Add new workspace",
+ "session.variant_none": "None",
+ "session.variant_low": "Low",
+ "session.variant_medium": "Medium",
+ "session.variant_high": "High",
+ "session.variant_xhigh": "X-High",
+ "session.click_to_expand": "Click to expand pasted text",
+ "session.pasted_text": "[pasted text]",
+ "common.remove": "Remove",
+ "sidebar.no_workspaces": "No workspaces in this session yet. Add one to get started.",
+
+ // Dashboard Updates
+ "dashboard.update_ready_btn": "Update ready",
+ "dashboard.install_update_btn": "Install update",
+ "dashboard.downloading_btn": "Downloading",
+ "dashboard.downloading_percent_btn": "Downloading {percent}%",
+ "dashboard.update_available_btn": "Update available",
+
+ // Status Bar
+ "status_bar.opencode_label": "OpenCode Engine: {status}",
+ "status_bar.openwork_label": "OpenWork Server: {status}",
+ "status_bar.connected": "Connected",
+ "status_bar.not_connected": "Not connected",
+ "status_bar.ready": "Ready",
+ "status_bar.limited_access": "Limited access",
+ "status_bar.unavailable": "Unavailable",
+ "status_bar.messaging_unavailable": "Messaging bridge unavailable",
+ "status_bar.messaging_ready": "Messaging bridge ready",
+ "status_bar.messaging_setup": "Messaging bridge setup",
+ "status_bar.messaging_offline": "Messaging bridge offline",
+ "status_bar.tip_connect_slack": "Connect Slack",
+ "status_bar.tip_connect_telegram": "Connect Telegram",
+ "status_bar.tip_connect_notion": "Connect Notion MCP",
+ "status_bar.tip_providers": "Use your own models (OpenRouter, Anthropic, OpenAI)",
+ "status_bar.tip_label": "Tip",
+ "status_bar.settings": "Settings",
+
+ // Identities Updates
+ "identities.connected_count": "{count} connected",
+ "identities.not_set": "Not set",
+ "identities.telegram_peer_placeholder": "Telegram chat id (e.g. 123456789)",
+ "identities.slack_peer_placeholder": "Slack peer id (e.g. D12345678|thread_ts)",
+
+ // Skills Updates
+ "skills.worker_profile": "Worker profile",
+ "skills.worker_profile_desc": "Skills are the core abilities of this worker. Add from Hub or create new ones directly in chat.",
+ "skills.create_in_chat": "Create skill in chat",
+ "skills.stat_installed": "Installed",
+ "skills.stat_hub_available": "Hub available",
+ "skills.stat_skill_creator": "Skill creator",
+ "skills.stat_not_installed": "Not installed",
+ "skills.stat_mode": "Mode",
+ "skills.mode_local": "Local",
+ "skills.mode_server": "Server",
+ "skills.search_placeholder": "Search installed or hub skills",
+ "skills.new_skill": "New skill",
+ "skills.install_link": "Install from link",
+ "skills.install_link_hint": "Install a skill from a link",
+ "skills.no_hub_skills": "No hub skills available.",
+ "skills.from_hub": "From openwork-hub",
+ "skills.trigger_label": "Trigger: {trigger}",
+ "skills.installing": "Installing",
+ "skills.add": "Add",
+ "skills.save": "Save",
+ "skills.close": "Close",
+ "skills.loading": "Loading...",
+ "skills.share_link_title": "Share link",
+ "skills.share_link_desc": "Publish a public link. Anyone with the URL can install this skill.",
+ "skills.publisher_label": "Publisher: {url}",
+ "skills.publishing": "Publishing...",
+ "skills.create_link": "Create link",
+ "skills.copy_link": "Copy link",
+ "skills.done": "Done",
+ "skills.install_link_title": "Install from link",
+ "skills.install_link_desc": "Paste a skill bundle URL, preview it, then install.",
+ "skills.link_label": "Link",
+ "skills.preview_label": "Preview",
+ "skills.skill_label": "Skill: {name}",
+ "skills.conflict_warning": "A skill with this name is already installed.",
+ "skills.keep_both": "Keep both",
+ "skills.overwrite": "Overwrite",
+ "skills.install_btn": "Install",
+ "skills.installing_btn": "Installing...",
+ "skills.edit": "Edit",
+ "skills.install_title": "Install skills",
+ "skills.refresh_hub_hint": "Refresh hub catalog",
+ "skills.refresh_hub": "Refresh hub",
+ "skills.capability_setup": "Capability setup",
} as const;
diff --git a/packages/app/src/i18n/locales/zh.ts b/packages/app/src/i18n/locales/zh.ts
index 85db04989..f5885585d 100644
--- a/packages/app/src/i18n/locales/zh.ts
+++ b/packages/app/src/i18n/locales/zh.ts
@@ -172,7 +172,6 @@ export default {
"session.rename_description": "更新此会话名称。",
"session.rename_label": "会话名称",
"session.rename_placeholder": "输入新的名称",
-
// ==================== Commands ====================
"commands.new": "新建",
"commands.empty_state": "保存提示词或命令,一键重复运行。",
@@ -222,7 +221,6 @@ export default {
"skills.import": "导入",
"skills.curated_packages": "精选包",
"skills.view": "查看",
- "skills.search_placeholder": "搜索包或列表(例如:claude、registry、community)",
"skills.no_matches": "没有匹配的精选包。尝试不同的搜索。",
"skills.install_package": "安装",
"skills.registry_notice": "发布到 OpenPackage 注册表(`opkg push`)目前需要身份验证。计划添加注册表搜索和精选列表同步。",
@@ -305,14 +303,31 @@ export default {
"plugins.add_hint": "添加 npm 包名称,例如 opencode-wakatime",
// ==================== MCP (Model Context Protocol) ====================
+ "mcp.apps_title": "应用",
+ "mcp.apps_subtitle": "连接您喜欢的工具,让 OpenWork 代表您使用它们。",
+ "mcp.app_connected": "个应用已连接",
+ "mcp.apps_connected": "个应用已连接",
"mcp.title": "MCP(测试版)",
"mcp.description": "MCP 服务器让您使用自己的凭据连接服务。",
"mcp.alpha_banner_title": "MCP 处于测试阶段,我们正在加强与 OpenCode 的 OAuth 集成。",
"mcp.alpha_banner_help": "如果您想帮忙,请提交 PR 并附上一个短视频,展示 OAuth 流程从端到端的工作。",
"mcp.mcps_title": "MCPs",
"mcp.connect_mcp_hint": "连接 MCP 服务器以扩展 OpenWork 的功能。",
+ "mcp.finish_setup": "只差一步",
+ "mcp.finish_setup_hint": "点击激活以完成应用连接。",
+ "mcp.activate_button": "激活",
"mcp.reload_banner_title": "需要重新加载",
"mcp.reload_banner_description": "更改需要快速重新加载以激活 MCP 工具。",
+ "mcp.reload_banner_description_blocked": "任务正在运行。请先停止任务,然后激活。",
+ "mcp.reload_banner_blocked_hint": "停止正在运行的任务以激活。",
+ "mcp.available_apps": "可用应用",
+ "mcp.one_click_connect": "一键连接",
+ "mcp.tap_to_connect": "点击连接",
+ "mcp.connected_badge": "已连接",
+ "mcp.your_apps": "您的应用",
+ "mcp.last_synced": "已同步",
+ "mcp.no_apps_yet": "尚未连接应用",
+ "mcp.no_apps_hint": "连接上方的一个应用以开始。",
"mcp.quick_connect_title": "快速连接",
"mcp.oauth_only_label": "OAuth + 本地",
"mcp.connected_status": "已连接",
@@ -326,6 +341,7 @@ export default {
"mcp.scope_project": "项目",
"mcp.scope_global": "全局",
"mcp.config_label": "配置",
+ "mcp.config_file": "配置文件",
"mcp.config_not_loaded": "尚未加载",
"mcp.open_file_label": "打开文件",
"mcp.reveal_in_finder": "在文件管理器中显示",
@@ -338,6 +354,8 @@ export default {
"mcp.alpha_warning": "MCP 处于测试阶段,我们正在加强与 OpenCode 的 OAuth 集成。",
"mcp.github_issue": "在 GitHub 上查看 issue #9510",
"mcp.contribution_guide": "如果您想帮忙,请提交 PR 并附上一个短视频,展示 OAuth 流程从端到端的工作。",
+ "mcp.advanced_settings": "高级设置",
+ "mcp.advanced_settings_hint": "手动编辑配置文件并管理连接。",
"mcp.hide_advanced": "隐藏高级设置",
"mcp.show_advanced": "显示高级设置",
"mcp.mcps_label": "MCPs",
@@ -387,18 +405,21 @@ export default {
"mcp.verify_connection": "验证连接",
"mcp.cli_guidance": "CLI 指南(从您的工作区运行)",
"mcp.config_locations": "配置可以位于 opencode.json、opencode.jsonc 或 .opencode/opencode.json。",
+ "mcp.app_details": "应用详情",
"mcp.details_title": "详情",
- "mcp.select_server": "选择服务器",
+ "mcp.select_app_hint": "选择一个应用以查看详情。",
"mcp.select_server_hint": "选择服务器以查看状态和配置。",
- "mcp.capabilities": "功能",
+ "mcp.connection_type": "连接",
+ "mcp.type_cloud": "云端(使用您的帐户登录)",
+ "mcp.type_local": "本地(在此设备上运行)",
"mcp.capabilities_label": "功能",
- "mcp.tools_enabled": "已启用工具",
+ "mcp.cap_tools": "AI 工具",
+ "mcp.cap_signin": "帐户登录",
"mcp.tools_enabled_label": "已启用工具",
- "mcp.oauth_ready": "OAuth 就绪",
"mcp.oauth_ready_label": "OAuth 就绪",
- "mcp.usage_hint": "在提示中使用 MCP 服务器名称来定位其工具。",
"mcp.usage_hint_text": "在提示中使用 MCP 服务器名称来定位其工具。",
- "mcp.next_steps": "后续步骤",
+ "mcp.issue_label": "问题",
+ "mcp.technical_details": "技术详情",
"mcp.next_steps_label": "后续步骤",
"mcp.reload_step": "添加服务器后重新加载引擎。",
"mcp.auth_step": "如果提示,为 OAuth 服务器运行 opencode mcp auth。",
@@ -408,6 +429,11 @@ export default {
"mcp.status_disabled": "已禁用",
"mcp.disconnected": "已断开",
"mcp.failed": "失败",
+ "mcp.friendly_status_ready": "就绪",
+ "mcp.friendly_status_needs_signin": "需要登录",
+ "mcp.friendly_status_paused": "已暂停",
+ "mcp.friendly_status_offline": "离线",
+ "mcp.friendly_status_issue": "问题",
"mcp.host_mode_only": "MCP 连接需要桌面应用。",
"mcp.pick_workspace_first": "先选择一个工作区文件夹。",
"mcp.desktop_required": "MCP 连接需要桌面应用。",
@@ -437,6 +463,17 @@ export default {
"mcp.auth.reload_engine_retry": "重新加载引擎并重试",
"mcp.auth.retry_now": "立即重试",
"mcp.auth.retry": "重试",
+ "mcp.auth.reload_before_oauth": "在开始 OAuth 之前,重新加载引擎以完成此 MCP 的设置。",
+ "mcp.auth.reload_notice": "通过重新加载引擎来完成设置以激活此 MCP。这是一个必需的步骤,不是错误。",
+ "mcp.auth.reload_blocked": "会话运行时暂停重新加载。停止运行以完成设置。",
+ "mcp.auth.reload_needed": "通过重新加载引擎完成设置,然后尝试再次连接。",
+ "mcp.auth.manual_finish_title": "远程服务器?",
+ "mcp.auth.manual_finish_hint": "粘贴回调 URL (localhost:19876) 或仅粘贴代码以完成连接。",
+ "mcp.auth.callback_label": "回调 URL 或代码",
+ "mcp.auth.callback_placeholder": "http://127.0.0.1:19876/mcp/oauth/callback?code=...",
+ "mcp.auth.complete_connection": "完成连接",
+ "mcp.auth.callback_invalid": "粘贴回调 URL 或 code 参数以完成 OAuth。",
+ "mcp.auth.port_forward_hint": "提示:如果需要,转发回调端口:ssh -L 19876:127.0.0.1:19876 user@host",
"mcp.auth.step1_title": "正在打开您的浏览器",
"mcp.auth.step1_description": "我们将自动启动 {server} 的登录流程。",
"mcp.auth.step2_title": "授权 OpenWork",
@@ -611,6 +648,7 @@ export default {
"reload.toast_warning": "重新加载可能会中断正在进行的会话或流程。",
"reload.toast_warning_active": "检测到活动运行,立即重新加载将会中断它们。",
"reload.toast_reload": "重新加载",
+ "reload.toast_reload_stopped": "重新加载并停止任务",
"reload.toast_reloading": "正在重新加载...",
"reload.toast_dismiss": "忽略",
"reload.toast_blocked_host": "仅本地工作区可重新加载。",
@@ -696,6 +734,10 @@ export default {
"onboarding.default_workspace_path": "~/OpenWork/Workspace",
"onboarding.authorize_folder": "授权文件夹",
"onboarding.choose_workspace_folder": "选择工作区文件夹",
+ "onboarding.import_label": "导入",
+ "onboarding.import_description": "使用现有的工作区配置。",
+ "onboarding.import_hint": "仅导入 `.opencode` 和 `opencode.json`。",
+ "onboarding.import_button": "导入配置",
// ==================== Common ====================
"common.alpha": "测试版",
@@ -721,6 +763,7 @@ export default {
// ==================== Status ====================
"status.connected": "已连接",
+ "status.limited": "受限",
"status.disconnected": "已断开",
"status.idle": "空闲",
"status.busy": "忙碌",
@@ -760,4 +803,684 @@ export default {
"app.error.command_name_template_required": "命令名称和指令为必填项。",
"app.error.workspace_commands_desktop": "命令需要桌面应用。",
"app.error.command_scope_unknown": "此命令无法在当前模式下管理。",
+
+ // ==================== Config ====================
+ "config.title": "工作区配置",
+ "config.subtitle": "这些设置影响当前工作区(共享、重载、机器人)。全局应用行为在设置中。",
+ "config.workspace_label": "工作区:",
+ "config.engine_reload_title": "引擎重载",
+ "config.engine_reload_subtitle": "重启此工作区的 OpenCode 服务器。",
+ "config.reload_now": "立即重载",
+ "config.reload_now_subtitle": "应用配置更新并重新连接会话。",
+ "config.reload_warning": "重载将停止活动任务。",
+ "config.reload_connect_hint": "连接到此工作区以重载。",
+ "config.reload_local_only": "重载仅适用于本地工作区或已连接的 OpenWork 服务器。",
+ "config.reloading": "正在重载...",
+ "config.reload_engine": "重载引擎",
+ "config.auto_reload_title": "自动重载(本地)",
+ "config.auto_reload_subtitle": "在代理/技能/命令/配置更改后自动重载(仅空闲时)。",
+ "config.auto_reload_desktop_only": "仅适用于桌面应用中的本地工作区。",
+ "config.resume_sessions_title": "自动重载后恢复会话",
+ "config.resume_sessions_subtitle": "如果在任务运行时排队重载,之后发送恢复消息。",
+ "config.enable_auto_reload_first": "请先启用自动重载",
+ "config.diagnostics_title": "诊断包",
+ "config.diagnostics_subtitle": "复制用于调试的运行时状态。",
+ "config.copied": "已复制",
+ "config.copy": "复制",
+ "config.sharing_title": "OpenWork 服务器共享",
+ "config.sharing_subtitle": "与受信任的设备共享这些详情。保持服务器在同一网络以获得最快设置。",
+ "config.status_offline": "离线",
+ "config.status_available": "可用",
+ "config.server_url_label": "OpenWork 服务器地址",
+ "config.mdns_hint": ".local 名称更容易记住,但可能无法在所有网络上解析。",
+ "config.ip_hint": "在同一 Wi-Fi 下使用本地 IP 以获得最快连接。",
+ "config.starting_server": "正在启动服务器...",
+ "config.access_token_label": "访问令牌",
+ "config.access_token_hint": "用于连接到此服务器的手机或笔记本电脑。",
+ "config.hide": "隐藏",
+ "config.show": "显示",
+ "config.server_token_label": "服务器令牌",
+ "config.server_token_hint": "保持私密。批准操作需要。",
+ "config.share_hint": "对于每个工作区的共享链接,请使用工作区菜单中的 共享...。",
+ "config.openwork_server_title": "OpenWork 服务器",
+ "config.openwork_server_subtitle": "连接到 OpenWork 服务器。使用服务器管理员提供的 URL 和访问令牌。",
+ "config.sync_hint": "同步技能、插件和命令需要 OpenWork 服务器连接。",
+ "config.identities_title": "消息身份",
+ "config.identities_subtitle": "在 身份 选项卡中管理 Telegram/Slack 身份和路由。",
+ "config.desktop_only_hint": "某些配置功能(本地服务器共享 + 消息桥接)需要桌面应用。",
+ "config.testing": "测试中...",
+ "config.test_connection": "测试连接",
+ "config.save": "保存",
+ "config.reset": "重置",
+ "config.testing_connection": "正在测试连接...",
+ "config.connection_status_updated": "连接状态已更新。",
+ "config.connection_success": "连接成功。",
+ "config.connection_failed_generic": "连接失败。",
+ "config.resolved_worker_url": "解析的工作区 URL:",
+ "config.worker_id": "工作区 ID:",
+ "config.not_set": "未设置",
+ "config.unavailable": "不可用",
+
+ // ==================== Dashboard New ====================
+ "dashboard.tab_automations": "自动化",
+ "dashboard.tab_skills": "技能",
+ "dashboard.tab_extensions": "扩展",
+ "dashboard.tab_identities": "身份",
+ "dashboard.tab_advanced": "高级",
+ "dashboard.tab_settings": "设置",
+ "dashboard.workspace_worker": "工作区",
+ "dashboard.workspace_sandbox": "沙盒",
+ "dashboard.workspace_remote": "远程",
+ "dashboard.workspace_local": "本地",
+ "dashboard.show_more": "显示更多",
+ "dashboard.show_more_count": "显示更多 {count} 个",
+ "dashboard.add_worker": "添加工作区",
+ "dashboard.new_worker": "新建工作区",
+ "dashboard.connect_remote": "连接远程",
+ "dashboard.import_config": "导入配置",
+ "dashboard.tasks_header": "任务",
+ "dashboard.tasks_error": "加载任务失败",
+ "dashboard.tasks_error_badge": "错误",
+ "dashboard.tasks_empty": "暂无任务。",
+ "dashboard.tasks_new": "+ 新建任务",
+ "dashboard.menu_edit_name": "编辑名称",
+ "dashboard.menu_share": "共享...",
+ "dashboard.menu_test_connection": "测试连接",
+ "dashboard.menu_edit_connection": "编辑连接",
+ "dashboard.menu_stop_sandbox": "停止沙盒",
+ "dashboard.menu_remove_worker": "移除工作区",
+ "dashboard.loading_tasks": "正在加载任务...",
+ "dashboard.update_ready": "更新就绪",
+ "dashboard.update_restart": "重启",
+ "dashboard.update_downloading": "下载中",
+ "dashboard.update_available": "可用更新",
+ "dashboard.update_ready_tooltip": "更新 {version} 已就绪。停止活动运行以重启。",
+ "dashboard.update_restart_tooltip": "重启以应用更新 {version}",
+ "dashboard.update_downloading_tooltip": "正在下载更新 {version}",
+ "dashboard.update_available_tooltip": "可用更新 {version}",
+ "dashboard.share_worker_url": "OpenWork 工作区 URL",
+ "dashboard.share_desktop_required": "需要桌面应用",
+ "dashboard.share_starting_server": "正在启动服务器...",
+ "dashboard.share_worker_hint": "用于连接到此工作区的手机或笔记本电脑。",
+ "dashboard.share_resolving_hint": "工作区 URL 正在解析;显示主机 URL 作为回退。",
+ "dashboard.share_host_hint": "用于连接到此主机的手机或笔记本电脑。",
+ "dashboard.share_token_advanced_hint": "在高级设置中设置令牌",
+ "dashboard.share_token_grant_hint": "此令牌授予对该主机上工作区的访问权限。",
+ "dashboard.share_opencode_url": "OpenCode 基础 URL",
+ "dashboard.share_directory": "目录",
+ "dashboard.share_auto": "(自动)",
+ "dashboard.share_direct_warning": "引擎运行时设置为直连。切换本地工作区可能会重启主机并断开客户端连接。重启后令牌可能会更改。",
+ "dashboard.export_local_desktop": "导出仅适用于桌面应用中的本地工作区。",
+ "dashboard.export_local_only": "导出仅支持本地工作区。",
+ "dashboard.export_desktop_only": "导出仅在桌面应用中可用。",
+ "dashboard.export_running": "导出已在运行。",
+ "dashboard.repairing_cache_btn": "正在修复缓存",
+ "dashboard.repair_cache_btn": "修复缓存",
+ "dashboard.nav_ids": "IDs",
+ // ==================== Extensions ====================
+ "extensions.title": "扩展",
+ "extensions.description": "Apps (MCP) 和 OpenCode 插件集中管理。",
+ "extensions.apps_connected": "{count} 个应用已连接",
+ "extensions.plugins_count": "{count} 个插件",
+ "extensions.tab_all": "全部",
+ "extensions.tab_apps": "Apps",
+ "extensions.tab_plugins": "插件",
+ "extensions.refresh": "刷新",
+ "extensions.section_apps": "Apps (MCP)",
+ "extensions.section_plugins": "插件 (OpenCode)",
+
+ // ==================== Identities ====================
+ "identities.title": "消息渠道",
+ "identities.repair_reconnect": "修复并重连",
+ "identities.refresh": "刷新",
+ "identities.description": "让人们通过消息应用联系您的 Worker。连接一个渠道,Worker 将自动读取并回复消息。",
+ "identities.workspace_scope": "工作区范围:{url}",
+ "identities.connect_host": "连接 OpenWork 服务器",
+ "identities.connect_host_hint": "当连接到 OpenWork 主机 (openwork) 时,身份可用。",
+ "identities.workspace_id_required": "管理身份需要工作区 ID。请使用工作区 URL(例如:/w/<workspace-id>)重新连接,或选择此主机上映射的工作区。",
+ "identities.tab_general": "常规",
+ "identities.tab_advanced": "高级",
+ "identities.worker_online": "Worker 在线",
+ "identities.worker_unavailable": "Worker 不可用",
+ "identities.worker_offline": "Worker 离线",
+ "identities.status_channels": "渠道",
+ "identities.status_messages_today": "今日消息",
+ "identities.status_last_activity": "最后活动",
+ "identities.available_channels": "可用渠道",
+ "identities.telegram": "Telegram",
+ "identities.telegram_description": "创建一个任何人都可以发送消息的 Telegram 机器人。非常适合个人自动化和外部联系人。",
+ "identities.slack": "Slack",
+ "identities.slack_description": "您的 Worker 在 Slack 频道中显示为机器人。团队成员可以直接发送消息或在话题中提及它。",
+ "identities.connected": "已连接",
+ "identities.status_active": "活跃",
+ "identities.status_stopped": "停止",
+ "identities.configured": "已配置",
+ "identities.channel_on": "开启",
+ "identities.channel_off": "关闭",
+ "identities.quick_setup": "快速设置",
+ "identities.telegram_step1": "打开 @BotFather 并运行 /newbot。",
+ "identities.telegram_step2": "复制机器人令牌并粘贴到下方。",
+ "identities.telegram_step3": "连接,然后向您的机器人发送 /start 以激活聊天。",
+ "identities.bot_token": "机器人令牌",
+ "identities.telegram_token_placeholder": "粘贴来自 @BotFather 的 Telegram 机器人令牌",
+ "identities.enabled": "已启用",
+ "identities.connect_telegram": "连接 Telegram",
+ "identities.connecting": "连接中...",
+ "identities.open_telegram_bot": "在 Telegram 中打开 @{username}",
+ "identities.slack_hint": "连接您的 Slack 工作区,让团队成员在频道和私信中与此 Worker 交互。",
+ "identities.app_token": "应用令牌",
+ "identities.connect_slack": "连接 Slack",
+ "identities.message_routing": "消息路由",
+ "identities.message_routing_description": "控制哪些对话进入哪个工作区文件夹。除非您在此处设置规则,否则消息将路由到 Worker 的默认文件夹。",
+ "identities.default_routing": "默认路由",
+ "identities.all_channels": "所有渠道",
+ "identities.routing_advanced_hint": "高级:在 Slack/Telegram 中回复 /dir <path> 以覆盖特定聊天的目录(仅限于此工作区根目录)。",
+ "identities.agent_behavior": "消息代理行为",
+ "identities.agent_behavior_hint": "每个工作区一个文件。添加可选的第一行 @agent <id> 以通过特定的 OpenCode 代理进行路由。",
+ "identities.active_scope": "活动范围:工作区 · 状态:{loaded} · 选定代理:{selected}",
+ "identities.loading_agent": "正在加载代理文件...",
+ "identities.agent_not_found": "在此工作区中尚未找到代理文件。",
+ "identities.agent_placeholder": "在此处为 opencodeRouter 添加消息行为指令...",
+ "identities.reload": "重新加载",
+ "identities.create_default_file": "创建默认文件",
+ "identities.save_behavior": "保存行为",
+ "identities.saving": "保存中...",
+ "identities.unsaved_changes": "未保存的更改",
+ "identities.send_test_message": "发送测试消息",
+ "identities.send_test_hint": "验证出站连接。使用对等 ID 进行直接发送,或留空对等 ID 以通过目录中的绑定进行扇出。",
+ "identities.peer_id": "对等 ID(可选)",
+ "identities.directory": "目录(可选)",
+ "identities.auto_bind": "直接发送时自动将对等端绑定到目录",
+ "identities.message": "消息",
+ "identities.test_message_placeholder": "测试消息内容",
+ "identities.sending": "发送中...",
+ "identities.disconnect": "断开连接",
+ "identities.status_label": "状态",
+ "identities.identities_label": "身份",
+ "identities.status_unavailable": "不可用",
+ "identities.status_unknown": "未知",
+ "identities.status_running": "运行中",
+ "identities.status_offline": "离线",
+ "identities.time_just_now": "刚刚",
+ "identities.time_minutes_ago": "{minutes}分钟前",
+ "identities.time_hours_ago": "{hours}小时前",
+ "identities.time_days_ago": "{days}天前",
+ "identities.msg_agent_created": "已创建默认消息代理文件。",
+ "identities.msg_agent_saved": "已保存消息行为。",
+ "identities.error_file_changed": "文件在远程已更改。请重新加载并再次保存。",
+ "identities.msg_dispatched": "已发送 {sent}/{attempted} 条消息。",
+ "identities.error_worker_scope_unavailable": "工作区范围不可用。",
+ "identities.error_worker_scope_unavailable_long": "工作区范围不可用。请使用工作区 URL 重连或切换到已知工作区。",
+ "identities.error_reconnect_failed": "重连失败。请检查 OpenWork URL/令牌并重试。",
+ "identities.msg_reconnected_refreshing": "已重连。正在刷新工作区状态...",
+ "identities.msg_reconnected": "已重连。",
+ "identities.msg_saved_pending": "已保存(等待应用)。",
+ "identities.msg_saved": "已保存。",
+ "identities.error_save_failed": "保存失败。",
+ "identities.msg_deleted_pending": "已删除(等待应用)。",
+ "identities.msg_deleted": "已删除。",
+ "identities.error_delete_failed": "删除失败。",
+ "identities.msg_saved_username": "已保存 (@{username})",
+ "identities.error_health_unavailable": "OpenCodeRouter 健康检查不可用 ({status})",
+ "identities.error_telegram_unavailable": "Telegram 身份不可用。",
+ "identities.error_slack_unavailable": "Slack 身份不可用。",
+
+ "scheduled.never": "从不",
+ "scheduled.custom_schedule": "自定义计划",
+ "scheduled.every_hour": "每小时",
+ "scheduled.every_n_hours": "每 {n} 小时",
+ "scheduled.every_day_at": "每天 {time}",
+ "scheduled.weekdays_at": "工作日 {time}",
+ "scheduled.weekends_at": "周末 {time}",
+ "scheduled.at_time": "在 {time}",
+ "scheduled.days_at_time": "{days} {time}",
+ "scheduled.day_sun": "周日",
+ "scheduled.day_mon": "周一",
+ "scheduled.day_tue": "周二",
+ "scheduled.day_wed": "周三",
+ "scheduled.day_thu": "周四",
+ "scheduled.day_fri": "周五",
+ "scheduled.day_sat": "周六",
+ "scheduled.label_command": "命令",
+ "scheduled.label_prompt": "提示词",
+ "scheduled.label_task": "任务",
+ "scheduled.no_prompt_command": "未找到提示词或命令。",
+ "scheduled.status_not_run": "尚未运行",
+ "scheduled.status_running": "运行中",
+ "scheduled.status_success": "成功",
+ "scheduled.status_failed": "失败",
+ "scheduled.template_daily_planning": "每日计划简报",
+ "scheduled.template_daily_planning_desc": "根据你的任务和日历构建重点计划。",
+ "scheduled.template_daily_planning_prompt": "查看我的待办任务和日历,然后起草一份今天的实用计划,包含首要任务和一个跟进提醒。",
+ "scheduled.template_inbox_zero": "收件箱清零助手",
+ "scheduled.template_inbox_zero_desc": "总结未读消息并起草简短回复。",
+ "scheduled.template_inbox_zero_prompt": "总结未读收件箱消息,建议优先级顺序,并为重要对话起草简明回复选项。",
+ "scheduled.template_meeting_prep": "会议准备笔记",
+ "scheduled.template_meeting_prep_desc": "为明天的会议生成准备要点。",
+ "scheduled.template_meeting_prep_prompt": "为明天的会议准备简报,包含背景、谈话要点和决策阻碍问题。",
+ "scheduled.template_weekly_wins": "每周成果回顾",
+ "scheduled.template_weekly_wins_desc": "创建周五回顾,包含成果、阻碍和下一步。",
+ "scheduled.template_weekly_wins_prompt": "总结本周成果、阻碍和明确的下一步行动,以便我与团队分享。",
+ "scheduled.template_learning_digest": "学习摘要",
+ "scheduled.template_learning_digest_desc": "将保存的链接和笔记转化为每周摘要。",
+ "scheduled.template_learning_digest_prompt": "收集我保存的链接和笔记,然后起草一份包含关键观点和后续行动的每周学习摘要。",
+ "scheduled.template_habit_checkin": "习惯检查",
+ "scheduled.template_habit_checkin_desc": "全天进行快速问责检查。",
+ "scheduled.template_habit_checkin_prompt": "向我发起快速进度检查,记录阻碍,并建议一个具体的下一步行动。",
+ "scheduled.run": "运行",
+ "scheduled.delete": "删除",
+ "scheduled.run_context": "运行上下文",
+ "scheduled.source": "来源:{source}",
+ "scheduled.last_run": "上次运行 {time}",
+ "scheduled.created": "创建于 {time}",
+ "scheduled.agent": "代理 {agent}",
+ "scheduled.model": "模型 {model}",
+ "scheduled.server_unavailable": "OpenWork 服务器不可用。连接以同步计划任务。",
+ "scheduled.desktop_required": "计划任务需要桌面应用。",
+ "scheduled.windows_not_supported": "Windows 暂不支持计划任务。",
+ "scheduled.desc_remote": "从连接的 OpenWork 服务器按计划运行的自动化。",
+ "scheduled.desc_local": "从此设备按计划运行的自动化。",
+ "scheduled.from_server": "来自 OpenWork 服务器",
+ "scheduled.from_local": "来自本地调度器",
+ "scheduled.server": "OpenWork 服务器",
+ "scheduled.local": "本地",
+ "scheduled.remote_instance": "远程实例",
+ "scheduled.launchd_systemd": "Launchd 或 systemd",
+ "scheduled.server_unavailable_short": "OpenWork 服务器不可用",
+ "scheduled.desktop_only": "仅限桌面",
+ "scheduled.delete_desc_remote": "这将移除计划并从连接的 OpenWork 服务器删除作业定义。",
+ "scheduled.delete_desc_local": "这将移除计划并从你的机器删除作业定义。",
+ "scheduled.not_synced": "尚未同步",
+ "scheduled.delete_failed": "删除作业失败。",
+ "scheduled.refreshing": "正在刷新",
+ "scheduled.refresh": "刷新",
+ "scheduled.new_automation": "新建自动化",
+ "scheduled.automations": "自动化",
+ "scheduled.beta": "测试版",
+ "scheduled.no_automations": "暂无自动化。选择一个模板或创建你自己的自动化提示词。",
+ "scheduled.explore_more": "探索更多",
+ "scheduled.delete_confirm_title": "删除自动化?",
+ "scheduled.deleting": "正在删除",
+ "scheduled.create_modal_title": "创建自动化",
+ "scheduled.create_modal_desc": "自动化通过在新线程中运行提示词来调度。我们将为你预填一个提示词以供发送。",
+ "scheduled.name": "名称",
+ "scheduled.projects": "项目",
+ "scheduled.choose_folder": "选择文件夹",
+ "scheduled.schedule": "计划",
+ "scheduled.daily": "每日",
+ "scheduled.interval": "间隔",
+ "scheduled.every": "每",
+ "scheduled.hours": "小时",
+ "scheduled.view_docs": "查看调度器文档",
+ "scheduled.cancel": "取消",
+ "scheduled.create": "创建",
+ "scheduled.run_now_prompt": "立即运行此自动化:{name}。\n计划:{schedule}。",
+ "scheduled.run_command_prompt": "立即运行此自动化:{name}。\n计划:{schedule}。\n\n运行以下命令:\n{cmd}{workdirHint}",
+ "scheduled.prompt_construct": "调度一个作业{name},cron 表达式为 \"{schedule}\",以执行 {prompt}{workdir}",
+ "scheduled.run_from": " 运行自 {workdir}。",
+ "scheduled.named": " 命名为 \"{name}\"",
+
+ // ==================== Settings New ====================
+ "settings.messaging.title": "消息",
+ "settings.messaging.description": "在",
+ "settings.messaging.identities_link": "身份",
+ "settings.messaging.tab": "选项卡中管理 Telegram/Slack 身份和绑定。",
+ "settings.providers.title": "提供商",
+ "settings.providers.description": "连接模型和工具的服务。",
+ "settings.providers.connect": "连接提供商",
+ "settings.providers.loading": "正在加载提供商...",
+ "settings.providers.summary.unavailable": "连接到 OpenCode 以加载提供商。",
+ "settings.providers.summary.available": "{available} 可用",
+ "settings.providers.summary.connected": "{connected} 已连接 · {available} 可用",
+ "settings.providers.api_keys_hint": "API 密钥由 OpenCode 本地存储。使用 /models 选择默认模型。",
+ "settings.appearance.title": "外观",
+ "settings.appearance.description": "自定义窗口外观。",
+ "settings.appearance.hide_titlebar": "隐藏标题栏",
+ "settings.appearance.hide_titlebar_hint": "隐藏窗口标题栏。适用于 Linux 上的平铺窗口管理器(Hyprland, i3, sway)。",
+ "settings.devtools.title": "开发者工具",
+ "settings.devtools.description": "Sidecar 健康状况、功能和审计日志。",
+ "settings.devtools.versions": "版本",
+ "settings.devtools.versions_description": "Sidecar + 桌面构建信息。",
+ "settings.devtools.opencode_engine": "OpenCode 引擎",
+ "settings.devtools.opencode_engine_description": "本地执行 sidecar。",
+ "settings.devtools.orchestrator": "Orchestrator 守护进程",
+ "settings.devtools.orchestrator_description": "工作区编排层。",
+ "settings.devtools.opencode_sdk": "OpenCode SDK",
+ "settings.devtools.opencode_sdk_description": "UI 连接诊断。",
+ "settings.devtools.openwork_server": "OpenWork 服务器",
+ "settings.devtools.openwork_server_description": "配置和审批 sidecar。",
+ "settings.devtools.opencode_router": "OpenCodeRouter sidecar",
+ "settings.devtools.opencode_router_description": "消息桥接服务。",
+ "settings.devtools.diagnostics": "OpenWork 服务器诊断",
+ "settings.devtools.capabilities": "OpenWork 服务器功能",
+ "settings.devtools.audit_log": "审计日志",
+ "settings.devtools.no_audit_entries": "暂无审计条目。",
+ "settings.engine.title": "引擎",
+ "settings.engine.description": "选择 OpenCode 在本地的运行方式。",
+ "settings.engine.source": "引擎来源",
+ "settings.engine.bundled": "内置(推荐)",
+ "settings.engine.system": "系统安装(PATH)",
+ "settings.engine.custom": "自定义二进制",
+ "settings.engine.bundled_hint": "内置引擎是最可靠的选项。仅当您自己管理 OpenCode 时才使用系统安装。",
+ "settings.engine.custom_label": "自定义 OpenCode 二进制",
+ "settings.engine.custom_choose": "选择",
+ "settings.engine.custom_clear": "清除",
+ "settings.engine.custom_hint": "使用此选项将 OpenWork 指向本地 OpenCode 构建(例如您的 fork)。在下次引擎启动或重新加载时生效。",
+ "settings.engine.runtime": "引擎运行时",
+ "settings.engine.runtime_direct": "直接(OpenCode)",
+ "settings.engine.runtime_orchestrator": "OpenWork Orchestrator",
+ "settings.engine.runtime_hint": "在下次引擎启动或重新加载时生效。",
+ "settings.reset_recovery.title": "重置与恢复",
+ "settings.reset_recovery.description": "清除数据或重新开始设置流程。",
+ "settings.tab.general": "常规",
+ "settings.tab.model": "模型",
+ "settings.tab.advanced": "高级",
+ "settings.tab.debug": "调试",
+ "settings.stop_local_server": "停止本地服务器",
+ "settings.disconnect_server": "断开服务器连接",
+ "settings.reconnect_server": "重新连接服务器",
+ "settings.reconnecting": "正在重新连接...",
+ "settings.restart": "重启",
+ "settings.restarting": "正在重启...",
+ "settings.stop": "停止",
+ "settings.clear": "清除",
+ "settings.last_stdout": "最新标准输出",
+ "settings.last_stderr": "最新标准错误",
+ "settings.last_error": "最新错误",
+ "settings.last_attempt": "上次尝试:{time}",
+ "settings.reason": "原因:{reason}",
+ "settings.metrics.healthy": "健康:{ms}ms",
+ "settings.metrics.load_sessions": "加载会话:{ms}ms",
+ "settings.metrics.pending_permissions": "待处理权限:{ms}ms",
+ "settings.metrics.providers": "提供商:{ms}ms",
+ "settings.metrics.total": "总计:{ms}ms",
+ "settings.diagnostics_unavailable": "诊断不可用。",
+ "settings.capabilities_unavailable": "功能不可用。请使用客户端令牌连接。",
+ "settings.started": "启动时间:{time}",
+ "settings.read_only": "只读:{value}",
+ "settings.approval": "审批:{mode} ({ms}ms)",
+ "settings.workspaces_count": "工作区数:{count}",
+ "settings.active_workspace": "活动工作区:{id}",
+ "settings.config_path": "配置路径:{path}",
+ "settings.token_source_client": "令牌来源:{source}",
+ "settings.token_source_host": "主机令牌来源:{source}",
+ "settings.updates_auto_download": "自动更新",
+ "settings.updates_auto_download_hint": "自动下载更新(提示重启)",
+ "settings.docker_containers": "OpenWork Docker 容器",
+ "settings.docker_containers_hint": "强制删除 OpenWork 启动的 Docker 容器(沙盒 + 本地开发堆栈)。",
+ "settings.workspace_debug_events": "工作区调试事件",
+ "settings.delete_containers": "删除容器",
+ "settings.removing_containers": "正在删除容器...",
+
+ // ==================== Session New ====================
+ "session.search_tooltip": "搜索对话 (Ctrl/Cmd+F)",
+ "session.search_aria": "搜索对话",
+ "session.undo_tooltip": "撤销上一条消息",
+ "session.redo_tooltip": "重做上一条撤销的消息",
+ "session.actions_tooltip": "会话操作",
+ "session.select_session_tooltip": "选择一个会话以管理它",
+ "session.delete_title": "删除会话",
+ "session.delete_modal_title": "删除会话?",
+ "session.delete_modal_message": "这将永久删除 \"{title}\" 及其消息。",
+ "session.delete_modal_message_default": "这将永久删除所选会话及其消息。",
+ "session.show_earlier_messages": "显示之前的 {count} 条消息",
+ "session.jump_to_latest": "跳转到最新",
+ "session.toast_no_undo": "暂无由于可撤销。",
+ "session.toast_undo_success": "已撤销上一条用户消息。",
+ "session.toast_no_redo": "暂无内容可重做。",
+ "session.toast_redo_success": "已恢复撤销的消息。",
+ "session.toast_undo_failed": "撤销失败",
+ "session.toast_redo_failed": "重做失败",
+ "session.toast_stopping": "正在停止运行...",
+ "session.toast_stopped": "已停止。",
+ "session.toast_retrying": "正在重试...",
+ "session.toast_no_retry": "暂无内容可重试",
+ "session.toast_no_session": "未选择会话",
+ "session.toast_session_deleted": "会话已删除",
+ "session.toast_delete_failed": "删除会话失败",
+ "session.toast_file_open_unavailable": "远程工作区无法打开文件。",
+ "session.toast_file_open_desktop": "打开文件仅在桌面应用中可用。",
+ "session.toast_pick_worker": "选择一个工作区以打开文件。",
+ "session.toast_auth_started": "认证流程已开始",
+ "session.toast_auth_failed": "认证失败",
+ "session.toast_api_key_saved": "API 密钥已保存",
+ "session.toast_api_key_failed": "保存 API 密钥失败",
+ "session.toast_upload_connect": "连接到 OpenWork 服务器以上传收件箱文件。",
+ "session.toast_uploading": "正在上传 {label} 到收件箱...",
+ "session.toast_uploaded": "已上传到收件箱。",
+ "session.toast_uploaded_summary": "已上传到收件箱:{summary}",
+ "session.toast_upload_failed": "收件箱上传失败",
+ "session.toast_open_failed": "无法打开文件",
+ "session.toast_load_agents_failed": "加载代理失败",
+ "session.toast_relative_only": "此处仅能打开工作区相关文件。",
+ "session.toast_markdown_only": "目前仅支持编辑 Markdown 文件。",
+ "session.toast_connect_failed": "连接失败",
+ "session.run_status_sending": "发送中",
+ "session.run_status_retrying": "重试中",
+ "session.run_status_responding": "响应中",
+ "session.run_status_thinking": "思考中",
+ "session.run_status_failed": "运行失败",
+ "session.flyout_new_task": "新任务",
+ "session.flyout_file_modified": "文件已修改",
+ "session.hero_title": "想做些什么?",
+ "session.hero_description": "选择一个起点或直接在下方输入。",
+ "session.search_placeholder": "在当前会话中搜索",
+ "session.search_prev": "上一个",
+ "session.search_next": "下一个",
+ "session.tasks_progress": "已完成 {completed}/{total} 个任务",
+ "session.attach_token_hint": "添加服务器令牌以附加文件。",
+ "session.attach_connect_hint": "连接到 OpenWork 服务器以附加文件。",
+ "session.status_delegating": "委派中",
+ "session.status_planning": "规划中",
+ "session.status_gathering_context": "收集上下文中",
+ "session.status_searching_codebase": "搜索代码库中",
+ "session.status_searching_web": "搜索网络中",
+ "session.status_writing_file": "写入文件中",
+ "session.status_running_shell": "运行 Shell 中",
+ "session.status_working": "工作中",
+ "session.status_thinking_about": "正在思考 {subject}",
+ "session.status_gathering_thoughts": "整理思路中",
+ "session.status_tool": "工具",
+ "session.status_reasoning": "推理中",
+ "session.status_draft": "草稿",
+
+ "dashboard.quick_start_browser": "自动化您的浏览器",
+ "dashboard.quick_start_browser_desc": "设置浏览器操作并从 OpenWork 运行可靠的 Web 任务。",
+ "dashboard.quick_start_soul": "赋予我灵魂",
+ "dashboard.quick_start_soul_desc": "通过轻量级计划检查,跨会话保持您的目标和偏好。",
+ "dashboard.quick_start_soul_note": "权衡:更多的自主权可能会产生额外的后台运行,但只需一个命令即可撤销。",
+ "session.search_no_matches": "无匹配",
+ "session.composer_loading_commands": "正在加载命令...",
+ "session.composer_no_commands": "未找到命令。",
+ "session.attachment_image": "图片",
+ "session.attachment_file": "文件",
+ "session.upload_to_inbox": "上传到收件箱",
+ "session.composer_remote_workspace": "远程工作区",
+ "session.attachments_unavailable": "附件不可用。",
+ "session.attach_files": "附加文件",
+ "session.agent_label": "代理",
+ "session.loading_agents": "正在加载代理...",
+ "session.default_agent": "默认代理",
+ "session.thinking_effort": "思考力度",
+ "session.active_variant": "当前",
+ "session.send_button": "发送",
+ "session.stop_button": "停止",
+ "session.toast_unsupported_type": "不支持的附件类型。",
+ "session.toast_file_too_large": "{name} 超过 8MB 限制。",
+ "session.toast_image_too_large": "{name} 编码后过大。请尝试更小的图片。",
+ "session.toast_read_failed": "读取附件失败",
+ "session.toast_remote_drop_hint": "这是一个远程工作区(沙盒也是远程的)。要与其共享文件,请上传到侧边栏的收件箱。",
+ "session.show_steps": "显示 {count} 个步骤",
+ "session.step_running": "运行中",
+ "session.copy_message": "复制消息",
+ "session.skill_badge": "技能",
+ "session.artifacts_title": "产物",
+ "session.artifacts_empty": "暂无产物。",
+ "session.artifacts_image_preview_soon": "(图片预览即将推出)",
+ "session.artifacts_show_fewer": "收起",
+ "session.artifacts_show_more": "显示更多 {count} 个",
+ // Context Panel
+ "session.context_title": "上下文",
+ "session.working_files_empty": "暂无。",
+ "session.plugins_title": "插件",
+ "session.plugins_empty": "未加载插件。",
+ "session.mcp_title": "MCP",
+ "session.mcp_empty": "未加载 MCP 服务器。",
+ "session.mcp_status_connected": "已连接",
+ "session.mcp_status_disconnected": "已断开",
+ "session.mcp_status_needs_auth": "需要认证",
+ "session.mcp_status_register": "注册客户端",
+ "session.mcp_status_failed": "失败",
+ "session.mcp_status_disabled": "已禁用",
+ "session.skills_title": "技能",
+ "session.skills_empty": "未加载技能。",
+ "session.authorized_folders_title": "授权文件夹",
+
+ // Inbox Panel
+ "session.inbox_title": "收件箱",
+ "session.inbox_refresh_tooltip": "刷新收件箱",
+ "session.inbox_drop_hint": "拖放文件到此处上传",
+ "session.inbox_upload_disabled": "连接到工作区以上传",
+ "session.inbox_uploading": "正在上传...",
+ "session.inbox_upload_cta": "拖放文件或点击上传",
+ "session.inbox_helper_text": "与您的远程工作区共享文件。",
+ "session.inbox_connect_hint": "连接以查看收件箱文件。",
+ "session.inbox_empty": "暂无收件箱文件。",
+ "session.inbox_copy_tooltip": "复制收件箱路径",
+ "session.inbox_download_tooltip": "下载",
+ "session.inbox_showing_first": "显示前 {count} 项。",
+ "session.inbox_toast_connect_upload": "连接到工作区以上传收件箱文件。",
+ "session.inbox_toast_uploading": "正在上传 {label}...",
+ "session.inbox_toast_uploaded": "已上传到工作区收件箱。",
+ "session.inbox_toast_upload_failed": "收件箱上传失败",
+ "session.inbox_toast_copied": "已复制:{path}",
+ "session.inbox_toast_copy_failed": "复制失败。您的浏览器可能会阻止剪贴板访问。",
+ "session.inbox_toast_connect_download": "连接到工作区以下载收件箱文件。",
+ "session.inbox_toast_missing_id": "缺少收件箱项目 ID。",
+ "session.inbox_toast_download_failed": "下载失败",
+ "session.inbox_toast_load_failed": "加载收件箱失败",
+
+ // Editor
+ "session.editor_default_title": "产物",
+ "session.editor_unsaved": "未保存",
+ "session.editor_reload_tooltip": "从磁盘重新加载",
+ "session.editor_reload": "重新加载",
+ "session.editor_save_tooltip": "保存 (Ctrl/Cmd+S)",
+ "session.editor_saving": "保存中...",
+ "session.editor_save": "保存",
+ "session.editor_close": "关闭",
+ "session.editor_connect_hint": "连接到 OpenWork 服务器工作区以编辑文件。",
+ "session.editor_overwrite_prompt": "文件自加载以来已更改。仍然覆盖?",
+ "session.editor_overwrite": "覆盖",
+ "session.editor_discard_prompt": "放弃未保存的更改并关闭?",
+ "session.editor_keep": "保留",
+ "session.editor_discard": "放弃",
+ "session.editor_switch_prompt": "切换到 {path}",
+ "session.editor_discard_switch": "放弃并切换",
+ "session.editor_save_switch": "保存并切换",
+ "session.editor_aria_label": "产物编辑器",
+ "session.editor_toast_save_connect": "无法保存:未连接 OpenWork 服务器",
+ "session.editor_toast_markdown_only": "仅支持 Markdown 文件",
+ "session.editor_toast_save_failed": "保存失败",
+ "session.editor_toast_reload_discard": "放弃更改以从磁盘重新加载(关闭并重新打开),或先保存。",
+ "session.editor_toast_load_failed": "加载文件失败",
+ "session.editor_toast_not_found": "文件未找到(工作区根目录或发件箱)。",
+
+ // Minimap
+ "session.minimap_user_msg": "用户消息 {index}",
+ "session.minimap_agent_msg": "代理消息 {index}",
+ "session.minimap_user": "用户",
+ "session.minimap_agent": "代理",
+ "sidebar.needs_attention": "需要注意",
+ "sidebar.expand": "展开",
+ "sidebar.collapse": "折叠",
+ "sidebar.drag_to_reorder": "拖动排序",
+ "sidebar.add_new_workspace": "添加新工作区",
+ "session.variant_none": "无",
+ "session.variant_low": "低",
+ "session.variant_medium": "中",
+ "session.variant_high": "高",
+ "session.variant_xhigh": "超高",
+ "session.click_to_expand": "点击展开粘贴的文本",
+ "session.pasted_text": "[粘贴的文本]",
+ "common.remove": "移除",
+ "sidebar.no_workspaces": "此会话中尚未添加工作区。添加一个以开始。",
+
+ // Dashboard Updates
+ "dashboard.update_ready_btn": "更新就绪",
+ "dashboard.install_update_btn": "安装更新",
+ "dashboard.downloading_btn": "正在下载",
+ "dashboard.downloading_percent_btn": "正在下载 {percent}%",
+ "dashboard.update_available_btn": "有可用更新",
+
+ // Status Bar
+ "status_bar.opencode_label": "OpenCode 引擎:{status}",
+ "status_bar.openwork_label": "OpenWork 服务器:{status}",
+ "status_bar.connected": "已连接",
+ "status_bar.not_connected": "未连接",
+ "status_bar.ready": "就绪",
+ "status_bar.limited_access": "受限访问",
+ "status_bar.unavailable": "不可用",
+ "status_bar.messaging_unavailable": "消息桥接不可用",
+ "status_bar.messaging_ready": "消息桥接就绪",
+ "status_bar.messaging_setup": "消息桥接设置",
+ "status_bar.messaging_offline": "消息桥接离线",
+ "status_bar.tip_connect_slack": "连接 Slack",
+ "status_bar.tip_connect_telegram": "连接 Telegram",
+ "status_bar.tip_connect_notion": "连接 Notion MCP",
+ "status_bar.tip_providers": "使用您自己的模型(OpenRouter, Anthropic, OpenAI)",
+ "status_bar.tip_label": "提示",
+ "status_bar.settings": "设置",
+
+ // Identities Updates
+ "identities.connected_count": "{count} 已连接",
+ "identities.not_set": "未设置",
+ "identities.telegram_peer_placeholder": "Telegram 聊天 ID (例如 123456789)",
+ "identities.slack_peer_placeholder": "Slack 对等 ID (例如 D12345678|thread_ts)",
+
+ // Skills Updates
+ "skills.worker_profile": "Worker 资料",
+ "skills.worker_profile_desc": "技能是此 Worker 的核心能力。从 Hub 添加或直接在聊天中创建新技能。",
+ "skills.create_in_chat": "在聊天中创建技能",
+ "skills.stat_installed": "已安装",
+ "skills.stat_hub_available": "Hub 可用",
+ "skills.stat_skill_creator": "技能创建器",
+ "skills.stat_not_installed": "未安装",
+ "skills.stat_mode": "模式",
+ "skills.mode_local": "本地",
+ "skills.mode_server": "服务器",
+ "skills.search_placeholder": "搜索已安装或 Hub 技能",
+ "skills.new_skill": "新建技能",
+ "skills.install_link": "从链接安装",
+ "skills.install_link_hint": "从链接安装技能",
+ "skills.no_hub_skills": "没有可用的 Hub 技能。",
+ "skills.from_hub": "来自 openwork-hub",
+ "skills.trigger_label": "触发器:{trigger}",
+ "skills.installing": "正在安装",
+ "skills.add": "添加",
+ "skills.save": "保存",
+ "skills.close": "关闭",
+ "skills.loading": "加载中...",
+ "skills.share_link_title": "分享链接",
+ "skills.share_link_desc": "发布公开链接。任何拥有该 URL 的人都可以安装此技能。",
+ "skills.publisher_label": "发布者:{url}",
+ "skills.publishing": "正在发布...",
+ "skills.create_link": "创建链接",
+ "skills.copy_link": "复制链接",
+ "skills.done": "完成",
+ "skills.install_link_title": "从链接安装",
+ "skills.install_link_desc": "粘贴技能包 URL,预览并安装。",
+ "skills.link_label": "链接",
+ "skills.preview_label": "预览",
+ "skills.skill_label": "技能:{name}",
+ "skills.conflict_warning": "已安装同名技能。",
+ "skills.keep_both": "保留两者",
+ "skills.overwrite": "覆盖",
+ "skills.install_btn": "安装",
+ "skills.installing_btn": "安装中...",
+ "skills.edit": "编辑",
+ "skills.install_title": "安装技能",
+ "skills.refresh_hub_hint": "刷新 Hub 目录",
+ "skills.refresh_hub": "刷新 Hub",
+ "skills.capability_setup": "能力设置",
} as const;