wip(app): i18n

This commit is contained in:
Adam
2026-01-20 10:20:43 -06:00
parent 7e8e4d9938
commit be493e8be0
6 changed files with 68 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
interface SessionContextUsageProps { interface SessionContextUsageProps {
variant?: "button" | "indicator" variant?: "button" | "indicator"
@@ -16,6 +17,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const sync = useSync() const sync = useSync()
const params = useParams() const params = useParams()
const layout = useLayout() const layout = useLayout()
const language = useLanguage()
const variant = createMemo(() => props.variant ?? "button") const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
@@ -24,14 +26,16 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const cost = createMemo(() => { const cost = createMemo(() => {
const locale = language.locale()
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat(locale, {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(total) }).format(total)
}) })
const context = createMemo(() => { const context = createMemo(() => {
const locale = language.locale()
const last = messages().findLast((x) => { const last = messages().findLast((x) => {
if (x.role !== "assistant") return false if (x.role !== "assistant") return false
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
@@ -42,7 +46,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID] const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
return { return {
tokens: total.toLocaleString(), tokens: total.toLocaleString(locale),
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null, percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
} }
}) })
@@ -67,21 +71,21 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<> <>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().tokens}</span> <span class="text-text-invert-strong">{ctx().tokens}</span>
<span class="text-text-invert-base">Tokens</span> <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span> <span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
<span class="text-text-invert-base">Usage</span> <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
</div> </div>
</> </>
)} )}
</Show> </Show>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-text-invert-strong">{cost()}</span> <span class="text-text-invert-strong">{cost()}</span>
<span class="text-text-invert-base">Cost</span> <span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
</div> </div>
<Show when={variant() === "button"}> <Show when={variant() === "button"}>
<div class="text-11-regular text-text-invert-base mt-1">Click to view context</div> <div class="text-11-regular text-text-invert-base mt-1">{language.t("context.usage.clickToView")}</div>
</Show> </Show>
</div> </div>
) )

View File

@@ -61,8 +61,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
}) })
const cost = createMemo(() => { const cost = createMemo(() => {
const locale = language.locale()
const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat(locale, {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(total) }).format(total)
@@ -91,18 +92,18 @@ export function SessionContextTab(props: SessionContextTabProps) {
const number = (value: number | null | undefined) => { const number = (value: number | null | undefined) => {
if (value === undefined) return "—" if (value === undefined) return "—"
if (value === null) return "—" if (value === null) return "—"
return value.toLocaleString() return value.toLocaleString(language.locale())
} }
const percent = (value: number | null | undefined) => { const percent = (value: number | null | undefined) => {
if (value === undefined) return "—" if (value === undefined) return "—"
if (value === null) return "—" if (value === null) return "—"
return value.toString() + "%" return value.toLocaleString(language.locale()) + "%"
} }
const time = (value: number | undefined) => { const time = (value: number | undefined) => {
if (!value) return "—" if (!value) return "—"
return DateTime.fromMillis(value).toLocaleString(DateTime.DATETIME_MED) return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED)
} }
const providerLabel = createMemo(() => { const providerLabel = createMemo(() => {
@@ -246,7 +247,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
const count = counts() const count = counts()
return [ return [
{ label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" }, { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString() }, { label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
{ label: language.t("context.stats.provider"), value: providerLabel() }, { label: language.t("context.stats.provider"), value: providerLabel() },
{ label: language.t("context.stats.model"), value: modelLabel() }, { label: language.t("context.stats.model"), value: modelLabel() },
{ label: language.t("context.stats.limit"), value: number(c?.limit) }, { label: language.t("context.stats.limit"), value: number(c?.limit) },
@@ -259,8 +260,8 @@ export function SessionContextTab(props: SessionContextTabProps) {
label: language.t("context.stats.cacheTokens"), label: language.t("context.stats.cacheTokens"),
value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`, value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
}, },
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString() }, { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
{ label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString() }, { label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString(language.locale()) },
{ label: language.t("context.stats.totalCost"), value: cost() }, { label: language.t("context.stats.totalCost"), value: cost() },
{ label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) }, { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
{ label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) }, { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },

View File

@@ -1,6 +1,7 @@
import { Show, createMemo } from "solid-js" import { Show, createMemo } from "solid-js"
import { DateTime } from "luxon" import { DateTime } from "luxon"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
export function NewSessionView(props: NewSessionViewProps) { export function NewSessionView(props: NewSessionViewProps) {
const sync = useSync() const sync = useSync()
const language = useLanguage()
const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
@@ -32,13 +34,13 @@ export function NewSessionView(props: NewSessionViewProps) {
const label = (value: string) => { const label = (value: string) => {
if (value === MAIN_WORKTREE) { if (value === MAIN_WORKTREE) {
if (isWorktree()) return "Main branch" if (isWorktree()) return language.t("session.new.worktree.main")
const branch = sync.data.vcs?.branch const branch = sync.data.vcs?.branch
if (branch) return `Main branch (${branch})` if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
return "Main branch" return language.t("session.new.worktree.main")
} }
if (value === CREATE_WORKTREE) return "Create new worktree" if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
return getFilename(value) return getFilename(value)
} }
@@ -48,7 +50,7 @@ export function NewSessionView(props: NewSessionViewProps) {
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6" class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }} style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
> >
<div class="text-20-medium text-text-weaker">New session</div> <div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
<div class="flex justify-center items-center gap-3"> <div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" /> <Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak"> <div class="text-12-medium text-text-weak">
@@ -76,9 +78,11 @@ export function NewSessionView(props: NewSessionViewProps) {
<div class="flex justify-center items-center gap-3"> <div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" /> <Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak"> <div class="text-12-medium text-text-weak">
Last modified&nbsp; {language.t("session.new.lastModified")}&nbsp;
<span class="text-text-strong"> <span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()} {DateTime.fromMillis(project().time.updated ?? project().time.created)
.setLocale(language.locale())
.toRelative()}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -256,6 +256,11 @@ export const dict = {
"context.stats.sessionCreated": "Session Created", "context.stats.sessionCreated": "Session Created",
"context.stats.lastActivity": "Last Activity", "context.stats.lastActivity": "Last Activity",
"context.usage.tokens": "Tokens",
"context.usage.usage": "Usage",
"context.usage.cost": "Cost",
"context.usage.clickToView": "Click to view context",
"language.en": "English", "language.en": "English",
"language.zh": "Chinese", "language.zh": "Chinese",
@@ -344,6 +349,11 @@ export const dict = {
"session.context.addToContext": "Add {{selection}} to context", "session.context.addToContext": "Add {{selection}} to context",
"session.new.worktree.main": "Main branch",
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
"session.new.worktree.create": "Create new worktree",
"session.new.lastModified": "Last modified",
"session.header.search.placeholder": "Search {{project}}", "session.header.search.placeholder": "Search {{project}}",
"session.share.popover.title": "Publish on web", "session.share.popover.title": "Publish on web",

View File

@@ -255,6 +255,11 @@ export const dict = {
"context.stats.sessionCreated": "创建时间", "context.stats.sessionCreated": "创建时间",
"context.stats.lastActivity": "最后活动", "context.stats.lastActivity": "最后活动",
"context.usage.tokens": "Token",
"context.usage.usage": "使用率",
"context.usage.cost": "成本",
"context.usage.clickToView": "点击查看上下文",
"language.en": "英语", "language.en": "英语",
"language.zh": "中文", "language.zh": "中文",
@@ -341,6 +346,11 @@ export const dict = {
"session.context.addToContext": "将 {{selection}} 添加到上下文", "session.context.addToContext": "将 {{selection}} 添加到上下文",
"session.new.worktree.main": "主分支",
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
"session.new.worktree.create": "创建新的 worktree",
"session.new.lastModified": "最后修改",
"session.header.search.placeholder": "搜索 {{project}}", "session.header.search.placeholder": "搜索 {{project}}",
"session.share.popover.title": "发布到网页", "session.share.popover.title": "发布到网页",

View File

@@ -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` (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` (plus new keys added in both dictionaries).
- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (354 keys each; no missing or extra keys). - Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (362 keys each; no missing or extra keys).
## Methodology ## Methodology
@@ -76,32 +76,30 @@ Completed (2026-01-20):
File: `packages/app/src/components/session/session-new-view.tsx` File: `packages/app/src/components/session/session-new-view.tsx`
**Untranslated strings** Completed (2026-01-20):
- "New session"
- "Main branch" / "Main branch ({branch})" - Reused existing `command.session.new` for the heading.
- "Create new worktree" - Localized worktree labels via `session.new.worktree.*` (main branch, main branch w/ branch name, create worktree).
- "Last modified" - Localized "Last modified" via `session.new.lastModified` and used `language.locale()` for Luxon relative time.
### 6) Context Usage Tooltip ### 6) Context Usage Tooltip
File: `packages/app/src/components/session-context-usage.tsx` File: `packages/app/src/components/session-context-usage.tsx`
**Untranslated tooltip labels** Completed (2026-01-20):
- "Tokens", "Usage", "Cost"
- "Click to view context"
**Locale formatting issue** - Localized tooltip labels + CTA via `context.usage.*` keys.
- Uses `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })`. - Switched currency and number formatting to the active locale (`language.locale()`).
- Recommendation: format using the active locale (e.g. `language.locale()`), or at least `navigator.language`.
### 7) Session Context Tab (Formatting) ### 7) Session Context Tab (Formatting)
File: `packages/app/src/components/session/session-context-tab.tsx` File: `packages/app/src/components/session/session-context-tab.tsx`
- Already uses many translation keys for labels (e.g. `context.breakdown.system`). Completed (2026-01-20):
- Still forces `Intl.NumberFormat("en-US", ...)` for currency.
- Has some non-translated fallback symbols like "--" and "-" style output (e.g. "---" / "-" / "--" equivalent "--" is used as "—" in code). - Switched currency formatting to the active locale (`language.locale()`).
- If you want fully localized punctuation, these should become keys as well. - Also used `language.locale()` for number/date formatting.
- Note: "—" placeholders remain hardcoded; optional to localize.
### 8) LSP Indicator ### 8) LSP Indicator
@@ -211,18 +209,18 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
## Prioritized Implementation Plan ## Prioritized Implementation Plan
1. `packages/app/src/components/session/session-new-view.tsx` 1. Small stragglers:
2. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`)
3. Small stragglers:
- `packages/app/src/components/session-lsp-indicator.tsx` - `packages/app/src/components/session-lsp-indicator.tsx`
- `packages/app/src/components/session/session-sortable-tab.tsx` - `packages/app/src/components/session/session-sortable-tab.tsx`
- `packages/app/src/components/titlebar.tsx` - `packages/app/src/components/titlebar.tsx`
- `packages/app/src/components/dialog-select-model.tsx` - `packages/app/src/components/dialog-select-model.tsx`
- `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
2. Context modules:
- `packages/app/src/context/notification.tsx` - `packages/app/src/context/notification.tsx`
- `packages/app/src/context/global-sync.tsx` - `packages/app/src/context/global-sync.tsx`
- `packages/app/src/context/file.tsx` + `packages/app/src/context/local.tsx` - `packages/app/src/context/file.tsx` + `packages/app/src/context/local.tsx`
- `packages/app/src/utils/prompt.ts` - `packages/app/src/utils/prompt.ts`
4. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`). 3. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
## Suggested Key Naming Conventions ## Suggested Key Naming Conventions
@@ -245,9 +243,6 @@ Pages:
- (none) - (none)
Components: Components:
- `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` (formatting locale)
- `packages/app/src/components/session-lsp-indicator.tsx` - `packages/app/src/components/session-lsp-indicator.tsx`
- `packages/app/src/components/session/session-sortable-tab.tsx` - `packages/app/src/components/session/session-sortable-tab.tsx`
- `packages/app/src/components/titlebar.tsx` - `packages/app/src/components/titlebar.tsx`