The Problem: DeepL Is a 200 MB App for a Keyboard Shortcut
I live in Madrid and handle German correspondence constantly: Finanzamt letters, Postbank notices, invoices, Arbeitsagentur forms. For years I used DeepL's macOS app, which advertises a slick double-Cmd+C translator. In practice, the app is 200 MB of Electron wrapper, auto-launches on boot, nags about subscriptions, and the popup window blocks the text I am actively reading. Uninstalling it left behind four launch agents and three preference files.
Apple's built-in Translate is worse. It requires you to open a separate app, paste your text, wait, then copy back. The whole point of a keyboard shortcut translator is that it lives out of the way until you need it. Neither DeepL nor Apple Translate clear that bar.
I wanted the ergonomics of DeepL's double-Cmd+C gesture with none of the bloat. One Lua file. One API key. No menu bar icon. No subscription. No telemetry. That became copy-translate.
copy-translate is open source at github.com/displace-agency/copy-translate. MIT licensed. ~200 lines of Lua. Install via git clone and a single ./install.sh.
By the numbers
$0.0001
Per translation
200
Lines of Lua
10 MB
Total footprint
0
Background apps
The Stack: Hammerspoon + Claude Haiku
Hammerspoon is a free, open-source macOS automation tool. It exposes the entire Objective-C event system as a Lua API, which means you can hook global keyboard shortcuts, read the clipboard, draw windows, and make HTTP requests from ~10 MB of pure Lua. Most people use it for window management. We use it as a zero-cost app container.
The translation engine is Claude Haiku 4.5 via the Anthropic Messages API. Haiku is fast, cheap, and good enough for context-aware translation that beats rule-based engines like DeepL. The full pipeline is: user selects text, presses Cmd+C twice within 400 ms, Hammerspoon reads the clipboard, posts to api.anthropic.com, renders the response in a side-by-side popup. End-to-end latency is around 600 ms on a warm connection.
The economics are absurd. Haiku 4.5 is roughly $0.80 per million input tokens and $4 per million output tokens. A typical German paragraph plus its English translation is under 500 tokens total, which works out to $0.0001 per translation. A thousand translations cost ten cents. DeepL Pro is $10.99 per month.
How Double-Cmd+C Detection Works
The clever bit is that the first Cmd+C is never swallowed. Normal copy still works, because the event tap returns false and lets the keystroke propagate to the focused app. The tap only triggers the translator when a second Cmd+C arrives within the configured window. This means every user in the world who already uses Cmd+C can layer our tool on top without changing a single muscle memory.
local lastCmdC = 0
local DOUBLE_TAP_WINDOW = 0.4
local watcher = eventtap.new({ eventtap.event.types.keyDown }, function(event)
local flags = event:getFlags()
if event:getKeyCode() == 8 and flags.cmd then
local now = timer.secondsSinceEpoch()
if now - lastCmdC < DOUBLE_TAP_WINDOW then
lastCmdC = 0
timer.doAfter(0.12, translate) -- let first Cmd+C finish copying
else
lastCmdC = now
end
end
return false -- never swallow the keystroke
end)
watcher:start()The 120 ms delay before calling translate() matters. macOS pasteboard writes are asynchronous, and if we read the clipboard immediately after the second Cmd+C, we sometimes get the old contents. A single animation frame of delay resolves it.
The Side-by-Side Popup
The popup is an hs.webview — a WebKit view Hammerspoon can embed in a native macOS window. That means we render the UI with HTML and CSS rather than wrestling with AppKit. Original text on the left, translation on the right, close button in the corner, click outside to dismiss, Esc to close. All under 80 lines of CSS.
The translated text is also copied to the clipboard, so pasting with Cmd+V after the popup closes gives you the translation ready to drop into a reply. That single detail turned this from a toy into a daily-driver tool. Reading a German bank letter and drafting an English response back to my accountant now takes 30 seconds instead of three minutes.
- Side-by-side panes: original left (darker), translation right
- Close: × button, Esc key, or click anywhere outside
- Translation auto-copied to clipboard for immediate paste
- HUD-style floating window, does not steal focus from the current app
- Configurable target language (default: German/Spanish/any → English, English → Spanish)
- Configurable popup size, hotkey window, and model
Swap the Engine in Three Lines
The Anthropic API call is maybe 15 lines. The prompt is a plain string constant at the top of the file. If you want to swap Claude for OpenAI, Gemini, or a local Ollama model, the change is three lines: the endpoint URL, the auth header, and the response path. This is a deliberate design choice — the tool is infrastructure, not a product, and you should own the LLM pipeline.
If you want to change the target language, edit the PROMPT constant. If you want Chinese → English only, rewrite the prompt to say so. If you want the popup wider, change POPUP_W. Every knob is a labeled constant at the top of init.lua. We have explicitly refused to add a preferences UI, because preferences UIs are where simple tools go to die.
-- Default (German/Spanish/any → English, English → Spanish) local PROMPT = [[Translate the following text to English. If the text is already in English, translate it to Spanish instead. Output ONLY the translation — no preamble, no quotes, no notes. Text: ]] -- Custom: force Spanish → English only local PROMPT = [[Translate the following Spanish text to English. Output ONLY the translation. Text: ]]
Why We Open-Sourced It
The tool solves a problem we had. Shipping it as a SaaS would require pricing, auth, a landing page, support, and an ongoing commitment we do not want. Shipping it as open source costs a GitHub repo and a README. The marginal cost is zero, and the tool becomes more robust as more people use it, fork it, and send edge-case bug reports.
This is the third open-source tool we have published this year, after x-bookmarks-exporter and FocusGuard. The pattern is consistent: if you find yourself paying a subscription for something a 200-line script could do, write the 200-line script and publish it. The web gets better one scratched itch at a time.
copy-translate is live at github.com/displace-agency/copy-translate. One file. MIT license. Swap in your own API key and it works in under a minute.

