Ver Fonte

wip(app): i18n

Adam há 2 meses atrás
pai
commit
be493e8be0

+ 10 - 6
packages/app/src/components/session-context-usage.tsx

@@ -7,6 +7,7 @@ import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
 
 import { useLayout } from "@/context/layout"
 import { useSync } from "@/context/sync"
+import { useLanguage } from "@/context/language"
 
 interface SessionContextUsageProps {
   variant?: "button" | "indicator"
@@ -16,6 +17,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
   const sync = useSync()
   const params = useParams()
   const layout = useLayout()
+  const language = useLanguage()
 
   const variant = createMemo(() => props.variant ?? "button")
   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 cost = createMemo(() => {
+    const locale = language.locale()
     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",
       currency: "USD",
     }).format(total)
   })
 
   const context = createMemo(() => {
+    const locale = language.locale()
     const last = messages().findLast((x) => {
       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
@@ -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
     const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
     return {
-      tokens: total.toLocaleString(),
+      tokens: total.toLocaleString(locale),
       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">
               <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 class="flex items-center gap-2">
               <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>
           </>
         )}
       </Show>
       <div class="flex items-center gap-2">
         <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>
       <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>
     </div>
   )

+ 8 - 7
packages/app/src/components/session/session-context-tab.tsx

@@ -61,8 +61,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
   })
 
   const cost = createMemo(() => {
+    const locale = language.locale()
     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",
       currency: "USD",
     }).format(total)
@@ -91,18 +92,18 @@ export function SessionContextTab(props: SessionContextTabProps) {
   const number = (value: number | null | undefined) => {
     if (value === undefined) return "—"
     if (value === null) return "—"
-    return value.toLocaleString()
+    return value.toLocaleString(language.locale())
   }
 
   const percent = (value: number | null | undefined) => {
     if (value === undefined) return "—"
     if (value === null) return "—"
-    return value.toString() + "%"
+    return value.toLocaleString(language.locale()) + "%"
   }
 
   const time = (value: number | undefined) => {
     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(() => {
@@ -246,7 +247,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
     const count = counts()
     return [
       { 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.model"), value: modelLabel() },
       { 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"),
         value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
       },
-      { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString() },
-      { label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString() },
+      { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
+      { 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.sessionCreated"), value: time(props.info()?.time.created) },
       { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },

+ 11 - 7
packages/app/src/components/session/session-new-view.tsx

@@ -1,6 +1,7 @@
 import { Show, createMemo } from "solid-js"
 import { DateTime } from "luxon"
 import { useSync } from "@/context/sync"
+import { useLanguage } from "@/context/language"
 import { Icon } from "@opencode-ai/ui/icon"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { Select } from "@opencode-ai/ui/select"
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
 
 export function NewSessionView(props: NewSessionViewProps) {
   const sync = useSync()
+  const language = useLanguage()
 
   const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
   const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
@@ -32,13 +34,13 @@ export function NewSessionView(props: NewSessionViewProps) {
 
   const label = (value: string) => {
     if (value === MAIN_WORKTREE) {
-      if (isWorktree()) return "Main branch"
+      if (isWorktree()) return language.t("session.new.worktree.main")
       const branch = sync.data.vcs?.branch
-      if (branch) return `Main branch (${branch})`
-      return "Main branch"
+      if (branch) return language.t("session.new.worktree.mainWithBranch", { 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)
   }
@@ -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"
       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">
         <Icon name="folder" size="small" />
         <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">
             <Icon name="pencil-line" size="small" />
             <div class="text-12-medium text-text-weak">
-              Last modified&nbsp;
+              {language.t("session.new.lastModified")}&nbsp;
               <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>
             </div>
           </div>

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

@@ -256,6 +256,11 @@ export const dict = {
   "context.stats.sessionCreated": "Session Created",
   "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.zh": "Chinese",
 
@@ -344,6 +349,11 @@ export const dict = {
 
   "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.share.popover.title": "Publish on web",

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

@@ -255,6 +255,11 @@ export const dict = {
   "context.stats.sessionCreated": "创建时间",
   "context.stats.lastActivity": "最后活动",
 
+  "context.usage.tokens": "Token",
+  "context.usage.usage": "使用率",
+  "context.usage.cost": "成本",
+  "context.usage.clickToView": "点击查看上下文",
+
   "language.en": "英语",
   "language.zh": "中文",
 
@@ -341,6 +346,11 @@ export const dict = {
 
   "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.share.popover.title": "发布到网页",

+ 19 - 24
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` (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).
+- 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 (362 keys each; no missing or extra keys).
 
 ## Methodology
 
@@ -76,32 +76,30 @@ Completed (2026-01-20):
 
 File: `packages/app/src/components/session/session-new-view.tsx`
 
-**Untranslated strings**
-- "New session"
-- "Main branch" / "Main branch ({branch})"
-- "Create new worktree"
-- "Last modified"
+Completed (2026-01-20):
+
+- Reused existing `command.session.new` for the heading.
+- Localized worktree labels via `session.new.worktree.*` (main branch, main branch w/ branch name, create worktree).
+- Localized "Last modified" via `session.new.lastModified` and used `language.locale()` for Luxon relative time.
 
 ### 6) Context Usage Tooltip
 
 File: `packages/app/src/components/session-context-usage.tsx`
 
-**Untranslated tooltip labels**
-- "Tokens", "Usage", "Cost"
-- "Click to view context"
+Completed (2026-01-20):
 
-**Locale formatting issue**
-- Uses `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })`.
-- Recommendation: format using the active locale (e.g. `language.locale()`), or at least `navigator.language`.
+- Localized tooltip labels + CTA via `context.usage.*` keys.
+- Switched currency and number formatting to the active locale (`language.locale()`).
 
 ### 7) Session Context Tab (Formatting)
 
 File: `packages/app/src/components/session/session-context-tab.tsx`
 
-- Already uses many translation keys for labels (e.g. `context.breakdown.system`).
-- 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).
-  - If you want fully localized punctuation, these should become keys as well.
+Completed (2026-01-20):
+
+- Switched currency formatting to the active locale (`language.locale()`).
+- Also used `language.locale()` for number/date formatting.
+- Note: "—" placeholders remain hardcoded; optional to localize.
 
 ### 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
 
-1. `packages/app/src/components/session/session-new-view.tsx`
-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:
+1. Small stragglers:
    - `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/components/dialog-select-server.tsx` (optional URL placeholder)
+2. Context modules:
    - `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`
-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
 
@@ -245,9 +243,6 @@ Pages:
 - (none)
 
 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/session-sortable-tab.tsx`
 - `packages/app/src/components/titlebar.tsx`