DEV Community

Cover image for How to Convert Kruti Dev Text to Unicode Hindi: 4 Real Tasks in JS
Sun Xiaoxu
Sun Xiaoxu

Posted on AI-assisted

How to Convert Kruti Dev Text to Unicode Hindi: 4 Real Tasks in JS

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.

Before: Kruti Dev text without the font. After: Unicode Hindi.

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;
}
Enter fullscreen mode Exit fullscreen mode

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("")));
Enter fullscreen mode Exit fullscreen mode
by run:   कार्यालय आदेश संख्या (Office Order No. 12/A) दिनांक 01/10/2026
all text: कार्यालय आदेश संख्या (Oिपिबम Oतकमत Nव. 12ध्।) दिनांक 01ध्10ध्2026
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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(","));
}
Enter fullscreen mode Exit fullscreen mode

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,विजय सिंह
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode
jes'k dqekj | blind: रमेश कुमार | guarded: रमेश कुमार
विजय सिंह | blind: वजिय सिंह | guarded: विजय सिंह
lquhrk 'kekZ | blind: सुनीता शर्मा | guarded: सुनीता शर्मा
अमित वर्मा | blind: अमति वर्मा | guarded: अमित वर्मा
Enter fullscreen mode Exit fullscreen mode

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})`);
}
Enter fullscreen mode Exit fullscreen mode
भारत सरकार -> Hkkjr ljdkj  (round trip: true)
प्रधानमंत्री -> iz/kkuea=h  (round trip: true)
स्थिति -> fLFkfr  (round trip: true)
सुनीता शर्मा -> lquhrk 'kekZ  (round trip: true)
Enter fullscreen mode Exit fullscreen mode

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


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)