Skip to content
Innovialabs

Uzbek Latin–Cyrillic Transliteration in Code: Rules and Pitfalls

Practice5 min

In short

Uzbek Latin converts reliably to Cyrillic with a lookup table that checks two-character combinations first, once all apostrophes are normalized to one character; the reverse direction loses information (ц, ъ after ў), so store a normalized key for search.

In short: how transliteration works

Most Uzbek Latin and Cyrillic letters map one to one. The difficulties come from three things:

  • apostrophes: users type up to five different characters in o‘ and g‘;
  • digraphs: sh, ch, yo, ya must be recognized before single letters;
  • context: е, э and ц depend on their position in the word, and some information is lost when converting from Latin.

A working approach: normalize apostrophes → scan the string checking longer combinations first → preserve case → keep an exceptions dictionary for ambiguous words.

Mapping table

Direct pairs: a–а, b–б, d–д, f–ф, g–г, i–и, j–ж, k–к, l–л, m–м, n–н, o–о, p–п, r–р, s–с, t–т, u–у, v–в, x–х, y–й, z–з.

LatinCyrillicNote
oʻ, gʻў, ғThe apostrophe is part of the letter
hҳNot to be confused with x = х
qқ
sh, chш, чDigraphs, check before s, c
ngнгTwo letters in both scripts
yo, yu, yaё, ю, я
ye / eе / эSee rules below
s / tsцOnly when converting from Cyrillic
ʼъTutuq belgisi (separation mark)
—ьDropped in Latin

Apostrophes: o‘ and g‘

Real data contains ASCII ' (U+0027), the backtick, ‘ (U+2018), ’ (U+2019), ʻ (U+02BB) and ʼ (U+02BC). For oʻ and gʻ the usual recommendation is U+02BB; for the tutuq belgisi it is U+02BC.

The normalization rule is simple: turn any apostrophe between letters into U+02BC first, and after o and g into U+02BB. Leave apostrophes outside words, such as quotation marks, untouched.

Digraph and context pitfalls

  • yoʻl → йўл, not ёл: if yo is followed by U+02BB, it is y + oʻ.
  • mingʻirlamoq → минғирламоқ: do not merge n and g into "ng" when g is followed by an apostrophe.
  • Isʼhoq → Исҳоқ: an apostrophe between s and h separates the letters; it does not mean ш or ъ.
  • е/э: from Latin, e at the start of a word or after a vowel is э (ertak → эртак), otherwise е; ye → е (yer → ер). From Cyrillic it is the reverse: е at the start or after a vowel becomes ye (поезд → poyezd).
  • ц: from Cyrillic it becomes s at the start of a word and after a consonant (цирк → sirk), and ts after a vowel (лицей → litsey). Going back, ц cannot be restored automatically: ketsa is кетса. You need a dictionary of loanwords.
  • ё in Cyrillic always becomes yo.
  • Case: Ш before a lowercase letter is Sh; inside an all-caps word it is SH.

Latin → Cyrillic function

const OKINA = "ʻ"; // ʻ in oʻ, gʻ
const TUTUQ = "ʼ"; // ʼ tutuq belgisi

function normalizeLatin(s) {
  return s
    .normalize("NFC")
    .replace(/(?<=\p{L})['`´‘’ʻʼ](?=\p{L})/gu, TUTUQ)
    .replace(/([oOgG])ʼ/g, "$1" + OKINA);
}

const L2C = [
  ["s" + TUTUQ + "h", "сҳ"], ["o" + OKINA, "ў"], ["g" + OKINA, "ғ"],
  ["sh", "ш"], ["ch", "ч"], ["yo", "ё"], ["yu", "ю"], ["ya", "я"], ["ye", "е"],
  ["a", "а"], ["b", "б"], ["d", "д"], ["f", "ф"], ["g", "г"], ["h", "ҳ"],
  ["i", "и"], ["j", "ж"], ["k", "к"], ["l", "л"], ["m", "м"], ["n", "н"],
  ["o", "о"], ["p", "п"], ["q", "қ"], ["r", "р"], ["s", "с"], ["t", "т"],
  ["u", "у"], ["v", "в"], ["x", "х"], ["y", "й"], ["z", "з"], [TUTUQ, "ъ"],
];

function latToCyr(input) {
  const s = normalizeLatin(input);
  let out = "";
  let i = 0;
  while (i < s.length) {
    const prev = s[i - 1] || "";
    let hit = null;
    for (const [lat, cyr] of L2C) {
      if (s.slice(i, i + lat.length).toLowerCase() !== lat) continue;
      if (lat === "yo" && s[i + 2] === OKINA) continue; // yoʻl -> йўл
      hit = [lat, cyr];
      break;
    }
    if (!hit && s[i].toLowerCase() === "e") {
      const startOrVowel = !/\p{L}/u.test(prev) || /[aeiou]/i.test(prev);
      hit = ["e", startOrVowel ? "э" : "е"];
    }
    if (!hit) { out += s[i++]; continue; }
    const [lat, cyr] = hit;
    const src = s.slice(i, i + lat.length);
    out += src[0] !== src[0].toLowerCase() ? cyr[0].toUpperCase() + cyr.slice(1) : cyr;
    i += lat.length;
  }
  return out;
}

Minimal tests worth keeping in the project:

import assert from "node:assert/strict";
assert.equal(latToCyr("O'zbekiston"), "Ўзбекистон");
assert.equal(latToCyr("O’zbekiston"), "Ўзбекистон");
assert.equal(latToCyr("yo'l"), "йўл");
assert.equal(latToCyr("ma'no"), "маъно");
assert.equal(latToCyr("Is'hoq"), "Исҳоқ");
assert.equal(latToCyr("ertak"), "эртак");
assert.equal(latToCyr("SHAHAR"), "ШАҲАР");

People search however is convenient: in Latin without apostrophes, or in Cyrillic on a Russian keyboard layout without ў, қ, ғ, ҳ. So compare a search key, not the raw strings:

const FOLD = { ў: "у", қ: "к", ғ: "г", ҳ: "х", ъ: "", ь: "", ё: "е", э: "е" };

function searchKey(text) {
  return latToCyr(text)
    .toLowerCase()
    .replace(/[ўқғҳъьёэ]/g, (c) => FOLD[c])
    .replace(/\s+/g, " ")
    .trim();
}
// searchKey("Qo‘qon") === searchKey("Кукон")

Tips:

  • Store the key in a separate field and index it rather than computing it on the fly.
  • Do not change the original text: the key is only for comparison.
  • If queries like Ozbekiston without an apostrophe are common, add a second key where apostrophes are simply removed.

FAQ

For storage, pick one canonical variant, usually U+02BB, and convert all incoming data to it. For display you can use the typographic ‘ as well; what matters is not mixing variants in the database.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.