Adam 1 месяц назад
Родитель
Сommit
850d50eb64

+ 2 - 2
packages/app/src/components/dialog-connect-provider.tsx

@@ -33,7 +33,7 @@ export function DialogConnectProvider(props: { provider: string }) {
       globalSync.data.provider_auth[props.provider] ?? [
         {
           type: "api",
-          label: "API key",
+          label: language.t("provider.connect.method.apiKey"),
         },
       ],
   )
@@ -245,7 +245,7 @@ export function DialogConnectProvider(props: { provider: string }) {
                           <div class="text-14-regular text-text-base">
                             {language.t("provider.connect.opencodeZen.visit.prefix")}
                             <Link href="https://opencode.ai/zen" tabIndex={-1}>
-                              opencode.ai/zen
+                              {language.t("provider.connect.opencodeZen.visit.link")}
                             </Link>
                             {language.t("provider.connect.opencodeZen.visit.suffix")}
                           </div>

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

@@ -77,7 +77,9 @@ export const DialogSelectMcp: Component = () => {
                     <span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
                   </Show>
                   <Show when={loading() === i.name}>
-                    <span class="text-11-regular text-text-weak">...</span>
+                    <span class="text-11-regular text-text-weak">
+                      {language.t("common.loading.ellipsis")}
+                    </span>
                   </Show>
                 </div>
                 <Show when={error()}>

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

@@ -115,8 +115,8 @@ export const ModelSelectorPopover: Component<{
                 variant="ghost"
                 iconSize="normal"
                 class="size-6"
-                aria-label="Manage models"
-                title="Manage models"
+                aria-label={language.t("dialog.model.manage")}
+                title={language.t("dialog.model.manage")}
                 onClick={handleManage}
               />
             }

+ 41 - 19
packages/app/src/components/model-tooltip.tsx

@@ -1,4 +1,5 @@
 import { Show, type Component } from "solid-js"
+import { useLanguage } from "@/context/language"
 
 type InputKey = "text" | "image" | "audio" | "video" | "pdf"
 type InputMap = Record<InputKey, boolean>
@@ -22,23 +23,31 @@ type ModelInfo = {
   }
 }
 
-function sourceName(model: ModelInfo) {
-  const value = `${model.id} ${model.name}`.toLowerCase()
-
-  if (/claude|anthropic/.test(value)) return "Anthropic"
-  if (/gpt|o[1-4]|codex|openai/.test(value)) return "OpenAI"
-  if (/gemini|palm|bard|google/.test(value)) return "Google"
-  if (/grok|xai/.test(value)) return "xAI"
-  if (/llama|meta/.test(value)) return "Meta"
+export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
+  const language = useLanguage()
+  const sourceName = (model: ModelInfo) => {
+    const value = `${model.id} ${model.name}`.toLowerCase()
 
-  return model.provider.name
-}
+    if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic")
+    if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai")
+    if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google")
+    if (/grok|xai/.test(value)) return language.t("model.provider.xai")
+    if (/llama|meta/.test(value)) return language.t("model.provider.meta")
 
-export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
+    return model.provider.name
+  }
+  const inputLabel = (value: string) => {
+    if (value === "text") return language.t("model.input.text")
+    if (value === "image") return language.t("model.input.image")
+    if (value === "audio") return language.t("model.input.audio")
+    if (value === "video") return language.t("model.input.video")
+    if (value === "pdf") return language.t("model.input.pdf")
+    return value
+  }
   const title = () => {
     const tags: Array<string> = []
-    if (props.latest) tags.push("Latest")
-    if (props.free) tags.push("Free")
+    if (props.latest) tags.push(language.t("model.tag.latest"))
+    if (props.free) tags.push(language.t("model.tag.free"))
     const suffix = tags.length ? ` (${tags.join(", ")})` : ""
     return `${sourceName(props.model)} ${props.model.name}${suffix}`
   }
@@ -46,22 +55,35 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
     if (props.model.capabilities) {
       const input = props.model.capabilities.input
       const order: Array<InputKey> = ["text", "image", "audio", "video", "pdf"]
-      const entries = order.filter((key) => input[key])
+      const entries = order.filter((key) => input[key]).map((key) => inputLabel(key))
       return entries.length ? entries.join(", ") : undefined
     }
-    return props.model.modalities?.input?.join(", ")
+    const raw = props.model.modalities?.input
+    if (!raw) return
+    const entries = raw.map((value) => inputLabel(value))
+    return entries.length ? entries.join(", ") : undefined
   }
   const reasoning = () => {
-    if (props.model.capabilities) return props.model.capabilities.reasoning ? "Allows reasoning" : "No reasoning"
-    return props.model.reasoning ? "Allows reasoning" : "No reasoning"
+    if (props.model.capabilities)
+      return props.model.capabilities.reasoning
+        ? language.t("model.tooltip.reasoning.allowed")
+        : language.t("model.tooltip.reasoning.none")
+    return props.model.reasoning
+      ? language.t("model.tooltip.reasoning.allowed")
+      : language.t("model.tooltip.reasoning.none")
   }
-  const context = () => `Context limit ${props.model.limit.context.toLocaleString()}`
+  const context = () =>
+    language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
 
   return (
     <div class="flex flex-col gap-1 py-1">
       <div class="text-13-medium">{title()}</div>
       <Show when={inputs()}>
-        {(value) => <div class="text-12-regular text-text-invert-base">Allows: {value()}</div>}
+        {(value) => (
+          <div class="text-12-regular text-text-invert-base">
+            {language.t("model.tooltip.allows", { inputs: value() })}
+          </div>
+        )}
       </Show>
       <div class="text-12-regular text-text-invert-base">{reasoning()}</div>
       <div class="text-12-regular text-text-invert-base">{context()}</div>

+ 3 - 1
packages/app/src/components/prompt-input.tsx

@@ -1696,7 +1696,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   <Match when={working()}>
                     <div class="flex items-center gap-2">
                       <span>{language.t("prompt.action.stop")}</span>
-                      <span class="text-icon-base text-12-medium text-[10px]!">ESC</span>
+                      <span class="text-icon-base text-12-medium text-[10px]!">
+                        {language.t("common.key.esc")}
+                      </span>
                     </div>
                   </Match>
                   <Match when={true}>

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

@@ -150,11 +150,11 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
             >
               <DropdownMenu.Item onSelect={edit}>
                 <Icon name="edit" class="w-4 h-4 mr-2" />
-                Rename
+                {language.t("common.rename")}
               </DropdownMenu.Item>
               <DropdownMenu.Item onSelect={close}>
                 <Icon name="close" class="w-4 h-4 mr-2" />
-                Close
+                {language.t("common.close")}
               </DropdownMenu.Item>
             </DropdownMenu.Content>
           </DropdownMenu.Portal>

+ 24 - 19
packages/app/src/components/settings-general.tsx

@@ -29,18 +29,19 @@ export const SettingsGeneral: Component = () => {
   )
 
   const fontOptions = [
-    { value: "ibm-plex-mono", label: "IBM Plex Mono" },
-    { value: "cascadia-code", label: "Cascadia Code" },
-    { value: "fira-code", label: "Fira Code" },
-    { value: "hack", label: "Hack" },
-    { value: "inconsolata", label: "Inconsolata" },
-    { value: "intel-one-mono", label: "Intel One Mono" },
-    { value: "jetbrains-mono", label: "JetBrains Mono" },
-    { value: "meslo-lgs", label: "Meslo LGS" },
-    { value: "roboto-mono", label: "Roboto Mono" },
-    { value: "source-code-pro", label: "Source Code Pro" },
-    { value: "ubuntu-mono", label: "Ubuntu Mono" },
-  ]
+    { value: "ibm-plex-mono", label: "font.option.ibmPlexMono" },
+    { value: "cascadia-code", label: "font.option.cascadiaCode" },
+    { value: "fira-code", label: "font.option.firaCode" },
+    { value: "hack", label: "font.option.hack" },
+    { value: "inconsolata", label: "font.option.inconsolata" },
+    { value: "intel-one-mono", label: "font.option.intelOneMono" },
+    { value: "jetbrains-mono", label: "font.option.jetbrainsMono" },
+    { value: "meslo-lgs", label: "font.option.mesloLgs" },
+    { value: "roboto-mono", label: "font.option.robotoMono" },
+    { value: "source-code-pro", label: "font.option.sourceCodePro" },
+    { value: "ubuntu-mono", label: "font.option.ubuntuMono" },
+  ] as const
+  const fontOptionsList = [...fontOptions]
 
   const soundOptions = [...SOUND_OPTIONS]
 
@@ -137,17 +138,21 @@ export const SettingsGeneral: Component = () => {
               description={language.t("settings.general.row.font.description")}
             >
               <Select
-                options={fontOptions}
-                current={fontOptions.find((o) => o.value === settings.appearance.font())}
+                options={fontOptionsList}
+                current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
                 value={(o) => o.value}
-                label={(o) => o.label}
+                label={(o) => language.t(o.label)}
                 onSelect={(option) => option && settings.appearance.setFont(option.value)}
                 variant="secondary"
                 size="small"
                 triggerVariant="settings"
                 triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
               >
-                {(option) => <span style={{ "font-family": monoFontFamily(option?.value) }}>{option?.label}</span>}
+                {(option) => (
+                  <span style={{ "font-family": monoFontFamily(option?.value) }}>
+                    {option ? language.t(option.label) : ""}
+                  </span>
+                )}
               </Select>
             </SettingsRow>
           </div>
@@ -203,7 +208,7 @@ export const SettingsGeneral: Component = () => {
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.agent())}
                 value={(o) => o.id}
-                label={(o) => o.label}
+                label={(o) => language.t(o.label)}
                 onHighlight={(option) => {
                   if (!option) return
                   playSound(option.src)
@@ -227,7 +232,7 @@ export const SettingsGeneral: Component = () => {
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
                 value={(o) => o.id}
-                label={(o) => o.label}
+                label={(o) => language.t(o.label)}
                 onHighlight={(option) => {
                   if (!option) return
                   playSound(option.src)
@@ -251,7 +256,7 @@ export const SettingsGeneral: Component = () => {
                 options={soundOptions}
                 current={soundOptions.find((o) => o.id === settings.sounds.errors())}
                 value={(o) => o.id}
-                label={(o) => o.label}
+                label={(o) => language.t(o.label)}
                 onHighlight={(option) => {
                   if (!option) return
                   playSound(option.src)

+ 3 - 3
packages/app/src/components/titlebar.tsx

@@ -94,7 +94,7 @@ export function Titlebar() {
               variant="ghost"
               class="size-8 rounded-md"
               onClick={layout.mobileSidebar.toggle}
-              aria-label="Toggle menu"
+              aria-label={language.t("sidebar.menu.toggle")}
             />
           </div>
         </Show>
@@ -105,7 +105,7 @@ export function Titlebar() {
               variant="ghost"
               class="size-8 rounded-md"
               onClick={layout.mobileSidebar.toggle}
-              aria-label="Toggle menu"
+              aria-label={language.t("sidebar.menu.toggle")}
             />
           </div>
         </Show>
@@ -119,7 +119,7 @@ export function Titlebar() {
             variant="ghost"
             class="group/sidebar-toggle size-6 p-0"
             onClick={layout.sidebar.toggle}
-            aria-label="Toggle sidebar"
+            aria-label={language.t("command.sidebar.toggle")}
             aria-expanded={layout.sidebar.opened()}
           >
             <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">

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

@@ -14,6 +14,7 @@ export const dict = {
   "command.category.agent": "Agent",
   "command.category.permissions": "Permissions",
   "command.category.workspace": "Workspace",
+  "command.category.settings": "Settings",
 
   "theme.scheme.system": "System",
   "theme.scheme.light": "Light",
@@ -23,6 +24,7 @@ export const dict = {
   "command.project.open": "Open project",
   "command.provider.connect": "Connect provider",
   "command.server.switch": "Switch server",
+  "command.settings.open": "Open settings",
   "command.session.previous": "Previous session",
   "command.session.next": "Next session",
   "command.session.archive": "Archive session",
@@ -115,6 +117,7 @@ export const dict = {
   "provider.connect.opencodeZen.line2":
     "With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.",
   "provider.connect.opencodeZen.visit.prefix": "Visit ",
+  "provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
   "provider.connect.opencodeZen.visit.suffix": " to collect your API key.",
   "provider.connect.oauth.code.visit.prefix": "Visit ",
   "provider.connect.oauth.code.visit.link": "this link",
@@ -134,9 +137,24 @@ export const dict = {
 
   "model.tag.free": "Free",
   "model.tag.latest": "Latest",
+  "model.provider.anthropic": "Anthropic",
+  "model.provider.openai": "OpenAI",
+  "model.provider.google": "Google",
+  "model.provider.xai": "xAI",
+  "model.provider.meta": "Meta",
+  "model.input.text": "text",
+  "model.input.image": "image",
+  "model.input.audio": "audio",
+  "model.input.video": "video",
+  "model.input.pdf": "pdf",
+  "model.tooltip.allows": "Allows: {{inputs}}",
+  "model.tooltip.reasoning.allowed": "Allows reasoning",
+  "model.tooltip.reasoning.none": "No reasoning",
+  "model.tooltip.context": "Context limit {{limit}}",
 
   "common.search.placeholder": "Search",
   "common.loading": "Loading",
+  "common.loading.ellipsis": "...",
   "common.cancel": "Cancel",
   "common.submit": "Submit",
   "common.save": "Save",
@@ -371,6 +389,7 @@ export const dict = {
   "session.messages.loadingEarlier": "Loading earlier messages...",
   "session.messages.loadEarlier": "Load earlier messages",
   "session.messages.loading": "Loading messages...",
+  "session.messages.jumpToLatest": "Jump to latest",
 
   "session.context.addToContext": "Add {{selection}} to context",
 
@@ -402,6 +421,8 @@ export const dict = {
   "terminal.loading": "Loading terminal...",
   "terminal.title": "Terminal",
   "terminal.title.numbered": "Terminal {{number}}",
+  "terminal.connectionLost.title": "Connection Lost",
+  "terminal.connectionLost.description": "The terminal connection was interrupted. This can happen when the server restarts.",
 
   "common.closeTab": "Close tab",
   "common.dismiss": "Dismiss",
@@ -414,7 +435,9 @@ export const dict = {
   "common.close": "Close",
   "common.edit": "Edit",
   "common.loadMore": "Load more",
+  "common.key.esc": "ESC",
 
+  "sidebar.menu.toggle": "Toggle menu",
   "sidebar.settings": "Settings",
   "sidebar.help": "Help",
   "sidebar.workspaces.enable": "Enable workspaces",
@@ -441,6 +464,29 @@ export const dict = {
   "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",
+  "font.option.ibmPlexMono": "IBM Plex Mono",
+  "font.option.cascadiaCode": "Cascadia Code",
+  "font.option.firaCode": "Fira Code",
+  "font.option.hack": "Hack",
+  "font.option.inconsolata": "Inconsolata",
+  "font.option.intelOneMono": "Intel One Mono",
+  "font.option.jetbrainsMono": "JetBrains Mono",
+  "font.option.mesloLgs": "Meslo LGS",
+  "font.option.robotoMono": "Roboto Mono",
+  "font.option.sourceCodePro": "Source Code Pro",
+  "font.option.ubuntuMono": "Ubuntu Mono",
+  "sound.option.staplebops01": "Boopy",
+  "sound.option.staplebops02": "Beepy",
+  "sound.option.staplebops03": "Staplebops 03",
+  "sound.option.staplebops04": "Staplebops 04",
+  "sound.option.staplebops05": "Staplebops 05",
+  "sound.option.staplebops06": "Staplebops 06",
+  "sound.option.staplebops07": "Staplebops 07",
+  "sound.option.nope01": "Nope 01",
+  "sound.option.nope02": "Nope 02",
+  "sound.option.nope03": "Oopsie",
+  "sound.option.nope04": "Nope 04",
+  "sound.option.nope05": "Nope 05",
 
   "settings.general.notifications.agent.title": "Agent",
   "settings.general.notifications.agent.description":

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

@@ -847,8 +847,8 @@ export default function Layout(props: ParentProps) {
       },
       {
         id: "settings.open",
-        title: "Open settings",
-        category: "Settings",
+        title: language.t("command.settings.open"),
+        category: language.t("command.category.settings"),
         keybind: "mod+comma",
         onSelect: () => openSettings(),
       },
@@ -1573,12 +1573,12 @@ export default function Layout(props: ParentProps) {
             keybind={command.keybind("session.archive")}
             gutter={8}
           >
-            <IconButton
-              icon="archive"
-              variant="ghost"
-              onClick={() => archiveSession(props.session)}
-              aria-label="Archive session"
-            />
+              <IconButton
+                icon="archive"
+                variant="ghost"
+                onClick={() => archiveSession(props.session)}
+                aria-label={language.t("command.session.archive")}
+              />
           </TooltipKeybind>
         </div>
       </div>
@@ -1759,7 +1759,7 @@ export default function Layout(props: ParentProps) {
                         icon="dot-grid"
                         variant="ghost"
                         class="size-6 rounded-md"
-                        aria-label="More options"
+                        aria-label={language.t("common.moreOptions")}
                       />
                     </Tooltip>
                     <DropdownMenu.Portal>
@@ -1808,7 +1808,7 @@ export default function Layout(props: ParentProps) {
                       variant="ghost"
                       class="size-6 rounded-md"
                       onClick={() => navigate(`/${slug()}/session`)}
-                      aria-label="New session"
+                      aria-label={language.t("command.session.new")}
                     />
                   </TooltipKeybind>
                 </div>
@@ -2122,7 +2122,7 @@ export default function Layout(props: ParentProps) {
                     variant="ghost"
                     size="large"
                     onClick={chooseProject}
-                    aria-label="Open project"
+                    aria-label={language.t("command.project.open")}
                   />
                 </Tooltip>
               </div>
@@ -2142,7 +2142,7 @@ export default function Layout(props: ParentProps) {
                 variant="ghost"
                 size="large"
                 onClick={openSettings}
-                aria-label="Settings"
+                aria-label={language.t("sidebar.settings")}
               />
             </TooltipKeybind>
             <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value={language.t("sidebar.help")}>
@@ -2151,7 +2151,7 @@ export default function Layout(props: ParentProps) {
                 variant="ghost"
                 size="large"
                 onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
-                aria-label="Help"
+                aria-label={language.t("sidebar.help")}
               />
             </Tooltip>
           </div>
@@ -2202,7 +2202,7 @@ export default function Layout(props: ParentProps) {
                           icon="dot-grid"
                           variant="ghost"
                           class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
-                          aria-label="Project options"
+                          aria-label={language.t("common.moreOptions")}
                         />
                         <DropdownMenu.Portal>
                           <DropdownMenu.Content class="mt-1">

+ 6 - 4
packages/app/src/pages/session.tsx

@@ -1366,7 +1366,7 @@ export default function Page() {
                               window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
                             }}
                           >
-                            Jump to latest
+                            {language.t("session.messages.jumpToLatest")}
                           </Button>
                         </div>
                       </Show>
@@ -2017,9 +2017,11 @@ export default function Page() {
                                 style={{ color: "rgba(239, 68, 68, 0.8)" }}
                               />
                               <div class="text-center" style={{ color: "rgba(255, 255, 255, 0.7)" }}>
-                                <div class="text-14-semibold mb-1">Connection Lost</div>
+                                <div class="text-14-semibold mb-1">
+                                  {language.t("terminal.connectionLost.title")}
+                                </div>
                                 <div class="text-12-regular" style={{ color: "rgba(255, 255, 255, 0.5)" }}>
-                                  The terminal connection was interrupted. This can happen when the server restarts.
+                                  {language.t("terminal.connectionLost.description")}
                                 </div>
                               </div>
                               <button
@@ -2037,7 +2039,7 @@ export default function Page() {
                                 }
                                 onClick={() => setDismissed(true)}
                               >
-                                Dismiss
+                                {language.t("common.dismiss")}
                               </button>
                             </div>
                           </Show>

+ 12 - 12
packages/app/src/utils/sound.ts

@@ -12,18 +12,18 @@ import staplebops06 from "@opencode-ai/ui/audio/staplebops-06.aac"
 import staplebops07 from "@opencode-ai/ui/audio/staplebops-07.aac"
 
 export const SOUND_OPTIONS = [
-  { id: "staplebops-01", label: "Boopy", src: staplebops01 },
-  { id: "staplebops-02", label: "Beepy", src: staplebops02 },
-  { id: "staplebops-03", label: "Staplebops 03", src: staplebops03 },
-  { id: "staplebops-04", label: "Staplebops 04", src: staplebops04 },
-  { id: "staplebops-05", label: "Staplebops 05", src: staplebops05 },
-  { id: "staplebops-06", label: "Staplebops 06", src: staplebops06 },
-  { id: "staplebops-07", label: "Staplebops 07", src: staplebops07 },
-  { id: "nope-01", label: "Nope 01", src: nope01 },
-  { id: "nope-02", label: "Nope 02", src: nope02 },
-  { id: "nope-03", label: "Oopsie", src: nope03 },
-  { id: "nope-04", label: "Nope 04", src: nope04 },
-  { id: "nope-05", label: "Nope 05", src: nope05 },
+  { id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 },
+  { id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 },
+  { id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 },
+  { id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 },
+  { id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 },
+  { id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 },
+  { id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 },
+  { id: "nope-01", label: "sound.option.nope01", src: nope01 },
+  { id: "nope-02", label: "sound.option.nope02", src: nope02 },
+  { id: "nope-03", label: "sound.option.nope03", src: nope03 },
+  { id: "nope-04", label: "sound.option.nope04", src: nope04 },
+  { id: "nope-05", label: "sound.option.nope05", src: nope05 },
 ] as const
 
 export type SoundOption = (typeof SOUND_OPTIONS)[number]