From 879bc449fb0ca4bad3a3ba8e174ec2292fbc0504 Mon Sep 17 00:00:00 2001 From: Eric-Guo Date: Mon, 5 Jan 2026 10:36:57 +0800 Subject: [PATCH 01/11] New language switch enabled --- packages/app/src/app.tsx | 3 + packages/app/src/context/language.tsx | 214 ++++++++++++++++++++++++++ packages/app/src/locales/en.json | 74 +++++++++ packages/app/src/locales/zh.json | 74 +++++++++ packages/app/src/pages/layout.tsx | 66 ++++++-- 5 files changed, 415 insertions(+), 16 deletions(-) create mode 100644 packages/app/src/context/language.tsx create mode 100644 packages/app/src/locales/en.json create mode 100644 packages/app/src/locales/zh.json 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/context/language.tsx b/packages/app/src/context/language.tsx new file mode 100644 index 00000000000..90bc49f61c0 --- /dev/null +++ b/packages/app/src/context/language.tsx @@ -0,0 +1,214 @@ +import { createContext, createSignal, useContext, type Accessor } from "solid-js" + +export type Language = "en" | "zh" + +export interface LanguageContextType { + language: Accessor + setLanguage: (language: Language) => void + t: (key: string, params?: Record) => string +} + +const LanguageContext = createContext() + +export function useLanguage(): LanguageContextType { + const context = useContext(LanguageContext) + if (!context) { + throw new Error("useLanguage must be used within a LanguageProvider") + } + return context +} + +export function LanguageProvider(props: { children: any }) { + const [language, setLanguage] = createSignal( + (localStorage.getItem("opencode-language") as Language) ?? "en", + ) + + function setLanguageWithStorage(lang: Language) { + setLanguage(lang) + localStorage.setItem("opencode-language", lang) + } + + async function loadTranslations(lang: Language) { + const translations = await import(`../locales/${lang}.json`) + return translations.default + } + + function t(key: string, params?: Record): string { + const lang = language() + const keys = key.split(".") + + // Simple fallback mechanism - in a real implementation you'd want to preload translations + const translations: Record = { + en: { + common: { + loading: "Loading...", + save: "Save", + cancel: "Cancel", + delete: "Delete", + edit: "Edit", + close: "Close", + open: "Open", + new: "New", + settings: "Settings", + search: "Search", + clear: "Clear", + confirm: "Confirm", + retry: "Retry", + back: "Back", + next: "Next", + previous: "Previous", + done: "Done", + }, + sidebar: { + toggle: "Toggle sidebar", + openProject: "Open project", + connectProvider: "Connect provider", + shareFeedback: "Share feedback", + newSession: "New session", + archiveSession: "Archive session", + editProject: "Edit project", + closeProject: "Close project", + gettingStarted: "Getting started", + gettingStartedDesc1: "OpenCode includes free models so you can start immediately.", + gettingStartedDesc2: "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", + }, + session: { + new: "New session", + previous: "Previous session", + next: "Next session", + archive: "Archive session", + noFiles: "No files changed", + filesChanged: "{count} file(s) changed", + }, + theme: { + switched: "Theme switched", + colorScheme: "Color scheme", + cycle: "Cycle theme", + cycleColorScheme: "Cycle color scheme", + }, + dialog: { + selectDirectory: "Select directory", + selectFile: "Select file", + editProject: "Edit project", + connectProvider: "Connect provider", + selectServer: "Select server", + selectModel: "Select model", + }, + notification: { + permissionRequired: "Permission required", + updateAvailable: "Update available", + updateDesc: "A new version of OpenCode ({version}) is now available to install.", + installAndRestart: "Install and restart", + notYet: "Not yet", + goToSession: "Go to session", + dismiss: "Dismiss", + }, + server: { switch: "Switch server" }, + provider: { connect: "Connect provider" }, + project: { open: "Open project" }, + }, + zh: { + common: { + loading: "加载中...", + save: "保存", + cancel: "取消", + delete: "删除", + edit: "编辑", + close: "关闭", + open: "打开", + new: "新建", + settings: "设置", + search: "搜索", + clear: "清除", + confirm: "确认", + retry: "重试", + back: "返回", + next: "下一个", + previous: "上一个", + done: "完成", + }, + sidebar: { + toggle: "切换侧边栏", + openProject: "打开项目", + connectProvider: "连接提供商", + shareFeedback: "分享反馈", + newSession: "新会话", + archiveSession: "归档会话", + editProject: "编辑项目", + closeProject: "关闭项目", + gettingStarted: "开始使用", + gettingStartedDesc1: "OpenCode 包含免费模型,您可以立即开始使用。", + gettingStartedDesc2: "连接任何提供商以使用模型,包括 Claude、GPT、Gemini 等。", + }, + session: { + new: "新会话", + previous: "上一个会话", + next: "下一个会话", + archive: "归档会话", + noFiles: "无文件更改", + filesChanged: "{count} 个文件已更改", + }, + theme: { + switched: "主题已切换", + colorScheme: "配色方案", + cycle: "循环切换主题", + cycleColorScheme: "循环切换配色方案", + }, + dialog: { + selectDirectory: "选择目录", + selectFile: "选择文件", + editProject: "编辑项目", + connectProvider: "连接提供商", + selectServer: "选择服务器", + selectModel: "选择模型", + }, + notification: { + permissionRequired: "需要权限", + updateAvailable: "有可用更新", + updateDesc: "OpenCode 的新版本 ({version}) 现在可以安装。", + installAndRestart: "安装并重启", + notYet: "暂不", + goToSession: "前往会话", + dismiss: "忽略", + }, + server: { switch: "切换服务器" }, + provider: { connect: "连接提供商" }, + project: { open: "打开项目" }, + }, + } + + let value: any = translations[lang] + for (const k of keys) { + value = value?.[k] + } + + if (typeof value !== "string") { + // Fallback to English if translation not found + value = translations.en + for (const k of keys) { + value = value?.[k] + } + } + + if (typeof value !== "string") { + return key // Return the key if no translation found + } + + // Replace parameters in the translation + if (params) { + return value.replace(/\{(\w+)\}/g, (match: string, param: string) => { + return params[param]?.toString() || match + }) + } + + return value + } + + const value: LanguageContextType = { + language, + setLanguage: setLanguageWithStorage, + t, + } + + return {props.children} +} diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json new file mode 100644 index 00000000000..dca54a72572 --- /dev/null +++ b/packages/app/src/locales/en.json @@ -0,0 +1,74 @@ +{ + "common": { + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "open": "Open", + "new": "New", + "settings": "Settings", + "search": "Search", + "clear": "Clear", + "confirm": "Confirm", + "retry": "Retry", + "back": "Back", + "next": "Next", + "previous": "Previous", + "done": "Done" + }, + "sidebar": { + "toggle": "Toggle sidebar", + "openProject": "Open project", + "connectProvider": "Connect provider", + "shareFeedback": "Share feedback", + "newSession": "New session", + "archiveSession": "Archive session", + "editProject": "Edit project", + "closeProject": "Close project", + "gettingStarted": "Getting started", + "gettingStartedDesc1": "OpenCode includes free models so you can start immediately.", + "gettingStartedDesc2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc." + }, + "session": { + "new": "New session", + "previous": "Previous session", + "next": "Next session", + "archive": "Archive session", + "noFiles": "No files changed", + "filesChanged": "{count} file{plural, s} changed" + }, + "theme": { + "switched": "Theme switched", + "colorScheme": "Color scheme", + "cycle": "Cycle theme", + "cycleColorScheme": "Cycle color scheme" + }, + "dialog": { + "selectDirectory": "Select directory", + "selectFile": "Select file", + "editProject": "Edit project", + "connectProvider": "Connect provider", + "selectServer": "Select server", + "selectModel": "Select model" + }, + "notification": { + "permissionRequired": "Permission required", + "updateAvailable": "Update available", + "updateDesc": "A new version of OpenCode ({version}) is now available to install.", + "installAndRestart": "Install and restart", + "notYet": "Not yet", + "goToSession": "Go to session", + "dismiss": "Dismiss" + }, + "server": { + "switch": "Switch server" + }, + "provider": { + "connect": "Connect provider" + }, + "project": { + "open": "Open project" + } +} diff --git a/packages/app/src/locales/zh.json b/packages/app/src/locales/zh.json new file mode 100644 index 00000000000..657cfa9a9c4 --- /dev/null +++ b/packages/app/src/locales/zh.json @@ -0,0 +1,74 @@ +{ + "common": { + "loading": "加载中...", + "save": "保存", + "cancel": "取消", + "delete": "删除", + "edit": "编辑", + "close": "关闭", + "open": "打开", + "new": "新建", + "settings": "设置", + "search": "搜索", + "clear": "清除", + "confirm": "确认", + "retry": "重试", + "back": "返回", + "next": "下一个", + "previous": "上一个", + "done": "完成" + }, + "sidebar": { + "toggle": "切换侧边栏", + "openProject": "打开项目", + "connectProvider": "连接提供商", + "shareFeedback": "分享反馈", + "newSession": "新会话", + "archiveSession": "归档会话", + "editProject": "编辑项目", + "closeProject": "关闭项目", + "gettingStarted": "开始使用", + "gettingStartedDesc1": "OpenCode 包含免费模型,您可以立即开始使用。", + "gettingStartedDesc2": "连接任何提供商以使用模型,包括 Claude、GPT、Gemini 等。" + }, + "session": { + "new": "新会话", + "previous": "上一个会话", + "next": "下一个会话", + "archive": "归档会话", + "noFiles": "无文件更改", + "filesChanged": "{count} 个文件已更改" + }, + "theme": { + "switched": "主题已切换", + "colorScheme": "配色方案", + "cycle": "循环切换主题", + "cycleColorScheme": "循环切换配色方案" + }, + "dialog": { + "selectDirectory": "选择目录", + "selectFile": "选择文件", + "editProject": "编辑项目", + "connectProvider": "连接提供商", + "selectServer": "选择服务器", + "selectModel": "选择模型" + }, + "notification": { + "permissionRequired": "需要权限", + "updateAvailable": "有可用更新", + "updateDesc": "OpenCode 的新版本 ({version}) 现在可以安装。", + "installAndRestart": "安装并重启", + "notYet": "暂不", + "goToSession": "前往会话", + "dismiss": "忽略" + }, + "server": { + "switch": "切换服务器" + }, + "provider": { + "connect": "连接提供商" + }, + "project": { + "open": "打开项目" + } +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5f5954c9037..9ba7fa0dae7 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -66,6 +66,7 @@ import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogEditProject } from "@/components/dialog-edit-project" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" +import { useLanguage, type Language } from "@/context/language" export default function Layout(props: ParentProps) { const [store, setStore, , ready] = persisted( @@ -105,6 +106,7 @@ export default function Layout(props: ParentProps) { const dialog = useDialog() const command = useCommand() const theme = useTheme() + const language = useLanguage() const initialDir = params.dir const availableThemeEntries = createMemo(() => Object.entries(theme.themes())) const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] @@ -113,6 +115,11 @@ export default function Layout(props: ParentProps) { light: "Light", dark: "Dark", } + const languages: Language[] = ["en", "zh"] + const languageLabel: Record = { + en: "English", + zh: "中文", + } const [editor, setEditor] = createStore({ active: "" as string, @@ -235,7 +242,7 @@ export default function Layout(props: ParentProps) { theme.setTheme(nextThemeId) const nextTheme = theme.themes()[nextThemeId] showToast({ - title: "Theme switched", + title: language.t("theme.switched"), description: nextTheme?.name ?? nextThemeId, }) } @@ -248,7 +255,7 @@ export default function Layout(props: ParentProps) { const next = colorSchemeOrder[nextIndex] theme.setColorScheme(next) showToast({ - title: "Color scheme", + title: language.t("theme.colorScheme"), description: colorSchemeLabel[next], }) } @@ -264,18 +271,18 @@ export default function Layout(props: ParentProps) { toastId = showToast({ persistent: true, icon: "download", - title: "Update available", - description: `A new version of OpenCode (${version}) is now available to install.`, + title: language.t("notification.updateAvailable"), + description: language.t("notification.updateDesc", { version: version || "" }), actions: [ { - label: "Install and restart", + label: language.t("notification.installAndRestart"), onClick: async () => { await platform.update!() await platform.restart!() }, }, { - label: "Not yet", + label: language.t("notification.notYet"), onClick: "dismiss", }, ], @@ -291,7 +298,7 @@ export default function Layout(props: ParentProps) { onMount(() => { const alerts = { "permission.asked": { - title: "Permission required", + title: language.t("notification.permissionRequired"), icon: "checklist" as const, description: (sessionTitle: string, projectName: string) => `${sessionTitle} in ${projectName} needs permission`, @@ -345,11 +352,11 @@ export default function Layout(props: ParentProps) { description, actions: [ { - label: "Go to session", + label: language.t("notification.goToSession"), onClick: () => navigate(href), }, { - label: "Dismiss", + label: language.t("notification.dismiss"), onClick: "dismiss", }, ], @@ -777,21 +784,21 @@ export default function Layout(props: ParentProps) { const commands: CommandOption[] = [ { id: "sidebar.toggle", - title: "Toggle sidebar", + title: language.t("sidebar.toggle"), category: "View", keybind: "mod+b", onSelect: () => layout.sidebar.toggle(), }, { id: "project.open", - title: "Open project", + title: language.t("project.open"), category: "Project", keybind: "mod+o", onSelect: () => chooseProject(), }, { id: "provider.connect", - title: "Connect provider", + title: language.t("provider.connect"), category: "Provider", onSelect: () => connectProvider(), }, @@ -803,7 +810,7 @@ export default function Layout(props: ParentProps) { }, { id: "session.previous", - title: "Previous session", + title: language.t("session.previous"), category: "Session", keybind: "alt+arrowup", onSelect: () => navigateSessionByOffset(-1), @@ -817,7 +824,7 @@ export default function Layout(props: ParentProps) { }, { id: "session.archive", - title: "Archive session", + title: language.t("session.archive"), category: "Session", keybind: "mod+shift+backspace", disabled: !params.dir || !params.id, @@ -828,7 +835,7 @@ export default function Layout(props: ParentProps) { }, { id: "theme.cycle", - title: "Cycle theme", + title: language.t("theme.cycle"), category: "Theme", keybind: "mod+shift+t", onSelect: () => cycleTheme(1), @@ -869,6 +876,33 @@ export default function Layout(props: ParentProps) { }) } + // Add language switching commands + commands.push({ + id: "language.cycle", + title: "Cycle language", + category: "Language", + keybind: "mod+shift+l", + onSelect: () => { + const currentIndex = languages.indexOf(language.language()) + const nextIndex = (currentIndex + 1) % languages.length + const nextLang = languages[nextIndex] + language.setLanguage(nextLang) + showToast({ + title: "Language switched", + description: languageLabel[nextLang], + }) + }, + }) + + for (const lang of languages) { + commands.push({ + id: `language.set.${lang}`, + title: `Use language: ${languageLabel[lang]}`, + category: "Language", + onSelect: () => language.setLanguage(lang), + }) + } + return commands }) @@ -2076,7 +2110,7 @@ export default function Layout(props: ParentProps) { layout.mobileSidebar.hide() }} > - New session + {language.t("session.new")}
From 3a9bc90046563aead84376411c6b2b0d525c6992 Mon Sep 17 00:00:00 2001 From: Eric-Guo Date: Sun, 18 Jan 2026 11:57:45 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8Expanded=20i18n=20coverage=20acr?= =?UTF-8?q?oss=20the=20app=20and=20centralized=20translations=20by=20switc?= =?UTF-8?q?hing=20the=20provider=20to=20load=20from=20the=20locale=20JSON?= =?UTF-8?q?=20files,=20then=20replaced=20many=20user=E2=80=91facing=20stri?= =?UTF-8?q?ngs=20with=20language.t=20keys=20and=20added=20matching=20entri?= =?UTF-8?q?es=20in=20the=20locale=20files.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dialog-connect-provider.tsx | 70 +++-- .../src/components/dialog-edit-project.tsx | 16 +- packages/app/src/components/dialog-fork.tsx | 8 +- .../src/components/dialog-manage-models.tsx | 8 +- .../components/dialog-select-directory.tsx | 8 +- .../app/src/components/dialog-select-file.tsx | 17 +- .../app/src/components/dialog-select-mcp.tsx | 19 +- .../components/dialog-select-model-unpaid.tsx | 18 +- .../src/components/dialog-select-model.tsx | 20 +- .../src/components/dialog-select-provider.tsx | 16 +- .../src/components/dialog-select-server.tsx | 26 +- packages/app/src/components/prompt-input.tsx | 74 +++-- .../src/components/session-context-usage.tsx | 10 +- .../src/components/session-mcp-indicator.tsx | 6 +- .../session/session-context-tab.tsx | 52 ++-- .../src/components/session/session-header.tsx | 12 +- .../components/session/session-new-view.tsx | 16 +- packages/app/src/components/titlebar.tsx | 4 +- packages/app/src/context/file.tsx | 4 +- packages/app/src/context/global-sync.tsx | 7 +- packages/app/src/context/language.tsx | 144 +-------- packages/app/src/context/local.tsx | 4 +- packages/app/src/locales/en.json | 289 +++++++++++++++++- packages/app/src/locales/zh.json | 289 +++++++++++++++++- packages/app/src/pages/error.tsx | 22 +- packages/app/src/pages/home.tsx | 18 +- packages/app/src/pages/layout.tsx | 119 ++++---- packages/app/src/pages/session.tsx | 150 ++++----- packages/app/tsconfig.json | 2 +- 29 files changed, 975 insertions(+), 473 deletions(-) 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..202dc375c4a 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -7,28 +7,30 @@ 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() return ( - + x?.id} items={providers.all} filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + groupBy={(x) => (popularProviders.includes(x.id) ? 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 === language.t("provider.popular") && b.category !== language.t("provider.popular")) return -1 + if (b.category === language.t("provider.popular") && a.category !== language.t("provider.popular")) return 1 return 0 }} onSelect={(x) => { @@ -41,10 +43,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")}
- +