Explorar o código

chore(app): i18n sync (#17283)

Adam hai 1 mes
pai
achega
05cb3c87ca
Modificáronse 65 ficheiros con 1776 adicións e 156 borrados
  1. 11 4
      packages/app/src/app.tsx
  2. 38 32
      packages/app/src/components/debug-bar.tsx
  3. 1 1
      packages/app/src/components/dialog-select-file.tsx
  4. 2 2
      packages/app/src/components/dialog-select-server.tsx
  5. 3 1
      packages/app/src/components/server/server-row.tsx
  6. 28 26
      packages/app/src/components/session/session-header.tsx
  7. 2 5
      packages/app/src/components/session/session-sortable-terminal-tab.tsx
  8. 1 1
      packages/app/src/components/settings-keybinds.tsx
  9. 1 1
      packages/app/src/components/terminal.tsx
  10. 51 10
      packages/app/src/context/command.tsx
  11. 3 3
      packages/app/src/context/file.tsx
  12. 4 2
      packages/app/src/context/global-sdk.tsx
  13. 1 0
      packages/app/src/context/global-sync.tsx
  14. 1 1
      packages/app/src/context/global-sync/bootstrap.ts
  15. 1 0
      packages/app/src/context/global-sync/child-store.test.ts
  16. 5 4
      packages/app/src/context/global-sync/child-store.ts
  17. 51 0
      packages/app/src/context/terminal-title.ts
  18. 4 7
      packages/app/src/context/terminal.tsx
  19. 73 0
      packages/app/src/i18n/ar.ts
  20. 75 0
      packages/app/src/i18n/br.ts
  21. 75 0
      packages/app/src/i18n/bs.ts
  22. 75 0
      packages/app/src/i18n/da.ts
  23. 76 0
      packages/app/src/i18n/de.ts
  24. 76 0
      packages/app/src/i18n/en.ts
  25. 75 0
      packages/app/src/i18n/es.ts
  26. 77 0
      packages/app/src/i18n/fr.ts
  27. 74 0
      packages/app/src/i18n/ja.ts
  28. 74 0
      packages/app/src/i18n/ko.ts
  29. 75 0
      packages/app/src/i18n/no.ts
  30. 76 0
      packages/app/src/i18n/pl.ts
  31. 75 0
      packages/app/src/i18n/ru.ts
  32. 75 0
      packages/app/src/i18n/th.ts
  33. 74 0
      packages/app/src/i18n/tr.ts
  34. 73 0
      packages/app/src/i18n/zh.ts
  35. 73 0
      packages/app/src/i18n/zht.ts
  36. 17 15
      packages/app/src/pages/error.tsx
  37. 1 1
      packages/app/src/pages/layout.tsx
  38. 5 3
      packages/app/src/pages/session.tsx
  39. 0 1
      packages/app/src/pages/session/composer/session-composer-region.tsx
  40. 1 1
      packages/app/src/pages/session/composer/session-question-dock.tsx
  41. 22 6
      packages/app/src/pages/session/composer/session-todo-dock.tsx
  42. 3 3
      packages/app/src/pages/session/terminal-label.ts
  43. 4 1
      packages/ui/src/components/basic-tool.tsx
  44. 7 4
      packages/ui/src/components/file-search.tsx
  45. 5 3
      packages/ui/src/components/line-comment-annotations.tsx
  46. 11 9
      packages/ui/src/components/message-part.tsx
  47. 10 6
      packages/ui/src/components/tool-error-card.tsx
  48. 12 0
      packages/ui/src/i18n/ar.ts
  49. 12 0
      packages/ui/src/i18n/br.ts
  50. 12 0
      packages/ui/src/i18n/bs.ts
  51. 12 0
      packages/ui/src/i18n/da.ts
  52. 12 0
      packages/ui/src/i18n/de.ts
  53. 13 0
      packages/ui/src/i18n/en.ts
  54. 12 0
      packages/ui/src/i18n/es.ts
  55. 12 0
      packages/ui/src/i18n/fr.ts
  56. 12 0
      packages/ui/src/i18n/ja.ts
  57. 12 0
      packages/ui/src/i18n/ko.ts
  58. 12 0
      packages/ui/src/i18n/no.ts
  59. 12 0
      packages/ui/src/i18n/pl.ts
  60. 12 0
      packages/ui/src/i18n/ru.ts
  61. 12 0
      packages/ui/src/i18n/th.ts
  62. 12 0
      packages/ui/src/i18n/tr.ts
  63. 12 0
      packages/ui/src/i18n/zh.ts
  64. 12 0
      packages/ui/src/i18n/zht.ts
  65. 6 3
      packages/ui/src/pierre/selection-bridge.ts

+ 11 - 4
packages/app/src/app.tsx

@@ -12,6 +12,7 @@ import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
 import { type Duration, Effect } from "effect"
 import {
   type Component,
+  createMemo,
   createResource,
   createSignal,
   ErrorBoundary,
@@ -67,7 +68,7 @@ const SessionIndexRoute = () => <Navigate href="session" />
 
 function UiI18nBridge(props: ParentProps) {
   const language = useLanguage()
-  return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
+  return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
 }
 
 declare global {
@@ -218,8 +219,12 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
 }
 
 function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
+  const language = useLanguage()
   const server = useServer()
   const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
+  const name = createMemo(() => server.name || server.key)
+  const serverToken = "\u0000server\u0000"
+  const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
 
   const timer = setInterval(() => props.onRetry?.(), 1000)
   onCleanup(() => clearInterval(timer))
@@ -229,13 +234,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
       <div class="flex flex-col items-center max-w-md text-center">
         <Splash class="w-12 h-15 mb-4" />
         <p class="text-14-regular text-text-base">
-          Could not reach <span class="text-text-strong font-medium">{server.name || server.key}</span>
+          {unreachable()[0]}
+          <span class="text-text-strong font-medium">{name()}</span>
+          {unreachable()[1]}
         </p>
-        <p class="mt-1 text-12-regular text-text-weak">Retrying automatically...</p>
+        <p class="mt-1 text-12-regular text-text-weak">{language.t("app.server.retrying")}</p>
       </div>
       <Show when={others().length > 0}>
         <div class="flex flex-col gap-2 w-full max-w-sm">
-          <span class="text-12-regular text-text-base text-center">Other servers</span>
+          <span class="text-12-regular text-text-base text-center">{language.t("app.server.otherServers")}</span>
           <div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2">
             <For each={others()}>
               {(conn) => {

+ 38 - 32
packages/app/src/components/debug-bar.tsx

@@ -2,6 +2,7 @@ import { useIsRouting, useLocation } from "@solidjs/router"
 import { batch, createEffect, onCleanup, onMount } from "solid-js"
 import { createStore } from "solid-js/store"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { useLanguage } from "@/context/language"
 
 type Mem = Performance & {
   memory?: {
@@ -27,17 +28,17 @@ type Obs = PerformanceObserverInit & {
 const span = 5000
 
 const ms = (n?: number, d = 0) => {
-  if (n === undefined || Number.isNaN(n)) return "n/a"
+  if (n === undefined || Number.isNaN(n)) return
   return `${n.toFixed(d)}ms`
 }
 
 const time = (n?: number) => {
-  if (n === undefined || Number.isNaN(n)) return "n/a"
+  if (n === undefined || Number.isNaN(n)) return
   return `${Math.round(n)}`
 }
 
 const mb = (n?: number) => {
-  if (n === undefined || Number.isNaN(n)) return "n/a"
+  if (n === undefined || Number.isNaN(n)) return
   const v = n / 1024 / 1024
   return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB`
 }
@@ -74,6 +75,7 @@ function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string;
 }
 
 export function DebugBar() {
+  const language = useLanguage()
   const location = useLocation()
   const routing = useIsRouting()
   const [state, setState] = createStore({
@@ -98,14 +100,15 @@ export function DebugBar() {
     },
   })
 
+  const na = () => language.t("debugBar.na")
   const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
   const heapv = () => {
     const value = heap()
-    if (value === undefined) return "n/a"
+    if (value === undefined) return na()
     return `${Math.round(value * 100)}%`
   }
-  const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`)
-  const navv = () => (state.nav.pending ? "..." : time(state.nav.dur))
+  const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`)
+  const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na()))
 
   let prev = ""
   let start = 0
@@ -359,7 +362,7 @@ export function DebugBar() {
 
   return (
     <aside
-      aria-label="Development performance diagnostics"
+      aria-label={language.t("debugBar.ariaLabel")}
       class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
       style={{
         "background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
@@ -368,67 +371,70 @@ export function DebugBar() {
     >
       <div class="grid grid-cols-5 gap-px font-mono">
         <Cell
-          label="NAV"
-          tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles."
+          label={language.t("debugBar.nav.label")}
+          tip={language.t("debugBar.nav.tip")}
           value={navv()}
           bad={bad(state.nav.dur, 400)}
           dim={state.nav.dur === undefined && !state.nav.pending}
         />
         <Cell
-          label="FPS"
-          tip="Rolling frames per second over the last 5 seconds."
-          value={state.fps === undefined ? "n/a" : `${Math.round(state.fps)}`}
+          label={language.t("debugBar.fps.label")}
+          tip={language.t("debugBar.fps.tip")}
+          value={state.fps === undefined ? na() : `${Math.round(state.fps)}`}
           bad={bad(state.fps, 50, true)}
           dim={state.fps === undefined}
         />
         <Cell
-          label="FRAME"
-          tip="Worst frame time over the last 5 seconds."
-          value={time(state.gap)}
+          label={language.t("debugBar.frame.label")}
+          tip={language.t("debugBar.frame.tip")}
+          value={time(state.gap) ?? na()}
           bad={bad(state.gap, 50)}
           dim={state.gap === undefined}
         />
         <Cell
-          label="JANK"
-          tip="Frames over 32ms in the last 5 seconds."
-          value={state.jank === undefined ? "n/a" : `${state.jank}`}
+          label={language.t("debugBar.jank.label")}
+          tip={language.t("debugBar.jank.tip")}
+          value={state.jank === undefined ? na() : `${state.jank}`}
           bad={bad(state.jank, 8)}
           dim={state.jank === undefined}
         />
         <Cell
-          label="LONG"
-          tip={`Blocked time and long-task count in the last 5 seconds. Max task: ${ms(state.long.max)}.`}
+          label={language.t("debugBar.long.label")}
+          tip={language.t("debugBar.long.tip", { max: ms(state.long.max) ?? na() })}
           value={longv()}
           bad={bad(state.long.block, 200)}
           dim={state.long.count === undefined}
         />
         <Cell
-          label="DELAY"
-          tip="Worst observed input delay in the last 5 seconds."
-          value={time(state.delay)}
+          label={language.t("debugBar.delay.label")}
+          tip={language.t("debugBar.delay.tip")}
+          value={time(state.delay) ?? na()}
           bad={bad(state.delay, 100)}
           dim={state.delay === undefined}
         />
         <Cell
-          label="INP"
-          tip="Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP."
-          value={time(state.inp)}
+          label={language.t("debugBar.inp.label")}
+          tip={language.t("debugBar.inp.tip")}
+          value={time(state.inp) ?? na()}
           bad={bad(state.inp, 200)}
           dim={state.inp === undefined}
         />
         <Cell
-          label="CLS"
-          tip="Cumulative layout shift for the current app lifetime."
-          value={state.cls === undefined ? "n/a" : state.cls.toFixed(2)}
+          label={language.t("debugBar.cls.label")}
+          tip={language.t("debugBar.cls.tip")}
+          value={state.cls === undefined ? na() : state.cls.toFixed(2)}
           bad={bad(state.cls, 0.1)}
           dim={state.cls === undefined}
         />
         <Cell
-          label="MEM"
+          label={language.t("debugBar.mem.label")}
           tip={
             state.heap.used === undefined
-              ? "Used JS heap vs heap limit. Chromium only."
-              : `Used JS heap vs heap limit. ${mb(state.heap.used)} of ${mb(state.heap.limit)}.`
+              ? language.t("debugBar.mem.tipUnavailable")
+              : language.t("debugBar.mem.tip", {
+                  used: mb(state.heap.used) ?? na(),
+                  limit: mb(state.heap.limit) ?? na(),
+                })
           }
           value={heapv()}
           bad={bad(heap(), 0.8)}

+ 1 - 1
packages/app/src/components/dialog-select-file.tsx

@@ -426,7 +426,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
                   </Show>
                 </div>
                 <Show when={item.keybind}>
-                  <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
+                  <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
                 </Show>
               </div>
             </Match>

+ 2 - 2
packages/app/src/components/dialog-select-server.tsx

@@ -149,7 +149,7 @@ function ServerForm(props: ServerFormProps) {
           <TextField
             type="text"
             label={language.t("dialog.server.add.username")}
-            placeholder="username"
+            placeholder={language.t("dialog.server.add.usernamePlaceholder")}
             value={props.username}
             disabled={props.busy}
             onChange={props.onUsernameChange}
@@ -158,7 +158,7 @@ function ServerForm(props: ServerFormProps) {
           <TextField
             type="password"
             label={language.t("dialog.server.add.password")}
-            placeholder="password"
+            placeholder={language.t("dialog.server.add.passwordPlaceholder")}
             value={props.password}
             disabled={props.busy}
             onChange={props.onPasswordChange}

+ 3 - 1
packages/app/src/components/server/server-row.tsx

@@ -10,6 +10,7 @@ import {
   type ParentProps,
   Show,
 } from "solid-js"
+import { useLanguage } from "@/context/language"
 import { type ServerConnection, serverName } from "@/context/server"
 import type { ServerHealth } from "@/utils/server-health"
 
@@ -25,6 +26,7 @@ interface ServerRowProps extends ParentProps {
 }
 
 export function ServerRow(props: ServerRowProps) {
+  const language = useLanguage()
   const [truncated, setTruncated] = createSignal(false)
   let nameRef: HTMLSpanElement | undefined
   let versionRef: HTMLSpanElement | undefined
@@ -100,7 +102,7 @@ export function ServerRow(props: ServerRowProps) {
                   {conn().http.username ? (
                     <span class="text-text-weak">{conn().http.username}</span>
                   ) : (
-                    <span class="text-text-weaker">no username</span>
+                    <span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
                   )}
                 </span>
                 {conn().http.password && <span class="text-text-weak">••••••••</span>}

+ 28 - 26
packages/app/src/components/session/session-header.tsx

@@ -46,63 +46,63 @@ type OS = "macos" | "windows" | "linux" | "unknown"
 const MAC_APPS = [
   {
     id: "vscode",
-    label: "VS Code",
+    label: "session.header.open.app.vscode",
     icon: "vscode",
     openWith: "Visual Studio Code",
   },
-  { id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
-  { id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
-  { id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
+  { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
+  { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
+  { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
   {
     id: "antigravity",
-    label: "Antigravity",
+    label: "session.header.open.app.antigravity",
     icon: "antigravity",
     openWith: "Antigravity",
   },
-  { id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
-  { id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
-  { id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
-  { id: "warp", label: "Warp", icon: "warp", openWith: "Warp" },
-  { id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
+  { id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
+  { id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
+  { id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
+  { id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
+  { id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
   {
     id: "android-studio",
-    label: "Android Studio",
+    label: "session.header.open.app.androidStudio",
     icon: "android-studio",
     openWith: "Android Studio",
   },
   {
     id: "sublime-text",
-    label: "Sublime Text",
+    label: "session.header.open.app.sublimeText",
     icon: "sublime-text",
     openWith: "Sublime Text",
   },
 ] as const
 
 const WINDOWS_APPS = [
-  { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
-  { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
-  { id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
+  { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
+  { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
+  { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
   {
     id: "powershell",
-    label: "PowerShell",
+    label: "session.header.open.app.powershell",
     icon: "powershell",
     openWith: "powershell",
   },
   {
     id: "sublime-text",
-    label: "Sublime Text",
+    label: "session.header.open.app.sublimeText",
     icon: "sublime-text",
     openWith: "Sublime Text",
   },
 ] as const
 
 const LINUX_APPS = [
-  { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
-  { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
-  { id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
+  { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
+  { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
+  { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
   {
     id: "sublime-text",
-    label: "Sublime Text",
+    label: "session.header.open.app.sublimeText",
     icon: "sublime-text",
     openWith: "Sublime Text",
   },
@@ -160,9 +160,9 @@ export function SessionHeader() {
   })
 
   const fileManager = createMemo(() => {
-    if (os() === "macos") return { label: "Finder", icon: "finder" as const }
-    if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const }
-    return { label: "File Manager", icon: "finder" as const }
+    if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const }
+    if (os() === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const }
+    return { label: "session.header.open.fileManager", icon: "finder" as const }
   })
 
   createEffect(() => {
@@ -187,8 +187,10 @@ export function SessionHeader() {
 
   const options = createMemo(() => {
     return [
-      { id: "finder", label: fileManager().label, icon: fileManager().icon },
-      ...apps().filter((app) => exists[app.id]),
+      { id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
+      ...apps()
+        .filter((app) => exists[app.id])
+        .map((app) => ({ ...app, label: language.t(app.label) })),
     ] as const
   })
 

+ 2 - 5
packages/app/src/components/session/session-sortable-terminal-tab.tsx

@@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { Icon } from "@opencode-ai/ui/icon"
+import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
 import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { useLanguage } from "@/context/language"
 import { focusTerminalById } from "@/pages/session/helpers"
@@ -27,11 +28,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
   const isDefaultTitle = () => {
     const number = props.terminal.titleNumber
     if (!Number.isFinite(number) || number <= 0) return false
-    const match = props.terminal.title.match(/^Terminal (\d+)$/)
-    if (!match) return false
-    const parsed = Number(match[1])
-    if (!Number.isFinite(parsed) || parsed <= 0) return false
-    return parsed === number
+    return isDefaultTerminalTitle(props.terminal.title, number)
   }
 
   const label = () => {

+ 1 - 1
packages/app/src/components/settings-keybinds.tsx

@@ -239,7 +239,7 @@ function useKeyCapture(input: {
         showToast({
           title: input.language.t("settings.shortcuts.conflict.title"),
           description: input.language.t("settings.shortcuts.conflict.description", {
-            keybind: formatKeybind(next),
+            keybind: formatKeybind(next, input.language.t),
             titles: [...conflicts.values()].join(", "),
           }),
         })

+ 1 - 1
packages/app/src/components/terminal.tsx

@@ -519,7 +519,7 @@ export const Terminal = (props: TerminalProps) => {
         if (event.code !== 1000) {
           if (once.value) return
           once.value = true
-          local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
+          local.onConnectError?.(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code })))
         }
       }
       socket.addEventListener("close", handleClose)

+ 51 - 10
packages/app/src/context/command.tsx

@@ -2,6 +2,7 @@ import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "sol
 import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { dict as en } from "@/i18n/en"
 import { useLanguage } from "@/context/language"
 import { useSettings } from "@/context/settings"
 import { Persist, persisted } from "@/utils/persist"
@@ -13,6 +14,27 @@ const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
 const SUGGESTED_PREFIX = "suggested."
 const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
 
+type KeyLabel =
+  | "common.key.ctrl"
+  | "common.key.alt"
+  | "common.key.shift"
+  | "common.key.meta"
+  | "common.key.space"
+  | "common.key.backspace"
+  | "common.key.enter"
+  | "common.key.tab"
+  | "common.key.delete"
+  | "common.key.home"
+  | "common.key.end"
+  | "common.key.pageUp"
+  | "common.key.pageDown"
+  | "common.key.insert"
+  | "common.key.esc"
+
+function keyText(key: KeyLabel, t?: (key: KeyLabel) => string) {
+  return t ? t(key) : en[key]
+}
+
 function actionId(id: string) {
   if (!id.startsWith(SUGGESTED_PREFIX)) return id
   return id.slice(SUGGESTED_PREFIX.length)
@@ -145,7 +167,7 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean
   return false
 }
 
-export function formatKeybind(config: string): string {
+export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
   if (!config || config === "none") return ""
 
   const keybinds = parseKeybind(config)
@@ -154,10 +176,10 @@ export function formatKeybind(config: string): string {
   const kb = keybinds[0]
   const parts: string[] = []
 
-  if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
-  if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
-  if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
-  if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
+  if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t))
+  if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t))
+  if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t))
+  if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t))
 
   if (kb.key) {
     const keys: Record<string, string> = {
@@ -167,10 +189,29 @@ export function formatKeybind(config: string): string {
       arrowright: "→",
       comma: ",",
       plus: "+",
-      space: "Space",
+    }
+    const named: Record<string, KeyLabel> = {
+      backspace: "common.key.backspace",
+      delete: "common.key.delete",
+      end: "common.key.end",
+      enter: "common.key.enter",
+      esc: "common.key.esc",
+      escape: "common.key.esc",
+      home: "common.key.home",
+      insert: "common.key.insert",
+      pagedown: "common.key.pageDown",
+      pageup: "common.key.pageUp",
+      space: "common.key.space",
+      tab: "common.key.tab",
     }
     const key = kb.key.toLowerCase()
-    const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1))
+    const displayKey =
+      keys[key] ??
+      (named[key]
+        ? keyText(named[key], t)
+        : key.length === 1
+          ? key.toUpperCase()
+          : key.charAt(0).toUpperCase() + key.slice(1))
     parts.push(displayKey)
   }
 
@@ -364,17 +405,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
       },
       keybind(id: string) {
         if (id === PALETTE_ID) {
-          return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
+          return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t)
         }
 
         const base = actionId(id)
         const option = options().find((x) => actionId(x.id) === base)
-        if (option?.keybind) return formatKeybind(option.keybind)
+        if (option?.keybind) return formatKeybind(option.keybind, language.t)
 
         const meta = catalog[base]
         const config = bind(base, meta?.keybind)
         if (!config) return ""
-        return formatKeybind(config)
+        return formatKeybind(config, language.t)
       },
       show: showPalette,
       keybinds(enabled: boolean) {

+ 3 - 3
packages/app/src/context/file.tsx

@@ -43,10 +43,10 @@ export {
   touchFileContent,
 }
 
-function errorMessage(error: unknown) {
+function errorMessage(error: unknown, fallback: string) {
   if (error instanceof Error && error.message) return error.message
   if (typeof error === "string" && error) return error
-  return "Unknown error"
+  return fallback
 }
 
 export const { use: useFile, provider: FileProvider } = createSimpleContext({
@@ -184,7 +184,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
         })
         .catch((e) => {
           if (scope() !== directory) return
-          setLoadError(file, errorMessage(e))
+          setLoadError(file, errorMessage(e, language.t("error.chain.unknown")))
         })
         .finally(() => {
           inflight.delete(key)

+ 4 - 2
packages/app/src/context/global-sdk.tsx

@@ -4,6 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus"
 import { batch, onCleanup } from "solid-js"
 import z from "zod"
 import { createSdkForServer } from "@/utils/server"
+import { useLanguage } from "./language"
 import { usePlatform } from "./platform"
 import { useServer } from "./server"
 
@@ -14,6 +15,7 @@ const abortError = z.object({
 export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
   name: "GlobalSDK",
   init: () => {
+    const language = useLanguage()
     const server = useServer()
     const platform = usePlatform()
     const abort = new AbortController()
@@ -30,7 +32,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
     })()
 
     const currentServer = server.current
-    if (!currentServer) throw new Error("No server available")
+    if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
 
     const eventSdk = createSdkForServer({
       signal: abort.signal,
@@ -218,7 +220,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
       event: emitter,
       createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
         const s = server.current
-        if (!s) throw new Error("Server not available")
+        if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
         return createSdkForServer({
           server: s.http,
           fetch: platform.fetch,

+ 1 - 0
packages/app/src/context/global-sync.tsx

@@ -164,6 +164,7 @@ function createGlobalSync() {
       sdkCache.delete(directory)
       clearSessionPrefetchDirectory(directory)
     },
+    translate: language.t,
   })
 
   const sdkFor = (directory: string) => {

+ 1 - 1
packages/app/src/context/global-sync/bootstrap.ts

@@ -139,7 +139,7 @@ export async function bootstrapDirectory(input: {
     const project = getFilename(input.directory)
     showToast({
       variant: "error",
-      title: `Failed to reload ${project}`,
+      title: input.translate("toast.project.reloadFailed.title", { project }),
       description: formatServerError(err, input.translate),
     })
     input.setStore("status", "partial")

+ 1 - 0
packages/app/src/context/global-sync/child-store.test.ts

@@ -21,6 +21,7 @@ describe("createChildStoreManager", () => {
       isLoadingSessions: () => false,
       onBootstrap() {},
       onDispose() {},
+      translate: (key) => key,
     })
 
     Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {

+ 5 - 4
packages/app/src/context/global-sync/child-store.ts

@@ -21,6 +21,7 @@ export function createChildStoreManager(input: {
   isLoadingSessions: (directory: string) => boolean
   onBootstrap: (directory: string) => void
   onDispose: (directory: string) => void
+  translate: (key: string, vars?: Record<string, string | number>) => string
 }) {
   const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
   const vcsCache = new Map<string, VcsCache>()
@@ -129,7 +130,7 @@ export function createChildStoreManager(input: {
           createStore({ value: undefined as VcsInfo | undefined }),
         ),
       )
-      if (!vcs) throw new Error("Failed to create persisted cache")
+      if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
       const vcsStore = vcs[0]
       vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
 
@@ -139,7 +140,7 @@ export function createChildStoreManager(input: {
           createStore({ value: undefined as ProjectMeta | undefined }),
         ),
       )
-      if (!meta) throw new Error("Failed to create persisted project metadata")
+      if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
       metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
 
       const icon = runWithOwner(input.owner, () =>
@@ -148,7 +149,7 @@ export function createChildStoreManager(input: {
           createStore({ value: undefined as string | undefined }),
         ),
       )
-      if (!icon) throw new Error("Failed to create persisted project icon")
+      if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
       iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
 
       const init = () =>
@@ -211,7 +212,7 @@ export function createChildStoreManager(input: {
     }
     mark(directory)
     const childStore = children[directory]
-    if (!childStore) throw new Error("Failed to create store")
+    if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
     return childStore
   }
 

+ 51 - 0
packages/app/src/context/terminal-title.ts

@@ -0,0 +1,51 @@
+import { dict as ar } from "@/i18n/ar"
+import { dict as br } from "@/i18n/br"
+import { dict as bs } from "@/i18n/bs"
+import { dict as da } from "@/i18n/da"
+import { dict as de } from "@/i18n/de"
+import { dict as en } from "@/i18n/en"
+import { dict as es } from "@/i18n/es"
+import { dict as fr } from "@/i18n/fr"
+import { dict as ja } from "@/i18n/ja"
+import { dict as ko } from "@/i18n/ko"
+import { dict as no } from "@/i18n/no"
+import { dict as pl } from "@/i18n/pl"
+import { dict as ru } from "@/i18n/ru"
+import { dict as th } from "@/i18n/th"
+import { dict as tr } from "@/i18n/tr"
+import { dict as zh } from "@/i18n/zh"
+import { dict as zht } from "@/i18n/zht"
+
+const numbered = Array.from(
+  new Set([
+    en["terminal.title.numbered"],
+    ar["terminal.title.numbered"],
+    br["terminal.title.numbered"],
+    bs["terminal.title.numbered"],
+    da["terminal.title.numbered"],
+    de["terminal.title.numbered"],
+    es["terminal.title.numbered"],
+    fr["terminal.title.numbered"],
+    ja["terminal.title.numbered"],
+    ko["terminal.title.numbered"],
+    no["terminal.title.numbered"],
+    pl["terminal.title.numbered"],
+    ru["terminal.title.numbered"],
+    th["terminal.title.numbered"],
+    tr["terminal.title.numbered"],
+    zh["terminal.title.numbered"],
+    zht["terminal.title.numbered"],
+  ]),
+)
+
+export function defaultTitle(number: number) {
+  return en["terminal.title.numbered"].replace("{{number}}", String(number))
+}
+
+export function isDefaultTitle(title: string, number: number) {
+  return numbered.some((text) => title === text.replace("{{number}}", String(number)))
+}
+
+export function titleNumber(title: string, max: number) {
+  return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number))
+}

+ 4 - 7
packages/app/src/context/terminal.tsx

@@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
 import { useParams } from "@solidjs/router"
 import { useSDK } from "./sdk"
 import type { Platform } from "./platform"
+import { defaultTitle, titleNumber } from "./terminal-title"
 import { Persist, persisted, removePersisted } from "@/utils/persist"
 
 export type LocalPTY = {
@@ -33,11 +34,7 @@ function num(value: unknown) {
 }
 
 function 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
+  return titleNumber(title, MAX_TERMINAL_SESSIONS)
 }
 
 function pty(value: unknown): LocalPTY | undefined {
@@ -202,13 +199,13 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
       const nextNumber = pickNextTerminalNumber()
 
       sdk.client.pty
-        .create({ title: `Terminal ${nextNumber}` })
+        .create({ title: defaultTitle(nextNumber) })
         .then((pty: { data?: { id?: string; title?: string } }) => {
           const id = pty.data?.id
           if (!id) return
           const newTerminal = {
             id,
-            title: pty.data?.title ?? "Terminal",
+            title: pty.data?.title ?? defaultTitle(nextNumber),
             titleNumber: nextNumber,
           }
           setStore("all", store.all.length, newTerminal)

+ 73 - 0
packages/app/src/i18n/ar.ts

@@ -778,4 +778,77 @@ export const dict = {
   "common.time.daysAgo.short": "قبل {{count}} ي",
   "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
   "settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
+
+  "app.server.unreachable": "تعذر الوصول إلى {{server}}",
+  "app.server.retrying": "جاري إعادة المحاولة تلقائيًا...",
+  "app.server.otherServers": "خوادم أخرى",
+  "dialog.server.add.usernamePlaceholder": "اسم المستخدم",
+  "dialog.server.add.passwordPlaceholder": "كلمة المرور",
+  "server.row.noUsername": "لا يوجد اسم مستخدم",
+  "session.review.noVcs.createGit.title": "إنشاء مستودع Git",
+  "session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع",
+  "session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...",
+  "session.review.noVcs.createGit.action": "إنشاء مستودع Git",
+  "session.todo.progress": "تم إكمال {{done}} من {{total}} مهام",
+  "session.question.progress": "{{current}} من {{total}} أسئلة",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "مستكشف الملفات",
+  "session.header.open.fileManager": "مدير الملفات",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "المحطة الطرفية",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "تشخيص أداء التطوير",
+  "debugBar.na": "غير متاح",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip": "آخر انتقال مكتمل للمسار يمس صفحة جلسة، مُقاسًا من بدء التوجيه حتى أول رسم بعد استقراره.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "الإطارات المتجددة في الثانية خلال آخر 5 ثوانٍ.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "أسوأ وقت للإطار خلال آخر 5 ثوانٍ.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "الإطارات التي تزيد عن 32 مللي ثانية في آخر 5 ثوانٍ.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "الوقت المحظور وعدد المهام الطويلة في آخر 5 ثوانٍ. أقصى مهمة: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "أسوأ تأخير إدخال تمت ملاحظته في آخر 5 ثوانٍ.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip": "مدة التفاعل التقريبية خلال آخر 5 ثوانٍ. هذا يشبه INP، وليس Web Vitals INP الرسمي.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "التحول التخطيطي التراكمي لعمر التطبيق الحالي.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "كومة JS المستخدمة مقابل حد الكومة. Chromium فقط.",
+  "debugBar.mem.tip": "كومة JS المستخدمة مقابل حد الكومة. {{used}} من {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "غير معروف",
+  "error.page.circular": "[دائري]",
+  "error.globalSDK.noServerAvailable": "لا يوجد خادم متاح",
+  "error.globalSDK.serverNotAvailable": "الخادم غير متاح",
+  "error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
+  "error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
+  "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
+  "error.childStore.storeCreateFailed": "فشل إنشاء المخزن",
+  "terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/br.ts

@@ -788,4 +788,79 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}d atrás",
   "settings.providers.connected.environmentDescription": "Conectado a partir de suas variáveis de ambiente",
   "settings.providers.custom.description": "Adicionar um provedor compatível com a OpenAI através do URL base.",
+
+  "app.server.unreachable": "Não foi possível conectar a {{server}}",
+  "app.server.retrying": "Tentando novamente automaticamente...",
+  "app.server.otherServers": "Outros servidores",
+  "dialog.server.add.usernamePlaceholder": "nome de usuário",
+  "dialog.server.add.passwordPlaceholder": "senha",
+  "server.row.noUsername": "sem nome de usuário",
+  "session.review.noVcs.createGit.title": "Criar um repositório Git",
+  "session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
+  "session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
+  "session.review.noVcs.createGit.action": "Criar repositório Git",
+  "session.todo.progress": "{{done}} de {{total}} tarefas concluídas",
+  "session.question.progress": "{{current}} de {{total}} perguntas",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Explorador de Arquivos",
+  "session.header.open.fileManager": "Gerenciador de Arquivos",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Diagnóstico de desempenho de desenvolvimento",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Última transição de rota concluída tocando em uma página de sessão, medida desde o início do roteador até a primeira pintura após o estabelecimento.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Quadros por segundo nos últimos 5 segundos.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Pior tempo de quadro nos últimos 5 segundos.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Quadros acima de 32ms nos últimos 5 segundos.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Tempo bloqueado e contagem de tarefas longas nos últimos 5 segundos. Tarefa máx: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Pior atraso de entrada observado nos últimos 5 segundos.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Duração aproximada da interação nos últimos 5 segundos. Isso é semelhante ao INP, não o INP oficial do Web Vitals.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Mudança cumulativa de layout para o tempo de vida atual do aplicativo.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Heap JS usado vs limite de heap. Apenas Chromium.",
+  "debugBar.mem.tip": "Heap JS usado vs limite de heap. {{used}} de {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Espaço",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "desconhecido",
+  "error.page.circular": "[Circular]",
+  "error.globalSDK.noServerAvailable": "Nenhum servidor disponível",
+  "error.globalSDK.serverNotAvailable": "Servidor indisponível",
+  "error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
+  "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
+  "error.childStore.storeCreateFailed": "Falha ao criar armazenamento",
+  "terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/bs.ts

@@ -864,4 +864,79 @@ export const dict = {
   "common.time.daysAgo.short": "prije {{count}} d",
   "settings.providers.connected.environmentDescription": "Povezano sa vašim varijablama okruženja",
   "settings.providers.custom.description": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
+
+  "app.server.unreachable": "Nije moguće pristupiti {{server}}",
+  "app.server.retrying": "Automatski ponovni pokušaj...",
+  "app.server.otherServers": "Drugi serveri",
+  "dialog.server.add.usernamePlaceholder": "korisničko ime",
+  "dialog.server.add.passwordPlaceholder": "lozinka",
+  "server.row.noUsername": "nema korisničkog imena",
+  "session.review.noVcs.createGit.title": "Kreiraj Git repozitorij",
+  "session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu",
+  "session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...",
+  "session.review.noVcs.createGit.action": "Kreiraj Git repozitorij",
+  "session.todo.progress": "{{done}} od {{total}} zadataka završeno",
+  "session.question.progress": "{{current}} od {{total}} pitanja",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "File Explorer",
+  "session.header.open.fileManager": "File Manager",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Dijagnostika performansi razvoja",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Posljednji završeni prelazak rute koji dotiče stranicu sesije, mjeren od početka rutera do prvog iscrtavanja nakon smirivanja.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Kadrovi u sekundi tokom posljednjih 5 sekundi.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Najgore vrijeme kadra u posljednjih 5 sekundi.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Kadrovi duži od 32ms u posljednjih 5 sekundi.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u posljednjih 5 sekundi. Maks zadatak: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Najgore zabilježeno kašnjenje unosa u posljednjih 5 sekundi.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Približno trajanje interakcije tokom posljednjih 5 sekundi. Ovo je slično INP-u, nije službeni Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Kumulativni pomak rasporeda za trenutni životni vijek aplikacije.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Korišteni JS heap naspram limita heapa. Samo Chromium.",
+  "debugBar.mem.tip": "Korišteni JS heap naspram limita heapa. {{used}} od {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "nepoznato",
+  "error.page.circular": "[Kružno]",
+  "error.globalSDK.noServerAvailable": "Nema dostupnog servera",
+  "error.globalSDK.serverNotAvailable": "Server nije dostupan",
+  "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
+  "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
+  "error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta",
+  "terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/da.ts

@@ -858,4 +858,79 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}d siden",
   "settings.providers.connected.environmentDescription": "Tilsluttet fra dine miljøvariabler",
   "settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
+
+  "app.server.unreachable": "Kunne ikke nå {{server}}",
+  "app.server.retrying": "Prøver igen automatisk...",
+  "app.server.otherServers": "Andre servere",
+  "dialog.server.add.usernamePlaceholder": "brugernavn",
+  "dialog.server.add.passwordPlaceholder": "adgangskode",
+  "server.row.noUsername": "intet brugernavn",
+  "session.review.noVcs.createGit.title": "Opret et Git-repository",
+  "session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
+  "session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
+  "session.review.noVcs.createGit.action": "Opret Git-repository",
+  "session.todo.progress": "{{done}} af {{total}} opgaver fuldført",
+  "session.question.progress": "{{current}} af {{total}} spørgsmål",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Stifinder",
+  "session.header.open.fileManager": "Filhåndtering",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Udviklingsydelsesdiagnostik",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Sidste gennemførte ruteovergang, der berører en sessionsside, målt fra routerstart til den første optegning efter den falder til ro.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Rullende billeder pr. sekund over de sidste 5 sekunder.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Værste billedtid over de sidste 5 sekunder.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Billeder over 32ms i de sidste 5 sekunder.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Blokeret tid og antal lange opgaver i de sidste 5 sekunder. Maks opgave: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Værste observerede inputforsinkelse i de sidste 5 sekunder.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Omtrentlig interaktionsvarighed over de sidste 5 sekunder. Dette er INP-lignende, ikke den officielle Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Kumulativt layoutskift for den nuværende app-levetid.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Brugt JS-heap vs heap-grænse. Kun Chromium.",
+  "debugBar.mem.tip": "Brugt JS-heap vs heap-grænse. {{used}} af {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Mellemrum",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "ukendt",
+  "error.page.circular": "[Cirkulær]",
+  "error.globalSDK.noServerAvailable": "Ingen server tilgængelig",
+  "error.globalSDK.serverNotAvailable": "Server ikke tilgængelig",
+  "error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
+  "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
+  "error.childStore.storeCreateFailed": "Kunne ikke oprette lager",
+  "terminal.connectionLost.abnormalClose": "WebSocket lukkede unormalt: {{code}}",
 }

+ 76 - 0
packages/app/src/i18n/de.ts

@@ -799,4 +799,80 @@ export const dict = {
   "common.time.daysAgo.short": "vor {{count}} Tg",
   "settings.providers.connected.environmentDescription": "Verbunden aus Ihren Umgebungsvariablen",
   "settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
+
+  "app.server.unreachable": "Konnte {{server}} nicht erreichen",
+  "app.server.retrying": "Automatische erneute Verbindung...",
+  "app.server.otherServers": "Andere Server",
+  "dialog.server.add.usernamePlaceholder": "Benutzername",
+  "dialog.server.add.passwordPlaceholder": "Passwort",
+  "server.row.noUsername": "Kein Benutzername",
+  "session.review.noVcs.createGit.title": "Git-Repository erstellen",
+  "session.review.noVcs.createGit.description":
+    "Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
+  "session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...",
+  "session.review.noVcs.createGit.action": "Git-Repository erstellen",
+  "session.todo.progress": "{{done}} von {{total}} Aufgaben erledigt",
+  "session.question.progress": "{{current}} von {{total}} Fragen",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Datei-Explorer",
+  "session.header.open.fileManager": "Dateimanager",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Entwicklungs-Leistungsdiagnose",
+  "debugBar.na": "n.v.",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Letzter abgeschlossener Routenübergang, der eine Sitzungsseite berührt, gemessen vom Start des Routers bis zum ersten Rendern nach dem Einschwingen.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Gleitende Bilder pro Sekunde in den letzten 5 Sekunden.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Schlechteste Frame-Zeit in den letzten 5 Sekunden.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Frames über 32ms in den letzten 5 Sekunden.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Blockierte Zeit und Anzahl langer Aufgaben in den letzten 5 Sekunden. Max Aufgabe: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Schlechteste beobachtete Eingabeverzögerung in den letzten 5 Sekunden.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Ungefähre Interaktionsdauer in den letzten 5 Sekunden. Dies ist INP-ähnlich, nicht das offizielle Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Kumulative Layoutverschiebung für die aktuelle App-Lebensdauer.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Verwendeter JS-Heap vs Heap-Limit. Nur Chromium.",
+  "debugBar.mem.tip": "Verwendeter JS-Heap vs Heap-Limit. {{used}} von {{limit}}.",
+  "common.key.ctrl": "Strg",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Umschalt",
+  "common.key.meta": "Meta",
+  "common.key.space": "Leertaste",
+  "common.key.backspace": "Rücktaste",
+  "common.key.enter": "Eingabe",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Entf",
+  "common.key.home": "Pos1",
+  "common.key.end": "Ende",
+  "common.key.pageUp": "Bild auf",
+  "common.key.pageDown": "Bild ab",
+  "common.key.insert": "Einfg",
+  "common.unknown": "unbekannt",
+  "error.page.circular": "[Zirkulär]",
+  "error.globalSDK.noServerAvailable": "Kein Server verfügbar",
+  "error.globalSDK.serverNotAvailable": "Server nicht verfügbar",
+  "error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
+  "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
+  "error.childStore.storeCreateFailed": "Speicher konnte nicht erstellt werden",
+  "terminal.connectionLost.abnormalClose": "WebSocket abnormal geschlossen: {{code}}",
 } satisfies Partial<Record<Keys, string>>

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

@@ -306,6 +306,10 @@ export const dict = {
   "dialog.directory.search.placeholder": "Search folders",
   "dialog.directory.empty": "No folders found",
 
+  "app.server.unreachable": "Could not reach {{server}}",
+  "app.server.retrying": "Retrying automatically...",
+  "app.server.otherServers": "Other servers",
+
   "dialog.server.title": "Servers",
   "dialog.server.description": "Switch which OpenCode server this app connects to.",
   "dialog.server.search.placeholder": "Search servers",
@@ -319,7 +323,9 @@ export const dict = {
   "dialog.server.add.name": "Server name (optional)",
   "dialog.server.add.namePlaceholder": "Localhost",
   "dialog.server.add.username": "Username (optional)",
+  "dialog.server.add.usernamePlaceholder": "username",
   "dialog.server.add.password": "Password (optional)",
+  "dialog.server.add.passwordPlaceholder": "password",
   "dialog.server.edit.title": "Edit server",
   "dialog.server.default.title": "Default server",
   "dialog.server.default.description":
@@ -335,6 +341,7 @@ export const dict = {
   "dialog.server.menu.delete": "Delete",
   "dialog.server.current": "Current Server",
   "dialog.server.status.default": "Default",
+  "server.row.noUsername": "no username",
 
   "dialog.project.edit.title": "Edit project",
   "dialog.project.edit.name": "Name",
@@ -456,6 +463,7 @@ export const dict = {
   "error.page.action.checking": "Checking...",
   "error.page.action.checkUpdates": "Check for updates",
   "error.page.action.updateTo": "Update to {{version}}",
+  "error.page.circular": "[Circular]",
   "error.page.report.prefix": "Please report this error to the OpenCode team",
   "error.page.report.discord": "on Discord",
   "error.page.version": "Version: {{version}}",
@@ -464,6 +472,12 @@ export const dict = {
     "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
 
   "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
+  "error.globalSDK.noServerAvailable": "No server available",
+  "error.globalSDK.serverNotAvailable": "Server not available",
+  "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
+  "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
+  "error.childStore.storeCreateFailed": "Failed to create store",
   "directory.error.invalidUrl": "Invalid directory in URL.",
 
   "error.chain.unknown": "Unknown error",
@@ -512,6 +526,10 @@ export const dict = {
   "session.review.loadingChanges": "Loading changes...",
   "session.review.empty": "No changes in this session yet",
   "session.review.noVcs": "No Git Version Control System detected, changes not displayed",
+  "session.review.noVcs.createGit.title": "Create a Git repository",
+  "session.review.noVcs.createGit.description": "Track, review, and undo changes in this project",
+  "session.review.noVcs.createGit.actionLoading": "Creating Git repository...",
+  "session.review.noVcs.createGit.action": "Create Git repository",
   "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
   "session.review.noChanges": "No changes",
 
@@ -530,6 +548,8 @@ export const dict = {
   "session.todo.title": "Todos",
   "session.todo.collapse": "Collapse",
   "session.todo.expand": "Expand",
+  "session.todo.progress": "{{done}} of {{total}} todos completed",
+  "session.question.progress": "{{current}} of {{total}} questions",
   "session.followupDock.summary.one": "{{count}} queued message",
   "session.followupDock.summary.other": "{{count}} queued messages",
   "session.followupDock.sendNow": "Send now",
@@ -555,6 +575,22 @@ export const dict = {
   "session.header.open.ariaLabel": "Open in {{app}}",
   "session.header.open.menu": "Open options",
   "session.header.open.copyPath": "Copy path",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "File Explorer",
+  "session.header.open.fileManager": "File Manager",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
 
   "status.popover.trigger": "Status",
   "status.popover.ariaLabel": "Server configurations",
@@ -587,6 +623,7 @@ export const dict = {
   "terminal.title.numbered": "Terminal {{number}}",
   "terminal.close": "Close terminal",
   "terminal.connectionLost.title": "Connection Lost",
+  "terminal.connectionLost.abnormalClose": "WebSocket closed abnormally: {{code}}",
   "terminal.connectionLost.description":
     "The terminal connection was interrupted. This can happen when the server restarts.",
 
@@ -604,6 +641,21 @@ export const dict = {
   "common.edit": "Edit",
   "common.loadMore": "Load more",
   "common.key.esc": "ESC",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "unknown",
 
   "common.time.justNow": "Just now",
   "common.time.minutesAgo.short": "{{count}}m ago",
@@ -623,6 +675,30 @@ export const dict = {
   "sidebar.project.viewAllSessions": "View all sessions",
   "sidebar.project.clearNotifications": "Clear notifications",
 
+  "debugBar.ariaLabel": "Development performance diagnostics",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Last completed route transition touching a session page, measured from router start until the first paint after it settles.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Rolling frames per second over the last 5 seconds.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Worst frame time over the last 5 seconds.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Frames over 32ms in the last 5 seconds.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Blocked time and long-task count in the last 5 seconds. Max task: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Worst observed input delay in the last 5 seconds.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Cumulative layout shift for the current app lifetime.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.",
+  "debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.",
+
   "app.name.desktop": "OpenCode Desktop",
 
   "settings.section.desktop": "Desktop",

+ 75 - 0
packages/app/src/i18n/es.ts

@@ -871,4 +871,79 @@ export const dict = {
   "common.time.daysAgo.short": "hace {{count}} d",
   "settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
   "settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por su URL base.",
+
+  "app.server.unreachable": "No se pudo conectar con {{server}}",
+  "app.server.retrying": "Reintentando automáticamente...",
+  "app.server.otherServers": "Otros servidores",
+  "dialog.server.add.usernamePlaceholder": "usuario",
+  "dialog.server.add.passwordPlaceholder": "contraseña",
+  "server.row.noUsername": "sin usuario",
+  "session.review.noVcs.createGit.title": "Crear repositorio Git",
+  "session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
+  "session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
+  "session.review.noVcs.createGit.action": "Crear repositorio Git",
+  "session.todo.progress": "{{done}} de {{total}} tareas completadas",
+  "session.question.progress": "{{current}} de {{total}} preguntas",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Explorador de archivos",
+  "session.header.open.fileManager": "Gestor de archivos",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Diagnóstico de rendimiento de desarrollo",
+  "debugBar.na": "n/d",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Última transición de ruta completada tocando una página de sesión, medida desde el inicio del router hasta el primer pintado después de asentarse.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Cuadros por segundo en los últimos 5 segundos.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Peor tiempo de cuadro en los últimos 5 segundos.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Cuadros superiores a 32ms en los últimos 5 segundos.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Tiempo bloqueado y recuento de tareas largas en los últimos 5 segundos. Tarea máx: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Peor retraso de entrada observado en los últimos 5 segundos.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Duración aproximada de la interacción en los últimos 5 segundos. Esto es similar a INP, no el INP oficial de Web Vitals.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Cambio de diseño acumulativo para la vida útil actual de la aplicación.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Heap JS usado vs límite de heap. Solo Chromium.",
+  "debugBar.mem.tip": "Heap JS usado vs límite de heap. {{used}} de {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Mayús",
+  "common.key.meta": "Meta",
+  "common.key.space": "Espacio",
+  "common.key.backspace": "Retroceso",
+  "common.key.enter": "Intro",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Supr",
+  "common.key.home": "Inicio",
+  "common.key.end": "Fin",
+  "common.key.pageUp": "RePág",
+  "common.key.pageDown": "AvPág",
+  "common.key.insert": "Insert",
+  "common.unknown": "desconocido",
+  "error.page.circular": "[Circular]",
+  "error.globalSDK.noServerAvailable": "Ningún servidor disponible",
+  "error.globalSDK.serverNotAvailable": "Servidor no disponible",
+  "error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
+  "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
+  "error.childStore.storeCreateFailed": "Error al crear almacén",
+  "terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}",
 }

+ 77 - 0
packages/app/src/i18n/fr.ts

@@ -796,4 +796,81 @@ export const dict = {
   "common.time.daysAgo.short": "il y a {{count}}j",
   "settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement",
   "settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.",
+
+  "app.server.unreachable": "Impossible de joindre {{server}}",
+  "app.server.retrying": "Nouvelle tentative automatique...",
+  "app.server.otherServers": "Autres serveurs",
+  "dialog.server.add.usernamePlaceholder": "nom d'utilisateur",
+  "dialog.server.add.passwordPlaceholder": "mot de passe",
+  "server.row.noUsername": "aucun nom d'utilisateur",
+  "session.review.noVcs.createGit.title": "Créer un dépôt Git",
+  "session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet",
+  "session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...",
+  "session.review.noVcs.createGit.action": "Créer un dépôt Git",
+  "session.todo.progress": "{{done}} tâches sur {{total}} terminées",
+  "session.question.progress": "{{current}} questions sur {{total}}",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Explorateur de fichiers",
+  "session.header.open.fileManager": "Gestionnaire de fichiers",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Diagnostics de performance de développement",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Dernière transition de route terminée touchant une page de session, mesurée du début du routeur jusqu'au premier affichage après stabilisation.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Images par seconde glissantes sur les 5 dernières secondes.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Pire temps d'image sur les 5 dernières secondes.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Images de plus de 32ms au cours des 5 dernières secondes.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip":
+    "Temps bloqué et nombre de tâches longues au cours des 5 dernières secondes. Tâche max : {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Pire délai d'entrée observé au cours des 5 dernières secondes.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Durée approximative d'interaction au cours des 5 dernières secondes. Ceci est similaire à INP, pas le INP officiel des Web Vitals.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Décalage cumulatif de la mise en page pour la durée de vie actuelle de l'application.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Tas JS utilisé vs limite de tas. Chromium uniquement.",
+  "debugBar.mem.tip": "Tas JS utilisé vs limite de tas. {{used}} sur {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Maj",
+  "common.key.meta": "Méta",
+  "common.key.space": "Espace",
+  "common.key.backspace": "Retour arrière",
+  "common.key.enter": "Entrée",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Suppr",
+  "common.key.home": "Début",
+  "common.key.end": "Fin",
+  "common.key.pageUp": "Page précédente",
+  "common.key.pageDown": "Page suivante",
+  "common.key.insert": "Inser",
+  "common.unknown": "inconnu",
+  "error.page.circular": "[Circulaire]",
+  "error.globalSDK.noServerAvailable": "Aucun serveur disponible",
+  "error.globalSDK.serverNotAvailable": "Serveur non disponible",
+  "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
+  "error.childStore.persistedProjectMetadataCreateFailed":
+    "Échec de la création des métadonnées de projet persistantes",
+  "error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante",
+  "error.childStore.storeCreateFailed": "Échec de la création du stockage",
+  "terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}",
 }

+ 74 - 0
packages/app/src/i18n/ja.ts

@@ -783,4 +783,78 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}} 日前",
   "settings.providers.connected.environmentDescription": "環境変数から接続されました",
   "settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。",
+
+  "app.server.unreachable": "{{server}} に到達できませんでした",
+  "app.server.retrying": "自動的に再試行中...",
+  "app.server.otherServers": "その他のサーバー",
+  "dialog.server.add.usernamePlaceholder": "ユーザー名",
+  "dialog.server.add.passwordPlaceholder": "パスワード",
+  "server.row.noUsername": "ユーザー名なし",
+  "session.review.noVcs.createGit.title": "Git リポジトリを作成",
+  "session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す",
+  "session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...",
+  "session.review.noVcs.createGit.action": "Git リポジトリを作成",
+  "session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了",
+  "session.question.progress": "{{total}} 問中 {{current}} 問",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "エクスプローラー",
+  "session.header.open.fileManager": "ファイルマネージャー",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "ターミナル",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "開発パフォーマンス診断",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip": "セッションページに触れる最後に完了したルート遷移。ルーター開始から安定後の最初の描画まで測定。",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "過去5秒間のローリングフレーム/秒。",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "過去5秒間の最悪フレーム時間。",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "過去5秒間で32msを超えたフレーム。",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "過去5秒間のブロック時間と長時間タスク数。最大タスク: {{max}}。",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "過去5秒間で観測された最悪の入力遅延。",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "過去5秒間の概算インタラクション時間。これは INP に似ていますが、公式の Web Vitals INP ではありません。",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "現在のアプリ寿命の累積レイアウトシフト。",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "使用中の JS ヒープ対ヒープ制限。Chromium のみ。",
+  "debugBar.mem.tip": "使用中の JS ヒープ対ヒープ制限。{{limit}} 中 {{used}}。",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "不明",
+  "error.page.circular": "[循環]",
+  "error.globalSDK.noServerAvailable": "利用可能なサーバーがありません",
+  "error.globalSDK.serverNotAvailable": "サーバーが利用できません",
+  "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
+  "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
+  "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
+  "error.childStore.storeCreateFailed": "ストアの作成に失敗しました",
+  "terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}",
 }

+ 74 - 0
packages/app/src/i18n/ko.ts

@@ -782,4 +782,78 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}일 전",
   "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
   "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
+
+  "app.server.unreachable": "{{server}}에 연결할 수 없습니다",
+  "app.server.retrying": "자동으로 재시도 중...",
+  "app.server.otherServers": "다른 서버",
+  "dialog.server.add.usernamePlaceholder": "사용자 이름",
+  "dialog.server.add.passwordPlaceholder": "비밀번호",
+  "server.row.noUsername": "사용자 이름 없음",
+  "session.review.noVcs.createGit.title": "Git 저장소 생성",
+  "session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소",
+  "session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...",
+  "session.review.noVcs.createGit.action": "Git 저장소 생성",
+  "session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료",
+  "session.question.progress": "{{total}}개의 질문 중 {{current}}개",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "파일 탐색기",
+  "session.header.open.fileManager": "파일 관리자",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "터미널",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "개발 성능 진단",
+  "debugBar.na": "해당 없음",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "세션 페이지에 닿은 마지막 완료된 라우트 전환. 라우터 시작부터 정착 후 첫 번째 페인트까지 측정됨.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "지난 5초간의 초당 프레임 수.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "지난 5초간의 최악의 프레임 시간.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "지난 5초간 32ms를 초과한 프레임.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "지난 5초간의 차단된 시간 및 긴 작업 수. 최대 작업: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "지난 5초간 관찰된 최악의 입력 지연.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip": "지난 5초간의 대략적인 상호작용 지속 시간. 이것은 공식 Web Vitals INP가 아닌 INP와 유사합니다.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "현재 앱 수명 동안의 누적 레이아웃 이동.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "사용된 JS 힙 대 힙 제한. Chromium 전용.",
+  "debugBar.mem.tip": "사용된 JS 힙 대 힙 제한. {{limit}} 중 {{used}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "알 수 없음",
+  "error.page.circular": "[순환]",
+  "error.globalSDK.noServerAvailable": "사용 가능한 서버 없음",
+  "error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음",
+  "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
+  "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
+  "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
+  "error.childStore.storeCreateFailed": "저장소 생성 실패",
+  "terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/no.ts

@@ -865,4 +865,79 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}} d siden",
   "settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine",
   "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
+
+  "app.server.unreachable": "Kunne ikke nå {{server}}",
+  "app.server.retrying": "Prøver på nytt automatisk...",
+  "app.server.otherServers": "Andre servere",
+  "dialog.server.add.usernamePlaceholder": "brukernavn",
+  "dialog.server.add.passwordPlaceholder": "passord",
+  "server.row.noUsername": "inget brukernavn",
+  "session.review.noVcs.createGit.title": "Opprett et Git-depot",
+  "session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet",
+  "session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...",
+  "session.review.noVcs.createGit.action": "Opprett Git-depot",
+  "session.todo.progress": "{{done}} av {{total}} oppgaver fullført",
+  "session.question.progress": "{{current}} av {{total}} spørsmål",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Filutforsker",
+  "session.header.open.fileManager": "Filbehandler",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Utviklingsytelsesdiagnostikk",
+  "debugBar.na": "i/t",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Siste fullførte ruteovergang som berører en sesjonsside, målt fra ruterstart til første opptegning etter at den har roet seg.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Rullende bilder per sekund over de siste 5 sekundene.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Verste bildetid over de siste 5 sekundene.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Bilder over 32ms i de siste 5 sekundene.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Blokkert tid og antall lange oppgaver i de siste 5 sekundene. Maks oppgave: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Verste observerte inndataforsinkelse i de siste 5 sekundene.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Omtrentlig interaksjonsvarighet over de siste 5 sekundene. Dette er INP-lignende, ikke den offisielle Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Kumulativ layoutforskyvning for gjeldende app-levetid.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Brukt JS-heap vs heap-grense. Kun Chromium.",
+  "debugBar.mem.tip": "Brukt JS-heap vs heap-grense. {{used}} av {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Mellomrom",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "ukjent",
+  "error.page.circular": "[Sirkulær]",
+  "error.globalSDK.noServerAvailable": "Ingen server tilgjengelig",
+  "error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig",
+  "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
+  "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
+  "error.childStore.storeCreateFailed": "Kunne ikke opprette lager",
+  "terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}",
 } satisfies Partial<Record<Keys, string>>

+ 76 - 0
packages/app/src/i18n/pl.ts

@@ -785,4 +785,80 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}} dni temu",
   "settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi",
   "settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI poprzez podstawowy URL.",
+
+  "app.server.unreachable": "Nie można połączyć z {{server}}",
+  "app.server.retrying": "Ponawianie automatycznie...",
+  "app.server.otherServers": "Inne serwery",
+  "dialog.server.add.usernamePlaceholder": "nazwa użytkownika",
+  "dialog.server.add.passwordPlaceholder": "hasło",
+  "server.row.noUsername": "brak nazwy użytkownika",
+  "session.review.noVcs.createGit.title": "Utwórz repozytorium Git",
+  "session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie",
+  "session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...",
+  "session.review.noVcs.createGit.action": "Utwórz repozytorium Git",
+  "session.todo.progress": "Ukończono {{done}} z {{total}} zadań",
+  "session.question.progress": "{{current}} z {{total}} pytań",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Eksplorator plików",
+  "session.header.open.fileManager": "Menedżer plików",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Diagnostyka wydajności deweloperskiej",
+  "debugBar.na": "n.d.",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Ostatnie zakończone przejście trasy dotykające strony sesji, mierzone od startu routera do pierwszego odrysowania po ustaleniu.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Średnia liczba klatek na sekundę w ciągu ostatnich 5 sekund.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Najgorszy czas klatki w ciągu ostatnich 5 sekund.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Klatki powyżej 32ms w ciągu ostatnich 5 sekund.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip":
+    "Zablokowany czas i liczba długich zadań w ciągu ostatnich 5 sekund. Maksymalne zadanie: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Najgorsze zaobserwowane opóźnienie wejścia w ciągu ostatnich 5 sekund.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Przybliżony czas trwania interakcji w ciągu ostatnich 5 sekund. Jest to podobne do INP, a nie oficjalne Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Skumulowane przesunięcie układu dla bieżącego czasu życia aplikacji.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Użyta sterta JS vs limit sterty. Tylko Chromium.",
+  "debugBar.mem.tip": "Użyta sterta JS vs limit sterty. {{used}} z {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Spacja",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "nieznany",
+  "error.page.circular": "[Cykliczne]",
+  "error.globalSDK.noServerAvailable": "Brak dostępnego serwera",
+  "error.globalSDK.serverNotAvailable": "Serwer niedostępny",
+  "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
+  "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
+  "error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu",
+  "terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/ru.ts

@@ -867,4 +867,79 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}} д назад",
   "settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения",
   "settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.",
+
+  "app.server.unreachable": "Не удалось связаться с {{server}}",
+  "app.server.retrying": "Автоматическая повторная попытка...",
+  "app.server.otherServers": "Другие серверы",
+  "dialog.server.add.usernamePlaceholder": "имя пользователя",
+  "dialog.server.add.passwordPlaceholder": "пароль",
+  "server.row.noUsername": "нет имени пользователя",
+  "session.review.noVcs.createGit.title": "Создать репозиторий Git",
+  "session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте",
+  "session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...",
+  "session.review.noVcs.createGit.action": "Создать репозиторий Git",
+  "session.todo.progress": "Выполнено {{done}} из {{total}} задач",
+  "session.question.progress": "{{current}} из {{total}} вопросов",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Проводник",
+  "session.header.open.fileManager": "Файловый менеджер",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Терминал",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Диагностика производительности разработки",
+  "debugBar.na": "н/д",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Последний завершенный переход маршрута, затрагивающий страницу сеанса, измеренный от запуска маршрутизатора до первой отрисовки после стабилизации.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Скользящая частота кадров в секунду за последние 5 секунд.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Худшее время кадра за последние 5 секунд.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Кадры более 32 мс за последние 5 секунд.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Заблокированное время и количество длинных задач за последние 5 секунд. Макс. задача: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Худшая наблюдаемая задержка ввода за последние 5 секунд.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "Приблизительная продолжительность взаимодействия за последние 5 секунд. Это похоже на INP, а не официальный Web Vitals INP.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Кумулятивный сдвиг макета за текущее время жизни приложения.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Используемая куча JS по сравнению с лимитом кучи. Только Chromium.",
+  "debugBar.mem.tip": "Используемая куча JS по сравнению с лимитом кучи. {{used}} из {{limit}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Пробел",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "неизвестно",
+  "error.page.circular": "[Циклично]",
+  "error.globalSDK.noServerAvailable": "Нет доступного сервера",
+  "error.globalSDK.serverNotAvailable": "Сервер недоступен",
+  "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
+  "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
+  "error.childStore.storeCreateFailed": "Не удалось создать хранилище",
+  "terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}",
 }

+ 75 - 0
packages/app/src/i18n/th.ts

@@ -854,4 +854,79 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}} วันที่แล้ว",
   "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
   "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก",
+
+  "app.server.unreachable": "ไม่สามารถติดต่อ {{server}}",
+  "app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...",
+  "app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ",
+  "dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้",
+  "dialog.server.add.passwordPlaceholder": "รหัสผ่าน",
+  "server.row.noUsername": "ไม่มีชื่อผู้ใช้",
+  "session.review.noVcs.createGit.title": "สร้าง Git repository",
+  "session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้",
+  "session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git repository...",
+  "session.review.noVcs.createGit.action": "สร้าง Git repository",
+  "session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ",
+  "session.question.progress": "{{current}} จาก {{total}} คำถาม",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "File Explorer",
+  "session.header.open.fileManager": "File Manager",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "การวินิจฉัยประสิทธิภาพการพัฒนา",
+  "debugBar.na": "n/a",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "การเปลี่ยนเส้นทางที่เสร็จสมบูรณ์ล่าสุดที่สัมผัสหน้าเซสชัน วัดจากจุดเริ่มต้นเราเตอร์จนถึงการวาดครั้งแรกหลังจากที่นิ่ง",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "เฟรมต่อวินาทีแบบต่อเนื่องในช่วง 5 วินาทีที่ผ่านมา",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "เวลาเฟรมที่แย่ที่สุดในช่วง 5 วินาทีที่ผ่านมา",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "เฟรมที่เกิน 32ms ในช่วง 5 วินาทีที่ผ่านมา",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "เวลาที่ถูกบล็อกและจำนวนงานยาวในช่วง 5 วินาทีที่ผ่านมา งานสูงสุด: {{max}}",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "ความล่าช้าในการป้อนข้อมูลที่แย่ที่สุดที่สังเกตได้ในช่วง 5 วินาทีที่ผ่านมา",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip":
+    "ระยะเวลาการโต้ตอบโดยประมาณในช่วง 5 วินาทีที่ผ่านมา นี่เป็นเหมือน INP ไม่ใช่ Web Vitals INP อย่างเป็นทางการ",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "การเลื่อนเลย์เอาต์สะสมสำหรับอายุการใช้งานของแอปปัจจุบัน",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "JS heap ที่ใช้เทียบกับขีดจำกัด heap เฉพาะ Chromium",
+  "debugBar.mem.tip": "JS heap ที่ใช้เทียบกับขีดจำกัด heap {{used}} จาก {{limit}}",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Space",
+  "common.key.backspace": "Backspace",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "ไม่ทราบ",
+  "error.page.circular": "[วงกลม]",
+  "error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
+  "error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
+  "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
+  "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
+  "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
+  "error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ",
+  "terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}",
 }

+ 74 - 0
packages/app/src/i18n/tr.ts

@@ -874,4 +874,78 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}g önce",
   "settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
   "settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
+
+  "app.server.unreachable": "{{server}} sunucusuna ulaşılamadı",
+  "app.server.retrying": "Otomatik olarak tekrar deneniyor...",
+  "app.server.otherServers": "Diğer sunucular",
+  "dialog.server.add.usernamePlaceholder": "kullanıcı adı",
+  "dialog.server.add.passwordPlaceholder": "parola",
+  "server.row.noUsername": "kullanıcı adı yok",
+  "session.review.noVcs.createGit.title": "Git deposu oluştur",
+  "session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al",
+  "session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...",
+  "session.review.noVcs.createGit.action": "Git deposu oluştur",
+  "session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı",
+  "session.question.progress": "{{total}} sorunun {{current}} tanesi",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "Dosya Gezgini",
+  "session.header.open.fileManager": "Dosya Yöneticisi",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "Terminal",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "Geliştirme performansı teşhisi",
+  "debugBar.na": "yok",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip":
+    "Yönlendirici başlangıcından yerleşme sonrası ilk boyamaya kadar ölçülen, bir oturum sayfasına dokunan son tamamlanmış rota geçişi.",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "Son 5 saniyedeki kayan saniye başına kare sayısı.",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "Son 5 saniyedeki en kötü kare süresi.",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "Son 5 saniyede 32ms üzerindeki kareler.",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "Son 5 saniyedeki engellenen süre ve uzun görev sayısı. Maksimum görev: {{max}}.",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "Son 5 saniyede gözlemlenen en kötü giriş gecikmesi.",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip": "Son 5 saniyedeki yaklaşık etkileşim süresi. Bu INP benzeridir, resmi Web Vitals INP değildir.",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "Mevcut uygulama ömrü için kümülatif düzen kayması.",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "Kullanılan JS yığını vs yığın sınırı. Yalnızca Chromium.",
+  "debugBar.mem.tip": "Kullanılan JS yığını vs yığın sınırı. {{limit}} içinde {{used}}.",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "Boşluk",
+  "common.key.backspace": "Geri",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "bilinmiyor",
+  "error.page.circular": "[Döngüsel]",
+  "error.globalSDK.noServerAvailable": "Sunucu yok",
+  "error.globalSDK.serverNotAvailable": "Sunucu mevcut değil",
+  "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
+  "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
+  "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
+  "error.childStore.storeCreateFailed": "Depo oluşturulamadı",
+  "terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}",
 } satisfies Partial<Record<Keys, string>>

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

@@ -853,4 +853,77 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}天前",
   "settings.providers.connected.environmentDescription": "已通过环境变量连接",
   "settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。",
+
+  "app.server.unreachable": "无法连接到 {{server}}",
+  "app.server.retrying": "正在自动重试...",
+  "app.server.otherServers": "其他服务器",
+  "dialog.server.add.usernamePlaceholder": "用户名",
+  "dialog.server.add.passwordPlaceholder": "密码",
+  "server.row.noUsername": "无用户名",
+  "session.review.noVcs.createGit.title": "创建 Git 仓库",
+  "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改",
+  "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...",
+  "session.review.noVcs.createGit.action": "创建 Git 仓库",
+  "session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)",
+  "session.question.progress": "{{current}}/{{total}} 个问题",
+  "session.header.open.finder": "访达",
+  "session.header.open.fileExplorer": "文件资源管理器",
+  "session.header.open.fileManager": "文件管理器",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "终端",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "开发性能诊断",
+  "debugBar.na": "不适用",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "过去 5 秒内的滚动帧率。",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "过去 5 秒内最差的帧时间。",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "当前应用生命周期的累积布局偏移。",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。",
+  "debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "空格",
+  "common.key.backspace": "退格",
+  "common.key.enter": "回车",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "未知",
+  "error.page.circular": "[循环]",
+  "error.globalSDK.noServerAvailable": "无可用服务器",
+  "error.globalSDK.serverNotAvailable": "服务器不可用",
+  "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
+  "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
+  "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
+  "error.childStore.storeCreateFailed": "创建存储失败",
+  "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}",
 } satisfies Partial<Record<Keys, string>>

+ 73 - 0
packages/app/src/i18n/zht.ts

@@ -848,4 +848,77 @@ export const dict = {
   "common.time.daysAgo.short": "{{count}}天前",
   "settings.providers.connected.environmentDescription": "已從環境變數連線",
   "settings.providers.custom.description": "透過基本 URL 新增與 OpenAI 相容的提供者。",
+
+  "app.server.unreachable": "無法連線至 {{server}}",
+  "app.server.retrying": "正在自動重試...",
+  "app.server.otherServers": "其他伺服器",
+  "dialog.server.add.usernamePlaceholder": "使用者名稱",
+  "dialog.server.add.passwordPlaceholder": "密碼",
+  "server.row.noUsername": "無使用者名稱",
+  "session.review.noVcs.createGit.title": "建立 Git 儲存庫",
+  "session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更",
+  "session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...",
+  "session.review.noVcs.createGit.action": "建立 Git 儲存庫",
+  "session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)",
+  "session.question.progress": "{{current}}/{{total}} 個問題",
+  "session.header.open.finder": "Finder",
+  "session.header.open.fileExplorer": "檔案總管",
+  "session.header.open.fileManager": "檔案管理員",
+  "session.header.open.app.vscode": "VS Code",
+  "session.header.open.app.cursor": "Cursor",
+  "session.header.open.app.zed": "Zed",
+  "session.header.open.app.textmate": "TextMate",
+  "session.header.open.app.antigravity": "Antigravity",
+  "session.header.open.app.terminal": "終端機",
+  "session.header.open.app.iterm2": "iTerm2",
+  "session.header.open.app.ghostty": "Ghostty",
+  "session.header.open.app.warp": "Warp",
+  "session.header.open.app.xcode": "Xcode",
+  "session.header.open.app.androidStudio": "Android Studio",
+  "session.header.open.app.powershell": "PowerShell",
+  "session.header.open.app.sublimeText": "Sublime Text",
+  "debugBar.ariaLabel": "開發效能診斷",
+  "debugBar.na": "不適用",
+  "debugBar.nav.label": "NAV",
+  "debugBar.nav.tip": "最後一次完成的涉及工作階段頁面的路由轉換,從路由器啟動到穩定後的第一次繪製。",
+  "debugBar.fps.label": "FPS",
+  "debugBar.fps.tip": "過去 5 秒內的滾動幀率。",
+  "debugBar.frame.label": "FRAME",
+  "debugBar.frame.tip": "過去 5 秒內最差的幀時間。",
+  "debugBar.jank.label": "JANK",
+  "debugBar.jank.tip": "過去 5 秒內超過 32ms 的幀。",
+  "debugBar.long.label": "LONG",
+  "debugBar.long.tip": "過去 5 秒內的阻塞時間和長任務計數。最大任務:{{max}}。",
+  "debugBar.delay.label": "DELAY",
+  "debugBar.delay.tip": "過去 5 秒內觀察到的最差輸入延遲。",
+  "debugBar.inp.label": "INP",
+  "debugBar.inp.tip": "過去 5 秒內的近似互動持續時間。這類似於 INP,而非官方的 Web Vitals INP。",
+  "debugBar.cls.label": "CLS",
+  "debugBar.cls.tip": "目前應用程式生命週期的累積版面配置位移。",
+  "debugBar.mem.label": "MEM",
+  "debugBar.mem.tipUnavailable": "使用的 JS 堆積與堆積限制。僅限 Chromium。",
+  "debugBar.mem.tip": "使用的 JS 堆積與堆積限制。{{used}} / {{limit}}。",
+  "common.key.ctrl": "Ctrl",
+  "common.key.alt": "Alt",
+  "common.key.shift": "Shift",
+  "common.key.meta": "Meta",
+  "common.key.space": "空白鍵",
+  "common.key.backspace": "退格鍵",
+  "common.key.enter": "Enter",
+  "common.key.tab": "Tab",
+  "common.key.delete": "Delete",
+  "common.key.home": "Home",
+  "common.key.end": "End",
+  "common.key.pageUp": "Page Up",
+  "common.key.pageDown": "Page Down",
+  "common.key.insert": "Insert",
+  "common.unknown": "未知",
+  "error.page.circular": "[循環]",
+  "error.globalSDK.noServerAvailable": "無可用的伺服器",
+  "error.globalSDK.serverNotAvailable": "伺服器無法使用",
+  "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
+  "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
+  "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
+  "error.childStore.storeCreateFailed": "建立儲存區失敗",
+  "terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}",
 } satisfies Partial<Record<Keys, string>>

+ 17 - 15
packages/app/src/pages/error.tsx

@@ -35,14 +35,14 @@ function isInitError(error: unknown): error is InitError {
   )
 }
 
-function safeJson(value: unknown): string {
+function safeJson(value: unknown, circular: string): string {
   const seen = new WeakSet<object>()
   const json = JSON.stringify(
     value,
     (_key, val) => {
       if (typeof val === "bigint") return val.toString()
       if (typeof val === "object" && val) {
-        if (seen.has(val)) return "[Circular]"
+        if (seen.has(val)) return circular
         seen.add(val)
       }
       return val
@@ -54,14 +54,15 @@ function safeJson(value: unknown): string {
 
 function formatInitError(error: InitError, t: Translator): string {
   const data = error.data
+  const json = (value: unknown) => safeJson(value, t("error.page.circular"))
   switch (error.name) {
     case "MCPFailed": {
       const name = typeof data.name === "string" ? data.name : ""
       return t("error.chain.mcpFailed", { name })
     }
     case "ProviderAuthError": {
-      const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
-      const message = typeof data.message === "string" ? data.message : safeJson(data.message)
+      const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
+      const message = typeof data.message === "string" ? data.message : json(data.message)
       return t("error.chain.providerAuthFailed", { provider: providerID, message })
     }
     case "APIError": {
@@ -101,24 +102,24 @@ function formatInitError(error: InitError, t: Translator): string {
       ].join("\n")
     }
     case "ProviderInitError": {
-      const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
+      const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
       return t("error.chain.providerInitFailed", { provider: providerID })
     }
     case "ConfigJsonError": {
-      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
+      const path = typeof data.path === "string" ? data.path : json(data.path)
       const message = typeof data.message === "string" ? data.message : ""
       if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
       return t("error.chain.configJsonInvalid", { path })
     }
     case "ConfigDirectoryTypoError": {
-      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
-      const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir)
-      const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion)
+      const path = typeof data.path === "string" ? data.path : json(data.path)
+      const dir = typeof data.dir === "string" ? data.dir : json(data.dir)
+      const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion)
       return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
     }
     case "ConfigFrontmatterError": {
-      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
-      const message = typeof data.message === "string" ? data.message : safeJson(data.message)
+      const path = typeof data.path === "string" ? data.path : json(data.path)
+      const message = typeof data.message === "string" ? data.message : json(data.message)
       return t("error.chain.configFrontmatterError", { path, message })
     }
     case "ConfigInvalidError": {
@@ -126,7 +127,7 @@ function formatInitError(error: InitError, t: Translator): string {
         ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join("."))
         : []
       const message = typeof data.message === "string" ? data.message : ""
-      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
+      const path = typeof data.path === "string" ? data.path : json(data.path)
 
       const line = message
         ? t("error.chain.configInvalidWithMessage", { path, message })
@@ -135,14 +136,15 @@ function formatInitError(error: InitError, t: Translator): string {
       return [line, ...issues].join("\n")
     }
     case "UnknownError":
-      return typeof data.message === "string" ? data.message : safeJson(data)
+      return typeof data.message === "string" ? data.message : json(data)
     default:
       if (typeof data.message === "string") return data.message
-      return safeJson(data)
+      return json(data)
   }
 }
 
 function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
+  const json = (value: unknown) => safeJson(value, t("error.page.circular"))
   if (!error) return t("error.chain.unknown")
 
   if (isInitError(error)) {
@@ -204,7 +206,7 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag
   }
 
   const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
-  return indent + safeJson(error)
+  return indent + json(error)
 }
 
 function formatError(error: unknown, t: Translator): string {

+ 1 - 1
packages/app/src/pages/layout.tsx

@@ -2159,7 +2159,7 @@ export default function Layout(props: ParentProps) {
                   {language.t("command.provider.connect")}
                 </Button>
                 <Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
-                  Not yet
+                  {language.t("toast.update.action.notYet")}
                 </Button>
               </div>
             </div>

+ 5 - 3
packages/app/src/pages/session.tsx

@@ -956,13 +956,15 @@ export default function Page() {
       return (
         <div class={input.emptyClass}>
           <div class="flex flex-col gap-3">
-            <div class="text-14-medium text-text-strong">Create a Git repository</div>
+            <div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
             <div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
-              Track, review, and undo changes in this project
+              {language.t("session.review.noVcs.createGit.description")}
             </div>
           </div>
           <Button size="large" disabled={ui.git} onClick={initGit}>
-            {ui.git ? "Creating Git repository..." : "Create Git repository"}
+            {ui.git
+              ? language.t("session.review.noVcs.createGit.actionLoading")
+              : language.t("session.review.noVcs.createGit.action")}
           </Button>
         </div>
       )

+ 0 - 1
packages/app/src/pages/session/composer/session-composer-region.tsx

@@ -196,7 +196,6 @@ export function SessionComposerRegion(props: {
                   <SessionTodoDock
                     sessionID={route.params.id}
                     todos={props.state.todos()}
-                    title={language.t("session.todo.title")}
                     collapseLabel={language.t("session.todo.collapse")}
                     expandLabel={language.t("session.todo.expand")}
                     dockProgress={value()}

+ 1 - 1
packages/app/src/pages/session/composer/session-question-dock.tsx

@@ -38,7 +38,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
 
   const summary = createMemo(() => {
     const n = Math.min(store.tab + 1, total())
-    return `${n} of ${total()} questions`
+    return language.t("session.question.progress", { current: n, total: total() })
   })
 
   const last = createMemo(() => store.tab >= total() - 1)

+ 22 - 6
packages/app/src/pages/session/composer/session-todo-dock.tsx

@@ -9,6 +9,10 @@ import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
 import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { composerEnabled, composerProbe } from "@/testing/session-composer"
+import { useLanguage } from "@/context/language"
+
+const doneToken = "\u0000done\u0000"
+const totalToken = "\u0000total\u0000"
 
 function dot(status: Todo["status"]) {
   if (status !== "in_progress") return undefined
@@ -38,11 +42,11 @@ function dot(status: Todo["status"]) {
 export function SessionTodoDock(props: {
   sessionID?: string
   todos: Todo[]
-  title: string
   collapseLabel: string
   expandLabel: string
   dockProgress: number
 }) {
+  const language = useLanguage()
   const [store, setStore] = createStore({
     collapsed: false,
     height: 320,
@@ -52,7 +56,12 @@ export function SessionTodoDock(props: {
 
   const total = createMemo(() => props.todos.length)
   const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
-  const label = createMemo(() => `${done()} of ${total()} ${props.title.toLowerCase()} completed`)
+  const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
+  const progress = createMemo(() =>
+    language
+      .t("session.todo.progress", { done: doneToken, total: totalToken })
+      .split(/(\u0000done\u0000|\u0000total\u0000)/),
+  )
 
   const active = createMemo(
     () =>
@@ -137,10 +146,17 @@ export function SessionTodoDock(props: {
               opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
             }}
           >
-            <AnimatedNumber value={done()} />
-            <span class="mx-1">of</span>
-            <AnimatedNumber value={total()} />
-            <span>&nbsp;{props.title.toLowerCase()} completed</span>
+            <Index each={progress()}>
+              {(item) =>
+                item() === doneToken ? (
+                  <AnimatedNumber value={done()} />
+                ) : item() === totalToken ? (
+                  <AnimatedNumber value={total()} />
+                ) : (
+                  <span>{item()}</span>
+                )
+              }
+            </Index>
           </span>
           <div
             data-slot="session-todo-preview"

+ 3 - 3
packages/app/src/pages/session/terminal-label.ts

@@ -1,3 +1,5 @@
+import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
+
 export const terminalTabLabel = (input: {
   title?: string
   titleNumber?: number
@@ -5,9 +7,7 @@ export const terminalTabLabel = (input: {
 }) => {
   const title = input.title ?? ""
   const number = input.titleNumber ?? 0
-  const match = title.match(/^Terminal (\d+)$/)
-  const parsed = match ? Number(match[1]) : undefined
-  const isDefaultTitle = Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === number
+  const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number)
 
   if (title && !isDefaultTitle) return title
   if (number > 0) return input.t("terminal.title.numbered", { number })

+ 4 - 1
packages/ui/src/components/basic-tool.tsx

@@ -1,5 +1,6 @@
 import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
 import { animate, type AnimationPlaybackControls } from "motion"
+import { useI18n } from "../context/i18n"
 import { createStore } from "solid-js/store"
 import { Collapsible } from "./collapsible"
 import type { IconProps } from "./icon"
@@ -233,12 +234,14 @@ export function GenericTool(props: {
   hideDetails?: boolean
   input?: Record<string, unknown>
 }) {
+  const i18n = useI18n()
+
   return (
     <BasicTool
       icon="mcp"
       status={props.status}
       trigger={{
-        title: `Called \`${props.tool}\``,
+        title: i18n.t("ui.basicTool.called", { tool: props.tool }),
         subtitle: label(props.input),
         args: args(props.input),
       }}

+ 7 - 4
packages/ui/src/components/file-search.tsx

@@ -1,4 +1,5 @@
 import { Portal } from "solid-js/web"
+import { useI18n } from "../context/i18n"
 import { Icon } from "./icon"
 
 export function FileSearchBar(props: {
@@ -13,6 +14,8 @@ export function FileSearchBar(props: {
   onPrev: () => void
   onNext: () => void
 }) {
+  const i18n = useI18n()
+
   return (
     <Portal>
       <div
@@ -26,7 +29,7 @@ export function FileSearchBar(props: {
         <Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" />
         <input
           ref={props.setInput}
-          placeholder="Find"
+          placeholder={i18n.t("ui.fileSearch.placeholder")}
           value={props.query()}
           class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak"
           onInput={(e) => props.onInput(e.currentTarget.value)}
@@ -40,7 +43,7 @@ export function FileSearchBar(props: {
             type="button"
             class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
             disabled={props.count() === 0}
-            aria-label="Previous match"
+            aria-label={i18n.t("ui.fileSearch.previousMatch")}
             onClick={props.onPrev}
           >
             <Icon name="chevron-down" size="small" class="rotate-180" />
@@ -49,7 +52,7 @@ export function FileSearchBar(props: {
             type="button"
             class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
             disabled={props.count() === 0}
-            aria-label="Next match"
+            aria-label={i18n.t("ui.fileSearch.nextMatch")}
             onClick={props.onNext}
           >
             <Icon name="chevron-down" size="small" />
@@ -58,7 +61,7 @@ export function FileSearchBar(props: {
         <button
           type="button"
           class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong"
-          aria-label="Close search"
+          aria-label={i18n.t("ui.fileSearch.close")}
           onClick={props.onClose}
         >
           <Icon name="close-small" size="small" />

+ 5 - 3
packages/ui/src/components/line-comment-annotations.tsx

@@ -2,6 +2,7 @@ import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
 import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js"
 import { createStore } from "solid-js/store"
 import { render as renderSolid } from "solid-js/web"
+import { useI18n } from "../context/i18n"
 import { createHoverCommentUtility } from "../pierre/comment-hover"
 import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
 import { LineComment, LineCommentEditor } from "./line-comment"
@@ -341,6 +342,7 @@ export function createLineCommentController<T extends LineCommentShape>(
 export function createLineCommentController<T extends LineCommentShape>(
   props: LineCommentControllerProps<T> | LineCommentControllerWithSideProps<T>,
 ) {
+  const i18n = useI18n()
   const note = createLineCommentState<string>(props.state)
 
   const annotations =
@@ -376,7 +378,7 @@ export function createLineCommentController<T extends LineCommentShape>(
           return note.isOpen(comment.id) || note.isEditing(comment.id)
         },
         comment: comment.comment,
-        selection: formatSelectedLineLabel(comment.selection),
+        selection: formatSelectedLineLabel(comment.selection, i18n.t),
         get actions() {
           return props.renderCommentActions?.(comment, { edit, remove })
         },
@@ -386,7 +388,7 @@ export function createLineCommentController<T extends LineCommentShape>(
                 get value() {
                   return note.draft()
                 },
-                selection: formatSelectedLineLabel(comment.selection),
+                selection: formatSelectedLineLabel(comment.selection, i18n.t),
                 onInput: note.setDraft,
                 onCancel: note.cancelDraft,
                 onSubmit: (value: string) => {
@@ -412,7 +414,7 @@ export function createLineCommentController<T extends LineCommentShape>(
       get value() {
         return note.draft()
       },
-      selection: formatSelectedLineLabel(range),
+      selection: formatSelectedLineLabel(range, i18n.t),
       onInput: note.setDraft,
       onCancel: note.cancelDraft,
       onSubmit: (comment) => {

+ 11 - 9
packages/ui/src/components/message-part.tsx

@@ -322,7 +322,7 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
     case "skill":
       return {
         icon: "brain",
-        title: input.name || "skill",
+        title: input.name || i18n.t("ui.tool.skill"),
       }
     default:
       return {
@@ -924,15 +924,12 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
     const match = data.store.provider?.all?.find((p) => p.id === providerID)
     return match?.models?.[modelID]?.name ?? modelID
   })
+  const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" }))
 
   const stamp = createMemo(() => {
     const created = props.message.time?.created
     if (typeof created !== "number") return ""
-    const date = new Date(created)
-    const hours = date.getHours()
-    const hour12 = hours % 12 || 12
-    const minute = String(date.getMinutes()).padStart(2, "0")
-    return `${hour12}:${minute} ${hours < 12 ? "AM" : "PM"}`
+    return timefmt().format(created)
   })
 
   const metaHead = createMemo(() => {
@@ -1318,6 +1315,7 @@ PART_MAPPING["compaction"] = function CompactionPartDisplay() {
 PART_MAPPING["text"] = function TextPartDisplay(props) {
   const data = useData()
   const i18n = useI18n()
+  const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale()))
   const part = () => props.part as TextPart
   const interrupted = createMemo(
     () =>
@@ -1343,10 +1341,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
           : -1
     if (!(ms >= 0)) return ""
     const total = Math.round(ms / 1000)
-    if (total < 60) return `${total}s`
+    if (total < 60) return i18n.t("ui.message.duration.seconds", { count: numfmt().format(total) })
     const minutes = Math.floor(total / 60)
     const seconds = total % 60
-    return `${minutes}m ${seconds}s`
+    return i18n.t("ui.message.duration.minutesSeconds", {
+      minutes: numfmt().format(minutes),
+      seconds: numfmt().format(seconds),
+    })
   })
 
   const meta = createMemo(() => {
@@ -2206,7 +2207,8 @@ ToolRegistry.register({
 ToolRegistry.register({
   name: "skill",
   render(props) {
-    const title = createMemo(() => props.input.name || "skill")
+    const i18n = useI18n()
+    const title = createMemo(() => props.input.name || i18n.t("ui.tool.skill"))
     const running = createMemo(() => props.status === "pending" || props.status === "running")
 
     const titleContent = () => <TextShimmer text={title()} active={running()} />

+ 10 - 6
packages/ui/src/components/tool-error-card.tsx

@@ -30,7 +30,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
       list: "ui.tool.list",
       glob: "ui.tool.glob",
       grep: "ui.tool.grep",
-      task: "Task",
+      task: "ui.tool.task",
       webfetch: "ui.tool.webfetch",
       websearch: "ui.tool.websearch",
       codesearch: "ui.tool.codesearch",
@@ -54,10 +54,10 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
   const subtitle = createMemo(() => {
     if (split.subtitle) return split.subtitle
     const parts = tail().split(": ")
-    if (parts.length <= 1) return "Failed"
+    if (parts.length <= 1) return i18n.t("ui.toolErrorCard.failed")
     const head = (parts[0] ?? "").trim()
-    if (!head) return "Failed"
-    return head[0] ? head[0].toUpperCase() + head.slice(1) : "Failed"
+    if (!head) return i18n.t("ui.toolErrorCard.failed")
+    return head[0] ? head[0].toUpperCase() + head.slice(1) : i18n.t("ui.toolErrorCard.failed")
   })
 
   const body = createMemo(() => {
@@ -116,7 +116,11 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
           <div data-slot="tool-error-card-content">
             <Show when={open()}>
               <div data-slot="tool-error-card-copy">
-                <Tooltip value={copied() ? i18n.t("ui.message.copied") : "Copy error"} placement="top" gutter={4}>
+                <Tooltip
+                  value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
+                  placement="top"
+                  gutter={4}
+                >
                   <IconButton
                     icon={copied() ? "check" : "copy"}
                     size="normal"
@@ -126,7 +130,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
                       e.stopPropagation()
                       copy()
                     }}
-                    aria-label={copied() ? i18n.t("ui.message.copied") : "Copy error"}
+                    aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
                   />
                 </Tooltip>
               </div>

+ 12 - 0
packages/ui/src/i18n/ar.ts

@@ -145,4 +145,16 @@ export const dict = {
   "ui.question.multiHint": "حدد كل ما ينطبق",
   "ui.question.singleHint": "حدد إجابة واحدة",
   "ui.question.custom.placeholder": "اكتب إجابتك...",
+
+  "ui.fileSearch.placeholder": "بحث",
+  "ui.fileSearch.previousMatch": "المطابقة السابقة",
+  "ui.fileSearch.nextMatch": "المطابقة التالية",
+  "ui.fileSearch.close": "إغلاق البحث",
+  "ui.tool.task": "مهمة",
+  "ui.tool.skill": "مهارة",
+  "ui.basicTool.called": "تم استدعاء `{{tool}}`",
+  "ui.toolErrorCard.failed": "فشل",
+  "ui.toolErrorCard.copyError": "نسخ الخطأ",
+  "ui.message.duration.seconds": "{{count}}ث",
+  "ui.message.duration.minutesSeconds": "{{minutes}}د {{seconds}}ث",
 }

+ 12 - 0
packages/ui/src/i18n/br.ts

@@ -145,4 +145,16 @@ export const dict = {
   "ui.question.multiHint": "Selecione todas que se aplicam",
   "ui.question.singleHint": "Selecione uma resposta",
   "ui.question.custom.placeholder": "Digite sua resposta...",
+
+  "ui.fileSearch.placeholder": "Localizar",
+  "ui.fileSearch.previousMatch": "Ocorrência anterior",
+  "ui.fileSearch.nextMatch": "Próxima ocorrência",
+  "ui.fileSearch.close": "Fechar busca",
+  "ui.tool.task": "Tarefa",
+  "ui.tool.skill": "Habilidade",
+  "ui.basicTool.called": "Chamou `{{tool}}`",
+  "ui.toolErrorCard.failed": "Falhou",
+  "ui.toolErrorCard.copyError": "Copiar erro",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/bs.ts

@@ -149,4 +149,16 @@ export const dict = {
   "ui.question.multiHint": "Odaberi sve što važi",
   "ui.question.singleHint": "Odaberi jedan odgovor",
   "ui.question.custom.placeholder": "Unesi svoj odgovor...",
+
+  "ui.fileSearch.placeholder": "Pronađi",
+  "ui.fileSearch.previousMatch": "Prethodno",
+  "ui.fileSearch.nextMatch": "Sljedeće",
+  "ui.fileSearch.close": "Zatvori pretragu",
+  "ui.tool.task": "Zadatak",
+  "ui.tool.skill": "Vještina",
+  "ui.basicTool.called": "Pozvan `{{tool}}`",
+  "ui.toolErrorCard.failed": "Neuspješno",
+  "ui.toolErrorCard.copyError": "Kopiraj grešku",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 } satisfies Partial<Record<Keys, string>>

+ 12 - 0
packages/ui/src/i18n/da.ts

@@ -144,4 +144,16 @@ export const dict = {
   "ui.question.multiHint": "Vælg alle der gælder",
   "ui.question.singleHint": "Vælg ét svar",
   "ui.question.custom.placeholder": "Skriv dit svar...",
+
+  "ui.fileSearch.placeholder": "Find",
+  "ui.fileSearch.previousMatch": "Forrige match",
+  "ui.fileSearch.nextMatch": "Næste match",
+  "ui.fileSearch.close": "Luk søgning",
+  "ui.tool.task": "Opgave",
+  "ui.tool.skill": "Færdighed",
+  "ui.basicTool.called": "Kaldte `{{tool}}`",
+  "ui.toolErrorCard.failed": "Fejlede",
+  "ui.toolErrorCard.copyError": "Kopier fejl",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/de.ts

@@ -150,4 +150,16 @@ export const dict = {
   "ui.question.multiHint": "Alle zutreffenden auswählen",
   "ui.question.singleHint": "Eine Antwort auswählen",
   "ui.question.custom.placeholder": "Geben Sie Ihre Antwort ein...",
+
+  "ui.fileSearch.placeholder": "Suchen",
+  "ui.fileSearch.previousMatch": "Vorheriges Ergebnis",
+  "ui.fileSearch.nextMatch": "Nächstes Ergebnis",
+  "ui.fileSearch.close": "Suche schließen",
+  "ui.tool.task": "Aufgabe",
+  "ui.tool.skill": "Fähigkeit",
+  "ui.basicTool.called": "`{{tool}}` aufgerufen",
+  "ui.toolErrorCard.failed": "Fehlgeschlagen",
+  "ui.toolErrorCard.copyError": "Fehler kopieren",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 } satisfies Partial<Record<Keys, string>>

+ 13 - 0
packages/ui/src/i18n/en.ts

@@ -80,6 +80,11 @@ export const dict: Record<string, string> = {
   "ui.list.emptyWithFilter.prefix": "No results for",
   "ui.list.emptyWithFilter.suffix": "",
 
+  "ui.fileSearch.placeholder": "Find",
+  "ui.fileSearch.previousMatch": "Previous match",
+  "ui.fileSearch.nextMatch": "Next match",
+  "ui.fileSearch.close": "Close search",
+
   "ui.messageNav.newMessage": "New message",
 
   "ui.textField.copyToClipboard": "Copy to clipboard",
@@ -94,6 +99,7 @@ export const dict: Record<string, string> = {
   "ui.tool.list": "List",
   "ui.tool.glob": "Glob",
   "ui.tool.grep": "Grep",
+  "ui.tool.task": "Task",
   "ui.tool.webfetch": "Webfetch",
   "ui.tool.websearch": "Web Search",
   "ui.tool.codesearch": "Code Search",
@@ -104,6 +110,11 @@ export const dict: Record<string, string> = {
   "ui.tool.questions": "Questions",
   "ui.tool.agent": "{{type}} Agent",
   "ui.tool.agent.default": "Agent",
+  "ui.tool.skill": "Skill",
+
+  "ui.basicTool.called": "Called `{{tool}}`",
+  "ui.toolErrorCard.failed": "Failed",
+  "ui.toolErrorCard.copyError": "Copy error",
 
   "ui.common.file.one": "file",
   "ui.common.file.other": "files",
@@ -131,6 +142,8 @@ export const dict: Record<string, string> = {
   "ui.message.revertMessage": "Revert message",
   "ui.message.copyResponse": "Copy response",
   "ui.message.copied": "Copied",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
   "ui.message.interrupted": "Interrupted",
   "ui.message.queued": "Queued",
   "ui.message.attachment.alt": "attachment",

+ 12 - 0
packages/ui/src/i18n/es.ts

@@ -145,4 +145,16 @@ export const dict = {
   "ui.question.multiHint": "Selecciona todas las que correspondan",
   "ui.question.singleHint": "Selecciona una respuesta",
   "ui.question.custom.placeholder": "Escribe tu respuesta...",
+
+  "ui.fileSearch.placeholder": "Buscar",
+  "ui.fileSearch.previousMatch": "Anterior",
+  "ui.fileSearch.nextMatch": "Siguiente",
+  "ui.fileSearch.close": "Cerrar búsqueda",
+  "ui.tool.task": "Tarea",
+  "ui.tool.skill": "Habilidad",
+  "ui.basicTool.called": "Llamado `{{tool}}`",
+  "ui.toolErrorCard.failed": "Falló",
+  "ui.toolErrorCard.copyError": "Copiar error",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/fr.ts

@@ -145,4 +145,16 @@ export const dict = {
   "ui.question.multiHint": "Sélectionnez tout ce qui s'applique",
   "ui.question.singleHint": "Sélectionnez une réponse",
   "ui.question.custom.placeholder": "Tapez votre réponse...",
+
+  "ui.fileSearch.placeholder": "Rechercher",
+  "ui.fileSearch.previousMatch": "Précédent",
+  "ui.fileSearch.nextMatch": "Suivant",
+  "ui.fileSearch.close": "Fermer la recherche",
+  "ui.tool.task": "Tâche",
+  "ui.tool.skill": "Compétence",
+  "ui.basicTool.called": "Appelé `{{tool}}`",
+  "ui.toolErrorCard.failed": "Échoué",
+  "ui.toolErrorCard.copyError": "Copier l'erreur",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/ja.ts

@@ -144,4 +144,16 @@ export const dict = {
   "ui.question.multiHint": "該当するものをすべて選択",
   "ui.question.singleHint": "1 つ選択",
   "ui.question.custom.placeholder": "回答を入力...",
+
+  "ui.fileSearch.placeholder": "検索",
+  "ui.fileSearch.previousMatch": "前の一致",
+  "ui.fileSearch.nextMatch": "次の一致",
+  "ui.fileSearch.close": "検索を閉じる",
+  "ui.tool.task": "タスク",
+  "ui.tool.skill": "スキル",
+  "ui.basicTool.called": "`{{tool}}` を呼び出しました",
+  "ui.toolErrorCard.failed": "失敗",
+  "ui.toolErrorCard.copyError": "エラーをコピー",
+  "ui.message.duration.seconds": "{{count}}秒",
+  "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒",
 }

+ 12 - 0
packages/ui/src/i18n/ko.ts

@@ -145,4 +145,16 @@ export const dict = {
   "ui.question.multiHint": "해당하는 항목 모두 선택",
   "ui.question.singleHint": "하나의 답변을 선택",
   "ui.question.custom.placeholder": "답변 입력...",
+
+  "ui.fileSearch.placeholder": "찾기",
+  "ui.fileSearch.previousMatch": "이전 항목",
+  "ui.fileSearch.nextMatch": "다음 항목",
+  "ui.fileSearch.close": "검색 닫기",
+  "ui.tool.task": "작업",
+  "ui.tool.skill": "스킬",
+  "ui.basicTool.called": "`{{tool}}` 호출됨",
+  "ui.toolErrorCard.failed": "실패",
+  "ui.toolErrorCard.copyError": "오류 복사",
+  "ui.message.duration.seconds": "{{count}}초",
+  "ui.message.duration.minutesSeconds": "{{minutes}}분 {{seconds}}초",
 }

+ 12 - 0
packages/ui/src/i18n/no.ts

@@ -148,4 +148,16 @@ export const dict: Record<Keys, string> = {
   "ui.question.multiHint": "Velg alle som gjelder",
   "ui.question.singleHint": "Velg ett svar",
   "ui.question.custom.placeholder": "Skriv svaret ditt...",
+
+  "ui.fileSearch.placeholder": "Finn",
+  "ui.fileSearch.previousMatch": "Forrige treff",
+  "ui.fileSearch.nextMatch": "Neste treff",
+  "ui.fileSearch.close": "Lukk søk",
+  "ui.tool.task": "Oppgave",
+  "ui.tool.skill": "Ferdighet",
+  "ui.basicTool.called": "Kalte `{{tool}}`",
+  "ui.toolErrorCard.failed": "Mislyktes",
+  "ui.toolErrorCard.copyError": "Kopier feil",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/pl.ts

@@ -144,4 +144,16 @@ export const dict = {
   "ui.question.multiHint": "Zaznacz wszystkie pasujące",
   "ui.question.singleHint": "Wybierz jedną odpowiedź",
   "ui.question.custom.placeholder": "Wpisz swoją odpowiedź...",
+
+  "ui.fileSearch.placeholder": "Szukaj",
+  "ui.fileSearch.previousMatch": "Poprzednie",
+  "ui.fileSearch.nextMatch": "Następne",
+  "ui.fileSearch.close": "Zamknij wyszukiwanie",
+  "ui.tool.task": "Zadanie",
+  "ui.tool.skill": "Umiejętność",
+  "ui.basicTool.called": "Wywołano `{{tool}}`",
+  "ui.toolErrorCard.failed": "Błąd",
+  "ui.toolErrorCard.copyError": "Kopiuj błąd",
+  "ui.message.duration.seconds": "{{count}}s",
+  "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
 }

+ 12 - 0
packages/ui/src/i18n/ru.ts

@@ -144,4 +144,16 @@ export const dict = {
   "ui.question.multiHint": "Выберите все подходящие",
   "ui.question.singleHint": "Выберите один ответ",
   "ui.question.custom.placeholder": "Введите ваш ответ...",
+
+  "ui.fileSearch.placeholder": "Найти",
+  "ui.fileSearch.previousMatch": "Предыдущее",
+  "ui.fileSearch.nextMatch": "Следующее",
+  "ui.fileSearch.close": "Закрыть поиск",
+  "ui.tool.task": "Задача",
+  "ui.tool.skill": "Навык",
+  "ui.basicTool.called": "Вызван `{{tool}}`",
+  "ui.toolErrorCard.failed": "Ошибка",
+  "ui.toolErrorCard.copyError": "Скопировать ошибку",
+  "ui.message.duration.seconds": "{{count}}с",
+  "ui.message.duration.minutesSeconds": "{{minutes}}м {{seconds}}с",
 }

+ 12 - 0
packages/ui/src/i18n/th.ts

@@ -146,4 +146,16 @@ export const dict = {
   "ui.question.multiHint": "เลือกทั้งหมดที่ใช้",
   "ui.question.singleHint": "เลือกหนึ่งคำตอบ",
   "ui.question.custom.placeholder": "พิมพ์คำตอบของคุณ...",
+
+  "ui.fileSearch.placeholder": "ค้นหา",
+  "ui.fileSearch.previousMatch": "ก่อนหน้า",
+  "ui.fileSearch.nextMatch": "ถัดไป",
+  "ui.fileSearch.close": "ปิดการค้นหา",
+  "ui.tool.task": "งาน",
+  "ui.tool.skill": "ทักษะ",
+  "ui.basicTool.called": "เรียกใช้ `{{tool}}`",
+  "ui.toolErrorCard.failed": "ล้มเหลว",
+  "ui.toolErrorCard.copyError": "คัดลอกข้อผิดพลาด",
+  "ui.message.duration.seconds": "{{count}}วิ",
+  "ui.message.duration.minutesSeconds": "{{minutes}}นาที {{seconds}}วิ",
 }

+ 12 - 0
packages/ui/src/i18n/tr.ts

@@ -151,4 +151,16 @@ export const dict = {
   "ui.question.multiHint": "Geçerli tüm cevapları seçin",
   "ui.question.singleHint": "Bir cevap seçin",
   "ui.question.custom.placeholder": "Cevabınızı yazın...",
+
+  "ui.fileSearch.placeholder": "Bul",
+  "ui.fileSearch.previousMatch": "Önceki",
+  "ui.fileSearch.nextMatch": "Sonraki",
+  "ui.fileSearch.close": "Aramayı kapat",
+  "ui.tool.task": "Görev",
+  "ui.tool.skill": "Yetenek",
+  "ui.basicTool.called": "`{{tool}}` çağrıldı",
+  "ui.toolErrorCard.failed": "Başarısız",
+  "ui.toolErrorCard.copyError": "Hatayı kopyala",
+  "ui.message.duration.seconds": "{{count}}sn",
+  "ui.message.duration.minutesSeconds": "{{minutes}}dk {{seconds}}sn",
 } satisfies Partial<Record<Keys, string>>

+ 12 - 0
packages/ui/src/i18n/zh.ts

@@ -149,4 +149,16 @@ export const dict = {
   "ui.question.multiHint": "可多选",
   "ui.question.singleHint": "选择一个答案",
   "ui.question.custom.placeholder": "输入你的答案...",
+
+  "ui.fileSearch.placeholder": "查找",
+  "ui.fileSearch.previousMatch": "上一个",
+  "ui.fileSearch.nextMatch": "下一个",
+  "ui.fileSearch.close": "关闭搜索",
+  "ui.tool.task": "任务",
+  "ui.tool.skill": "技能",
+  "ui.basicTool.called": "调用了 `{{tool}}`",
+  "ui.toolErrorCard.failed": "失败",
+  "ui.toolErrorCard.copyError": "复制错误",
+  "ui.message.duration.seconds": "{{count}}秒",
+  "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒",
 } satisfies Partial<Record<Keys, string>>

+ 12 - 0
packages/ui/src/i18n/zht.ts

@@ -149,4 +149,16 @@ export const dict = {
   "ui.question.multiHint": "可多選",
   "ui.question.singleHint": "選擇一個答案",
   "ui.question.custom.placeholder": "輸入你的答案...",
+
+  "ui.fileSearch.placeholder": "搜尋",
+  "ui.fileSearch.previousMatch": "上一個",
+  "ui.fileSearch.nextMatch": "下一個",
+  "ui.fileSearch.close": "關閉搜尋",
+  "ui.tool.task": "任務",
+  "ui.tool.skill": "技能",
+  "ui.basicTool.called": "呼叫了 `{{tool}}`",
+  "ui.toolErrorCard.failed": "失敗",
+  "ui.toolErrorCard.copyError": "複製錯誤",
+  "ui.message.duration.seconds": "{{count}}秒",
+  "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒",
 } satisfies Partial<Record<Keys, string>>

+ 6 - 3
packages/ui/src/pierre/selection-bridge.ts

@@ -1,14 +1,17 @@
 import { type SelectedLineRange } from "@pierre/diffs"
 
+type SelectionKey = "ui.sessionReview.selection.line" | "ui.sessionReview.selection.lines"
+type SelectionVars = Record<string, string | number>
+
 type PointerMode = "none" | "text" | "numbers"
 type Side = SelectedLineRange["side"]
 type LineSpan = Pick<SelectedLineRange, "start" | "end">
 
-export function formatSelectedLineLabel(range: LineSpan) {
+export function formatSelectedLineLabel(range: LineSpan, t: (key: SelectionKey, params: SelectionVars) => string) {
   const start = Math.min(range.start, range.end)
   const end = Math.max(range.start, range.end)
-  if (start === end) return `line ${start}`
-  return `lines ${start}-${end}`
+  if (start === end) return t("ui.sessionReview.selection.line", { line: start })
+  return t("ui.sessionReview.selection.lines", { start, end })
 }
 
 export function previewSelectedLines(source: string, range: LineSpan) {