wip(app): i18n
parent
0f2e8ea2b4
commit
9b7d9c8173
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": "忽略",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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).
|
- 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 (371 keys each; no missing or extra keys).
|
- 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".
|
Completed (2026-01-20):
|
||||||
- There is parsing logic `^Terminal (\d+)$` to compute the next number.
|
|
||||||
|
|
||||||
Recommendation:
|
- Terminal display labels are now rendered from a stable numeric `titleNumber` and localized via `terminal.title.*`.
|
||||||
- Either keep these English intentionally (stable internal naming), OR
|
- Added a one-time migration to backfill missing `titleNumber` by parsing the stored title string.
|
||||||
- Change the data model to store a stable numeric `titleNumber` and render the localized display label separately.
|
|
||||||
|
|
||||||
## 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`).
|
1. Optional: `packages/app/src/components/dialog-select-server.tsx` placeholder example URL.
|
||||||
2. Optional: `packages/app/src/components/dialog-select-server.tsx` placeholder example URL.
|
2. Optional: `packages/app/src/entry.tsx` dev-only root mount error.
|
||||||
3. 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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue