diff --git a/src/main/i18n/locales/en_US/menu.json b/src/main/i18n/locales/en_US/menu.json index 8c7546457..95185ed03 100644 --- a/src/main/i18n/locales/en_US/menu.json +++ b/src/main/i18n/locales/en_US/menu.json @@ -62,6 +62,13 @@ }, "edit": { "label": "Edit", + "undo": "Undo", + "redo": "Redo", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "delete": "Delete", + "selectAll": "Select All", "find": "Find" }, "editor": { @@ -98,6 +105,9 @@ }, "window": { "label": "Window", - "minimize": "Minimize" + "minimize": "Minimize", + "zoom": "Zoom", + "front": "Bring All to Front", + "close": "Close" } } diff --git a/src/main/i18n/locales/ru_RU/menu.json b/src/main/i18n/locales/ru_RU/menu.json index 37dcdf2f9..cb2a42f5f 100644 --- a/src/main/i18n/locales/ru_RU/menu.json +++ b/src/main/i18n/locales/ru_RU/menu.json @@ -62,6 +62,13 @@ }, "edit": { "label": "Правка", + "undo": "Отменить", + "redo": "Повторить", + "cut": "Вырезать", + "copy": "Копировать", + "paste": "Вставить", + "delete": "Удалить", + "selectAll": "Выделить всё", "find": "Найти" }, "editor": { @@ -98,6 +105,9 @@ }, "window": { "label": "Окно", - "minimize": "Свернуть" + "minimize": "Свернуть", + "zoom": "Изменить размер окна", + "front": "Все окна на передний план", + "close": "Закрыть" } } diff --git a/src/main/menu/__tests__/mainLocalization.test.ts b/src/main/menu/__tests__/mainLocalization.test.ts new file mode 100644 index 000000000..e84f9961b --- /dev/null +++ b/src/main/menu/__tests__/mainLocalization.test.ts @@ -0,0 +1,85 @@ +import type { MenuItemConstructorOptions } from 'electron' +import process from 'node:process' +import { createInstance } from 'i18next' +import { afterEach, describe, expect, it, vi } from 'vitest' +import english from '../../i18n/locales/en_US/menu.json' +import russian from '../../i18n/locales/ru_RU/menu.json' + +vi.mock('electron', () => ({ + app: { getVersion: () => '1.0.0' }, + BrowserWindow: { getFocusedWindow: () => null }, + dialog: {}, + Menu: { buildFromTemplate: (template: unknown) => template }, + shell: {}, +})) + +vi.mock('../../i18n', async () => { + const i18n = createInstance() + await i18n.init({ + lng: 'en_US', + fallbackLng: 'en_US', + resources: { + en_US: { menu: english }, + ru_RU: { menu: russian }, + }, + }) + return { default: i18n } +}) + +vi.mock('../../ipc', () => ({ send: vi.fn() })) +vi.mock('../../updates', () => ({ checkForUpdatesFromMenu: vi.fn() })) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe.each(['win32', 'darwin', 'linux'])('main menu localization on %s', (platform) => { + it.each(['ru_RU', 'en_US', 'missing_locale'])('uses %s labels and keeps native roles', async (locale) => { + vi.resetModules() + vi.stubGlobal('process', { ...process, platform }) + const { default: i18n } = await import('../../i18n') + await i18n.changeLanguage(locale) + const { createMainMenu } = await import('../main') + const template = createMainMenu() as unknown as MenuItemConstructorOptions[] + const edit = template.find(item => item.role === 'editMenu')! + const window = template.find(item => item.role === 'windowMenu')! + const editItems = edit.submenu as MenuItemConstructorOptions[] + const windowItems = window.submenu as MenuItemConstructorOptions[] + const isRussian = locale === 'ru_RU' + + expect(edit.label).toBe(isRussian ? 'Правка' : 'Edit') + expect(window.label).toBe(isRussian ? 'Окно' : 'Window') + expect(editItems.filter(item => item.role).map(item => [item.role, item.label])) + .toEqual([ + ['undo', isRussian ? 'Отменить' : 'Undo'], + ['redo', isRussian ? 'Повторить' : 'Redo'], + ['cut', isRussian ? 'Вырезать' : 'Cut'], + ['copy', isRussian ? 'Копировать' : 'Copy'], + ['paste', isRussian ? 'Вставить' : 'Paste'], + ['delete', isRussian ? 'Удалить' : 'Delete'], + ['selectAll', isRussian ? 'Выделить всё' : 'Select All'], + ]) + expect(editItems.at(-1)).toMatchObject({ + label: isRussian ? 'Найти' : 'Find', + accelerator: 'CommandOrControl+F', + click: expect.any(Function), + }) + expect(windowItems.map(item => item.role ?? item.type)).toEqual( + platform === 'darwin' + ? ['minimize', 'zoom', 'separator', 'front'] + : ['minimize', 'zoom', 'close'], + ) + expect(windowItems.filter(item => item.role).map(item => item.label)).toEqual([ + isRussian ? 'Свернуть' : 'Minimize', + isRussian ? 'Изменить размер окна' : 'Zoom', + platform === 'darwin' + ? (isRussian ? 'Все окна на передний план' : 'Bring All to Front') + : (isRussian ? 'Закрыть' : 'Close'), + ]) + // Electron supplies the actions and platform-specific shortcuts for roles. + for (const item of [...editItems, ...windowItems].filter(item => item.role)) { + expect(item.click).toBeUndefined() + expect(item.accelerator).toBeUndefined() + } + }) +}) diff --git a/src/main/menu/main.ts b/src/main/menu/main.ts index 9cecf121e..79ff5230d 100644 --- a/src/main/menu/main.ts +++ b/src/main/menu/main.ts @@ -239,30 +239,37 @@ if (isDev) { const editMenuItems: MenuConfig[] = [ { + label: i18n.t('menu:edit.undo'), role: 'undo', }, { + label: i18n.t('menu:edit.redo'), role: 'redo', }, { type: 'separator', }, { + label: i18n.t('menu:edit.cut'), role: 'cut', }, { + label: i18n.t('menu:edit.copy'), role: 'copy', }, { + label: i18n.t('menu:edit.paste'), role: 'paste', }, { + label: i18n.t('menu:edit.delete'), role: 'delete', }, { type: 'separator', }, { + label: i18n.t('menu:edit.selectAll'), role: 'selectAll', }, { @@ -275,6 +282,25 @@ const editMenuItems: MenuConfig[] = [ }, ] +function createWindowMenuItems(): MenuItemConstructorOptions[] { + return [ + { + label: i18n.t('menu:window.minimize'), + role: 'minimize', + }, + { + label: i18n.t('menu:window.zoom'), + role: 'zoom', + }, + ...(process.platform === 'darwin' + ? [ + { type: 'separator' as const }, + { label: i18n.t('menu:window.front'), role: 'front' as const }, + ] + : [{ label: i18n.t('menu:window.close'), role: 'close' as const }]), + ] +} + function getPrimaryActionLabel(action: MainMenuPrimaryAction) { if (action === 'new-snippet') return i18n.t('action.new.snippet') @@ -654,6 +680,7 @@ function createMainMenuTemplate( submenu: createPlatformMenuItems(appMenuItems), }, { + label: i18n.t('menu:edit.label'), role: 'editMenu', submenu: createPlatformMenuItems(editMenuItems), }, @@ -662,7 +689,9 @@ function createMainMenuTemplate( submenu: createPlatformMenuItems(historyMenuItems), }, { + label: i18n.t('menu:window.label'), role: 'windowMenu', + submenu: createWindowMenuItems(), }, { label: i18n.t('menu:help.label'),