style}
- label={(style) => (style === "unified" ? "Unified" : "Split")}
+ label={(style) =>
+ i18n.t(style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split")
+ }
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
/>
{props.actions}
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 21d00cf00a..737f4f41aa 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -9,6 +9,7 @@ import {
} from "@opencode-ai/sdk/v2/client"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
+import { type UiI18nKey, type UiI18nParams, useI18n } from "../context/i18n"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Binary } from "@opencode-ai/util/binary"
@@ -29,29 +30,31 @@ import { DateTime, DurationUnit, Interval } from "luxon"
import { createAutoScroll } from "../hooks"
import { createResizeObserver } from "@solid-primitives/resize-observer"
-function computeStatusFromPart(part: PartType | undefined): string | undefined {
+type Translator = (key: UiI18nKey, params?: UiI18nParams) => string
+
+function computeStatusFromPart(part: PartType | undefined, t: Translator): string | undefined {
if (!part) return undefined
if (part.type === "tool") {
switch (part.tool) {
case "task":
- return "Delegating work"
+ return t("ui.sessionTurn.status.delegating")
case "todowrite":
case "todoread":
- return "Planning next steps"
+ return t("ui.sessionTurn.status.planning")
case "read":
- return "Gathering context"
+ return t("ui.sessionTurn.status.gatheringContext")
case "list":
case "grep":
case "glob":
- return "Searching the codebase"
+ return t("ui.sessionTurn.status.searchingCodebase")
case "webfetch":
- return "Searching the web"
+ return t("ui.sessionTurn.status.searchingWeb")
case "edit":
case "write":
- return "Making edits"
+ return t("ui.sessionTurn.status.makingEdits")
case "bash":
- return "Running commands"
+ return t("ui.sessionTurn.status.runningCommands")
default:
return undefined
}
@@ -59,11 +62,11 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
if (part.type === "reasoning") {
const text = part.text ?? ""
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
- if (match) return `Thinking · ${match[1].trim()}`
- return "Thinking"
+ if (match) return t("ui.sessionTurn.status.thinkingWithTopic", { topic: match[1].trim() })
+ return t("ui.sessionTurn.status.thinking")
}
if (part.type === "text") {
- return "Gathering thoughts"
+ return t("ui.sessionTurn.status.gatheringThoughts")
}
return undefined
}
@@ -133,6 +136,7 @@ export function SessionTurn(
}
}>,
) {
+ const i18n = useI18n()
const data = useData()
const diffComponent = useDiffComponent()
@@ -328,12 +332,12 @@ export function SessionTurn(
const msgParts = data.store.part[msg.id] ?? emptyParts
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
- if (part) return computeStatusFromPart(part)
+ if (part) return computeStatusFromPart(part, i18n.t)
}
}
}
- return computeStatusFromPart(last)
+ return computeStatusFromPart(last, i18n.t)
})
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
@@ -368,7 +372,7 @@ export function SessionTurn(
const interval = Interval.fromDateTimes(from, to)
const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
- return interval.toDuration(unit).normalize().toHuman({
+ return interval.toDuration(unit).normalize().reconfigure({ locale: i18n.locale() }).toHuman({
notation: "compact",
unitDisplay: "narrow",
compactDisplay: "short",
@@ -532,13 +536,18 @@ export function SessionTurn(
})()}
- · retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
+ · {i18n.t("ui.sessionTurn.retry.retrying")}
+ {store.retrySeconds > 0
+ ? " " + i18n.t("ui.sessionTurn.retry.inSeconds", { seconds: store.retrySeconds })
+ : ""}
(#{retry()?.attempt})
-
{store.status ?? "Considering next steps"}
-
Hide steps
-
Show steps
+
+ {store.status ?? i18n.t("ui.sessionTurn.status.consideringNextSteps")}
+
+
{i18n.t("ui.sessionTurn.steps.hide")}
+
{i18n.t("ui.sessionTurn.steps.show")}
·
{store.duration}
@@ -580,7 +589,7 @@ export function SessionTurn(
-
Response
+ {i18n.t("ui.sessionTurn.summary.response")}
- Show more changes (
- {(data.store.session_diff?.[props.sessionID]?.length ?? 0) - store.diffLimit})
+ {i18n.t("ui.sessionTurn.diff.showMore", {
+ count: (data.store.session_diff?.[props.sessionID]?.length ?? 0) - store.diffLimit,
+ })}
diff --git a/packages/ui/src/context/i18n.tsx b/packages/ui/src/context/i18n.tsx
index fd8b05d3cf..a2ff0f37b0 100644
--- a/packages/ui/src/context/i18n.tsx
+++ b/packages/ui/src/context/i18n.tsx
@@ -3,7 +3,7 @@ import { dict as en } from "../i18n/en"
export type UiI18nKey = keyof typeof en
-export type UiI18nParams = Record
+export type UiI18nParams = Record
export type UiI18n = {
locale: Accessor
@@ -15,8 +15,7 @@ function resolveTemplate(text: string, params?: UiI18nParams) {
return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
const key = String(rawKey)
const value = params[key]
- if (value === undefined || value === null) return ""
- return String(value)
+ return value === undefined ? "" : String(value)
})
}
diff --git a/specs/07-ui-i18n-audit.md b/specs/07-ui-i18n-audit.md
index 280818c00c..f6f67db738 100644
--- a/specs/07-ui-i18n-audit.md
+++ b/specs/07-ui-i18n-audit.md
@@ -122,12 +122,12 @@ Examples (non-exhaustive):
## Prioritized Implementation Plan
-1. Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
-2. Add UI dictionaries (`packages/ui/src/i18n/en.ts`, `packages/ui/src/i18n/zh.ts`) + export them.
-3. Wire `I18nProvider` into:
+1. Completed (2026-01-20): Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
+2. Completed (2026-01-20): Add UI dictionaries (`packages/ui/src/i18n/en.ts`, `packages/ui/src/i18n/zh.ts`) + export them.
+3. Completed (2026-01-20): Wire `I18nProvider` into:
- `packages/app/src/app.tsx`
- - `packages/enterprise/src/routes/share/[shareID].tsx`
-4. Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
+ - `packages/enterprise/src/app.tsx`
+4. Completed (2026-01-20): Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
5. Convert `packages/ui/src/components/message-part.tsx`.
6. Do a full `packages/ui/src/components` + `packages/ui/src/context` audit for additional hardcoded copy.