LinLingo

v1.2.0 繁體中文

Why I don't hook the keyboard

The obvious way to build a tool that translates as you type anywhere is to hook the keyboard. I didn't, because it silently destroys Chinese, Japanese and Korean input methods. Here is why — and what I built instead.

The obvious approach: hook the keyboard

Windows lets you install a low-level keyboard hook (SetWindowsHookExW(WH_KEYBOARD_LL, ...)) that sees every keystroke before it reaches any application — and can swallow it.

For a translation tool that is very tempting. You see exactly what the user types in any text box, translate it, and inject the result back. The user never notices anything happened.

The problem is that "swallow it" part.

An IME does not send one character per key

Here is what actually happens when you type with a phonetic IME:

  1. You press the keys for the sounds, one after another
  2. The IME collects them into a composition buffer and shows you the in-progress text plus a candidate list
  3. You pick a candidate (number key, space, and so on)
  4. Only then does the IME commit the finished character to the application

Between step 1 and step 3 there is no finished text at all — just a sequence of undecided keystrokes.

If a low-level hook returns 1 (consume), those keystrokes never reach the IME. The IME never sees the first key, so it can never compose anything.

What it looks like

You pressExpectedWith a keyboard hook
the phonetic keysa candidate list appearsnothing appears
a selection keythe character is insertedthere is nothing to select
plain English insteadworksworks — which is why the bug ships

That last row is the trap. A developer testing in English sees a tool that works perfectly. The breakage only shows up for users of CJK input methods, and only sometimes.

Why "just pass through while composing" is hard

It sounds easy: detect that an IME is composing, let those keys through, intercept everything else. In practice there are three obstacles:

The result is something that works "most of the time" — and CJK users hit unpredictable failures that are very hard to reproduce.

The trade-off

Hook the keyboardDon't (type in the card)
Feel Magical — you type right where you were One extra card, but every key behaves normally
Chinese / Japanese / Korean Composition breaks Works
Password fields May be intercepted too Never touched — the card is a separate window
Edge cases you now own Backspace, candidate selection, paste, drag-select, modifier combos… The OS and the IME handle all of it

That left column is longer than it looks. Once you intercept the keyboard you are re-implementing a text editor — and you will never catch up with the real one.

What I built instead: the card is the input surface

  1. Press Ctrl+Alt+T in whatever text box you were using
  2. A floating card appears and takes focus
  3. You type in the card in your own language — Zhuyin, Pinyin, Japanese IME all work
  4. The translation appears below as you type
  5. Press Enter and the translation is inserted back into the original box

The important part is that nobody is guessing what you are typing. Your keystrokes go into a real text widget, and the OS and the IME do exactly what they were designed to do.

A lot of problems simply disappear:

Implementation choices worth mentioning

RegisterHotKey, not a keyboard hook

There are two ways to get a global hotkey: a low-level hook, or RegisterHotKey.

RegisterHotKey asks the system to post a WM_HOTKEY when the combination is pressed, and the keystroke still goes to the focused window as usual. That is exactly what I want — to be notified, not to block anything.

HotkeyPurpose
Ctrl+Alt+TToggle translation mode
Ctrl+Alt+CBring focus back to the card
Ctrl+Alt+EnterInsert the translation from anywhere

The third one is worth noting: a hotkey that must work while another window has focus is only reliably achievable with RegisterHotKey. A keyboard hook actually makes it harder, because it cannot reliably tell which text box currently has focus.

Focus and the foreground lock

Windows only lets the foreground process change the foreground window. A tray app is in the background, so handing focus to the card — or giving it back to the target text box — gets refused.

The workaround is to tap Alt first:

inject_key(VK_MENU)
inject_key(VK_MENU, KEYEVENTF_KEYUP)
user32.SetForegroundWindow(target)

That makes the system believe the user just provided input, which releases the lock. Alt on its own produces no text, so it is a safe choice.

Inserting the result: clipboard plus Ctrl+V

You can insert text into someone else's text box either by simulating each keystroke, or through the clipboard.

Simulating keystrokes (SendInput) sounds cleaner but breaks down in practice: CJK text and emoji do not always survive VK_PACKET, and you end up triggering the target app's key handling.

Clipboard plus Ctrl+V works almost everywhere. Its only real downside is overwriting whatever the user had copied.

Window placement: why not Tk's geometry()

A small thing that cost real time. Tk's geometry() interprets negative coordinates as "this far from the right/bottom edge", so "move this window onto the monitor to the left" silently lands somewhere strange.

The fix is to skip Tk and call Win32 SetWindowPos with SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE — reposition only, without resizing, restacking or stealing focus.

DPI and mixed-scale monitors

On a multi-monitor setup with different scale factors, a process that has not declared DPI awareness receives virtualised coordinates and the window drifts. One call at startup fixes it:

user32.SetProcessDpiAwarenessContext(-4)   # PER_MONITOR_AWARE_V2

When a hook is the right tool

Hooks are not evil; they are just used in the wrong place. If your tool's job is to record or block keystrokes — a password manager, a macro recorder — a hook is exactly right.

The question to ask is: does my tool need to know what the user typed? If yes, you have to solve the IME problem. If no, keep your hands off the keyboard.

Closing

Not hooking the keyboard removed an entire class of bugs. The cost was one extra card. For a tool that has to work inside every other application, that was an easy trade.

If you are building something similar, I hope this saves you some time.

LinLingo is the Windows translator that came out of these trade-offs: press Ctrl+Alt+T, type in the card, press Enter to insert. Zhuyin, Pinyin and other IMEs compose normally. Overview · Download · Source