From 324f61c6f28aa49fd9c290cbc3fe7397da595a91 Mon Sep 17 00:00:00 2001 From: wu-ai-liu-lan Date: Tue, 7 Jul 2026 03:23:56 +0800 Subject: [PATCH] feat: i18n support for desktop application menu Add menu.* i18n keys to en.ts and zh.ts, update desktop-menu.ts labels to use i18n keys, wire translation in windows-app-menu.tsx via useLanguage(), and resolve labels in menu.ts for macOS native menu. --- .../app/src/components/windows-app-menu.tsx | 7 +- packages/app/src/desktop-menu.test.ts | 4 +- packages/app/src/desktop-menu.ts | 94 +++++++++---------- packages/app/src/i18n/en.ts | 60 ++++++++++++ packages/app/src/i18n/zh.ts | 60 ++++++++++++ packages/desktop/src/main/menu.ts | 10 +- 6 files changed, 182 insertions(+), 53 deletions(-) diff --git a/packages/app/src/components/windows-app-menu.tsx b/packages/app/src/components/windows-app-menu.tsx index 017ac4cf8dea..4b56e5ea13b4 100644 --- a/packages/app/src/components/windows-app-menu.tsx +++ b/packages/app/src/components/windows-app-menu.tsx @@ -8,12 +8,15 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { useCommand } from "@/context/command" import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu" import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" export function WindowsAppMenu(props: { command: ReturnType platform: ReturnType variant?: "legacy" | "v2" }) { + const language = useLanguage() + const t = language.t as (key: string) => string let lastFocused: HTMLElement | undefined const rememberFocus = () => { @@ -79,7 +82,7 @@ export function WindowsAppMenu(props: { OpenCode {DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => ( - + {menu.items ?.filter((entry) => desktopMenuVisible(entry, "windows")) .map((entry) => @@ -87,7 +90,7 @@ export function WindowsAppMenu(props: { ) : ( runEntry(entry)} diff --git a/packages/app/src/desktop-menu.test.ts b/packages/app/src/desktop-menu.test.ts index c7e36a7cb73b..33d1207a4297 100644 --- a/packages/app/src/desktop-menu.test.ts +++ b/packages/app/src/desktop-menu.test.ts @@ -4,10 +4,10 @@ import { DESKTOP_MENU } from "./desktop-menu" describe("desktop menu", () => { test("exports logs through the desktop command registry", () => { const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter( - (item) => item.type === "item" && item.label === "Export Logs...", + (item) => item.type === "item" && item.label === "menu.help.exportLogs", ) - expect(items).toHaveLength(2) + expect(items).toHaveLength(1) expect(items.every((item) => item.type === "item" && item.command === "logs.export" && !item.action)).toBe(true) }) }) diff --git a/packages/app/src/desktop-menu.ts b/packages/app/src/desktop-menu.ts index d3902cd2b245..72781ba628fa 100644 --- a/packages/app/src/desktop-menu.ts +++ b/packages/app/src/desktop-menu.ts @@ -72,15 +72,15 @@ export type DesktopMenu = { export const DESKTOP_MENU: DesktopMenu[] = [ { id: "app", - label: "OpenCode", + label: "menu.app", platforms: ["macos"], items: [ { type: "item", role: "about" }, - { type: "item", label: "Check for Updates...", action: "app.checkForUpdates", enabled: "updater" }, - { type: "item", label: "Settings", command: "settings.open", accelerator: { macos: "Cmd+," } }, - { type: "item", label: "Reload Webview", action: "view.reload" }, - { type: "item", label: "Restart", action: "app.relaunch" }, - { type: "item", label: "Export Logs...", command: "logs.export" }, + { type: "item", label: "menu.app.checkUpdates", action: "app.checkForUpdates", enabled: "updater" }, + { type: "item", label: "menu.app.settings", command: "settings.open", accelerator: { macos: "Cmd+," } }, + { type: "item", label: "menu.app.reloadWebview", action: "view.reload" }, + { type: "item", label: "menu.app.restart", action: "app.relaunch" }, + { type: "item", label: "menu.app.exportLogs", command: "logs.export" }, { type: "separator" }, { type: "item", role: "hide" }, { type: "item", role: "hideOthers" }, @@ -91,46 +91,46 @@ export const DESKTOP_MENU: DesktopMenu[] = [ }, { id: "file", - label: "File", + label: "menu.file", items: [ { type: "item", - label: "New Session", + label: "menu.file.newSession", command: "session.new", accelerator: { macos: "Shift+Cmd+S" }, }, - { type: "item", label: "Open Project...", command: "project.open", accelerator: { macos: "Cmd+O" } }, + { type: "item", label: "menu.file.openProject", command: "project.open", accelerator: { macos: "Cmd+O" } }, { type: "item", - label: "Settings", + label: "menu.file.settings", command: "settings.open", accelerator: { windows: "Ctrl+," }, platforms: ["windows"], }, { type: "item", - label: "New Window", + label: "menu.file.newWindow", action: "window.new", accelerator: { macos: "Cmd+Shift+N", windows: "Ctrl+Shift+N" }, }, { type: "separator" }, - { type: "item", label: "Close Window", action: "window.close", role: "close" }, + { type: "item", label: "menu.file.closeWindow", action: "window.close", role: "close" }, ], }, { id: "edit", - label: "Edit", + label: "menu.edit", items: [ - { type: "item", label: "Undo", action: "edit.undo", role: "undo", accelerator: { windows: "Ctrl+Z" } }, - { type: "item", label: "Redo", action: "edit.redo", role: "redo", accelerator: { windows: "Ctrl+Y" } }, + { type: "item", label: "menu.edit.undo", action: "edit.undo", role: "undo", accelerator: { windows: "Ctrl+Z" } }, + { type: "item", label: "menu.edit.redo", action: "edit.redo", role: "redo", accelerator: { windows: "Ctrl+Y" } }, { type: "separator" }, - { type: "item", label: "Cut", action: "edit.cut", role: "cut", accelerator: { windows: "Ctrl+X" } }, - { type: "item", label: "Copy", action: "edit.copy", role: "copy", accelerator: { windows: "Ctrl+C" } }, - { type: "item", label: "Paste", action: "edit.paste", role: "paste", accelerator: { windows: "Ctrl+V" } }, - { type: "item", label: "Delete", action: "edit.delete" }, + { type: "item", label: "menu.edit.cut", action: "edit.cut", role: "cut", accelerator: { windows: "Ctrl+X" } }, + { type: "item", label: "menu.edit.copy", action: "edit.copy", role: "copy", accelerator: { windows: "Ctrl+C" } }, + { type: "item", label: "menu.edit.paste", action: "edit.paste", role: "paste", accelerator: { windows: "Ctrl+V" } }, + { type: "item", label: "menu.edit.delete", action: "edit.delete" }, { type: "item", - label: "Select All", + label: "menu.edit.selectAll", action: "edit.selectAll", role: "selectAll", accelerator: { windows: "Ctrl+A" }, @@ -139,47 +139,47 @@ export const DESKTOP_MENU: DesktopMenu[] = [ }, { id: "view", - label: "View", + label: "menu.view", items: [ - { type: "item", label: "Toggle Sidebar", command: "sidebar.toggle" }, - { type: "item", label: "Toggle Terminal", command: "terminal.toggle", accelerator: { macos: "Ctrl+`" } }, - { type: "item", label: "Toggle File Tree", command: "fileTree.toggle" }, + { type: "item", label: "menu.view.toggleSidebar", command: "sidebar.toggle" }, + { type: "item", label: "menu.view.toggleTerminal", command: "terminal.toggle", accelerator: { macos: "Ctrl+`" } }, + { type: "item", label: "menu.view.toggleFileTree", command: "fileTree.toggle" }, { type: "separator" }, - { type: "item", label: "Reload", action: "view.reload", role: "reload" }, - { type: "item", label: "Toggle Developer Tools", action: "view.toggleDevTools", role: "toggleDevTools" }, + { type: "item", label: "menu.view.reload", action: "view.reload", role: "reload" }, + { type: "item", label: "menu.view.toggleDevTools", action: "view.toggleDevTools", role: "toggleDevTools" }, { type: "separator" }, { type: "item", - label: "Actual Size", + label: "menu.view.actualSize", action: "view.resetZoom", role: "resetZoom", accelerator: { windows: "Ctrl+0" }, }, - { type: "item", label: "Zoom In", action: "view.zoomIn", role: "zoomIn", accelerator: { windows: "Ctrl++" } }, - { type: "item", label: "Zoom Out", action: "view.zoomOut", role: "zoomOut", accelerator: { windows: "Ctrl+-" } }, + { type: "item", label: "menu.view.zoomIn", action: "view.zoomIn", role: "zoomIn", accelerator: { windows: "Ctrl++" } }, + { type: "item", label: "menu.view.zoomOut", action: "view.zoomOut", role: "zoomOut", accelerator: { windows: "Ctrl+-" } }, { type: "separator" }, - { type: "item", label: "Toggle Full Screen", action: "view.toggleFullscreen", role: "togglefullscreen" }, + { type: "item", label: "menu.view.toggleFullScreen", action: "view.toggleFullscreen", role: "togglefullscreen" }, ], }, { id: "go", - label: "Go", + label: "menu.go", items: [ - { type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } }, - { type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } }, + { type: "item", label: "menu.go.back", command: "common.goBack", accelerator: { macos: "Cmd+[" } }, + { type: "item", label: "menu.go.forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } }, { type: "separator" }, - { type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } }, - { type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } }, + { type: "item", label: "menu.go.previousSession", command: "session.previous", accelerator: { macos: "Option+Up" } }, + { type: "item", label: "menu.go.nextSession", command: "session.next", accelerator: { macos: "Option+Down" } }, { type: "separator" }, { type: "item", - label: "Previous Project", + label: "menu.go.previousProject", command: "project.previous", accelerator: { macos: "Cmd+Option+Up" }, }, { type: "item", - label: "Next Project", + label: "menu.go.nextProject", command: "project.next", accelerator: { macos: "Cmd+Option+Down" }, }, @@ -187,31 +187,31 @@ export const DESKTOP_MENU: DesktopMenu[] = [ }, { id: "window", - label: "Window", + label: "menu.window", role: "windowMenu", items: [ - { type: "item", label: "Minimize", action: "window.minimize" }, - { type: "item", label: "Maximize", action: "window.toggleMaximize" }, + { type: "item", label: "menu.window.minimize", action: "window.minimize" }, + { type: "item", label: "menu.window.maximize", action: "window.toggleMaximize" }, { type: "separator" }, - { type: "item", label: "Close Window", action: "window.close" }, + { type: "item", label: "menu.window.closeWindow", action: "window.close" }, ], }, { id: "help", - label: "Help", + label: "menu.help", items: [ - { type: "item", label: "OpenCode Documentation", href: "https://opencode.ai/docs" }, - { type: "item", label: "Support Forum", href: "https://discord.com/invite/opencode" }, - { type: "item", label: "Export Logs...", command: "logs.export" }, + { type: "item", label: "menu.help.documentation", href: "https://opencode.ai/docs" }, + { type: "item", label: "menu.help.supportForum", href: "https://discord.com/invite/opencode" }, + { type: "item", label: "menu.help.exportLogs", command: "logs.export" }, { type: "separator" }, { type: "item", - label: "Share Feedback", + label: "menu.help.shareFeedback", href: "https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml", }, { type: "item", - label: "Report a Bug", + label: "menu.help.reportBug", href: "https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml", }, ], diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 66e60df30e09..2e6ce725a76f 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1076,4 +1076,64 @@ export const dict = { "workspace.reset.archived.one": "1 session will be archived.", "workspace.reset.archived.many": "{{count}} sessions will be archived.", "workspace.reset.note": "This will reset the workspace to match the default branch.", + + "menu.app": "OpenCode", + "menu.file": "File", + "menu.edit": "Edit", + "menu.view": "View", + "menu.go": "Go", + "menu.window": "Window", + "menu.help": "Help", + + "menu.file.newSession": "New Session", + "menu.file.openProject": "Open Project...", + "menu.file.settings": "Settings", + "menu.file.newWindow": "New Window", + "menu.file.closeWindow": "Close Window", + + "menu.edit.undo": "Undo", + "menu.edit.redo": "Redo", + "menu.edit.cut": "Cut", + "menu.edit.copy": "Copy", + "menu.edit.paste": "Paste", + "menu.edit.delete": "Delete", + "menu.edit.selectAll": "Select All", + + "menu.view.toggleSidebar": "Toggle Sidebar", + "menu.view.toggleTerminal": "Toggle Terminal", + "menu.view.toggleFileTree": "Toggle File Tree", + "menu.view.reload": "Reload", + "menu.view.toggleDevTools": "Toggle Developer Tools", + "menu.view.actualSize": "Actual Size", + "menu.view.zoomIn": "Zoom In", + "menu.view.zoomOut": "Zoom Out", + "menu.view.toggleFullScreen": "Toggle Full Screen", + + "menu.go.back": "Back", + "menu.go.forward": "Forward", + "menu.go.previousSession": "Previous Session", + "menu.go.nextSession": "Next Session", + "menu.go.previousProject": "Previous Project", + "menu.go.nextProject": "Next Project", + + "menu.window.minimize": "Minimize", + "menu.window.maximize": "Maximize", + "menu.window.closeWindow": "Close Window", + + "menu.help.documentation": "OpenCode Documentation", + "menu.help.supportForum": "Support Forum", + "menu.help.exportLogs": "Export Logs...", + "menu.help.shareFeedback": "Share Feedback", + "menu.help.reportBug": "Report a Bug", + + "menu.app.about": "About OpenCode", + "menu.app.checkUpdates": "Check for Updates...", + "menu.app.settings": "Settings", + "menu.app.reloadWebview": "Reload Webview", + "menu.app.restart": "Restart", + "menu.app.exportLogs": "Export Logs...", + "menu.app.hide": "Hide OpenCode", + "menu.app.hideOthers": "Hide Others", + "menu.app.showAll": "Show All", + "menu.app.quit": "Quit OpenCode", } diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 15d873ee04a9..222fef1e8638 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -933,4 +933,64 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", "error.childStore.storeCreateFailed": "创建存储失败", "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", + + "menu.app": "OpenCode", + "menu.file": "文件", + "menu.edit": "编辑", + "menu.view": "视图", + "menu.go": "转到", + "menu.window": "窗口", + "menu.help": "帮助", + + "menu.file.newSession": "新建会话", + "menu.file.openProject": "打开项目...", + "menu.file.settings": "设置", + "menu.file.newWindow": "新建窗口", + "menu.file.closeWindow": "关闭窗口", + + "menu.edit.undo": "撤销", + "menu.edit.redo": "重做", + "menu.edit.cut": "剪切", + "menu.edit.copy": "复制", + "menu.edit.paste": "粘贴", + "menu.edit.delete": "删除", + "menu.edit.selectAll": "全选", + + "menu.view.toggleSidebar": "切换侧边栏", + "menu.view.toggleTerminal": "切换终端", + "menu.view.toggleFileTree": "切换文件树", + "menu.view.reload": "重新加载", + "menu.view.toggleDevTools": "切换开发者工具", + "menu.view.actualSize": "实际大小", + "menu.view.zoomIn": "放大", + "menu.view.zoomOut": "缩小", + "menu.view.toggleFullScreen": "切换全屏", + + "menu.go.back": "后退", + "menu.go.forward": "前进", + "menu.go.previousSession": "上一个会话", + "menu.go.nextSession": "下一个会话", + "menu.go.previousProject": "上一个项目", + "menu.go.nextProject": "下一个项目", + + "menu.window.minimize": "最小化", + "menu.window.maximize": "最大化", + "menu.window.closeWindow": "关闭窗口", + + "menu.help.documentation": "OpenCode 文档", + "menu.help.supportForum": "支持论坛", + "menu.help.exportLogs": "导出日志...", + "menu.help.shareFeedback": "分享反馈", + "menu.help.reportBug": "报告 Bug", + + "menu.app.about": "关于 OpenCode", + "menu.app.checkUpdates": "检查更新...", + "menu.app.settings": "设置", + "menu.app.reloadWebview": "重新加载页面", + "menu.app.restart": "重启", + "menu.app.exportLogs": "导出日志...", + "menu.app.hide": "隐藏 OpenCode", + "menu.app.hideOthers": "隐藏其他", + "menu.app.showAll": "显示全部", + "menu.app.quit": "退出 OpenCode", } satisfies Partial> diff --git a/packages/desktop/src/main/menu.ts b/packages/desktop/src/main/menu.ts index 7adc65a429b3..cd42c8e2abb0 100644 --- a/packages/desktop/src/main/menu.ts +++ b/packages/desktop/src/main/menu.ts @@ -6,6 +6,7 @@ import { type DesktopMenuEntry, type DesktopMenuRole, } from "@opencode-ai/app/desktop-menu" +import { dict as en } from "@opencode-ai/app/i18n/en" import { UPDATER_ENABLED } from "./constants" import { runDesktopMenuAction } from "./desktop-menu-actions" @@ -16,13 +17,18 @@ type Deps = { relaunch: () => void } +function t(key: string) { + const dict = en as Record + return dict[key] ?? key +} + export function createMenu(deps: Deps) { if (process.platform !== "darwin") return const template = DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "macos")).map((menu) => { if (menu.role) return { role: nativeRole(menu.role) } return { - label: menu.label, + label: t(menu.label), submenu: menu.items ?.filter((entry) => desktopMenuVisible(entry, "macos")) .map((entry) => nativeItem(entry, deps)), @@ -37,7 +43,7 @@ function nativeItem(entry: DesktopMenuEntry, deps: Deps): MenuItemConstructorOpt if (entry.role) return { role: nativeRole(entry.role) } const item: MenuItemConstructorOptions = { - label: entry.label, + label: t(entry.label ?? ""), accelerator: entry.accelerator?.macos, enabled: entry.enabled === "updater" ? UPDATER_ENABLED : undefined, }