Skip to main content
All articles
Tool launch
We Got Fed Up With DeepL, So We Built a Double-Cmd+C Translator in 200 Lines of Lua

Tool launch

We Got Fed Up With DeepL, So We Built a Double-Cmd+C Translator in 200 Lines of Lua

DeepL on macOS is clunky, bloated, and wants you on a subscription. We replaced it with a single Hammerspoon script that pipes selected text through Claude. Open source, costs ~$0.0001 per translation, and the whole thing fits in one file.

Claude APIDeveloper ToolsmacOSOpen Source
AuthorSantiago Lobo
RoleFounder & Lead Developer
Published2026-04-20
Reading time6 min
Sections6
Scroll to read

01 / READ

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.

lua
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.

lua
-- 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.