Forgot how to split a character?Look it up.
Years of typing Wubi 86, and characters like 鼎 still trip me up. Most lookup sites just give you a code — I wanted the why: which roots, which strokes each root covers, and whether the last letter is an ID code.
More than four roots: take 1st, 2nd, 3rd and last. The 夂 in between is not coded, so it stays grey.
- Microsoft Wubi entries
- 534.1k
- characters
- 27.5k
- with stroke diagrams
- 9.6k
- JSON shards, loaded on demand
- 2.7k
Features
More than a code lookup
See every stroke
Each root gets its own colour on the grid; hover a letter to light up exactly its strokes.
Look up by code
Type etg to see candidates and every 4-letter code that starts with it; Z works as a wildcard in slots 3–4.
Fewest keystrokes
Dynamic programming splits a sentence into dictionary words — one plan with space-only commits, one that allows digit picks.
Root keyboard & mnemonics
Five zones colour-coded on a keyboard; click a mnemonic line to see every root on that key.
Review cards
History stays in localStorage; starred characters become flash cards — split it in your head, then flip.
Static, no ads
React + Vite on Cloudflare Pages. Works on phone and desktop, no sign-up.
Sentences
How few keys can “今天吃什么” take?
Whole sentences aren’t in the dictionary, and a rule-derived code won’t type them. Instead the sentence is split into real dictionary entries; each chunk costs its code length plus the commit key, and DP finds the cheapest split.
// '' = auto commit, ' ' = space, '2'..'9' = pick
function commitKey(code: string, rank: number, n: number) {
if (rank > 9) return null
if (code.length === 4 && n === 1) return ''
return rank === 1 ? ' ' : String(rank)
}Recommended
First candidates only — nothing to memorise
Optimised
Digit picks allowed — fewer keys
// Record layout: u16 len | u16 weight | u16 codeLen | 4 x u16 code | utf16le word | u16 0
let p = 0xa8
while (p + 2 <= b.length) {
const len = b.readUInt16LE(p)
if (len === 0) break
const weight = b.readUInt16LE(p + 2)
const codeLen = b.readUInt16LE(p + 4)
const code = b.toString('utf16le', p + 6, p + 6 + codeLen * 2)
const word = b.toString('utf16le', p + 14, p + len - 2)
lines.push(`${code}\t${word}\t${weight}`)
p += len
}Data
Identical to the IME on your PC
Wubi 86 tables floating around the web disagree on short codes, phrases and candidate order. These codes are parsed straight from ChsWubiNew.lex that ships with Windows — undocumented but simple: 530k+ records, and sorting by weight gives the exact candidate order.
So when the site says “2nd candidate, press 2”, that is exactly what you press.
Algorithm
The hard part: matching strokes to roots
Root splits come from my earlier search-wubi project and stroke vectors from Make Me a Hanzi — but nothing says which stroke belongs to which root, except for ~2,000 hand-labelled characters. An algorithm fills in the rest:
- 012.2k
Hand-labelled
Characters in search-wubi that already map strokes to roots.
- 02Learn
Root → stroke pattern
Learn which stroke-type sequences each root is usually made of.
- 036k
Strict backtracking
Backtrack the stroke sequence onto the roots; score ties statistically.
- 04496
Loose + edit distance
Allow reordering within a chunk, then fall back to edit distance — flagged as “inferred”.
Validated against the labelled set, ~95% match exactly. 8.7k characters now have stroke-level diagrams, covering nearly all common ones.
Next time you forget, just look it up
Works on phone and desktop, no sign-up, no ads. Open source under AGPL-3.0 — spot a wrong split? Open an issue.
Related writing