([\s\S]*?)<\/highlight>/g
+ let match
+
+ while ((match = regex.exec(body)) !== null) {
+ const source = match[1]
+ const content = match[2]
+
+ const titleMatch = content.match(/([^<]+)<\/h2>/)
+ const pMatch = content.match(/ ([^<]+)<\/p>/)
+ const imgMatch = content.match(/ {
+ if (videoMatch) return { type: "video", src: videoMatch[1] } satisfies HighlightMedia
+ if (imgMatch) {
+ return {
+ type: "image",
+ src: imgMatch[3],
+ width: imgMatch[1],
+ height: imgMatch[2],
+ } satisfies HighlightMedia
+ }
+ })()
+
+ if (!titleMatch || !media) continue
+
+ const item: HighlightItem = {
+ title: titleMatch[1],
+ description: pMatch?.[2] || "",
+ shortDescription: pMatch?.[1],
+ media,
+ }
+
+ if (!groups.has(source)) groups.set(source, [])
+ groups.get(source)!.push(item)
+ }
+
+ return Array.from(groups.entries()).map(([source, items]) => ({ source, items }))
+}
+
+function parseMarkdown(body: string) {
+ const lines = body.split("\n")
+ const sections: { title: string; items: string[] }[] = []
+ let current: { title: string; items: string[] } | null = null
+ let skip = false
+
+ for (const line of lines) {
+ if (line.startsWith("## ")) {
+ if (current) sections.push(current)
+ current = { title: line.slice(3).trim(), items: [] }
+ skip = false
+ continue
+ }
+
+ if (line.startsWith("**Thank you")) {
+ skip = true
+ continue
+ }
+
+ if (line.startsWith("- ") && !skip) current?.items.push(line.slice(2).trim())
+ }
+
+ if (current) sections.push(current)
+ return { sections, highlights: parseHighlights(body) }
+}
diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts
index a23f205032..f06c1be9b4 100644
--- a/packages/console/app/src/routes/changelog.json.ts
+++ b/packages/console/app/src/routes/changelog.json.ts
@@ -1,114 +1,30 @@
-type Release = {
- tag_name: string
- name: string
- body: string
- published_at: string
- html_url: string
+import { loadChangelog } from "~/lib/changelog"
+
+const cors = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
}
-type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
-
-type HighlightItem = {
- title: string
- description: string
- shortDescription?: string
- media: HighlightMedia
-}
-
-type HighlightGroup = {
- source: string
- items: HighlightItem[]
-}
-
-function parseHighlights(body: string): HighlightGroup[] {
- const groups = new Map()
- const regex = /([\s\S]*?)<\/highlight>/g
- let match
-
- while ((match = regex.exec(body)) !== null) {
- const source = match[1]
- const content = match[2]
-
- const titleMatch = content.match(/([^<]+)<\/h2>/)
- const pMatch = content.match(/ ([^<]+)<\/p>/)
- const imgMatch = content.match(/ ({ source, items }))
-}
-
-function parseMarkdown(body: string) {
- const lines = body.split("\n")
- const sections: { title: string; items: string[] }[] = []
- let current: { title: string; items: string[] } | null = null
- let skip = false
-
- for (const line of lines) {
- if (line.startsWith("## ")) {
- if (current) sections.push(current)
- const title = line.slice(3).trim()
- current = { title, items: [] }
- skip = false
- } else if (line.startsWith("**Thank you")) {
- skip = true
- } else if (line.startsWith("- ") && !skip) {
- current?.items.push(line.slice(2).trim())
- }
- }
- if (current) sections.push(current)
-
- const highlights = parseHighlights(body)
-
- return { sections, highlights }
-}
+const ok = "public, max-age=1, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400"
+const error = "public, max-age=1, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400"
export async function GET() {
- const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
+ const result = await loadChangelog().catch(() => ({ ok: false, releases: [] }))
+
+ return new Response(JSON.stringify({ releases: result.releases }), {
+ status: result.ok ? 200 : 503,
headers: {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "OpenCode-Console",
+ "Content-Type": "application/json",
+ "Cache-Control": result.ok ? ok : error,
+ ...cors,
},
})
-
- if (!response.ok) {
- return { releases: [] }
- }
-
- const releases = (await response.json()) as Release[]
-
- return {
- releases: releases.map((release) => {
- const parsed = parseMarkdown(release.body || "")
- return {
- tag: release.tag_name,
- name: release.name,
- date: release.published_at,
- url: release.html_url,
- highlights: parsed.highlights,
- sections: parsed.sections,
- }
- }),
- }
+}
+
+export async function OPTIONS() {
+ return new Response(null, {
+ status: 200,
+ headers: cors,
+ })
}
diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css
index e5dd3ff74b..950762ef6b 100644
--- a/packages/console/app/src/routes/changelog/index.css
+++ b/packages/console/app/src/routes/changelog/index.css
@@ -371,7 +371,7 @@
top: 80px;
align-self: start;
background: var(--color-background);
- padding: 8px 0;
+ padding: 44px 0 8px;
@media (max-width: 50rem) {
position: static;
diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx
index 87e021ec88..dff0a427f7 100644
--- a/packages/console/app/src/routes/changelog/index.tsx
+++ b/packages/console/app/src/routes/changelog/index.tsx
@@ -1,36 +1,14 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
-import { createAsync, query } from "@solidjs/router"
+import { createAsync } from "@solidjs/router"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { config } from "~/config"
+import { changelog } from "~/lib/changelog"
+import type { HighlightGroup } from "~/lib/changelog"
import { For, Show, createSignal } from "solid-js"
-type Release = {
- tag_name: string
- name: string
- body: string
- published_at: string
- html_url: string
-}
-
-const getReleases = query(async () => {
- "use server"
- const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
- headers: {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "OpenCode-Console",
- },
- cf: {
- cacheTtl: 60 * 5,
- cacheEverything: true,
- },
- } as any)
- if (!response.ok) return []
- return response.json() as Promise
-}, "releases.get")
-
function formatDate(dateString: string) {
const date = new Date(dateString)
return date.toLocaleDateString("en-US", {
@@ -40,84 +18,6 @@ function formatDate(dateString: string) {
})
}
-type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
-
-type HighlightItem = {
- title: string
- description: string
- shortDescription?: string
- media: HighlightMedia
-}
-
-type HighlightGroup = {
- source: string
- items: HighlightItem[]
-}
-
-function parseHighlights(body: string): HighlightGroup[] {
- const groups = new Map()
- const regex = /([\s\S]*?)<\/highlight>/g
- let match
-
- while ((match = regex.exec(body)) !== null) {
- const source = match[1]
- const content = match[2]
-
- const titleMatch = content.match(/([^<]+)<\/h2>/)
- const pMatch = content.match(/ ([^<]+)<\/p>/)
- const imgMatch = content.match(/ ({ source, items }))
-}
-
-function parseMarkdown(body: string) {
- const lines = body.split("\n")
- const sections: { title: string; items: string[] }[] = []
- let current: { title: string; items: string[] } | null = null
- let skip = false
-
- for (const line of lines) {
- if (line.startsWith("## ")) {
- if (current) sections.push(current)
- const title = line.slice(3).trim()
- current = { title, items: [] }
- skip = false
- } else if (line.startsWith("**Thank you")) {
- skip = true
- } else if (line.startsWith("- ") && !skip) {
- current?.items.push(line.slice(2).trim())
- }
- }
- if (current) sections.push(current)
-
- const highlights = parseHighlights(body)
-
- return { sections, highlights }
-}
-
function ReleaseItem(props: { item: string }) {
const parts = () => {
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
@@ -197,7 +97,8 @@ function CollapsibleSections(props: { sections: { title: string; items: string[]
}
export default function Changelog() {
- const releases = createAsync(() => getReleases())
+ const data = createAsync(() => changelog())
+ const releases = () => data() ?? []
return (
@@ -215,30 +116,34 @@ export default function Changelog() {
+
+
+ No changelog entries found. View JSON
+
+
{(release) => {
- const parsed = () => parseMarkdown(release.body || "")
return (
- {formatDate(release.published_at)}
+ {formatDate(release.date)}
-
0}>
+ 0}>
- {(group) => }
+ {(group) => }
- 0 && parsed().sections.length > 0}>
-
+ 0 && release.sections.length > 0}>
+
-
-
+
+
{(section) => (
{section.title}
@@ -255,9 +160,9 @@ export default function Changelog() {
}}
-
-
+
+
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index cc6b3af99f..49e032339c 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -15,6 +15,7 @@
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
+ "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock
index a41739a697..294d7ad6ce 100644
--- a/packages/desktop/src-tauri/Cargo.lock
+++ b/packages/desktop/src-tauri/Cargo.lock
@@ -3028,6 +3028,8 @@ dependencies = [
"futures",
"gtk",
"listeners",
+ "objc2 0.6.3",
+ "objc2-web-kit",
"reqwest",
"semver",
"serde",
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index 6296b8325f..b875f928b0 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -47,6 +47,10 @@ comrak = { version = "0.50", default-features = false }
gtk = "0.18.2"
webkit2gtk = "=2.0.1"
+[target.'cfg(target_os = "macos")'.dependencies]
+objc2 = "0.6"
+objc2-web-kit = "0.3"
+
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32_Foundation",
diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs
index b019d66b5c..f64beed6a1 100644
--- a/packages/desktop/src-tauri/src/cli.rs
+++ b/packages/desktop/src-tauri/src/cli.rs
@@ -157,6 +157,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
.unwrap()
.args(args.split_whitespace())
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
+ .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir);
@@ -174,6 +175,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
app.shell()
.command(&shell)
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
+ .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args(["-il", "-c", &cmd])
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index e086acd936..dab98f4a00 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -328,7 +328,15 @@ pub fn run() {
.hidden_title(true);
#[cfg(windows)]
- let window_builder = window_builder.decorations(false);
+ let window_builder = window_builder
+ // Some VPNs set a global/system proxy that WebView2 applies even for loopback
+ // connections, which breaks the app's localhost sidecar server.
+ // Note: when setting additional args, we must re-apply wry's default
+ // `--disable-features=...` flags.
+ .additional_browser_args(
+ "--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection",
+ )
+ .decorations(false);
let window = window_builder.build().expect("Failed to create window");
@@ -525,4 +533,4 @@ async fn spawn_local_server(
break Ok(child);
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs
index e3be4a44da..9ffee8aa5c 100644
--- a/packages/desktop/src-tauri/src/main.rs
+++ b/packages/desktop/src-tauri/src/main.rs
@@ -52,7 +52,32 @@ fn configure_display_backend() -> Option {
}
fn main() {
- unsafe { std::env::set_var("NO_PROXY", "127.0.0.1,localhost,::1") };
+ // Ensure loopback connections are never sent through proxy settings.
+ // Some VPNs/proxies set HTTP_PROXY/HTTPS_PROXY/ALL_PROXY without excluding localhost.
+ const LOOPBACK: [&str; 3] = ["127.0.0.1", "localhost", "::1"];
+
+ let upsert = |key: &str| {
+ let mut items = std::env::var(key)
+ .unwrap_or_default()
+ .split(',')
+ .map(|v| v.trim())
+ .filter(|v| !v.is_empty())
+ .map(|v| v.to_string())
+ .collect::>();
+
+ for host in LOOPBACK {
+ if items.iter().any(|v| v.eq_ignore_ascii_case(host)) {
+ continue;
+ }
+ items.push(host.to_string());
+ }
+
+ // Safety: called during startup before any threads are spawned.
+ unsafe { std::env::set_var(key, items.join(",")) };
+ };
+
+ upsert("NO_PROXY");
+ upsert("no_proxy");
#[cfg(target_os = "linux")]
{
diff --git a/packages/desktop/src-tauri/src/markdown.rs b/packages/desktop/src-tauri/src/markdown.rs
index a2a53b2224..c3ca73857e 100644
--- a/packages/desktop/src-tauri/src/markdown.rs
+++ b/packages/desktop/src-tauri/src/markdown.rs
@@ -1,4 +1,43 @@
-use comrak::{markdown_to_html, Options};
+use comrak::{create_formatter, parse_document, Arena, Options, html::ChildRendering, nodes::NodeValue};
+use std::fmt::Write;
+
+create_formatter!(ExternalLinkFormatter, {
+ NodeValue::Link(ref nl) => |context, node, entering| {
+ let skip = context.options.parse.relaxed_autolinks
+ && node.parent().is_some_and(|p| comrak::node_matches!(p, NodeValue::Link(..)));
+ if skip {
+ return Ok(ChildRendering::HTML);
+ }
+
+ if entering {
+ context.write_str("",
+ )?;
+ } else {
+ context.write_str(" ")?;
+ }
+ },
+});
pub fn parse_markdown(input: &str) -> String {
let mut options = Options::default();
@@ -8,7 +47,11 @@ pub fn parse_markdown(input: &str) -> String {
options.extension.autolink = true;
options.render.r#unsafe = true;
- markdown_to_html(input, &options)
+ let arena = Arena::new();
+ let doc = parse_document(&arena, input, &options);
+ let mut html = String::new();
+ ExternalLinkFormatter::format_document(doc, &options, &mut html).unwrap_or_default();
+ html
}
#[tauri::command]
diff --git a/packages/desktop/src-tauri/src/window_customizer.rs b/packages/desktop/src-tauri/src/window_customizer.rs
index cd42fd0299..682f57f247 100644
--- a/packages/desktop/src-tauri/src/window_customizer.rs
+++ b/packages/desktop/src-tauri/src/window_customizer.rs
@@ -29,6 +29,18 @@ impl Plugin for PinchZoomDisablePlugin {
gobject_ffi::g_signal_handlers_destroy(data.as_ptr().cast());
}
}
+
+ #[cfg(target_os = "macos")]
+ unsafe {
+ use objc2::rc::Retained;
+ use objc2_web_kit::WKWebView;
+
+ // Get the WKWebView pointer and disable magnification gestures
+ // This prevents Cmd+Ctrl+scroll and pinch-to-zoom from changing the zoom level
+ let wk_webview: Retained =
+ Retained::retain(_webview.inner().cast()).unwrap();
+ wk_webview.setAllowsMagnification(false);
+ }
});
}
}
diff --git a/packages/desktop/src-tauri/tauri.prod.conf.json b/packages/desktop/src-tauri/tauri.prod.conf.json
index 282db5b26c..7ce4c78420 100644
--- a/packages/desktop/src-tauri/tauri.prod.conf.json
+++ b/packages/desktop/src-tauri/tauri.prod.conf.json
@@ -2,27 +2,6 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCode",
"identifier": "ai.opencode.desktop",
- "app": {
- "windows": [
- {
- "label": "main",
- "create": false,
- "title": "OpenCode",
- "url": "/",
- "decorations": true,
- "dragDropEnabled": false,
- "zoomHotkeysEnabled": true,
- "titleBarStyle": "Overlay",
- "hiddenTitle": true,
- "trafficLightPosition": { "x": 12.0, "y": 18.0 }
- }
- ],
- "withGlobalTauri": true,
- "security": {
- "csp": null
- },
- "macOSPrivateApi": true
- },
"bundle": {
"createUpdaterArtifacts": true,
"icon": [
diff --git a/packages/desktop/src/cli.ts b/packages/desktop/src/cli.ts
index 965ed6ddc0..5a8875cf89 100644
--- a/packages/desktop/src/cli.ts
+++ b/packages/desktop/src/cli.ts
@@ -1,13 +1,15 @@
import { invoke } from "@tauri-apps/api/core"
import { message } from "@tauri-apps/plugin-dialog"
+import { initI18n, t } from "./i18n"
+
export async function installCli(): Promise {
+ await initI18n()
+
try {
const path = await invoke("install_cli")
- await message(`CLI installed to ${path}\n\nRestart your terminal to use the 'opencode' command.`, {
- title: "CLI Installed",
- })
+ await message(t("desktop.cli.installed.message", { path }), { title: t("desktop.cli.installed.title") })
} catch (e) {
- await message(`Failed to install CLI: ${e}`, { title: "Installation Failed" })
+ await message(t("desktop.cli.failed.message", { error: String(e) }), { title: t("desktop.cli.failed.title") })
}
}
diff --git a/packages/desktop/src/i18n/ar.ts b/packages/desktop/src/i18n/ar.ts
new file mode 100644
index 0000000000..c3205cb85e
--- /dev/null
+++ b/packages/desktop/src/i18n/ar.ts
@@ -0,0 +1,30 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...",
+ "desktop.menu.installCli": "تثبيت CLI...",
+ "desktop.menu.reloadWebview": "إعادة تحميل Webview",
+ "desktop.menu.restart": "إعادة تشغيل",
+
+ "desktop.dialog.chooseFolder": "اختر مجلدًا",
+ "desktop.dialog.chooseFile": "اختر ملفًا",
+ "desktop.dialog.saveFile": "حفظ ملف",
+
+ "desktop.updater.checkFailed.title": "فشل التحقق من التحديثات",
+ "desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات",
+ "desktop.updater.none.title": "لا توجد تحديثات متاحة",
+ "desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode",
+ "desktop.updater.downloadFailed.title": "فشل التحديث",
+ "desktop.updater.downloadFailed.message": "فشل تنزيل التحديث",
+ "desktop.updater.downloaded.title": "تم تنزيل التحديث",
+ "desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟",
+ "desktop.updater.installFailed.title": "فشل التحديث",
+ "desktop.updater.installFailed.message": "فشل تثبيت التحديث",
+
+ "desktop.cli.installed.title": "تم تثبيت CLI",
+ "desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
+ "desktop.cli.failed.title": "فشل التثبيت",
+ "desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "فشل تشغيل OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "تعذر بدء تشغيل خادم OpenCode المحلي. أعد تشغيل التطبيق، أو تحقق من إعدادات الشبكة (VPN/proxy) وحاول مرة أخرى.",
+}
diff --git a/packages/desktop/src/i18n/br.ts b/packages/desktop/src/i18n/br.ts
new file mode 100644
index 0000000000..8b5a58756d
--- /dev/null
+++ b/packages/desktop/src/i18n/br.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Verificar atualizações...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recarregar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Escolher uma pasta",
+ "desktop.dialog.chooseFile": "Escolher um arquivo",
+ "desktop.dialog.saveFile": "Salvar arquivo",
+
+ "desktop.updater.checkFailed.title": "Falha ao verificar atualizações",
+ "desktop.updater.checkFailed.message": "Falha ao verificar atualizações",
+ "desktop.updater.none.title": "Nenhuma atualização disponível",
+ "desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode",
+ "desktop.updater.downloadFailed.title": "Falha na atualização",
+ "desktop.updater.downloadFailed.message": "Falha ao baixar a atualização",
+ "desktop.updater.downloaded.title": "Atualização baixada",
+ "desktop.updater.downloaded.prompt":
+ "A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?",
+ "desktop.updater.installFailed.title": "Falha na atualização",
+ "desktop.updater.installFailed.message": "Falha ao instalar a atualização",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.",
+ "desktop.cli.failed.title": "Falha na instalação",
+ "desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "Falha ao iniciar o OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Não foi possível iniciar o servidor local do OpenCode. Reinicie o aplicativo ou verifique suas configurações de rede (VPN/proxy) e tente novamente.",
+}
diff --git a/packages/desktop/src/i18n/da.ts b/packages/desktop/src/i18n/da.ts
new file mode 100644
index 0000000000..73d47db303
--- /dev/null
+++ b/packages/desktop/src/i18n/da.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Tjek for opdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Genindlæs Webview",
+ "desktop.menu.restart": "Genstart",
+
+ "desktop.dialog.chooseFolder": "Vælg en mappe",
+ "desktop.dialog.chooseFile": "Vælg en fil",
+ "desktop.dialog.saveFile": "Gem fil",
+
+ "desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes",
+ "desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer",
+ "desktop.updater.none.title": "Ingen opdatering tilgængelig",
+ "desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode",
+ "desktop.updater.downloadFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen",
+ "desktop.updater.downloaded.title": "Opdatering downloadet",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?",
+ "desktop.updater.installFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere opdateringen",
+
+ "desktop.cli.installed.title": "CLI installeret",
+ "desktop.cli.installed.message":
+ "CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installation mislykkedes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode kunne ikke starte",
+ "desktop.error.serverStartFailed.description":
+ "Den lokale OpenCode-server kunne ikke startes. Genstart appen, eller tjek dine netværksindstillinger (VPN/proxy) og prøv igen.",
+}
diff --git a/packages/desktop/src/i18n/de.ts b/packages/desktop/src/i18n/de.ts
new file mode 100644
index 0000000000..2559d981e2
--- /dev/null
+++ b/packages/desktop/src/i18n/de.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Nach Updates suchen...",
+ "desktop.menu.installCli": "CLI installieren...",
+ "desktop.menu.reloadWebview": "Webview neu laden",
+ "desktop.menu.restart": "Neustart",
+
+ "desktop.dialog.chooseFolder": "Ordner auswählen",
+ "desktop.dialog.chooseFile": "Datei auswählen",
+ "desktop.dialog.saveFile": "Datei speichern",
+
+ "desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen",
+ "desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden",
+ "desktop.updater.none.title": "Kein Update verfügbar",
+ "desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode",
+ "desktop.updater.downloadFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden",
+ "desktop.updater.downloaded.title": "Update heruntergeladen",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?",
+ "desktop.updater.installFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.installFailed.message": "Update konnte nicht installiert werden",
+
+ "desktop.cli.installed.title": "CLI installiert",
+ "desktop.cli.installed.message":
+ "CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.",
+ "desktop.cli.failed.title": "Installation fehlgeschlagen",
+ "desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode konnte nicht gestartet werden",
+ "desktop.error.serverStartFailed.description":
+ "Der lokale OpenCode-Server konnte nicht gestartet werden. Starten Sie die App neu oder überprüfen Sie Ihre Netzwerkeinstellungen (VPN/Proxy) und versuchen Sie es erneut.",
+}
diff --git a/packages/desktop/src/i18n/en.ts b/packages/desktop/src/i18n/en.ts
new file mode 100644
index 0000000000..c2981f519d
--- /dev/null
+++ b/packages/desktop/src/i18n/en.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Check for Updates...",
+ "desktop.menu.installCli": "Install CLI...",
+ "desktop.menu.reloadWebview": "Reload Webview",
+ "desktop.menu.restart": "Restart",
+
+ "desktop.dialog.chooseFolder": "Choose a folder",
+ "desktop.dialog.chooseFile": "Choose a file",
+ "desktop.dialog.saveFile": "Save file",
+
+ "desktop.updater.checkFailed.title": "Update Check Failed",
+ "desktop.updater.checkFailed.message": "Failed to check for updates",
+ "desktop.updater.none.title": "No Update Available",
+ "desktop.updater.none.message": "You are already using the latest version of OpenCode",
+ "desktop.updater.downloadFailed.title": "Update Failed",
+ "desktop.updater.downloadFailed.message": "Failed to download update",
+ "desktop.updater.downloaded.title": "Update Downloaded",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?",
+ "desktop.updater.installFailed.title": "Update Failed",
+ "desktop.updater.installFailed.message": "Failed to install update",
+
+ "desktop.cli.installed.title": "CLI Installed",
+ "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
+ "desktop.cli.failed.title": "Installation Failed",
+ "desktop.cli.failed.message": "Failed to install CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode failed to start",
+ "desktop.error.serverStartFailed.description":
+ "The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy) and try again.",
+}
diff --git a/packages/desktop/src/i18n/es.ts b/packages/desktop/src/i18n/es.ts
new file mode 100644
index 0000000000..d1045a90cf
--- /dev/null
+++ b/packages/desktop/src/i18n/es.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Buscar actualizaciones...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recargar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Elegir una carpeta",
+ "desktop.dialog.chooseFile": "Elegir un archivo",
+ "desktop.dialog.saveFile": "Guardar archivo",
+
+ "desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida",
+ "desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones",
+ "desktop.updater.none.title": "No hay actualizaciones disponibles",
+ "desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode",
+ "desktop.updater.downloadFailed.title": "Actualización fallida",
+ "desktop.updater.downloadFailed.message": "No se pudo descargar la actualización",
+ "desktop.updater.downloaded.title": "Actualización descargada",
+ "desktop.updater.downloaded.prompt":
+ "Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?",
+ "desktop.updater.installFailed.title": "Actualización fallida",
+ "desktop.updater.installFailed.message": "No se pudo instalar la actualización",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.",
+ "desktop.cli.failed.title": "Instalación fallida",
+ "desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode no pudo iniciarse",
+ "desktop.error.serverStartFailed.description":
+ "No se pudo iniciar el servidor local de OpenCode. Reinicia la aplicación o revisa tu configuración de red (VPN/proxy) y vuelve a intentarlo.",
+}
diff --git a/packages/desktop/src/i18n/fr.ts b/packages/desktop/src/i18n/fr.ts
new file mode 100644
index 0000000000..5c574edf53
--- /dev/null
+++ b/packages/desktop/src/i18n/fr.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Vérifier les mises à jour...",
+ "desktop.menu.installCli": "Installer la CLI...",
+ "desktop.menu.reloadWebview": "Recharger la Webview",
+ "desktop.menu.restart": "Redémarrer",
+
+ "desktop.dialog.chooseFolder": "Choisir un dossier",
+ "desktop.dialog.chooseFile": "Choisir un fichier",
+ "desktop.dialog.saveFile": "Enregistrer le fichier",
+
+ "desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour",
+ "desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour",
+ "desktop.updater.none.title": "Aucune mise à jour disponible",
+ "desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode",
+ "desktop.updater.downloadFailed.title": "Échec de la mise à jour",
+ "desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour",
+ "desktop.updater.downloaded.title": "Mise à jour téléchargée",
+ "desktop.updater.downloaded.prompt":
+ "La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?",
+ "desktop.updater.installFailed.title": "Échec de la mise à jour",
+ "desktop.updater.installFailed.message": "Impossible d'installer la mise à jour",
+
+ "desktop.cli.installed.title": "CLI installée",
+ "desktop.cli.installed.message":
+ "CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.",
+ "desktop.cli.failed.title": "Échec de l'installation",
+ "desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}",
+
+ "desktop.error.serverStartFailed.title": "Échec du démarrage d'OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Impossible de démarrer le serveur OpenCode local. Redémarrez l'application ou vérifiez vos paramètres réseau (VPN/proxy) et réessayez.",
+}
diff --git a/packages/desktop/src/i18n/index.ts b/packages/desktop/src/i18n/index.ts
new file mode 100644
index 0000000000..f2496346fc
--- /dev/null
+++ b/packages/desktop/src/i18n/index.ts
@@ -0,0 +1,147 @@
+import * as i18n from "@solid-primitives/i18n"
+import { Store } from "@tauri-apps/plugin-store"
+
+import { dict as desktopEn } from "./en"
+import { dict as desktopZh } from "./zh"
+import { dict as desktopZht } from "./zht"
+import { dict as desktopKo } from "./ko"
+import { dict as desktopDe } from "./de"
+import { dict as desktopEs } from "./es"
+import { dict as desktopFr } from "./fr"
+import { dict as desktopDa } from "./da"
+import { dict as desktopJa } from "./ja"
+import { dict as desktopPl } from "./pl"
+import { dict as desktopRu } from "./ru"
+import { dict as desktopAr } from "./ar"
+import { dict as desktopNo } from "./no"
+import { dict as desktopBr } from "./br"
+
+import { dict as appEn } from "../../../app/src/i18n/en"
+import { dict as appZh } from "../../../app/src/i18n/zh"
+import { dict as appZht } from "../../../app/src/i18n/zht"
+import { dict as appKo } from "../../../app/src/i18n/ko"
+import { dict as appDe } from "../../../app/src/i18n/de"
+import { dict as appEs } from "../../../app/src/i18n/es"
+import { dict as appFr } from "../../../app/src/i18n/fr"
+import { dict as appDa } from "../../../app/src/i18n/da"
+import { dict as appJa } from "../../../app/src/i18n/ja"
+import { dict as appPl } from "../../../app/src/i18n/pl"
+import { dict as appRu } from "../../../app/src/i18n/ru"
+import { dict as appAr } from "../../../app/src/i18n/ar"
+import { dict as appNo } from "../../../app/src/i18n/no"
+import { dict as appBr } from "../../../app/src/i18n/br"
+
+export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
+
+type RawDictionary = typeof appEn & typeof desktopEn
+type Dictionary = i18n.Flatten
+
+const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
+
+function detectLocale(): Locale {
+ if (typeof navigator !== "object") return "en"
+
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
+ for (const language of languages) {
+ if (!language) continue
+ if (language.toLowerCase().startsWith("zh")) {
+ if (language.toLowerCase().includes("hant")) return "zht"
+ return "zh"
+ }
+ if (language.toLowerCase().startsWith("ko")) return "ko"
+ if (language.toLowerCase().startsWith("de")) return "de"
+ if (language.toLowerCase().startsWith("es")) return "es"
+ if (language.toLowerCase().startsWith("fr")) return "fr"
+ if (language.toLowerCase().startsWith("da")) return "da"
+ if (language.toLowerCase().startsWith("ja")) return "ja"
+ if (language.toLowerCase().startsWith("pl")) return "pl"
+ if (language.toLowerCase().startsWith("ru")) return "ru"
+ if (language.toLowerCase().startsWith("ar")) return "ar"
+ if (
+ language.toLowerCase().startsWith("no") ||
+ language.toLowerCase().startsWith("nb") ||
+ language.toLowerCase().startsWith("nn")
+ )
+ return "no"
+ if (language.toLowerCase().startsWith("pt")) return "br"
+ }
+
+ return "en"
+}
+
+function parseLocale(value: unknown): Locale | null {
+ if (!value) return null
+ if (typeof value !== "string") return null
+ if ((LOCALES as readonly string[]).includes(value)) return value as Locale
+ return null
+}
+
+function parseRecord(value: unknown) {
+ if (!value || typeof value !== "object") return null
+ if (Array.isArray(value)) return null
+ return value as Record
+}
+
+function pickLocale(value: unknown): Locale | null {
+ const direct = parseLocale(value)
+ if (direct) return direct
+
+ const record = parseRecord(value)
+ if (!record) return null
+
+ return parseLocale(record.locale)
+}
+
+const base = i18n.flatten({ ...appEn, ...desktopEn })
+
+function build(locale: Locale): Dictionary {
+ if (locale === "en") return base
+ if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) }
+ if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) }
+ if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) }
+ if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) }
+ if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) }
+ if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) }
+ if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) }
+ if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) }
+ if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) }
+ if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
+ if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
+ if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
+ return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
+}
+
+const state = {
+ locale: detectLocale(),
+ dict: base as Dictionary,
+ init: undefined as Promise | undefined,
+}
+
+state.dict = build(state.locale)
+
+const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
+
+export function t(key: keyof Dictionary, params?: Record) {
+ return translate(key, params)
+}
+
+export function initI18n(): Promise {
+ const cached = state.init
+ if (cached) return cached
+
+ const promise = (async () => {
+ const store = await Store.load("opencode.global.dat").catch(() => null)
+ if (!store) return state.locale
+
+ const raw = await store.get("language").catch(() => null)
+ const value = typeof raw === "string" ? JSON.parse(raw) : raw
+ const next = pickLocale(value) ?? state.locale
+
+ state.locale = next
+ state.dict = build(next)
+ return next
+ })().catch(() => state.locale)
+
+ state.init = promise
+ return promise
+}
diff --git a/packages/desktop/src/i18n/ja.ts b/packages/desktop/src/i18n/ja.ts
new file mode 100644
index 0000000000..94681ab069
--- /dev/null
+++ b/packages/desktop/src/i18n/ja.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "アップデートを確認...",
+ "desktop.menu.installCli": "CLI をインストール...",
+ "desktop.menu.reloadWebview": "Webview を再読み込み",
+ "desktop.menu.restart": "再起動",
+
+ "desktop.dialog.chooseFolder": "フォルダーを選択",
+ "desktop.dialog.chooseFile": "ファイルを選択",
+ "desktop.dialog.saveFile": "ファイルを保存",
+
+ "desktop.updater.checkFailed.title": "アップデートの確認に失敗しました",
+ "desktop.updater.checkFailed.message": "アップデートを確認できませんでした",
+ "desktop.updater.none.title": "利用可能なアップデートはありません",
+ "desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています",
+ "desktop.updater.downloadFailed.title": "アップデートに失敗しました",
+ "desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした",
+ "desktop.updater.downloaded.title": "アップデートをダウンロードしました",
+ "desktop.updater.downloaded.prompt":
+ "OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?",
+ "desktop.updater.installFailed.title": "アップデートに失敗しました",
+ "desktop.updater.installFailed.message": "アップデートをインストールできませんでした",
+
+ "desktop.cli.installed.title": "CLI をインストールしました",
+ "desktop.cli.installed.message":
+ "CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。",
+ "desktop.cli.failed.title": "インストールに失敗しました",
+ "desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode の起動に失敗しました",
+ "desktop.error.serverStartFailed.description":
+ "ローカルの OpenCode サーバーを起動できませんでした。アプリを再起動するか、ネットワーク設定 (VPN/proxy) を確認して再試行してください。",
+}
diff --git a/packages/desktop/src/i18n/ko.ts b/packages/desktop/src/i18n/ko.ts
new file mode 100644
index 0000000000..93136f2ddd
--- /dev/null
+++ b/packages/desktop/src/i18n/ko.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "업데이트 확인...",
+ "desktop.menu.installCli": "CLI 설치...",
+ "desktop.menu.reloadWebview": "Webview 새로고침",
+ "desktop.menu.restart": "다시 시작",
+
+ "desktop.dialog.chooseFolder": "폴더 선택",
+ "desktop.dialog.chooseFile": "파일 선택",
+ "desktop.dialog.saveFile": "파일 저장",
+
+ "desktop.updater.checkFailed.title": "업데이트 확인 실패",
+ "desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다",
+ "desktop.updater.none.title": "사용 가능한 업데이트 없음",
+ "desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다",
+ "desktop.updater.downloadFailed.title": "업데이트 실패",
+ "desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다",
+ "desktop.updater.downloaded.title": "업데이트 다운로드 완료",
+ "desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?",
+ "desktop.updater.installFailed.title": "업데이트 실패",
+ "desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다",
+
+ "desktop.cli.installed.title": "CLI 설치됨",
+ "desktop.cli.installed.message":
+ "CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.",
+ "desktop.cli.failed.title": "설치 실패",
+ "desktop.cli.failed.message": "CLI 설치 실패: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 시작 실패",
+ "desktop.error.serverStartFailed.description":
+ "로컬 OpenCode 서버를 시작할 수 없습니다. 앱을 다시 시작하거나 네트워크 설정(VPN/proxy)을 확인한 후 다시 시도하세요.",
+}
diff --git a/packages/desktop/src/i18n/no.ts b/packages/desktop/src/i18n/no.ts
new file mode 100644
index 0000000000..7deb74687d
--- /dev/null
+++ b/packages/desktop/src/i18n/no.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Se etter oppdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Last inn Webview på nytt",
+ "desktop.menu.restart": "Start på nytt",
+
+ "desktop.dialog.chooseFolder": "Velg en mappe",
+ "desktop.dialog.chooseFile": "Velg en fil",
+ "desktop.dialog.saveFile": "Lagre fil",
+
+ "desktop.updater.checkFailed.title": "Oppdateringssjekk mislyktes",
+ "desktop.updater.checkFailed.message": "Kunne ikke se etter oppdateringer",
+ "desktop.updater.none.title": "Ingen oppdatering tilgjengelig",
+ "desktop.updater.none.message": "Du bruker allerede den nyeste versjonen av OpenCode",
+ "desktop.updater.downloadFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke laste ned oppdateringen",
+ "desktop.updater.downloaded.title": "Oppdatering lastet ned",
+ "desktop.updater.downloaded.prompt":
+ "Versjon {{version}} av OpenCode er lastet ned. Vil du installere den og starte på nytt?",
+ "desktop.updater.installFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere oppdateringen",
+
+ "desktop.cli.installed.title": "CLI installert",
+ "desktop.cli.installed.message":
+ "CLI installert til {{path}}\n\nStart terminalen på nytt for å bruke 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installasjon mislyktes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode kunne ikke starte",
+ "desktop.error.serverStartFailed.description":
+ "Den lokale OpenCode-serveren kunne ikke startes. Start appen på nytt, eller sjekk nettverksinnstillingene dine (VPN/proxy) og prøv igjen.",
+}
diff --git a/packages/desktop/src/i18n/pl.ts b/packages/desktop/src/i18n/pl.ts
new file mode 100644
index 0000000000..dac2992ba4
--- /dev/null
+++ b/packages/desktop/src/i18n/pl.ts
@@ -0,0 +1,32 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...",
+ "desktop.menu.installCli": "Zainstaluj CLI...",
+ "desktop.menu.reloadWebview": "Przeładuj Webview",
+ "desktop.menu.restart": "Restartuj",
+
+ "desktop.dialog.chooseFolder": "Wybierz folder",
+ "desktop.dialog.chooseFile": "Wybierz plik",
+ "desktop.dialog.saveFile": "Zapisz plik",
+
+ "desktop.updater.checkFailed.title": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.checkFailed.message": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.none.title": "Brak dostępnych aktualizacji",
+ "desktop.updater.none.message": "Korzystasz już z najnowszej wersji OpenCode",
+ "desktop.updater.downloadFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.downloadFailed.message": "Nie udało się pobrać aktualizacji",
+ "desktop.updater.downloaded.title": "Aktualizacja pobrana",
+ "desktop.updater.downloaded.prompt":
+ "Pobrano wersję {{version}} OpenCode. Czy chcesz ją zainstalować i uruchomić ponownie?",
+ "desktop.updater.installFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.installFailed.message": "Nie udało się zainstalować aktualizacji",
+
+ "desktop.cli.installed.title": "CLI zainstalowane",
+ "desktop.cli.installed.message":
+ "CLI zainstalowane w {{path}}\n\nUruchom ponownie terminal, aby użyć polecenia 'opencode'.",
+ "desktop.cli.failed.title": "Instalacja nie powiodła się",
+ "desktop.cli.failed.message": "Nie udało się zainstalować CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "Nie udało się uruchomić OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Nie udało się uruchomić lokalnego serwera OpenCode. Uruchom ponownie aplikację lub sprawdź ustawienia sieciowe (VPN/proxy) i spróbuj ponownie.",
+}
diff --git a/packages/desktop/src/i18n/ru.ts b/packages/desktop/src/i18n/ru.ts
new file mode 100644
index 0000000000..6e34e1aa7d
--- /dev/null
+++ b/packages/desktop/src/i18n/ru.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Проверить обновления...",
+ "desktop.menu.installCli": "Установить CLI...",
+ "desktop.menu.reloadWebview": "Перезагрузить Webview",
+ "desktop.menu.restart": "Перезапустить",
+
+ "desktop.dialog.chooseFolder": "Выберите папку",
+ "desktop.dialog.chooseFile": "Выберите файл",
+ "desktop.dialog.saveFile": "Сохранить файл",
+
+ "desktop.updater.checkFailed.title": "Не удалось проверить обновления",
+ "desktop.updater.checkFailed.message": "Не удалось проверить обновления",
+ "desktop.updater.none.title": "Обновлений нет",
+ "desktop.updater.none.message": "Вы уже используете последнюю версию OpenCode",
+ "desktop.updater.downloadFailed.title": "Обновление не удалось",
+ "desktop.updater.downloadFailed.message": "Не удалось скачать обновление",
+ "desktop.updater.downloaded.title": "Обновление загружено",
+ "desktop.updater.downloaded.prompt": "Версия OpenCode {{version}} загружена. Хотите установить и перезапустить?",
+ "desktop.updater.installFailed.title": "Обновление не удалось",
+ "desktop.updater.installFailed.message": "Не удалось установить обновление",
+
+ "desktop.cli.installed.title": "CLI установлен",
+ "desktop.cli.installed.message":
+ "CLI установлен в {{path}}\n\nПерезапустите терминал, чтобы использовать команду 'opencode'.",
+ "desktop.cli.failed.title": "Ошибка установки",
+ "desktop.cli.failed.message": "Не удалось установить CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "Не удалось запустить OpenCode",
+ "desktop.error.serverStartFailed.description":
+ "Не удалось запустить локальный сервер OpenCode. Перезапустите приложение или проверьте настройки сети (VPN/proxy) и попробуйте снова.",
+}
diff --git a/packages/desktop/src/i18n/zh.ts b/packages/desktop/src/i18n/zh.ts
new file mode 100644
index 0000000000..3f5fe59d40
--- /dev/null
+++ b/packages/desktop/src/i18n/zh.ts
@@ -0,0 +1,30 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "检查更新...",
+ "desktop.menu.installCli": "安装 CLI...",
+ "desktop.menu.reloadWebview": "重新加载 Webview",
+ "desktop.menu.restart": "重启",
+
+ "desktop.dialog.chooseFolder": "选择文件夹",
+ "desktop.dialog.chooseFile": "选择文件",
+ "desktop.dialog.saveFile": "保存文件",
+
+ "desktop.updater.checkFailed.title": "检查更新失败",
+ "desktop.updater.checkFailed.message": "无法检查更新",
+ "desktop.updater.none.title": "没有可用更新",
+ "desktop.updater.none.message": "你已经在使用最新版本的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失败",
+ "desktop.updater.downloadFailed.message": "无法下载更新",
+ "desktop.updater.downloaded.title": "更新已下载",
+ "desktop.updater.downloaded.prompt": "已下载 OpenCode {{version}} 版本,是否安装并重启?",
+ "desktop.updater.installFailed.title": "更新失败",
+ "desktop.updater.installFailed.message": "无法安装更新",
+
+ "desktop.cli.installed.title": "CLI 已安装",
+ "desktop.cli.installed.message": "CLI 已安装到 {{path}}\n\n重启终端以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安装失败",
+ "desktop.cli.failed.message": "无法安装 CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 启动失败",
+ "desktop.error.serverStartFailed.description":
+ "无法启动本地 OpenCode 服务器。请重启应用,或检查网络设置 (VPN/proxy) 后重试。",
+}
diff --git a/packages/desktop/src/i18n/zht.ts b/packages/desktop/src/i18n/zht.ts
new file mode 100644
index 0000000000..b09bff742c
--- /dev/null
+++ b/packages/desktop/src/i18n/zht.ts
@@ -0,0 +1,30 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "檢查更新...",
+ "desktop.menu.installCli": "安裝 CLI...",
+ "desktop.menu.reloadWebview": "重新載入 Webview",
+ "desktop.menu.restart": "重新啟動",
+
+ "desktop.dialog.chooseFolder": "選擇資料夾",
+ "desktop.dialog.chooseFile": "選擇檔案",
+ "desktop.dialog.saveFile": "儲存檔案",
+
+ "desktop.updater.checkFailed.title": "檢查更新失敗",
+ "desktop.updater.checkFailed.message": "無法檢查更新",
+ "desktop.updater.none.title": "沒有可用更新",
+ "desktop.updater.none.message": "你已在使用最新版的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失敗",
+ "desktop.updater.downloadFailed.message": "無法下載更新",
+ "desktop.updater.downloaded.title": "更新已下載",
+ "desktop.updater.downloaded.prompt": "已下載 OpenCode {{version}} 版本,是否安裝並重新啟動?",
+ "desktop.updater.installFailed.title": "更新失敗",
+ "desktop.updater.installFailed.message": "無法安裝更新",
+
+ "desktop.cli.installed.title": "CLI 已安裝",
+ "desktop.cli.installed.message": "CLI 已安裝到 {{path}}\n\n重新啟動終端機以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安裝失敗",
+ "desktop.cli.failed.message": "無法安裝 CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode 啟動失敗",
+ "desktop.error.serverStartFailed.description":
+ "無法啟動本地 OpenCode 伺服器。請重新啟動應用程式,或檢查網路設定 (VPN/proxy) 後再試一次。",
+}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index fe9e3f92e2..344c6be8d9 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -18,16 +18,17 @@ import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup }
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
+import { initI18n, t } from "./i18n"
import pkg from "../package.json"
import "./styles.css"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
- throw new Error(
- "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
- )
+ throw new Error(t("error.dev.rootNotFound"))
}
+void initI18n()
+
// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements).
// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows.
const originalGetComputedStyle = window.getComputedStyle
@@ -54,7 +55,7 @@ const createPlatform = (password: Accessor): Platform => ({
const result = await open({
directory: true,
multiple: opts?.multiple ?? false,
- title: opts?.title ?? "Choose a folder",
+ title: opts?.title ?? t("desktop.dialog.chooseFolder"),
})
return result
},
@@ -63,14 +64,14 @@ const createPlatform = (password: Accessor): Platform => ({
const result = await open({
directory: false,
multiple: opts?.multiple ?? false,
- title: opts?.title ?? "Choose a file",
+ title: opts?.title ?? t("desktop.dialog.chooseFile"),
})
return result
},
async saveFilePickerDialog(opts) {
const result = await save({
- title: opts?.title ?? "Save file",
+ title: opts?.title ?? t("desktop.dialog.saveFile"),
defaultPath: opts?.defaultPath,
})
return result
@@ -80,6 +81,14 @@ const createPlatform = (password: Accessor): Platform => ({
void shellOpen(url).catch(() => undefined)
},
+ back() {
+ window.history.back()
+ },
+
+ forward() {
+ window.history.forward()
+ },
+
storage: (() => {
type StoreLike = {
get(key: string): Promise
@@ -372,7 +381,7 @@ function ServerGate(props: { children: (data: Accessor) => JSX.
const errorMessage = () => {
const error = serverData.error
- if (!error) return "Unknown error"
+ if (!error) return t("error.chain.unknown")
if (typeof error === "string") return error
if (error instanceof Error) return error.message
return String(error)
@@ -402,16 +411,15 @@ function ServerGate(props: { children: (data: Accessor) => JSX.
}
>
-
OpenCode failed to start
+
{t("desktop.error.serverStartFailed.title")}
- The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy)
- and try again.
+ {t("desktop.error.serverStartFailed.description")}
void restartApp()}>
- Restart App
+ {t("error.page.action.restart")}
diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts
index 1b4c611353..2edeff42b2 100644
--- a/packages/desktop/src/menu.ts
+++ b/packages/desktop/src/menu.ts
@@ -5,10 +5,13 @@ import { relaunch } from "@tauri-apps/plugin-process"
import { runUpdater, UPDATER_ENABLED } from "./updater"
import { installCli } from "./cli"
+import { initI18n, t } from "./i18n"
export async function createMenu() {
if (ostype() !== "macos") return
+ await initI18n()
+
const menu = await Menu.new({
items: [
await Submenu.new({
@@ -20,22 +23,22 @@ export async function createMenu() {
await MenuItem.new({
enabled: UPDATER_ENABLED,
action: () => runUpdater({ alertOnFail: true }),
- text: "Check For Updates...",
+ text: t("desktop.menu.checkForUpdates"),
}),
await MenuItem.new({
action: () => installCli(),
- text: "Install CLI...",
+ text: t("desktop.menu.installCli"),
}),
await MenuItem.new({
action: async () => window.location.reload(),
- text: "Reload Webview",
+ text: t("desktop.menu.reloadWebview"),
}),
await MenuItem.new({
action: async () => {
await invoke("kill_sidecar").catch(() => undefined)
await relaunch().catch(() => undefined)
},
- text: "Restart",
+ text: t("desktop.menu.restart"),
}),
await PredefinedMenuItem.new({
item: "Separator",
diff --git a/packages/desktop/src/updater.ts b/packages/desktop/src/updater.ts
index 4753ee6639..b48bb6be02 100644
--- a/packages/desktop/src/updater.ts
+++ b/packages/desktop/src/updater.ts
@@ -4,41 +4,45 @@ import { ask, message } from "@tauri-apps/plugin-dialog"
import { invoke } from "@tauri-apps/api/core"
import { type as ostype } from "@tauri-apps/plugin-os"
+import { initI18n, t } from "./i18n"
+
export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
+ await initI18n()
+
let update
try {
update = await check()
} catch {
- if (alertOnFail) await message("Failed to check for updates", { title: "Update Check Failed" })
+ if (alertOnFail)
+ await message(t("desktop.updater.checkFailed.message"), { title: t("desktop.updater.checkFailed.title") })
return
}
if (!update) {
- if (alertOnFail)
- await message("You are already using the latest version of OpenCode", { title: "No Update Available" })
+ if (alertOnFail) await message(t("desktop.updater.none.message"), { title: t("desktop.updater.none.title") })
return
}
try {
await update.download()
} catch {
- if (alertOnFail) await message("Failed to download update", { title: "Update Failed" })
+ if (alertOnFail)
+ await message(t("desktop.updater.downloadFailed.message"), { title: t("desktop.updater.downloadFailed.title") })
return
}
- const shouldUpdate = await ask(
- `Version ${update.version} of OpenCode has been downloaded, would you like to install it and relaunch?`,
- { title: "Update Downloaded" },
- )
+ const shouldUpdate = await ask(t("desktop.updater.downloaded.prompt", { version: update.version }), {
+ title: t("desktop.updater.downloaded.title"),
+ })
if (!shouldUpdate) return
try {
if (ostype() === "windows") await invoke("kill_sidecar")
await update.install()
} catch {
- await message("Failed to install update", { title: "Update Failed" })
+ await message(t("desktop.updater.installFailed.message"), { title: t("desktop.updater.installFailed.title") })
return
}
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 483db4d932..a2607891c8 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -20,6 +20,7 @@ import { createStore } from "solid-js/store"
import z from "zod"
import NotFound from "../[...404]"
import { Tabs } from "@opencode-ai/ui/tabs"
+import { MessageNav } from "@opencode-ai/ui/message-nav"
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
import { clientOnly } from "@solidjs/start"
@@ -362,6 +363,15 @@ export default function () {
{title()}