IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime55/69-dars21 daqiqa
Mundarija (27)

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-Language sarlavhasidan 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-Language sarlavhasini o'qib, q qiymatlari va profil bilan tilni tanlaysiz — uz-Cyrl tuzog'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:

  1. Profil sozlamasi — foydalanuvchi o'zi tanlagan. Eng ishonchli.
  2. URL yoki parametr — /ru/... yoki ?til=ru (ochiq sahifalar, SEO uchun).
  3. Cookie — kirmagan foydalanuvchining oxirgi tanlovi.
  4. Accept-Language — brauzer yoki telefon tili.
  5. 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):

js
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:

text
"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-Latn

Har 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-Language ga bog'liq bo'lsa, javobga Vary: Accept-Language sarlavhasini 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):

js
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:

text
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→other

O'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:

js
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):

text
uz-Latn  1 ta vazifa | 2 ta vazifa | 5 ta vazifa | 21 ta vazifa | Salom, &lt;b&gt;Aziz&lt;&#x2F;b&gt;!
uz-Cyrl  1 та вазифа | 2 та вазифа | 5 та вазифа | 21 та вазифа | Салом, &lt;b&gt;Aziz&lt;&#x2F;b&gt;!
ru       1 задача | 2 задачи | 5 задач | 21 задача | Привет, &lt;b&gt;Aziz&lt;&#x2F;b&gt;!
i18next sukut escapeValue: HTML ekranlaydi
yo'q kalit: "yoq_kalit" | en (resursi yo'q) → fallback: Salom, A!

Uchta muhim kuzatuv:

  1. HTML ekranlash sukut bo'yicha yoqilgan (escapeValue: true): <b> → &lt;b&gt;, hatto / ham &#x2F; 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 &lt;b&gt; ni ko'radi. Qoida: chiqish formatiga qarab — HTML'da ekranlash yoqiq, JSON va oddiy matnda (SMS, Telegram oddiy rejim) interpolation: { escapeValue: false }.
  2. Yo'q kalit — kalitning o'zi qaytadi: "yoq_kalit". Foydalanuvchi xato.matn_bosh kabi g'alati matnni ko'radi. Testda barcha kalitlar har tilda borligini tekshiring.
  3. Fallback: en resursi 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:

js
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):

text
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}} — =0 aniq 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 kodda if (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:

text
son/valyuta/sana Intl bilan: 1 234 567,5 | chorshanba, 28-oktabr, 2026 | чоршанба, 28 октябр, 2026 | kecha

Sana 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):

js
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:

text
Oʻzbekiston hayoti: shahar, yer, yomgʻir
Toshkent vaqti — YER YuZI

Qoidalar 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 alifbodagi oʻ (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:

js
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:

text
ўзбекистон, ғалаба, соғлиқ, маъно
исъҳоқ, телевизор, ишхона

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 apostrofsiz Ishoq yozilsa — ишоқ bo'lardi. Lotinda sh ikki xil o'qiladi: ш (shahar) va с+ҳ (Ishoq). Lug'atsiz farqlab bo'lmaydi.
  • televizor → телевизор — bu so'z to'g'ri chiqdi. Lekin rus o'zlashmalarida e/э va s/ц noaniq: lotindagi sirk kirillda цирк 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:

js
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:

text
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):

sql
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": "Линейка"}');
text
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:

sql
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;
text
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):

text
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.

js
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 &lt;b&gt; 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 — q qiymatlari bilan afzallik ro'yxati.
  • one — "21 задача".
  • 2 — one va other.
  • many — _one, _few, _many, _other.
  • lotin — teskarisi sh/s+h kabi 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
js
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:

text
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
js
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:

text
oqish-rejasi-3
yangi-yil-sovgalari
vazifa-2
yerga-ishlov-berish

Kirill 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-i18n paketi bu darsdagi g'oyalarni modul qilib beradi (30-qism).
  • Intervyuda: "Server foydalanuvchi tilini qanday aniqlaydi?", "Nega count === 1 ko'plik uchun yetmaydi?", "Ko'p tilli mahsulot nomini bazada qanday saqlaysiz?"

Xulosa

  • Til tartibi: profil → URL → cookie → Accept-Language (q bo'yicha) → sukut; uz-Cyrl ni 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!