cherry-studio/src/main/services/AppMenuService.ts
Jake Jia 816a92c609
feat(app-menu): add full i18n support and sync lanuage with app language settings (#11131)
Previously, the macOS menu bar was always displayed in English regardless of
system language or in-app language settings. This change enables the menu bar
to dynamically follow the application's language preference.

Key changes:
- Add language change listener to automatically update menu when user switches language
- Refactor AppMenuService with proper subscription management and cleanup
- Add appMenu translations for en-us, zh-cn, and zh-tw locales
- Implement destroy method to prevent memory leaks from config subscriptions
- Convert all menu items (File, Edit, View, Window, Help) to use localized labels

The menu bar now respects the in-app language setting and updates in real-time
when users change their preferences, providing a consistent multilingual experience.
2025-11-06 14:46:42 +08:00

133 lines
4.2 KiB
TypeScript

import { isMac } from '@main/constant'
import { windowService } from '@main/services/WindowService'
import { locales } from '@main/utils/locales'
import { IpcChannel } from '@shared/IpcChannel'
import type { MenuItemConstructorOptions } from 'electron'
import { app, Menu, shell } from 'electron'
import { configManager } from './ConfigManager'
export class AppMenuService {
private languageChangeCallback?: (newLanguage: string) => void
constructor() {
// Subscribe to language change events
this.languageChangeCallback = () => {
this.setupApplicationMenu()
}
configManager.subscribe('language', this.languageChangeCallback)
}
public destroy(): void {
// Clean up subscription to prevent memory leaks
if (this.languageChangeCallback) {
configManager.unsubscribe('language', this.languageChangeCallback)
}
}
public setupApplicationMenu(): void {
const locale = locales[configManager.getLanguage()]
const { appMenu } = locale.translation
const template: MenuItemConstructorOptions[] = [
{
label: app.name,
submenu: [
{
label: appMenu.about + ' ' + app.name,
click: () => {
// Emit event to navigate to About page
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.Windows_NavigateToAbout)
windowService.showMainWindow()
}
}
},
{ type: 'separator' },
{ role: 'services', label: appMenu.services },
{ type: 'separator' },
{ role: 'hide', label: `${appMenu.hide} ${app.name}` },
{ role: 'hideOthers', label: appMenu.hideOthers },
{ role: 'unhide', label: appMenu.unhide },
{ type: 'separator' },
{ role: 'quit', label: `${appMenu.quit} ${app.name}` }
]
},
{
label: appMenu.file,
submenu: [{ role: 'close', label: appMenu.close }]
},
{
label: appMenu.edit,
submenu: [
{ role: 'undo', label: appMenu.undo },
{ role: 'redo', label: appMenu.redo },
{ type: 'separator' },
{ role: 'cut', label: appMenu.cut },
{ role: 'copy', label: appMenu.copy },
{ role: 'paste', label: appMenu.paste },
{ role: 'delete', label: appMenu.delete },
{ role: 'selectAll', label: appMenu.selectAll }
]
},
{
label: appMenu.view,
submenu: [
{ role: 'reload', label: appMenu.reload },
{ role: 'forceReload', label: appMenu.forceReload },
{ role: 'toggleDevTools', label: appMenu.toggleDevTools },
{ type: 'separator' },
{ role: 'resetZoom', label: appMenu.resetZoom },
{ role: 'zoomIn', label: appMenu.zoomIn },
{ role: 'zoomOut', label: appMenu.zoomOut },
{ type: 'separator' },
{ role: 'togglefullscreen', label: appMenu.toggleFullscreen }
]
},
{
label: appMenu.window,
submenu: [
{ role: 'minimize', label: appMenu.minimize },
{ role: 'zoom', label: appMenu.zoom },
{ type: 'separator' },
{ role: 'front', label: appMenu.front }
]
},
{
label: appMenu.help,
submenu: [
{
label: appMenu.website,
click: () => {
shell.openExternal('https://cherry-ai.com')
}
},
{
label: appMenu.documentation,
click: () => {
shell.openExternal('https://cherry-ai.com/docs')
}
},
{
label: appMenu.feedback,
click: () => {
shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose')
}
},
{
label: appMenu.releases,
click: () => {
shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases')
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
}
export const appMenuService = isMac ? new AppMenuService() : null