Localize macOS application menu and correct AITURK package checks
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# macOS yerel menüsünün Türkçe desteği
|
||||
|
||||
Dosya, Düzen, Görünüm, Pencere, Yardım ve uygulama menüsündeki komutlar
|
||||
Türkçe gösterilir. Ana pencerenin dil seçimi mevcut `display.language`
|
||||
ayarından gelir. Dar kapsamlı `setUiLanguage` köprüsü yalnızca menü
|
||||
etiketlerini günceller; ayrı bir tercih dosyası veya sunucu ayarı oluşturmaz.
|
||||
Dil kaydı başarısız olursa arayüzle birlikte menü dili de geri alınır.
|
||||
Yardımcı pencereler ana pencerenin menü dilini değiştiremez.
|
||||
|
||||
Menü öğelerinin eylemleri, Electron rolleri ve klavye kısayolları korunur.
|
||||
Özellikle renderer tarafından yönetilen yeni pencere, klasör açma, sekme
|
||||
kapatma ve sayfa yenileme komutlarına yerel kısayol eklenmez. İngilizce
|
||||
seçilince özgün İngilizce şablon kullanılır; diğer diller yerel menüde
|
||||
mevcut İngilizce desteğini kullanmaya devam eder.
|
||||
|
||||
Paket doğrulama yolları `package.json` içindeki AITURK ürün ve çalıştırılabilir
|
||||
dosya adlarını kullanır. Intel Mac için `release/mac`, Apple Silicon için
|
||||
`release/mac-arm64` aranır. Temiz kurulum denemesi açıkça izole
|
||||
`AITURK_IDE_HOME` ve Electron kullanıcı dizini seçer. Başka bir Hermes
|
||||
kurulumunun çalışma dizinine yönelmez.
|
||||
|
||||
## Kanıt ve yayın sınırı
|
||||
|
||||
- Menü birim testleri eylem, rol ve kısayol bütünlüğünü; kaynak pencere ve
|
||||
dil doğrulamasını kontrol eder.
|
||||
- React testi yüklenen dil, iyimser geçiş ve kayıt hatasında geri dönüşün
|
||||
yerel köprüye iletildiğini doğrular.
|
||||
- Gerçek Electron E2E testi köprüyü kullanır. macOS'ta yerel menü etiketlerini,
|
||||
Windows'ta uygulama menüsünün kapalı kalmasını kontrol eder.
|
||||
- Windows üzerinde Türkçe ayar kaydı ve köprü testleri geçti. Paket yolu,
|
||||
mevcut Windows beta.5 dosyası üzerinde açılış ve içerik kontrolüyle doğrulandı.
|
||||
|
||||
Bu kaynak değişikliği, yayımlanan Windows beta.5 dosyasını değiştirmez.
|
||||
macOS dalındaki E2E testi, DMG/ZIP derlemesi ve gerçek Mac'te çalıştırma henüz
|
||||
doğrulanmadı. Mac paketi bu kanıtlar tamamlanmadan yayımlanmış sayılmaz.
|
||||
@@ -33,6 +33,9 @@ import { installErrorBannerGuard } from './test'
|
||||
const DESKTOP_ROOT = path.resolve(import.meta.dirname, '..')
|
||||
const REPO_ROOT = path.resolve(DESKTOP_ROOT, '..', '..')
|
||||
const RELEASE_ROOT = path.join(DESKTOP_ROOT, 'release')
|
||||
const PACKAGE_CONFIG = JSON.parse(fs.readFileSync(path.join(DESKTOP_ROOT, 'package.json'), 'utf8')).build as {
|
||||
executableName: string
|
||||
}
|
||||
|
||||
// ─── Credential stripping (matches launch.spec.ts) ──────────────────────
|
||||
|
||||
@@ -519,16 +522,19 @@ providers:
|
||||
*/
|
||||
function resolvePackagedBinaryPath(): string {
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(RELEASE_ROOT, 'win-unpacked', 'Hermes.exe')
|
||||
return path.join(RELEASE_ROOT, 'win-unpacked', `${PACKAGE_CONFIG.executableName}.exe`)
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const arch = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
|
||||
return path.join(RELEASE_ROOT, `mac-${arch}`, 'Hermes.app', 'Contents', 'MacOS', 'Hermes')
|
||||
return path.join(
|
||||
RELEASE_ROOT, arch === 'x64' ? 'mac' : `mac-${arch}`,
|
||||
`${PACKAGE_CONFIG.executableName}.app`, 'Contents', 'MacOS', PACKAGE_CONFIG.executableName
|
||||
)
|
||||
}
|
||||
|
||||
return path.join(RELEASE_ROOT, 'linux-unpacked', 'hermes')
|
||||
return path.join(RELEASE_ROOT, 'linux-unpacked', PACKAGE_CONFIG.executableName)
|
||||
}
|
||||
|
||||
export const PACKAGED_BINARY_PATH = resolvePackagedBinaryPath()
|
||||
|
||||
@@ -34,9 +34,9 @@ test.afterAll(async () => {
|
||||
fixture = null
|
||||
})
|
||||
|
||||
test('window opens with the Hermes title', async () => {
|
||||
test('window opens with the AITURK title', async () => {
|
||||
const title = await fixture!.page.title()
|
||||
expect(title).toContain('Hermes')
|
||||
expect(title).toContain('AITURK')
|
||||
})
|
||||
|
||||
test('renderer loads and shows DOM content', async () => {
|
||||
|
||||
@@ -6,7 +6,10 @@ import { expect, test } from './test'
|
||||
|
||||
let fixture: MockBackendFixture | null = null
|
||||
type DesktopTestWindow = Window & {
|
||||
hermesDesktop: { api<T = unknown>(request: { path: string }): Promise<T> }
|
||||
hermesDesktop: {
|
||||
api<T = unknown>(request: { path: string }): Promise<T>
|
||||
setUiLanguage(locale: string): void
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
@@ -75,3 +78,20 @@ test('Turkish safety settings persist the backend enum through the real desktop
|
||||
await expect(approvals.getByRole('combobox')).toContainText('Elle onayla')
|
||||
await page.screenshot({ path: test.info().outputPath('turkish-safety-settings.png'), fullPage: true })
|
||||
})
|
||||
|
||||
test('native language bridge updates the Mac menu and preserves menu-free Windows', async () => {
|
||||
const { app, page } = fixture!
|
||||
const platform = await app.evaluate(() => process.platform)
|
||||
for (const locale of ['en', 'tr']) {
|
||||
await page.evaluate(language => {
|
||||
;(window as unknown as DesktopTestWindow).hermesDesktop.setUiLanguage(language)
|
||||
}, locale)
|
||||
await expect
|
||||
.poll(() => app.evaluate(({ Menu }) => Menu.getApplicationMenu()?.items.map((item: { label: string }) => item.label) ?? null))
|
||||
.toEqual(
|
||||
platform === 'darwin'
|
||||
? expect.arrayContaining([locale === 'tr' ? 'Dosya' : 'File', locale === 'tr' ? 'Düzen' : 'Edit'])
|
||||
: null
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,10 +19,10 @@ const PLATFORM = process.platform
|
||||
// launch via install.ps1 / install.sh, per the Phase 1 thin-installer flow).
|
||||
const APP = (() => {
|
||||
if (PLATFORM === 'darwin') {
|
||||
const appPath = path.join(RELEASE_ROOT, `mac-${ARCH}`, 'Hermes.app')
|
||||
const appPath = path.join(RELEASE_ROOT, ARCH === 'x64' ? 'mac' : `mac-${ARCH}`, `${PACKAGE_JSON.build.executableName}.app`)
|
||||
return {
|
||||
appPath,
|
||||
binary: path.join(appPath, 'Contents', 'MacOS', 'Hermes'),
|
||||
binary: path.join(appPath, 'Contents', 'MacOS', PACKAGE_JSON.build.executableName),
|
||||
resourcesPath: path.join(appPath, 'Contents', 'Resources'),
|
||||
asarPath: path.join(appPath, 'Contents', 'Resources', 'app.asar'),
|
||||
unpackedDistIndex: path.join(appPath, 'Contents', 'Resources', 'app.asar.unpacked', 'dist', 'index.html')
|
||||
@@ -32,7 +32,7 @@ const APP = (() => {
|
||||
const unpacked = path.join(RELEASE_ROOT, 'win-unpacked')
|
||||
return {
|
||||
appPath: unpacked,
|
||||
binary: path.join(unpacked, 'Hermes.exe'),
|
||||
binary: path.join(unpacked, `${PACKAGE_JSON.build.executableName}.exe`),
|
||||
resourcesPath: path.join(unpacked, 'resources'),
|
||||
asarPath: path.join(unpacked, 'resources', 'app.asar'),
|
||||
unpackedDistIndex: path.join(unpacked, 'resources', 'app.asar.unpacked', 'dist', 'index.html')
|
||||
@@ -42,24 +42,24 @@ const APP = (() => {
|
||||
const unpacked = path.join(RELEASE_ROOT, 'linux-unpacked')
|
||||
return {
|
||||
appPath: unpacked,
|
||||
binary: path.join(unpacked, 'Hermes'),
|
||||
binary: path.join(unpacked, PACKAGE_JSON.build.executableName),
|
||||
resourcesPath: path.join(unpacked, 'resources'),
|
||||
asarPath: path.join(unpacked, 'resources', 'app.asar'),
|
||||
unpackedDistIndex: path.join(unpacked, 'resources', 'app.asar.unpacked', 'dist', 'index.html')
|
||||
}
|
||||
})()
|
||||
|
||||
// Default HERMES_HOME for non-sandboxed runs -- matches main.ts's
|
||||
// resolveHermesHome(). On Windows it's %LOCALAPPDATA%\hermes; elsewhere
|
||||
// it's ~/.hermes. The fresh-install sandbox launchFresh() sets its own
|
||||
// HERMES_HOME and never touches this.
|
||||
const DEFAULT_HERMES_HOME = (() => {
|
||||
// Match AITURK's product-owned runtime; never inspect another Hermes install.
|
||||
const DEFAULT_AGENT_HOME = (() => {
|
||||
if (process.env.AITURK_IDE_HOME) return path.resolve(process.env.AITURK_IDE_HOME)
|
||||
const userData = process.env.AITURK_IDE_USER_DATA_DIR || process.env.HERMES_DESKTOP_USER_DATA_DIR
|
||||
if (userData) return path.join(path.resolve(userData), 'agent-home')
|
||||
if (PLATFORM === 'win32' && process.env.LOCALAPPDATA) {
|
||||
return path.join(process.env.LOCALAPPDATA, 'hermes')
|
||||
return path.join(process.env.LOCALAPPDATA, 'TurkServis', 'AITURK-IDE', 'agent')
|
||||
}
|
||||
return path.join(os.homedir(), '.hermes')
|
||||
return path.join(os.homedir(), '.aiturk-ide', 'agent')
|
||||
})()
|
||||
const VENV_ROOT = path.join(DEFAULT_HERMES_HOME, 'hermes-agent', 'venv')
|
||||
const VENV_ROOT = path.join(DEFAULT_AGENT_HOME, 'hermes-agent', 'venv')
|
||||
const FRESH_SANDBOX_ROOT = path.join(os.tmpdir(), 'hermes-desktop-fresh-install')
|
||||
|
||||
function die(message) {
|
||||
@@ -124,10 +124,10 @@ function ensurePackagedApp() {
|
||||
|
||||
function resolveDmgPath() {
|
||||
if (!exists(RELEASE_ROOT)) {
|
||||
return path.join(RELEASE_ROOT, `Hermes-${PACKAGE_JSON.version}-${ARCH}.dmg`)
|
||||
return path.join(RELEASE_ROOT, `AITURK-IDE-${PACKAGE_JSON.version}-mac-${ARCH}.dmg`)
|
||||
}
|
||||
|
||||
const prefix = `Hermes-${PACKAGE_JSON.version}`
|
||||
const prefix = `AITURK-IDE-${PACKAGE_JSON.version}-mac-`
|
||||
const candidates = fs
|
||||
.readdirSync(RELEASE_ROOT)
|
||||
.filter(name => name.endsWith('.dmg'))
|
||||
@@ -141,15 +141,15 @@ function resolveDmgPath() {
|
||||
|
||||
return candidates.length > 0
|
||||
? path.join(RELEASE_ROOT, candidates[0])
|
||||
: path.join(RELEASE_ROOT, `Hermes-${PACKAGE_JSON.version}-${ARCH}.dmg`)
|
||||
: path.join(RELEASE_ROOT, `AITURK-IDE-${PACKAGE_JSON.version}-mac-${ARCH}.dmg`)
|
||||
}
|
||||
|
||||
function resolveNsisPath() {
|
||||
// electron-builder NSIS artifactName template is 'Hermes-${version}-${os}-${arch}.${ext}'
|
||||
// Select only this AITURK release and architecture.
|
||||
if (!exists(RELEASE_ROOT)) return null
|
||||
const candidates = fs
|
||||
.readdirSync(RELEASE_ROOT)
|
||||
.filter(name => /\.exe$/i.test(name) && /win/i.test(name))
|
||||
.filter(name => name === `AITURK-IDE-${PACKAGE_JSON.version}-win-${ARCH}.exe`)
|
||||
.sort((a, b) => {
|
||||
const aMtime = fs.statSync(path.join(RELEASE_ROOT, a)).mtimeMs
|
||||
const bMtime = fs.statSync(path.join(RELEASE_ROOT, b)).mtimeMs
|
||||
@@ -261,6 +261,9 @@ function launchFresh() {
|
||||
env.HERMES_DESKTOP_TEST_MODE = 'fresh-install'
|
||||
env.HERMES_DESKTOP_USER_DATA_DIR = userDataDir
|
||||
env.HERMES_HOME = hermesHome
|
||||
env.AITURK_IDE_HOME = hermesHome
|
||||
env.AITURK_IDE_USER_DATA_DIR = userDataDir
|
||||
delete env.HERMES_DESKTOP_PYTHON
|
||||
delete env.HERMES_DESKTOP_HERMES
|
||||
delete env.HERMES_DESKTOP_HERMES_ROOT
|
||||
|
||||
@@ -275,7 +278,7 @@ function launchFresh() {
|
||||
console.log('\nFresh install sandbox:')
|
||||
console.log(` root: ${sandbox}`)
|
||||
console.log(` electron userData: ${userDataDir}`)
|
||||
console.log(` HERMES_HOME: ${hermesHome}`)
|
||||
console.log(` AITURK_IDE_HOME: ${hermesHome}`)
|
||||
console.log(` cwd: ${cwd}`)
|
||||
|
||||
return { runtimeRoot: path.join(hermesHome, 'hermes-agent', 'venv') }
|
||||
@@ -399,8 +402,8 @@ function printArtifacts(options = {}) {
|
||||
|
||||
function help() {
|
||||
console.log(`Usage:
|
||||
npm run test:desktop:existing # build packaged app, launch with normal PATH/existing Hermes
|
||||
npm run test:desktop:fresh # build packaged app, launch with temp userData + HERMES_HOME
|
||||
npm run test:desktop:existing # build packaged app, launch with the existing AITURK profile
|
||||
npm run test:desktop:fresh # build packaged app, launch with temp userData + AITURK_IDE_HOME
|
||||
npm run test:desktop:dmg # (macOS only) build DMG and open it
|
||||
npm run test:desktop:nsis # (win32 only) build NSIS installer
|
||||
npm run test:desktop:all # build installer, validate app payload, print paths
|
||||
|
||||
Vendored
+1
@@ -17,6 +17,7 @@ export {}
|
||||
declare global {
|
||||
interface Window {
|
||||
hermesDesktop: {
|
||||
setUiLanguage?: (locale: string) => void
|
||||
// Resolve a backend connection. Omit `profile` (or pass the primary) for
|
||||
// the window's backend; pass a named profile to lazily spawn/reuse that
|
||||
// profile's backend from the pool.
|
||||
|
||||
@@ -30,6 +30,42 @@ describe('I18nProvider', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('keeps the native menu aligned with loaded, optimistic and rolled-back language', async () => {
|
||||
const original = window.hermesDesktop
|
||||
const setUiLanguage = vi.fn()
|
||||
window.hermesDesktop = { ...original, setUiLanguage }
|
||||
let rejectSave: (error: Error) => void = () => undefined
|
||||
|
||||
const configClient: I18nConfigClient = {
|
||||
getConfig: vi.fn().mockResolvedValue({ display: { language: 'en' } }),
|
||||
saveConfig: vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectSave = reject
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
render(
|
||||
<I18nProvider configClient={configClient}>
|
||||
<LanguageProbe target="tr" />
|
||||
</I18nProvider>
|
||||
)
|
||||
await waitFor(() => expect(setUiLanguage).toHaveBeenLastCalledWith('en'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
|
||||
await waitFor(() => expect(configClient.saveConfig).toHaveBeenCalledTimes(1))
|
||||
expect(setUiLanguage).toHaveBeenLastCalledWith('tr')
|
||||
rejectSave(new Error('Cannot save language'))
|
||||
await waitFor(() => expect(screen.getByTestId('save-error').textContent).toBe('Cannot save language'))
|
||||
expect(setUiLanguage).toHaveBeenLastCalledWith('en')
|
||||
expect(screen.getByTestId('locale').textContent).toBe('en')
|
||||
} finally {
|
||||
cleanup()
|
||||
window.hermesDesktop = original
|
||||
}
|
||||
})
|
||||
|
||||
it('defaults to Turkish without a config client', () => {
|
||||
render(
|
||||
<I18nProvider configClient={null}>
|
||||
|
||||
@@ -105,6 +105,10 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
|
||||
localeRef.current = locale
|
||||
setRuntimeI18nLocale(locale)
|
||||
applyDocumentLocale(locale)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.hermesDesktop?.setUiLanguage?.(locale)
|
||||
}
|
||||
}, [locale])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user