diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d03d10d0ea7..5d1cce43d47 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -12,6 +12,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme" import { GlobalSyncProvider } from "@/context/global-sync" import { PermissionProvider } from "@/context/permission" import { LayoutProvider } from "@/context/layout" +import { LanguageProvider } from "@/context/language" import { GlobalSDKProvider } from "@/context/global-sdk" import { ServerProvider, useServer } from "@/context/server" import { TerminalProvider } from "@/context/terminal" @@ -78,6 +79,7 @@ export function AppInterface(props: { defaultUrl?: string }) { return ( + + ) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 789a5d3b748..5437adb35c6 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -17,6 +17,7 @@ import { Link } from "@/components/link" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" import { DialogSelectModel } from "./dialog-select-model" import { DialogSelectProvider } from "./dialog-select-provider" @@ -25,13 +26,14 @@ export function DialogConnectProvider(props: { provider: string }) { const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const platform = usePlatform() + const language = useLanguage() const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => globalSync.data.provider_auth[props.provider] ?? [ { type: "api", - label: "API key", + label: language.t("provider.apiKey"), }, ], ) @@ -112,8 +114,8 @@ export function DialogConnectProvider(props: { provider: string }) { showToast({ variant: "success", icon: "circle-check", - title: `${provider().name} connected`, - description: `${provider().name} models are now available to use.`, + title: language.t("provider.connectedTitle", { provider: provider().name }), + description: language.t("provider.connectedDescription", { provider: provider().name }), }) } @@ -142,16 +144,18 @@ export function DialogConnectProvider(props: { provider: string }) {
- Login with Claude Pro/Max + {language.t("provider.loginClaude")} - Connect {provider().name} + {language.t("provider.connectTo", { provider: provider().name })}
-
Select login method for {provider().name}.
+
+ {language.t("provider.selectMethod", { provider: provider().name })} +
{ @@ -179,7 +183,7 @@ export function DialogConnectProvider(props: { provider: string }) {
- Authorization in progress... + {language.t("provider.authInProgress")}
@@ -187,7 +191,7 @@ export function DialogConnectProvider(props: { provider: string }) {
- Authorization failed: {store.error} + {language.t("provider.authFailed", { error: store.error ?? "" })}
@@ -206,7 +210,7 @@ export function DialogConnectProvider(props: { provider: string }) { const apiKey = formData.get("apiKey") as string if (!apiKey?.trim()) { - setFormStore("error", "API key is required") + setFormStore("error", language.t("provider.apiKeyRequired")) return } @@ -227,25 +231,23 @@ export function DialogConnectProvider(props: { provider: string }) {
- OpenCode Zen gives you access to a curated set of reliable optimized models for coding - agents. + {language.t("provider.opencodeZenIntro")}
- With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more. + {language.t("provider.opencodeZenModels")}
- Visit{" "} + {language.t("provider.opencodeZenVisit")}{" "} opencode.ai/zen {" "} - to collect your API key. + {language.t("provider.opencodeZenCollect")}
- Enter your {provider().name} API key to connect your account and use {provider().name} models - in OpenCode. + {language.t("provider.enterApiKey", { provider: provider().name })}
@@ -253,8 +255,8 @@ export function DialogConnectProvider(props: { provider: string }) {
@@ -292,7 +294,7 @@ export function DialogConnectProvider(props: { provider: string }) { const code = formData.get("code") as string if (!code?.trim()) { - setFormStore("error", "Authorization code is required") + setFormStore("error", language.t("provider.authCodeRequired")) return } @@ -306,21 +308,22 @@ export function DialogConnectProvider(props: { provider: string }) { await complete() return } - setFormStore("error", "Invalid authorization code") + setFormStore("error", language.t("provider.authCodeInvalid")) } return (
- Visit this link to collect your authorization - code to connect your account and use {provider().name} models in OpenCode. + {language.t("provider.visitLinkToAuthorize")}{" "} + {language.t("provider.thisLink")}{" "} + {language.t("provider.collectAuthCode", { provider: provider().name })}
@@ -361,13 +364,20 @@ export function DialogConnectProvider(props: { provider: string }) { return (
- Visit this link and enter the code below to - connect your account and use {provider().name} models in OpenCode. + {language.t("provider.visitLinkToAuthorize")}{" "} + {language.t("provider.thisLink")}{" "} + {language.t("provider.enterCodeBelow", { provider: provider().name })}
- +
- Waiting for authorization... + {language.t("provider.waitingForAuth")}
) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index a2dc7b623c7..466c58809b8 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/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 { getFilename } from "@opencode-ai/util/path" import { Avatar } from "@opencode-ai/ui/avatar" +import { useLanguage } from "@/context/language" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export function DialogEditProject(props: { project: LocalProject }) { const dialog = useDialog() const globalSDK = useGlobalSDK() + const language = useLanguage() const folderName = createMemo(() => getFilename(props.project.worktree)) const defaultName = createMemo(() => props.project.name || folderName()) @@ -81,20 +83,20 @@ export function DialogEditProject(props: { project: LocalProject }) { } return ( - +
setStore("name", v)} />
- +
setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
} > - Project icon + {language.t("project.iconAlt")}
- +
{(color) => ( @@ -208,10 +210,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 472a1994f13..4d1a853e3c5 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" +import { useLanguage } from "@/context/language" interface ForkableMessage { id: string @@ -27,6 +28,7 @@ export const DialogFork: Component = () => { const sdk = useSDK() const prompt = usePrompt() const dialog = useDialog() + const language = useLanguage() const messages = createMemo((): ForkableMessage[] => { const sessionID = params.id @@ -73,11 +75,11 @@ export const DialogFork: Component = () => { } return ( - + x.id} items={messages} filterKeys={["text"]} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 66d12528891..b40f6a1a246 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/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 { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" +import { useLanguage } from "@/context/language" export const DialogManageModels: Component = () => { const local = useLocal() + const language = useLanguage() return ( - + `${x?.provider?.id}:${x?.id}`} items={local.model.list()} filterKeys={["provider.name", "name", "id"]} diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index bf4a1f9edd4..991f7276e46 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/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 { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" interface DialogSelectDirectoryProps { title?: string @@ -17,6 +18,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const sync = useGlobalSync() const sdk = useGlobalSDK() const dialog = useDialog() + const language = useLanguage() const home = createMemo(() => sync.data.path.home) const root = createMemo(() => sync.data.path.home || sync.data.path.directory) @@ -81,10 +83,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { } return ( - + x} onSelect={(path) => { diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 0e8d69628bb..e4880eb3df2 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/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 { useLayout } from "@/context/layout" import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" type EntryType = "command" | "file" @@ -18,7 +19,7 @@ type Entry = { title: string description?: string keybind?: string - category: "Commands" | "Files" + category: string option?: CommandOption path?: string } @@ -29,6 +30,7 @@ export function DialogSelectFile() { const file = useFile() const dialog = useDialog() const params = useParams() + const language = useLanguage() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -49,7 +51,7 @@ export function DialogSelectFile() { title: option.title, description: option.description, keybind: option.keybind, - category: "Commands", + category: language.t("commandPalette.commands"), option, }) @@ -57,7 +59,7 @@ export function DialogSelectFile() { id: "file:" + path, type: "file", title: path, - category: "Files", + category: language.t("commandPalette.files"), path, }) @@ -136,8 +138,13 @@ export function DialogSelectFile() { return ( item.id} filterKeys={["title", "description", "category"]} diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index c29cd827e3b..ffe0e512c3c 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/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 { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { useLanguage } from "@/context/language" export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() + const language = useLanguage() const [loading, setLoading] = createSignal(null) const items = createMemo(() => @@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => { const totalCount = createMemo(() => items().length) return ( - + x?.name ?? ""} items={items} filterKeys={["name", "status"]} @@ -60,16 +65,16 @@ export const DialogSelectMcp: Component = () => {
{i.name} - connected + {language.t("status.connected")} - failed + {language.t("status.failed")} - needs auth + {language.t("status.needsAuth")} - disabled + {language.t("status.disabled")} ... diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 24ec8092deb..ea0979a2a0c 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/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 { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" +import { useLanguage } from "@/context/language" export const DialogSelectModelUnpaid: Component = () => { const local = useLocal() const dialog = useDialog() const providers = useProviders() + const language = useLanguage() let listRef: ListRef | undefined const handleKey = (e: KeyboardEvent) => { @@ -30,9 +32,9 @@ export const DialogSelectModelUnpaid: Component = () => { }) return ( - +
-
Free models provided by OpenCode
+
{language.t("model.freeProvided")}
(listRef = ref)} items={local.model.list} @@ -48,9 +50,9 @@ export const DialogSelectModelUnpaid: Component = () => { {(i) => (
{i.name} - Free + {language.t("tag.free")} - Latest + {language.t("tag.latest")}
)} @@ -61,7 +63,7 @@ export const DialogSelectModelUnpaid: Component = () => {
-
Add more models from popular providers
+
{language.t("model.addMore")}
{ {i.name} - Recommended + {language.t("tag.recommended")} -
Connect with Claude Pro/Max or API key
+
{language.t("provider.connectClaude")}
)} @@ -99,7 +101,7 @@ export const DialogSelectModelUnpaid: Component = () => { dialog.show(() => ) }} > - View all providers + {language.t("provider.viewAll")}
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index d54f9369af1..ecabf93274d 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/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 { DialogSelectProvider } from "./dialog-select-provider" import { DialogManageModels } from "./dialog-manage-models" +import { useLanguage } from "@/context/language" const ModelList: Component<{ provider?: string @@ -16,6 +17,7 @@ const ModelList: Component<{ onSelect: () => void }> = (props) => { const local = useLocal() + const language = useLanguage() const models = createMemo(() => local.model @@ -27,8 +29,8 @@ const ModelList: Component<{ return ( `${x.provider.id}:${x.id}`} items={models} current={local.model.current()} @@ -55,10 +57,10 @@ const ModelList: Component<{
{i.name} - Free + {language.t("tag.free")} - Latest + {language.t("tag.latest")}
)} @@ -71,13 +73,14 @@ export const ModelSelectorPopover: Component<{ children: JSX.Element }> = (props) => { const [open, setOpen] = createSignal(false) + const language = useLanguage() return ( {props.children} - Select model + {language.t("dialog.selectModel")} setOpen(false)} class="p-1" /> @@ -87,10 +90,11 @@ export const ModelSelectorPopover: Component<{ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { const dialog = useDialog() + const language = useLanguage() return ( = (props) => { tabIndex={-1} onClick={() => dialog.show(() => )} > - Connect provider + {language.t("provider.connect")} } > @@ -108,7 +112,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={() => dialog.show(() => )} > - Manage models + {language.t("model.manageTitle")} ) diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 5bbde5d41a2..61d342572d5 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -7,28 +7,35 @@ import { Tag } from "@opencode-ai/ui/tag" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" import { DialogConnectProvider } from "./dialog-connect-provider" +import { useLanguage } from "@/context/language" export const DialogSelectProvider: Component = () => { const dialog = useDialog() const providers = useProviders() + const language = useLanguage() + const popularCategory = "popular" + const otherCategory = "other" return ( - + x?.id} items={providers.all} filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + groupBy={(x) => (popularProviders.includes(x.id) ? popularCategory : otherCategory)} + groupLabel={(category) => + category === popularCategory ? language.t("provider.popular") : language.t("provider.other") + } sortBy={(a, b) => { if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return a.name.localeCompare(b.name) }} sortGroupsBy={(a, b) => { - if (a.category === "Popular" && b.category !== "Popular") return -1 - if (b.category === "Popular" && a.category !== "Popular") return 1 + if (a.category === popularCategory && b.category !== popularCategory) return -1 + if (b.category === popularCategory && a.category !== popularCategory) return 1 return 0 }} onSelect={(x) => { @@ -41,10 +48,10 @@ export const DialogSelectProvider: Component = () => { {i.name} - Recommended + {language.t("tag.recommended")} -
Connect with Claude Pro/Max or API key
+
{language.t("provider.connectClaude")}
)} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 90f37212888..3ebeff0e8dc 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/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 { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { useNavigate } from "@solidjs/router" +import { useLanguage } from "@/context/language" type ServerStatus = { healthy: boolean; version?: string } @@ -30,6 +31,7 @@ export function DialogSelectServer() { const dialog = useDialog() const server = useServer() const platform = usePlatform() + const language = useLanguage() const [store, setStore] = createStore({ url: "", adding: false, @@ -109,7 +111,7 @@ export function DialogSelectServer() { setStore("adding", false) if (!result.healthy) { - setStore("error", "Could not connect to server") + setStore("error", language.t("server.connectFailed")) return } @@ -122,11 +124,11 @@ export function DialogSelectServer() { } return ( - +
x} current={current()} @@ -168,14 +170,14 @@ export function DialogSelectServer() {
-

Add a server

+

{language.t("server.add")}

@@ -197,9 +199,9 @@ export function DialogSelectServer() {
-

Default server

+

{language.t("server.default")}

- Connect to this server on app launch instead of starting a local server. Requires restart. + {language.t("server.defaultDescription")}

@@ -208,7 +210,7 @@ export function DialogSelectServer() { fallback={ No server selected} + fallback={{language.t("server.noneSelected")}} > } @@ -234,7 +236,7 @@ export function DialogSelectServer() { defaultUrlActions.refetch() }} > - Clear + {language.t("common.clear")}
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 56bbdc8cb55..fbf4bdae41c 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -51,6 +51,7 @@ import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client" import { Binary } from "@opencode-ai/util/binary" import { showToast } from "@opencode-ai/ui/toast" @@ -118,6 +119,7 @@ export const PromptInput: Component = (props) => { const providers = useProviders() const command = useCommand() const permission = usePermission() + const language = useLanguage() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement let scrollRef!: HTMLDivElement @@ -998,8 +1000,8 @@ export const PromptInput: Component = (props) => { const currentAgent = local.agent.current() if (!currentModel || !currentAgent) { showToast({ - title: "Select an agent and model", - description: "Choose an agent and model before sending a prompt.", + title: language.t("prompt.selectAgentModelTitle"), + description: language.t("prompt.selectAgentModelDescription"), }) return } @@ -1010,7 +1012,7 @@ export const PromptInput: Component = (props) => { if (data?.message) return data.message } if (err instanceof Error) return err.message - return "Request failed" + return language.t("common.requestFailed") } addToHistory(currentPrompt, mode) @@ -1031,7 +1033,7 @@ export const PromptInput: Component = (props) => { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to create worktree", + title: language.t("prompt.failedCreateWorktree"), description: errorMessage(err), }) return undefined @@ -1039,8 +1041,8 @@ export const PromptInput: Component = (props) => { if (!createdWorktree?.directory) { showToast({ - title: "Failed to create worktree", - description: "Request failed", + title: language.t("prompt.failedCreateWorktree"), + description: language.t("common.requestFailed"), }) return } @@ -1115,7 +1117,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send shell command", + title: language.t("prompt.failedSendShell"), description: errorMessage(err), }) restoreInput() @@ -1147,7 +1149,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send command", + title: language.t("prompt.failedSendCommand"), description: errorMessage(err), }) restoreInput() @@ -1315,7 +1317,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send prompt", + title: language.t("prompt.failedSendPrompt"), description: errorMessage(err), }) removeOptimisticMessage() @@ -1339,7 +1341,7 @@ export const PromptInput: Component = (props) => { 0} - fallback={
No matching results
} + fallback={
{language.t("prompt.noMatches")}
} > {(item) => ( @@ -1385,7 +1387,7 @@ export const PromptInput: Component = (props) => { 0} - fallback={
No matching commands
} + fallback={
{language.t("prompt.noCommands")}
} > {(cmd) => ( @@ -1407,7 +1409,7 @@ export const PromptInput: Component = (props) => {
- custom + {language.t("prompt.customCommand")} @@ -1436,7 +1438,7 @@ export const PromptInput: Component = (props) => {
- Drop images or PDFs here + {language.t("prompt.dropFiles")}
@@ -1449,7 +1451,7 @@ export const PromptInput: Component = (props) => {
{getDirectory(path())} {getFilename(path())} - active + {language.t("prompt.active")}
= (props) => { onClick={() => prompt.context.addActive()} > - Include active file + {language.t("prompt.includeActiveFile")} @@ -1561,8 +1563,8 @@ export const PromptInput: Component = (props) => {
{store.mode === "shell" - ? "Enter shell command..." - : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} + ? language.t("prompt.shellPlaceholder") + : language.t("prompt.askAnything", { example: PLACEHOLDERS[store.placeholder] })}
@@ -1572,12 +1574,16 @@ export const PromptInput: Component = (props) => {
- Shell - esc to exit + {language.t("prompt.shellMode")} + {language.t("prompt.shellExitHint")}
- +