TL;DR — Mac-Translator is a single-purpose macOS menubar app: select text anywhere, Force Click (or ⌘⌥T), see the translation in a floating HUD next to your cursor, dismiss with Escape. No window. No tab. No paste. The product decision that made it stick was the input gesture, not the translation engine. The macOS gotchas below cost me 6 hours I won’t get back.
JTBD
When I’m reading an English/Vietnamese document, email, or chat, I want to translate a single phrase or sentence inline, so that I don’t break flow to open a translator tab, paste, and copy back.
The pain isn’t translation quality. Translation quality has been solved for years (DeepL, Google, Apple). The pain is the context switch: every translator UI demands I leave what I’m reading, open something, paste, wait, copy, switch back. For 6-word phrases, the round-trip cost > the value.
Existing alternatives — input gesture audit
| Tool | Input gesture | Why it broke flow |
|---|---|---|
| DeepL desktop app | ⌘C ⌘C (double copy) | Two-handed, requires clipboard, app steals focus |
| Apple Translate (Services menu) | Right-click → Services → Translate | 4 clicks, latency 1–2s, modal panel |
| Translate Tab | Browser-only | Doesn’t work in Mail, Slack, Notes, PDFs |
| Pop Clip + extension | Selection → popup menu → click translate | Extra hop, popup obscures text |
| Built-in macOS Translate (Sequoia) | Right-click → Translate | Same as Services — modal panel, breaks reading |
None of them solve “translate without leaving the sentence I’m reading.”
The product decision: Force Click as the trigger
Force Click (firm press on a Force Touch trackpad) was sitting unused on every Mac since 2015. Apple uses it for Look Up. That’s it.
Reframing it as Translate Selection has three properties no other input method has:
- One-handed: thumb on trackpad while reading.
- Zero-modal: HUD appears at cursor, dismissed by Escape or any keypress.
- Discoverable but invisible: doesn’t pollute right-click menus, doesn’t require remembering a chord.
The fallback chord (⌘⌥T) exists for non-trackpad users, but Force Click is the primary surface.
Why a translator without a window is a different product
I almost shipped a window-based version first. Pros: easier history, easier “translate longer document” feature. Cons: it would be the same product as every existing translator.
Killing the window forced three constraints that made the product:
- Output ≤ 280 chars or auto-truncate — anything longer means the user wanted a real translator, not this.
- No history UI — if you need to revisit, copy from the HUD before dismissing. The product is the moment, not the archive.
- No source-language picker — auto-detect or fail loud. Pickers are a “give me a menu instead of an answer” anti-pattern.
Less surface = more focus on the one thing.
The macOS gotchas (so you don’t repeat them)
Global pressure events ≠ NSEvent global monitor
NSEvent.addGlobalMonitorForEvents(matching: .pressure) silently doesn’t fire. Force Click pressure events are not delivered to global monitors. You need a CGEventTap on leftMouseDragged filtered by kCGMouseEventPressure + a movement-distance filter (to distinguish Force Click from a press-and-drag).
This cost me 4 hours of “why isn’t my handler called.”
AX/InputMonitoring/ScreenRecording permissions reset on every ad-hoc rebuild
If your app is ad-hoc signed (no Developer ID), the codehash changes on every rebuild. macOS treats the new hash as a different app and silently revokes Accessibility, Input Monitoring, and Screen Recording grants.
You don’t get an error. You get a working build that just doesn’t see global events anymore.
Fix workflow:
tccutil reset Accessibility com.your.app- Retoggle the permission in System Settings
- Restart the app — event watchers registered while AX was false stay dead silently
Don’t rebuild between “grant permission” and “test”
The cycle “grant → rebuild → test” looks like the permission was never granted. Always run the same binary you granted the permission to.
Numbers from month 2
| Metric | Value |
|---|---|
| Daily translations | ~40 |
| Median latency (DeepL backend) | 180ms |
| Force Click vs ⌘⌥T usage | 78% / 22% |
| Time-to-translation (gesture to HUD visible) | 220ms |
What I’d tell a PM building macOS utility apps
- Input gesture is the product. A great gesture with an OK backend beats a great backend with a bad gesture.
- Modal panels break flow. If your utility opens a window, it’s not a utility anymore — it’s an app you visit.
- Less surface = more focus. Killing the history/window/source-picker forced the product to be good at one thing.
- Permission reset is invisible. Build a startup check that logs “AX = false” so you don’t waste hours wondering why nothing fires.
- Ad-hoc signing is fine for personal apps, but document the rebuild ritual.
Single .app bundle, no installer, no analytics, no subscription. Backend: DeepL free tier (~$0/month at my volume). The product is the gesture.