Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ file is the human-readable summary of what changed and why.
session, prompt, token, commit, branch, and agent stay in English (the
glossary lives in `.codev/glossary/vi.md`).
([#307](https://github.com/quickbeard/codev-code/pull/307))
- **The app menu is translated too** — File, Edit, View, Go, Window and Help
and everything inside them stayed English no matter which language the rest
of the app was in: the menu was built from fixed English text rather than
from the dictionaries. It now follows your language like every other
surface, on the macOS menu bar and the Windows/Linux titlebar menu alike,
and switching language updates it without a restart. Developer terms stay in
English where Vietnamese developers expect them — session, terminal,
webview, developer tools.
([#322](https://github.com/quickbeard/codev-code/pull/322))

### Privacy

Expand Down
7 changes: 4 additions & 3 deletions DIVERGENCES.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"exports": {
".": "./src/index.ts",
"./desktop-menu": "./src/desktop-menu.ts",
"./i18n/*": "./src/i18n/*.ts",
"./updater": "./src/updater.ts",
"./wsl/types": "./src/wsl/types.ts",
"./sandbox/types": "./src/sandbox/types.ts",
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/components/windows-app-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"

import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import {
DESKTOP_MENU,
desktopMenuItemDisabled,
Expand All @@ -20,6 +21,7 @@ export function WindowsAppMenu(props: {
platform: ReturnType<typeof usePlatform>
variant?: "legacy" | "v2"
}) {
const language = useLanguage()
let lastFocused: HTMLElement | undefined

const rememberFocus = () => {
Expand Down Expand Up @@ -85,15 +87,15 @@ export function WindowsAppMenu(props: {
<DropdownMenu.Group>
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">CoDev</DropdownMenu.GroupLabel>
{DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => (
<DesktopMenuSubmenu label={menu.label}>
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
{menu.items
?.filter((entry) => desktopMenuVisible(entry, "windows"))
.map((entry) =>
entry.type === "separator" ? (
<DropdownMenu.Separator />
) : (
<DesktopMenuItem
label={entry.label ?? ""}
label={entry.labelKey ? language.t(entry.labelKey) : ""}
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
disabled={desktopMenuItemDisabled(entry, {
updaterStatus: props.platform.updater?.state().status,
Expand Down
24 changes: 21 additions & 3 deletions packages/app/src/desktop-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,31 @@ import { DESKTOP_MENU, desktopMenuItemDisabled, desktopMenuVisible, type Desktop
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.labelKey === "menu.exportLogs",
)

expect(items).toHaveLength(2)
expect(items.every((item) => item.type === "item" && item.command === "logs.export" && !item.action)).toBe(true)
})

// The menu is the one surface whose labels are resolved twice — by the
// renderer for the Windows/Linux titlebar menu and by the desktop main
// process for the macOS menu bar — and neither can fall back to English text
// that is no longer in the table. A key missing from a locale renders that
// item blank there.
test("translates every menu label in every locale", async () => {
const keys = DESKTOP_MENU.flatMap((menu) => [
menu.labelKey,
...(menu.items ?? []).flatMap((item) => (item.type === "item" && item.labelKey ? [item.labelKey] : [])),
])

expect(keys.length).toBeGreaterThan(0)
for (const locale of ["en", "es", "vi"]) {
const { dict } = (await import(`./i18n/${locale}`)) as { dict: Record<string, string> }
expect({ locale, missing: keys.filter((key) => !dict[key]) }).toEqual({ locale, missing: [] })
}
})

test("offers an update check on every platform exactly once", () => {
for (const platform of ["macos", "windows"] as const) {
const items = DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, platform)).flatMap((menu) =>
Expand Down Expand Up @@ -45,7 +63,7 @@ describe("desktop menu", () => {
}

test("defers to the command registry for command entries", () => {
const entry = { type: "item", label: "Export Logs...", command: "logs.export" } as const
const entry = { type: "item", labelKey: "menu.exportLogs", command: "logs.export" } as const

expect(desktopMenuItemDisabled(entry, { commandDisabled: (id) => id === "logs.export" })).toBe(true)
expect(desktopMenuItemDisabled(entry, { commandDisabled: never })).toBe(false)
Expand All @@ -54,7 +72,7 @@ describe("desktop menu", () => {
test("leaves link and role entries alone", () => {
expect(
desktopMenuItemDisabled(
{ type: "item", label: "Report a Bug", href: "https://example.test" },
{ type: "item", labelKey: "menu.reportBug", href: "https://example.test" },
{
commandDisabled: never,
},
Expand Down
130 changes: 77 additions & 53 deletions packages/app/src/desktop-menu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { DOCS_URL } from "@codev/brand"
import type { dict } from "./i18n/en"
import type { UpdaterState } from "./updater"

// Menu labels are dictionary keys, not text: this table is read by the macOS
// native menu in the desktop main process and by the Windows/Linux titlebar
// menu in the renderer, and both resolve it against the app dictionaries. A
// literal here is a label that never translates.
export type DesktopMenuLabelKey = keyof typeof dict

export type DesktopMenuPlatform = "macos" | "windows"

export type DesktopMenuAction =
Expand Down Expand Up @@ -48,7 +55,7 @@ export type DesktopMenuRole =

export type DesktopMenuItem = {
type: "item"
label?: string
labelKey?: DesktopMenuLabelKey
command?: string
action?: DesktopMenuAction
role?: DesktopMenuRole
Expand All @@ -67,7 +74,7 @@ export type DesktopMenuEntry = DesktopMenuItem | DesktopMenuSeparator

export type DesktopMenu = {
id: string
label: string
labelKey: DesktopMenuLabelKey
role?: DesktopMenuRole
items?: DesktopMenuEntry[]
platforms?: DesktopMenuPlatform[]
Expand All @@ -76,17 +83,17 @@ export type DesktopMenu = {
export const DESKTOP_MENU: DesktopMenu[] = [
{
id: "app",
label: "CoDev",
labelKey: "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: "Log in/Configure", command: "gateway.apiKey.new" },
{ type: "item", label: "Upload Session Logs", action: "upload.sessions" },
{ 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", labelKey: "menu.checkForUpdates", action: "app.checkForUpdates", enabled: "updater" },
{ type: "item", labelKey: "menu.settings", command: "settings.open", accelerator: { macos: "Cmd+," } },
{ type: "item", labelKey: "menu.login", command: "gateway.apiKey.new" },
{ type: "item", labelKey: "menu.uploadSessionLogs", action: "upload.sessions" },
{ type: "item", labelKey: "menu.reloadWebview", action: "view.reload" },
{ type: "item", labelKey: "menu.restart", action: "app.relaunch" },
{ type: "item", labelKey: "menu.exportLogs", command: "logs.export" },
{ type: "separator" },
{ type: "item", role: "hide" },
{ type: "item", role: "hideOthers" },
Expand All @@ -97,18 +104,18 @@ export const DESKTOP_MENU: DesktopMenu[] = [
},
{
id: "file",
label: "File",
labelKey: "menu.file",
items: [
{
type: "item",
label: "New Session",
labelKey: "menu.newSession",
command: "session.new",
accelerator: { macos: "Shift+Cmd+S" },
},
{ type: "item", label: "Open Project...", command: "project.open", accelerator: { macos: "Cmd+O" } },
{ type: "item", labelKey: "menu.openProject", command: "project.open", accelerator: { macos: "Cmd+O" } },
{
type: "item",
label: "Settings",
labelKey: "menu.settings",
command: "settings.open",
accelerator: { windows: "Ctrl+," },
platforms: ["windows"],
Expand All @@ -117,41 +124,41 @@ export const DESKTOP_MENU: DesktopMenu[] = [
// Windows/Linux menu does not render — same split Settings itself uses.
{
type: "item",
label: "Log in/Configure",
labelKey: "menu.login",
command: "gateway.apiKey.new",
platforms: ["windows"],
},
// macOS carries this in the app menu below its sign-in sibling.
{
type: "item",
label: "Upload Session Logs",
labelKey: "menu.uploadSessionLogs",
action: "upload.sessions",
platforms: ["windows"],
},
{
type: "item",
label: "New Window",
labelKey: "menu.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", labelKey: "menu.closeWindow", action: "window.close", role: "close" },
],
},
{
id: "edit",
label: "Edit",
labelKey: "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", labelKey: "menu.undo", action: "edit.undo", role: "undo", accelerator: { windows: "Ctrl+Z" } },
{ type: "item", labelKey: "menu.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", labelKey: "menu.cut", action: "edit.cut", role: "cut", accelerator: { windows: "Ctrl+X" } },
{ type: "item", labelKey: "menu.copy", action: "edit.copy", role: "copy", accelerator: { windows: "Ctrl+C" } },
{ type: "item", labelKey: "menu.paste", action: "edit.paste", role: "paste", accelerator: { windows: "Ctrl+V" } },
{ type: "item", labelKey: "menu.delete", action: "edit.delete" },
{
type: "item",
label: "Select All",
labelKey: "menu.selectAll",
action: "edit.selectAll",
role: "selectAll",
accelerator: { windows: "Ctrl+A" },
Expand All @@ -160,78 +167,95 @@ export const DESKTOP_MENU: DesktopMenu[] = [
},
{
id: "view",
label: "View",
labelKey: "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", labelKey: "menu.toggleSidebar", command: "sidebar.toggle" },
{ type: "item", labelKey: "menu.toggleTerminal", command: "terminal.toggle", accelerator: { macos: "Ctrl+`" } },
{ type: "item", labelKey: "menu.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", labelKey: "menu.reload", action: "view.reload", role: "reload" },
{ type: "item", labelKey: "menu.toggleDevTools", action: "view.toggleDevTools", role: "toggleDevTools" },
{ type: "separator" },
{
type: "item",
label: "Actual Size",
labelKey: "menu.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",
labelKey: "menu.zoomIn",
action: "view.zoomIn",
role: "zoomIn",
accelerator: { windows: "Ctrl++" },
},
{
type: "item",
labelKey: "menu.zoomOut",
action: "view.zoomOut",
role: "zoomOut",
accelerator: { windows: "Ctrl+-" },
},
{ type: "separator" },
{ type: "item", label: "Toggle Full Screen", action: "view.toggleFullscreen", role: "togglefullscreen" },
{ type: "item", labelKey: "menu.toggleFullScreen", action: "view.toggleFullscreen", role: "togglefullscreen" },
],
},
{
id: "go",
label: "Go",
labelKey: "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", labelKey: "menu.back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
{ type: "item", labelKey: "menu.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",
labelKey: "menu.previousSession",
command: "session.previous",
accelerator: { macos: "Option+Up" },
},
{ type: "item", labelKey: "menu.nextSession", command: "session.next", accelerator: { macos: "Option+Down" } },
{ type: "separator" },
{
type: "item",
label: "Previous Project",
labelKey: "menu.previousProject",
command: "project.previous",
accelerator: { macos: "Cmd+Option+Up" },
},
{
type: "item",
label: "Next Project",
labelKey: "menu.nextProject",
command: "project.next",
accelerator: { macos: "Cmd+Option+Down" },
},
],
},
{
id: "window",
label: "Window",
labelKey: "menu.window",
role: "windowMenu",
items: [
{ type: "item", label: "Minimize", action: "window.minimize" },
{ type: "item", label: "Maximize", action: "window.toggleMaximize" },
{ type: "item", labelKey: "menu.minimize", action: "window.minimize" },
{ type: "item", labelKey: "menu.maximize", action: "window.toggleMaximize" },
{ type: "separator" },
{ type: "item", label: "Close Window", action: "window.close" },
{ type: "item", labelKey: "menu.closeWindow", action: "window.close" },
],
},
{
id: "help",
label: "Help",
labelKey: "menu.help",
items: [
{ type: "item", label: "CoDev Documentation", href: DOCS_URL },
{ type: "item", label: "Export Logs...", command: "logs.export" },
{ type: "item", labelKey: "menu.documentation", href: DOCS_URL },
{ type: "item", labelKey: "menu.exportLogs", command: "logs.export" },
{ type: "separator" },
{
type: "item",
label: "Share Feedback",
labelKey: "menu.shareFeedback",
href: "https://github.com/quickbeard/codev-code/issues/new?template=feature-request.yml",
},
{
type: "item",
label: "Report a Bug",
labelKey: "menu.reportBug",
href: "https://github.com/quickbeard/codev-code/issues/new?template=bug-report.yml",
},
// macOS has this in the app menu, which the Windows/Linux menu does not
Expand All @@ -240,7 +264,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [
{ type: "separator", platforms: ["windows"] },
{
type: "item",
label: "Check for Updates...",
labelKey: "menu.checkForUpdates",
action: "app.checkForUpdates",
enabled: "updater",
platforms: ["windows"],
Expand Down
Loading
Loading