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.
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.
Here is what actually happens when you type with a phonetic IME:
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.
| You press | Expected | With a keyboard hook |
|---|---|---|
| the phonetic keys | a candidate list appears | nothing appears |
| a selection key | the character is inserted | there is nothing to select |
| plain English instead | works | works — 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.
It sounds easy: detect that an IME is composing, let those keys through, intercept everything else. In practice there are three obstacles:
ImmGetCompositionString needs the IME context of the target
window, and a low-level hook runs outside that thread.
The result is something that works "most of the time" — and CJK users hit unpredictable failures that are very hard to reproduce.
| Hook the keyboard | Don'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.
Ctrl+Alt+T in whatever text box you were usingEnter and the translation is inserted back into the original boxThe 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:
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.
| Hotkey | Purpose |
|---|---|
Ctrl+Alt+T | Toggle translation mode |
Ctrl+Alt+C | Bring focus back to the card |
Ctrl+Alt+Enter | Insert 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.
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.
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.
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.
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
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.
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