A client sends a Hindi Word file, you paste it into a web page, and it reads dk;kZy; vkns'k instead of कार्यालय आदेश.
By the end of this post you'll have a ~70-line JavaScript converter and four tasks done with it: a Word office order, a CSV column, a half-fixed column, and the reverse direction.
Prerequisite: the text must be Kruti Dev 010 (DevLys 010 uses the same layout, so it works too). Other versions such as 011 or 055 aren't covered.
The converter (and why the text looks broken)
Kruti Dev stores ASCII key presses, and only the font draws them as Hindi. Search, databases, and phones without the font see the raw keys:
| Stored bytes | What it should say |
|---|---|
Hkkjr ljdkj |
भारत सरकार |
fganh |
हिंदी |
Fixing it takes a lookup table plus two characters that have to move. Here's the whole converter. It's a subset of the real table, but every example below runs on it:
// kruti.mjs: a tiny Kruti Dev 010 <-> Unicode converter (a subset of the full table)
const REPH = "\uE000"; // placeholder for Kruti "Z" until we know where it goes
const K2U = {
// multi-key sequences first in spirit: longest match wins anyway
"vks": "ओ", "vk": "आ", "ks": "ो", "kS": "ौ",
"v": "अ", "b": "इ", ",": "ए",
"d": "क", "x": "ग", "p": "च", "t": "ज", "r": "त", "n": "द", "u": "न",
"i": "प", "c": "ब", "e": "म", ";": "य", "j": "र", "y": "ल", "o": "व",
"l": "स", "g": "ह", "=": "त्र",
// half forms: no single key for the full letter, "k" (the stem) completes it
"H": "भ्", "/": "ध्", "F": "थ्", "'": "श्", "L": "स्", "[": "ख्",
"k": "ा", "h": "ी", "q": "ु", "w": "ू", "s": "े", "S": "ै",
"f": "ि", "a": "ं", "z": "्र", "Z": REPH,
"A": "।", "]": ",",
};
// longest-match scanner over any table
function scan(input, table, stem = false) {
const max = Math.max(...Object.keys(table).map((k) => k.length));
const out = [];
for (let i = 0; i < input.length; ) {
const prev = out[out.length - 1] ?? "";
if (stem && input[i] === "k" && prev.endsWith("्")) {
out[out.length - 1] = prev.slice(0, -1); // भ् + k = भ
i++;
continue;
}
let len = Math.min(max, input.length - i);
while (len > 0 && !(input.slice(i, i + len) in table)) len--;
if (len === 0) { out.push(input[i++]); continue; } // unknown: keep as is
out.push(table[input.slice(i, i + len)]);
i += len;
}
return out.join("");
}
const C = "[\u0915-\u0939]"; // consonants क..ह
const CLUSTER = `(?:${C}्)*${C}`; // स्थ, त्र, ...
const SIGNS = "[\u093E-\u094C\u0901\u0902]"; // vowel signs, ँ, ं
export function krutiToUnicode(input) {
let s = scan(input, K2U, true);
s = s.replace(new RegExp(`ि(${CLUSTER})`, "g"), "$1ि"); // ि: typed before, stored after
s = s.replace(new RegExp(`(${CLUSTER})(${SIGNS}*)${REPH}`, "g"), "र्$1$2"); // reph: typed after, stored before
return s.replaceAll(REPH, "र्");
}
// Unicode -> Kruti: the same table inverted, plus "half form + k" for letters with no single key
const U2K = Object.fromEntries(Object.entries(K2U).map(([k, u]) => [u, k]));
Object.assign(U2K, { "भ": "Hk", "ध": "/k", "थ": "Fk", "श": "'k", "ख": "[k" });
export function unicodeToKruti(input) {
let s = input.replace(new RegExp(`र्(${CLUSTER})(${SIGNS}*)`, "g"), `$1$2${REPH}`);
s = s.replace(new RegExp(`(${CLUSTER})ि`, "g"), "ि$1");
return scan(s, U2K);
}
// Word files: convert only the runs set in a Kruti Dev font
export const convertRuns = (runs) =>
runs.map((r) => (/kruti\s*dev/i.test(r.font) ? krutiToUnicode(r.text) : r.text)).join("");
// already Unicode Hindi? Kruti Dev text never contains Devanagari code points
export function looksLikeUnicodeHindi(text) {
const deva = (text.match(/[\u0900-\u097F]/g) ?? []).length;
const latin = (text.match(/[A-Za-z]/g) ?? []).length;
return deva >= 3 && deva * 4 >= latin;
}
Task 1: Make an old Word office order searchable
You have a government circular in .docx. Once it's converted, the Hindi can be searched, copied into a web page, and read without installing Kruti Dev.
Before → after (real output, example data):
import { krutiToUnicode, convertRuns } from "./kruti.mjs";
// example data: one paragraph of an office order, as runs with their font
const runs = [
{ font: "Kruti Dev 010", text: "dk;kZy; vkns'k la[;k " },
{ font: "Arial", text: "(Office Order No. 12/A)" },
{ font: "Kruti Dev 010", text: " fnukad " },
{ font: "Arial", text: "01/10/2026" },
];
console.log("by run: ", convertRuns(runs));
console.log("all text:", krutiToUnicode(runs.map((r) => r.text).join("")));
by run: कार्यालय आदेश संख्या (Office Order No. 12/A) दिनांक 01/10/2026
all text: कार्यालय आदेश संख्या (Oिपिबम Oतकमत Nव. 12ध्।) दिनांक 01ध्10ध्2026
How: in a Word file, every run of text carries its font name. Convert only the runs set in Kruti Dev. The table matches the longest key sequence first, because vks is ओ, not अ + ो. Reph (Z) is typed after its syllable, so the converter moves र् back in front of it.
Gotcha: typists switch to Arial for English and numbers. If you convert the paragraph as plain text, (Office Order No. 12/A) turns into junk (second line above). Convert by font run.
Task 2: Convert a column of names in a CSV export
An old HR or admissions export where the names were typed in Kruti Dev. One script converts the whole column, instead of someone retyping the names one by one.
Before (example data):
id,name_kruti,city
1,jes'k dqekj,Lucknow
2,lquhrk 'kekZ,Jaipur
3,vfer oekZ,Bhopal
4,fot; flag,Patna
// node task2.mjs names.csv > names-unicode.csv
import { readFileSync } from "node:fs";
import { krutiToUnicode } from "./kruti.mjs";
const [header, ...rows] = readFileSync(process.argv[2], "utf8").trimEnd().split("\n");
const cols = header.split(",");
const col = cols.indexOf("name_kruti");
console.log([...cols, "name"].join(","));
for (const row of rows) {
const cells = row.split(","); // fine for this sample; use a real CSV parser for quoted fields
console.log([...cells, krutiToUnicode(cells[col])].join(","));
}
After (node task2.mjs names.csv):
id,name_kruti,city,name
1,jes'k dqekj,Lucknow,रमेश कुमार
2,lquhrk 'kekZ,Jaipur,सुनीता शर्मा
3,vfer oekZ,Bhopal,अमित वर्मा
4,fot; flag,Patna,विजय सिंह
How: keep the original column and add the converted one next to it, so anyone can spot-check. The other two rules show up here. ि is typed before its consonant (fot; = विजय), so it moves after the whole consonant cluster. And a k right after a half form completes that letter ('k = श).
Gotcha: split(",") breaks on quoted names that contain commas. For real files, use a CSV parser.
Task 3: Don't double-convert a half-fixed column
Someone already retyped some rows in Unicode by hand. You want to convert the rest without breaking the rows that are already correct.
Before → after:
import { krutiToUnicode, looksLikeUnicodeHindi } from "./kruti.mjs";
// example data: a column where someone already fixed some rows by hand
const rows = ["jes'k dqekj", "विजय सिंह", "lquhrk 'kekZ", "अमित वर्मा"];
for (const r of rows) {
const blind = krutiToUnicode(r);
const safe = looksLikeUnicodeHindi(r) ? r : krutiToUnicode(r);
console.log(`${r} | blind: ${blind} | guarded: ${safe}`);
}
jes'k dqekj | blind: रमेश कुमार | guarded: रमेश कुमार
विजय सिंह | blind: वजिय सिंह | guarded: विजय सिंह
lquhrk 'kekZ | blind: सुनीता शर्मा | guarded: सुनीता शर्मा
अमित वर्मा | blind: अमति वर्मा | guarded: अमित वर्मा
How: Kruti Dev text never contains Devanagari code points. If a cell already has them, skip it.
Gotcha: blind conversion damages the correct rows without any error: विजय → वजिय, अमित → अमति. The words still look like Hindi, so nobody notices until search starts missing them.
Task 4: Go back to Kruti Dev for a form that requires it
You have Unicode Hindi, but the office or print shop wants the file in Kruti Dev 010. Convert it instead of retyping it.
How: run the same rules backwards and check the round trip.
import { unicodeToKruti, krutiToUnicode } from "./kruti.mjs";
for (const u of ["भारत सरकार", "प्रधानमंत्री", "स्थिति", "सुनीता शर्मा"]) {
const k = unicodeToKruti(u);
console.log(`${u} -> ${k} (round trip: ${krutiToUnicode(k) === u})`);
}
भारत सरकार -> Hkkjr ljdkj (round trip: true)
प्रधानमंत्री -> iz/kkuea=h (round trip: true)
स्थिति -> fLFkfr (round trip: true)
सुनीता शर्मा -> lquhrk 'kekZ (round trip: true)
Gotcha: don't trust the output until Unicode → Kruti → Unicode gives back exactly the string you started with. That one check catches most table mistakes.
Limitations
- Kruti Dev 010 only (DevLys 010 uses the same layout). If the output is still nonsense, check the font name in Word first. Chanakya, Kruti Dev 011/055 and other legacy fonts need their own table.
- Mixed text needs its font info. The converter can't tell Kruti Dev from English, because both are Latin letters. Convert from the Word file (by font run) or from columns you know are Kruti Dev, not from plain text where Hindi and English are mixed.
References
- The Unicode Standard 17.0, Chapter 12: South and Central Asia-I (Devanagari), rules R2 (reph) and R15 (short i)
- Unicode Devanagari code chart (U+0900–U+097F)
- Kruti Dev on Wikipedia
I put the full version into a free converter that runs entirely in your browser: kruti-dev-to-unicode.com. Bug reports are welcome.
Have you had to deal with legacy Hindi fonts like Kruti Dev? What broke for you?

Top comments (0)