Skip to content
Open
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
3 changes: 3 additions & 0 deletions apps/desktop/assets/ru.lproj/InfoPlist.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"NSAudioCaptureUsageDescription" = "Hermes использует захват аудио для голосовых диалогов.";
"NSCameraUsageDescription" = "Hermes использует камеру, когда этого требует включённый вами плагин или функция.";
"NSMicrophoneUsageDescription" = "Hermes использует микрофон для голосового ввода и голосовых диалогов.";
62 changes: 34 additions & 28 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ import {
tokenNeedsRefresh
} from './native-oauth'
import { runNativeLogin } from './native-oauth-login'
import { currentNativeCopy } from './native-copy'
import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request'
import { createKeepAwake } from './power-save'
import { FirstRunSetupResetError, runPrimaryBackendStartup } from './primary-backend-startup'
Expand Down Expand Up @@ -1104,7 +1105,7 @@ let nativeThemeListenerInstalled = false
let bootProgressState = {
error: null,
fakeMode: BOOT_FAKE_MODE,
message: 'Waiting to start Hermes backend',
message: currentNativeCopy().waitingBackend,
phase: 'idle',
progress: 0,
running: false,
Expand Down Expand Up @@ -1602,7 +1603,7 @@ function getFirstRunSetupGate() {
updateBootProgress(
{
error: null,
message: `Still waiting for first-run setup choice after ${Math.round(stuckAfterMs / 1000)} seconds`,
message: currentNativeCopy().waitingFirstRunChoiceFor(Math.round(stuckAfterMs / 1000)),
phase: 'bootstrap.choice',
progress: 12,
running: true
Expand All @@ -1627,7 +1628,7 @@ async function waitForFirstRunSetupChoice(backend) {
updateBootProgress(
{
error: null,
message: 'Waiting for first-run setup choice',
message: currentNativeCopy().waitingFirstRunChoice,
phase: 'bootstrap.choice',
progress: 12,
running: true
Expand Down Expand Up @@ -2449,7 +2450,7 @@ async function checkUpdates() {
return {
supported: false,
reason: 'not-a-git-checkout',
message: `${updateRoot} isn't a git checkout — desktop self-update only runs against a source install.`,
message: currentNativeCopy().notGitCheckout(updateRoot),
hermesRoot: updateRoot,
branch
}
Expand Down Expand Up @@ -3294,7 +3295,7 @@ async function applyUpdatesPosixInApp(opts: any) {
// best effort
}

emitUpdateProgress({ stage: 'update', message: 'Updating Hermes (git + dependencies)…', percent: 10 })
emitUpdateProgress({ stage: 'update', message: currentNativeCopy().updatingHermes, percent: 10 })

const updated = (await runStreamedUpdate(hermes, ['update', '--yes', ...branchArgs], {
cwd: updateRoot,
Expand All @@ -3303,7 +3304,11 @@ async function applyUpdatesPosixInApp(opts: any) {
})) as any

if (updated.code !== 0) {
emitUpdateProgress({ stage: 'error', message: 'hermes update failed.', error: updated.error || 'update-failed' })
emitUpdateProgress({
stage: 'error',
message: currentNativeCopy().updateFailed,
error: updated.error || 'update-failed'
})

return { ok: false, error: 'hermes update failed' }
}
Expand All @@ -3315,7 +3320,7 @@ async function applyUpdatesPosixInApp(opts: any) {
// off the healed dist so we reach the swap+relaunch below instead of bailing.
const rebuilt = await runRebuildWithRetry(attempt => {
if (attempt > 0) {
emitUpdateProgress({ stage: 'rebuild', message: 'Retrying the desktop rebuild…', percent: 60 })
emitUpdateProgress({ stage: 'rebuild', message: currentNativeCopy().retryingDesktopRebuild, percent: 60 })
}

return runStreamedUpdate(hermes, ['desktop', '--build-only'], { cwd: updateRoot, env, stage: 'rebuild' })
Expand All @@ -3324,7 +3329,7 @@ async function applyUpdatesPosixInApp(opts: any) {
if (rebuilt.code !== 0) {
emitUpdateProgress({
stage: 'error',
message: 'Backend updated, but the desktop rebuild failed. Restart Hermes to retry.',
message: currentNativeCopy().backendUpdatedDesktopBuildFailed,
error: rebuilt.error || 'rebuild-failed'
})

Expand Down Expand Up @@ -3371,7 +3376,7 @@ async function applyUpdatesPosixInApp(opts: any) {
const outcome = decideRelaunchOutcome({ underUnpacked, sandboxOk })

if (outcome === 'relaunch') {
emitUpdateProgress({ stage: 'restart', message: 'Restarting Hermes…', percent: 100 })
emitUpdateProgress({ stage: 'restart', message: currentNativeCopy().restartingHermes, percent: 100 })
// Preserve launch context across the re-exec: replay the original args
// (filtered of Electron internals) and the env/cwd that define which
// backend/profile/root this instance talks to. Without this the
Expand Down Expand Up @@ -3409,7 +3414,7 @@ async function applyUpdatesPosixInApp(opts: any) {
backendUpdated: true,
guiUpdated: false,
manualRestart: true,
message: 'Backend updated. Quit and reopen Hermes to load the new version.'
message: currentNativeCopy().backendUpdatedReopen
}
}
}
Expand Down Expand Up @@ -3468,7 +3473,7 @@ async function applyUpdatesPosixInApp(opts: any) {
return { ok: true, backendUpdated: true, rebuiltApp: rebuiltApp || null }
}

emitUpdateProgress({ stage: 'restart', message: 'Installing the updated app and restarting…', percent: 95 })
emitUpdateProgress({ stage: 'restart', message: currentNativeCopy().installingUpdatedApp, percent: 95 })

// Detached swapper: wait for THIS process to exit (so the bundle is free),
// ditto the rebuilt app over the running one, clear quarantine, relaunch.
Expand Down Expand Up @@ -3500,7 +3505,7 @@ fi
} catch (err) {
emitUpdateProgress({
stage: 'done',
message: 'Backend + app updated. Restart Hermes to load the new version.',
message: currentNativeCopy().backendAndAppUpdated,
percent: 100
})
rememberLog(`[updates] could not write swap script: ${err.message}; rebuilt app at ${rebuiltApp}`)
Expand Down Expand Up @@ -3967,7 +3972,7 @@ function resolveHermesBackend(backendArgs) {
// is a recoverable state the GUI can drive through.
return {
kind: 'bootstrap-needed',
label: 'Hermes Agent not installed yet; bootstrap required',
label: currentNativeCopy().bootstrapRequired,
command: null,
args: backendArgs,
bootstrap: true,
Expand Down Expand Up @@ -4140,7 +4145,7 @@ async function ensureRuntime(backend) {
backend.label = `Hermes at ${ACTIVE_HERMES_ROOT} (venv: ${VENV_ROOT})`
updateBootProgress({
phase: 'runtime.ready',
message: 'Hermes runtime is ready',
message: currentNativeCopy().runtimeReady,
progress: 82,
running: true,
error: null
Expand Down Expand Up @@ -4763,7 +4768,7 @@ async function saveImageFromUrl(rawUrl) {
const fallbackName = filenameFromUrl(rawUrl, `image${extensionForMimeType(mimeType) || '.png'}`)

const result = await dialog.showSaveDialog(mainWindow, {
title: 'Save Image',
title: currentNativeCopy().saveImage,
defaultPath: fallbackName
})

Expand Down Expand Up @@ -5254,18 +5259,19 @@ function sendWindowStateChanged(nextIsFullscreen?: boolean, target = mainWindow)
}

function buildApplicationMenu() {
const copy = currentNativeCopy()
const template = []

const checkForUpdatesItem = {
label: 'Check for Updates…',
label: copy.checkForUpdates,
click: () => sendOpenUpdatesRequested()
}

if (IS_MAC) {
template.push({
label: APP_NAME,
submenu: [
{ label: `About ${APP_NAME}`, click: () => showAboutPanelFresh() },
{ label: copy.about(APP_NAME), click: () => showAboutPanelFresh() },
checkForUpdatesItem,
{ type: 'separator' },
{ role: 'services' },
Expand All @@ -5280,16 +5286,16 @@ function buildApplicationMenu() {
}

template.push({
label: 'File',
label: copy.file,
submenu: [
// No accelerator: ⌘⇧N is a rebindable renderer keybind (session.newWindow);
// a menu accelerator would fight the rebind panel and (on macOS) be
// swallowed before the renderer sees it. Here purely for discoverability.
{ click: () => createInstanceWindow(), label: 'New Window' },
{ click: () => createInstanceWindow(), label: copy.newWindow },
// Same no-accelerator rationale: ⌘O is the rebindable renderer keybind
// (workspace.openFolder). Clicking runs the same open-folder-as-project
// flow through the renderer.
{ click: () => sendOpenFolderRequested(), label: 'Open Folder…' },
{ click: () => sendOpenFolderRequested(), label: copy.openFolder },
{ type: 'separator' },
IS_MAC
? {
Expand All @@ -5300,13 +5306,13 @@ function buildApplicationMenu() {
// renderer's close-active-tab. Clicking the item still closes the tab
// (or window) via the same request.
click: () => sendClosePreviewRequested(),
label: 'Close'
label: copy.close
}
: { role: 'quit' }
]
})
template.push({
label: 'Edit',
label: copy.edit,
submenu: [
{ role: 'undo' },
{ role: 'redo' },
Expand All @@ -5319,21 +5325,21 @@ function buildApplicationMenu() {
]
})
template.push({
label: 'View',
label: copy.view,
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{
label: 'Actual Size',
label: copy.actualSize,
accelerator: 'CommandOrControl+0',
click: () => {
setAndPersistZoomLevel(mainWindow, DEFAULT_ZOOM_LEVEL)
}
},
{
label: 'Zoom In',
label: copy.zoomIn,
accelerator: 'CommandOrControl+Plus',
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
Expand All @@ -5342,7 +5348,7 @@ function buildApplicationMenu() {
}
},
{
label: 'Zoom Out',
label: copy.zoomOut,
accelerator: 'CommandOrControl+-',
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
Expand All @@ -5355,13 +5361,13 @@ function buildApplicationMenu() {
]
})
template.push({
label: 'Window',
label: copy.window,
submenu: IS_MAC
? [{ role: 'minimize' }, { role: 'zoom' }, { role: 'front' }]
: [{ role: 'minimize' }, { role: 'close' }]
})
template.push({
label: 'Help',
label: copy.help,
role: 'help',
submenu: [checkForUpdatesItem]
})
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/electron/native-copy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from 'vitest'

import { nativeCopyForLocale } from './native-copy'

test('nativeCopyForLocale keeps English as the fallback', () => {
expect(nativeCopyForLocale(undefined).file).toBe('File')
expect(nativeCopyForLocale('de-DE').checkForUpdates).toBe('Check for Updates…')
})

test('nativeCopyForLocale recognizes Russian locale tags', () => {
const copy = nativeCopyForLocale('ru_RU')

expect(copy.file).toBe('Файл')
expect(copy.saveImage).toBe('Сохранить изображение')
expect(copy.copyImageAddress).toBe('Копировать адрес изображения')
expect(copy.waitingBackend).toBe('Ожидаем запуска бэкенда Hermes')
expect(copy.waitingFirstRunChoiceFor(12)).toBe('Ожидаем выбора способа первоначальной настройки уже 12 с')
expect(copy.backendStartFailed('boom')).toBe('Не удалось запустить бэкенд Hermes: boom')
expect(copy.desktopBootFailed('boom')).toBe('Не удалось запустить приложение: boom')
expect(copy.notGitCheckout('/tmp/hermes')).toContain('/tmp/hermes не является Git-репозиторием')
expect(copy.uninstallerAgentMissing('/tmp/venv')).toContain('/tmp/venv')
expect(copy.quitMessage(1)).toBe('Hermes всё ещё работает в 1 чате.')
expect(copy.quitMessage(5)).toBe('Hermes всё ещё работает в 5 чатах.')
})
Loading