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.
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) |
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-detectionv0.5— Svelte action / Solid directive + paste auto-correctionv0.6— browser extension (fix mistyped text on any site — context menu + hotkey) — 🚧 in repo (extension/), store submission pending
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
