Adam 2 месяцев назад
Родитель
Сommit
92beae1410

+ 15 - 8
packages/app/src/components/dialog-edit-project.tsx

@@ -9,12 +9,14 @@ import { useGlobalSDK } from "@/context/global-sdk"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
 import { getFilename } from "@opencode-ai/util/path"
 import { getFilename } from "@opencode-ai/util/path"
 import { Avatar } from "@opencode-ai/ui/avatar"
 import { Avatar } from "@opencode-ai/ui/avatar"
+import { useLanguage } from "@/context/language"
 
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 
 
 export function DialogEditProject(props: { project: LocalProject }) {
 export function DialogEditProject(props: { project: LocalProject }) {
   const dialog = useDialog()
   const dialog = useDialog()
   const globalSDK = useGlobalSDK()
   const globalSDK = useGlobalSDK()
+  const language = useLanguage()
 
 
   const folderName = createMemo(() => getFilename(props.project.worktree))
   const folderName = createMemo(() => getFilename(props.project.worktree))
   const defaultName = createMemo(() => props.project.name || folderName())
   const defaultName = createMemo(() => props.project.name || folderName())
@@ -81,20 +83,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
   }
   }
 
 
   return (
   return (
-    <Dialog title="Edit project" class="w-full max-w-[480px] mx-auto">
+    <Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
       <form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
       <form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
         <div class="flex flex-col gap-4">
         <div class="flex flex-col gap-4">
           <TextField
           <TextField
             autofocus
             autofocus
             type="text"
             type="text"
-            label="Name"
+            label={language.t("dialog.project.edit.name")}
             placeholder={folderName()}
             placeholder={folderName()}
             value={store.name}
             value={store.name}
             onChange={(v) => setStore("name", v)}
             onChange={(v) => setStore("name", v)}
           />
           />
 
 
           <div class="flex flex-col gap-2">
           <div class="flex flex-col gap-2">
-            <label class="text-12-medium text-text-weak">Icon</label>
+            <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
             <div class="flex gap-3 items-start">
             <div class="flex gap-3 items-start">
               <div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
               <div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
                 <div
                 <div
@@ -128,7 +130,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
                       </div>
                       </div>
                     }
                     }
                   >
                   >
-                    <img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
+                    <img
+                      src={store.iconUrl}
+                      alt={language.t("dialog.project.edit.icon.alt")}
+                      class="size-full object-cover"
+                    />
                   </Show>
                   </Show>
                 </div>
                 </div>
                 <div
                 <div
@@ -172,14 +178,15 @@ export function DialogEditProject(props: { project: LocalProject }) {
               </div>
               </div>
               <input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
               <input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
               <div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
               <div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
-                <span>Recommended size 128x128px</span>
+                <span>{language.t("dialog.project.edit.icon.hint")}</span>
+                <span>{language.t("dialog.project.edit.icon.recommended")}</span>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
 
 
           <Show when={!store.iconUrl}>
           <Show when={!store.iconUrl}>
             <div class="flex flex-col gap-2">
             <div class="flex flex-col gap-2">
-              <label class="text-12-medium text-text-weak">Color</label>
+              <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
               <div class="flex gap-1.5">
               <div class="flex gap-1.5">
                 <For each={AVATAR_COLOR_KEYS}>
                 <For each={AVATAR_COLOR_KEYS}>
                   {(color) => (
                   {(color) => (
@@ -209,10 +216,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
 
 
         <div class="flex justify-end gap-2">
         <div class="flex justify-end gap-2">
           <Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
           <Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
-            Cancel
+            {language.t("common.cancel")}
           </Button>
           </Button>
           <Button type="submit" variant="primary" size="large" disabled={store.saving}>
           <Button type="submit" variant="primary" size="large" disabled={store.saving}>
-            {store.saving ? "Saving..." : "Save"}
+            {store.saving ? language.t("common.saving") : language.t("common.save")}
           </Button>
           </Button>
         </div>
         </div>
       </form>
       </form>

+ 5 - 3
packages/app/src/components/dialog-fork.tsx

@@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list"
 import { extractPromptFromParts } from "@/utils/prompt"
 import { extractPromptFromParts } from "@/utils/prompt"
 import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
 import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { base64Encode } from "@opencode-ai/util/encode"
+import { useLanguage } from "@/context/language"
 
 
 interface ForkableMessage {
 interface ForkableMessage {
   id: string
   id: string
@@ -27,6 +28,7 @@ export const DialogFork: Component = () => {
   const sdk = useSDK()
   const sdk = useSDK()
   const prompt = usePrompt()
   const prompt = usePrompt()
   const dialog = useDialog()
   const dialog = useDialog()
+  const language = useLanguage()
 
 
   const messages = createMemo((): ForkableMessage[] => {
   const messages = createMemo((): ForkableMessage[] => {
     const sessionID = params.id
     const sessionID = params.id
@@ -73,11 +75,11 @@ export const DialogFork: Component = () => {
   }
   }
 
 
   return (
   return (
-    <Dialog title="Fork from message">
+    <Dialog title={language.t("command.session.fork")}>
       <List
       <List
         class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
         class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
-        search={{ placeholder: "Search", autofocus: true }}
-        emptyMessage="No messages to fork from"
+        search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
+        emptyMessage={language.t("dialog.fork.empty")}
         key={(x) => x.id}
         key={(x) => x.id}
         items={messages}
         items={messages}
         filterKeys={["text"]}
         filterKeys={["text"]}

+ 5 - 3
packages/app/src/components/dialog-manage-models.tsx

@@ -4,14 +4,16 @@ import { Switch } from "@opencode-ai/ui/switch"
 import type { Component } from "solid-js"
 import type { Component } from "solid-js"
 import { useLocal } from "@/context/local"
 import { useLocal } from "@/context/local"
 import { popularProviders } from "@/hooks/use-providers"
 import { popularProviders } from "@/hooks/use-providers"
+import { useLanguage } from "@/context/language"
 
 
 export const DialogManageModels: Component = () => {
 export const DialogManageModels: Component = () => {
   const local = useLocal()
   const local = useLocal()
+  const language = useLanguage()
   return (
   return (
-    <Dialog title="Manage models" description="Customize which models appear in the model selector.">
+    <Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
       <List
       <List
-        search={{ placeholder: "Search models", autofocus: true }}
-        emptyMessage="No model results"
+        search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
+        emptyMessage={language.t("dialog.model.empty")}
         key={(x) => `${x?.provider?.id}:${x?.id}`}
         key={(x) => `${x?.provider?.id}:${x?.id}`}
         items={local.model.list()}
         items={local.model.list()}
         filterKeys={["provider.name", "name", "id"]}
         filterKeys={["provider.name", "name", "id"]}

+ 6 - 3
packages/app/src/components/dialog-select-directory.tsx

@@ -6,6 +6,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { createMemo } from "solid-js"
 import { createMemo } from "solid-js"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
+import { useLanguage } from "@/context/language"
 
 
 interface DialogSelectDirectoryProps {
 interface DialogSelectDirectoryProps {
   title?: string
   title?: string
@@ -17,6 +18,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   const sync = useGlobalSync()
   const sync = useGlobalSync()
   const sdk = useGlobalSDK()
   const sdk = useGlobalSDK()
   const dialog = useDialog()
   const dialog = useDialog()
+  const language = useLanguage()
 
 
   const home = createMemo(() => sync.data.path.home)
   const home = createMemo(() => sync.data.path.home)
   const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
   const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
@@ -81,10 +83,11 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   }
   }
 
 
   return (
   return (
-    <Dialog title={props.title ?? "Open project"}>
+    <Dialog title={props.title ?? language.t("command.project.open")}>
       <List
       <List
-        search={{ placeholder: "Search folders", autofocus: true }}
-        emptyMessage="No folders found"
+        search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
+        emptyMessage={language.t("dialog.directory.empty")}
+        loadingMessage={language.t("common.loading")}
         items={directories}
         items={directories}
         key={(x) => x}
         key={(x) => x}
         onSelect={(path) => {
         onSelect={(path) => {

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

@@ -9,6 +9,7 @@ import { createMemo, createSignal, onCleanup, Show } from "solid-js"
 import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
 import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
 import { useFile } from "@/context/file"
 import { useFile } from "@/context/file"
+import { useLanguage } from "@/context/language"
 
 
 type EntryType = "command" | "file"
 type EntryType = "command" | "file"
 
 
@@ -18,13 +19,14 @@ type Entry = {
   title: string
   title: string
   description?: string
   description?: string
   keybind?: string
   keybind?: string
-  category: "Commands" | "Files"
+  category: string
   option?: CommandOption
   option?: CommandOption
   path?: string
   path?: string
 }
 }
 
 
 export function DialogSelectFile() {
 export function DialogSelectFile() {
   const command = useCommand()
   const command = useCommand()
+  const language = useLanguage()
   const layout = useLayout()
   const layout = useLayout()
   const file = useFile()
   const file = useFile()
   const dialog = useDialog()
   const dialog = useDialog()
@@ -56,7 +58,7 @@ export function DialogSelectFile() {
     title: option.title,
     title: option.title,
     description: option.description,
     description: option.description,
     keybind: option.keybind,
     keybind: option.keybind,
-    category: "Commands",
+    category: language.t("palette.group.commands"),
     option,
     option,
   })
   })
 
 
@@ -64,7 +66,7 @@ export function DialogSelectFile() {
     id: "file:" + path,
     id: "file:" + path,
     type: "file",
     type: "file",
     title: path,
     title: path,
-    category: "Files",
+    category: language.t("palette.group.files"),
     path,
     path,
   })
   })
 
 
@@ -143,8 +145,14 @@ export function DialogSelectFile() {
   return (
   return (
     <Dialog class="pt-3 pb-0 !max-h-[480px]">
     <Dialog class="pt-3 pb-0 !max-h-[480px]">
       <List
       <List
-        search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }}
-        emptyMessage="No results found"
+        search={{
+          placeholder: language.t("palette.search.placeholder"),
+          autofocus: true,
+          hideIcon: true,
+          class: "pl-3 pr-2 !mb-0",
+        }}
+        emptyMessage={language.t("palette.empty")}
+        loadingMessage={language.t("common.loading")}
         items={items}
         items={items}
         key={(item) => item.id}
         key={(item) => item.id}
         filterKeys={["title", "description", "category"]}
         filterKeys={["title", "description", "category"]}

+ 12 - 7
packages/app/src/components/dialog-select-mcp.tsx

@@ -4,10 +4,12 @@ import { useSDK } from "@/context/sdk"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
 import { List } from "@opencode-ai/ui/list"
 import { Switch } from "@opencode-ai/ui/switch"
 import { Switch } from "@opencode-ai/ui/switch"
+import { useLanguage } from "@/context/language"
 
 
 export const DialogSelectMcp: Component = () => {
 export const DialogSelectMcp: Component = () => {
   const sync = useSync()
   const sync = useSync()
   const sdk = useSDK()
   const sdk = useSDK()
+  const language = useLanguage()
   const [loading, setLoading] = createSignal<string | null>(null)
   const [loading, setLoading] = createSignal<string | null>(null)
 
 
   const items = createMemo(() =>
   const items = createMemo(() =>
@@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => {
   const totalCount = createMemo(() => items().length)
   const totalCount = createMemo(() => items().length)
 
 
   return (
   return (
-    <Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
+    <Dialog
+      title={language.t("dialog.mcp.title")}
+      description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
+    >
       <List
       <List
-        search={{ placeholder: "Search", autofocus: true }}
-        emptyMessage="No MCPs configured"
+        search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
+        emptyMessage={language.t("dialog.mcp.empty")}
         key={(x) => x?.name ?? ""}
         key={(x) => x?.name ?? ""}
         items={items}
         items={items}
         filterKeys={["name", "status"]}
         filterKeys={["name", "status"]}
@@ -60,16 +65,16 @@ export const DialogSelectMcp: Component = () => {
                 <div class="flex items-center gap-2">
                 <div class="flex items-center gap-2">
                   <span class="truncate">{i.name}</span>
                   <span class="truncate">{i.name}</span>
                   <Show when={status() === "connected"}>
                   <Show when={status() === "connected"}>
-                    <span class="text-11-regular text-text-weaker">connected</span>
+                    <span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span>
                   </Show>
                   </Show>
                   <Show when={status() === "failed"}>
                   <Show when={status() === "failed"}>
-                    <span class="text-11-regular text-text-weaker">failed</span>
+                    <span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span>
                   </Show>
                   </Show>
                   <Show when={status() === "needs_auth"}>
                   <Show when={status() === "needs_auth"}>
-                    <span class="text-11-regular text-text-weaker">needs auth</span>
+                    <span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span>
                   </Show>
                   </Show>
                   <Show when={status() === "disabled"}>
                   <Show when={status() === "disabled"}>
-                    <span class="text-11-regular text-text-weaker">disabled</span>
+                    <span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
                   </Show>
                   </Show>
                   <Show when={loading() === i.name}>
                   <Show when={loading() === i.name}>
                     <span class="text-11-regular text-text-weak">...</span>
                     <span class="text-11-regular text-text-weak">...</span>

+ 12 - 10
packages/app/src/components/dialog-select-model-unpaid.tsx

@@ -10,11 +10,13 @@ import { useLocal } from "@/context/local"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { DialogConnectProvider } from "./dialog-connect-provider"
 import { DialogConnectProvider } from "./dialog-connect-provider"
 import { DialogSelectProvider } from "./dialog-select-provider"
 import { DialogSelectProvider } from "./dialog-select-provider"
+import { useLanguage } from "@/context/language"
 
 
 export const DialogSelectModelUnpaid: Component = () => {
 export const DialogSelectModelUnpaid: Component = () => {
   const local = useLocal()
   const local = useLocal()
   const dialog = useDialog()
   const dialog = useDialog()
   const providers = useProviders()
   const providers = useProviders()
+  const language = useLanguage()
 
 
   let listRef: ListRef | undefined
   let listRef: ListRef | undefined
   const handleKey = (e: KeyboardEvent) => {
   const handleKey = (e: KeyboardEvent) => {
@@ -30,9 +32,9 @@ export const DialogSelectModelUnpaid: Component = () => {
   })
   })
 
 
   return (
   return (
-    <Dialog title="Select model">
+    <Dialog title={language.t("dialog.model.select.title")}>
       <div class="flex flex-col gap-3 px-2.5">
       <div class="flex flex-col gap-3 px-2.5">
-        <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
+        <div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
         <List
         <List
           ref={(ref) => (listRef = ref)}
           ref={(ref) => (listRef = ref)}
           items={local.model.list}
           items={local.model.list}
@@ -48,9 +50,9 @@ export const DialogSelectModelUnpaid: Component = () => {
           {(i) => (
           {(i) => (
             <div class="w-full flex items-center gap-x-2.5">
             <div class="w-full flex items-center gap-x-2.5">
               <span>{i.name}</span>
               <span>{i.name}</span>
-              <Tag>Free</Tag>
+              <Tag>{language.t("model.tag.free")}</Tag>
               <Show when={i.latest}>
               <Show when={i.latest}>
-                <Tag>Latest</Tag>
+                <Tag>{language.t("model.tag.latest")}</Tag>
               </Show>
               </Show>
             </div>
             </div>
           )}
           )}
@@ -60,9 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => {
       </div>
       </div>
       <div class="px-1.5 pb-1.5">
       <div class="px-1.5 pb-1.5">
         <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
         <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
-          <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
-            <div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
-            <div class="w-full">
+            <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
+            <div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
+              <div class="w-full">
               <List
               <List
                 class="w-full px-0"
                 class="w-full px-0"
                 key={(x) => x?.id}
                 key={(x) => x?.id}
@@ -83,10 +85,10 @@ export const DialogSelectModelUnpaid: Component = () => {
                     <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
                     <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
                     <span>{i.name}</span>
                     <span>{i.name}</span>
                     <Show when={i.id === "opencode"}>
                     <Show when={i.id === "opencode"}>
-                      <Tag>Recommended</Tag>
+                      <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
                     </Show>
                     </Show>
                     <Show when={i.id === "anthropic"}>
                     <Show when={i.id === "anthropic"}>
-                      <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
+                      <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
                     </Show>
                     </Show>
                   </div>
                   </div>
                 )}
                 )}
@@ -99,7 +101,7 @@ export const DialogSelectModelUnpaid: Component = () => {
                   dialog.show(() => <DialogSelectProvider />)
                   dialog.show(() => <DialogSelectProvider />)
                 }}
                 }}
               >
               >
-                View all providers
+                {language.t("dialog.provider.viewAll")}
               </Button>
               </Button>
             </div>
             </div>
           </div>
           </div>

+ 12 - 8
packages/app/src/components/dialog-select-model.tsx

@@ -9,6 +9,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
 import { List } from "@opencode-ai/ui/list"
 import { DialogSelectProvider } from "./dialog-select-provider"
 import { DialogSelectProvider } from "./dialog-select-provider"
 import { DialogManageModels } from "./dialog-manage-models"
 import { DialogManageModels } from "./dialog-manage-models"
+import { useLanguage } from "@/context/language"
 
 
 const ModelList: Component<{
 const ModelList: Component<{
   provider?: string
   provider?: string
@@ -16,6 +17,7 @@ const ModelList: Component<{
   onSelect: () => void
   onSelect: () => void
 }> = (props) => {
 }> = (props) => {
   const local = useLocal()
   const local = useLocal()
+  const language = useLanguage()
 
 
   const models = createMemo(() =>
   const models = createMemo(() =>
     local.model
     local.model
@@ -27,8 +29,8 @@ const ModelList: Component<{
   return (
   return (
     <List
     <List
       class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
       class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
-      search={{ placeholder: "Search models", autofocus: true }}
-      emptyMessage="No model results"
+      search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
+      emptyMessage={language.t("dialog.model.empty")}
       key={(x) => `${x.provider.id}:${x.id}`}
       key={(x) => `${x.provider.id}:${x.id}`}
       items={models}
       items={models}
       current={local.model.current()}
       current={local.model.current()}
@@ -55,10 +57,10 @@ const ModelList: Component<{
         <div class="w-full flex items-center gap-x-2 text-13-regular">
         <div class="w-full flex items-center gap-x-2 text-13-regular">
           <span class="truncate">{i.name}</span>
           <span class="truncate">{i.name}</span>
           <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
           <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
-            <Tag>Free</Tag>
+            <Tag>{language.t("model.tag.free")}</Tag>
           </Show>
           </Show>
           <Show when={i.latest}>
           <Show when={i.latest}>
-            <Tag>Latest</Tag>
+            <Tag>{language.t("model.tag.latest")}</Tag>
           </Show>
           </Show>
         </div>
         </div>
       )}
       )}
@@ -71,13 +73,14 @@ export const ModelSelectorPopover: Component<{
   children: JSX.Element
   children: JSX.Element
 }> = (props) => {
 }> = (props) => {
   const [open, setOpen] = createSignal(false)
   const [open, setOpen] = createSignal(false)
+  const language = useLanguage()
 
 
   return (
   return (
     <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
     <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
       <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
       <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
       <Kobalte.Portal>
       <Kobalte.Portal>
         <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
         <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
-          <Kobalte.Title class="sr-only">Select model</Kobalte.Title>
+          <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
           <ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
           <ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
         </Kobalte.Content>
         </Kobalte.Content>
       </Kobalte.Portal>
       </Kobalte.Portal>
@@ -87,10 +90,11 @@ export const ModelSelectorPopover: Component<{
 
 
 export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
 export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
   const dialog = useDialog()
   const dialog = useDialog()
+  const language = useLanguage()
 
 
   return (
   return (
     <Dialog
     <Dialog
-      title="Select model"
+      title={language.t("dialog.model.select.title")}
       action={
       action={
         <Button
         <Button
           class="h-7 -my-1 text-14-medium"
           class="h-7 -my-1 text-14-medium"
@@ -98,7 +102,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
           tabIndex={-1}
           tabIndex={-1}
           onClick={() => dialog.show(() => <DialogSelectProvider />)}
           onClick={() => dialog.show(() => <DialogSelectProvider />)}
         >
         >
-          Connect provider
+          {language.t("command.provider.connect")}
         </Button>
         </Button>
       }
       }
     >
     >
@@ -108,7 +112,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
         class="ml-3 mt-5 mb-6 text-text-base self-start"
         class="ml-3 mt-5 mb-6 text-text-base self-start"
         onClick={() => dialog.show(() => <DialogManageModels />)}
         onClick={() => dialog.show(() => <DialogManageModels />)}
       >
       >
-        Manage models
+        {language.t("dialog.model.manage")}
       </Button>
       </Button>
     </Dialog>
     </Dialog>
   )
   )

+ 18 - 8
packages/app/src/components/dialog-select-provider.tsx

@@ -7,28 +7,38 @@ import { Tag } from "@opencode-ai/ui/tag"
 import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { IconName } from "@opencode-ai/ui/icons/provider"
 import { IconName } from "@opencode-ai/ui/icons/provider"
 import { DialogConnectProvider } from "./dialog-connect-provider"
 import { DialogConnectProvider } from "./dialog-connect-provider"
+import { useLanguage } from "@/context/language"
 
 
 export const DialogSelectProvider: Component = () => {
 export const DialogSelectProvider: Component = () => {
   const dialog = useDialog()
   const dialog = useDialog()
   const providers = useProviders()
   const providers = useProviders()
+  const language = useLanguage()
+
+  const popularGroup = () => language.t("dialog.provider.group.popular")
+  const otherGroup = () => language.t("dialog.provider.group.other")
 
 
   return (
   return (
-    <Dialog title="Connect provider">
+    <Dialog title={language.t("command.provider.connect")}>
       <List
       <List
-        search={{ placeholder: "Search providers", autofocus: true }}
+        search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
+        emptyMessage={language.t("dialog.provider.empty")}
         activeIcon="plus-small"
         activeIcon="plus-small"
         key={(x) => x?.id}
         key={(x) => x?.id}
-        items={providers.all}
+        items={() => {
+          language.locale()
+          return providers.all()
+        }}
         filterKeys={["id", "name"]}
         filterKeys={["id", "name"]}
-        groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
+        groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
         sortBy={(a, b) => {
         sortBy={(a, b) => {
           if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
           if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
             return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
             return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
           return a.name.localeCompare(b.name)
           return a.name.localeCompare(b.name)
         }}
         }}
         sortGroupsBy={(a, b) => {
         sortGroupsBy={(a, b) => {
-          if (a.category === "Popular" && b.category !== "Popular") return -1
-          if (b.category === "Popular" && a.category !== "Popular") return 1
+          const popular = popularGroup()
+          if (a.category === popular && b.category !== popular) return -1
+          if (b.category === popular && a.category !== popular) return 1
           return 0
           return 0
         }}
         }}
         onSelect={(x) => {
         onSelect={(x) => {
@@ -41,10 +51,10 @@ export const DialogSelectProvider: Component = () => {
             <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
             <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
             <span>{i.name}</span>
             <span>{i.name}</span>
             <Show when={i.id === "opencode"}>
             <Show when={i.id === "opencode"}>
-              <Tag>Recommended</Tag>
+              <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
             </Show>
             </Show>
             <Show when={i.id === "anthropic"}>
             <Show when={i.id === "anthropic"}>
-              <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
+              <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
             </Show>
             </Show>
           </div>
           </div>
         )}
         )}

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

@@ -10,6 +10,7 @@ import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/serv
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { useNavigate } from "@solidjs/router"
 import { useNavigate } from "@solidjs/router"
+import { useLanguage } from "@/context/language"
 
 
 type ServerStatus = { healthy: boolean; version?: string }
 type ServerStatus = { healthy: boolean; version?: string }
 
 
@@ -30,6 +31,7 @@ export function DialogSelectServer() {
   const dialog = useDialog()
   const dialog = useDialog()
   const server = useServer()
   const server = useServer()
   const platform = usePlatform()
   const platform = usePlatform()
+  const language = useLanguage()
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
     url: "",
     url: "",
     adding: false,
     adding: false,
@@ -109,7 +111,7 @@ export function DialogSelectServer() {
     setStore("adding", false)
     setStore("adding", false)
 
 
     if (!result.healthy) {
     if (!result.healthy) {
-      setStore("error", "Could not connect to server")
+      setStore("error", language.t("dialog.server.add.error"))
       return
       return
     }
     }
 
 
@@ -122,11 +124,11 @@ export function DialogSelectServer() {
   }
   }
 
 
   return (
   return (
-    <Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
+    <Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}>
       <div class="flex flex-col gap-4 pb-4">
       <div class="flex flex-col gap-4 pb-4">
         <List
         <List
-          search={{ placeholder: "Search servers", autofocus: true }}
-          emptyMessage="No servers yet"
+          search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
+          emptyMessage={language.t("dialog.server.empty")}
           items={sortedItems}
           items={sortedItems}
           key={(x) => x}
           key={(x) => x}
           current={current()}
           current={current()}
@@ -168,14 +170,14 @@ export function DialogSelectServer() {
 
 
         <div class="mt-6 px-3 flex flex-col gap-1.5">
         <div class="mt-6 px-3 flex flex-col gap-1.5">
           <div class="px-3">
           <div class="px-3">
-            <h3 class="text-14-regular text-text-weak">Add a server</h3>
+            <h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3>
           </div>
           </div>
           <form onSubmit={handleSubmit}>
           <form onSubmit={handleSubmit}>
             <div class="flex items-start gap-2">
             <div class="flex items-start gap-2">
               <div class="flex-1 min-w-0 h-auto">
               <div class="flex-1 min-w-0 h-auto">
                 <TextField
                 <TextField
                   type="text"
                   type="text"
-                  label="Server URL"
+                  label={language.t("dialog.server.add.url")}
                   hideLabel
                   hideLabel
                   placeholder="http://localhost:4096"
                   placeholder="http://localhost:4096"
                   value={store.url}
                   value={store.url}
@@ -188,7 +190,7 @@ export function DialogSelectServer() {
                 />
                 />
               </div>
               </div>
               <Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
               <Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
-                {store.adding ? "Checking..." : "Add"}
+                {store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
               </Button>
               </Button>
             </div>
             </div>
           </form>
           </form>
@@ -197,9 +199,9 @@ export function DialogSelectServer() {
         <Show when={isDesktop}>
         <Show when={isDesktop}>
           <div class="mt-6 px-3 flex flex-col gap-1.5">
           <div class="mt-6 px-3 flex flex-col gap-1.5">
             <div class="px-3">
             <div class="px-3">
-              <h3 class="text-14-regular text-text-weak">Default server</h3>
+              <h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
               <p class="text-12-regular text-text-weak mt-1">
               <p class="text-12-regular text-text-weak mt-1">
-                Connect to this server on app launch instead of starting a local server. Requires restart.
+                {language.t("dialog.server.default.description")}
               </p>
               </p>
             </div>
             </div>
             <div class="flex items-center gap-2 px-3 py-2">
             <div class="flex items-center gap-2 px-3 py-2">
@@ -208,7 +210,7 @@ export function DialogSelectServer() {
                 fallback={
                 fallback={
                   <Show
                   <Show
                     when={server.url}
                     when={server.url}
-                    fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
+                    fallback={<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>}
                   >
                   >
                     <Button
                     <Button
                       variant="secondary"
                       variant="secondary"
@@ -218,7 +220,7 @@ export function DialogSelectServer() {
                         defaultUrlActions.refetch(server.url)
                         defaultUrlActions.refetch(server.url)
                       }}
                       }}
                     >
                     >
-                      Set current server as default
+                      {language.t("dialog.server.default.set")}
                     </Button>
                     </Button>
                   </Show>
                   </Show>
                 }
                 }
@@ -234,7 +236,7 @@ export function DialogSelectServer() {
                     defaultUrlActions.refetch()
                     defaultUrlActions.refetch()
                   }}
                   }}
                 >
                 >
-                  Clear
+                  {language.t("dialog.server.default.clear")}
                 </Button>
                 </Button>
               </Show>
               </Show>
             </div>
             </div>

+ 26 - 12
packages/app/src/components/prompt-input.tsx

@@ -49,6 +49,7 @@ import { Persist, persisted } from "@/utils/persist"
 import { Identifier } from "@/utils/id"
 import { Identifier } from "@/utils/id"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { usePermission } from "@/context/permission"
 import { usePermission } from "@/context/permission"
+import { useLanguage } from "@/context/language"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
 import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
 import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
@@ -118,6 +119,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const providers = useProviders()
   const providers = useProviders()
   const command = useCommand()
   const command = useCommand()
   const permission = usePermission()
   const permission = usePermission()
+  const language = useLanguage()
   let editorRef!: HTMLDivElement
   let editorRef!: HTMLDivElement
   let fileInputRef!: HTMLInputElement
   let fileInputRef!: HTMLInputElement
   let scrollRef!: HTMLDivElement
   let scrollRef!: HTMLDivElement
@@ -1560,8 +1562,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           <Show when={!prompt.dirty()}>
           <Show when={!prompt.dirty()}>
             <div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
             <div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
               {store.mode === "shell"
               {store.mode === "shell"
-                ? "Enter shell command..."
-                : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
+                ? language.t("prompt.placeholder.shell")
+                : language.t("prompt.placeholder.normal", { example: PLACEHOLDERS[store.placeholder] })}
             </div>
             </div>
           </Show>
           </Show>
         </div>
         </div>
@@ -1571,12 +1573,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
               <Match when={store.mode === "shell"}>
               <Match when={store.mode === "shell"}>
                 <div class="flex items-center gap-2 px-2 h-6">
                 <div class="flex items-center gap-2 px-2 h-6">
                   <Icon name="console" size="small" class="text-icon-primary" />
                   <Icon name="console" size="small" class="text-icon-primary" />
-                  <span class="text-12-regular text-text-primary">Shell</span>
-                  <span class="text-12-regular text-text-weak">esc to exit</span>
+                  <span class="text-12-regular text-text-primary">{language.t("prompt.mode.shell")}</span>
+                  <span class="text-12-regular text-text-weak">{language.t("prompt.mode.shell.exit")}</span>
                 </div>
                 </div>
               </Match>
               </Match>
               <Match when={store.mode === "normal"}>
               <Match when={store.mode === "normal"}>
-                <TooltipKeybind placement="top" title="Cycle agent" keybind={command.keybind("agent.cycle")}>
+                <TooltipKeybind
+                  placement="top"
+                  title={language.t("command.agent.cycle")}
+                  keybind={command.keybind("agent.cycle")}
+                >
                   <Select
                   <Select
                     options={local.agent.list().map((agent) => agent.name)}
                     options={local.agent.list().map((agent) => agent.name)}
                     current={local.agent.current()?.name ?? ""}
                     current={local.agent.current()?.name ?? ""}
@@ -1588,24 +1594,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 <Show
                 <Show
                   when={providers.paid().length > 0}
                   when={providers.paid().length > 0}
                   fallback={
                   fallback={
-                    <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
+                    <TooltipKeybind
+                      placement="top"
+                      title={language.t("command.model.choose")}
+                      keybind={command.keybind("model.choose")}
+                    >
                       <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
                       <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
                         <Show when={local.model.current()?.provider?.id}>
                         <Show when={local.model.current()?.provider?.id}>
                           <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
                           <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
                         </Show>
                         </Show>
-                        {local.model.current()?.name ?? "Select model"}
+                        {local.model.current()?.name ?? language.t("dialog.model.select.title")}
                         <Icon name="chevron-down" size="small" />
                         <Icon name="chevron-down" size="small" />
                       </Button>
                       </Button>
                     </TooltipKeybind>
                     </TooltipKeybind>
                   }
                   }
                 >
                 >
                   <ModelSelectorPopover>
                   <ModelSelectorPopover>
-                    <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
+                    <TooltipKeybind
+                      placement="top"
+                      title={language.t("command.model.choose")}
+                      keybind={command.keybind("model.choose")}
+                    >
                       <Button as="div" variant="ghost">
                       <Button as="div" variant="ghost">
                         <Show when={local.model.current()?.provider?.id}>
                         <Show when={local.model.current()?.provider?.id}>
                           <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
                           <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
                         </Show>
                         </Show>
-                        {local.model.current()?.name ?? "Select model"}
+                        {local.model.current()?.name ?? language.t("dialog.model.select.title")}
                         <Icon name="chevron-down" size="small" />
                         <Icon name="chevron-down" size="small" />
                       </Button>
                       </Button>
                     </TooltipKeybind>
                     </TooltipKeybind>
@@ -1614,7 +1628,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 <Show when={local.model.variant.list().length > 0}>
                 <Show when={local.model.variant.list().length > 0}>
                   <TooltipKeybind
                   <TooltipKeybind
                     placement="top"
                     placement="top"
-                    title="Thinking effort"
+                    title={language.t("command.model.variant.cycle")}
                     keybind={command.keybind("model.variant.cycle")}
                     keybind={command.keybind("model.variant.cycle")}
                   >
                   >
                     <Button
                     <Button
@@ -1622,14 +1636,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                       class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
                       class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
                       onClick={() => local.model.variant.cycle()}
                       onClick={() => local.model.variant.cycle()}
                     >
                     >
-                      {local.model.variant.current() ?? "Default"}
+                      {local.model.variant.current() ?? language.t("common.default")}
                     </Button>
                     </Button>
                   </TooltipKeybind>
                   </TooltipKeybind>
                 </Show>
                 </Show>
                 <Show when={permission.permissionsEnabled() && params.id}>
                 <Show when={permission.permissionsEnabled() && params.id}>
                   <TooltipKeybind
                   <TooltipKeybind
                     placement="top"
                     placement="top"
-                    title="Auto-accept edits"
+                    title={language.t("command.permissions.autoaccept.enable")}
                     keybind={command.keybind("permissions.autoaccept")}
                     keybind={command.keybind("permissions.autoaccept")}
                   >
                   >
                     <Button
                     <Button

+ 30 - 25
packages/app/src/components/session/session-context-tab.tsx

@@ -11,6 +11,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
 import { Code } from "@opencode-ai/ui/code"
 import { Code } from "@opencode-ai/ui/code"
 import { Markdown } from "@opencode-ai/ui/markdown"
 import { Markdown } from "@opencode-ai/ui/markdown"
 import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
 import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
+import { useLanguage } from "@/context/language"
 
 
 interface SessionContextTabProps {
 interface SessionContextTabProps {
   messages: () => Message[]
   messages: () => Message[]
@@ -22,6 +23,7 @@ interface SessionContextTabProps {
 export function SessionContextTab(props: SessionContextTabProps) {
 export function SessionContextTab(props: SessionContextTabProps) {
   const params = useParams()
   const params = useParams()
   const sync = useSync()
   const sync = useSync()
+  const language = useLanguage()
 
 
   const ctx = createMemo(() => {
   const ctx = createMemo(() => {
     const last = props.messages().findLast((x) => {
     const last = props.messages().findLast((x) => {
@@ -172,7 +174,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
           return [
           return [
             {
             {
               key: "system",
               key: "system",
-              label: "System",
+              label: language.t("context.breakdown.system"),
               tokens: tokens.system,
               tokens: tokens.system,
               width: pct(tokens.system),
               width: pct(tokens.system),
               percent: pctLabel(tokens.system),
               percent: pctLabel(tokens.system),
@@ -180,7 +182,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
             },
             },
             {
             {
               key: "user",
               key: "user",
-              label: "User",
+              label: language.t("context.breakdown.user"),
               tokens: tokens.user,
               tokens: tokens.user,
               width: pct(tokens.user),
               width: pct(tokens.user),
               percent: pctLabel(tokens.user),
               percent: pctLabel(tokens.user),
@@ -188,7 +190,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
             },
             },
             {
             {
               key: "assistant",
               key: "assistant",
-              label: "Assistant",
+              label: language.t("context.breakdown.assistant"),
               tokens: tokens.assistant,
               tokens: tokens.assistant,
               width: pct(tokens.assistant),
               width: pct(tokens.assistant),
               percent: pctLabel(tokens.assistant),
               percent: pctLabel(tokens.assistant),
@@ -196,7 +198,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
             },
             },
             {
             {
               key: "tool",
               key: "tool",
-              label: "Tool Calls",
+              label: language.t("context.breakdown.tool"),
               tokens: tokens.tool,
               tokens: tokens.tool,
               width: pct(tokens.tool),
               width: pct(tokens.tool),
               percent: pctLabel(tokens.tool),
               percent: pctLabel(tokens.tool),
@@ -204,7 +206,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
             },
             },
             {
             {
               key: "other",
               key: "other",
-              label: "Other",
+              label: language.t("context.breakdown.other"),
               tokens: tokens.other,
               tokens: tokens.other,
               width: pct(tokens.other),
               width: pct(tokens.other),
               percent: pctLabel(tokens.other),
               percent: pctLabel(tokens.other),
@@ -243,22 +245,25 @@ export function SessionContextTab(props: SessionContextTabProps) {
     const c = ctx()
     const c = ctx()
     const count = counts()
     const count = counts()
     return [
     return [
-      { label: "Session", value: props.info()?.title ?? params.id ?? "—" },
-      { label: "Messages", value: count.all.toLocaleString() },
-      { label: "Provider", value: providerLabel() },
-      { label: "Model", value: modelLabel() },
-      { label: "Context Limit", value: number(c?.limit) },
-      { label: "Total Tokens", value: number(c?.total) },
-      { label: "Usage", value: percent(c?.usage) },
-      { label: "Input Tokens", value: number(c?.input) },
-      { label: "Output Tokens", value: number(c?.output) },
-      { label: "Reasoning Tokens", value: number(c?.reasoning) },
-      { label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` },
-      { label: "User Messages", value: count.user.toLocaleString() },
-      { label: "Assistant Messages", value: count.assistant.toLocaleString() },
-      { label: "Total Cost", value: cost() },
-      { label: "Session Created", value: time(props.info()?.time.created) },
-      { label: "Last Activity", value: time(c?.message.time.created) },
+      { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
+      { label: language.t("context.stats.messages"), value: count.all.toLocaleString() },
+      { label: language.t("context.stats.provider"), value: providerLabel() },
+      { label: language.t("context.stats.model"), value: modelLabel() },
+      { label: language.t("context.stats.limit"), value: number(c?.limit) },
+      { label: language.t("context.stats.totalTokens"), value: number(c?.total) },
+      { label: language.t("context.stats.usage"), value: percent(c?.usage) },
+      { label: language.t("context.stats.inputTokens"), value: number(c?.input) },
+      { label: language.t("context.stats.outputTokens"), value: number(c?.output) },
+      { label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
+      {
+        label: language.t("context.stats.cacheTokens"),
+        value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
+      },
+      { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString() },
+      { label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString() },
+      { label: language.t("context.stats.totalCost"), value: cost() },
+      { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
+      { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
     ] satisfies { label: string; value: JSX.Element }[]
     ] satisfies { label: string; value: JSX.Element }[]
   })
   })
 
 
@@ -371,7 +376,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
 
 
         <Show when={breakdown().length > 0}>
         <Show when={breakdown().length > 0}>
           <div class="flex flex-col gap-2">
           <div class="flex flex-col gap-2">
-            <div class="text-12-regular text-text-weak">Context Breakdown</div>
+            <div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div>
             <div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
             <div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
               <For each={breakdown()}>
               <For each={breakdown()}>
                 {(segment) => (
                 {(segment) => (
@@ -397,7 +402,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
               </For>
               </For>
             </div>
             </div>
             <div class="hidden text-11-regular text-text-weaker">
             <div class="hidden text-11-regular text-text-weaker">
-              Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.
+              {language.t("context.breakdown.note")}
             </div>
             </div>
           </div>
           </div>
         </Show>
         </Show>
@@ -405,7 +410,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
         <Show when={systemPrompt()}>
         <Show when={systemPrompt()}>
           {(prompt) => (
           {(prompt) => (
             <div class="flex flex-col gap-2">
             <div class="flex flex-col gap-2">
-              <div class="text-12-regular text-text-weak">System Prompt</div>
+              <div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div>
               <div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
               <div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
                 <Markdown text={prompt()} class="text-12-regular" />
                 <Markdown text={prompt()} class="text-12-regular" />
               </div>
               </div>
@@ -414,7 +419,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
         </Show>
         </Show>
 
 
         <div class="flex flex-col gap-2">
         <div class="flex flex-col gap-2">
-          <div class="text-12-regular text-text-weak">Raw messages</div>
+          <div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
           <Accordion multiple>
           <Accordion multiple>
             <For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
             <For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
           </Accordion>
           </Accordion>

+ 3 - 1
packages/app/src/context/command.tsx

@@ -2,6 +2,7 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount, type Access
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useLanguage } from "@/context/language"
 import { useSettings } from "@/context/settings"
 import { useSettings } from "@/context/settings"
 import { Persist, persisted } from "@/utils/persist"
 import { Persist, persisted } from "@/utils/persist"
 
 
@@ -154,6 +155,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
   init: () => {
   init: () => {
     const dialog = useDialog()
     const dialog = useDialog()
     const settings = useSettings()
     const settings = useSettings()
+    const language = useLanguage()
     const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
     const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
     const [suspendCount, setSuspendCount] = createSignal(0)
     const [suspendCount, setSuspendCount] = createSignal(0)
 
 
@@ -213,7 +215,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
         ...suggested.map((x) => ({
         ...suggested.map((x) => ({
           ...x,
           ...x,
           id: SUGGESTED_PREFIX + x.id,
           id: SUGGESTED_PREFIX + x.id,
-          category: "Suggested",
+          category: language.t("command.category.suggested"),
         })),
         })),
         ...resolved,
         ...resolved,
       ]
       ]

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

@@ -1,9 +1,205 @@
 export const dict = {
 export const dict = {
+  "command.category.suggested": "Suggested",
+  "command.category.view": "View",
+  "command.category.project": "Project",
+  "command.category.provider": "Provider",
+  "command.category.server": "Server",
+  "command.category.session": "Session",
+  "command.category.theme": "Theme",
   "command.category.language": "Language",
   "command.category.language": "Language",
+  "command.category.file": "File",
+  "command.category.terminal": "Terminal",
+  "command.category.model": "Model",
+  "command.category.mcp": "MCP",
+  "command.category.agent": "Agent",
+  "command.category.permissions": "Permissions",
+
+  "theme.scheme.system": "System",
+  "theme.scheme.light": "Light",
+  "theme.scheme.dark": "Dark",
+
+  "command.sidebar.toggle": "Toggle sidebar",
+  "command.project.open": "Open project",
+  "command.provider.connect": "Connect provider",
+  "command.server.switch": "Switch server",
+  "command.session.previous": "Previous session",
+  "command.session.next": "Next session",
+  "command.session.archive": "Archive session",
+
+  "command.theme.cycle": "Cycle theme",
+  "command.theme.set": "Use theme: {{theme}}",
+  "command.theme.scheme.cycle": "Cycle color scheme",
+  "command.theme.scheme.set": "Use color scheme: {{scheme}}",
+
   "command.language.cycle": "Cycle language",
   "command.language.cycle": "Cycle language",
   "command.language.set": "Use language: {{language}}",
   "command.language.set": "Use language: {{language}}",
+
+  "command.session.new": "New session",
+  "command.file.open": "Open file",
+  "command.file.open.description": "Search files and commands",
+  "command.terminal.toggle": "Toggle terminal",
+  "command.review.toggle": "Toggle review",
+  "command.terminal.new": "New terminal",
+  "command.terminal.new.description": "Create a new terminal tab",
+  "command.steps.toggle": "Toggle steps",
+  "command.steps.toggle.description": "Show or hide steps for the current message",
+  "command.message.previous": "Previous message",
+  "command.message.previous.description": "Go to the previous user message",
+  "command.message.next": "Next message",
+  "command.message.next.description": "Go to the next user message",
+  "command.model.choose": "Choose model",
+  "command.model.choose.description": "Select a different model",
+  "command.mcp.toggle": "Toggle MCPs",
+  "command.mcp.toggle.description": "Toggle MCPs",
+  "command.agent.cycle": "Cycle agent",
+  "command.agent.cycle.description": "Switch to the next agent",
+  "command.agent.cycle.reverse": "Cycle agent backwards",
+  "command.agent.cycle.reverse.description": "Switch to the previous agent",
+  "command.model.variant.cycle": "Cycle thinking effort",
+  "command.model.variant.cycle.description": "Switch to the next effort level",
+  "command.permissions.autoaccept.enable": "Auto-accept edits",
+  "command.permissions.autoaccept.disable": "Stop auto-accepting edits",
+  "command.session.undo": "Undo",
+  "command.session.undo.description": "Undo the last message",
+  "command.session.redo": "Redo",
+  "command.session.redo.description": "Redo the last undone message",
+  "command.session.compact": "Compact session",
+  "command.session.compact.description": "Summarize the session to reduce context size",
+  "command.session.fork": "Fork from message",
+  "command.session.fork.description": "Create a new session from a previous message",
+  "command.session.share": "Share session",
+  "command.session.share.description": "Share this session and copy the URL to clipboard",
+  "command.session.unshare": "Unshare session",
+  "command.session.unshare.description": "Stop sharing this session",
+
+  "palette.search.placeholder": "Search files and commands",
+  "palette.empty": "No results found",
+  "palette.group.commands": "Commands",
+  "palette.group.files": "Files",
+
+  "dialog.provider.search.placeholder": "Search providers",
+  "dialog.provider.empty": "No providers found",
+  "dialog.provider.group.popular": "Popular",
+  "dialog.provider.group.other": "Other",
+  "dialog.provider.tag.recommended": "Recommended",
+  "dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
+
+  "dialog.model.select.title": "Select model",
+  "dialog.model.search.placeholder": "Search models",
+  "dialog.model.empty": "No model results",
+  "dialog.model.manage": "Manage models",
+  "dialog.model.manage.description": "Customize which models appear in the model selector.",
+
+  "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
+  "dialog.model.unpaid.addMore.title": "Add more models from popular providers",
+
+  "dialog.provider.viewAll": "View all providers",
+
+  "model.tag.free": "Free",
+  "model.tag.latest": "Latest",
+
+  "common.search.placeholder": "Search",
+  "common.loading": "Loading",
+  "common.cancel": "Cancel",
+  "common.save": "Save",
+  "common.saving": "Saving...",
+  "common.default": "Default",
+
+  "prompt.placeholder.shell": "Enter shell command...",
+  "prompt.placeholder.normal": "Ask anything... \"{{example}}\"",
+  "prompt.mode.shell": "Shell",
+  "prompt.mode.shell.exit": "esc to exit",
+
+  "dialog.mcp.title": "MCPs",
+  "dialog.mcp.description": "{{enabled}} of {{total}} enabled",
+  "dialog.mcp.empty": "No MCPs configured",
+
+  "mcp.status.connected": "connected",
+  "mcp.status.failed": "failed",
+  "mcp.status.needs_auth": "needs auth",
+  "mcp.status.disabled": "disabled",
+
+  "dialog.fork.empty": "No messages to fork from",
+
+  "dialog.directory.search.placeholder": "Search folders",
+  "dialog.directory.empty": "No folders found",
+
+  "dialog.server.title": "Servers",
+  "dialog.server.description": "Switch which OpenCode server this app connects to.",
+  "dialog.server.search.placeholder": "Search servers",
+  "dialog.server.empty": "No servers yet",
+  "dialog.server.add.title": "Add a server",
+  "dialog.server.add.url": "Server URL",
+  "dialog.server.add.error": "Could not connect to server",
+  "dialog.server.add.checking": "Checking...",
+  "dialog.server.add.button": "Add",
+  "dialog.server.default.title": "Default server",
+  "dialog.server.default.description": "Connect to this server on app launch instead of starting a local server. Requires restart.",
+  "dialog.server.default.none": "No server selected",
+  "dialog.server.default.set": "Set current server as default",
+  "dialog.server.default.clear": "Clear",
+
+  "dialog.project.edit.title": "Edit project",
+  "dialog.project.edit.name": "Name",
+  "dialog.project.edit.icon": "Icon",
+  "dialog.project.edit.icon.alt": "Project icon",
+  "dialog.project.edit.icon.hint": "Click or drag an image",
+  "dialog.project.edit.icon.recommended": "Recommended: 128x128px",
+  "dialog.project.edit.color": "Color",
+
+  "context.breakdown.title": "Context Breakdown",
+  "context.breakdown.note": "Approximate breakdown of input tokens. \"Other\" includes tool definitions and overhead.",
+  "context.breakdown.system": "System",
+  "context.breakdown.user": "User",
+  "context.breakdown.assistant": "Assistant",
+  "context.breakdown.tool": "Tool Calls",
+  "context.breakdown.other": "Other",
+
+  "context.systemPrompt.title": "System Prompt",
+  "context.rawMessages.title": "Raw messages",
+
+  "context.stats.session": "Session",
+  "context.stats.messages": "Messages",
+  "context.stats.provider": "Provider",
+  "context.stats.model": "Model",
+  "context.stats.limit": "Context Limit",
+  "context.stats.totalTokens": "Total Tokens",
+  "context.stats.usage": "Usage",
+  "context.stats.inputTokens": "Input Tokens",
+  "context.stats.outputTokens": "Output Tokens",
+  "context.stats.reasoningTokens": "Reasoning Tokens",
+  "context.stats.cacheTokens": "Cache Tokens (read/write)",
+  "context.stats.userMessages": "User Messages",
+  "context.stats.assistantMessages": "Assistant Messages",
+  "context.stats.totalCost": "Total Cost",
+  "context.stats.sessionCreated": "Session Created",
+  "context.stats.lastActivity": "Last Activity",
+
   "language.en": "English",
   "language.en": "English",
   "language.zh": "Chinese",
   "language.zh": "Chinese",
+
   "toast.language.title": "Language",
   "toast.language.title": "Language",
   "toast.language.description": "Switched to {{language}}",
   "toast.language.description": "Switched to {{language}}",
+
+  "toast.theme.title": "Theme switched",
+  "toast.scheme.title": "Color scheme",
+
+  "toast.permissions.autoaccept.on.title": "Auto-accepting edits",
+  "toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
+  "toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
+  "toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval",
+
+  "toast.model.none.title": "No model selected",
+  "toast.model.none.description": "Connect a provider to summarize this session",
+
+  "toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
+  "toast.session.share.success.title": "Session shared",
+  "toast.session.share.success.description": "Share URL copied to clipboard!",
+  "toast.session.share.failed.title": "Failed to share session",
+  "toast.session.share.failed.description": "An error occurred while sharing the session",
+
+  "toast.session.unshare.success.title": "Session unshared",
+  "toast.session.unshare.success.description": "Session unshared successfully!",
+  "toast.session.unshare.failed.title": "Failed to unshare session",
+  "toast.session.unshare.failed.description": "An error occurred while unsharing the session",
 }
 }

+ 203 - 7
packages/app/src/i18n/zh.ts

@@ -3,11 +3,207 @@ import { dict as en } from "./en"
 type Keys = keyof typeof en
 type Keys = keyof typeof en
 
 
 export const dict = {
 export const dict = {
-  "command.category.language": "\u8bed\u8a00",
-  "command.language.cycle": "\u5207\u6362\u8bed\u8a00",
-  "command.language.set": "\u4f7f\u7528\u8bed\u8a00: {{language}}",
-  "language.en": "\u82f1\u8bed",
-  "language.zh": "\u4e2d\u6587",
-  "toast.language.title": "\u8bed\u8a00",
-  "toast.language.description": "\u5df2\u5207\u6362\u5230{{language}}",
+  "command.category.suggested": "建议",
+  "command.category.view": "视图",
+  "command.category.project": "项目",
+  "command.category.provider": "提供商",
+  "command.category.server": "服务器",
+  "command.category.session": "会话",
+  "command.category.theme": "主题",
+  "command.category.language": "语言",
+  "command.category.file": "文件",
+  "command.category.terminal": "终端",
+  "command.category.model": "模型",
+  "command.category.mcp": "MCP",
+  "command.category.agent": "智能体",
+  "command.category.permissions": "权限",
+
+  "theme.scheme.system": "系统",
+  "theme.scheme.light": "浅色",
+  "theme.scheme.dark": "深色",
+
+  "command.sidebar.toggle": "切换侧边栏",
+  "command.project.open": "打开项目",
+  "command.provider.connect": "连接提供商",
+  "command.server.switch": "切换服务器",
+  "command.session.previous": "上一个会话",
+  "command.session.next": "下一个会话",
+  "command.session.archive": "归档会话",
+
+  "command.theme.cycle": "切换主题",
+  "command.theme.set": "使用主题: {{theme}}",
+  "command.theme.scheme.cycle": "切换配色方案",
+  "command.theme.scheme.set": "使用配色方案: {{scheme}}",
+
+  "command.language.cycle": "切换语言",
+  "command.language.set": "使用语言: {{language}}",
+
+  "command.session.new": "新建会话",
+  "command.file.open": "打开文件",
+  "command.file.open.description": "搜索文件和命令",
+  "command.terminal.toggle": "切换终端",
+  "command.review.toggle": "切换审查",
+  "command.terminal.new": "新建终端",
+  "command.terminal.new.description": "创建新的终端标签页",
+  "command.steps.toggle": "切换步骤",
+  "command.steps.toggle.description": "显示或隐藏当前消息的步骤",
+  "command.message.previous": "上一条消息",
+  "command.message.previous.description": "跳转到上一条用户消息",
+  "command.message.next": "下一条消息",
+  "command.message.next.description": "跳转到下一条用户消息",
+  "command.model.choose": "选择模型",
+  "command.model.choose.description": "选择不同的模型",
+  "command.mcp.toggle": "切换 MCPs",
+  "command.mcp.toggle.description": "切换 MCPs",
+  "command.agent.cycle": "切换智能体",
+  "command.agent.cycle.description": "切换到下一个智能体",
+  "command.agent.cycle.reverse": "反向切换智能体",
+  "command.agent.cycle.reverse.description": "切换到上一个智能体",
+  "command.model.variant.cycle": "切换思考强度",
+  "command.model.variant.cycle.description": "切换到下一个强度等级",
+  "command.permissions.autoaccept.enable": "自动接受编辑",
+  "command.permissions.autoaccept.disable": "停止自动接受编辑",
+  "command.session.undo": "撤销",
+  "command.session.undo.description": "撤销上一条消息",
+  "command.session.redo": "重做",
+  "command.session.redo.description": "重做上一条撤销的消息",
+  "command.session.compact": "精简会话",
+  "command.session.compact.description": "总结会话以减少上下文大小",
+  "command.session.fork": "从消息分叉",
+  "command.session.fork.description": "从之前的消息创建新会话",
+  "command.session.share": "分享会话",
+  "command.session.share.description": "分享此会话并将链接复制到剪贴板",
+  "command.session.unshare": "取消分享会话",
+  "command.session.unshare.description": "停止分享此会话",
+
+  "palette.search.placeholder": "搜索文件和命令",
+  "palette.empty": "未找到结果",
+  "palette.group.commands": "命令",
+  "palette.group.files": "文件",
+
+  "dialog.provider.search.placeholder": "搜索提供商",
+  "dialog.provider.empty": "未找到提供商",
+  "dialog.provider.group.popular": "热门",
+  "dialog.provider.group.other": "其他",
+  "dialog.provider.tag.recommended": "推荐",
+  "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
+
+  "dialog.model.select.title": "选择模型",
+  "dialog.model.search.placeholder": "搜索模型",
+  "dialog.model.empty": "未找到模型",
+  "dialog.model.manage": "管理模型",
+  "dialog.model.manage.description": "自定义模型选择器中显示的模型。",
+
+  "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
+  "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
+
+  "dialog.provider.viewAll": "查看全部提供商",
+
+  "model.tag.free": "免费",
+  "model.tag.latest": "最新",
+
+  "common.search.placeholder": "搜索",
+  "common.loading": "加载中",
+  "common.cancel": "取消",
+  "common.save": "保存",
+  "common.saving": "保存中...",
+  "common.default": "默认",
+
+  "prompt.placeholder.shell": "输入 shell 命令...",
+  "prompt.placeholder.normal": "随便问点什么... \"{{example}}\"",
+  "prompt.mode.shell": "Shell",
+  "prompt.mode.shell.exit": "按 esc 退出",
+
+  "dialog.mcp.title": "MCPs",
+  "dialog.mcp.description": "已启用 {{enabled}} / {{total}}",
+  "dialog.mcp.empty": "未配置 MCPs",
+
+  "mcp.status.connected": "已连接",
+  "mcp.status.failed": "失败",
+  "mcp.status.needs_auth": "需要授权",
+  "mcp.status.disabled": "已禁用",
+
+  "dialog.fork.empty": "没有可用于分叉的消息",
+
+  "dialog.directory.search.placeholder": "搜索文件夹",
+  "dialog.directory.empty": "未找到文件夹",
+
+  "dialog.server.title": "服务器",
+  "dialog.server.description": "切换此应用连接的 OpenCode 服务器。",
+  "dialog.server.search.placeholder": "搜索服务器",
+  "dialog.server.empty": "暂无服务器",
+  "dialog.server.add.title": "添加服务器",
+  "dialog.server.add.url": "服务器 URL",
+  "dialog.server.add.error": "无法连接到服务器",
+  "dialog.server.add.checking": "检查中...",
+  "dialog.server.add.button": "添加",
+  "dialog.server.default.title": "默认服务器",
+  "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
+  "dialog.server.default.none": "未选择服务器",
+  "dialog.server.default.set": "将当前服务器设为默认",
+  "dialog.server.default.clear": "清除",
+
+  "dialog.project.edit.title": "编辑项目",
+  "dialog.project.edit.name": "名称",
+  "dialog.project.edit.icon": "图标",
+  "dialog.project.edit.icon.alt": "项目图标",
+  "dialog.project.edit.icon.hint": "点击或拖拽图片",
+  "dialog.project.edit.icon.recommended": "建议:128x128px",
+  "dialog.project.edit.color": "颜色",
+
+  "context.breakdown.title": "上下文拆分",
+  "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
+  "context.breakdown.system": "系统",
+  "context.breakdown.user": "用户",
+  "context.breakdown.assistant": "助手",
+  "context.breakdown.tool": "工具调用",
+  "context.breakdown.other": "其他",
+
+  "context.systemPrompt.title": "系统提示词",
+  "context.rawMessages.title": "原始消息",
+
+  "context.stats.session": "会话",
+  "context.stats.messages": "消息数",
+  "context.stats.provider": "提供商",
+  "context.stats.model": "模型",
+  "context.stats.limit": "上下文限制",
+  "context.stats.totalTokens": "总 token",
+  "context.stats.usage": "使用率",
+  "context.stats.inputTokens": "输入 token",
+  "context.stats.outputTokens": "输出 token",
+  "context.stats.reasoningTokens": "推理 token",
+  "context.stats.cacheTokens": "缓存 token(读/写)",
+  "context.stats.userMessages": "用户消息",
+  "context.stats.assistantMessages": "助手消息",
+  "context.stats.totalCost": "总成本",
+  "context.stats.sessionCreated": "创建时间",
+  "context.stats.lastActivity": "最后活动",
+
+  "language.en": "英语",
+  "language.zh": "中文",
+
+  "toast.language.title": "语言",
+  "toast.language.description": "已切换到{{language}}",
+
+  "toast.theme.title": "主题已切换",
+  "toast.scheme.title": "配色方案",
+
+  "toast.permissions.autoaccept.on.title": "自动接受编辑",
+  "toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
+  "toast.permissions.autoaccept.off.title": "已停止自动接受编辑",
+  "toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准",
+
+  "toast.model.none.title": "未选择模型",
+  "toast.model.none.description": "请先连接提供商以总结此会话",
+
+  "toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
+  "toast.session.share.success.title": "会话已分享",
+  "toast.session.share.success.description": "分享链接已复制到剪贴板",
+  "toast.session.share.failed.title": "分享会话失败",
+  "toast.session.share.failed.description": "分享会话时发生错误",
+
+  "toast.session.unshare.success.title": "已取消分享会话",
+  "toast.session.unshare.success.description": "会话已成功取消分享",
+  "toast.session.unshare.failed.title": "取消分享失败",
+  "toast.session.unshare.failed.description": "取消分享会话时发生错误",
 } satisfies Partial<Record<Keys, string>>
 } satisfies Partial<Record<Keys, string>>

+ 30 - 29
packages/app/src/pages/layout.tsx

@@ -114,11 +114,12 @@ export default function Layout(props: ParentProps) {
   const initialDir = params.dir
   const initialDir = params.dir
   const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
   const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
   const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
   const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
-  const colorSchemeLabel: Record<ColorScheme, string> = {
-    system: "System",
-    light: "Light",
-    dark: "Dark",
+  const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
+    system: "theme.scheme.system",
+    light: "theme.scheme.light",
+    dark: "theme.scheme.dark",
   }
   }
+  const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
 
 
   const [editor, setEditor] = createStore({
   const [editor, setEditor] = createStore({
     active: "" as string,
     active: "" as string,
@@ -252,7 +253,7 @@ export default function Layout(props: ParentProps) {
     theme.setTheme(nextThemeId)
     theme.setTheme(nextThemeId)
     const nextTheme = theme.themes()[nextThemeId]
     const nextTheme = theme.themes()[nextThemeId]
     showToast({
     showToast({
-      title: "Theme switched",
+      title: language.t("toast.theme.title"),
       description: nextTheme?.name ?? nextThemeId,
       description: nextTheme?.name ?? nextThemeId,
     })
     })
   }
   }
@@ -265,8 +266,8 @@ export default function Layout(props: ParentProps) {
     const next = colorSchemeOrder[nextIndex]
     const next = colorSchemeOrder[nextIndex]
     theme.setColorScheme(next)
     theme.setColorScheme(next)
     showToast({
     showToast({
-      title: "Color scheme",
-      description: colorSchemeLabel[next],
+      title: language.t("toast.scheme.title"),
+      description: colorSchemeLabel(next),
     })
     })
   }
   }
 
 
@@ -827,28 +828,28 @@ export default function Layout(props: ParentProps) {
     const commands: CommandOption[] = [
     const commands: CommandOption[] = [
       {
       {
         id: "sidebar.toggle",
         id: "sidebar.toggle",
-        title: "Toggle sidebar",
-        category: "View",
+        title: language.t("command.sidebar.toggle"),
+        category: language.t("command.category.view"),
         keybind: "mod+b",
         keybind: "mod+b",
         onSelect: () => layout.sidebar.toggle(),
         onSelect: () => layout.sidebar.toggle(),
       },
       },
       {
       {
         id: "project.open",
         id: "project.open",
-        title: "Open project",
-        category: "Project",
+        title: language.t("command.project.open"),
+        category: language.t("command.category.project"),
         keybind: "mod+o",
         keybind: "mod+o",
         onSelect: () => chooseProject(),
         onSelect: () => chooseProject(),
       },
       },
       {
       {
         id: "provider.connect",
         id: "provider.connect",
-        title: "Connect provider",
-        category: "Provider",
+        title: language.t("command.provider.connect"),
+        category: language.t("command.category.provider"),
         onSelect: () => connectProvider(),
         onSelect: () => connectProvider(),
       },
       },
       {
       {
         id: "server.switch",
         id: "server.switch",
-        title: "Switch server",
-        category: "Server",
+        title: language.t("command.server.switch"),
+        category: language.t("command.category.server"),
         onSelect: () => openServer(),
         onSelect: () => openServer(),
       },
       },
       {
       {
@@ -860,22 +861,22 @@ export default function Layout(props: ParentProps) {
       },
       },
       {
       {
         id: "session.previous",
         id: "session.previous",
-        title: "Previous session",
-        category: "Session",
+        title: language.t("command.session.previous"),
+        category: language.t("command.category.session"),
         keybind: "alt+arrowup",
         keybind: "alt+arrowup",
         onSelect: () => navigateSessionByOffset(-1),
         onSelect: () => navigateSessionByOffset(-1),
       },
       },
       {
       {
         id: "session.next",
         id: "session.next",
-        title: "Next session",
-        category: "Session",
+        title: language.t("command.session.next"),
+        category: language.t("command.category.session"),
         keybind: "alt+arrowdown",
         keybind: "alt+arrowdown",
         onSelect: () => navigateSessionByOffset(1),
         onSelect: () => navigateSessionByOffset(1),
       },
       },
       {
       {
         id: "session.archive",
         id: "session.archive",
-        title: "Archive session",
-        category: "Session",
+        title: language.t("command.session.archive"),
+        category: language.t("command.category.session"),
         keybind: "mod+shift+backspace",
         keybind: "mod+shift+backspace",
         disabled: !params.dir || !params.id,
         disabled: !params.dir || !params.id,
         onSelect: () => {
         onSelect: () => {
@@ -885,8 +886,8 @@ export default function Layout(props: ParentProps) {
       },
       },
       {
       {
         id: "theme.cycle",
         id: "theme.cycle",
-        title: "Cycle theme",
-        category: "Theme",
+        title: language.t("command.theme.cycle"),
+        category: language.t("command.category.theme"),
         keybind: "mod+shift+t",
         keybind: "mod+shift+t",
         onSelect: () => cycleTheme(1),
         onSelect: () => cycleTheme(1),
       },
       },
@@ -895,8 +896,8 @@ export default function Layout(props: ParentProps) {
     for (const [id, definition] of availableThemeEntries()) {
     for (const [id, definition] of availableThemeEntries()) {
       commands.push({
       commands.push({
         id: `theme.set.${id}`,
         id: `theme.set.${id}`,
-        title: `Use theme: ${definition.name ?? id}`,
-        category: "Theme",
+        title: language.t("command.theme.set", { theme: definition.name ?? id }),
+        category: language.t("command.category.theme"),
         onSelect: () => theme.commitPreview(),
         onSelect: () => theme.commitPreview(),
         onHighlight: () => {
         onHighlight: () => {
           theme.previewTheme(id)
           theme.previewTheme(id)
@@ -907,8 +908,8 @@ export default function Layout(props: ParentProps) {
 
 
     commands.push({
     commands.push({
       id: "theme.scheme.cycle",
       id: "theme.scheme.cycle",
-      title: "Cycle color scheme",
-      category: "Theme",
+      title: language.t("command.theme.scheme.cycle"),
+      category: language.t("command.category.theme"),
       keybind: "mod+shift+s",
       keybind: "mod+shift+s",
       onSelect: () => cycleColorScheme(1),
       onSelect: () => cycleColorScheme(1),
     })
     })
@@ -916,8 +917,8 @@ export default function Layout(props: ParentProps) {
     for (const scheme of colorSchemeOrder) {
     for (const scheme of colorSchemeOrder) {
       commands.push({
       commands.push({
         id: `theme.scheme.${scheme}`,
         id: `theme.scheme.${scheme}`,
-        title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
-        category: "Theme",
+        title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }),
+        category: language.t("command.category.theme"),
         onSelect: () => theme.commitPreview(),
         onSelect: () => theme.commitPreview(),
         onHighlight: () => {
         onHighlight: () => {
           theme.previewColorScheme(scheme)
           theme.previewColorScheme(scheme)

+ 87 - 84
packages/app/src/pages/session.tsx

@@ -33,6 +33,7 @@ import { DialogSelectModel } from "@/components/dialog-select-model"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
 import { DialogFork } from "@/components/dialog-fork"
 import { DialogFork } from "@/components/dialog-fork"
 import { useCommand } from "@/context/command"
 import { useCommand } from "@/context/command"
+import { useLanguage } from "@/context/language"
 import { useNavigate, useParams } from "@solidjs/router"
 import { useNavigate, useParams } from "@solidjs/router"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import type { FileDiff } from "@opencode-ai/sdk/v2/client"
 import type { FileDiff } from "@opencode-ai/sdk/v2/client"
@@ -161,6 +162,7 @@ export default function Page() {
   const dialog = useDialog()
   const dialog = useDialog()
   const codeComponent = useCodeComponent()
   const codeComponent = useCodeComponent()
   const command = useCommand()
   const command = useCommand()
+  const language = useLanguage()
   const platform = usePlatform()
   const platform = usePlatform()
   const params = useParams()
   const params = useParams()
   const navigate = useNavigate()
   const navigate = useNavigate()
@@ -433,51 +435,51 @@ export default function Page() {
   command.register(() => [
   command.register(() => [
     {
     {
       id: "session.new",
       id: "session.new",
-      title: "New session",
-      category: "Session",
+      title: language.t("command.session.new"),
+      category: language.t("command.category.session"),
       keybind: "mod+shift+s",
       keybind: "mod+shift+s",
       slash: "new",
       slash: "new",
       onSelect: () => navigate(`/${params.dir}/session`),
       onSelect: () => navigate(`/${params.dir}/session`),
     },
     },
     {
     {
       id: "file.open",
       id: "file.open",
-      title: "Open file",
-      description: "Search files and commands",
-      category: "File",
+      title: language.t("command.file.open"),
+      description: language.t("command.file.open.description"),
+      category: language.t("command.category.file"),
       keybind: "mod+p",
       keybind: "mod+p",
       slash: "open",
       slash: "open",
       onSelect: () => dialog.show(() => <DialogSelectFile />),
       onSelect: () => dialog.show(() => <DialogSelectFile />),
     },
     },
     {
     {
       id: "terminal.toggle",
       id: "terminal.toggle",
-      title: "Toggle terminal",
+      title: language.t("command.terminal.toggle"),
       description: "",
       description: "",
-      category: "View",
+      category: language.t("command.category.view"),
       keybind: "ctrl+`",
       keybind: "ctrl+`",
       slash: "terminal",
       slash: "terminal",
       onSelect: () => view().terminal.toggle(),
       onSelect: () => view().terminal.toggle(),
     },
     },
     {
     {
       id: "review.toggle",
       id: "review.toggle",
-      title: "Toggle review",
+      title: language.t("command.review.toggle"),
       description: "",
       description: "",
-      category: "View",
+      category: language.t("command.category.view"),
       keybind: "mod+shift+r",
       keybind: "mod+shift+r",
       onSelect: () => view().reviewPanel.toggle(),
       onSelect: () => view().reviewPanel.toggle(),
     },
     },
     {
     {
       id: "terminal.new",
       id: "terminal.new",
-      title: "New terminal",
-      description: "Create a new terminal tab",
-      category: "Terminal",
+      title: language.t("command.terminal.new"),
+      description: language.t("command.terminal.new.description"),
+      category: language.t("command.category.terminal"),
       keybind: "ctrl+alt+t",
       keybind: "ctrl+alt+t",
       onSelect: () => terminal.new(),
       onSelect: () => terminal.new(),
     },
     },
     {
     {
       id: "steps.toggle",
       id: "steps.toggle",
-      title: "Toggle steps",
-      description: "Show or hide steps for the current message",
-      category: "View",
+      title: language.t("command.steps.toggle"),
+      description: language.t("command.steps.toggle.description"),
+      category: language.t("command.category.view"),
       keybind: "mod+e",
       keybind: "mod+e",
       slash: "steps",
       slash: "steps",
       disabled: !params.id,
       disabled: !params.id,
@@ -489,62 +491,62 @@ export default function Page() {
     },
     },
     {
     {
       id: "message.previous",
       id: "message.previous",
-      title: "Previous message",
-      description: "Go to the previous user message",
-      category: "Session",
+      title: language.t("command.message.previous"),
+      description: language.t("command.message.previous.description"),
+      category: language.t("command.category.session"),
       keybind: "mod+arrowup",
       keybind: "mod+arrowup",
       disabled: !params.id,
       disabled: !params.id,
       onSelect: () => navigateMessageByOffset(-1),
       onSelect: () => navigateMessageByOffset(-1),
     },
     },
     {
     {
       id: "message.next",
       id: "message.next",
-      title: "Next message",
-      description: "Go to the next user message",
-      category: "Session",
+      title: language.t("command.message.next"),
+      description: language.t("command.message.next.description"),
+      category: language.t("command.category.session"),
       keybind: "mod+arrowdown",
       keybind: "mod+arrowdown",
       disabled: !params.id,
       disabled: !params.id,
       onSelect: () => navigateMessageByOffset(1),
       onSelect: () => navigateMessageByOffset(1),
     },
     },
     {
     {
       id: "model.choose",
       id: "model.choose",
-      title: "Choose model",
-      description: "Select a different model",
-      category: "Model",
+      title: language.t("command.model.choose"),
+      description: language.t("command.model.choose.description"),
+      category: language.t("command.category.model"),
       keybind: "mod+'",
       keybind: "mod+'",
       slash: "model",
       slash: "model",
       onSelect: () => dialog.show(() => <DialogSelectModel />),
       onSelect: () => dialog.show(() => <DialogSelectModel />),
     },
     },
     {
     {
       id: "mcp.toggle",
       id: "mcp.toggle",
-      title: "Toggle MCPs",
-      description: "Toggle MCPs",
-      category: "MCP",
+      title: language.t("command.mcp.toggle"),
+      description: language.t("command.mcp.toggle.description"),
+      category: language.t("command.category.mcp"),
       keybind: "mod+;",
       keybind: "mod+;",
       slash: "mcp",
       slash: "mcp",
       onSelect: () => dialog.show(() => <DialogSelectMcp />),
       onSelect: () => dialog.show(() => <DialogSelectMcp />),
     },
     },
     {
     {
       id: "agent.cycle",
       id: "agent.cycle",
-      title: "Cycle agent",
-      description: "Switch to the next agent",
-      category: "Agent",
+      title: language.t("command.agent.cycle"),
+      description: language.t("command.agent.cycle.description"),
+      category: language.t("command.category.agent"),
       keybind: "mod+.",
       keybind: "mod+.",
       slash: "agent",
       slash: "agent",
       onSelect: () => local.agent.move(1),
       onSelect: () => local.agent.move(1),
     },
     },
     {
     {
       id: "agent.cycle.reverse",
       id: "agent.cycle.reverse",
-      title: "Cycle agent backwards",
-      description: "Switch to the previous agent",
-      category: "Agent",
+      title: language.t("command.agent.cycle.reverse"),
+      description: language.t("command.agent.cycle.reverse.description"),
+      category: language.t("command.category.agent"),
       keybind: "shift+mod+.",
       keybind: "shift+mod+.",
       onSelect: () => local.agent.move(-1),
       onSelect: () => local.agent.move(-1),
     },
     },
     {
     {
       id: "model.variant.cycle",
       id: "model.variant.cycle",
-      title: "Cycle thinking effort",
-      description: "Switch to the next effort level",
-      category: "Model",
+      title: language.t("command.model.variant.cycle"),
+      description: language.t("command.model.variant.cycle.description"),
+      category: language.t("command.category.model"),
       keybind: "shift+mod+d",
       keybind: "shift+mod+d",
       onSelect: () => {
       onSelect: () => {
         local.model.variant.cycle()
         local.model.variant.cycle()
@@ -554,30 +556,31 @@ export default function Page() {
       id: "permissions.autoaccept",
       id: "permissions.autoaccept",
       title:
       title:
         params.id && permission.isAutoAccepting(params.id, sdk.directory)
         params.id && permission.isAutoAccepting(params.id, sdk.directory)
-          ? "Stop auto-accepting edits"
-          : "Auto-accept edits",
-      category: "Permissions",
+          ? language.t("command.permissions.autoaccept.disable")
+          : language.t("command.permissions.autoaccept.enable"),
+      category: language.t("command.category.permissions"),
       keybind: "mod+shift+a",
       keybind: "mod+shift+a",
       disabled: !params.id || !permission.permissionsEnabled(),
       disabled: !params.id || !permission.permissionsEnabled(),
       onSelect: () => {
       onSelect: () => {
         const sessionID = params.id
         const sessionID = params.id
         if (!sessionID) return
         if (!sessionID) return
         permission.toggleAutoAccept(sessionID, sdk.directory)
         permission.toggleAutoAccept(sessionID, sdk.directory)
+        const enabled = permission.isAutoAccepting(sessionID, sdk.directory)
         showToast({
         showToast({
-          title: permission.isAutoAccepting(sessionID, sdk.directory)
-            ? "Auto-accepting edits"
-            : "Stopped auto-accepting edits",
-          description: permission.isAutoAccepting(sessionID, sdk.directory)
-            ? "Edit and write permissions will be automatically approved"
-            : "Edit and write permissions will require approval",
+          title: enabled
+            ? language.t("toast.permissions.autoaccept.on.title")
+            : language.t("toast.permissions.autoaccept.off.title"),
+          description: enabled
+            ? language.t("toast.permissions.autoaccept.on.description")
+            : language.t("toast.permissions.autoaccept.off.description"),
         })
         })
       },
       },
     },
     },
     {
     {
       id: "session.undo",
       id: "session.undo",
-      title: "Undo",
-      description: "Undo the last message",
-      category: "Session",
+      title: language.t("command.session.undo"),
+      description: language.t("command.session.undo.description"),
+      category: language.t("command.category.session"),
       slash: "undo",
       slash: "undo",
       disabled: !params.id || visibleUserMessages().length === 0,
       disabled: !params.id || visibleUserMessages().length === 0,
       onSelect: async () => {
       onSelect: async () => {
@@ -604,9 +607,9 @@ export default function Page() {
     },
     },
     {
     {
       id: "session.redo",
       id: "session.redo",
-      title: "Redo",
-      description: "Redo the last undone message",
-      category: "Session",
+      title: language.t("command.session.redo"),
+      description: language.t("command.session.redo.description"),
+      category: language.t("command.category.session"),
       slash: "redo",
       slash: "redo",
       disabled: !params.id || !info()?.revert?.messageID,
       disabled: !params.id || !info()?.revert?.messageID,
       onSelect: async () => {
       onSelect: async () => {
@@ -633,9 +636,9 @@ export default function Page() {
     },
     },
     {
     {
       id: "session.compact",
       id: "session.compact",
-      title: "Compact session",
-      description: "Summarize the session to reduce context size",
-      category: "Session",
+      title: language.t("command.session.compact"),
+      description: language.t("command.session.compact.description"),
+      category: language.t("command.category.session"),
       slash: "compact",
       slash: "compact",
       disabled: !params.id || visibleUserMessages().length === 0,
       disabled: !params.id || visibleUserMessages().length === 0,
       onSelect: async () => {
       onSelect: async () => {
@@ -644,8 +647,8 @@ export default function Page() {
         const model = local.model.current()
         const model = local.model.current()
         if (!model) {
         if (!model) {
           showToast({
           showToast({
-            title: "No model selected",
-            description: "Connect a provider to summarize this session",
+            title: language.t("toast.model.none.title"),
+            description: language.t("toast.model.none.description"),
           })
           })
           return
           return
         }
         }
@@ -658,72 +661,72 @@ export default function Page() {
     },
     },
     {
     {
       id: "session.fork",
       id: "session.fork",
-      title: "Fork from message",
-      description: "Create a new session from a previous message",
-      category: "Session",
+      title: language.t("command.session.fork"),
+      description: language.t("command.session.fork.description"),
+      category: language.t("command.category.session"),
       slash: "fork",
       slash: "fork",
       disabled: !params.id || visibleUserMessages().length === 0,
       disabled: !params.id || visibleUserMessages().length === 0,
       onSelect: () => dialog.show(() => <DialogFork />),
       onSelect: () => dialog.show(() => <DialogFork />),
     },
     },
     ...(sync.data.config.share !== "disabled"
     ...(sync.data.config.share !== "disabled"
       ? [
       ? [
-          {
-            id: "session.share",
-            title: "Share session",
-            description: "Share this session and copy the URL to clipboard",
-            category: "Session",
-            slash: "share",
-            disabled: !params.id || !!info()?.share?.url,
-            onSelect: async () => {
+           {
+             id: "session.share",
+             title: language.t("command.session.share"),
+             description: language.t("command.session.share.description"),
+             category: language.t("command.category.session"),
+             slash: "share",
+             disabled: !params.id || !!info()?.share?.url,
+             onSelect: async () => {
               if (!params.id) return
               if (!params.id) return
               await sdk.client.session
               await sdk.client.session
                 .share({ sessionID: params.id })
                 .share({ sessionID: params.id })
                 .then((res) => {
                 .then((res) => {
                   navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
                   navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
                     showToast({
                     showToast({
-                      title: "Failed to copy URL to clipboard",
+                      title: language.t("toast.session.share.copyFailed.title"),
                       variant: "error",
                       variant: "error",
                     }),
                     }),
                   )
                   )
                 })
                 })
                 .then(() =>
                 .then(() =>
                   showToast({
                   showToast({
-                    title: "Session shared",
-                    description: "Share URL copied to clipboard!",
+                    title: language.t("toast.session.share.success.title"),
+                    description: language.t("toast.session.share.success.description"),
                     variant: "success",
                     variant: "success",
                   }),
                   }),
                 )
                 )
                 .catch(() =>
                 .catch(() =>
                   showToast({
                   showToast({
-                    title: "Failed to share session",
-                    description: "An error occurred while sharing the session",
+                    title: language.t("toast.session.share.failed.title"),
+                    description: language.t("toast.session.share.failed.description"),
                     variant: "error",
                     variant: "error",
                   }),
                   }),
                 )
                 )
             },
             },
           },
           },
-          {
-            id: "session.unshare",
-            title: "Unshare session",
-            description: "Stop sharing this session",
-            category: "Session",
-            slash: "unshare",
-            disabled: !params.id || !info()?.share?.url,
-            onSelect: async () => {
+           {
+             id: "session.unshare",
+             title: language.t("command.session.unshare"),
+             description: language.t("command.session.unshare.description"),
+             category: language.t("command.category.session"),
+             slash: "unshare",
+             disabled: !params.id || !info()?.share?.url,
+             onSelect: async () => {
               if (!params.id) return
               if (!params.id) return
               await sdk.client.session
               await sdk.client.session
                 .unshare({ sessionID: params.id })
                 .unshare({ sessionID: params.id })
                 .then(() =>
                 .then(() =>
                   showToast({
                   showToast({
-                    title: "Session unshared",
-                    description: "Session unshared successfully!",
+                    title: language.t("toast.session.unshare.success.title"),
+                    description: language.t("toast.session.unshare.success.description"),
                     variant: "success",
                     variant: "success",
                   }),
                   }),
                 )
                 )
                 .catch(() =>
                 .catch(() =>
                   showToast({
                   showToast({
-                    title: "Failed to unshare session",
-                    description: "An error occurred while unsharing the session",
+                    title: language.t("toast.session.unshare.failed.title"),
+                    description: language.t("toast.session.unshare.failed.description"),
                     variant: "error",
                     variant: "error",
                   }),
                   }),
                 )
                 )

+ 5 - 2
packages/ui/src/components/list.tsx

@@ -16,6 +16,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
   class?: string
   class?: string
   children: (item: T) => JSX.Element
   children: (item: T) => JSX.Element
   emptyMessage?: string
   emptyMessage?: string
+  loadingMessage?: string
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
   onMove?: (item: T | undefined) => void
   onMove?: (item: T | undefined) => void
   activeIcon?: IconProps["name"]
   activeIcon?: IconProps["name"]
@@ -207,8 +208,10 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
           fallback={
           fallback={
             <div data-slot="list-empty-state">
             <div data-slot="list-empty-state">
               <div data-slot="list-message">
               <div data-slot="list-message">
-                {props.emptyMessage ?? (grouped.loading ? "Loading" : "No results")} for{" "}
-                <span data-slot="list-filter">&quot;{filter()}&quot;</span>
+                {grouped.loading ? props.loadingMessage ?? "Loading" : props.emptyMessage ?? "No results"}
+                <Show when={!props.emptyMessage && !props.loadingMessage && !!filter()}>
+                  {" "}for <span data-slot="list-filter">&quot;{filter()}&quot;</span>
+                </Show>
               </div>
               </div>
             </div>
             </div>
           }
           }