Adam 3 недель назад
Родитель
Сommit
233d003b49

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

@@ -2,6 +2,7 @@ import { Component } from "solid-js"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { Icon } from "@opencode-ai/ui/icon"
+import { useLanguage } from "@/context/language"
 import { SettingsGeneral } from "./settings-general"
 import { SettingsKeybinds } from "./settings-keybinds"
 import { SettingsPermissions } from "./settings-permissions"
@@ -12,6 +13,8 @@ import { SettingsCommands } from "./settings-commands"
 import { SettingsMcp } from "./settings-mcp"
 
 export const DialogSettings: Component = () => {
+  const language = useLanguage()
+
   return (
     <Dialog size="x-large">
       <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
@@ -26,15 +29,15 @@ export const DialogSettings: Component = () => {
               "padding-bottom": "12px",
             }}
           >
-            <Tabs.SectionTitle>Desktop</Tabs.SectionTitle>
+            <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
             <div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
               <Tabs.Trigger value="general">
                 <Icon name="sliders" />
-                General
+                {language.t("settings.tab.general")}
               </Tabs.Trigger>
               <Tabs.Trigger value="shortcuts">
                 <Icon name="keyboard" />
-                Shortcuts
+                {language.t("settings.tab.shortcuts")}
               </Tabs.Trigger>
             </div>
           </div>

+ 5 - 2
packages/app/src/components/settings-agents.tsx

@@ -1,11 +1,14 @@
 import { Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 export const SettingsAgents: Component = () => {
+  const language = useLanguage()
+
   return (
     <div class="flex flex-col h-full overflow-y-auto">
       <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">Agents</h2>
-        <p class="text-14-regular text-text-weak">Agent settings will be configurable here.</p>
+        <h2 class="text-16-medium text-text-strong">{language.t("settings.agents.title")}</h2>
+        <p class="text-14-regular text-text-weak">{language.t("settings.agents.description")}</p>
       </div>
     </div>
   )

+ 5 - 2
packages/app/src/components/settings-commands.tsx

@@ -1,11 +1,14 @@
 import { Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 export const SettingsCommands: Component = () => {
+  const language = useLanguage()
+
   return (
     <div class="flex flex-col h-full overflow-y-auto">
       <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">Commands</h2>
-        <p class="text-14-regular text-text-weak">Command settings will be configurable here.</p>
+        <h2 class="text-16-medium text-text-strong">{language.t("settings.commands.title")}</h2>
+        <p class="text-14-regular text-text-weak">{language.t("settings.commands.description")}</p>
       </div>
     </div>
   )

+ 71 - 23
packages/app/src/components/settings-general.tsx

@@ -2,22 +2,33 @@ import { Component, createMemo, type JSX } from "solid-js"
 import { Select } from "@opencode-ai/ui/select"
 import { Switch } from "@opencode-ai/ui/switch"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
+import { useLanguage } from "@/context/language"
 import { useSettings, monoFontFamily } from "@/context/settings"
 import { playSound, SOUND_OPTIONS } from "@/utils/sound"
 
 export const SettingsGeneral: Component = () => {
   const theme = useTheme()
+  const language = useLanguage()
   const settings = useSettings()
 
   const themeOptions = createMemo(() =>
     Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
   )
 
-  const colorSchemeOptions: { value: ColorScheme; label: string }[] = [
-    { value: "system", label: "System setting" },
-    { value: "light", label: "Light" },
-    { value: "dark", label: "Dark" },
-  ]
+  const colorSchemeOptions = createMemo(
+    (): { value: ColorScheme; label: string }[] => [
+      { value: "system", label: language.t("theme.scheme.system") },
+      { value: "light", label: language.t("theme.scheme.light") },
+      { value: "dark", label: language.t("theme.scheme.dark") },
+    ],
+  )
+
+  const languageOptions = createMemo(() =>
+    language.locales.map((locale) => ({
+      value: locale,
+      label: language.label(locale),
+    })),
+  )
 
   const fontOptions = [
     { value: "ibm-plex-mono", label: "IBM Plex Mono" },
@@ -45,20 +56,39 @@ export const SettingsGeneral: Component = () => {
         }}
       >
         <div class="flex flex-col gap-1 pt-6 pb-8">
-          <h2 class="text-16-medium text-text-strong">General</h2>
+          <h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
         </div>
       </div>
 
       <div class="flex flex-col gap-8 w-full">
         {/* Appearance Section */}
         <div class="flex flex-col gap-1">
-          <h3 class="text-14-medium text-text-strong pb-2">Appearance</h3>
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
 
           <div class="bg-surface-raised-base px-4 rounded-lg">
-            <SettingsRow title="Appearance" description="Customise how OpenCode looks on your device">
+            <SettingsRow
+              title={language.t("settings.general.row.language.title")}
+              description={language.t("settings.general.row.language.description")}
+            >
               <Select
-                options={colorSchemeOptions}
-                current={colorSchemeOptions.find((o) => o.value === theme.colorScheme())}
+                options={languageOptions()}
+                current={languageOptions().find((o) => o.value === language.locale())}
+                value={(o) => o.value}
+                label={(o) => o.label}
+                onSelect={(option) => option && language.setLocale(option.value)}
+                variant="secondary"
+                size="small"
+                triggerVariant="settings"
+              />
+            </SettingsRow>
+
+            <SettingsRow
+              title={language.t("settings.general.row.appearance.title")}
+              description={language.t("settings.general.row.appearance.description")}
+            >
+              <Select
+                options={colorSchemeOptions()}
+                current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
                 value={(o) => o.value}
                 label={(o) => o.label}
                 onSelect={(option) => option && theme.setColorScheme(option.value)}
@@ -74,12 +104,12 @@ export const SettingsGeneral: Component = () => {
             </SettingsRow>
 
             <SettingsRow
-              title="Theme"
+              title={language.t("settings.general.row.theme.title")}
               description={
                 <>
-                  Customise how OpenCode is themed.{" "}
+                  {language.t("settings.general.row.theme.description")} {" "}
                   <a href="#" class="text-text-interactive-base">
-                    Learn more
+                    {language.t("common.learnMore")}
                   </a>
                 </>
               }
@@ -104,7 +134,10 @@ export const SettingsGeneral: Component = () => {
               />
             </SettingsRow>
 
-            <SettingsRow title="Font" description="Customise the mono font used in code blocks">
+            <SettingsRow
+              title={language.t("settings.general.row.font.title")}
+              description={language.t("settings.general.row.font.description")}
+            >
               <Select
                 options={fontOptions}
                 current={fontOptions.find((o) => o.value === settings.appearance.font())}
@@ -124,12 +157,12 @@ export const SettingsGeneral: Component = () => {
 
         {/* System notifications Section */}
         <div class="flex flex-col gap-1">
-          <h3 class="text-14-medium text-text-strong pb-2">System notifications</h3>
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
 
           <div class="bg-surface-raised-base px-4 rounded-lg">
             <SettingsRow
-              title="Agent"
-              description="Show system notification when the agent is complete or needs attention"
+              title={language.t("settings.general.notifications.agent.title")}
+              description={language.t("settings.general.notifications.agent.description")}
             >
               <Switch
                 checked={settings.notifications.agent()}
@@ -137,14 +170,20 @@ export const SettingsGeneral: Component = () => {
               />
             </SettingsRow>
 
-            <SettingsRow title="Permissions" description="Show system notification when a permission is required">
+            <SettingsRow
+              title={language.t("settings.general.notifications.permissions.title")}
+              description={language.t("settings.general.notifications.permissions.description")}
+            >
               <Switch
                 checked={settings.notifications.permissions()}
                 onChange={(checked) => settings.notifications.setPermissions(checked)}
               />
             </SettingsRow>
 
-            <SettingsRow title="Errors" description="Show system notification when an error occurs">
+            <SettingsRow
+              title={language.t("settings.general.notifications.errors.title")}
+              description={language.t("settings.general.notifications.errors.description")}
+            >
               <Switch
                 checked={settings.notifications.errors()}
                 onChange={(checked) => settings.notifications.setErrors(checked)}
@@ -155,10 +194,13 @@ export const SettingsGeneral: Component = () => {
 
         {/* Sound effects Section */}
         <div class="flex flex-col gap-1">
-          <h3 class="text-14-medium text-text-strong pb-2">Sound effects</h3>
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
 
           <div class="bg-surface-raised-base px-4 rounded-lg">
-            <SettingsRow title="Agent" description="Play sound when the agent is complete or needs attention">
+            <SettingsRow
+              title={language.t("settings.general.sounds.agent.title")}
+              description={language.t("settings.general.sounds.agent.description")}
+            >
               <Select
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.agent())}
@@ -179,7 +221,10 @@ export const SettingsGeneral: Component = () => {
               />
             </SettingsRow>
 
-            <SettingsRow title="Permissions" description="Play sound when a permission is required">
+            <SettingsRow
+              title={language.t("settings.general.sounds.permissions.title")}
+              description={language.t("settings.general.sounds.permissions.description")}
+            >
               <Select
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
@@ -200,7 +245,10 @@ export const SettingsGeneral: Component = () => {
               />
             </SettingsRow>
 
-            <SettingsRow title="Errors" description="Play sound when an error occurs">
+            <SettingsRow
+              title={language.t("settings.general.sounds.errors.title")}
+              description={language.t("settings.general.sounds.errors.description")}
+            >
               <Select
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.errors())}

+ 39 - 10
packages/app/src/components/settings-keybinds.tsx

@@ -2,6 +2,7 @@ import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } fr
 import { Button } from "@opencode-ai/ui/button"
 import { showToast } from "@opencode-ai/ui/toast"
 import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
+import { useLanguage } from "@/context/language"
 import { useSettings } from "@/context/settings"
 
 const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
@@ -17,6 +18,23 @@ type KeybindMeta = {
 
 const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
 
+type GroupKey =
+  | "settings.shortcuts.group.general"
+  | "settings.shortcuts.group.session"
+  | "settings.shortcuts.group.navigation"
+  | "settings.shortcuts.group.modelAndAgent"
+  | "settings.shortcuts.group.terminal"
+  | "settings.shortcuts.group.prompt"
+
+const groupKey: Record<KeybindGroup, GroupKey> = {
+  General: "settings.shortcuts.group.general",
+  Session: "settings.shortcuts.group.session",
+  Navigation: "settings.shortcuts.group.navigation",
+  "Model and agent": "settings.shortcuts.group.modelAndAgent",
+  Terminal: "settings.shortcuts.group.terminal",
+  Prompt: "settings.shortcuts.group.prompt",
+}
+
 function groupFor(id: string): KeybindGroup {
   if (id === PALETTE_ID) return "General"
   if (id.startsWith("terminal.")) return "Terminal"
@@ -86,6 +104,7 @@ function signatures(config: string | undefined) {
 
 export const SettingsKeybinds: Component = () => {
   const command = useCommand()
+  const language = useLanguage()
   const settings = useSettings()
 
   const [active, setActive] = createSignal<string | null>(null)
@@ -117,12 +136,16 @@ export const SettingsKeybinds: Component = () => {
   const resetAll = () => {
     stop()
     settings.keybinds.resetAll()
-    showToast({ title: "Shortcuts reset", description: "Keyboard shortcuts have been reset to defaults." })
+    showToast({
+      title: language.t("settings.shortcuts.reset.toast.title"),
+      description: language.t("settings.shortcuts.reset.toast.description"),
+    })
   }
 
   const list = createMemo(() => {
+    language.locale()
     const out = new Map<string, KeybindMeta>()
-    out.set(PALETTE_ID, { title: "Command palette", group: "General" })
+    out.set(PALETTE_ID, { title: language.t("command.palette"), group: "General" })
 
     for (const opt of command.catalog) {
       if (opt.id.startsWith("suggested.")) continue
@@ -188,7 +211,7 @@ export const SettingsKeybinds: Component = () => {
 
     const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
     for (const sig of signatures(palette)) {
-      add(sig, { id: PALETTE_ID, title: "Command palette" })
+      add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) })
     }
 
     const valueFor = (id: string) => {
@@ -258,8 +281,11 @@ export const SettingsKeybinds: Component = () => {
 
       if (conflicts.size > 0) {
         showToast({
-          title: "Shortcut already in use",
-          description: `${formatKeybind(next)} is already assigned to ${[...conflicts.values()].join(", ")}.`,
+          title: language.t("settings.shortcuts.conflict.title"),
+          description: language.t("settings.shortcuts.conflict.description", {
+            keybind: formatKeybind(next),
+            titles: [...conflicts.values()].join(", "),
+          }),
         })
         return
       }
@@ -288,9 +314,9 @@ export const SettingsKeybinds: Component = () => {
         }}
       >
         <div class="flex items-center justify-between gap-4 pt-6 pb-8 max-w-[720px]">
-          <h2 class="text-16-medium text-text-strong">Keyboard shortcuts</h2>
+          <h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
           <Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
-            Reset to defaults
+            {language.t("settings.shortcuts.reset.button")}
           </Button>
         </div>
       </div>
@@ -300,7 +326,7 @@ export const SettingsKeybinds: Component = () => {
           {(group) => (
             <Show when={(grouped().get(group) ?? []).length > 0}>
               <div class="flex flex-col gap-1">
-                <h3 class="text-14-medium text-text-strong pb-2">{group}</h3>
+                <h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
                 <div class="bg-surface-raised-base px-4 rounded-lg">
                   <For each={grouped().get(group) ?? []}>
                     {(id) => (
@@ -316,8 +342,11 @@ export const SettingsKeybinds: Component = () => {
                           }}
                           onClick={() => start(id)}
                         >
-                          <Show when={active() === id} fallback={command.keybind(id) || "Unassigned"}>
-                            Press keys
+                          <Show
+                            when={active() === id}
+                            fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
+                          >
+                            {language.t("settings.shortcuts.pressKeys")}
                           </Show>
                         </button>
                       </div>

+ 5 - 2
packages/app/src/components/settings-mcp.tsx

@@ -1,11 +1,14 @@
 import { Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 export const SettingsMcp: Component = () => {
+  const language = useLanguage()
+
   return (
     <div class="flex flex-col h-full overflow-y-auto">
       <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">MCP</h2>
-        <p class="text-14-regular text-text-weak">MCP settings will be configurable here.</p>
+        <h2 class="text-16-medium text-text-strong">{language.t("settings.mcp.title")}</h2>
+        <p class="text-14-regular text-text-weak">{language.t("settings.mcp.description")}</p>
       </div>
     </div>
   )

+ 5 - 2
packages/app/src/components/settings-models.tsx

@@ -1,11 +1,14 @@
 import { Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 export const SettingsModels: Component = () => {
+  const language = useLanguage()
+
   return (
     <div class="flex flex-col h-full overflow-y-auto">
       <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">Models</h2>
-        <p class="text-14-regular text-text-weak">Model settings will be configurable here.</p>
+        <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
+        <p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
       </div>
     </div>
   )

+ 105 - 31
packages/app/src/components/settings-permissions.tsx

@@ -2,6 +2,7 @@ import { Select } from "@opencode-ai/ui/select"
 import { showToast } from "@opencode-ai/ui/toast"
 import { Component, For, createMemo, type JSX } from "solid-js"
 import { useGlobalSync } from "@/context/global-sync"
+import { useLanguage } from "@/context/language"
 
 type PermissionAction = "allow" | "ask" | "deny"
 
@@ -15,30 +16,94 @@ type PermissionItem = {
   description: string
 }
 
-const ACTIONS: Array<{ value: PermissionAction; label: string }> = [
-  { value: "allow", label: "Allow" },
-  { value: "ask", label: "Ask" },
-  { value: "deny", label: "Deny" },
-]
-
-const ITEMS: PermissionItem[] = [
-  { id: "read", title: "Read", description: "Reading a file (matches the file path)" },
-  { id: "edit", title: "Edit", description: "Modify files, including edits, writes, patches, and multi-edits" },
-  { id: "glob", title: "Glob", description: "Match files using glob patterns" },
-  { id: "grep", title: "Grep", description: "Search file contents using regular expressions" },
-  { id: "list", title: "List", description: "List files within a directory" },
-  { id: "bash", title: "Bash", description: "Run shell commands" },
-  { id: "task", title: "Task", description: "Launch sub-agents" },
-  { id: "skill", title: "Skill", description: "Load a skill by name" },
-  { id: "lsp", title: "LSP", description: "Run language server queries" },
-  { id: "todoread", title: "Todo Read", description: "Read the todo list" },
-  { id: "todowrite", title: "Todo Write", description: "Update the todo list" },
-  { id: "webfetch", title: "Web Fetch", description: "Fetch content from a URL" },
-  { id: "websearch", title: "Web Search", description: "Search the web" },
-  { id: "codesearch", title: "Code Search", description: "Search code on the web" },
-  { id: "external_directory", title: "External Directory", description: "Access files outside the project directory" },
-  { id: "doom_loop", title: "Doom Loop", description: "Detect repeated tool calls with identical input" },
-]
+const ACTIONS = [
+  { value: "allow", label: "settings.permissions.action.allow" },
+  { value: "ask", label: "settings.permissions.action.ask" },
+  { value: "deny", label: "settings.permissions.action.deny" },
+] as const
+
+const ITEMS = [
+  {
+    id: "read",
+    title: "settings.permissions.tool.read.title",
+    description: "settings.permissions.tool.read.description",
+  },
+  {
+    id: "edit",
+    title: "settings.permissions.tool.edit.title",
+    description: "settings.permissions.tool.edit.description",
+  },
+  {
+    id: "glob",
+    title: "settings.permissions.tool.glob.title",
+    description: "settings.permissions.tool.glob.description",
+  },
+  {
+    id: "grep",
+    title: "settings.permissions.tool.grep.title",
+    description: "settings.permissions.tool.grep.description",
+  },
+  {
+    id: "list",
+    title: "settings.permissions.tool.list.title",
+    description: "settings.permissions.tool.list.description",
+  },
+  {
+    id: "bash",
+    title: "settings.permissions.tool.bash.title",
+    description: "settings.permissions.tool.bash.description",
+  },
+  {
+    id: "task",
+    title: "settings.permissions.tool.task.title",
+    description: "settings.permissions.tool.task.description",
+  },
+  {
+    id: "skill",
+    title: "settings.permissions.tool.skill.title",
+    description: "settings.permissions.tool.skill.description",
+  },
+  {
+    id: "lsp",
+    title: "settings.permissions.tool.lsp.title",
+    description: "settings.permissions.tool.lsp.description",
+  },
+  {
+    id: "todoread",
+    title: "settings.permissions.tool.todoread.title",
+    description: "settings.permissions.tool.todoread.description",
+  },
+  {
+    id: "todowrite",
+    title: "settings.permissions.tool.todowrite.title",
+    description: "settings.permissions.tool.todowrite.description",
+  },
+  {
+    id: "webfetch",
+    title: "settings.permissions.tool.webfetch.title",
+    description: "settings.permissions.tool.webfetch.description",
+  },
+  {
+    id: "websearch",
+    title: "settings.permissions.tool.websearch.title",
+    description: "settings.permissions.tool.websearch.description",
+  },
+  {
+    id: "codesearch",
+    title: "settings.permissions.tool.codesearch.title",
+    description: "settings.permissions.tool.codesearch.description",
+  },
+  {
+    id: "external_directory",
+    title: "settings.permissions.tool.external_directory.title",
+    description: "settings.permissions.tool.external_directory.description",
+  },
+  {
+    id: "doom_loop",
+    title: "settings.permissions.tool.doom_loop.title",
+    description: "settings.permissions.tool.doom_loop.description",
+  },
+] as const
 
 const VALID_ACTIONS = new Set<PermissionAction>(["allow", "ask", "deny"])
 
@@ -67,6 +132,15 @@ function getRuleDefault(value: unknown): PermissionAction | undefined {
 
 export const SettingsPermissions: Component = () => {
   const globalSync = useGlobalSync()
+  const language = useLanguage()
+
+  const actions = createMemo(
+    (): Array<{ value: PermissionAction; label: string }> =>
+      ACTIONS.map((action) => ({
+        value: action.value,
+        label: language.t(action.label),
+      })),
+  )
 
   const permission = createMemo(() => {
     return toMap(globalSync.data.config.permission)
@@ -95,7 +169,7 @@ export const SettingsPermissions: Component = () => {
     globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => {
       globalSync.set("config", "permission", before)
       const message = err instanceof Error ? err.message : String(err)
-      showToast({ title: "Failed to update permissions", description: message })
+      showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message })
     })
   }
 
@@ -109,21 +183,21 @@ export const SettingsPermissions: Component = () => {
         }}
       >
         <div class="flex flex-col gap-1 p-8 max-w-[720px]">
-          <h2 class="text-16-medium text-text-strong">Permissions</h2>
-          <p class="text-14-regular text-text-weak">Control what tools the server can use by default.</p>
+          <h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
+          <p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
         </div>
       </div>
 
       <div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
         <div class="flex flex-col gap-2">
-          <h3 class="text-14-medium text-text-strong">Appearance</h3>
+          <h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
           <div class="border border-border-weak-base rounded-lg overflow-hidden">
             <For each={ITEMS}>
               {(item) => (
-                <SettingsRow title={item.title} description={item.description}>
+                <SettingsRow title={language.t(item.title)} description={language.t(item.description)}>
                   <Select
-                    options={ACTIONS}
-                    current={ACTIONS.find((o) => o.value === actionFor(item.id))}
+                    options={actions()}
+                    current={actions().find((o) => o.value === actionFor(item.id))}
                     value={(o) => o.value}
                     label={(o) => o.label}
                     onSelect={(option) => option && setPermission(item.id, option.value)}

+ 5 - 2
packages/app/src/components/settings-providers.tsx

@@ -1,11 +1,14 @@
 import { Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 export const SettingsProviders: Component = () => {
+  const language = useLanguage()
+
   return (
     <div class="flex flex-col h-full overflow-y-auto">
       <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">Providers</h2>
-        <p class="text-14-regular text-text-weak">Provider settings will be configurable here.</p>
+        <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
+        <p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
       </div>
     </div>
   )

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

@@ -26,6 +26,8 @@ export const dict = {
   "command.session.next": "Next session",
   "command.session.archive": "Archive session",
 
+  "command.palette": "Command palette",
+
   "command.theme.cycle": "Cycle theme",
   "command.theme.set": "Use theme: {{theme}}",
   "command.theme.scheme.cycle": "Cycle color scheme",
@@ -395,6 +397,7 @@ export const dict = {
   "common.dismiss": "Dismiss",
   "common.requestFailed": "Request failed",
   "common.moreOptions": "More options",
+  "common.learnMore": "Learn more",
   "common.rename": "Rename",
   "common.reset": "Reset",
   "common.delete": "Delete",
@@ -412,6 +415,106 @@ export const dict = {
   "sidebar.project.recentSessions": "Recent sessions",
   "sidebar.project.viewAllSessions": "View all sessions",
 
+  "settings.section.desktop": "Desktop",
+  "settings.tab.general": "General",
+  "settings.tab.shortcuts": "Shortcuts",
+
+  "settings.general.section.appearance": "Appearance",
+  "settings.general.section.notifications": "System notifications",
+  "settings.general.section.sounds": "Sound effects",
+
+  "settings.general.row.language.title": "Language",
+  "settings.general.row.language.description": "Change the display language for OpenCode",
+  "settings.general.row.appearance.title": "Appearance",
+  "settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
+  "settings.general.row.theme.title": "Theme",
+  "settings.general.row.theme.description": "Customise how OpenCode is themed.",
+  "settings.general.row.font.title": "Font",
+  "settings.general.row.font.description": "Customise the mono font used in code blocks",
+
+  "settings.general.notifications.agent.title": "Agent",
+  "settings.general.notifications.agent.description": "Show system notification when the agent is complete or needs attention",
+  "settings.general.notifications.permissions.title": "Permissions",
+  "settings.general.notifications.permissions.description": "Show system notification when a permission is required",
+  "settings.general.notifications.errors.title": "Errors",
+  "settings.general.notifications.errors.description": "Show system notification when an error occurs",
+
+  "settings.general.sounds.agent.title": "Agent",
+  "settings.general.sounds.agent.description": "Play sound when the agent is complete or needs attention",
+  "settings.general.sounds.permissions.title": "Permissions",
+  "settings.general.sounds.permissions.description": "Play sound when a permission is required",
+  "settings.general.sounds.errors.title": "Errors",
+  "settings.general.sounds.errors.description": "Play sound when an error occurs",
+
+  "settings.shortcuts.title": "Keyboard shortcuts",
+  "settings.shortcuts.reset.button": "Reset to defaults",
+  "settings.shortcuts.reset.toast.title": "Shortcuts reset",
+  "settings.shortcuts.reset.toast.description": "Keyboard shortcuts have been reset to defaults.",
+  "settings.shortcuts.conflict.title": "Shortcut already in use",
+  "settings.shortcuts.conflict.description": "{{keybind}} is already assigned to {{titles}}.",
+  "settings.shortcuts.unassigned": "Unassigned",
+  "settings.shortcuts.pressKeys": "Press keys",
+
+  "settings.shortcuts.group.general": "General",
+  "settings.shortcuts.group.session": "Session",
+  "settings.shortcuts.group.navigation": "Navigation",
+  "settings.shortcuts.group.modelAndAgent": "Model and agent",
+  "settings.shortcuts.group.terminal": "Terminal",
+  "settings.shortcuts.group.prompt": "Prompt",
+
+  "settings.providers.title": "Providers",
+  "settings.providers.description": "Provider settings will be configurable here.",
+  "settings.models.title": "Models",
+  "settings.models.description": "Model settings will be configurable here.",
+  "settings.agents.title": "Agents",
+  "settings.agents.description": "Agent settings will be configurable here.",
+  "settings.commands.title": "Commands",
+  "settings.commands.description": "Command settings will be configurable here.",
+  "settings.mcp.title": "MCP",
+  "settings.mcp.description": "MCP settings will be configurable here.",
+
+  "settings.permissions.title": "Permissions",
+  "settings.permissions.description": "Control what tools the server can use by default.",
+  "settings.permissions.section.tools": "Tools",
+  "settings.permissions.toast.updateFailed.title": "Failed to update permissions",
+
+  "settings.permissions.action.allow": "Allow",
+  "settings.permissions.action.ask": "Ask",
+  "settings.permissions.action.deny": "Deny",
+
+  "settings.permissions.tool.read.title": "Read",
+  "settings.permissions.tool.read.description": "Reading a file (matches the file path)",
+  "settings.permissions.tool.edit.title": "Edit",
+  "settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits",
+  "settings.permissions.tool.glob.title": "Glob",
+  "settings.permissions.tool.glob.description": "Match files using glob patterns",
+  "settings.permissions.tool.grep.title": "Grep",
+  "settings.permissions.tool.grep.description": "Search file contents using regular expressions",
+  "settings.permissions.tool.list.title": "List",
+  "settings.permissions.tool.list.description": "List files within a directory",
+  "settings.permissions.tool.bash.title": "Bash",
+  "settings.permissions.tool.bash.description": "Run shell commands",
+  "settings.permissions.tool.task.title": "Task",
+  "settings.permissions.tool.task.description": "Launch sub-agents",
+  "settings.permissions.tool.skill.title": "Skill",
+  "settings.permissions.tool.skill.description": "Load a skill by name",
+  "settings.permissions.tool.lsp.title": "LSP",
+  "settings.permissions.tool.lsp.description": "Run language server queries",
+  "settings.permissions.tool.todoread.title": "Todo Read",
+  "settings.permissions.tool.todoread.description": "Read the todo list",
+  "settings.permissions.tool.todowrite.title": "Todo Write",
+  "settings.permissions.tool.todowrite.description": "Update the todo list",
+  "settings.permissions.tool.webfetch.title": "Web Fetch",
+  "settings.permissions.tool.webfetch.description": "Fetch content from a URL",
+  "settings.permissions.tool.websearch.title": "Web Search",
+  "settings.permissions.tool.websearch.description": "Search the web",
+  "settings.permissions.tool.codesearch.title": "Code Search",
+  "settings.permissions.tool.codesearch.description": "Search code on the web",
+  "settings.permissions.tool.external_directory.title": "External Directory",
+  "settings.permissions.tool.external_directory.description": "Access files outside the project directory",
+  "settings.permissions.tool.doom_loop.title": "Doom Loop",
+  "settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input",
+
   "workspace.new": "New workspace",
   "workspace.type.local": "local",
   "workspace.type.sandbox": "sandbox",

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

@@ -30,6 +30,8 @@ export const dict = {
   "command.session.next": "下一个会话",
   "command.session.archive": "归档会话",
 
+  "command.palette": "命令面板",
+
   "command.theme.cycle": "切换主题",
   "command.theme.set": "使用主题: {{theme}}",
   "command.theme.scheme.cycle": "切换配色方案",
@@ -391,6 +393,7 @@ export const dict = {
   "common.dismiss": "忽略",
   "common.requestFailed": "请求失败",
   "common.moreOptions": "更多选项",
+  "common.learnMore": "了解更多",
   "common.rename": "重命名",
   "common.reset": "重置",
   "common.delete": "删除",
@@ -408,6 +411,106 @@ export const dict = {
   "sidebar.project.recentSessions": "最近会话",
   "sidebar.project.viewAllSessions": "查看全部会话",
 
+  "settings.section.desktop": "桌面",
+  "settings.tab.general": "通用",
+  "settings.tab.shortcuts": "快捷键",
+
+  "settings.general.section.appearance": "外观",
+  "settings.general.section.notifications": "系统通知",
+  "settings.general.section.sounds": "音效",
+
+  "settings.general.row.language.title": "语言",
+  "settings.general.row.language.description": "更改 OpenCode 的显示语言",
+  "settings.general.row.appearance.title": "外观",
+  "settings.general.row.appearance.description": "自定义 OpenCode 在你的设备上的外观",
+  "settings.general.row.theme.title": "主题",
+  "settings.general.row.theme.description": "自定义 OpenCode 的主题。",
+  "settings.general.row.font.title": "字体",
+  "settings.general.row.font.description": "自定义代码块使用的等宽字体",
+
+  "settings.general.notifications.agent.title": "智能体",
+  "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知",
+  "settings.general.notifications.permissions.title": "权限",
+  "settings.general.notifications.permissions.description": "当需要权限时显示系统通知",
+  "settings.general.notifications.errors.title": "错误",
+  "settings.general.notifications.errors.description": "发生错误时显示系统通知",
+
+  "settings.general.sounds.agent.title": "智能体",
+  "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音",
+  "settings.general.sounds.permissions.title": "权限",
+  "settings.general.sounds.permissions.description": "当需要权限时播放声音",
+  "settings.general.sounds.errors.title": "错误",
+  "settings.general.sounds.errors.description": "发生错误时播放声音",
+
+  "settings.shortcuts.title": "键盘快捷键",
+  "settings.shortcuts.reset.button": "重置为默认值",
+  "settings.shortcuts.reset.toast.title": "快捷键已重置",
+  "settings.shortcuts.reset.toast.description": "键盘快捷键已重置为默认设置。",
+  "settings.shortcuts.conflict.title": "快捷键已被占用",
+  "settings.shortcuts.conflict.description": "{{keybind}} 已分配给 {{titles}}。",
+  "settings.shortcuts.unassigned": "未设置",
+  "settings.shortcuts.pressKeys": "按下按键",
+
+  "settings.shortcuts.group.general": "通用",
+  "settings.shortcuts.group.session": "会话",
+  "settings.shortcuts.group.navigation": "导航",
+  "settings.shortcuts.group.modelAndAgent": "模型与智能体",
+  "settings.shortcuts.group.terminal": "终端",
+  "settings.shortcuts.group.prompt": "提示",
+
+  "settings.providers.title": "提供商",
+  "settings.providers.description": "提供商设置将在此处可配置。",
+  "settings.models.title": "模型",
+  "settings.models.description": "模型设置将在此处可配置。",
+  "settings.agents.title": "智能体",
+  "settings.agents.description": "智能体设置将在此处可配置。",
+  "settings.commands.title": "命令",
+  "settings.commands.description": "命令设置将在此处可配置。",
+  "settings.mcp.title": "MCP",
+  "settings.mcp.description": "MCP 设置将在此处可配置。",
+
+  "settings.permissions.title": "权限",
+  "settings.permissions.description": "控制服务器默认可以使用哪些工具。",
+  "settings.permissions.section.tools": "工具",
+  "settings.permissions.toast.updateFailed.title": "更新权限失败",
+
+  "settings.permissions.action.allow": "允许",
+  "settings.permissions.action.ask": "询问",
+  "settings.permissions.action.deny": "拒绝",
+
+  "settings.permissions.tool.read.title": "读取",
+  "settings.permissions.tool.read.description": "读取文件(匹配文件路径)",
+  "settings.permissions.tool.edit.title": "编辑",
+  "settings.permissions.tool.edit.description": "修改文件,包括编辑、写入、补丁和多重编辑",
+  "settings.permissions.tool.glob.title": "Glob",
+  "settings.permissions.tool.glob.description": "使用 glob 模式匹配文件",
+  "settings.permissions.tool.grep.title": "Grep",
+  "settings.permissions.tool.grep.description": "使用正则表达式搜索文件内容",
+  "settings.permissions.tool.list.title": "列表",
+  "settings.permissions.tool.list.description": "列出目录中的文件",
+  "settings.permissions.tool.bash.title": "Bash",
+  "settings.permissions.tool.bash.description": "运行 shell 命令",
+  "settings.permissions.tool.task.title": "Task",
+  "settings.permissions.tool.task.description": "启动子智能体",
+  "settings.permissions.tool.skill.title": "Skill",
+  "settings.permissions.tool.skill.description": "按名称加载技能",
+  "settings.permissions.tool.lsp.title": "LSP",
+  "settings.permissions.tool.lsp.description": "运行语言服务器查询",
+  "settings.permissions.tool.todoread.title": "读取待办",
+  "settings.permissions.tool.todoread.description": "读取待办列表",
+  "settings.permissions.tool.todowrite.title": "更新待办",
+  "settings.permissions.tool.todowrite.description": "更新待办列表",
+  "settings.permissions.tool.webfetch.title": "Web Fetch",
+  "settings.permissions.tool.webfetch.description": "从 URL 获取内容",
+  "settings.permissions.tool.websearch.title": "Web Search",
+  "settings.permissions.tool.websearch.description": "搜索网页",
+  "settings.permissions.tool.codesearch.title": "Code Search",
+  "settings.permissions.tool.codesearch.description": "在网上搜索代码",
+  "settings.permissions.tool.external_directory.title": "外部目录",
+  "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件",
+  "settings.permissions.tool.doom_loop.title": "Doom Loop",
+  "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用",
+
   "workspace.new": "新建工作区",
   "workspace.type.local": "本地",
   "workspace.type.sandbox": "沙盒",