Транслитерация узбекского латиница–кириллица в коде: правила и ловушки
Практика5 минРедакция Innovialabs
Коротко
Узбекскую латиницу можно надёжно перевести в кириллицу таблицей с порядком «сначала двухсимвольные сочетания», если заранее привести все апострофы к одному символу; обратный перевод теряет информацию (ц, ъ после ў), поэтому для поиска храните нормализованный ключ.
Коротко: как устроена транслитерация
Большинство букв узбекской латиницы и кириллицы соответствуют друг другу один к одному. Сложности дают три вещи:
- апострофы: в
o‘иg‘пользователи вставляют до пяти разных символов; - диграфы:
sh,ch,yo,yaнужно распознать раньше одиночных букв; - контекст:
е,эицзависят от позиции в слове, а часть информации при переводе с латиницы теряется.
Рабочая схема: нормализовать апострофы → идти по строке, проверяя сначала длинные сочетания → сохранять регистр → для спорных слов держать словарь исключений.
Таблица соответствий
Прямые пары: a–а, b–б, d–д, f–ф, g–г, i–и, j–ж, k–к, l–л, m–м, n–н, o–о, p–п, r–р, s–с, t–т, u–у, v–в, x–х, y–й, z–з.
| Латиница | Кириллица | Комментарий |
|---|---|---|
oʻ, gʻ | ў, ғ | Апостроф входит в букву |
h | ҳ | Не путать с x = х |
q | қ | |
sh, ch | ш, ч | Диграфы, проверять раньше s, c |
ng | нг | В обеих системах две буквы |
yo, yu, ya | ё, ю, я | |
ye / e | е / э | См. правила ниже |
s / ts | ц | Только при переводе из кириллицы |
ʼ | ъ | Тутук белгиси (знак разделения) |
| — | ь | В латинице опускается |
Апострофы: o‘ и g‘
В реальных данных встречаются: ASCII ' (U+0027), обратный апостроф, ‘ (U+2018), ’ (U+2019), ʻ (U+02BB) и ʼ (U+02BC). Для букв oʻ, gʻ обычно рекомендуют U+02BB, для тутук белгиси — U+02BC.
Правило нормализации простое: любой апостроф между буквами сначала превращаем в U+02BC, а после o и g — в U+02BB. Апострофы вне слов, например кавычки, не трогаем.
Ловушки диграфов и контекста
yoʻl→ йўл, а не ёл: если послеyoстоит U+02BB, этоy+oʻ.mingʻirlamoq→ минғирламоқ: не склеивайтеnиgв «ng», если заgидёт апостроф.Isʼhoq→ Исҳоқ: апостроф междуsиhразделяет буквы, а не означает ш или ъ.- е/э: из латиницы
eв начале слова или после гласной — э (ertak→ эртак), иначе е;ye→ е (yer→ ер). Из кириллицы наоборот: е в начале слова и после гласной —ye(поезд→poyezd). - ц: из кириллицы —
sв начале слова и после согласной (цирк→sirk),tsпосле гласной (лицей→litsey). Обратно восстановить ц автоматически нельзя:ketsa— это кетса. Нужен словарь заимствований. - ё в кириллице всегда даёт
yo. - Регистр:
Шперед строчной —Sh, внутри слова капслоком —SH.
Функция латиница → кириллица
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;
}
Минимальные тесты, которые стоит держать в проекте:
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"), "ШАҲАР");
Нормализация для поиска
Пользователи ищут как удобно: латиницей без апострофов, кириллицей с русской раскладки без ў, қ, ғ, ҳ. Поэтому сравнивайте не исходные строки, а поисковый ключ:
const FOLD = { ў: "у", қ: "к", ғ: "г", ҳ: "х", ъ: "", ь: "", ё: "е", э: "е" };
function searchKey(text) {
return latToCyr(text)
.toLowerCase()
.replace(/[ўқғҳъьёэ]/g, (c) => FOLD[c])
.replace(/\s+/g, " ")
.trim();
}
// searchKey("Qo‘qon") === searchKey("Кукон")
Советы:
- Храните ключ в отдельном поле и индексируйте его, а не считайте на лету.
- Оригинальный текст не меняйте: ключ нужен только для сравнения.
- Если запрос вида
Ozbekistonбез апострофа частый, добавьте второй ключ, где апострофы просто удаляются.