diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx
index 63f3c5dbe..66c5bd0b8 100644
--- a/packages/app/src/components/dialog-connect-provider.tsx
+++ b/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 }) {
{language.t("provider.connect.opencodeZen.visit.prefix")}
- opencode.ai/zen
+ {language.t("provider.connect.opencodeZen.visit.link")}
{language.t("provider.connect.opencodeZen.visit.suffix")}
diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx
index 25ef8df01..b91699561 100644
--- a/packages/app/src/components/dialog-select-mcp.tsx
+++ b/packages/app/src/components/dialog-select-mcp.tsx
@@ -77,7 +77,9 @@ export const DialogSelectMcp: Component = () => {
{language.t("mcp.status.disabled")}
- ...
+
+ {language.t("common.loading.ellipsis")}
+
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index a030e952b..4fd8af1b3 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/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}
/>
}
diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx
index 14b4ba799..54550cf76 100644
--- a/packages/app/src/components/model-tooltip.tsx
+++ b/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
@@ -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"
-
- return model.provider.name
-}
-
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()
+
+ 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")
+
+ 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 = []
- 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 = ["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 (
{title()}
- {(value) => Allows: {value()}
}
+ {(value) => (
+
+ {language.t("model.tooltip.allows", { inputs: value() })}
+
+ )}
{reasoning()}
{context()}
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 35a74f43e..d80abf0c3 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -1696,7 +1696,9 @@ export const PromptInput: Component
= (props) => {
{language.t("prompt.action.stop")}
- ESC
+
+ {language.t("common.key.esc")}
+
diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
index 63efa54a8..03f07fa56 100644
--- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx
+++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
@@ -150,11 +150,11 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
>
- Rename
+ {language.t("common.rename")}
- Close
+ {language.t("common.close")}
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index c356b269b..69d180f9d 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/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")}
>
@@ -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)
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx
index a5103d90d..877fe8492 100644
--- a/packages/app/src/components/titlebar.tsx
+++ b/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")}
/>
@@ -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")}
/>
@@ -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()}
>
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 8e4c9aa50..27361a2e6 100644
--- a/packages/app/src/i18n/en.ts
+++ b/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":
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index e00928cfc..1318217b5 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/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}
>
- archiveSession(props.session)}
- aria-label="Archive session"
- />
+ archiveSession(props.session)}
+ aria-label={language.t("command.session.archive")}
+ />
@@ -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")}
/>
@@ -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")}
/>
@@ -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")}
/>
@@ -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")}
/>
@@ -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")}
/>
@@ -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")}
/>
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 8d6122819..48b059459 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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")}
@@ -2017,9 +2017,11 @@ export default function Page() {
style={{ color: "rgba(239, 68, 68, 0.8)" }}
/>
-
Connection Lost
+
+ {language.t("terminal.connectionLost.title")}
+
- The terminal connection was interrupted. This can happen when the server restarts.
+ {language.t("terminal.connectionLost.description")}
diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts
index e8db0bf7b..fa65f1d61 100644
--- a/packages/app/src/utils/sound.ts
+++ b/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]