IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA3/32-dars30 daqiqa
Mundarija (42)

React i18n II: o'zbek lotin va kirill, til manzilda va type-safe kalitlar

Qisqacha: Ko'p tilli ilovada til manzilda turadi: /statistika — o'zbekcha, /ru/statistika — ruscha. Shunda ulashilgan havola o'sha tilda ochiladi. O'zbek tilining ikki yozuvi bor — ular ikki xil til belgisi: uz (lotin) va uz-Cyrl (kirill). Kirill fayli qo'lda yozilmaydi, lotinchadan skript bilan yasaladi. Tarjima kalitlarining turi TypeScript'ga aytiladi: t("bolimlar.royxatt") dagi xato ilova ishga tushmasdan ushlanadi. Yana bir tuzoq: Chrome'da Intl o'zbek lotin sanasini bilmaydi va «2026 M10 17» chiqaradi.

Bu darsda

  • uz, uz-Cyrl, uz-Latn, ru-RU kabi til belgilarini o'qiysiz va <html lang> ni to'g'ri qo'yasiz.
  • Kirill tarjimasini lotinchadan skript bilan yasaysiz va test bilan "eskirmaganini" tekshirasiz.
  • Tilni manzilga ko'chirasiz: /ru/..., /uz-cyrl/..., /en/... va router basename.
  • Tarjima kalitlarini type-safe qilasiz va i18next-cli bilan kalitlarni tekshirasiz.
  • Chrome'dagi Intl tuzog'ini taniysiz va o'zbekcha sanani o'zingiz formatlaysiz.

Oldin bilishingiz kerak: i18n I: react-i18next bilan ko'p tilli ilova, Kutubxona turlari va module augmentation, Intl.DateTimeFormat, Intl.PluralRules, ListFormat, DisplayNames, React Router I, Ko'p tilli matn, yo'nalish va tahrir izlari.

1. Nega bu kerak?

Oldingi darsda vazifalar ikki tilda gapira boshladi: o'zbekcha va ruscha. Tanlangan til localStorage da saqlanadi. Ilova ishlayapti, lekin Malika bir kun to'rtta muammoga duch keldi.

Birinchisi — havola. Malika statistika sahifasini ruscha ochib, havolasini Sardorga yubordi. Sardorda esa sahifa o'zbekcha ochildi. Til Malikaning brauzerida qolgan edi, havolada emas.

Ikkinchisi — yozuv. Malikaning buvisi o'zbekchani faqat kirillda o'qiydi. "O'zbekcha" tanlovi esa bitta, u lotincha.

Uchinchisi — xato kalit. Sardor t("bolimlar.royxatt") deb yozdi (bitta t ortiqcha). Hech qanday xato chiqmadi — ekranda shunchaki bolimlar.royxatt degan yozuv paydo bo'ldi. Uni bir hafta o'tib foydalanuvchi topdi.

To'rtinchisi — sana. Statistikada "Holat: 17-oktabr" chiqishi kerak edi. Chrome'da esa «2026 M10 17» chiqdi.

Hayotiy o'xshatish: mehmonxona xonasining raqami eshikda yozilgan — kimga aytsangiz ham, o'sha xonaga boradi. Til manzilda bo'lsa, havola ham kimga yuborilsa, o'sha tilda ochiladi. Bu dars to'rtala muammoni navbat bilan yechadi.

2. Til belgisi: uz, uz-Cyrl va uz-Latn

2.1 Belgi qanday tuzilgan

Til belgisi (locale) — tilni va kerak bo'lsa yozuvni hamda mamlakatni bildiruvchi qisqa kod. U Intl darslarida uchragan edi: uz, ru, en-US. Belgilar qoidasi BCP 47 deb ataladi. Belgi tire bilan ajratilgan uch qismdan iborat bo'lishi mumkin:

Qism Nima Misol
til 2–3 kichik harf uz, ru, en
yozuv (script) 4 harf, birinchisi katta Latn (lotin), Cyrl (kirill), Arab
hudud (region) 2 katta harf UZ, RU, US

Demak uz-Cyrl-UZ — "O'zbekistondagi o'zbek tili, kirill yozuvida". Ko'pincha hudud kerak emas: uz-Cyrl yetadi. Yozuvsiz uz esa o'zbek tilining asosiy yozuvini bildiradi — bugun u lotin. Intl.Locale buni o'zi aytib beradi:

js
const kirill = new Intl.Locale("uz-Cyrl-UZ");
console.log(kirill.language, kirill.script, kirill.region);
console.log(new Intl.Locale("uz").maximize().toString());
console.log(Intl.getCanonicalLocales(["UZ-cyrl", "uz-latn-uz"]));

Konsolda:

text
uz Cyrl UZ
uz-Latn-UZ
[ 'uz-Cyrl', 'uz-Latn-UZ' ]

maximize() — "to'liq shaklini ayt": uz → uz-Latn-UZ, ya'ni yozuvsiz uz = lotin. getCanonicalLocales belgini standart yozilishiga keltiradi: harflar katta-kichikligi ham qoidaga kiradi (uz-Cyrl, uz-cyrl emas). Shuning uchun vazifalar dagi to'rt til: uz, uz-Cyrl, ru, en. Lotin uchun qisqa uz yetadi — maximize ko'rsatganidek, u uz-Latn bilan bir xil.

2.2 <html lang> — butun sahifaning tili

Brauzer til belgisini <html lang="…"> atributidan o'qiydi va uch joyda ishlatadi:

  • Ekran o'quvchi — Malikaning ekran o'quvchisi lang="ru" da ruscha talaffuzga o'tadi. lang="uz" sahifada ruscha matn bo'lsa, u ruscha so'zlarni o'zbekcha qoidada o'qiydi — tushunib bo'lmaydi.
  • Brauzer tarjimoni va imlo tekshiruvi — "sahifa qaysi tilda" degan savolga javob shu.
  • CSS :lang() va Intl — tilga qarab bezash va formatlash.

Bitta element boshqa tilda bo'lsa — o'sha elementga lang (Ko'p tilli matn darsida). vazifalar dagi til tanlovida har til nomi o'z tilida va o'z lang i bilan yozilgan: «Ўзбекча» — lang="uz-Cyrl", «Русский» — lang="ru". Kanon <html lang> ni ilova chizilishidan oldin qo'yadi (tilniTayyorla(), document.documentElement.lang = til).

Tekshirib ko'ring: Nega «Ўзбекча» yozuvli radio tugmaga lang="uz-Cyrl" qo'yilgan, butun sahifa esa lang="uz"?

Javob

Ekran o'quvchi har so'zni o'sha elementning tili qoidasida o'qiydi. «Ўзбекча» kirillda yozilgan — lang siz u lotin qoidasida o'qilib, buzib talaffuz qilinadi. Element darajasidagi lang shu so'z uchun to'g'ri qoidani beradi.

3. Kirill: ikkinchi fayl qo'lda yozilmaydi

3.1 Muammo: ikki nusxa — ikki xil haqiqat

Kirill tarjimasini qo'lda yozish mumkin. Lekin bir oydan keyin Sardor lotinchada "Ro'yxat" ni "Vazifalar ro'yxati" ga o'zgartirdi, kirillni esa unutdi. Endi ikki fayl ikki xil gapiradi.

O'zbek lotin va kirill yozuvlari harfma-harf deyarli to'liq mos keladi. Bunday o'girish transliteratsiya (transliteration) deyiladi — so'z tarjima qilinmaydi, faqat harflari boshqa alifboga almashadi. Demak kirill faylini dastur yasay oladi. Kanon qarori: manba — faqat lotin fayli, kirill — undan skript bilan.

3.2 Qoidalar

Ko'p harf birga-bir almashadi (a → а, b → б). Qiziqlari — qolganlari:

Lotin Kirill Izoh
o', g' ў, ғ apostrof — harfning bir qismi
sh, ch ш, ч ikki harf → bitta
yo, yu, ya, ye ё, ю, я, е lekin yo'q → йўқ (y + o')
h, q, x ҳ, қ, х o'zbekchaga xos harflar
e э yoki е so'z boshida va unlidan keyin — э
' (so'z ichida) ъ tutuq belgisi: ma'lumot → маълумот

Kanon skriptining yuragi — kirillga funksiyasi. Uning soddalashtirilgan nusxasi (qoidalar o'sha, kanondagi izohlar va JSDoc olib tashlangan):

js
const APOSTROFLAR = "'‘’ʻʼ`";
const HARFLAR = {
  a: "а", b: "б", c: "ц", d: "д", e: "е", f: "ф", g: "г",
  h: "ҳ", i: "и", j: "ж", k: "к", l: "л", m: "м", n: "н",
  o: "о", p: "п", q: "қ", r: "р", s: "с", t: "т", u: "у",
  v: "в", x: "х", y: "й", z: "з",
};
const BIRIKMALAR = [
  ["sh", "ш"], ["ch", "ч"], ["yo", "ё"],
  ["yu", "ю"], ["ya", "я"], ["ye", "е"],
];
const harfmi = (h) => /\p{L}/u.test(h);
const holatida = (asl, k) =>
  asl !== asl.toLowerCase() ? k.toUpperCase() : k;

function bolakniOgir(s) {
  let natija = "";
  for (let i = 0; i < s.length; i++) {
    const h = s[i];
    const kichik = h.toLowerCase();
    const keyingi = s[i + 1] ?? "";
    const oldingi = s[i - 1] ?? "";
    if ("og".includes(kichik) && APOSTROFLAR.includes(keyingi)) {
      natija += holatida(h, kichik === "o" ? "ў" : "ғ");
      i++;
      continue;
    }
    const ikki = kichik + keyingi.toLowerCase();
    const birikma = BIRIKMALAR.find(([lotin]) => lotin === ikki);
    const yoQ = ikki === "yo" && APOSTROFLAR.includes(s[i + 2]);
    if (birikma && !yoQ) {
      natija += holatida(h, birikma[1]);
      i++;
      continue;
    }
    if (kichik === "e") {
      const boshida = !harfmi(oldingi) || "aeiou".includes(oldingi);
      natija += holatida(h, boshida ? "э" : "е");
      continue;
    }
    const tutuq = harfmi(oldingi) && harfmi(keyingi);
    if (APOSTROFLAR.includes(h) && tutuq) {
      natija += "ъ";
      continue;
    }
    natija += HARFLAR[kichik] ? holatida(h, HARFLAR[kichik]) : h;
  }
  return natija;
}

// {{o'rin}} ichi tegilmaydi: "{{royxat}}" kirillga o'tmasin
function kirillga(matn) {
  return matn
    .split(/(\{\{[^}]*\}\})/)
    .map((bolak, i) => (i % 2 === 1 ? bolak : bolakniOgir(bolak)))
    .join("");
}

console.log(kirillga("O'chirishdan oldin so'rash"));
console.log(kirillga("Teg yo'q. Ma'lumot: {{royxat}}"));
console.log(kirillga("Eslatma: shahar, ekran"));
console.log(kirillga("Sirk va konsert"));

Konsolda:

text
Ўчиришдан олдин сўраш
Тег йўқ. Маълумот: {{royxat}}
Эслатма: шаҳар, экран
Сирк ва консерт

Uch qatorni o'qing. {{royxat}} o'zgarmadi — bu i18next'ning o'rni (i18n I dagi interpolatsiya), uni o'girsak {{ройхат}} bo'lib qoladi va qiymat qo'yilmaydi. yo'q → йўқ, ё emas.

To'rtinchi qator — chegara. To'g'ri imlo «Цирк ва концерт»: ruscha o'zlashmalarda s ba'zan ц bo'ladi, harf qoidasi buni bilmaydi. Kanon skripti izohida ham shu: natija ko'z bilan tekshiriladi.

3.3 npm run kirill va "eskirmaganlik" testi

Kanondagi skriptlar/kirill.js uz/*.json dan uz-Cyrl/*.json ni yozadi — kalitlar o'zgarmaydi, qiymatlar o'giriladi (Node 24):

bash
npm run kirill
text
uz-Cyrl/sozlamalar.json
uz-Cyrl/umumiy.json

Skriptni unutish mumkin. Shuning uchun tarjima.test.js skript hozir yozadigan matnni diskdagi fayl bilan solishtiradi (kanon):

js
test("uz-Cyrl fayllari lotinchadan yangi yasalgan (npm run kirill)", () => {
  for (const [fayl, matn] of kirillFayllari(ILDIZ)) {
    assert.equal(
      readFileSync(join(I18N, "uz-Cyrl", fayl), "utf8"),
      matn,
      fayl,
    );
  }
});

Lotinchada so'z o'zgarib, npm run kirill unutilsa — npm test qizil.

Tekshirib ko'ring: Nega skript kirill faylini "yangilamaydi", balki har safar butunlay qaytadan yozadi?

Javob

Kirill fayli to'liq lotinchadan kelib chiqadi. Qaytadan yozish lotinchada o'chirilgan kalitni kirilldan ham o'chiradi va qo'lda kiritilgan o'zgarishni yo'qotadi. Test ham oddiy qoladi: "diskdagi fayl = skript natijasi". Tuzatish kerak bo'lsa (консерт), u skript qoidasiga yoziladi, faylga emas.

4. Til manzilda

4.1 Nega localStorage emas

Til localStorage da bo'lsa, u brauzerniki. Manzilda bo'lsa — havolaniki. Farqi uch joyda seziladi:

Vaziyat localStorage Manzil (/ru/...)
havolani ulashish qabul qiluvchi o'z tilida ko'radi yuborilgan tilda ochiladi
qidiruv tizimi bitta sahifa, bitta til har til — alohida sahifa
«Orqaga» tugmasi til qaytmaydi til ham qaytadi

Qidiruv tizimiga har tilni ko'rsatish (hreflang) — SPA uchun SEO darsida. Bugun — manzilning o'zi.

4.2 Manzil sxemasi

Kanon qarori — asosiy til prefikssiz, qolganlari bitta bo'lak bilan:

Manzil Til
/statistika uz (lotin)
/uz-cyrl/statistika uz-Cyrl
/ru/statistika ru
/en/statistika en

O'zbekcha prefikssiz — eski havolalar va xatcho'plar (bookmark) buzilmasin. Manzilda bo'lak kichik harfda (uz-cyrl), til kodi esa BCP 47 bo'yicha (uz-Cyrl). Noma'lum prefiks (/de/statistika) — oddiy yo'l: «Sahifa topilmadi».

Manzil → til → chizish oqimi:

flowchart TB
  M["Manzil: /ru/statistika"] --> Y["yoldanTil()"]
  Y --> T["til: ru"]
  Y --> A["basename: /ru"]
  T --> I["i18next lng + html lang"]
  A --> R["Router: /statistika"]
  I --> E["Ruscha statistika sahifasi"]
  R --> E
  E -->|til tanlandi| L["location.assign(/en/statistika)"]
  L --> M

Diagrammaga qarang: manzil bitta, undan ikki narsa chiqadi — til va router uchun asos. Marshrutlar (/statistika, /vazifa/:id) tildan bexabar qoladi.

4.3 yol.ts — sof funksiyalar

Manzilni o'qish va yasash — DOM'siz, sof funksiyalar. Kanondagi yol.ts aynan (pastidagi console.log lar — biz qo'shdik):

ts
export const TILLAR = ["uz", "uz-Cyrl", "ru", "en"] as const;
export type Til = (typeof TILLAR)[number];
export const ASOSIY_TIL: Til = "uz";

/** Til → manzil bo'lagi. BCP 47 kodi ("uz-Cyrl"),
 *  manzilda — kichik harf */
const PREFIKSLAR: Readonly<Record<Til, string>> = Object.freeze({
  uz: "",
  "uz-Cyrl": "uz-cyrl",
  ru: "ru",
  en: "en",
});

/** Satr bizning tillardan birimi (tashqi qiymat) */
export function tilmi(qiymat: unknown): qiymat is Til {
  return TILLAR.some((til) => til === qiymat);
}

export type TilYoli = {
  readonly til: Til;
  /** router basename'i: ilova papkasi + til ("/vazifalar/ru") */
  readonly asos: string;
  /** til prefiksidan keyingi yo'l ("/statistika") */
  readonly qolgani: string;
};

export function yoldanTil(yol: string, ilova = "/"): TilYoli {
  const papka = ilova.endsWith("/") ? ilova : `${ilova}/`;
  const ichki = yol.startsWith(papka) ? yol.slice(papka.length) : "";
  const [birinchi = "", ...qolgan] = ichki.split("/");
  const til = TILLAR.find(
    (t) => t !== ASOSIY_TIL && PREFIKSLAR[t] === birinchi.toLowerCase(),
  );
  const ildiz = papka.replace(/\/$/, "");
  if (til === undefined) {
    return {
      til: ASOSIY_TIL,
      asos: ildiz || "/",
      qolgani: `/${ichki}`,
    };
  }
  return {
    til,
    asos: `${ildiz}/${PREFIKSLAR[til]}`,
    qolgani: `/${qolgan.join("/")}`,
  };
}

export function tildaYol(
  yol: string,
  qidiruv: string,
  til: Til,
  ilova = "/",
): string {
  const { qolgani } = yoldanTil(yol, ilova);
  const papka = (ilova.endsWith("/") ? ilova : `${ilova}/`).replace(
    /\/$/,
    "",
  );
  const prefiks = PREFIKSLAR[til] === "" ? "" : `/${PREFIKSLAR[til]}`;
  const toliq = `${papka}${prefiks}${qolgani}`;
  return `${toliq === "" ? "/" : toliq}${qidiruv}`;
}

console.log(yoldanTil("/ru/vazifa/2"));
const pages = yoldanTil("/vazifalar/ru/statistika", "/vazifalar/");
console.log(pages.asos);
console.log(tildaYol("/statistika", "?filtr=faol", "uz-Cyrl"));
console.log(tildaYol("/ru/statistika", "", "uz"));
console.log(tilmi("uz-cyrl"));

Konsolda:

text
{ til: 'ru', asos: '/ru', qolgani: '/vazifa/2' }
/vazifalar/ru
/uz-cyrl/statistika?filtr=faol
/statistika
false

Ikki qatorga e'tibor bering. /vazifalar/ru/statistika — GitHub Pages'dagi nusxa: ilova /vazifalar/ papkasida turadi, shuning uchun ilova parametri bor (SPA routing qanday ishlaydi dagi GitHub Pages). Oxirgi qator: tilmi("uz-cyrl") — false. Manzildagi kichik harfli bo'lak til kodi emas, uni faqat PREFIKSLAR jadvali tarjima qiladi.

4.4 Router bilan: basename

basename — router marshrutlari qaysi manzildan "boshlanishi". main.tsx dagi o'zgarish (kanon):

diff
 const router = createBrowserRouter(marshrutlar, {
-  basename: import.meta.env.BASE_URL.replace(/\/$/, "") || "/",
+  // 21/#03: til ham manzilda — "/ru/statistika" da basename
+  // "/ru" (Pages'da "/vazifalar/ru"): marshrutlar va havolalar
+  // tildan bexabar qoladi
+  basename: tilYoli.asos,
 });

/ru/statistika ochilganda basename = /ru, router esa /statistika ni ko'radi. <Link to="/sozlamalar"> o'zi /ru/sozlamalar ga aylanadi — birorta havolani o'zgartirish kerak bo'lmadi.

Narxi bor: basename router yaratilganda qotadi. Til tanlansa, router'ni almashtirib bo'lmaydi — kanon sahifani yangi manzilda qayta ochadi:

ts
export function tilniAlmashtir(til: Til): void {
  location.assign(
    tildaYol(
      location.pathname,
      location.search,
      til,
      import.meta.env.BASE_URL,
    ),
  );
}

Qayta yuklash tez: fayllar Service Worker keshida (Service Worker hayot sikli), tarjima — kichik bitta fayl, ro'yxat localStorage da qoladi. Baribir bu qayd etilgan qarz (TEXNIK-QARZ 62).

Ikkinchi yo'l ham bor: tilni har sahifada manzildan hisoblash. Natija oynasida manzil satri yo'q, shuning uchun xotiradagi router ishlatamiz (React Router I dagidek). Til — state emas, manzildan hisoblanadi:

jsx
import { useEffect } from "react";
import {
  createMemoryRouter,
  RouterProvider,
  Link,
  useLocation,
} from "react-router";
import i18next from "i18next";

const i18n = i18next.createInstance();
void i18n.init({
  lng: "uz",
  resources: {
    uz: { translation: { royxat: "Ro'yxat", stat: "Statistika" } },
    "uz-Cyrl": {
      translation: { royxat: "Рўйхат", stat: "Статистика" },
    },
    ru: {
      translation: { royxat: "Список", stat: "Статистика" },
    },
  },
});

// til → manzil bo'lagi (asosiy til — prefikssiz)
const PREFIKS = { uz: "", "uz-Cyrl": "/uz-cyrl", ru: "/ru" };

function yolniOqi(yol) {
  const [, birinchi, ...qolgan] = yol.split("/");
  const til = Object.keys(PREFIKS).find(
    (t) => t !== "uz" && PREFIKS[t] === `/${birinchi}`,
  );
  return til
    ? { til, qolgani: `/${qolgan.join("/")}` }
    : { til: "uz", qolgani: yol };
}

function Sahifa() {
  const { pathname } = useLocation();
  const { til, qolgani } = yolniOqi(pathname);
  const t = i18n.getFixedT(til); // shu til uchun t()
  const p = PREFIKS[til];
  useEffect(() => {
    document.documentElement.lang = til;
  }, [til]);
  return (
    <>
      <nav>
        <Link to={`${p}/`}>{t("royxat")}</Link>{" "}
        <Link to={`${p}/statistika`}>{t("stat")}</Link>
      </nav>
      <p>Manzil: {pathname}</p>
      <p>lang: {til}</p>
      <nav>
        <Link to={qolgani} lang="uz">
          O'zbekcha
        </Link>{" "}
        <Link to={`/uz-cyrl${qolgani}`} lang="uz-Cyrl">
          Ўзбекча
        </Link>{" "}
        <Link to={`/ru${qolgani}`} lang="ru">
          Русский
        </Link>
      </nav>
    </>
  );
}

const marshrutlar = [{ path: "*", Component: Sahifa }];
const router = createMemoryRouter(marshrutlar, {
  initialEntries: ["/statistika"],
});

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda («Русский» → «Список»):

text
Список Статистика
Manzil: /ru/
lang: ru
O'zbekcha Ўзбекча Русский

Sahifa qayta yuklanmadi: manzil o'zgardi, yolniOqi tilni hisobladi, getFixedT(til) shu tildagi t ni berdi. «Список» esa prefiksni saqlab, ruscha bosh sahifaga olib bordi.

Bu yo'lning narxi — har havola prefiksni o'zi qo'shishi kerak (${p}/statistika). Bitta havolada unutilsa, foydalanuvchi jimgina o'zbekchaga "tushib ketadi". Kanondagi basename buni router darajasida hal qiladi — havolalar ko'p ilovada shu xavfsizroq. Next.js'da til odatda marshrut segmenti bo'ladi (app/[lang]/…) — i18n Next.js'da darsida ko'ramiz.

Tekshirib ko'ring: Kanonda /uz/statistika manzili qaysi tilda ochiladi?

Javob

«Sahifa topilmadi». PREFIKSLAR.uz — bo'sh satr, shuning uchun uz bo'lagi prefiks deb tanilmaydi va yo'lda qoladi: yoldanTil("/uz/statistika").qolgani — "/uz/statistika" (kanon testi). Bitta sahifaga ikki manzil (/ va /uz/) bo'lmaydi.

5. Type-safe kalitlar

5.1 CustomTypeOptions — kalitlar ro'yxati TypeScript'da

Kutubxona turlari va module augmentation darsidagi "Register" naqshini eslang: kutubxona bo'sh interfeys beradi, siz to'ldirasiz. i18next'da u CustomTypeOptions — o'sha darsdagi va'da shu yerda bajariladi. Kanondagi turlar.d.ts (kod aynan, izoh qatorlari telefonga sig'ishi uchun bo'lingan):

ts
// i18n/turlar.d.ts — tarjima kalitlarining turi (21/#03).
// i18next'ga asosiy til fayllarining SHAKLI aytiladi:
// t("bolimlar.royxatt") — tsc xatosi (ish vaqtida jim kalitni
// chiqarardi), {{o'rin}} lar ham tekshiriladi.
// Faqat tur: build'da yo'qoladi
import "i18next";
import type umumiy from "./uz/umumiy.json";
import type sozlamalar from "./uz/sozlamalar.json";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "umumiy";
    resources: {
      umumiy: typeof umumiy;
      sozlamalar: typeof sozlamalar;
    };
  }
}

Tur asosiy til faylidan olinadi: uz/umumiy.json ga kalit qo'shilsa, tur o'zi yangilanadi. Ichma-ich kalitlar "bolimlar.royxat" ko'rinishidagi satrlarga aylanadi — buni Template literal types qiladi.

Natija oynasida JSON fayl yo'q, shuning uchun shu g'oya bitta blokda — resurslar as const obyekt:

tsx
import i18next from "i18next";
import { initReactI18next, useTranslation } from "react-i18next";

const umumiy = {
  bolimlar: { royxat: "Ro'yxat", statistika: "Statistika" },
  salom: "Salom, {{ism}}!",
} as const;

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "umumiy";
    resources: { umumiy: typeof umumiy };
  }
}

void i18next.use(initReactI18next).init({
  lng: "uz",
  defaultNS: "umumiy",
  resources: { uz: { umumiy } },
});

export default function App() {
  const { t } = useTranslation();
  return (
    <p>
      {t("salom", { ism: "Malika" })} {t("bolimlar.royxat")}
    </p>
  );
}

Ekranda:

text
Salom, Malika! Ro'yxat

Endi Sardorning xatosi — o'sha modulga yangi komponent:

tsx
function Menyu() {
  const { t } = useTranslation();
  return <a href="/">{t("bolimlar.royxatt")}</a>;
}

Xato:

text
TS2345: Argument of type '["bolimlar.royxatt"]' is not assignable to parameter of type '[key: string | string[], options: TOptionsBase & $Dictionary & { defaultValue: string; }] | [key: "bolimlar.royxat" | "bolimlar.statistika" | "salom" | ("bolimlar.royxat" | "bolimlar.statistika" | "salom" | TemplateStringsArray)[] | TemplateStringsArray, options?: (TOptionsBase & ... 1 more ... & { ...; }) | undefin...'.

Xabar uzun, lekin qo'rqmang (TS xato xabarlarini o'qish). Tarjimasi: "["bolimlar.royxatt"] ni bu parametr turiga berib bo'lmaydi". O'rtada ruxsat etilgan kalitlar: "bolimlar.royxat" | "bolimlar.statistika" | "salom". Oxiridagi ... — tsc uzun turni qisqartirgan. Muharrir esa shu ro'yxatni t(" yozishingiz bilan taklif qiladi.

Kanon xuddi shu xatoni haqiqiy ilovada ham tekshirgan: Qobiq.tsx da ataylab "bolimlar.royxatt" yozilganda tsc aynan TS2345 bergan (03-kalit-turi.txt; u yerda ro'yxat ... 23 more ... bilan qisqargan — kalitlar 26 ta).

5.2 O'rinlar ham tekshiriladi — lekin yarmi

{{ism}} kabi o'rinlar ham turga kiradi. Biz uch holatni sinab ko'rdik (shu blok, tsc 7.0.2):

Chaqiruv Natija
t("salom", { ism: "Ali" }) xato yo'q
t("salom", { ismi: "Ali" }) TS2345 — noma'lum o'rin nomi
t("salom") xato yo'q, ekranda Salom, {{ism}}!

Ortiqcha yoki xato nomli o'rin ushlanadi. Umuman berilmagan o'rin esa ushlanmaydi. Demak tur — birinchi himoya, oxirgisi emas.

5.3 Namespace prefiksi va tur

Kanonda bitta qator turlar yoqilgach o'zgardi. Sozlamalar sahifasi useTranslation("sozlamalar") bilan ishlaydi, sarlavha esa umumiy faylidan:

diff
-      <title>{t("umumiy:sahifaNomi", { sahifa: t("sarlavha") })}</title>
+      <title>
+        {t("sahifaNomi", { ns: "umumiy", sahifa: t("sarlavha") })}
+      </title>

Sababi — biz tekshirdik: useTranslation("sozlamalar") qaytargan t turida faqat sozlamalar kalitlari bor. "umumiy:sahifaNomi" satri u yerda yo'q — TS2345. { ns: "umumiy" } esa turga mos va ma'nosi bir xil.

5.4 i18next-cli: kalitlarni koddan yig'ish

Tur faylda yo'q kalitni ushlaydi. Ruscha faylda yetishmayotgan kalitni-chi? Buning uchun i18next jamoasining CLI vositasi bor. Eski maqolalardagi i18next-parser npm'da eskirgan deb belgilangan:

text
deprecated = 'Project is deprecated, use i18next-cli instead'

Biz i18next-cli 1.75.2 ni alohida kichik loyihada sinadik. Unda Menyu.tsx to'rtta kalit ishlatadi, bittasi (bolimlar.arxiv) faylda yo'q. Ruscha faylda faqat ikki kalit:

bash
npx i18next-cli status
text
i18next Project Status
------------------------
🔑 Keys Found:         4
📚 Namespaces Found:   1
🌍 Locales:            uz, ru
✅ Primary Language:   uz

Translation Progress:
- ru: [■■■■■■■■■■□□□□□□□□□□] 50% (2/4 keys)  — 2 absent

⚠ Primary language "uz" is missing 1 key(s) that are used in code.
  Run "i18next-cli status uz" for details, or "i18next-cli extract" to add them.
✖ Error: Incomplete translations detected.

Tarjimasi: "ruscha — 50%, asosiy tilda 1 ta kalit yo'q". extract kalitlarni fayllarga yozadi. Lekin bir tuzoq bor: u kodda topilmagan kalitni o'chirib yubordi — bizda bolimlar.sozlamalar yo'qoldi. Kalit dinamik yasalsa (t(`bolimlar.${nom}`)), vosita uni ko'rmaydi va tarjimani o'chiradi. extract dan keyin git diff — majburiy. types buyrug'i esa CustomTypeOptions ni fayllardan o'zi yasaydi.

Kanon CLI'ni olmadi — to'rt til uchun test yetarli. tarjima.test.js har tilda o'zbekchadagi hamma kalit va {{o'rinlar}} borligini tekshiradi. Ko'plik kalitlari tilning Intl.PluralRules toifalari bo'yicha: o'zbekchada one, other, ruschada yana few, many.

Tekshirib ko'ring: Sardor kalitni t(sahifa.kalit) deb o'zgaruvchidan beradi (sahifa.kalit — string). Type-safe kalitlar uni ushlaydimi?

Javob

Ushlaydi — biz tekshirdik: string turidagi kalit TS2345 beradi, chunki string ichida noto'g'ri kalitlar ham bor. TS sizni turni toraytirishga majbur qiladi (kalit: "bolimlar.royxat" | "bolimlar.statistika"). Bitta yo'lak ochiq: t(kalit, { defaultValue: "…" }) — zaxira matn bilan string ham o'tadi. as any ham turni o'chiradi. Bunday joylarni CLI ham topa olmaydi — kalit kodda satr bo'lib yozilmagan.

6. Sana, son va foiz: Intl va Chrome tuzog'i

6.1 Formatlash tarjima ichida

Sana va son formatlashni Intl darslaridan bilasiz. i18next uni tarjima ichiga olib kiradi: {{ulush, number(style: percent)}} — o'rin qiymati Intl.NumberFormat dan o'tadi. Har til o'z qoidasini oladi: o'zbekcha 33%, ruscha 33 % (orasida uzilmas bo'sh joy).

Sana uchun esa kanon o'z formatlovchisini qo'shdi — {{vaqt, sana}}. Sababi — tuzoq.

6.2 Tuzoq: Chrome o'zbek lotinini bilmaydi

Bir xil kodni Node 24 va Chrome 155 da ishga tushirdik. Sana — 2026-yil 17-oktabr, 19:04:

Kod Node 24 Chrome 155
DateTimeFormat("uz", long) 17-oktabr, 2026, 19:04 2026 M10 17 19:04
DateTimeFormat("uz", {month: "long"}) Oktabr M10
DateTimeFormat("uz-Cyrl", long) 17 октябр, 2026 19:04 17 октябр, 2026 19:04
NumberFormat("uz").format(35000) 35 000 35,000
Kod Node 24 Chrome 155
RelativeTimeFormat("uz").format(-2, "day") 2 kun oldin -2 d
RelativeTimeFormat("uz", auto).format(-1, "day") kecha yesterday
ListFormat("uz").format([…]) osh, manti va lag'mon osh, manti and lag'mon
DisplayNames(["uz"], language).of("ru") ruscha ru

Manba: o'zimiz o'lchadik — Node 24.21.0 (ICU 78.3) va Chrome 155.0.8059.82 (puppeteer, headless), 2026-10-17.

Jadvalga qarang. Chrome'da uz (lotin) uchun ma'lumot umuman yo'q: oy nomi o'rniga M10, raqamlar inglizcha vergul bilan, so'zlar inglizcha. Kirill (uz-Cyrl) esa to'g'ri. Node'da hammasi to'g'ri — shuning uchun Node'dagi testlar xatoni ko'rmaydi. Kanon uni brauzer tekshiruvida topdi.

O'zingiz ko'ring. Bu blok faqat saytda ishlaydi (tekshiruvchimiz Node'da — u boshqa javob beradi). «Ishga tushir» ni Chrome'da bosing:

jsx
const sana = new Date(2026, 9, 17, 19, 4);
const uzun = { dateStyle: "long", timeStyle: "short" };
console.log(new Intl.DateTimeFormat("uz", uzun).format(sana));
console.log(new Intl.DateTimeFormat("uz-Cyrl", uzun).format(sana));
console.log(new Intl.NumberFormat("uz").format(35000));
console.log(new Intl.RelativeTimeFormat("uz").format(-2, "day"));

Chrome 155 da shunday chiqdi (o'lchandi):

text
2026 M10 17 19:04
17 октябр, 2026 19:04
35,000
-2 d

Firefox va Safari'ni o'lchamadik. Xulosa: o'zbek lotin uchun brauzer Intl'iga ishonmang, tekshiring.

6.3 Yechim: o'zbekcha uchun o'z formatlovchi

Kanondagi sana.ts — bitta funksiya sanaYoz(sana, til): "uz" bo'lsa CLDR'dagi shakl (d-MMMM, y HH:mm) o'zimiz yig'iladi, qolgan tillar — Intl. Izohida Chrome o'lchovi ham yozilgan. Quyidagi blokda o'sha funksiyaning JS nusxasi (TypeScript turlari olib tashlangan).

Formatlovchi i18next'ga bir marta ulanadi va tarjima matnida nomi bilan chaqiriladi:

jsx
import i18next from "i18next";

const OYLAR = ["yanvar", "fevral", "mart", "aprel", "may", "iyun",
  "iyul", "avgust", "sentabr", "oktabr", "noyabr", "dekabr"];
const ikki = (son) => String(son).padStart(2, "0");

function sanaYoz(sana, til) {
  if (til === "uz") {
    const oy = OYLAR[sana.getMonth()];
    const soat = ikki(sana.getHours());
    const vaqt = `${soat}:${ikki(sana.getMinutes())}`;
    return `${sana.getDate()}-${oy}, ${sana.getFullYear()} ${vaqt}`;
  }
  return new Intl.DateTimeFormat(til, {
    dateStyle: "long",
    timeStyle: "short",
  }).format(sana);
}

const FOIZ = "{{n}} ({{ulush, number(style: percent)}})";
const i18n = i18next.createInstance();
void i18n.init({
  lng: "uz",
  resources: {
    uz: {
      translation: { vaqt: "Holat: {{vaqt, sana}}", foiz: FOIZ },
    },
    "uz-Cyrl": {
      translation: { vaqt: "Ҳолат: {{vaqt, sana}}", foiz: FOIZ },
    },
    ru: {
      translation: {
        vaqt: "Состояние на {{vaqt, sana}}",
        foiz: FOIZ,
      },
    },
    en: { translation: { vaqt: "As of {{vaqt, sana}}", foiz: FOIZ } },
  },
});
// {{…, sana}} — bizning formatlovchi; til — ikkinchi argument
i18n.services.formatter.add("sana", (qiymat, til) =>
  sanaYoz(qiymat, til),
);

const vaqt = new Date(2026, 9, 17, 19, 4);
for (const lng of ["uz", "uz-Cyrl", "ru", "en"]) {
  console.log(i18n.t("vaqt", { lng, vaqt }));
  console.log(i18n.t("foiz", { lng, n: 1, ulush: 1 / 3 }));
}

Konsolda:

text
Holat: 17-oktabr, 2026 19:04
1 (33%)
Ҳолат: 17 октябр, 2026 19:04
1 (33%)
Состояние на 17 октября 2026 г. в 19:04
1 (33 %)
As of October 17, 2026 at 7:04 PM
1 (33%)

Bu chiqish Chrome va Node'da bir xil — o'zbek lotin sanasini Intl'ga bermadik. Kanon brauzer tekshiruvi (holat 03) ham xuddi shuni ko'rdi:

text
   /statistika: «Holat: 17-oktabr, 2026 19:09», «1 (33%)»
   /en/statistika: «As of October 17, 2026 at 7:09 PM», «1 (33%)»
   /uz-cyrl/statistika: «Ҳолат: 17 октябр, 2026 19:09», «1 (33%)»

Tekshirib ko'ring: Nega kanon sanaYoz da faqat "uz" ni qo'lda formatlaydi, "uz-Cyrl" ni emas?

Javob

O'lchov shuni ko'rsatdi: Chrome'da uz-Cyrl uchun ma'lumot bor va natija to'g'ri («17 октябр, 2026 19:04»). Qo'lda yozilgan kod — qo'shimcha xato manbai, uni faqat haqiqatan kerak joyda yozamiz. Agar ertaga Chrome o'zbek lotin ma'lumotini qo'shsa, "uz" shoxchasini ham olib tashlash mumkin — buni testdagi o'lchov aytadi, taxmin emas.

7. Tarjimani boshqarish, muqobillar va RTL

7.1 Tarjimonlar uchun xizmatlar (TMS)

Tarjimonlar dasturchi bo'lmasa, JSON faylni git'da tahrirlash ular uchun noqulay. Buning uchun tarjimani boshqarish tizimi (TMS — translation management system) bor: tarjimon kalitlarni veb-sahifadagi jadvalda tarjima qiladi, dasturchi fayllarni tortib oladi.

Xizmat Qisqacha
Tolgee ochiq kodli, o'z serveringizga o'rnatsa bo'ladi; sahifada turib tarjima qilish
Crowdin katta jamoalar va ochiq loyihalar; git bilan sinxronlash
Locize i18next bilan to'g'ridan ulanadi (i18next-locize-backend, i18next-cli locize-sync)

Hammasi hisob ochishni talab qiladi — biz ularni sinamadik. To'rt til va bitta dasturchi uchun JSON + git + test yetarli; TMS — tarjimon jamoasi paydo bo'lganda.

7.2 Muqobil kutubxonalar: Lingui va FormatJS

i18next yagona yo'l emas. Ikki mashhur muqobil ICU MessageFormat sintaksisidan foydalanadi — ko'plik tarjima matnining ichida yoziladi:

text
{n, plural, one {# задача} few {# задачи} many {# задач} other {# задачи}}
  • FormatJS (react-intl) — <FormattedMessage id="qoldi" values={{ n: 5 }} />, Intl ustida.
  • Lingui — xabarlar build paytida kompilyatsiya qilinadi (makro), ish vaqtidagi kodi kichik.

Ularning narxi (Lingui +2.3 KB, react-intl +14.0 KB, i18next +15.4 KB) va kanon nega i18next'ni tanlagani — oldingi darsning «Natija va narx» bo'limida. Qisqasi: Lingui — hajm muhim bo'lsa va build qadamiga rozi bo'lsangiz, o'z t() — ikki til va o'nta satr bo'lsa.

7.3 RTL — o'ngdan chapga yozuvlar (ko'rsatkich)

Arab, fors va ibroniy tillari o'ngdan chapga yoziladi — RTL (right-to-left). vazifalar da bunday til yo'q, lekin bilib qo'ying: sahifa butunlay "ko'zgu" bo'lib aylanadi. i18next tilning yo'nalishini biladi:

jsx
import i18next from "i18next";

console.log(i18next.dir("ar"), i18next.dir("uz"), i18next.dir("ru"));

Konsolda:

text
rtl ltr ltr

Uni <html dir="rtl"> ga qo'yasiz (Ko'p tilli matn, yo'nalish). CSS'da margin-left o'rniga mantiqiy xususiyatlar: margin-inline-start, inline-size. Ular yo'nalishga qarab o'zi aylanadi — kanon CSS'i allaqachon shunday yozilgan (max-inline-size). "Orqaga" strelkasi kabi ikonkalar ham aylantiriladi.

8. Vazifalar qadami: til manzilda (holat 03)

8.1 O'zgarishlar

bash
git switch -c feat/i18n-url

Holat 02 → 03 (kanon):

  • i18n/yol.ts (yangi) — yoldanTil, tildaYol, TILLAR (to'rt til). Yuqorida to'liq ko'rdik.
  • i18n/index.ts — til manzildan (tilYoli), sana formatlovchisi, tilniAlmashtir — location.assign; vazifalar:til kaliti o'chdi.
  • i18n/turlar.d.ts (yangi) — kalitlar turi.
  • i18n/sana.ts (yangi), en/ va uz-Cyrl/ tarjimalari, skriptlar/kirill.js + package.json da "kirill" skripti.
  • main.tsx — basename: tilYoli.asos.
  • TilTanlov.tsx — «Til manzilda saqlanadi…» izohi (aria-describedby).
  • StatistikaSahifasi.tsx — foiz Intl orqali (ulush), sahifa ochilgan vaqt (Holat: …).
  • Testlar: tarjima.test.js va til-yoli.test.js (yangi).

init dagi yangi sozlama — load: "currentOnly". Usiz i18next uz-Cyrl tanlanganda uz ni ham yuklaydi: uz-Cyrl ning "ota" tili uz, i18next uni zaxira deb hisoblaydi. Bizda u build'da bor — ortiqcha so'rov kerak emas.

8.2 Natija va narx

Tekshiruvlar (haqiqiy, holat 03):

  • npm test — node:test tests 221 · pass 221 (203 edi, +18), Vitest Tests 65 passed (65).
  • lint, format:check, tip, knip — exit 0. axe — 0 buzilish.
  • Birinchi yuklash JS: 219.0 → 219.6 KB gzip (+0.6). Har til fayli alohida chunk, 0.3–0.7 KB.
  • Brauzer: 159, 72, 100 va 77 tekshiruv o'tdi. 21-qismning e2e-21 tekshiruvi — 18 ta, hammasi ; ulardan bir qismi:
text
✅ ruscha: manzil /ru/sozlamalar, lang, sarlavha, title, tanlangan: ["/ru/sozlamalar","ru","Настройки","Настройки — Задачи",true]
✅ ruscha statistika: foiz (Intl, ru) va sana: ["1 (33 %)",true]
✅ localStorage'da til yo'q (manba — manzil): null
✅ /uz-cyrl/: lang, hisob: ["uz-Cyrl","2 та вазифа қолди, 1 та бажарилди"]
✅ oflayn: /ru/statistika SW keshidan, ruscha: ["Статистика","ru"]

Oxirgi qatorga qarang: oflayn holatda ham /ru/statistika ochildi. Service Worker (Service Worker hayot sikli) har til manzilida bir xil index.html ni beradi, til esa manzildan o'qiladi.

bash
git add -A
git commit

Commit (kanon): feat: til manzilda (/ru, /uz-cyrl, /en), kirill skript bilan, kalit turlari, Intl sana/foiz.

9. Ko'p uchraydigan xatolar

9.1 npm run kirill jim hech narsa qilmaydi

Sardorning kompyuterida Node 22. U npm run kirill ni ishga tushirdi — xato yo'q, chiqish yo'q, fayl ham yo'q. Biz takrorladik (Node 22.17.0): exit 0, uz-Cyrl/ papkasi yaratilmadi.

Sabab — skript oxiridagi if (import.meta.main) ("fayl to'g'ridan-to'g'ri ishga tushirilganmi"). Node 24 da u bor, Node 22 da — undefined, shart bajarilmaydi va skript jim tugaydi. package.json dagi "engines": { "node": ">=24" } ni npm sukut bo'yicha faqat ogohlantirish deb biladi. Davosi — loyihaning Node versiyasi (.nvmrc).

9.2 i18next instance ulanmagan

initReactI18next ni unutsangiz, komponent tarjima o'rniga kalitni chiqaradi va konsolda ogohlantirish (haqiqiy matn):

text
react-i18next:: useTranslation: You will need to pass in an i18next instance by using initReactI18next or by passing it via props or context. In monorepo setups, make sure there is only one instance of react-i18next. {code: 'NO_I18NEXT_INSTANCE'}

Tarjimasi: "i18next nusxasini initReactI18next yoki props/context orqali bering". Davosi — .use(initReactI18next).

9.3 Boshqalari

Belgi Sabab Davosi
Chrome'da «2026 M10 17», «35,000», «-2 d» brauzerda uz lotin Intl ma'lumoti yo'q o'zbekcha uchun o'z formatlovchi, brauzerda tekshirish
t("umumiy:kalit") — TS2345 turlangan t boshqa namespace prefiksini bilmaydi t("kalit", { ns: "umumiy" })
tilmi("uz-cyrl") — false manzil bo'lagi ≠ til kodi PREFIKSLAR jadvali orqali
uz-Cyrl da uz ham yuklanadi i18next ota tilni zaxira deb oladi load: "currentOnly"
extract dan keyin tarjima yo'qoldi dinamik kalitni vosita ko'rmaydi dinamik kalitdan qoching; git diff

10. Mashqlar

Mashqlar kurs/mashqlar/21/03-i18n-url/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): belgi va manzil

To'ldiring:

  • O'zbek tili, kirill yozuvi —
  • new Intl.Locale("uz").maximize().toString() —
  • Kanonda tildaYol("/statistika", "?x=1", "ru") —
  • Kanonda tildaYol("/en/vazifa/3", "", "uz") —
  • kirillga("Yo'q") —
Yechim
  • uz-Cyrl — til + 4 harfli yozuv, yozuvning birinchi harfi katta.
  • uz-Latn-UZ — yozuvsiz uz = lotin, maximize to'liq shaklini aytadi.
  • /ru/statistika?x=1 — prefiks qo'shildi, so'rov saqlandi.
  • /vazifa/3 — asosiy til prefikssiz.
  • Йўқ — yo' — ё emas, й + ў; katta harf saqlanadi.

2-mashq (o'rta): til tanlovi havolalari

Komponent joriy manzildan uchta til havolasini chizsin. Har havola — xuddi shu sahifa boshqa tilda; havolada lang (o'sha tilda) va hrefLang; joriy til havolasida aria-current="page". Ostida havolalar manzili chiqsin: uz → /statistika kabi.

Ishora: «Router bilan: basename» bo'limidagi PREFIKS va yolniOqi ni ko'chiring, til nomlari: { uz: "O'zbekcha", "uz-Cyrl": "Ўзбекча", ru: "Русский" }. Komponent: <TilHavolalari manzil="/ru/statistika" />. aria-current — React'da accessibility dagi "hozirgi sahifa" belgisi.

Yechim
jsx
const PREFIKS = { uz: "", "uz-Cyrl": "/uz-cyrl", ru: "/ru" };
const NOMLAR = {
  uz: "O'zbekcha",
  "uz-Cyrl": "Ўзбекча",
  ru: "Русский",
};

function yolniOqi(yol) {
  const [, birinchi, ...qolgan] = yol.split("/");
  const til = Object.keys(PREFIKS).find(
    (t) => t !== "uz" && PREFIKS[t] === `/${birinchi}`,
  );
  return til
    ? { til, qolgani: `/${qolgan.join("/")}` }
    : { til: "uz", qolgani: yol };
}

function TilHavolalari({ manzil }) {
  const { til: joriy, qolgani } = yolniOqi(manzil);
  const tillar = Object.keys(PREFIKS);
  return (
    <>
      <nav aria-label="Til">
        {tillar.map((til) => (
          <a
            key={til}
            href={`${PREFIKS[til]}${qolgani}`}
            lang={til}
            hrefLang={til}
            aria-current={til === joriy ? "page" : undefined}
          >
            {NOMLAR[til]}{" "}
          </a>
        ))}
      </nav>
      <ul>
        {tillar.map((til) => (
          <li key={til}>
            {til} → {PREFIKS[til]}{qolgani}
          </li>
        ))}
      </ul>
    </>
  );
}

export default function App() {
  return <TilHavolalari manzil="/ru/statistika" />;
}

Ekranda:

text
O'zbekcha Ўзбекча Русский
uz → /statistika
uz-Cyrl → /uz-cyrl/statistika
ru → /ru/statistika

lang — havola matni qaysi tilda (ekran o'quvchi talaffuzi uchun). hrefLang — havola ochadigan sahifa qaysi tilda. Bu yerda ikkalasi bir xil, chunki har til nomi o'z tilida yozilgan. aria-current="page" faqat «Русский» da — Malika qaysi tilda turganini eshitadi.

3-mashq (qiyin): "2 kun oldin" — o'zbekcha nisbiy vaqt

Jadvalda ko'rdik: Chrome'da RelativeTimeFormat("uz") «-2 d» va «yesterday» chiqaradi. nisbiyVaqt(kunlar, til) funksiyasini yozing:

  1. uz uchun o'zingiz: 0 — «bugun», -1 — «kecha», 1 — «ertaga», -3 — «3 kun oldin», 2 — «2 kundan keyin».
  2. Boshqa tillar uchun — new Intl.RelativeTimeFormat(til, { numeric: "auto" }).format(kunlar, "day").
  3. Uni i18next'ga nisbiy formatlovchisi qilib ulang va {{kun, nisbiy}} bilan uch tilda chiqaring: uz, ru, en, qiymatlar -1 va -3.

Ishora: formatlovchi ulash — «Yechim: o'zbekcha uchun o'z formatlovchi» bo'limidagi i18n.services.formatter.add. Manfiy sondan musbat — Math.abs.

Yechim
jsx
import i18next from "i18next";

function nisbiyVaqt(kunlar, til) {
  if (til === "uz") {
    if (kunlar === 0) return "bugun";
    if (kunlar === -1) return "kecha";
    if (kunlar === 1) return "ertaga";
    const n = Math.abs(kunlar);
    return kunlar < 0 ? `${n} kun oldin` : `${n} kundan keyin`;
  }
  return new Intl.RelativeTimeFormat(til, { numeric: "auto" }).format(
    kunlar,
    "day",
  );
}

const i18n = i18next.createInstance();
void i18n.init({
  lng: "uz",
  resources: {
    uz: { translation: { ozgardi: "O'zgardi: {{kun, nisbiy}}" } },
    ru: { translation: { ozgardi: "Изменено: {{kun, nisbiy}}" } },
    en: { translation: { ozgardi: "Changed: {{kun, nisbiy}}" } },
  },
});
i18n.services.formatter.add("nisbiy", (qiymat, til) =>
  nisbiyVaqt(qiymat, til),
);

for (const lng of ["uz", "ru", "en"]) {
  console.log(i18n.t("ozgardi", { lng, kun: -1 }));
  console.log(i18n.t("ozgardi", { lng, kun: -3 }));
}

Konsolda:

text
O'zgardi: kecha
O'zgardi: 3 kun oldin
Изменено: вчера
Изменено: 3 дня назад
Changed: yesterday
Changed: 3 days ago

Ruscha «3 дня назад» — ko'plik shaklini Intl o'zi tanladi (few). O'zbekchada ko'plik bitta shaklda (3 kun), shuning uchun qo'lda yozish oson bo'ldi. numeric: "auto" — "1 kun oldin" o'rniga «вчера», «yesterday».

4-mashq: Vazifalar qadami

feat/i18n-url branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni qiling: yol.ts, sana.ts, turlar.d.ts, en/ tarjimalari, skriptlar/kirill.js va npm run kirill, main.tsx dagi basename, ikki yangi test. Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm test — node 221, Vitest 65. «uz-Cyrl fayllari … yangi yasalgan» yiqilsa — npm run kirill (Node 24 bilan!).
  • npm run tip — 0 xato. Biror sahifada t("…") qizarsa — kalitni uz/*.json dan tekshiring yoki namespace prefiksini { ns } ga o'tkazing.
  • Brauzerda: /ru/statistika — ruscha va <html lang="ru">; til tanlovi — manzil o'zgaradi va sahifa qayta ochiladi; «Orqaga» — oldingi til.
  • DevTools → Application → Local Storage: vazifalar:til kaliti yo'q.
  • npm run build — birinchi yuklash ~219.6 KB gzip.

11. Real ishda

  • O'zbekiston saytlari — ko'pincha uch til: o'zbek lotin, kirill va rus. Kirillni transliteratsiya bilan yasash qulay, ruscha o'zlashmalar — qo'lda.
  • Manzildagi til — ko'p xalqaro saytlarda shunday: /ru/, /en/. Next.js'da bu marshrut segmenti ([lang]) — i18n Next.js'da.
  • Type-safe kalitlar va i18next-cli — katta jamoalarda CI'da status yoki test bilan "tarjima yetishmaydi" xatosi ushlanadi.
  • Intervyu savollari: "tilni qayerda saqlaysiz va nega?", "uz va uz-Cyrl farqi?", "lang va hreflang farqi?", "Intl natijasiga brauzerda ishonsa bo'ladimi?".

Xulosa

  • Til belgisi BCP 47: til-Yozuv-HUDUD. uz = o'zbek lotin, uz-Cyrl = kirill. <html lang> — ekran o'quvchi, tarjimon va CSS uchun.
  • Kirill fayli lotinchadan skript bilan yasaladi (npm run kirill, Node 24), test uning eskirmaganini tekshiradi. Ruscha o'zlashmalar — ko'z bilan.
  • Til manzilda: havola tilini o'zi olib yuradi. Kanon — basename + qayta yuklash; muqobil — tilni har sahifada manzildan hisoblash.
  • CustomTypeOptions — xato kalit va xato o'rin nomi tsc xatosiga aylanadi; berilmagan o'rinni esa tur ushlamaydi.
  • Chrome'da Intl o'zbek lotinni bilmaydi («2026 M10 17», «35,000», «-2 d») — o'zbekcha uchun o'z formatlovchi, qolgan tillar — Intl.
  • i18next-parser eskirgan — i18next-cli (status, extract, types); extract dinamik kalitlarni o'chirishi mumkin.

Keyingi dars: Grafiklar I: Recharts — statistika sahifasiga teglar grafigi: Recharts komponentlari, Tooltip va formatlash, va nega kanon oxirida o'z SVG grafigini tanladi (+103.4 KB va 12 ta tsc xatosi).

Manbalar

  • i18next: TypeScript — i18next.com/overview/typescript; Formatting — i18next.com/translation-function/formatting; load, supportedLngs — i18next.com/overview/configuration-options (i18next 26.4.3, react-i18next 17.0.16)
  • i18next-cli — github.com/i18next/i18next-cli (1.75.2 sinaldi, 2026-10-17)
  • BCP 47 va Intl.Locale — MDN: Intl.Locale, Intl.getCanonicalLocales
  • MDN: lang va hreflang atributlari
  • Kanon: vazifalar holat 03, natijalar/tekshiruv-03.txt, 03-kalit-turi.txt, brauzer-tekshiruv/natija-21-03.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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