WinTranslator is a tray-only desktop translation utility (no dock icon, no main window). User selects text in any app, presses a global hotkey, and a floating popup streams LLM translation results. Supports macOS, Windows, Linux.
- Version: 1.2.8
- Bundle ID:
com.wintranslator.app - Repo: https://github.com/cornradio/WinTranslator
- Runtime: Electron 36
- Frontend: React 19 + TypeScript 5.7
- Build: Vite 6 + vite-plugin-electron
- Packaging: electron-builder 25
- Text capture: @nut-tree-fork/nut-js (simulates Cmd+C / Ctrl+C)
- LLM: Anthropic Messages API or OpenAI-compatible (configurable)
| Command | What it does |
|---|---|
npm run dev |
Start Vite dev server + Electron (hot reload) |
npm run build |
Vite build only (no packaging) |
npm run build:mac |
Full macOS build → DMG + ZIP in release/ |
npm run build:win |
Full Windows build → NSIS + portable in release/ |
npm run typecheck |
tsc --noEmit (no output = pass) |
npm run clean |
Delete release/, dist/, dist-electron/ |
Main Process (Node.js) Renderer (Browser)
┌──────────────────────┐ ┌──────────────────────┐
│ index.ts (entry) │ │ main.tsx (entry) │
│ hotkeys.ts │──IPC──▶│ popup/App.tsx │
│ clipboard.ts │ │ settings/App.tsx │
│ tray.ts │ └──────────────────────┘
│ store.ts │ ▲
│ ipc-handlers.ts │ Preload │ contextBridge
│ windows/ │────────┘ │
│ popup-window.ts │ ▼
│ settings-window.ts│ window.electronAPI
└──────────────────────┘
Two windows, one HTML entry:
index.htmlis the single entry point for both windows- Hash routing:
#popup→ popup UI, anything else → settings UI - Popup: frameless, transparent, always-on-top, never destroyed (hide on close)
- Settings: standard framed window, single-instance (focus if already open)
Data flow:
- Global hotkey fires → main process captures selected text (simulates Cmd+C)
- Main process shows popup window near cursor, sends text via IPC
popup:show-text - Renderer streams parallel LLM requests (one per prompt in the FunctionGroup)
- Results render with Markdown (react-markdown + remark-gfm)
| File | Role |
|---|---|
index.ts |
App entry. Dock hide, macOS state restoration prevention, hotkey/tray/IPC setup, popup creation, first-run detection |
hotkeys.ts |
Global hotkey registration via globalShortcut. Only registers FunctionGroup hotkeys (NOT settings shortcut — that was removed to avoid stealing Cmd+, from other apps) |
clipboard.ts |
Text capture: simulates Cmd+C via nut-js on macOS, VBScript/PowerShell/nut-js on Windows |
store.ts |
JSON file store in userData/config.json. Settings + translation history with deep-merge defaults |
tray.ts |
System tray icon + context menu. Dynamic menu from FunctionGroups, history, settings, quit |
ipc-handlers.ts |
All IPC handler registrations. Re-registers hotkeys when functions change |
windows/popup-window.ts |
Floating popup BrowserWindow. Hide-on-close pattern, cursor positioning |
windows/settings-window.ts |
Settings BrowserWindow. Single-instance, tab-based UI |
| File | Role |
|---|---|
main.tsx |
React entry. Hash-based routing: #popup → PopupApp, else SettingsApp |
popup/App.tsx |
Popup UI. Receives text, streams LLM results, keyboard shortcuts (ESC/Cmd+C/Shift+C/arrows), auto-dismiss, chat-to-AI-services menu |
settings/App.tsx |
Settings shell with tab bar (AI, Functions, Appearance, General), save/cancel, export/import |
settings-components/ |
Individual setting tabs: AIConfigTab, FunctionsTab (hotkey recorder), AppearanceTab (theme/opacity/blur), GeneralTab (auto-hide/launch-at-login) |
components/ |
Shared UI: StreamingText (Markdown + cursor), CopyButton, ErrorBanner, HistoryList |
hooks/ |
useAutoResize (debounced popup height), useAutoDismiss (auto-hide timer) |
lib/api-client.ts |
LLM streaming client. Anthropic Messages API + OpenAI Chat Completions with SSE parsing |
| File | Role |
|---|---|
types.ts |
All TypeScript types: FunctionGroup, AppSettings, ElectronAPI (context bridge interface), etc. |
constants.ts |
Defaults (2 built-in FunctionGroups: Translate Alt+T, Refine Alt+R), popup dimensions, version |
ipc-channels.ts |
IPC channel name constants (21 channels total) |
The fundamental unit of functionality. Each group has a name, icon, hotkey, and one or more prompts. When triggered, all prompts in the group run in parallel, each producing a streaming result card. Default groups: "Translate" (1 prompt) and "Refine" (2 prompts: Concise + Polished).
On hotkey press, main process simulates Cmd+C to copy selected text, then reads clipboard. Falls back to existing clipboard content if capture fails. This avoids requiring accessibility permissions for text reading.
Popup is created once at startup (show: false), never destroyed. Show/hide cycle:
showPopupAtCursor()→ position near cursor,win.show()- Send text to renderer via IPC
- Renderer streams results
- Auto-dismiss timer or ESC/blur hides the popup
closeevent is intercepted →hide()instead of destroy
Uses Electron's globalShortcut module. Only FunctionGroup hotkeys are registered globally. The Settings shortcut (Cmd+,) was intentionally removed from global registration to avoid intercepting the standard macOS Preferences shortcut from other apps. Settings is accessible via tray menu only.
-
Electron 36 API gaps:
BrowserWindowhas nosetRestorable()method and norestorableconstructor option.apphas nobrowser-window-restoreevent. To prevent macOS window state restoration, useexecSync('defaults write com.wintranslator.app NSQuitAlwaysKeepsWindows -bool false'). -
Global Cmd+, bug (fixed): Previously
hotkeys.tsregisteredCommand+,as a global hotkey, stealing it from all other apps. This was removed; tray menu accelerator is display-only. -
macOS state restoration (fixed): Login Items + macOS "Reopen windows when logging back in" would restore Settings/popup windows with stale Vite dev URLs. Fixed by disabling
NSQuitAlwaysKeepsWindowsviadefaults writeon each startup. -
First-run detection: Uses a marker file
.setup-done(project root in dev,userDatain packaged). Previous approach usinghasCompletedSetupin config store was unreliable — only set after API key entry, causing repeated first-run triggers. -
Launch at Login: Implemented via
app.setLoginItemSettings({ openAtLogin, args: [] }). Emptyargsprevents Vite dev flags from leaking into the login item. -
nut-js external: Must be in
rollupOptions.externalin vite.config.ts — it's a native module that can't be bundled.
dist/— Renderer build (HTML + CSS + JS)dist-electron/main/— Main process builddist-electron/preload/— Preload script buildrelease/— Packaged apps (DMG, NSIS, AppImage, etc.)
@shared→src/shared@renderer→src/renderer
Three fixes applied but not yet committed:
src/main/hotkeys.ts— Removed globalCommand+,registration andsetSettingsHandlersrc/main/index.ts— AddedexecSyncforNSQuitAlwaysKeepsWindows, switched first-run detection to marker file, removedgetSettingimport.gitignore— Added.setup-done