Mundarija (27)
- Bu darsda
- 1. Nega bu kerak?
- 2. Tilni aniqlash
- 2.1 Manbalar tartibi
- 2.2 Accept-Language muzokarasi
- 2.3 Kanon o'z xatosidan nima o'rgandi
- 3. Ko'plik shakllari
- 3.1 CLDR qoidalari
- 3.2 i18next bilan
- 4. ICU MessageFormat
- 5. Lotin ↔ kirill transliteratsiyasi
- 5.1 Kirilldan lotinga
- 5.2 Lotindan kirillga — taxminiy
- 5.3 Slug
- 6. Bazada ko'p tilli kontent
- 6.1 Ikki usul
- 6.2 Kanon o'lchovi va tanlov
- 7. API javobida til
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): katta harfli so'zlar
- 3-mashq (qiyin): ko'p tilli slug
- 10. Loyiha qadami: kanon tajribasi, loyihaga hali kirmaydi
- 11. Real ishda
- Xulosa
- Manbalar
Backend'da ko'p tillilik (i18n): Accept-Language, i18next, ICU va lotin-kirill
Qisqacha: Backend ham foydalanuvchi tilida gapirishi kerak: xato xabarlari, email va bazadagi mahsulot nomlari. Til profil sozlamasidan, bo'lmasa
Accept-Languagesarlavhasidan tanlanadi va qo'llab-quvvatlanadigan ro'yxatga moslanadi. Matnlar kalit bo'yicha tarjima faylida turadi (i18next yoki ICU MessageFormat), ko'plik shakllari esa har til uchun CLDR qoidasi bilan tanlanadi — o'zbekchada ikkita, ruschada to'rtta. Bazadagi ko'p tilli kontent — JSONB ustun yoki alohida tarjima jadvali.
Bu darsda
Accept-Languagesarlavhasini o'qib,qqiymatlari va profil bilan tilni tanlaysiz —uz-Cyrltuzog'ini chetlab.- i18next va ICU MessageFormat bilan ko'plik, tanlov (
select) va sana formatini yozasiz. - Lotin ↔ kirill transliteratsiyasini va URL uchun slug'ni yozib, ularning chegaralarini bilasiz.
- Bazada ko'p tilli nomni JSONB va tarjima jadvali bilan saqlab, fallback bilan o'qiysiz.
Oldin bilishingiz kerak: Intl.PluralRules, ListFormat, DisplayNames, i18n II: o'zbek lotin/kirill, URL va type-safe kalitlar, Xato javoblari: Problem Details, JSONB.
1. Nega bu kerak?
21-qismda vazifalar frontendini uch tilda qildik: o'zbek (lotin va kirill) va rus. Tugmalar tarjima bo'ldi. Lekin foydalanuvchi bo'sh vazifa yuborsa, server javobi baribir o'zbekcha lotinda keladi: "Matn bo'sh bo'lmasin". Rus tilidagi foydalanuvchi parolni tiklash xatini ham lotinda oladi. Telegram bot eslatmasi ham.
Frontend tarjimasi faqat ekrandagi matnni qamraydi. Backend yaratadigan matn esa — xato xabarlari, email, SMS, bot xabarlari, PDF hisobotlar va bazadagi kontent (mahsulot nomi, kategoriya) — server tomonda tarjima qilinishi kerak.
i18n — internationalization (i va n orasida 18 harf): ilovani ko'p tilga tayyorlash. l10n — localization: aniq bir tilga moslash (tarjima, sana formati). Bu darsda backend'ni tayyorlaymiz.
Darsdagi kutubxona chiqishlari — kanon tajribasidan (i18next 26.5.3, intl-messageformat 12.1.4, Node 24.21.0, 2026-10-27). Brauzerda ishlaydigan bloklar va SQL — biz ishga tushirdik.
2. Tilni aniqlash
2.1 Manbalar tartibi
Server foydalanuvchi tilini bir nechta joydan biladi. Ustuvorlik tartibi odatda shunday:
- Profil sozlamasi — foydalanuvchi o'zi tanlagan. Eng ishonchli.
- URL yoki parametr —
/ru/...yoki?til=ru(ochiq sahifalar, SEO uchun). - Cookie — kirmagan foydalanuvchining oxirgi tanlovi.
Accept-Language— brauzer yoki telefon tili.- Sukut —
uz-Latn.
flowchart LR
P{"Profil tili bormi?"} -- ha --> T["Shu til"]
P -- yo'q --> U{"URL yoki cookie?"}
U -- ha --> T
U -- yo'q --> A{"Accept-Language mos?"}
A -- ha --> T
A -- yo'q --> S["uz-Latn"]Diagrammaga qarang: har bosqich faqat oldingisi javob bermaganda ishlaydi. Natija har doim qo'llab-quvvatlanadigan tillardan biri.
2.2 Accept-Language muzokarasi
Brauzer har so'rovda Accept-Language: uz-UZ,uz;q=0.9,ru;q=0.8 kabi sarlavha yuboradi. Bu — afzallik ro'yxati: q (quality, 0 dan 1 gacha) qiymati bo'lmasa — 1. Serverning vazifasi — o'zi biladigan tillar ichidan eng mosini tanlash. Bu content negotiation (muzokara).
Kanon funksiyasi (telefon uchun qatorlarni bo'ldik, mantiq o'sha):
const TILLAR = ["uz-Latn", "uz-Cyrl", "ru", "en"];
function tilTanla(sarlavha, profil) {
if (profil && TILLAR.includes(profil)) return profil;
const sorov = (sarlavha ?? "")
.split(",")
.map((q) => {
const [t, ...p] = q.trim().split(";");
const qv = p.find((x) => x.trim().startsWith("q="));
return { t: t.trim(), q: qv ? Number(qv.split("=")[1]) : 1 };
})
.filter((x) => x.t && x.q > 0)
.sort((a, b) => b.q - a.q);
for (const { t } of sorov) {
const teg = t === "*" ? "uz-Latn" : t;
const kanon = Intl.getCanonicalLocales(teg)[0];
if (TILLAR.includes(kanon)) return kanon;
if (/^uz-Cyrl/i.test(kanon)) return "uz-Cyrl";
const lotinmi = kanon === "uz" || kanon === "uz-UZ";
if (lotinmi || /^uz-Latn/i.test(kanon)) return "uz-Latn";
const asos = kanon.split("-")[0];
const m = TILLAR.find((x) => x.split("-")[0] === asos);
if (m) return m;
}
return "uz-Latn";
}
const holatlar = [
["uz-UZ,uz;q=0.9,ru;q=0.8", null],
["ru-RU,ru;q=0.9,en;q=0.8", null],
["uz-Cyrl-UZ", null],
["de-DE,de;q=0.9", null],
["en-US;q=0.5,ru;q=0.6", null],
["ru", "uz-Cyrl"],
["", null],
];
for (const [h, p] of holatlar) {
const izoh = p ? ` + profil ${p}` : "";
console.log(`"${h}"${izoh} → ${tilTanla(h, p)}`);
}Konsolda:
"uz-UZ,uz;q=0.9,ru;q=0.8" → uz-Latn
"ru-RU,ru;q=0.9,en;q=0.8" → ru
"uz-Cyrl-UZ" → uz-Cyrl
"de-DE,de;q=0.9" → uz-Latn
"en-US;q=0.5,ru;q=0.6" → ru
"ru" + profil uz-Cyrl → uz-Cyrl
"" → uz-LatnHar qatorni o'qing. en-US;q=0.5,ru;q=0.6 — ru ning q si katta, u tanlandi: tartib emas, q hal qiladi. de ni bilmaymiz — sukut. Profil bo'lsa, sarlavha umuman qaralmaydi.
Intl.getCanonicalLocales yozuvni standart shaklga keltiradi: UZ-cyrl-uz → uz-Cyrl-UZ. Til teglari (BCP 47) katta-kichik harfga sezgir emas, lekin taqqoslash uchun bir shakl kerak.
2.3 Kanon o'z xatosidan nima o'rgandi
Kanonning birinchi varianti "asos til" bo'yicha moslardi: uz-Cyrl-UZ → asosi uz → ro'yxatda birinchi uz- → uz-Latn. Kirill yozuvidagi foydalanuvchi lotin oldi. Xato jim edi — test yozilmaguncha ko'rinmadi. Tuzatish — skript subtegi (Cyrl, Latn) asos tildan oldin alohida tekshiriladi. Yuqoridagi koddagi /^uz-Cyrl/ qatori aynan shu tuzatish.
O'zbek tili uchun bu ayniqsa muhim: bitta til, ikki yozuv. Ko'p kutubxonalar uz ni bitta til deb oladi. Telefon sozlamasida uz-Cyrl-UZ ham, uz-UZ ham, uz-Latn-UZ ham uchraydi.
Diqqat: Javob tili
Accept-Languagega bog'liq bo'lsa, javobgaVary: Accept-Languagesarlavhasini qo'shing. Aks holda kesh (HTTP caching) rus foydalanuvchiga o'zbekcha javobni berib yuboradi.
3. Ko'plik shakllari
3.1 CLDR qoidalari
"1 ta vazifa", "5 ta vazifa" — o'zbekchada son bilan ot o'zgarmaydi. Ruschada esa: "1 задача", "2 задачи", "5 задач", "21 задача". Inglizchada: "1 task", "2 tasks". Har tilning qoidasi bor va ular CLDR (Unicode Common Locale Data Repository) ma'lumotlar bazasida yozilgan. JavaScript'da ular Intl.PluralRules orqali ochiq (Intl.PluralRules darsi):
for (const til of ["uz", "ru", "en"]) {
const pr = new Intl.PluralRules(til);
const kat = pr.resolvedOptions().pluralCategories.join("/");
const sonlar = [1, 2, 5, 21].map((n) => `${n}→${pr.select(n)}`);
console.log(`${til}: ${kat}; ${sonlar.join(", ")}`);
}Konsolda:
uz: one/other; 1→one, 2→other, 5→other, 21→other
ru: one/few/many/other; 1→one, 2→few, 5→many, 21→one
en: one/other; 1→one, 2→other, 5→other, 21→otherO'zbekchada CLDR bo'yicha ikki kategoriya (one, other) — grammatikada farq bo'lmasa ham. Ruschada to'rtta, va 21 — one ("21 задача"). Shuning uchun "son 1 bo'lsa birlik, aks holda ko'plik" degan if rus tilida xato.
3.2 i18next bilan
i18next — Node va brauzerda eng ko'p ishlatiladigan i18n kutubxonasi (21-qismdagi react-i18next ham uning ustiga qurilgan). Kalitlarga CLDR kategoriyasi qo'shimcha sifatida yoziladi: vazifa_one, vazifa_few, vazifa_many, vazifa_other. Kanon kodi:
import i18next from "i18next";
await i18next.init({
lng: "uz-Latn",
fallbackLng: "uz-Latn",
resources: {
"uz-Latn": { translation: {
vazifa_one: "{{count}} ta vazifa",
vazifa_other: "{{count}} ta vazifa",
salom: "Salom, {{ism}}!",
} },
ru: { translation: {
vazifa_one: "{{count}} задача",
vazifa_few: "{{count}} задачи",
vazifa_many: "{{count}} задач",
vazifa_other: "{{count}} задачи",
salom: "Привет, {{ism}}!",
} },
},
});
const t = (lng, n) => i18next.t("vazifa", { count: n, lng });
console.log([1, 2, 5, 21].map((n) => t("ru", n)).join(" | "));
console.log(i18next.t("salom", { ism: "<b>Aziz</b>", lng: "ru" }));Kanon chiqishidan tegishli qatorlar (uz-Cyrl resursi ham bor edi):
uz-Latn 1 ta vazifa | 2 ta vazifa | 5 ta vazifa | 21 ta vazifa | Salom, <b>Aziz</b>!
uz-Cyrl 1 та вазифа | 2 та вазифа | 5 та вазифа | 21 та вазифа | Салом, <b>Aziz</b>!
ru 1 задача | 2 задачи | 5 задач | 21 задача | Привет, <b>Aziz</b>!
i18next sukut escapeValue: HTML ekranlaydi
yo'q kalit: "yoq_kalit" | en (resursi yo'q) → fallback: Salom, A!Uchta muhim kuzatuv:
- HTML ekranlash sukut bo'yicha yoqilgan (
escapeValue: true):<b>→<b>, hatto/ham/bo'ldi. HTML email uchun bu himoya (XSS — foydalanuvchi ismi orqali skript). Lekin JSON API javobi uchun bu shovqin: frontend React o'zi ekranlaydi va foydalanuvchi ekranda<b>ni ko'radi. Qoida: chiqish formatiga qarab — HTML'da ekranlash yoqiq, JSON va oddiy matnda (SMS, Telegram oddiy rejim)interpolation: { escapeValue: false }. - Yo'q kalit — kalitning o'zi qaytadi:
"yoq_kalit". Foydalanuvchixato.matn_boshkabi g'alati matnni ko'radi. Testda barcha kalitlar har tilda borligini tekshiring. - Fallback:
enresursi yo'q —fallbackLng(uz-Latn) ishladi. Tarjima chala bo'lsa ham foydalanuvchi bo'sh joy ko'rmaydi.
4. ICU MessageFormat
i18next'ning {{count}} sintaksisi o'ziniki. Sanoatda boshqa standart ham bor — ICU MessageFormat: bitta satr ichida ko'plik, tanlov (masalan jins) va son/sana formati. Uni FormatJS (intl-messageformat), Java, Android va tarjima platformalari tushunadi. Kanon misoli:
import { IntlMessageFormat } from "intl-messageformat";
const uz =
"{ism} {jins, select, ayol {o'z} erkak {o'z} other {o'z}} " +
"vazifalaridan {n, plural, =0 {hech birini} other {# tasini}} " +
"bajardi; muddat: {m, date, long}";
const f = new IntlMessageFormat(uz, "uz-Latn");
const m = new Date("2026-10-28T13:00:00Z");
for (const n of [0, 1, 3, 25]) {
console.log(f.format({ ism: "Dilnoza", jins: "ayol", n, m }));
}Kanon chiqishi (o'zbekcha va ruscha satr):
uz-Latn Dilnoza o'z vazifalaridan hech birini bajardi; muddat: 28-oktabr, 2026
uz-Latn Dilnoza o'z vazifalaridan 1 tasini bajardi; muddat: 28-oktabr, 2026
uz-Latn Dilnoza o'z vazifalaridan 3 tasini bajardi; muddat: 28-oktabr, 2026
uz-Latn Dilnoza o'z vazifalaridan 25 tasini bajardi; muddat: 28-oktabr, 2026
ru Dilnoza выполнила ни одной задачи; срок: 28 октября 2026 г.
ru Dilnoza выполнила 1 задачу; срок: 28 октября 2026 г.
ru Dilnoza выполнила 3 задачи; срок: 28 октября 2026 г.
ru Dilnoza выполнила 25 задач; срок: 28 октября 2026 г.Qismlarni ko'ramiz:
{n, plural, =0 {...} other {# tasini}}—=0aniq son uchun alohida shakl,#— sonning o'zi.{jins, select, ayol {...} other {...}}— o'zbekchada jins fe'lga ta'sir qilmaydi (uchala variant bir xil), ruschada esaвыполнила/выполнил. Shuning uchun tanlov xabarning ichida: dasturchi koddaif (jins === "ayol")yozmaydi, tarjimon har til uchun o'zi hal qiladi.{m, date, long}— sana tilga mos: "28-oktabr, 2026" va "28 октября 2026 г.".
Diqqat qiling: ruscha =0 shaklidagi "выполнила ни одной задачи" grammatik jihatdan noto'g'ri (to'g'risi — "не выполнила ни одной задачи"). Bu kanon tarjimasidagi xato, formatlovchi aybdor emas. Tarjimani ona tilida so'zlashuvchi tekshirishi kerakligining yaxshi misoli.
Kanon Intl bilan to'g'ridan formatlashni ham sinagan:
son/valyuta/sana Intl bilan: 1 234 567,5 | chorshanba, 28-oktabr, 2026 | чоршанба, 28 октябр, 2026 | kechaSana Toshkent vaqtida (timeZone: "Asia/Tashkent") formatlandi. Serverda formatlashda vaqt zonasini doim aniq bering — server UTC'da ishlaydi, foydalanuvchi esa yo'q (Backend'da sana va vaqt).
| i18next | ICU MessageFormat | |
|---|---|---|
| Ko'plik | kalit qo'shimchasi (_few) |
satr ichida {n, plural, ...} |
| Tanlov (jins) | kontekst kalitlari | {x, select, ...} |
| Ekotizim | plaginlar, React, backend'lar | FormatJS, tarjima platformalari |
| Qachon | ko'p JS loyihalar, 21-qism bilan bir xil | murakkab grammatika, professional tarjimonlar |
i18next ICU formatini ham plagin orqali o'qiy oladi — ikkalasini aralashtirish shart emas.
5. Lotin ↔ kirill transliteratsiyasi
5.1 Kirilldan lotinga
O'zbek tilida ikki yozuv bir vaqtda ishlatiladi. Foydalanuvchi kirill yozuvida qidiradi, bazada lotin matn. Yoki aksincha: kirill interfeysdagi foydalanuvchiga lotinda kiritilgan vazifa nomi. Bu yerda transliteratsiya (bir yozuvdan ikkinchisiga harfma-harf o'tkazish) kerak. Tarjima emas — so'z o'zgarmaydi.
Kirilldan lotinga yo'nalish deyarli bir ma'noli. Kanon funksiyasi (qatorlarni bo'ldik):
const K2L = {
а: "a", б: "b", в: "v", г: "g", ғ: "gʻ", д: "d", е: "e", ё: "yo",
ж: "j", з: "z", и: "i", й: "y", к: "k", қ: "q", л: "l", м: "m",
н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ў: "oʻ",
ф: "f", х: "x", ҳ: "h", ц: "ts", ч: "ch", ш: "sh", ъ: "ʼ", ь: "",
э: "e", ю: "yu", я: "ya",
};
function kirill2lotin(s) {
return s
.replace(/(^|[^а-яёўқғҳ])(е)/giu, (_m, o, e) =>
`${o}${e === "Е" ? "YE" : "ye"}`)
.replace(/[а-яёўқғҳ]/giu, (c) => {
const k = K2L[c.toLowerCase()] ?? c;
if (c === c.toLowerCase()) return k;
return k.charAt(0).toUpperCase() + k.slice(1);
});
}
console.log(kirill2lotin("Ўзбекистон ҳаёти: шаҳар, ер, ёмғир"));
console.log(kirill2lotin("Тошкент вақти — ЕР ЮЗИ"));Konsolda:
Oʻzbekiston hayoti: shahar, yer, yomgʻir
Toshkent vaqti — YER YuZIQoidalar rasmiy lotin alifbosiga mos: е so'z boshida ye (ер → yer), so'z ichida e; ў → oʻ, ғ → gʻ (maxsus ʻ belgisi bilan); ъ → tutuq belgisi ʼ (қалъа → qalʼa).
Ikkinchi qatorda kanon tan olgan cheklov ko'rinadi: ЕР ЮЗИ → YER YuZI. Hammasi katta harfli so'zda Ю → YU bo'lishi kerak edi, funksiya esa faqat birinchi harfni katta qildi. YE uchun alohida qoida yozilgan, Ю, Я, Ё, Ч, Ш uchun esa yo'q. Mashqda tuzatamiz.
Maslahat: Darsdagi matnda biz oddiy apostrof (
o') yozamiz — bu kursning qoidasi. Transliteratsiya natijasida esa rasmiy alifbodagioʻ(U+02BB) belgisi chiqadi. Qidiruvda ikkalasi Qidiruv tizimlari darsidagi normallash bilan birlashtiriladi.
5.2 Lotindan kirillga — taxminiy
Teskari yo'nalish qiyinroq. Kanon funksiyasi sh → ш, ch → ч kabi juftlarni birinchi tekshiradi:
const L2K = [
["sh", "ш"], ["ch", "ч"], ["yo", "ё"], ["yu", "ю"], ["ya", "я"],
["ye", "е"], ["oʻ", "ў"], ["gʻ", "ғ"], ["ts", "ц"], ["a", "а"],
["b", "б"], ["d", "д"], ["e", "е"], ["f", "ф"], ["g", "г"],
["h", "ҳ"], ["i", "и"], ["j", "ж"], ["k", "к"], ["l", "л"],
["m", "м"], ["n", "н"], ["o", "о"], ["p", "п"], ["q", "қ"],
["r", "р"], ["s", "с"], ["t", "т"], ["u", "у"], ["v", "в"],
["x", "х"], ["y", "й"], ["z", "з"], ["ʼ", "ъ"],
];
function lotin2kirill(s) {
// o' va g' — harf (ʻ), boshqa joyda — tutuq belgisi (ʼ)
const x = s
.toLowerCase()
.replace(/[‘’'`ʻ]/g, "ʼ")
.replace(/([og])ʼ/g, "$1ʻ");
let r = "";
for (let i = 0; i < x.length; ) {
const m = L2K.find(([l]) => x.startsWith(l, i));
if (m) {
r += m[1];
i += m[0].length;
} else {
r += x[i];
i++;
}
}
return r;
}
console.log(lotin2kirill("Oʻzbekiston, gʻalaba, sog'liq, ma'no"));
console.log(lotin2kirill("Is'hoq, televizor, ishxona"));Konsolda:
ўзбекистон, ғалаба, соғлиқ, маъно
исъҳоқ, телевизор, ишхонаBirinchi qator to'g'ri: sog'liq → соғлиқ (apostrof g dan keyin — harf), ma'no → маъно (tutuq belgisi). Ikkinchi qatorda chegaralarni ko'ramiz:
ishxona→ишхона: to'g'ri — bu so'zda haqiqatanш.Is'hoq→исъҳоқ: tutuq belgisi to'g'ri ajratdi. Lekin apostrofsizIshoqyozilsa —ишоқbo'lardi. Lotindashikki xil o'qiladi:ш(shahar) vaс+ҳ(Ishoq). Lug'atsiz farqlab bo'lmaydi.televizor→телевизор— bu so'z to'g'ri chiqdi. Lekin rus o'zlashmalaridae/эvas/цnoaniq: lotindagisirkkirilldaциркyoziladi, funksiya esaсиркberadi.
Xulosa: kirill → lotin — ishonchli, lotin → kirill — taxminiy. Asl matnni bitta yozuvda (lotin) saqlang, ikkinchisini faqat ko'rsatish uchun hosil qiling. Kirill kontent sifatli bo'lishi kerak bo'lsa — uni alohida tarjima sifatida saqlang (pastdagi jadvallar).
5.3 Slug
Slug — URL uchun toza matn: /vazifalar/ozbek-tilida-tolov. Kanon funksiyasi kirillni lotinga o'tkazadi, belgilarni olib tashlaydi:
const slug = (s) =>
s
.normalize("NFKD")
.toLowerCase()
.replace(/[ʻʼ'’‘`]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80);
console.log(slug("Oʻzbek tilida “To‘lov”: reja #2"));
console.log(slug("Café va kofe!!!"));
console.log(JSON.stringify(slug("Ўқиш режаси")));Konsolda:
ozbek-tilida-tolov-reja-2
cafe-va-kofe
""Uchinchi qatorga qarang: kirill matn bo'sh slug ("") berdi — [^a-z0-9] hamma kirill harfni o'chirdi. Shuning uchun kanon slug'dan oldin kirill2lotin chaqiradi (mashqda). normalize("NFKD") + o'chirish é ni e qildi: harf va urg'u belgisi ajraldi, belgi keyin tashlandi. Slug'ni bazada noyob qiling — ikki vazifa bir xil nomli bo'lsa, oxiriga -2 qo'shiladi.
6. Bazada ko'p tilli kontent
6.1 Ikki usul
Mahsulot nomi uch tilda. Qanday saqlaymiz? Ikki asosiy usul:
JSONB ustun — bitta ustunda hamma tillar (JSONB). Biz ishga tushirdik (PostgreSQL 17.6):
CREATE TABLE mahsulot_j (
id int PRIMARY KEY,
nom jsonb NOT NULL CHECK (nom ? 'uz-Latn')
);
INSERT INTO mahsulot_j VALUES
(1, '{"uz-Latn": "Daftar", "ru": "Тетрадь", "en": "Notebook"}'),
(2, '{"uz-Latn": "Ruchka", "uz-Cyrl": "Ручка"}');
SELECT id, coalesce(nom->>'ru', nom->>'uz-Latn') AS nom
FROM mahsulot_j ORDER BY id;
INSERT INTO mahsulot_j VALUES (3, '{"ru": "Линейка"}');CREATE TABLE
INSERT 0 2
id | nom
----+---------
1 | Тетрадь
2 | Ruchka
(2 rows)
ERROR: new row for relation "mahsulot_j" violates check constraint "mahsulot_j_nom_check"
DETAIL: Failing row contains (3, {"ru": "Линейка"}).coalesce(nom->>'ru', nom->>'uz-Latn') — fallback: ruscha bo'lmasa, o'zbekcha. CHECK (nom ? 'uz-Latn') — asosiy til doim bo'lishini baza kafolatlaydi: faqat ruscha nom bilan mahsulot qo'shib bo'lmadi.
Tarjima jadvali — har til alohida qator:
CREATE TABLE mahsulot (id int PRIMARY KEY);
CREATE TABLE mahsulot_tarjima (
mahsulot_id int REFERENCES mahsulot,
til text,
nom text NOT NULL,
PRIMARY KEY (mahsulot_id, til)
);
INSERT INTO mahsulot VALUES (1), (2);
INSERT INTO mahsulot_tarjima VALUES
(1, 'uz-Latn', 'Daftar'), (1, 'ru', 'Тетрадь'),
(2, 'uz-Latn', 'Ruchka');
SELECT m.id, coalesce(t.nom, d.nom) AS nom
FROM mahsulot m
LEFT JOIN mahsulot_tarjima t
ON t.mahsulot_id = m.id AND t.til = 'ru'
JOIN mahsulot_tarjima d
ON d.mahsulot_id = m.id AND d.til = 'uz-Latn'
ORDER BY m.id;CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 3
id | nom
----+---------
1 | Тетрадь
2 | Ruchka
(2 rows)Natija bir xil, so'rov esa ikki JOIN bilan. LEFT JOIN — kerakli til (bo'lmasligi mumkin), oddiy JOIN — asosiy til (doim bor).
6.2 Kanon o'lchovi va tanlov
Kanon 10 000 mahsulot × 4 til bilan ikkalasini solishtirgan (PostgreSQL 18.6, 50 qatorli sahifa, 200 o'lchov medianasi):
JSONB: 2.72 ms, 1728 kB; tarjima jadvali (JOIN + fallback): 3.07 ms, 4176 kB. JSONB — oddiy, tillar kam; jadval — tarjimon ish jarayoni, til bo'yicha FTS/indeks.JSONB 2.4 barobar kam joy oldi va biroz tezroq. Lekin tanlov tezlik bilan emas, ish jarayoni bilan hal bo'ladi:
| Savol | JSONB | Tarjima jadvali |
|---|---|---|
| Tillar soni | kam, o'zgarmaydi | ko'p yoki qo'shilib boradi |
| Kim tarjima qiladi | dasturchi/admin bir formada | tarjimonlar, holat bilan ("tekshirildi") |
| Til bo'yicha qidiruv | ifoda indeksi kerak | til ustuni bilan oddiy indeks, FTS |
| Har tarjimaga metama'lumot | qiyin | qo'shimcha ustun (muallif, sana) |
Uch-to'rt til va admin panelda bitta forma — JSONB. Tarjima jarayoni, ko'p til va har til uchun alohida qidiruv — jadval.
Tekshirib ko'ring: Xato xabarlari (
"Matn bo'sh bo'lmasin") ham bazada saqlanadimi?
Javob
Odatda yo'q. Xato xabarlari, email shablonlari, bot javoblari — kod bilan birga o'zgaradigan matn: yangi validatsiya qo'shilsa, yangi kalit ham qo'shiladi. Ular JSON tarjima fayllarida, git'da turadi va deploy bilan chiqadi. Bazada esa foydalanuvchi yoki admin yaratgan kontent: mahsulot nomi, kategoriya, maqola. Chegara: "bu matnni kim o'zgartiradi — dasturchimi yoki admin?"
7. API javobida til
Xato javoblari Problem Details formatida: title va detail — inson uchun matn, type — mashina uchun barqaror identifikator. Tarjima faqat inson matniga qilinadi, type va maydon nomlari o'zgarmaydi. Frontend xatoni type bo'yicha ajratadi, foydalanuvchiga detail ni ko'rsatadi. Javobga Content-Language: ru sarlavhasini qo'shing — mijoz va kesh qaysi tilda kelganini biladi.
app.use((req, _res, next) => {
req.til = tilTanla(req.get("accept-language"), req.user?.til);
next();
});
function muammo(res, req, kalit, status) {
res.set("Content-Language", req.til);
res.set("Vary", "Accept-Language");
res.status(status).type("application/problem+json").json({
type: `https://vazifalar.example/xato/${kalit}`,
title: i18next.t(`xato.${kalit}.title`, { lng: req.til }),
status,
});
}req.user — 28-qismdagi autentifikatsiyadan keladi. Email va Telegram xabarlari ham req.til bilan emas, qabul qiluvchining profil tili bilan yaratiladi: eslatma fonda yuboriladi, u yerda so'rov yo'q.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Kirill foydalanuvchiga lotin javob | uz-Cyrl-UZ asos til bo'yicha moslandi |
skript subtegini alohida tekshirish |
| "21 задачи" | ko'plik n === 1 bilan tanlandi |
Intl.PluralRules / i18next _one/_few/_many |
Ekranda <b> |
i18next JSON javobda ham ekranladi | JSON uchun escapeValue: false |
Ekranda xato.matn_bosh |
kalit tarjima faylida yo'q | test: kalitlar hamma tilda |
| Kesh boshqa tildagi javobni berdi | Vary: Accept-Language yo'q |
sarlavhani qo'shing |
| Kirill slug bo'sh | slug faqat lotin harflarni qoldiradi | avval kirill2lotin |
| Sana bir kun farq qiladi | server UTC'da formatladi | timeZone ni aniq bering |
9. Mashqlar
Mashqlar kurs/mashqlar/29/55-i18n/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- Brauzer afzal tillarini yuboradigan sarlavha —
- Rus tilida 21 soni uchun CLDR kategoriyasi —
- O'zbek tilida CLDR kategoriyalari soni —
- i18next'da ruscha "5 задач" kaliti qo'shimchasi —
vazifa_ - Ishonchli transliteratsiya yo'nalishi — kirilldan ga
Yechim
- Accept-Language —
qqiymatlari bilan afzallik ro'yxati. - one — "21 задача".
- 2 —
onevaother. - many —
_one,_few,_many,_other. - lotin — teskarisi
sh/s+hkabi joylarda taxminiy.
2-mashq (o'rta): katta harfli so'zlar
Kanon cheklovini tuzating: kirill2lotin hammasi katta harfli so'zda (ЮЗИ, ЧОЙ, ШАҲАР) ikki harfli moslikni ham katta qilsin: YUZI, CHOY, SHAHAR. Aralash so'zda (Чой) eski xatti-harakat qolsin: Choy. Ishora: so'zlarga bo'lib, so'z to'liq katta harfli bo'lsa — natijani toUpperCase() qiling.
Yechim
const K2L = {
а: "a", б: "b", в: "v", г: "g", ғ: "gʻ", д: "d", е: "e", ё: "yo",
ж: "j", з: "z", и: "i", й: "y", к: "k", қ: "q", л: "l", м: "m",
н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ў: "oʻ",
ф: "f", х: "x", ҳ: "h", ц: "ts", ч: "ch", ш: "sh", ъ: "ʼ", ь: "",
э: "e", ю: "yu", я: "ya",
};
function kirill2lotin(s) {
return s
.replace(/(^|[^а-яёўқғҳ])(е)/giu, (_m, o, e) =>
`${o}${e === "Е" ? "YE" : "ye"}`)
.replace(/[а-яёўқғҳ]/giu, (c) => {
const k = K2L[c.toLowerCase()] ?? c;
if (c === c.toLowerCase()) return k;
return k.charAt(0).toUpperCase() + k.slice(1);
});
}
function kirill2lotinToliq(s) {
return s.replace(/[\p{L}ʼ]+/gu, (soz) => {
const lotin = kirill2lotin(soz);
const kattami = soz === soz.toUpperCase() && soz.length > 1;
return kattami ? lotin.toUpperCase() : lotin;
});
}
console.log(kirill2lotinToliq("Тошкент вақти — ЕР ЮЗИ"));
console.log(kirill2lotinToliq("ЧОЙ ва Чой, ШАҲАР"));
console.log(kirill2lotinToliq("ЎҚИШ"));Konsolda:
Toshkent vaqti — YER YUZI
CHOY va Choy, SHAHAR
OʻQISHЎҚИШ → OʻQISH: toUpperCase() ʻ belgisini o'zgartirmaydi (unda katta-kichik yo'q), shuning uchun natija to'g'ri. soz.length > 1 sharti bitta harfli А kabi so'zni "hammasi katta" deb hisoblamaslik uchun — aks holda Я → YA bo'lardi, gap boshidagi Я esa Ya bo'lishi kerak.
3-mashq (qiyin): ko'p tilli slug
slugYasa(nom, band) yozing: avval kirillni lotinga o'tkazsin (darsdagi kirill2lotin), keyin darsdagi slug ni qo'llasin. band — bazada mavjud slug'lar Set i. Slug band bo'lsa, oxiriga -2, -3, ... qo'shib, birinchi bo'sh variantni qaytarsin. Bo'sh natija (masalan faqat belgilar) bo'lsa — "vazifa".
Yechim
const K2L = {
а: "a", б: "b", в: "v", г: "g", ғ: "gʻ", д: "d", е: "e", ё: "yo",
ж: "j", з: "z", и: "i", й: "y", к: "k", қ: "q", л: "l", м: "m",
н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ў: "oʻ",
ф: "f", х: "x", ҳ: "h", ц: "ts", ч: "ch", ш: "sh", ъ: "ʼ", ь: "",
э: "e", ю: "yu", я: "ya",
};
function kirill2lotin(s) {
return s
.replace(/(^|[^а-яёўқғҳ])(е)/giu, (_m, o, e) =>
`${o}${e === "Е" ? "YE" : "ye"}`)
.replace(/[а-яёўқғҳ]/giu, (c) => {
const k = K2L[c.toLowerCase()] ?? c;
if (c === c.toLowerCase()) return k;
return k.charAt(0).toUpperCase() + k.slice(1);
});
}
const slug = (s) =>
s
.normalize("NFKD")
.toLowerCase()
.replace(/[ʻʼ'’‘`]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80);
function slugYasa(nom, band) {
const asos = slug(kirill2lotin(nom)) || "vazifa";
if (!band.has(asos)) return asos;
for (let n = 2; ; n++) {
const variant = `${asos}-${n}`;
if (!band.has(variant)) return variant;
}
}
const band = new Set(["oqish-rejasi", "oqish-rejasi-2", "vazifa"]);
console.log(slugYasa("Ўқиш режаси", band));
console.log(slugYasa("Yangi yil sovg'alari", band));
console.log(slugYasa("!!!", band));
console.log(slugYasa("Ерга ишлов бериш", band));Konsolda:
oqish-rejasi-3
yangi-yil-sovgalari
vazifa-2
yerga-ishlov-berishKirill nom endi bo'sh emas: Ўқиш режаси → oqish-rejasi, band bo'lgani uchun -2 ham band, -3 qaytdi. Haqiqiy bazada band.has o'rniga noyob indeks va INSERT ... ON CONFLICT bilan qayta urinish kerak: ikki so'rov bir vaqtda bir xil slug'ni "bo'sh" deb ko'rishi mumkin (Amaliyot: race condition'larni hal qilish).
10. Loyiha qadami: kanon tajribasi, loyihaga hali kirmaydi
Bu darsda vazifalar-api ga kod qo'shilmaydi. Kanon tilni aniqlash, ko'plik, ICU, transliteratsiya va ko'p tilli kontentni tajribada sinadi va bitta o'z xatosini (uz-Cyrl-UZ) topdi. Backend i18n loyihaga 28-qismdan keyin ulanadi: foydalanuvchi profilida til maydoni paydo bo'lgach, xato xabarlari va email shu tilda chiqadi.
O'zingiz sinab ko'ring: vazifalar-api ning bitta xato xabarini (matn bo'sh) uch tilda tarjima faylga chiqaring. curl -H "Accept-Language: ru" ... bilan javob tilini tekshiring va javobda Content-Language borligini ko'ring.
11. Real ishda
- Internet-do'kon, davlat xizmatlari, bank ilovalari — O'zbekistonda odatda uch til (o'zbek lotin, o'zbek kirill, rus), ko'pincha ingliz ham.
- Tarjima platformalari — Crowdin, Lokalise, Weblate: tarjimonlar JSON/ICU fayllar bilan ishlaydi, git bilan sinxronlanadi.
- Email va SMS shablonlari — har til uchun alohida shablon yoki kalitlar (Email shablonlari).
- NestJS —
nestjs-i18npaketi bu darsdagi g'oyalarni modul qilib beradi (30-qism). - Intervyuda: "Server foydalanuvchi tilini qanday aniqlaydi?", "Nega
count === 1ko'plik uchun yetmaydi?", "Ko'p tilli mahsulot nomini bazada qanday saqlaysiz?"
Xulosa
- Til tartibi: profil → URL → cookie →
Accept-Language(qbo'yicha) → sukut;uz-Cyrlni asos til bilan adashtirmang. - Ko'plik CLDR qoidasi bilan: o'zbekchada 2, ruschada 4 kategoriya; i18next
_one/_few/_many, ICU{n, plural, ...}. - i18next sukut bo'yicha HTML ekranlaydi — HTML uchun yaxshi, JSON uchun o'chiring.
- Kirill → lotin ishonchli, lotin → kirill taxminiy; asl matn bitta yozuvda.
- Slug'dan oldin transliteratsiya; noyoblik bazada.
- Ko'p tilli kontent: kam til — JSONB +
coalesce, tarjima jarayoni — alohida jadval.
Keyingi dars: Geolokatsiya — masofa, eng yaqin obyektlar va yetkazish zonalari: Haversine, Redis GEO, GeoJSON va manzilni koordinataga aylantirish.
Manbalar
- RFC 9110 §12.5.4 —
Accept-Language: https://www.rfc-editor.org/rfc/rfc9110#name-accept-language - BCP 47 — til teglari: https://www.rfc-editor.org/info/bcp47
- CLDR — ko'plik qoidalari: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
- i18next — ko'plik va interpolatsiya (
escapeValue): https://www.i18next.com/translation-function/plurals va https://www.i18next.com/translation-function/interpolation - FormatJS — ICU MessageFormat sintaksisi: https://formatjs.github.io/docs/core-concepts/icu-syntax
- Kanon tajribasi (IlmHamroh, 2026-10-27): i18next 26.5.3, intl-messageformat 12.1.4, PostgreSQL 18.6, Node 24.21.0. Brauzer bloklari, SQL (PostgreSQL 17.6) va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!