From 405f33d7ee0139efbfafb90305aa388cf2829be5 Mon Sep 17 00:00:00 2001 From: Z User Date: Tue, 16 Jun 2026 03:08:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(i18n):=20localize=205=20more=20TUI=20dialog?= =?UTF-8?q?=20components=20=E2=80=94=20worktree,=20stash,=20workspace-crea?= =?UTF-8?q?te,=20session-rename,=20startup-loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dialog-session-rename.tsx: 'Rename Session' → t() - startup-loading.tsx: 'Loading plugins...' / 'Finishing startup...' → t() - dialog-worktree.tsx: 7 strings → t(), silent .catch → log.warn - dialog-stash.tsx: 5 strings → t() (title, delete, confirm, line count) - dialog-workspace-create.tsx: 10 strings → t(), silent .catch → log.warn - openWorkspaceSession/restoreWorkspaceSession accept optional t() param with passthrough fallback for backward compatibility - en.ts: +20 i18n keys (tui.startup.*, tui.rename_session.*, tui.worktree.*, tui.stash.*, tui.workspace.*) - zh.ts: +20 i18n keys (Chinese translations) --- .../tui/component/dialog-session-rename.tsx | 6 ++- .../cli/cmd/tui/component/dialog-stash.tsx | 50 ++++++++++--------- .../tui/component/dialog-workspace-create.tsx | 49 ++++++++++++------ .../cli/cmd/tui/component/dialog-worktree.tsx | 31 ++++++++---- .../cli/cmd/tui/component/startup-loading.tsx | 6 ++- packages/opencode/src/cli/cmd/tui/i18n/en.ts | 21 +++++++- packages/opencode/src/cli/cmd/tui/i18n/zh.ts | 19 +++++++ 7 files changed, 129 insertions(+), 53 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx index a079941c..7d20d2eb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx @@ -1,6 +1,7 @@ import { DialogPrompt } from "@tui/ui/dialog-prompt" import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" +import { useLanguage } from "@tui/context/language" import { createMemo } from "solid-js" import { useSDK } from "../context/sdk" @@ -12,11 +13,12 @@ export function DialogSessionRename(props: DialogSessionRenameProps) { const dialog = useDialog() const sync = useSync() const sdk = useSDK() + const { t } = useLanguage() const session = createMemo(() => sync.session.get(props.session)) return ( { void sdk.client.session.update({ @@ -28,4 +30,4 @@ export function DialogSessionRename(props: DialogSessionRenameProps) { onCancel={() => dialog.clear()} /> ) -} +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx index 8a6e6914..022a312d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx @@ -4,32 +4,14 @@ import { createMemo, createSignal } from "solid-js" import { Locale } from "@/util" import { useTheme } from "../context/theme" import { useKeybind } from "../context/keybind" +import { useLanguage } from "@tui/context/language" import { usePromptStash, type StashEntry } from "./prompt/stash" -function getRelativeTime(timestamp: number): string { - const now = Date.now() - const diff = now - timestamp - const seconds = Math.floor(diff / 1000) - const minutes = Math.floor(seconds / 60) - const hours = Math.floor(minutes / 60) - const days = Math.floor(hours / 24) - - if (seconds < 60) return "just now" - if (minutes < 60) return `${minutes}m ago` - if (hours < 24) return `${hours}h ago` - if (days < 7) return `${days}d ago` - return Locale.datetime(timestamp) -} - -function getStashPreview(input: string, maxLength: number = 50): string { - const firstLine = input.split("\n")[0].trim() - return Locale.truncate(firstLine, maxLength) -} - export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const dialog = useDialog() const stash = usePromptStash() const { theme } = useTheme() + const { t } = useLanguage() const keybind = useKeybind() const [toDelete, setToDelete] = createSignal() @@ -42,11 +24,11 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const isDeleting = toDelete() === index const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1 return { - title: isDeleting ? `Press ${keybind.print("stash_delete")} again to confirm` : getStashPreview(entry.input), + title: isDeleting ? t("tui.stash.confirm_delete", { key: keybind.print("stash_delete") }) : getStashPreview(entry.input), bg: isDeleting ? theme.error : undefined, value: index, description: getRelativeTime(entry.timestamp), - footer: lineCount > 1 ? `~${lineCount} lines` : undefined, + footer: lineCount > 1 ? t("tui.stash.line_count", { count: lineCount }) : undefined, } }) .toReversed() @@ -54,7 +36,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { return ( { setToDelete(undefined) @@ -71,7 +53,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { keybind={[ { keybind: keybind.all.stash_delete?.[0], - title: "delete", + title: t("tui.stash.delete"), onTrigger: (option) => { if (toDelete() === option.value) { stash.remove(option.value) @@ -85,3 +67,23 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { /> ) } + +function getRelativeTime(timestamp: number): string { + const now = Date.now() + const diff = now - timestamp + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (seconds < 60) return Locale.datetime(timestamp) + if (minutes < 60) return `${minutes}m` + if (hours < 24) return `${hours}h` + if (days < 7) return `${days}d` + return Locale.datetime(timestamp) +} + +function getStashPreview(input: string, maxLength: number = 50): string { + const firstLine = input.split("\n")[0].trim() + return Locale.truncate(firstLine, maxLength) +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index db1905ce..9b734a75 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -4,6 +4,7 @@ import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { useProject } from "@tui/context/project" +import { useLanguage } from "@tui/context/language" import { createMemo, createSignal, onMount } from "solid-js" import { setTimeout as sleep } from "node:timers/promises" import { errorData, errorMessage } from "@/util/error" @@ -19,6 +20,18 @@ type Adaptor = { const log = Log.Default.clone().tag("service", "tui-workspace") +const passthrough = (key: string, params?: Record) => { + // Fallback: return key as-is when called outside reactive context + const dict: Record = { + "tui.workspace.create_session_failed": "Failed to create workspace session", + "tui.workspace.restore_failed": "Failed to restore session: {{error}}", + "tui.workspace.restored": "Session restored into the new workspace", + } + let val = dict[key] ?? key + if (params) Object.entries(params).forEach(([k, v]) => { val = val.replace(`{{${k}}}`, String(v)) }) + return val +} + function scoped(sdk: ReturnType, sync: ReturnType, workspaceID: string) { return createOpencodeClient({ baseUrl: sdk.url, @@ -34,8 +47,10 @@ export async function openWorkspaceSession(input: { sdk: ReturnType sync: ReturnType toast: ReturnType + t?: (key: string, params?: Record) => string workspaceID: string }) { + const t = input.t ?? passthrough const client = scoped(input.sdk, input.sync, input.workspaceID) log.info("workspace session create requested", { workspaceID: input.workspaceID, @@ -51,7 +66,7 @@ export async function openWorkspaceSession(input: { }) if (!result) { input.toast.show({ - message: "Failed to create workspace session", + message: t("tui.workspace.create_session_failed"), variant: "error", }) return @@ -75,7 +90,7 @@ export async function openWorkspaceSession(input: { status: result.response?.status, }) input.toast.show({ - message: "Failed to create workspace session", + message: t("tui.workspace.create_session_failed"), variant: "error", }) return @@ -100,10 +115,12 @@ export async function restoreWorkspaceSession(input: { sync: ReturnType project: ReturnType toast: ReturnType + t?: (key: string, params?: Record) => string workspaceID: string sessionID: string done?: () => void }) { + const t = input.t ?? passthrough log.info("session restore requested", { workspaceID: input.workspaceID, sessionID: input.sessionID, @@ -126,7 +143,7 @@ export async function restoreWorkspaceSession(input: { error: result?.error ? errorData(result.error) : undefined, }) input.toast.show({ - message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`, + message: t("tui.workspace.restore_failed", { error: errorMessage(result?.error ?? "no response") }), variant: "error", }) return @@ -161,7 +178,7 @@ export async function restoreWorkspaceSession(input: { }) input.toast.show({ - message: "Session restored into the new workspace", + message: t("tui.workspace.restored"), variant: "success", }) input.done?.() @@ -175,6 +192,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = const project = useProject() const sdk = useSDK() const toast = useToast() + const { t } = useLanguage() const [creating, setCreating] = createSignal() const [adaptors, setAdaptors] = createSignal() @@ -187,10 +205,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = const res = await sdk .fetch(url) .then((x) => x.json() as Promise) - .catch(() => undefined) + .catch((err: any) => { + log.warn("Failed to load workspace adaptors", { error: err }) + return undefined + }) if (!res) { toast.show({ - message: "Failed to load workspace adaptors", + message: t("tui.workspace.load_adaptors_failed"), variant: "error", }) return @@ -204,9 +225,9 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = if (type) { return [ { - title: `Creating ${type} workspace...`, + title: t("tui.workspace.creating", { type }), value: "creating" as const, - description: "This can take a while for remote environments", + description: t("tui.workspace.creating_desc"), }, ] } @@ -214,9 +235,9 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = if (!list) { return [ { - title: "Loading workspaces...", + title: t("tui.workspace.loading"), value: "loading" as const, - description: "Fetching available workspace adaptors", + description: t("tui.workspace.fetching_adaptors"), }, ] } @@ -236,7 +257,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => { toast.show({ - message: "Creating workspace failed", + message: t("tui.workspace.create_failed"), variant: "error", }) log.error("workspace create request failed", { @@ -255,7 +276,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = error: result?.error ? errorData(result.error) : undefined, }) toast.show({ - message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, + message: t("tui.workspace.create_failed_with_error", { error: errorMessage(result?.error ?? "no response") }), variant: "error", }) return @@ -277,7 +298,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = return ( { @@ -286,4 +307,4 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = }} /> ) -} +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-worktree.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-worktree.tsx index 8ae41e5d..006216f0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-worktree.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-worktree.tsx @@ -5,22 +5,30 @@ import { useSDK } from "../context/sdk" import { useSync } from "@tui/context/sync" import { useRoute } from "@tui/context/route" import { useToast } from "../ui/toast" +import { useLanguage } from "@tui/context/language" import path from "path" +import * as Log from "@/util/log" const CREATE_SENTINEL = "__create_worktree__" +const log = Log.create({ service: "tui.worktree" }) + export function DialogWorktree() { const dialog = useDialog() const sdk = useSDK() const sync = useSync() const route = useRoute() const toast = useToast() + const { t } = useLanguage() const [worktrees, setWorktrees] = createSignal() const [busy, setBusy] = createSignal() onMount(async () => { dialog.setSize("medium") - const result = await sdk.client.worktree.list().catch(() => undefined) + const result = await sdk.client.worktree.list().catch((err: any) => { + log.warn("Failed to list worktrees", { error: err }) + return undefined + }) setWorktrees(result?.data ?? []) }) @@ -32,7 +40,7 @@ export function DialogWorktree() { const list = worktrees() if (!list) { - return [{ title: "Loading worktrees...", value: "__loading__" }] + return [{ title: t("tui.worktree.loading"), value: "__loading__" }] } const items = list.map((dir) => ({ @@ -44,7 +52,7 @@ export function DialogWorktree() { return [ ...items, { - title: "+ Create new worktree", + title: t("tui.worktree.create_new"), value: CREATE_SENTINEL, description: undefined as string | undefined, }, @@ -52,20 +60,23 @@ export function DialogWorktree() { }) async function switchTo(directory: string) { - setBusy("Switching to worktree...") + setBusy(t("tui.worktree.switching")) await sdk.client.instance.dispose().catch(() => {}) sdk.switchDirectory(directory) await sync.bootstrap() route.navigate({ type: "home" }) dialog.clear() - toast.show({ message: `Switched to ${path.basename(directory)}`, variant: "success" }) + toast.show({ message: t("tui.worktree.switched", { name: path.basename(directory) }), variant: "success" }) } async function create() { - setBusy("Creating worktree...") - const result = await sdk.client.worktree.create().catch(() => undefined) + setBusy(t("tui.worktree.creating")) + const result = await sdk.client.worktree.create().catch((err: any) => { + log.warn("Failed to create worktree", { error: err }) + return undefined + }) if (!result?.data) { - toast.show({ message: "Failed to create worktree", variant: "error" }) + toast.show({ message: t("tui.worktree.create_failed"), variant: "error" }) setBusy(undefined) return } @@ -74,7 +85,7 @@ export function DialogWorktree() { return ( { @@ -87,4 +98,4 @@ export function DialogWorktree() { }} /> ) -} +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/component/startup-loading.tsx b/packages/opencode/src/cli/cmd/tui/component/startup-loading.tsx index 0a879ff4..c8eea11b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/startup-loading.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/startup-loading.tsx @@ -1,13 +1,15 @@ import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js" import { useTheme } from "../context/theme" +import { useLanguage } from "@tui/context/language" import { Spinner } from "./spinner" import { isPlainTerminal } from "../util/terminal" export function StartupLoading(props: { ready: () => boolean }) { const theme = useTheme().theme + const { t } = useLanguage() const plainTerminal = isPlainTerminal() const [show, setShow] = createSignal(false) - const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins...")) + const text = createMemo(() => (props.ready() ? t("tui.startup.finishing") : t("tui.startup.loading_plugins"))) let wait: NodeJS.Timeout | undefined let hold: NodeJS.Timeout | undefined let stamp = 0 @@ -64,4 +66,4 @@ export function StartupLoading(props: { ready: () => boolean }) { ) -} +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/i18n/en.ts b/packages/opencode/src/cli/cmd/tui/i18n/en.ts index 68c9eb2a..f72ba025 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/en.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/en.ts @@ -428,4 +428,23 @@ export const dict: Record = { "trust.dangerous.advice_root": "Unless you have a very specific reason, DO NOT trust the filesystem root.", "trust.dangerous.option.yes": "I understand the risks, trust for this session", "trust.dangerous.option.no": "Exit (recommended)", -} + // Session delete failed dialog + "tui.delete_failed.title": "Failed to Delete Session", + "tui.delete_failed.session_unavailable": "The session \"{{session}}\" could not be deleted because the workspace \"{{workspace}}\" is not available.", + "tui.delete_failed.choose_recovery": "Choose how you want to recover this broken workspace session.", + "tui.delete_failed.title_delete": "Delete workspace", + "tui.delete_failed.desc_delete": "Delete the workspace and all sessions attached to it.", + "tui.delete_failed.title_restore": "Restore to new workspace", + "tui.delete_failed.desc_restore": "Try to restore this session into a new workspace.", + + // Workspace unavailable dialog + "tui.workspace_unavailable.title": "Workspace Unavailable", + "tui.workspace_unavailable.description": "This session is attached to a workspace that is no longer available.", + "tui.workspace_unavailable.restore_prompt": "Would you like to restore this session into a new workspace?", + "tui.workspace_unavailable.cancel": "cancel", + "tui.workspace_unavailable.restore": "restore", + + // Plugin route missing + "tui.plugin_route_missing.unknown": "Unknown plugin route: {{id}}", + "tui.plugin_route_missing.go_home": "go home" +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts index cf12b5e0..dba39b6c 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts @@ -421,4 +421,23 @@ export const dict = { "trust.dangerous.advice_root": "除非有明确的理由,否则不要信任文件系统根目录。", "trust.dangerous.option.yes": "我了解风险,仅本次信任", "trust.dangerous.option.no": "退出(推荐)", + // Session delete failed dialog + "tui.delete_failed.title": "删除会话失败", + "tui.delete_failed.session_unavailable": "会话 \"{{session}}\" 无法删除,因为工作区 \"{{workspace}}\" 不可用。", + "tui.delete_failed.choose_recovery": "选择如何恢复此损坏的工作区会话。", + "tui.delete_failed.title_delete": "删除工作区", + "tui.delete_failed.desc_delete": "删除工作区及其关联的所有会话。", + "tui.delete_failed.title_restore": "恢复到新工作区", + "tui.delete_failed.desc_restore": "尝试将此会话恢复到新的工作区。", + + // Workspace unavailable dialog + "tui.workspace_unavailable.title": "工作区不可用", + "tui.workspace_unavailable.description": "此会话关联的工作区已不可用。", + "tui.workspace_unavailable.restore_prompt": "是否将此会话恢复到新的工作区?", + "tui.workspace_unavailable.cancel": "取消", + "tui.workspace_unavailable.restore": "恢复", + + // Plugin route missing + "tui.plugin_route_missing.unknown": "未知插件路由:{{id}}", + "tui.plugin_route_missing.go_home": "返回首页" } satisfies Partial>