An EN↔RU translator that lives in the MacBook notch. Hover over the notch and a drawer slides down with an input field.
Full description in SPEC.md.
Translation runs on Apple's on-device Translation framework: no API keys, no quotas, no network. Dictionary lookups use Foundation Models, also on-device.
- macOS 26.0+
- Xcode 26+
- XcodeGen (
brew install xcodegen)
Island.xcodeproj is not checked in — it is generated from project.yml.
make run # generate, build and run with logging in the terminal
make build # build only
make project # generate Island.xcodeproj only (for working in Xcode)
make stop # kill a running instance
make cleanThe app runs without a Dock icon. Everything else is under the menu bar icon: toggle the drawer, tint the panel for debugging, run the translation self-test, quit.
Translation is hard to verify by hand from a script, because typing into the drawer would mean posting synthetic key events, which needs the Accessibility permission. Instead:
ISLAND_SELFTEST=1 build/Build/Products/Debug/Island.app/Contents/MacOS/IslandIt drives the whole flow and reports pass/fail per check: automatic direction detection in both directions, translation, drawer growth, copying the result, clipboard auto-paste on open, and direction pinning. The same is available from the menu bar.
Key bindings still need a human — pressing them from a script would mean posting synthetic key events, which needs the Accessibility permission this app is designed never to ask for. Everything those bindings call is covered.
| Stage | Scope | State |
|---|---|---|
| 0 | Skeleton, LSUIElement, menu bar |
✅ |
| 1 | Window on the notch above the menu bar | ✅ |
| 2 | Hover, expansion, input focus | ✅ |
| 3 | Translation via Apple Translation | ✅ |
| 4 | Language detection, hotkeys, clipboard | ✅ |
| 5 | History and settings | ✅ |
| 6 | Dictionary mode, polish | ✅ |
-
The window sits at window layer 25 against 24 for the menu bar, and is first in the Window Server stack.
-
The notch is derived from
NSScreen: 185×32 pt centred on the top edge of a 16" MacBook Pro. -
NSTrackingAreadeliversmouseEnteredeven though the window is fully click-through, so no Accessibility permission is required. -
After expanding, the panel becomes key and the first responder is the text field editor. Activation is asynchronous — focus lands roughly 300 ms later.
-
Moving the cursor away collapses the drawer and returns focus to the previous app.
-
EN→RU and RU→EN both translate correctly on-device.
-
Direction detection picks the right pair from the text alone, and a manual pin survives further typing until the drawer closes.
-
The Carbon global hotkey registers with no permission prompt.
-
Clipboard auto-paste fills and translates on open; copying the result does not paste it back next time.
-
History records on close, deduplicates a repeated phrase, pins, reloads into the input, and survives a restart on disk.
-
Dictionary mode hides itself with Apple Intelligence off, and clears with the input.
-
Opening on hover leaves the panel non-key: verified by driving the cursor onto the notch (
isKeyWindow: false, keyboard still with the other app) and then into the drawer body (isKeyWindow: true, first responder is the text field).
Not verified yet: behaviour with networking off, in full screen, with an external monitor attached, and dictionary mode with Apple Intelligence actually on — this machine has it off, so only the degraded path is covered.
- No network. The app makes no network requests of any kind. Translation and dictionary lookups run on-device; the only traffic macOS makes is the one-off language asset download, handled by the system Translation framework itself.
- No permissions. Nothing is requested at any point — no Accessibility, no Screen
Recording, no Full Disk Access. Hover uses an
NSTrackingAreaand the global hotkey uses Carbon, precisely because both work without prompting. - History lives at
~/Library/Application Support/com.nikolaik.island/history.json, created0600inside a0700directory, since it holds everything you have translated. It can be turned off and deleted from Settings → General. - The clipboard is read only at the moment the drawer opens, and only to prefill the input. Nothing polls it in the background. Toggle it off under Settings → Behaviour.
- Debug logging and the self-test are compiled out of Release builds entirely — verified by inspecting the binary.
- App Sandbox is off because the app is not distributed through the App Store. Hardened Runtime is on; ad-hoc signing disables it for local Debug builds.
Opening the drawer on hover deliberately leaves the keyboard where it was. A cursor resting on the notch while you type in another app would otherwise capture those keystrokes — in the worst case part of a password, which would then be translated and written to history.
The drawer becomes typable only when the cursor moves into its body, below the notch strip, which is a deliberate movement rather than a parked cursor. Summoning it with the global hotkey gives it focus straight away, since the cursor is nowhere near it. Clipboard auto-paste still runs on hover, so glancing at a translation needs no click at all.
Type a single word and, when Apple Intelligence is available, alternative meanings and an example sentence appear under the translation. It runs on the on-device model, is cancelled whenever the input changes, and never delays the translation itself. With Apple Intelligence off the block is simply absent — the reason goes to the log, never to the UI.
| Key | Action |
|---|---|
⌥Space |
Open/close the drawer (global) |
Esc |
Close |
⌘↩ |
Copy the result and close |
⌘⇧C |
Copy the result, stay open |
⌘L |
Flip direction and pin it |
⌘H |
Show or hide history |
⌘K |
Clear the input |
⇧↩ |
Newline |
prepareTranslation()returns as soon as the asset download has been requested. Callingtranslate()before the assets land blocks forever without throwing, sosession.isReadyis polled until it flips.NSApp.activate()is asynchronous; asserting onisKeyWindowright after it always fails.- An
NSTrackingAreainstalled while the cursor is already inside does not reliably delivermouseExited, which is why cursor exit is polled while the drawer is open. TranslationSessioncannot satisfy complete strict concurrency in either isolation direction;@preconcurrency import Translationscopes the exemption to one file.
xcodebuild -project Island.xcodeproj -scheme Island \
-configuration Release -derivedDataPath build-release build
cp -R build-release/Build/Products/Release/Island.app /Applications/Launch at login only works from a real location such as /Applications — SMAppService
refuses to register an app running out of a build directory.
The build is ad-hoc signed and not notarised, so Gatekeeper will ask on first launch. Signing with a Developer ID certificate and notarising removes that.
Island/
├── IslandApp.swift entry point, MenuBarExtra, NSApplicationDelegate
├── App/ coordinator, menu bar, logging
├── Notch/ notch geometry, NSPanel, hit-testing, state machine
├── Translation/ session hub and the SwiftUI bridge
├── Model/ drawer state, direction, preferences, history
├── Support/ global hotkey, clipboard, hotkey presets, launch at login
└── UI/ SwiftUI drawer, history list, settings
MIT — see LICENSE.