Localize macOS application menu and correct AITURK package checks
This commit is contained in:
@@ -261,6 +261,7 @@ import {
|
||||
resolveOauthRestAuth,
|
||||
resolveReadinessProbeAuth
|
||||
} from './native-auth-decisions'
|
||||
import { localizeNativeMenu, type NativeMenuLocale, resolveNativeMenuLocale } from './native-menu-locale'
|
||||
import {
|
||||
nativeRefreshUrl,
|
||||
type NativeTokenSet,
|
||||
@@ -6755,11 +6756,13 @@ function sendWindowStateChanged(nextIsFullscreen?: boolean, target = mainWindow)
|
||||
webContents.send('hermes:window-state-changed', state)
|
||||
}
|
||||
|
||||
let nativeMenuLocale: NativeMenuLocale = 'tr'
|
||||
|
||||
function buildApplicationMenu() {
|
||||
const template = []
|
||||
|
||||
const checkForUpdatesItem = {
|
||||
label: 'AITURK IDE güncellemeleri…',
|
||||
label: 'Check for Updates…',
|
||||
click: () => IS_PACKAGED ? void shell.openExternal(AITURK_PRODUCT.downloads) : sendOpenUpdatesRequested()
|
||||
}
|
||||
|
||||
@@ -6886,7 +6889,7 @@ function buildApplicationMenu() {
|
||||
submenu: [checkForUpdatesItem]
|
||||
})
|
||||
|
||||
return Menu.buildFromTemplate(template)
|
||||
return Menu.buildFromTemplate(localizeNativeMenu(template, nativeMenuLocale, APP_NAME))
|
||||
}
|
||||
|
||||
function toggleDevTools(window) {
|
||||
@@ -16955,6 +16958,22 @@ ipcMain.on('hermes:titlebar-theme', (_event, payload) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Language stays in backend display.language; this only updates native labels.
|
||||
ipcMain.on('hermes:ui-language', (event, locale) => {
|
||||
if (!IS_MAC) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = resolveNativeMenuLocale(locale, event.sender.id, mainWindow?.webContents.id)
|
||||
|
||||
if (next === null || next === nativeMenuLocale) {
|
||||
return
|
||||
}
|
||||
|
||||
nativeMenuLocale = next
|
||||
Menu.setApplicationMenu(buildApplicationMenu())
|
||||
})
|
||||
|
||||
// Pin the native appearance to the app theme (see NATIVE_THEME_CONFIG_PATH).
|
||||
ipcMain.on('hermes:native-theme', (_event, mode) => {
|
||||
if (!THEME_SOURCES.has(mode)) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { localizeNativeMenu, resolveNativeMenuLocale } from './native-menu-locale'
|
||||
|
||||
describe('native menu language', () => {
|
||||
it('translates nested labels without changing actions, roles or keyboard ownership', () => {
|
||||
const openFolder = vi.fn()
|
||||
const zoom = vi.fn()
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [{ label: 'Open Folder…', click: openFolder }, { type: 'separator' }, { role: 'quit' }]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [{ label: 'Zoom In', accelerator: 'CommandOrControl+Plus', click: zoom }, { role: 'togglefullscreen' }]
|
||||
},
|
||||
{ label: 'My custom action', enabled: false }
|
||||
]
|
||||
|
||||
const translated = localizeNativeMenu(template, 'tr', 'AITURK IDE')
|
||||
const file = translated[0].submenu as MenuItemConstructorOptions[]
|
||||
const view = translated[1].submenu as MenuItemConstructorOptions[]
|
||||
expect(translated[0].label).toBe('Dosya')
|
||||
expect(file[0]).toEqual({ label: 'Klasör aç…', click: openFolder })
|
||||
expect(file[1]).toEqual({ type: 'separator' })
|
||||
expect(file[2]).toEqual({ role: 'quit', label: 'AITURK IDE uygulamasından çık' })
|
||||
expect(view[0]).toEqual({ label: 'Yakınlaştır', accelerator: 'CommandOrControl+Plus', click: zoom })
|
||||
expect(view[1].role).toBe('togglefullscreen')
|
||||
expect(translated[2]).toEqual(template[2])
|
||||
expect(localizeNativeMenu(template, 'en', 'AITURK IDE')).toBe(template)
|
||||
expect((template[0].submenu as MenuItemConstructorOptions[])[0].label).toBe('Open Folder…')
|
||||
expect(openFolder).not.toHaveBeenCalled()
|
||||
expect(zoom).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts only supported language messages from the primary renderer', () => {
|
||||
expect(resolveNativeMenuLocale('tr', 42, 42)).toBe('tr')
|
||||
expect(resolveNativeMenuLocale('en', 42, 42)).toBe('en')
|
||||
expect(resolveNativeMenuLocale('ja', 42, 42)).toBe('en')
|
||||
|
||||
for (const locale of ['tr', 'en']) {
|
||||
expect(resolveNativeMenuLocale(locale, 43, 42)).toBeNull()
|
||||
expect(resolveNativeMenuLocale(locale, 42, undefined)).toBeNull()
|
||||
}
|
||||
|
||||
for (const value of [null, {}, ['tr'], 'unsupported', '__proto__']) {
|
||||
expect(resolveNativeMenuLocale(value, 42, 42)).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
|
||||
export type NativeMenuLocale = 'en' | 'tr'
|
||||
|
||||
// Only the primary settings surface owns the app-wide menu language. Utility
|
||||
// windows have their own I18nProvider and must not overwrite that preference.
|
||||
export function resolveNativeMenuLocale(
|
||||
value: unknown,
|
||||
senderId: number,
|
||||
primaryId: number | undefined
|
||||
): NativeMenuLocale | null {
|
||||
if (primaryId === undefined || senderId !== primaryId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (value === 'tr') {
|
||||
return 'tr'
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && ['en', 'zh', 'zh-hant', 'ja', 'ar', 'ru'].includes(value)) {
|
||||
return 'en'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function localizeNativeMenu(
|
||||
template: MenuItemConstructorOptions[],
|
||||
locale: NativeMenuLocale,
|
||||
appName: string
|
||||
): MenuItemConstructorOptions[] {
|
||||
if (locale !== 'tr') {
|
||||
return template
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
[`About ${appName}`]: `${appName} Hakkında`,
|
||||
'Check for Updates…': 'Güncellemeleri kontrol et…',
|
||||
File: 'Dosya',
|
||||
'New Window': 'Yeni pencere',
|
||||
'Open Folder…': 'Klasör aç…',
|
||||
Close: 'Kapat',
|
||||
Edit: 'Düzen',
|
||||
View: 'Görünüm',
|
||||
Reload: 'Yenile',
|
||||
'Toggle Developer Tools': 'Geliştirici araçlarını aç/kapat',
|
||||
'Actual Size': 'Gerçek boyut',
|
||||
'Zoom In': 'Yakınlaştır',
|
||||
'Zoom Out': 'Uzaklaştır',
|
||||
Window: 'Pencere',
|
||||
Help: 'Yardım'
|
||||
}
|
||||
|
||||
const roles: Partial<Record<NonNullable<MenuItemConstructorOptions['role']>, string>> = {
|
||||
services: 'Servisler',
|
||||
hide: `${appName} uygulamasını gizle`,
|
||||
hideOthers: 'Diğerlerini gizle',
|
||||
unhide: 'Tümünü göster',
|
||||
quit: `${appName} uygulamasından çık`,
|
||||
undo: 'Geri al',
|
||||
redo: 'Yinele',
|
||||
cut: 'Kes',
|
||||
copy: 'Kopyala',
|
||||
paste: 'Yapıştır',
|
||||
pasteAndMatchStyle: 'Biçimlendirmeden yapıştır',
|
||||
delete: 'Sil',
|
||||
selectAll: 'Tümünü seç',
|
||||
forceReload: 'Uygulamayı zorla yenile',
|
||||
togglefullscreen: 'Tam ekranı aç/kapat',
|
||||
minimize: 'Simge durumuna küçült',
|
||||
zoom: 'Pencereyi büyüt/küçült',
|
||||
front: 'Tümünü öne getir',
|
||||
close: 'Kapat'
|
||||
}
|
||||
|
||||
return template.map(item => {
|
||||
const label =
|
||||
item.label !== undefined
|
||||
? Object.hasOwn(labels, item.label)
|
||||
? labels[item.label]
|
||||
: item.label
|
||||
: item.role && roles[item.role]
|
||||
|
||||
return {
|
||||
...item,
|
||||
...(label !== undefined && { label }),
|
||||
...(Array.isArray(item.submenu) && { submenu: localizeNativeMenu(item.submenu, locale, appName) })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const hudNativeDrag = hudWindowing?.nativeDrag === true
|
||||
const launchFlags = ipcRenderer.sendSync('hermes:launch-flags')
|
||||
|
||||
contextBridge.exposeInMainWorld('hermesDesktop', {
|
||||
setUiLanguage: (locale: string) => ipcRenderer.send('hermes:ui-language', locale),
|
||||
glassSupported: translucencySupport?.glass === true,
|
||||
translucencySupported: translucencySupport?.translucency === true,
|
||||
// Launch-flag fact: the app was started with --local, so the renderer may
|
||||
|
||||
Reference in New Issue
Block a user