Typing Hindi like a Remington typewriter, in a browser textarea
Many Indian government jobs need a computer typing test: the Staff Selection Commission's data entry test, the railways' typing skill test, court and police clerk exams. Many candidates type in Hindi, on one of two layou
Many Indian government jobs need a computer typing test: the Staff Selection Commission's data entry test, the railways' typing skill test, court and police clerk exams. Many candidates type in Hindi, on one of two layouts. Typists trained on a typewriter use the Remington layout. It survives in the legacy Kruti Dev and DevLys fonts, and in the Unicode keyboard called Remington GAIL. Others use Inscript, the Government of India standard that ships with Windows.
Practising either at home usually means installing an input method or a legacy font. We wanted a page where the keyboard stays on English, the candidate picks the exam's layout, and the output is correct Unicode Devanagari. Here is how it works, from key events to scoring.
Read the physical key, not the character
KeyboardEvent.key is the character the operating system produced. KeyboardEvent.code is the physical key (KeyD, Semicolon, BracketLeft), whatever OS layout is active. A Hindi layout is defined by key positions, so code is the right input. We first turn it into the US-QWERTY character for that key, because every published layout chart is written that way:
const CODE_TO_US: Record<string, [string, string]> = {
Semicolon: [';', ':'], BracketLeft: ['[', '{'], Quote: ["'", '"'],
// ...letters, digits and the other punctuation keys
};
const usCharForCode = (code: string, shift: boolean) =>
CODE_TO_US[code]?.[shift ? 1 : 0] ?? null;
The keydown handler asks the layout for a result. If there is one, it cancels the default and writes the new value into the textarea itself:
const native = e.nativeEvent as KeyboardEvent;
if (!e.ctrlKey && !e.metaKey && !e.altKey &&
!native.isComposing && e.keyCode !== 229) {
const next = applyHindiKey(layout, { text, caret, pendingI }, e.code, e.shiftKey);
if (next) { e.preventDefault(); commit(next); }
}
Shortcuts with Ctrl, Alt or Meta pass through, and so does anything an input method is composing (isComposing, or the legacy keyCode 229 that Android keyboards send). Phone keyboards compose text in their own IME, so on phones we accept whatever they produce and score it the same way.
A layout is a table plus a few rules
Most of a typewriter layout is a lookup from key to output. A few Remington GAIL keys:
const REMINGTON: Record<string, string> = {
d: 'क', k: 'ा', j: 'र', ';': 'य',
D: 'क्', '[': 'ख्', "'": 'श्', // half letters: consonant + halant
K: 'ज्ञ', J: 'श्र', // conjunct keys
z: '्र', Z: 'र्', // rakar and reph
f: 'ि', s: 'े', a: 'ं',
};
The rest is context. A typewriter builds some letters out of pieces, so the reducer looks at the characters before the caret:
- a half letter followed by
ाbecomes the full letter (ख्+ा=ख); -
ाfollowed byेbecomesो, andअfollowed byाbecomesआ; -
ॅfollowed byंbecomes the chandrabinduँ; - two dandas make
॥.
The GAIL table follows SIL's MIT-licensed "Remington GAIL (SIL)" Keyman keyboard. Kruti Dev uses the same physical layout with a handful of keys moved: the danda is on Shift+A, = gives त्र, and the nukta and halant sit on other keys. So Kruti Dev is the same reducer with a different table. Inscript is typed in spoken order and needs almost no context rules.
The hard part: ि comes first
Unicode stores text in logical (spoken) order. In किसान the vowel sign ि is stored after क, even though it is drawn to its left. A typewriter typist presses keys in visual order: ि first (the f key), then क. That is what their fingers know, so a practice tool has to accept it.
The reducer's state is the text, the caret and a flag, pendingI. Pressing f inserts ि at once, so the screen shows something, and sets the flag. When the next output is a consonant, it is slotted in front of the waiting sign:
if (pendingI && prev === 'ि' && isConsonantOutput(out)) {
const stillHalf = out.endsWith('्'); // a half letter keeps ि waiting
return {
text: text.slice(0, caret - 1) + out + text.slice(caret - 1),
caret: caret + out.length,
pendingI: stillHalf,
};
}
A half letter keeps the sign waiting, because the cluster is not finished. स्थिति is typed f L F k f r:
| Key | Output | Text after the key |
|---|---|---|
| f | ि |
ि, waiting |
| L | स् |
स्ि, still waiting |
| F | थ् |
स्थ्ि, still waiting |
| k | ा |
स्थि: ा completes थ्, so its halant goes |
| f | ि |
स्थि + ि, waiting |
| r | त | स्थिति |
Halant, nukta and rakar belong to the consonant. If one of them is typed after a waiting ि, it moves in front of the sign: पि + z = प्रि.
The reph (the र् drawn on top of a letter) has the opposite problem. Typists press Shift+Z after the syllable it sits on, so काय + Shift+Z must become कार्य. The reducer walks back from the caret over trailing signs (matras, anusvara, nukta) to the start of the last consonant cluster, and inserts र् there.
The reducer is a pure function of state and key, so each rule is a one-line test:
it.each([
['fdlku', 'किसान'],
['fLFkfr', 'स्थिति'],
['dk;Z', 'कार्य'],
['Kku', 'ज्ञान'],
])('%s → %s', (keys, expected) => {
expect(typeSequence('remington', keys)).toBe(expected);
});
Normalise before you compare
Two strings that look the same can differ in code points. The usual culprit is a nukta letter. Some keyboards emit क़ as one precomposed code point (U+0958), while a passage may store क followed by the nukta sign. U+0958 is a Unicode composition exclusion, so NFC turns it into the two-code-point form. Normalising both sides to NFC therefore makes them equal. We also drop the invisible zero-width joiners, non-joiners and soft hyphens some Hindi keyboards insert, and fold curly quotes to straight ones. The danda । is in the punctuation class, so a missing danda counts like a missing full stop.
Scoring: align words, using anchors
Exam rules count mistakes per word: an omitted word, an extra word, a wrong spelling. Some, such as a split word or a missing full stop, count as half. A character diff is not enough. If a candidate skips a line, every later word shifts, and a naive comparison marks the rest of the passage wrong.
We align at the word level in two passes.
Anchors. Words become integer ids. A word pair that occurs exactly once in the passage and once in the typed text is an anchor candidate. From those candidates we keep the longest chain that moves forward in both texts, found with the patience-sorting trick for the longest increasing subsequence, in O(n log n). After a skipped line or a retyped block, the alignment resyncs within a couple of correctly typed words.
One guard matters in practice. A slip near the end can form a pair that happens to occur once, much later in the passage. Trusting it would charge every passage word in between as missing. So a trailing run that jumps away from the previous anchor's diagonal must cover at least three matched words before it counts.
Between anchors. Each stretch is short, so a banded edit distance over words is cheap. Substitution, deletion and insertion carry slightly different costs (1.05, 1 and 1.1), so ties break the way an examiner would read them. Two extra moves cost 0.5: a split or merged word ("type writer" for "typewriter") and a swapped pair. After the last anchor the passage end stays open, because the candidate simply has not reached it yet.
The word steps are then classified. A skipped, extra or wrong word is one mistake; a split, merge or swap is half. Mistake weight is capped per word, but character accuracy still counts every wrong keystroke. Exam-specific rules sit on top as data. Railway tests, for example, ignore mistakes up to 5% of the words typed, then deduct ten words per remaining mistake. The live view re-aligns on every keystroke, so the passage's word codes are cached.
What we chose not to do
We do not host Kruti Dev, DevLys or Mangal font files. Their licences are unclear or proprietary, and with Unicode output a Kruti Dev typist trains the same finger positions and gets text any font can show. Layouts are not emulated on phones, and the scores are practice feedback, not an official result.
You can try the three layouts on the Hindi typing test page. Questions about the reducer or the alignment are welcome in the comments.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.