Răsfoiți Sursa

wip(app): i18n

Adam 2 luni în urmă
părinte
comite
9b7d9c8173

+ 13 - 1
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 { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { useTerminal, type LocalPTY } from "@/context/terminal"
+import { useLanguage } from "@/context/language"
 
 
 export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element {
 export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element {
   const terminal = useTerminal()
   const terminal = useTerminal()
+  const language = useLanguage()
   const sortable = createSortable(props.terminal.id)
   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 (
   return (
     // @ts-ignore
     // @ts-ignore
     <div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
     <div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
@@ -19,7 +31,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element
             )
             )
           }
           }
         >
         >
-          {props.terminal.title}
+          {label()}
         </Tabs.Trigger>
         </Tabs.Trigger>
       </div>
       </div>
     </div>
     </div>

+ 30 - 10
packages/app/src/context/terminal.tsx

@@ -1,6 +1,6 @@
 import { createStore, produce } from "solid-js/store"
 import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 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 { useParams } from "@solidjs/router"
 import { useSDK } from "./sdk"
 import { useSDK } from "./sdk"
 import { Persist, persisted } from "@/utils/persist"
 import { Persist, persisted } from "@/utils/persist"
@@ -28,6 +28,14 @@ type TerminalCacheEntry = {
 function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, session?: string) {
 function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, session?: string) {
   const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`]
   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(
   const [store, setStore, _, ready] = persisted(
     Persist.workspace(dir, "terminal", legacy),
     Persist.workspace(dir, "terminal", legacy),
     createStore<{
     createStore<{
@@ -54,24 +62,36 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
   })
   })
   onCleanup(unsub)
   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 {
   return {
     ready,
     ready,
     all: createMemo(() => Object.values(store.all)),
     all: createMemo(() => Object.values(store.all)),
     active: createMemo(() => store.active),
     active: createMemo(() => store.active),
     new() {
     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(
       const existingTitleNumbers = new Set(
         store.all.flatMap((pty) => {
         store.all.flatMap((pty) => {
           const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
           const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
           if (direct !== undefined) return [direct]
           if (direct !== undefined) return [direct]
-          const parsed = parse(pty.title)
+          const parsed = numberFromTitle(pty.title)
           if (parsed === undefined) return []
           if (parsed === undefined) return []
           return [parsed]
           return [parsed]
         }),
         }),

+ 2 - 0
packages/app/src/i18n/en.ts

@@ -384,6 +384,8 @@ export const dict = {
 
 
   "prompt.loading": "Loading prompt...",
   "prompt.loading": "Loading prompt...",
   "terminal.loading": "Loading terminal...",
   "terminal.loading": "Loading terminal...",
+  "terminal.title": "Terminal",
+  "terminal.title.numbered": "Terminal {{number}}",
 
 
   "common.closeTab": "Close tab",
   "common.closeTab": "Close tab",
   "common.dismiss": "Dismiss",
   "common.dismiss": "Dismiss",

+ 2 - 0
packages/app/src/i18n/zh.ts

@@ -381,6 +381,8 @@ export const dict = {
 
 
   "prompt.loading": "正在加载提示...",
   "prompt.loading": "正在加载提示...",
   "terminal.loading": "正在加载终端...",
   "terminal.loading": "正在加载终端...",
+  "terminal.title": "终端",
+  "terminal.title.numbered": "终端 {{number}}",
 
 
   "common.closeTab": "关闭标签页",
   "common.closeTab": "关闭标签页",
   "common.dismiss": "忽略",
   "common.dismiss": "忽略",

+ 20 - 2
packages/app/src/pages/session.tsx

@@ -1172,7 +1172,18 @@ export default function Page() {
 
 
   createEffect(() => {
   createEffect(() => {
     if (!terminal.ready()) return
     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(() => {
   createEffect(() => {
@@ -1906,7 +1917,14 @@ export default function Page() {
                       <Show when={pty()}>
                       <Show when={pty()}>
                         {(t) => (
                         {(t) => (
                           <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
                           <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
-                            {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")
+                            })()}
                           </div>
                           </div>
                         )}
                         )}
                       </Show>
                       </Show>

+ 8 - 11
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
 ## Current State
 
 
 - The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
 - 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
 ## Methodology
 
 
@@ -174,12 +174,10 @@ Completed (2026-01-20):
 
 
 File: `packages/app/src/context/terminal.tsx`
 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
 ## 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
 ## 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
 ## Suggested Key Naming Conventions
 
 
@@ -229,7 +226,7 @@ Components:
 - `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
 - `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
 
 
 Context:
 Context:
-- `packages/app/src/context/terminal.tsx` (naming)
+- (none)
 
 
 Utils:
 Utils:
 - `packages/app/src/entry.tsx` (dev-only)
 - `packages/app/src/entry.tsx` (dev-only)