From 9b7d9c8173c222c880cf731b859fc78fed5265fc Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:50:46 -0600 Subject: [PATCH] wip(app): i18n --- .../session/session-sortable-terminal-tab.tsx | 14 ++++++- packages/app/src/context/terminal.tsx | 40 ++++++++++++++----- packages/app/src/i18n/en.ts | 2 + packages/app/src/i18n/zh.ts | 2 + packages/app/src/pages/session.tsx | 22 +++++++++- specs/06-app-i18n-audit.md | 19 ++++----- 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index d20f587f4..0e387b9fb 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -3,10 +3,22 @@ import { createSortable } from "@thisbeyond/solid-dnd" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" import { useTerminal, type LocalPTY } from "@/context/terminal" +import { useLanguage } from "@/context/language" export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element { const terminal = useTerminal() + const language = useLanguage() const sortable = createSortable(props.terminal.id) + + const label = () => { + language.locale() + const number = props.terminal.titleNumber + if (Number.isFinite(number) && number > 0) { + return language.t("terminal.title.numbered", { number }) + } + if (props.terminal.title) return props.terminal.title + return language.t("terminal.title") + } return ( // @ts-ignore
@@ -19,7 +31,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element ) } > - {props.terminal.title} + {label()}
diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 5732114b4..8bde12da1 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -1,6 +1,6 @@ import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createMemo, createRoot, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" import { Persist, persisted } from "@/utils/persist" @@ -28,6 +28,14 @@ type TerminalCacheEntry = { function createTerminalSession(sdk: ReturnType, dir: string, session?: string) { const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`] + const numberFromTitle = (title: string) => { + const match = title.match(/^Terminal (\d+)$/) + if (!match) return + const value = Number(match[1]) + if (!Number.isFinite(value) || value <= 0) return + return value + } + const [store, setStore, _, ready] = persisted( Persist.workspace(dir, "terminal", legacy), createStore<{ @@ -54,24 +62,36 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess }) onCleanup(unsub) + const meta = { migrated: false } + + createEffect(() => { + if (!ready()) return + if (meta.migrated) return + meta.migrated = true + + setStore("all", (all) => { + const next = all.map((pty) => { + const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined + if (direct !== undefined) return pty + const parsed = numberFromTitle(pty.title) + if (parsed === undefined) return pty + return { ...pty, titleNumber: parsed } + }) + if (next.every((pty, index) => pty === all[index])) return all + return next + }) + }) + return { ready, all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), new() { - const parse = (title: string) => { - const match = title.match(/^Terminal (\d+)$/) - if (!match) return - const value = Number(match[1]) - if (!Number.isFinite(value) || value <= 0) return - return value - } - const existingTitleNumbers = new Set( store.all.flatMap((pty) => { const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined if (direct !== undefined) return [direct] - const parsed = parse(pty.title) + const parsed = numberFromTitle(pty.title) if (parsed === undefined) return [] return [parsed] }), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 34fec6177..3d35e8f86 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -384,6 +384,8 @@ export const dict = { "prompt.loading": "Loading prompt...", "terminal.loading": "Loading terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", "common.closeTab": "Close tab", "common.dismiss": "Dismiss", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 7ae62350d..242d8a170 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -381,6 +381,8 @@ export const dict = { "prompt.loading": "正在加载提示...", "terminal.loading": "正在加载终端...", + "terminal.title": "终端", + "terminal.title.numbered": "终端 {{number}}", "common.closeTab": "关闭标签页", "common.dismiss": "忽略", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 7733784f9..ebc6b8c23 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1172,7 +1172,18 @@ export default function Page() { createEffect(() => { if (!terminal.ready()) return - handoff.terminals = terminal.all().map((t) => t.title) + language.locale() + + const label = (pty: LocalPTY) => { + const number = pty.titleNumber + if (Number.isFinite(number) && number > 0) { + return language.t("terminal.title.numbered", { number }) + } + if (pty.title) return pty.title + return language.t("terminal.title") + } + + handoff.terminals = terminal.all().map(label) }) createEffect(() => { @@ -1906,7 +1917,14 @@ export default function Page() { {(t) => (
- {t().title} + {(() => { + const number = t().titleNumber + if (Number.isFinite(number) && number > 0) { + return language.t("terminal.title.numbered", { number }) + } + if (t().title) return t().title + return language.t("terminal.title") + })()}
)}
diff --git a/specs/06-app-i18n-audit.md b/specs/06-app-i18n-audit.md index 42d0c0c8d..c933d842d 100644 --- a/specs/06-app-i18n-audit.md +++ b/specs/06-app-i18n-audit.md @@ -9,8 +9,8 @@ This report documents the remaining user-facing strings in `packages/app/src` th ## Current State - The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`. -- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/app/src/components/dialog-connect-provider.tsx`, `packages/app/src/components/session/session-header.tsx`, `packages/app/src/pages/error.tsx`, `packages/app/src/components/session/session-new-view.tsx`, `packages/app/src/components/session-context-usage.tsx`, `packages/app/src/components/session/session-context-tab.tsx`, `packages/app/src/components/session-lsp-indicator.tsx`, `packages/app/src/components/session/session-sortable-tab.tsx`, `packages/app/src/components/titlebar.tsx`, `packages/app/src/components/dialog-select-model.tsx`, `packages/app/src/context/notification.tsx`, `packages/app/src/context/global-sync.tsx`, `packages/app/src/context/file.tsx`, `packages/app/src/context/local.tsx`, `packages/app/src/utils/prompt.ts` (plus new keys added in both dictionaries). -- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (371 keys each; no missing or extra keys). +- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/app/src/components/dialog-connect-provider.tsx`, `packages/app/src/components/session/session-header.tsx`, `packages/app/src/pages/error.tsx`, `packages/app/src/components/session/session-new-view.tsx`, `packages/app/src/components/session-context-usage.tsx`, `packages/app/src/components/session/session-context-tab.tsx`, `packages/app/src/components/session-lsp-indicator.tsx`, `packages/app/src/components/session/session-sortable-tab.tsx`, `packages/app/src/components/titlebar.tsx`, `packages/app/src/components/dialog-select-model.tsx`, `packages/app/src/context/notification.tsx`, `packages/app/src/context/global-sync.tsx`, `packages/app/src/context/file.tsx`, `packages/app/src/context/local.tsx`, `packages/app/src/utils/prompt.ts`, `packages/app/src/context/terminal.tsx`, `packages/app/src/components/session/session-sortable-terminal-tab.tsx` (plus new keys added in both dictionaries). +- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (373 keys each; no missing or extra keys). ## Methodology @@ -174,12 +174,10 @@ Completed (2026-01-20): File: `packages/app/src/context/terminal.tsx` -- User-visible terminal titles are generated as "Terminal" and "Terminal N". -- There is parsing logic `^Terminal (\d+)$` to compute the next number. +Completed (2026-01-20): -Recommendation: -- Either keep these English intentionally (stable internal naming), OR -- Change the data model to store a stable numeric `titleNumber` and render the localized display label separately. +- Terminal display labels are now rendered from a stable numeric `titleNumber` and localized via `terminal.title.*`. +- Added a one-time migration to backfill missing `titleNumber` by parsing the stored title string. ## Low Priority: Utils / Dev-Only Copy @@ -201,9 +199,8 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr ## Prioritized Implementation Plan -1. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`). -2. Optional: `packages/app/src/components/dialog-select-server.tsx` placeholder example URL. -3. Optional: `packages/app/src/entry.tsx` dev-only root mount error. +1. Optional: `packages/app/src/components/dialog-select-server.tsx` placeholder example URL. +2. Optional: `packages/app/src/entry.tsx` dev-only root mount error. ## Suggested Key Naming Conventions @@ -229,7 +226,7 @@ Components: - `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder) Context: -- `packages/app/src/context/terminal.tsx` (naming) +- (none) Utils: - `packages/app/src/entry.tsx` (dev-only)