Keyboard layout converter — restore text typed with the wrong layout. Korean (Dubeolsik ↔ QWERTY) built in; Russian, Ukrainian, Hebrew, Greek, Thai, Arabic and Georgian available as tree-shakeable subpath imports. TypeScript-first, zero dependencies, ESM/CJS dual package.
English · 한국어 · Русский · Українська · עברית · Ελληνικά · ไทย · العربية · ქართული · Live demo
Try it online: ⚡ StackBlitz — Vanilla · Vue · React · Svelte · Solid | 📦 CodeSandbox
Ever typed dkssud when you meant 안녕, or scanned a barcode while the
Korean IME was on and got ㅇㄴㅁ쇼2068601 instead of DSATY2068601?
kokey converts between what was typed and what was meant — in both
directions, exactly the way a Dubeolsik IME composes Hangul.
The same slip exists in every language that toggles a non-Latin layout with
QWERTY: Russians type ghbdtn for привет, Israelis akuo for שלום,
Thais scan barcodes with Kedmanee on. kokey covers those layouts too —
see Beyond Korean.
Numeric fields in the same form — amounts that need live comma grouping, a stable caret, and right alignment? That's kokey's sibling, numkey.
npm install @devslab/kokeyOr straight from a CDN — no build step, everything under the kokey global:
<script src="https://cdn.jsdelivr.net/npm/@devslab/kokey/dist/kokey.global.js"></script>
<script>
kokey.enToKo('dkssud') // '안녕'
kokey.toEn('привет안녕') // 'ghbdtndkssud' — the CDN build ships every layout
kokey.observe() // auto-bind every <input data-kokey> / <input data-hangul>
</script>import { koToEn, enToKo } from '@devslab/kokey'
// Hangul → the QWERTY keystrokes that produced it
koToEn('안녕') // 'dkssud'
koToEn('값없는 닭갈비') // 'rkqtdjqtsms ekfrrkfql'
koToEn('ㅇㄴㅁ쇼2068601') // 'dsaty2068601' (wedge-scanner rescue)
// QWERTY keystrokes → composed Hangul (full IME automaton)
enToKo('dkssud') // '안녕'
enToKo('gksrmf') // '한글'
enToKo('ekfrl') // '달기' (compound-final split, like a real IME)- Shift is honored:
R→ ㄲ,r→ ㄱ,koToEn('뛰다') === 'Enlek' - Compound vowels/finals: ㅘ ↔
hk, ㄵ ↔sw, … - 받침 넘김 (final-consonant carry-over):
enToKo('dkswk') === '안자' - Pass-through: digits, punctuation, and unmapped letters are left as-is
- Round-trip safe for Korean text:
enToKo(koToEn(s)) === s
Each layout is a subpath import (unused ones never reach your bundle) and plugs into the same machinery:
import { register, toEn, fromEn } from '@devslab/kokey'
import { ru, ruToEn, enToRu } from '@devslab/kokey/ru'
import { he } from '@devslab/kokey/he'
// direct, per-layout
ruToEn('привет') // 'ghbdtn' — the Punto Switcher classic
enToRu('ghbdtn') // 'привет'
// or register + auto-detect by script, mixed strings included
register(ru, he)
toEn('안녕 привет שלום') // 'dkssud ghbdtn akuo'
fromEn('ghbdtn', 'ru') // 'привет'| Layout | Import | Notes |
|---|---|---|
| Korean 두벌식 | built-in (ko) |
full IME composition automaton |
| Russian ЙЦУКЕН | @devslab/kokey/ru |
moved punctuation (ё on backtick, №, . on /) mapped faithfully |
| Ukrainian Enhanced | @devslab/kokey/uk |
і/є/ї; AltGr-only ґ restored in reverse; ru/uk auto-disambiguated |
| Hebrew | @devslab/kokey/he |
final forms, swapped brackets, caps-lock safe |
| Greek | @devslab/kokey/el |
tonos/dialytika dead keys (;a → ά), final sigma |
| Thai Kedmanee | @devslab/kokey/th |
full digit-row remap — barcode rescue works for Thai too |
| Arabic (101) | @devslab/kokey/ar |
lam-alef لا on b, hamza forms, tashkeel |
| Georgian QWERTY | @devslab/kokey/ka |
near-phonetic (gamarjoba ↔ გამარჯობა) |
Languages whose IME needs a candidate-selection step (Chinese pinyin,
Japanese kanji) are out of scope by construction — the keystroke ↔ text
relation there isn't deterministic. Need another deterministic layout?
It's one defineLayout({ id, script, fromKey }) table —
PRs welcome.
Force an <input>/<textarea> to a specific mode regardless of the user's
IME state — the field converts as you type, composition-safe, cursor
preserved:
<input data-kokey="ko"> <!-- QWERTY keystrokes compose into Hangul -->
<input data-kokey="ru"> <!-- QWERTY keystrokes become Russian (register(ru) first) -->
<input data-kokey="en"> <!-- any registered script restored to QWERTY -->
<input data-hangul="ko"> <!-- legacy attribute, still supported -->import { bind, observe } from '@devslab/kokey'
observe() // bind all [data-kokey]/[data-hangul] + watch for new ones
const unbind = bind(el, 'en') // or bind a single element explicitlydata-kokey="en" shines on invoice/e-mail/ID fields: whatever layout the
user forgot to switch off — Korean, Russian, Thai — the field self-heals to
Latin with no per-language branching.
Fix wrong-layout gibberish on paste without forcing a mode on the input:
<input data-kokey-paste> <!-- picked up by observe() -->import { bindPaste, fixMistyped } from '@devslab/kokey'
bindPaste(el) // imperative version
fixMistyped('dkssudgktpdy') // '안녕하세요' — or null if it looks fine
fixMistyped('hello') // nullDetection is deliberately conservative and Korean-only, because composition
itself is the signal: standalone vowel jamo mixed into text marks English
typed in Korean mode (ㅗ디ㅣㅐ → hello), and Latin words that recompose
into complete syllables with nothing left over mark Korean typed in English
mode (dkssudgktpdy → 안녕하세요). Real Korean — including ㅋㅋㅋ/ㅠㅠ
laughter — and real English pass through untouched; a single short word must
yield at least three syllables before it fires. Other layouts have no such
validity signal (any Latin string maps to Cyrillic), so use explicit modes
for those. Before replacing, a cancelable kokey-paste CustomEvent fires
with detail: { pasted, fixed } — preventDefault() it to veto, or use
fixMistyped directly to build a suggest-UI instead.
When a field's value looks mistyped, float a small button at its right edge that converts it on click:
<input data-kokey-suggest> <!-- picked up by observe() -->import { bindSuggest } from '@devslab/kokey'
bindSuggest(el) // your CSS styles .kokey-suggest
bindSuggest(el, { theme: 'auto' }) // or let kokey pick a readable paletteNothing changes until the click. It offers only what fixMistyped is
confident about — never the "compose this Latin text into Korean" guess
that an explicit convert(text, 'ko') will happily make, because an
uninvited button has to be right nearly always. So dkssudgktpdy gets a
button and a bare dkssud does not.
Styling is yours. The button carries the class kokey-suggest and no
colours, so your stylesheet owns it. theme: 'auto' is the opt-in for
contexts with no stylesheet of their own — it measures the field's own
background brightness and picks a readable kokey palette, verifying WCAG
contrast. It never tries to copy the page's colours; a page's "theme
colour" is only incidentally related to its accent, and reading it as fact
ships unreadable controls. (This is what the browser extension uses.)
For plain (uncontrolled) inputs, the directive/hook:
<script setup>
import { vKokey } from '@devslab/kokey/vue'
</script>
<template>
<input v-kokey="'ko'">
<input v-kokey="'ru'">
</template>import { useKokey } from '@devslab/kokey/react'
function Field() {
return <input ref={useKokey('en')} />
}For v-model / controlled inputs, use the KokeyInput component — it
converts inside the framework's data flow, so your bound state always
holds the converted value (the ref-based bindings mutate the DOM after the
framework reads it, which fights v-model/value=):
<script setup>
import { KokeyInput } from '@devslab/kokey/vue'
const name = ref('')
</script>
<template>
<KokeyInput v-model="name" mode="ko" />
<KokeyInput v-model="memo" mode="en" as="textarea" />
</template>import { KokeyInput } from '@devslab/kokey/react'
function Form() {
const [v, setV] = useState('')
return <KokeyInput mode="en" value={v} onChange={(e) => setV(e.target.value)} />
}Svelte gets an action — bind:value works, the action re-syncs the binding
after converting (and it imports nothing from svelte, so there is no peer
dependency at all):
<script>
import { kokey, kokeyPaste } from '@devslab/kokey/svelte'
let name = ''
</script>
<input use:kokey={'ko'} bind:value={name} />
<input use:kokeyPaste />Solid gets a use: directive (reactive to a mode signal) and a ref factory —
Solid's delegated onInput already reads the converted value:
import { kokey, useKokey } from '@devslab/kokey/solid'
<input use:kokey={mode()} onInput={(e) => setV(e.currentTarget.value)} />
<input ref={useKokey('en')} />All are thin wrappers over the DOM layer — vue/react/solid-js are
optional peer dependencies (Svelte needs none), so the core stays
zero-dependency. The legacy vHangul / useHangul names still work.
| Function | Signature | Description |
|---|---|---|
koToEn |
(text: string) => string |
Decompose Hangul syllables/jamo into their Dubeolsik QWERTY key sequence |
enToKo |
(text: string) => string |
Compose QWERTY key sequence into Hangul via the standard IME automaton |
toEn |
(text: string) => string |
Restore any registered script to QWERTY, auto-detected per run |
fromEn |
(text, layoutId) => string |
Compose QWERTY keystrokes into the given registered layout |
register |
(...layouts) => void |
Register layouts for toEn and the DOM data-kokey modes |
defineLayout |
(def) => Layout |
Build a table-driven layout ({ id, script, fromKey }) |
bind |
(el, mode?) => unbind |
Enforce a mode on one input/textarea (mode defaults to its data-kokey/data-hangul attribute) |
observe |
(root?) => stop |
Bind every [data-kokey]/[data-hangul] under root and keep watching via MutationObserver |
createRefBinder |
(mode?) => (el | null) => void |
Framework-agnostic ref-callback factory (what useKokey wraps) |
fixMistyped |
(text) => string | null |
Correct wrong-layout gibberish, or null if the text looks fine (heuristic, Korean-only) |
bindPaste |
(el) => unbind |
Auto-correct wrong-layout pastes on one input (data-kokey-paste via observe) |
bindSuggest |
(el, opts?) => unbind |
Offer a one-click fix in the field (data-kokey-suggest via observe; theme: 'auto' for self-styling) |
vKokey |
@devslab/kokey/vue |
Vue 3 directive: v-kokey="'ko'" (legacy vHangul kept) |
KokeyInput |
@devslab/kokey/vue · /react |
Component for v-model / controlled inputs (mode, as="input|textarea") |
useKokey |
@devslab/kokey/react · /solid |
Hook/ref factory returning a ref callback (legacy useHangul kept) |
kokey |
@devslab/kokey/svelte · /solid |
Svelte action / Solid directive for use:kokey (+ kokeyPaste in both) |
convert |
(text, mode) => string |
One-shot conversion for a mode ('en' or a layout id) |
applyToInput |
(el, mode) => boolean |
Convert an input's value in place, caret preserved |
Per-layout modules also export direct converters: ruToEn/enToRu,
heToEn/enToHe, thToEn/enToTh, … Low-level Korean tables (CHOSUNG,
JUNGSUNG, JONGSUNG, JAMO_TO_KEY, KEY_TO_JAMO) are exported for
advanced use.
✅ shippedv0.2— DOM layer✅ shippedv0.3— Vue directive / React hook✅ shippedv0.4— multi-layout: ru/uk/he/el/th/ar/ka +toEnauto-detection✅ shippedv0.5— Svelte action / Solid directive + paste auto-correction✅ shippedv0.6— in-field suggest button (bindSuggest,data-kokey-suggest)
The same conversions on any site — a context menu entry, Alt+K, and the
suggest button — as a Manifest V3 extension, with an options page. All
processing is local.
Install: Chrome · Firefox · Whale. Edge is on hold.
The extension is a separate deliverable on its own version track (it is at 0.7.0 while the library is at 0.6.0 — the numbers are unrelated). Source in extension/.
Firefox users: MV3 grants no site access at install, so the extension looks inert until you allow it — about:addons → kokey → Permissions → "Access your data for all websites".
Ideas we considered and why they are not here (more layouts, the ones we won't do, extension follow-ups): docs/backlog.md.
inko pioneered this space but has been
unmaintained since 2019 and predates modern TypeScript/ESM packaging.
kokey is a from-scratch implementation: typed, tree-shakeable, dual
ESM/CJS, tested against real IME behavior (compound finals, carry-over,
shift handling).
Issues and PRs welcome — new layouts especially. See CONTRIBUTING.md for the dev setup and the two hard rules for layout tables (anchor verification + round-trip tests).
- numkey — the numeric sibling in the "-key" input family: live thousands grouping, Korean amount UX, string-first canonical values
- More open source from devslab
MIT © devslab
