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

i18n React'da: react-i18next bilan ko'p tilli ilova — t(), namespace, ko'plik va Trans

Qisqacha: i18n — interfeys matnlarini koddan tarjima fayllariga chiqarish. Komponent matnni emas, kalitni aytadi: t("hisob.qoldi", { count: 2 }). Kutubxona (i18next) tanlangan tilning faylidan matnni topadi va {{count}} o'rniga sonni qo'yadi. Ko'plik shaklini u Intl.PluralRules bilan tanlaydi: 2 ta vazifa qoldi, Осталось 2 задачи, Осталось 5 задач. React tomoni (react-i18next) esa useTranslation hook'ini beradi — til almashsa, komponent qayta chiziladi. vazifalar da narxi — birinchi yuklashga +17.1 KB gzip.

Bu darsda

  • i18next va react-i18next ni ulaysiz: init, useTranslation, t(), changeLanguage.
  • Interpolatsiya ({{ism}}) va nega React'da escapeValue: false xavfsiz ekanini tushuntirasiz.
  • Ko'plik shakllarini (_one, _few, _many, _other, _zero) o'zbek, rus va ingliz tillari uchun yozasiz.
  • Matnlarni namespace'larga bo'lasiz, havola yoki qalin so'z bor gapni Trans bilan tarjima qilasiz.
  • Tilni qanday aniqlash, qayerda saqlash va boshqa til faylini kech yuklashni bilasiz; vazifalar ni ikki tilli qilasiz.

Oldin bilishingiz kerak: Intl.PluralRules, ListFormat, DisplayNames, Intl.NumberFormat, Context I, Frontend xavfsizligi React'da, lazy va code splitting.

1. Nega bu kerak?

O'tgan darsda «Bahor»ga bron kalendari qo'shdik. Endi Jasur aka yangi iltimos bilan keldi: mehmonlarning yarmi ruscha so'raydi, turistlar inglizcha. Menyu va bron sahifasi uch tilda bo'lsin.

Birinchi o'ylaydigan yechim — har joyda shart:

jsx
<h1>{til === "ru" ? "Меню" : til === "en" ? "Menu" : "Menyu"}</h1>

Ilovada esa yuzlab matn bor, har birida shu ? : zanjiri. To'rtinchi til qo'shilsa — hamma joyni qayta ochish kerak. Tarjimon esa kodga tegolmaydi.

Ikkinchi muammo — ko'plik. vazifalar dagi hisob shunday yozilgan:

jsx
{`${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`}

O'zbekchada son qanday bo'lmasin, "ta vazifa" o'zgarmaydi. Ruschada esa uch shakl: 1 задача, 2 задачи, 5 задач. 21 — yana задача. Bu qoidani Intl.PluralRules darsida ko'rgan edik. Har komponentda uni qo'lda yozish — xato manbai.

Yechim — i18n (internationalization — xalqarolashtirish; "i", keyin 18 harf, keyin "n"). Ilova matnlarni koddan tashqariga chiqaradi. Kod faqat kalitni aytadi, matn esa har til uchun alohida faylda.

Hayotiy o'xshatish: restoran menyusi. Oshpaz "7-taom" deydi. Ofitsiant mehmonning tilidagi varaqdan o'qiydi: "Osh", Плов, "Pilaf". Oshxona bir xil ishlaydi, varaqlar alohida. Kalit — taom raqami, tarjima fayli — o'sha tildagi varaq.

Yonida yana bir atama uchraydi: l10n (localization — mahalliylashtirish). i18n — ilovani ko'p tilga tayyorlash (kalitlar, fayllar, ko'plik). l10n — aniq bir tilga moslash: tarjima, sana va pul formati.

2. Birinchi tarjima

2.1 Ikki paket

React'da eng ko'p ishlatiladigan i18n yo'li — ikki paket:

Paket Versiya (kanon) Nima qiladi
i18next 26.4.3 yadro: tillar, fayllar, kalit qidirish, ko'plik, interpolatsiya. React'siz ham ishlaydi (Node, Vue)
react-i18next 17.0.16 React ulagichi: useTranslation hook'i, Trans komponenti, til almashganda qayta chizish
bash
npm i -E i18next@26.4.3 react-i18next@17.0.16

Natija oynasida ikkalasi bor — quyidagi misollar jonli.

2.2 init, useTranslation, t()

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

i18next.use(initReactI18next).init({
  lng: "uz", // boshlang'ich til
  fallbackLng: "uz", // kalit topilmasa — shu tildan
  resources: {
    uz: {
      translation: {
        menyu: { sarlavha: "Menyu", savat: "Savatcha" },
        til: "Til",
      },
    },
    ru: {
      translation: {
        menyu: { sarlavha: "Меню", savat: "Корзина" },
      },
    },
  },
  interpolation: { escapeValue: false }, // nega — keyingi bo'limda
});

function App() {
  const { t, i18n } = useTranslation();
  return (
    <section>
      <h2>{t("menyu.sarlavha")}</h2>
      <p>
        {t("menyu.savat")} · {t("til")}
      </p>
      <button onClick={() => i18n.changeLanguage("uz")}>
        O'zbekcha
      </button>{" "}
      <button onClick={() => i18n.changeLanguage("ru")}>
        Русский
      </button>
    </section>
  );
}

Ekranda:

text
Меню
Корзина · Til
O'zbekcha Русский

Qatorma-qator:

  • i18next.use(initReactI18next) — yadroga React ulagichini ulaydi. Shundan keyin useTranslation shu nusxani topadi. Provider yozish shart emas, lekin React'dagi Context g'oyasi ichida o'sha: komponentlar umumiy nusxaga obuna bo'ladi.
  • resources — tarjimalar: til → namespace → kalitlar. translation — standart namespace nomi (namespace'lar haqida — alohida bo'limda).
  • t("menyu.sarlavha") — nuqta ichma-ich obyektga kiradi. Kalitlar guruhlanadi: menyu.*, xato.*.
  • i18n.changeLanguage("ru") — tilni almashtiradi. useTranslation ishlatgan har komponent qayta chiziladi.

Ikkinchi qatorga qarang: Корзина · Til. Ruscha faylda til kaliti yo'q. i18next fallbackLng dagi o'zbekchadan oldi. Tarjima to'liq bo'lmasa ham ilova buzilmaydi — faqat bitta so'z o'zbekcha qoladi.

2.3 Kalit hech qayerda yo'q bo'lsa

jsx
import i18next from "i18next";

i18next.init({
  lng: "uz",
  resources: { uz: { translation: { salom: "Salom!" } } },
});

console.log(i18next.t("salom"));
console.log(i18next.t("menyu.sarlavha"));
console.log(i18next.options.fallbackLng);

Konsolda:

text
Salom!
menyu.sarlavha
(1) ['dev']

Topilmagan kalit — kalitning o'zi qaytadi. Xato tashlanmaydi, konsolda ham jim. Ekranda "menyu.sarlavha" ko'rinsa — tarjima fayliga qarang.

Uchinchi qator — tuzoq. fallbackLng berilmasa, sukut bo'yicha ["dev"] — mavjud bo'lmagan "dev" tili. Tillarni serverdan yuklasangiz, i18next dev faylini ham so'raydi. Shuning uchun fallbackLng ni doim o'z asosiy tilingiz bilan yozing.

useTranslation siz ham ishlaydi: i18next.t() — oddiy funksiya. React tashqarisida (masalan Zod xabarlarida yoki document.title da) shundan foydalaniladi. Lekin komponent ichida — faqat hook: i18next.t til almashganini React'ga aytmaydi.

Tekshirib ko'ring: Ruscha faylda menyu.savat yo'q, o'zbekchada bor. fallbackLng: "uz". Til — ru. t("menyu.savat") nima qaytaradi? fallbackLng umuman yozilmagan bo'lsa-chi?

Javob

Birinchi holatda — «Savatcha»: ruschada topilmadi, o'zbekchadan olindi. Ikkinchisida — «menyu.savat»: zaxira til "dev", unda hech narsa yo'q, kalitning o'zi qaytdi. Shuning uchun fallbackLng — har loyihada birinchi yoziladigan sozlama.

3. Interpolatsiya: matn ichidagi qiymat

3.1 {{ism}}

Ko'p matnda o'zgaruvchi qism bor: "Salom, Ali!", "Osh — 35 000 so'm". Tarjimada joyi ham o'zgaradi: o'zbekchada "Ali, xush kelibsiz", inglizchada "Welcome, Ali". Shuning uchun qiymat satrga qo'shib yozilmaydi — tarjimaning o'zida joy qoldiriladi. Bu — interpolatsiya (interpolation): {{ism}} o'rniga t() ga berilgan qiymat qo'yiladi.

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

i18next.use(initReactI18next).init({
  lng: "uz",
  fallbackLng: "uz",
  resources: {
    uz: {
      translation: {
        xush: "{{ism}}, xush kelibsiz!",
        narx: "{{taom}} — {{narx}} so'm",
      },
    },
  },
  interpolation: { escapeValue: false },
});

function App() {
  const { t } = useTranslation();
  return (
    <>
      <p>{t("xush", { ism: "Malika" })}</p>
      <p>{t("narx", { taom: "Osh", narx: "35 000" })}</p>
    </>
  );
}

Ekranda:

text
Malika, xush kelibsiz!
Osh — 35 000 so'm

Narxni bu yerda tayyor satr qilib berdik. Sonni tilga qarab formatlash (35 000 / 35,000) — Intl.NumberFormat ishi. i18next uni tarjima ichida ham chaqira oladi — buni i18n II darsida ko'ramiz.

3.2 escapeValue nega false

i18next sukut bo'yicha qiymatni ekranlaydi: < → &lt;. Bu XSS dan himoya — natija innerHTML ga yozilsa, <script> ishlamasin. Lekin React matnni o'zi ekranlaydi (Frontend xavfsizligi). Ikki marta ekranlash nima beradi? Ikkita alohida nusxa bilan sinaymiz:

jsx
import i18next from "i18next";

const resources = {
  uz: { translation: { xush: "{{ism}}, xush kelibsiz!" } },
};
// sukut: escapeValue true
const standard = i18next.createInstance();
standard.init({ lng: "uz", resources });
// React uchun: escapeValue false
const forReact = i18next.createInstance();
forReact.init({
  lng: "uz",
  resources,
  interpolation: { escapeValue: false },
});

const ism = "<b>Ali</b>";
console.log(standard.t("xush", { ism }));
console.log(forReact.t("xush", { ism }));

export default function App() {
  return <p>{standard.t("xush", { ism })}</p>;
}

Konsolda:

text
&lt;b&gt;Ali&lt;&#x2F;b&gt;, xush kelibsiz!
<b>Ali</b>, xush kelibsiz!

Ekranda:

text
&lt;b&gt;Ali&lt;&#x2F;b&gt;, xush kelibsiz!

Ekranga qarang: foydalanuvchi &lt;b&gt; ni ko'rdi. i18next ekranladi, React esa natijani yana matn qilib chiqardi — ekranlash belgilari ekranga tushdi. escapeValue: false bilan React ekranga <b>Ali</b> ni matn qilib chiqaradi: teg emas, qalin shrift emas. Himoya o'z joyida — React'da.

createInstance() — alohida i18next nusxasi. Kanon ham shunday qiladi: global i18next emas, o'z nusxasi. Testlarda va bitta sahifadagi ikki mustaqil vidjetda bu qulay.

Diqqat: escapeValue: false faqat natija React orqali chiqqanda xavfsiz. t() natijasini innerHTML yoki dangerouslySetInnerHTML ga yozsangiz, himoya yo'qoladi — «Hujumchi nigohi» bo'limida.

4. Ko'plik shakllari

4.1 Kalit + qo'shimcha

i18next'da ko'plik uchun bitta kalitning bir nechta varianti yoziladi. Qo'shimcha — Intl.PluralRules kategoriyasi: _one, _few, _many, _other. t() ga count berilsa, i18next kategoriyani o'zi so'raydi va mos variantni oladi:

flowchart TD
  A["t('qoldi', count: 5)"] --> B["til: ru"]
  B --> C["PluralRules('ru')<br/>select(5)"]
  C --> D["'many'"]
  D --> E["qoldi_many"]
  E --> F["'Осталось {{count}} задач'"]
  F --> G["Осталось 5 задач"]

Nimaga qarang: tilning qoidasini i18next o'zi bilmaydi — brauzerdagi Intl.PluralRules dan so'raydi. Biz faqat har kategoriyaga matn yozamiz.

Kategoriyalar soni tilga bog'liq (Intl.PluralRules darsidagi jadval): o'zbek va ingliz — one, other; rus — one, few, many, other.

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

i18next.use(initReactI18next).init({
  lng: "ru",
  fallbackLng: "uz",
  resources: {
    uz: {
      translation: {
        qoldi_one: "{{count}} ta vazifa qoldi",
        qoldi_other: "{{count}} ta vazifa qoldi",
      },
    },
    ru: {
      translation: {
        qoldi_one: "Осталась {{count}} задача",
        qoldi_few: "Осталось {{count}} задачи",
        qoldi_many: "Осталось {{count}} задач",
        qoldi_other: "Осталось {{count}} задачи",
      },
    },
  },
  interpolation: { escapeValue: false },
});

function App() {
  const { t } = useTranslation();
  return (
    <ul>
      {[1, 2, 5, 21, 0, 1.5].map((n) => (
        <li key={n}>{t("qoldi", { count: n })}</li>
      ))}
    </ul>
  );
}

Ekranda:

text
Осталась 1 задача
Осталось 2 задачи
Осталось 5 задач
Осталась 21 задача
Осталось 0 задач
Осталось 1.5 задачи
count PluralRules("ru") Kalit
1, 21 one qoldi_one
2 few qoldi_few
5, 0 many qoldi_many
1.5 other qoldi_other

21 — "bir" bilan tugaydi, shuning uchun one (задача), garchi 21 ta bo'lsa ham. Kasr son ruschada other. vazifalar da kasr bo'lmaydi, lekin _other ni baribir yozing: u har tilda bor va oxirgi zaxira.

O'zbekchada esa ikkala kalit bir xil matn. Nega yozamiz? one va other — o'zbek tilining CLDR kategoriyalari. _one bo'lmasa, count: 1 da ekranga kalitning o'zi — qoldi chiqadi.

4.2 Nol uchun alohida matn: _zero

Осталось 0 задач — to'g'ri, lekin quruq. Ko'pincha 0 uchun boshqa gap kerak: "Hammasi bajarildi!". i18next'da buning uchun _zero qo'shimchasi bor. U tilning qoidasidan qat'i nazar, count: 0 da birinchi tekshiriladi:

jsx
import i18next from "i18next";

i18next.init({
  lng: "uz",
  resources: {
    uz: {
      translation: {
        savat_zero: "Savatcha bo'sh",
        savat_one: "Savatchada {{count}} ta taom",
        savat_other: "Savatchada {{count}} ta taom",
      },
    },
  },
});

for (const count of [0, 1, 3]) {
  console.log(i18next.t("savat", { count }));
}

Konsolda:

text
Savatcha bo'sh
Savatchada 1 ta taom
Savatchada 3 ta taom

_zero — Intl kategoriyasi emas, i18next'ning qo'shimchasi. Yozilmasa, 0 tilning odatdagi qoidasiga tushadi (ruschada many).

Tekshirib ko'ring: Ingliz tili uchun qoldi_one: "{{count}} task left" va qoldi_other: "{{count}} tasks left" yozildi. count: 0 da nima chiqadi?

Javob

«0 tasks left». Ingliz tilida PluralRules("en").select(0) — other. _zero yozilmagan, shuning uchun qoldi_other olinadi. "No tasks left" kerak bo'lsa — qoldi_zero qo'shing.

5. Namespace'lar: tarjimalarni fayllarga bo'lish

5.1 Nega bitta fayl emas

Ilova o'sgani sari tarjima fayli ham o'sadi: yuzlab kalit. Hammasini birinchi sahifada yuklash shart emas. Sozlamalar sahifasidagi matnlar faqat o'sha sahifa ochilganda kerak. Tarjimonlar ham bo'lim bo'yicha ishlaydi.

Namespace (nomlar fazosi) — tarjimalarning alohida guruhi, odatda alohida fayl: uz/umumiy.json, uz/sozlamalar.json. Komponent qaysi guruhdan o'qishini useTranslation("sozlamalar") bilan aytadi:

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

i18next.use(initReactI18next).init({
  lng: "uz",
  fallbackLng: "uz",
  ns: ["umumiy", "sozlamalar"],
  defaultNS: "umumiy",
  resources: {
    uz: {
      umumiy: { sahifaNomi: "{{sahifa}} — Vazifalar" },
      sozlamalar: { sarlavha: "Sozlamalar", tema: { nomi: "Tema" } },
    },
  },
  interpolation: { escapeValue: false },
});

function SettingsPage() {
  const { t } = useTranslation("sozlamalar");
  const pageTitle = t("umumiy:sahifaNomi", { sahifa: t("sarlavha") });
  console.log(pageTitle);
  return (
    <>
      <h2>{t("sarlavha")}</h2>
      <p>{t("tema.nomi")}</p>
    </>
  );
}

export default SettingsPage;

Konsolda:

text
Sozlamalar — Vazifalar

Ekranda:

text
Sozlamalar
Tema
  • useTranslation("sozlamalar") — bu komponentdagi t() sukut bo'yicha sozlamalar dan o'qiydi.
  • t("umumiy:sahifaNomi") — ikki nuqta bilan boshqa namespace'ga murojaat. Sahifa nomi andozasi umumiy — har sahifa o'z nomini unga qo'yadi.
  • defaultNS: "umumiy" — namespace aytilmagan useTranslation() shu yerdan o'qiydi.

Bu misol kanon kodidan olingan: SozlamalarSahifasi.tsx dagi <title> aynan shunday yozilgan («Vazifalar qadami» bo'limida).

5.2 Kalitlarni qanday nomlash

Ba'zi loyihalar matnning o'zini kalit qiladi: t("Ro'yxatga qaytish"). Lekin o'zbekcha matn o'zgarsa, kalit ham o'zgaradi va barcha tarjimalar "uziladi". Kanon ma'noli kalitlarni joy bo'yicha guruhlaydi: bolimlar.*, statistika.*, topilmadi.*.

6. Trans: havola yoki qalin so'z bor gap

6.1 Muammo

Gap: "Buyurtma berish uchun kiring", "kiring" — havola. Uni uch bo'lakka bo'lish mumkin:

jsx
// ❌ gap bo'laklarga bo'lindi
{t("boshi")} <a href="/kirish">{t("havola")}</a> {t("oxiri")}

Tarjimon "boshi" va "oxiri" ni alohida ko'radi — gapni butunligicha ko'rmaydi. Ruschada so'z tartibi boshqa, havola gap boshiga o'tishi mumkin. Bo'laklar esa qotib qolgan.

6.2 Yechim: belgilar tarjimaning ichida

Trans komponenti gapni butunligicha tarjimada saqlaydi. Havola o'rnini esa nomli belgi bilan ko'rsatadi:

jsx
import i18next from "i18next";
import { initReactI18next, Trans } from "react-i18next";

i18next.use(initReactI18next).init({
  lng: "ru",
  fallbackLng: "uz",
  resources: {
    uz: {
      translation: {
        kirish: "Buyurtma uchun <havola>kiring</havola>.",
      },
    },
    ru: {
      translation: {
        kirish: "<havola>Войдите</havola>, чтобы заказать.",
      },
    },
  },
  interpolation: { escapeValue: false },
});

export default function App() {
  return (
    <p ref={(el) => el && console.log(el.innerHTML)}>
      <Trans
        i18nKey="kirish"
        components={{ havola: <a href="/kirish" /> }}
      />
    </p>
  );
}

Konsolda:

text
<a href="/kirish">Войдите</a>, чтобы заказать.

Ekranda:

text
Войдите, чтобы заказать.

Ruschada havola gap boshiga o'tdi — tarjimon buni o'zi hal qildi. <havola> — HTML teg emas, faqat belgi: Trans uni components dagi <a> elementi bilan almashtirdi. Tarjima fayliga haqiqiy <a href> yozilmaydi. Manzil — kodda, matn — faylda.

<havola>…</havola> dagi belgi nomi components obyektidagi kalit bilan bir xil bo'lishi shart.

Diqqat: Belgiga HTML'ning bo'sh (void) elementlari nomini bermang: link, img, br, input, meta. Biz birinchi variantda <link>Войдите</link> yozgan edik — natija <a href="/kirish"></a>Войдите bo'ldi: havola bo'sh, so'z undan tashqarida. Trans tarjimani HTML qoidalari bilan o'qiydi, HTML'da esa <link> yopilmaydigan teg. Xato tashlanmaydi — faqat havola "yo'qoladi". Shuning uchun belgilarga o'zbekcha nom beramiz: <havola>, <qalin>.

Tekshirib ko'ring: Nega tarjima fayliga "<a href=\"/kirish\">kiring</a>" yozib, dangerouslySetInnerHTML bilan chiqarish yomon fikr?

Javob

Uch sabab. Birinchisi — xavfsizlik: tarjima fayli HTML bo'lib qoladi, uni tahrirlagan har kim sahifaga skript qo'sha oladi («Hujumchi nigohi»). Ikkinchisi — vazifalar da Trusted Types satrni innerHTML ga yozishni umuman to'sadi (XSS va DOM xavfsizligi). Uchinchisi — manzil har tilda takrorlanadi: /kirish o'zgarsa, hamma faylni tuzatish kerak. Trans da manzil bitta joyda, kodda.

7. Til: qayerdan olinadi va qayerda saqlanadi

7.1 Til almashganda nima bo'ladi

changeLanguage dan keyin ikki ish qilinishi kerak: interfeys qayta chiziladi (buni react-i18next qiladi) va <html lang> yangilanadi. Ikkinchisini biz qilamiz. lang atributiga ekran o'quvchi talaffuzi, brauzer tarjimoni, imlo tekshiruvi va CSS :lang() qaraydi (Ko'p tilli matn). i18next til almashganda languageChanged hodisasini chiqaradi:

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

i18next.use(initReactI18next).init({
  lng: "uz",
  fallbackLng: "uz",
  resources: {
    uz: { translation: { salom: "Xush kelibsiz" } },
    ru: { translation: { salom: "Добро пожаловать" } },
  },
});

i18next.on("languageChanged", (lng) => {
  document.documentElement.lang = lng;
  console.log("html lang:", document.documentElement.lang);
});

function App() {
  const { t, i18n } = useTranslation();
  async function choose(lng) {
    await i18n.changeLanguage(lng);
    // i18n — hook bergan nusxa: eski render'niki
    console.log("hook nusxasi:", i18n.language);
    console.log("i18next:", i18next.language);
  }
  return (
    <>
      <p>{t("salom")}</p>
      <button onClick={() => choose("ru")}>Русский</button>
    </>
  );
}

Konsolda:

text
html lang: ru
hook nusxasi: uz
i18next: ru

Ekranda:

text
Добро пожаловать
Русский

Ikkinchi qatorga qarang — kutilmagan narsa. await dan keyin hook bergan i18n.language hali ham uz. react-i18next 17 har til uchun i18n ning yangi "nusxa-o'ramini" beradi. Shu sabab React til almashganini obyekt o'zgarganidan sezadi. Bizning choose funksiyamiz esa eski render'da yaratilgan — u eski o'ramni ushlab turibdi. Bu 17-qismdagi eskirgan qiymat (stale closure) bilan bir xil holat. Qoida: yangi tilni keyingi render'da i18n.language dan yoki i18next.language dan o'qing.

7.2 Tilni aniqlash: detector

Foydalanuvchi birinchi marta kirdi. Qaysi tilda ko'rsatamiz? Tayyor plagin — i18next-browser-languagedetector. U tilni ketma-ket bir necha joydan qidiradi va tanlovni eslab qoladi (hujjat bo'yicha, kanonda ishlatilmagan):

js
import LanguageDetector from "i18next-browser-languagedetector";

i18next.use(LanguageDetector).init({
  fallbackLng: "uz",
  supportedLngs: ["uz", "ru"],
  detection: {
    // ?lng=ru → saqlangan tanlov → brauzer tili
    order: ["querystring", "localStorage", "navigator"],
    caches: ["localStorage"],
  },
});

navigator — brauzer sozlamasidagi til (navigator.languages). Kanon esa uni ataylab o'qimaydi. Izohdan (i18n/index.ts): "O'zbekistonda brauzerlarning ko'pi en-US yoki ru — foydalanuvchi o'zbekcha saytni ochgan, tilni o'zi tanlaydi". Brauzer tili — telefon sotib olinganda qo'yilgan sozlama, odamning xohlagan tili emas.

Kanon qoidasi:

  1. Birinchi kirish — har doim o'zbekcha (asosiy til).
  2. Foydalanuvchi tanlasa — localStorage ga (vazifalar:til) yoziladi.
  3. Keyingi kirishda — saqlangan til.

Qaysi yo'l to'g'ri — auditoriyaga bog'liq. Xalqaro do'kon uchun brauzer tili yaxshi taxmin. O'zbek tilidagi xizmat uchun esa o'zbekcha birinchi bo'lishi kerak. Tilni manzilda saqlash (/ru/…) — keyingi qadam: havolani ulashsa, til ham birga ketadi. Kanon buni i18n II da qiladi.

8. Boshqa tillarni kech yuklash

8.1 Hamma tilni birdaniga yuklamang

O'zbekcha foydalanuvchiga ruscha va inglizcha fayllar kerak emas. Har til fayli — qo'shimcha baytlar. Yechim — backend plagini: i18next tarjima kerak bo'lganda uni read() orqali so'raydi.

Mashhur variant — i18next-http-backend: u fayllarni serverdan fetch bilan oladi, manzil andozasi loadPath: "/locales/{{lng}}/{{ns}}.json" (hujjat bo'yicha, kanon o'lchamagan).

Kanon boshqa yo'lni tanladi — o'z kichik backend'i, import() bilan. Code splitting darsidagi dinamik import har til faylini alohida chunk qiladi. Fayl nomida hash bor, kesh muammosi yo'q, CSP o'zgarmaydi — fayl o'z origin'imizdan keladi ("Vazifalar qadami" da kodi).

8.2 Jonli: til fayli kelguncha

Natija oynasida dinamik import() ishlamaydi. Shuning uchun faylni setTimeout bilan "kech keladigan" qilib taqlid qilamiz (o'quv modeli):

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

const FILES = {
  ru: { translation: { menyu: "Меню", savat: "Корзина" } },
};

// O'quv modeli: kanondagi import() o'rniga 200 ms kechikish
const slowBackend = {
  type: "backend",
  init() {},
  read(lng, ns, done) {
    console.log(`yuklanmoqda: ${lng}/${ns}`);
    setTimeout(() => done(null, FILES[lng][ns]), 200);
  },
};

i18next
  .use(slowBackend)
  .use(initReactI18next)
  .init({
    lng: "uz",
    fallbackLng: "uz",
    // o'zbekcha — shu yerda, boshqasi — backend'dan
    resources: {
      uz: { translation: { menyu: "Menyu", savat: "Savatcha" } },
    },
    partialBundledLanguages: true,
    react: { useSuspense: false },
  });

function App() {
  const { t, i18n } = useTranslation();
  const [loading, setLoading] = useState(false);
  async function choose(lng) {
    setLoading(true);
    await i18n.changeLanguage(lng);
    setLoading(false);
  }
  return (
    <div>
      <button onClick={() => choose("ru")}>Русский</button>
      <p>
        {loading ? "Yuklanmoqda…" : t("menyu")} · {t("savat")}
      </p>
    </div>
  );
}

Konsolda:

text
yuklanmoqda: ru/translation

Ekranda:

text
Русский
Меню · Корзина
Qadam Til Ekranda
boshida uz (fayl resources da) Menyu · Savatcha
Русский bosildi hali uz, ru yuklanmoqda Yuklanmoqda… · Savatcha
200 ms dan keyin ru Меню · Корзина

Nimaga qarang: til fayli kelgandan keyin almashdi. Yarim o'zbekcha, yarim ruscha ekran bo'lmadi — changeLanguage avval faylni kutadi.

Uch sozlama:

  • partialBundledLanguages: true — "ba'zi tillar resources da, qolgani backend'dan". Usiz resources berilgan bo'lsa, backend umuman chaqirilmaydi.
  • fallbackLng: "uz" — yozilmasa, backend dev/translation ni ham so'raydi. Bizning FILES.dev yo'q — blok TypeError bilan yiqilardi (sinab ko'rdik).
  • useSuspense: false — sukut bo'yicha true: tarjima tayyor bo'lmasa, useTranslation komponentni Suspense ga "to'xtatadi" va eng yaqin fallback chiqadi. Kanon false qo'ygan va birinchi chizishdan oldin tilni o'zi yuklaydi («Vazifalar qadami»).

9. Vazifalar qadami: ikki tilli interfeys (holat 02)

9.1 Branch va paketlar

  • Branch: feat/i18n
bash
git switch -c feat/i18n
npm i -E i18next@26.4.3 react-i18next@17.0.16
Fayl O'zgarish
src/i18n/index.ts yangi: nusxa, kech yuklash backend'i, <html lang>, til tanlash
src/i18n/uz/*.json, src/i18n/ru/*.json yangi: umumiy va sozlamalar namespace'lari
komponentlar/TilTanlov.tsx yangi: til tanlovi (radio)
main.tsx birinchi chizishdan oldin tilni tayyorlash
Qobiq.tsx, Statistika.tsx, TemaTanlov.tsx, 3 ta sahifa matnlar → t()
tsconfig.app.json, tekshiruv/vitest-sozlash.ts, asos.css JSON importi, testlarda i18n, #til-tanlov uslubi

9.2 src/i18n/index.ts

Kanondan, ikki bo'lak (izohlar qisqartirilgan). Birinchisi — tillar va saqlangan tanlov:

ts
import i18next from "i18next";
import type { BackendModule, ReadCallback } from "i18next";
import { initReactI18next } from "react-i18next";
import umumiy from "./uz/umumiy.json";
import sozlamalar from "./uz/sozlamalar.json";

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

// Til nomi — o'sha tilning o'zida (autonym)
export const TIL_NOMLARI: Readonly<Record<Til, string>> = Object.freeze(
  {
    uz: "O'zbekcha",
    ru: "Русский",
  },
);

const TIL_KALITI = "vazifalar:til";

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

// navigator.language ATAYLAB o'qilmaydi (izohi — «Tilni aniqlash»)
function saqlanganTil(): Til {
  try {
    const til = localStorage.getItem(TIL_KALITI);
    return tilmi(til) ? til : ASOSIY_TIL;
  } catch {
    return ASOSIY_TIL; // xotira yopiq (maxfiy rejim, sozlama)
  }
}

Uch narsaga qarang:

  • O'zbekcha JSON statik import qilingan — u asosiy build'ga tushadi va birinchi chizish tarjimani kutmaydi.
  • tilmi — type guard: localStorage dagi satr — tashqi qiymat, unga ishonilmaydi. "de" yoki buzuq satr — o'zbekcha.
  • try/catch — maxfiy rejimda localStorage xato tashlashi mumkin (localStorage).

Ikkinchi bo'lak — kech yuklash va sozlash:

ts
// import() — Vite har til/namespace faylini alohida chunk qiladi
// (http backend kerak emas: fayl o'z origin'imizdan, CSP o'zgarmaydi)
const kechYuklash: BackendModule = {
  type: "backend",
  init() {},
  read(til: string, ns: string, tayyor: ReadCallback) {
    import(`./${til}/${ns}.json`).then(
      (modul: { default: Record<string, unknown> }) =>
        tayyor(null, modul.default),
      (xato: unknown) => tayyor(xato as Error, false),
    );
  },
};

const i18n = i18next.createInstance();

void i18n
  .use(kechYuklash)
  .use(initReactI18next)
  .init({
    lng: saqlanganTil(),
    fallbackLng: ASOSIY_TIL,
    supportedLngs: TILLAR,
    ns: ["umumiy", "sozlamalar"],
    defaultNS: "umumiy",
    // O'zbekcha — shu yerda; boshqa til — kechYuklash orqali
    resources: { uz: { umumiy, sozlamalar } },
    partialBundledLanguages: true,
    // React o'zi ekranlaydi (XSS'dan) — ikkinchi marta kerak emas
    interpolation: { escapeValue: false },
    // Til fayli kelguncha komponent Suspense'ga tushmasin: main.tsx
    // birinchi chizishdan oldin tilni yuklab oladi (tilniTayyorla)
    react: { useSuspense: false },
  });

// <html lang>: ekran o'quvchi talaffuzi, brauzer tarjimoni, imlo
// tekshiruvi, CSS :lang() — hammasi shunga qaraydi
i18n.on("languageChanged", (til) => {
  document.documentElement.lang = til;
});

Darsdagi hamma sozlama shu yerda: fallbackLng, ns/defaultNS, partialBundledLanguages, escapeValue: false, useSuspense: false, languageChanged. Jonli misoldagi setTimeout o'rnida — haqiqiy import(). supportedLngs — ro'yxatdan tashqari til (de) so'ralsa, i18next uni qabul qilmaydi.

Faylning oxirida ikki funksiya:

ts
export function tilniTayyorla(): Promise<unknown> | null {
  document.documentElement.lang = i18n.language;
  return i18n.language === ASOSIY_TIL
    ? null
    : i18n.loadNamespaces(["umumiy", "sozlamalar"]);
}

/** Til tanlandi: saqlanadi va interfeys o'sha tilga o'tadi */
export async function tilniAlmashtir(til: Til): Promise<void> {
  try {
    localStorage.setItem(TIL_KALITI, til);
  } catch {
    // saqlanmasa ham — shu sahifada til o'zgaradi
  }
  await i18n.changeLanguage(til);
}

9.3 main.tsx: o'zbekcha — darhol, boshqasi — fayl kelgach

tsx
const tilTayyor = tilniTayyorla();
if (tilTayyor === null) {
  chiz(ildiz);
} else {
  // Til fayli kelmasa (oflayn, birinchi tashrif) — o'zbekcha bilan
  tilTayyor.finally(() => chiz(ildiz)).catch(() => {});
}

O'zbekcha foydalanuvchi uchun ilova avvalgidek sinxron chiziladi (13-qismdagi blocking="render", CLS 0). Ruscha tanlagan foydalanuvchi avval kichik faylni kutadi, keyin darhol ruscha sahifani ko'radi. Fayl kelmasa (oflayn) — ilova o'zbekcha ochiladi, yiqilmaydi.

9.4 Tarjima fayllari va ko'plik

src/i18n/ru/umumiy.json dan (hisob qismi):

json
"hisob": {
  "matn": "{{qoldi}}, {{bajarildi}}",
  "qoldi_one": "Осталась {{count}} задача",
  "qoldi_few": "Осталось {{count}} задачи",
  "qoldi_many": "Осталось {{count}} задач",
  "qoldi_other": "Осталось {{count}} задачи",
  "bajarildi_one": "{{count}} выполнена",
  "bajarildi_few": "{{count}} выполнены",
  "bajarildi_many": "{{count}} выполнено",
  "bajarildi_other": "{{count}} выполнено"
}

O'zbekchasida — faqat _one va _other, matni bir xil («{{count}} ta vazifa qoldi»). Statistika.tsx dagi o'zgarish:

diff
-        {`${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`}
+        {t("hisob.matn", {
+          qoldi: t("hisob.qoldi", { count: qoldi }),
+          bajarildi: t("hisob.bajarildi", { count: bajarildi }),
+        })}

Gap ikki ko'plikli bo'lakdan yig'iladi. Har bo'lak o'z soni bilan o'z shaklini oladi: Осталось 5 задач, 2 выполнены. Bo'laklarni birlashtiradigan andoza (hisob.matn) ham tarjimada — tartibni tarjimon o'zgartira oladi.

9.5 Til tanlovi

TilTanlov.tsx — TemaTanlov kabi radio guruh (kanondan, qisqartirilgan):

tsx
  const { t, i18n } = useTranslation("sozlamalar");
  // ...
      {TILLAR.map((til) => (
        <Fragment key={til}>
          <input
            type="radio"
            name="til"
            id={`til-${til}`}
            value={til}
            checked={i18n.resolvedLanguage === til}
            onChange={(event) => {
              const tanlangan = event.target.value;
              if (tilmi(tanlangan)) {
                void tilniAlmashtir(tanlangan);
              }
            }}
          />
          {/* lang — til nomi o'z tilida o'qilsin
            ("Русский" ruscha) */}
          <label htmlFor={`til-${til}`} lang={til}>
            {TIL_NOMLARI[til]}
          </label>
        </Fragment>
      ))}
  • Til nomlari o'z tilida: «O'zbekcha», Русский. Ruscha bilgan, lekin o'zbekchani tushunmaydigan odam "Ruscha" so'zini o'qiy olmasligi mumkin — Русский ni esa taniydi.
  • lang={til} — Malikaning ekran o'quvchisi Русский ni ruscha talaffuz bilan o'qiydi.
  • i18n.resolvedLanguage — hook bergan nusxadan, render paytida o'qiladi. Bu «Til almashganda nima bo'ladi» bo'limidagi tuzoqqa tushmaydi.
  • tilmi — radio qiymati ham satr, tekshirib o'tkaziladi.

9.6 Testlar va TypeScript

Ikki kichik, lekin majburiy o'zgarish. tsconfig.app.json ga "resolveJsonModule": true qo'shildi. Testlar sozlamasiga esa i18n importi:

ts
// tekshiruv/vitest-sozlash.ts oxiri
// 21/#02: komponentlar matnni t() dan oladi — i18next sozlangan
// bo'lishi kerak (aks holda t() kalitning o'zini qaytaradi:
// "hisob.matn").
import "../src/i18n/index.ts";

Importsiz Vitest'dagi komponent testlari "hisob.matn" ni ko'rib yiqilardi — «Kalit hech qayerda yo'q bo'lsa» bo'limidagi xulq. resolveJsonModule siz esa import umumiy from "./uz/umumiy.json" — tsc xatosi.

9.7 Natija va narx

Kanon tekshiruvi (natijalar/tekshiruv-02.txt): build, node:test 203, Vitest 65, lint, format, tip, knip — toza. Build bitta ogohlantirish berdi:

text
[INEFFECTIVE_DYNAMIC_IMPORT] src/i18n/uz/umumiy.json is dynamically imported by src/i18n/index.ts but also statically imported by src/i18n/index.ts, dynamic import will not move module into another chunk.

Tarjimasi: "uz/umumiy.json dinamik ham, statik ham import qilingan — dinamik import uni alohida chunk'ga ajratmaydi." import(\./${til}/${ns}.json`)andozasi o'zbekcha fayllarni ham qamraydi, ular esa statik import qilingan. Bu ataylab: o'zbekcha baribir asosiy build'da bo'lishi kerak. Ogohlantirish zararsiz, ruscha fayllar esa alohida chunk bo'ldi:umumiy— 0.64 KB,sozlamalar` — 0.34 KB gzip.

Birinchi yuklash JS (gzip): 201.8 → 219.0 KB — i18next va react-i18next uchun +17.1 KB. Bu — shu qismdagi eng katta birinchi yuklash narxi. Kanon muqobillarni ham o'lchadi (kutubxonalar-21.txt, bitta ruscha ko'plikli gap):

i18n: bitta ko'plikli gap uchun JS (gzip, React'dan tashqari)
  • o'z t() + Intl.PluralRules0,1 KB
  • Lingui 6.9.1 (runtime)kompilyatsiya qadami kerak2,3 KB
  • react-intl 12.1.414 KB
  • i18next + react-i18next15,4 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, React 19.3.0, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt, 2026-10-17

O'z t() — atigi 0.1 KB: Intl.PluralRules brauzerda bor. Nega kanon baribir i18next'ni oldi? Namespace'lar, kech yuklash, Trans, zaxira til, formatlash va tarjima xizmatlari bilan integratsiya — hammasi tayyor. Bu yo'l eng keng tarqalgan, yangi dasturchi uni taniydi. Narx halol yozildi (TEXNIK-QARZ 61), qaror: qabul. Lingui va FormatJS'ning ICU sintaksisi — i18n II da.

Hamma matn ham ko'chmadi. Tarjima qilingan: qobiq (menyu, skip link), ro'yxat hisobi, statistika sahifasi, sozlamalar, 404. Formalar, e'lonlar, xatolar, dialoglar — hozircha o'zbekcha (TEXNIK-QARZ 60). Sabab: ~900 qatorli App va 60 dan ortiq komponent. Real loyihada ham i18n shunday kiradi — bosqichma-bosqich, har yangi komponent t() bilan.

Brauzer tekshiruvi — 21-qismning yangi e2e-21.mjs, Chrome 155 (brauzer-tekshiruv/natija-21-02.txt, 16 qatordan 10 tasi):

text
✅ o'zbekcha: html lang, menyu, hisob: ["uz",["Ro'yxat","Statistika","Sozlamalar"],"2 ta vazifa qoldi, 1 ta bajarildi"]
✅ ruscha chunk hali yuklanmagan: []
✅ ruscha: lang, menyu, sarlavha, title, legend: [["Список","Статистика","Настройки"],"Настройки","Настройки — Задачи","Язык","Тема"]
✅ ruscha fayllar — tanlangandan keyin, bir marta: ["sozlamalar","umumiy"]
✅ CSP buzilishi yo'q: []
✅ ruscha ko'plik: 21/1: "Осталась 21 задача, 1 выполнена"
✅ ruscha ko'plik: 5/2: "Осталось 5 задач, 2 выполнены"
✅ ruscha ko'plik: 0/5: "Осталось 0 задач, 5 выполнено"
✅ noma'lum til → o'zbekcha: ["uz","2 ta vazifa qoldi, 1 ta bajarildi"]
✅ konsolda xato yo'q: []
HAMMASI ✅

Ikkinchi qator — "ruscha chunk hali yuklanmagan": o'zbekcha foydalanuvchi ruscha fayllar uchun bir bayt ham to'lamaydi. 13–20-qismlarning 159, 72, 100 va 77 tekshiruvi ham o'tdi. axe — 0 buzilish, jumladan yangi «sozlamalar — ruscha interfeys» ko'rinishida.

Yo'l-yo'lakay axe eski xatoni topdi: skip link boshqa sahifalarda yo'q #royxat ga ko'rsatardi (v10 dan beri). Endi u yerda — #ilova (Qobiq.tsx).

9.8 Commit

bash
git add -A
git commit -m "feat: react-i18next — qobiq, hisob (ko'plik), statistika, sozlamalar; ruscha kech yuklanadi"

10. Hujumchi nigohi

Tarjima fayllari ko'pincha dasturchilar qo'lida emas: tarjimonlar, tashqi xizmatlar (Crowdin, Locize), ba'zan ko'ngillilar tahrirlaydi. Demak tarjima — tashqi ma'lumot. Hujumchi tarjimaga shunday "gap" qo'shadi:

json
{ "kirish": "Kiring <img src=x onerror=\"alert(document.cookie)\">" }

Kod uni dangerouslySetInnerHTML={{ __html: t("kirish") }} bilan chiqarsa, har tashrif buyuruvchining brauzerida skript ishlaydi.

Himoya qatlamlari:

  1. Tarjimani faqat matn sifatida chiqaring: {t("…")} — React ekranlaydi. Belgi kerak bo'lsa — Trans va components (teglarni kod beradi, tarjima faqat nomini aytadi).
  2. escapeValue: false — faqat React bilan. t() natijasi innerHTML, document.write yoki shablon satri bilan HTML'ga ketsa — escapeValue ni yoqing yoki DOM'ni boshqacha quring.
  3. Trusted Types (vazifalar da yoqilgan) — innerHTML ga oddiy satr yozishni brauzer darajasida to'sadi. Yuqoridagi xato kod ishlamaydi, konsolda buzilish chiqadi.

11. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Ekranda menyu.sarlavha kalit tarjimada yo'q yoki i18n sozlanmagan (testda ham) faylni tekshiring; testlarda i18n'ni import qiling
Ekranda &lt;b&gt; escapeValue sukut bo'yicha true, React ikkinchi marta ekranladi React bilan — escapeValue: false
TypeError yoki dev fayliga so'rov fallbackLng yozilmagan — sukut ["dev"] fallbackLng: "uz"
Ruschada Осталось 21 задач ko'plik qo'lda (count > 4) yoki _one yo'q count + _one/_few/_many/_other
Til almashdi, komponent eski tilda i18next.t() komponent ichida (obuna yo'q) useTranslation() dan t
await changeLanguage() dan keyin i18n.language eski hook bergan o'ram — eski render'niki keyingi render'da yoki i18next.language
Til fayli kelguncha bo'sh ekran / fallback useSuspense: true (sukut) va Suspense yo'q Suspense qo'ying yoki tilni chizishdan oldin yuklang

12. Mashqlar

Mashqlar kurs/mashqlar/21/02-react-i18next/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Komponentga t ni beradigan hook:
  • Ruscha t("qoldi", { count: 5 }) qaysi qo'shimchali kalitni oladi: qoldi_
  • Boshqa namespace'dagi kalitda namespace va kalit orasidagi belgi (umumiy?sahifaNomi):
  • count: 0 uchun alohida matn qo'shimchasi: _
  • React bilan ishlaganda escapeValue qiymati:
Yechim

useTranslation — t va i18n ni beradi va til almashganda komponentni qayta chizadi. 5 ruschada many (5 задач). Namespace — ikki nuqta bilan: umumiy:sahifaNomi. _zero — i18next qo'shimchasi, Intl kategoriyasi emas. escapeValue: false — React o'zi ekranlaydi, ikkinchi marta kerak emas.

2-mashq (o'rta): «Bahor» savatchasi ruscha

«Bahor» savatchasi uchun savat kalitini uch tilda yozing:

  • o'zbekcha: 0 → «Savatcha bo'sh», boshqasi → «Savatchada N ta taom»;
  • ruscha: 0 → Корзина пуста, 1 → В корзине 1 блюдо, 2 → В корзине 2 блюда, 5 → В корзине 5 блюд;
  • inglizcha: 0 → «Your cart is empty», 1 → «1 dish in your cart», 3 → «3 dishes in your cart».

Komponent til tugmalarini va [0, 1, 2, 5, 21] sonlari uchun ro'yxatni chizsin. Русский bosilganda ekranda ruscha ro'yxat bo'lsin.

Ishora: ko'plik qo'shimchalari — «Ko'plik shakllari» bo'limidagi jadval; _zero — o'sha bo'limning oxirida.

Yechim
jsx
import { Fragment } from "react";
import i18next from "i18next";
import { initReactI18next, useTranslation } from "react-i18next";

i18next.use(initReactI18next).init({
  lng: "uz",
  fallbackLng: "uz",
  resources: {
    uz: {
      translation: {
        savat_zero: "Savatcha bo'sh",
        savat_one: "Savatchada {{count}} ta taom",
        savat_other: "Savatchada {{count}} ta taom",
      },
    },
    ru: {
      translation: {
        savat_zero: "Корзина пуста",
        savat_one: "В корзине {{count}} блюдо",
        savat_few: "В корзине {{count}} блюда",
        savat_many: "В корзине {{count}} блюд",
        savat_other: "В корзине {{count}} блюда",
      },
    },
    en: {
      translation: {
        savat_zero: "Your cart is empty",
        savat_one: "{{count}} dish in your cart",
        savat_other: "{{count}} dishes in your cart",
      },
    },
  },
  interpolation: { escapeValue: false },
});

const LANGS = [
  ["uz", "O'zbekcha"],
  ["ru", "Русский"],
  ["en", "English"],
];

function App() {
  const { t, i18n } = useTranslation();
  return (
    <>
      <p>
        {LANGS.map(([lng, name]) => (
          <Fragment key={lng}>
            <button
              lang={lng}
              onClick={() => i18n.changeLanguage(lng)}
            >
              {name}
            </button>{" "}
          </Fragment>
        ))}
      </p>
      <ul>
        {[0, 1, 2, 5, 21].map((n) => (
          <li key={n}>{t("savat", { count: n })}</li>
        ))}
      </ul>
    </>
  );
}

Ekranda:

text
O'zbekcha Русский English
Корзина пуста
В корзине 1 блюдо
В корзине 2 блюда
В корзине 5 блюд
В корзине 21 блюдо

21 — one (блюдо). savat_other ruschada kasr sonlar uchun (1.5 блюда) — bu ro'yxatda ishlatilmadi, lekin har tilda bo'lishi kerak. Tugmalarda lang — har til nomi o'z talaffuzi bilan o'qiladi.

3-mashq (qiyin): Trans va kech keladigan til

Kirish taklifi: «Buyurtmalar tarixi uchun kiring» — «kiring» havola (/kirish). Talablar:

  • o'zbekcha — resources da, ruscha (<havola>Войдите</havola>, чтобы увидеть историю заказов.) — «Boshqa tillarni kech yuklash» bo'limidagi kabi 200 ms kechikadigan backend'dan;
  • Русский bosilganda fayl kelguncha tugma disabled bo'lsin va matni «…» ga almashsin;
  • til almashganda <html lang> yangilansin va konsolga chiqsin;
  • havola Trans + components bilan.

Sinov: Русский ni bosing, oraliqda «…» ko'rinsin, oxirida ruscha gap va havola.

Yechim
jsx
import { useEffect, useRef, useState } from "react";
import i18next from "i18next";
import { initReactI18next, Trans } from "react-i18next";
import { useTranslation } from "react-i18next";

const UZ = "Buyurtmalar tarixi uchun <havola>kiring</havola>.";
const RU = "<havola>Войдите</havola>, чтобы увидеть историю заказов.";
const FILES = { ru: { translation: { kirish: RU } } };

i18next
  .use({
    type: "backend",
    init() {},
    read(lng, ns, done) {
      setTimeout(() => done(null, FILES[lng][ns]), 200);
    },
  })
  .use(initReactI18next)
  .init({
    lng: "uz",
    fallbackLng: "uz",
    resources: { uz: { translation: { kirish: UZ } } },
    partialBundledLanguages: true,
    interpolation: { escapeValue: false },
    react: { useSuspense: false },
  });

i18next.on("languageChanged", (lng) => {
  document.documentElement.lang = lng;
  console.log("lang:", lng);
});

function App() {
  const { i18n } = useTranslation();
  const [loading, setLoading] = useState(false);
  const textRef = useRef(null);
  useEffect(() => {
    console.log(textRef.current.innerHTML);
  }, [i18n.language]);
  async function choose(lng) {
    setLoading(true);
    await i18next.changeLanguage(lng);
    setLoading(false);
  }
  return (
    <>
      <button disabled={loading} onClick={() => choose("ru")}>
        {loading ? "…" : "Русский"}
      </button>
      <p ref={textRef}>
        <Trans
          i18nKey="kirish"
          components={{ havola: <a href="/kirish" /> }}
        />
      </p>
    </>
  );
}

Konsolda:

text
Buyurtmalar tarixi uchun <a href="/kirish">kiring</a>.
lang: ru
<a href="/kirish">Войдите</a>, чтобы увидеть историю заказов.

Ekranda:

text
Русский
Войдите, чтобы увидеть историю заказов.

Effekt i18n.language ga bog'langan: til almashgan render'dan keyin bir marta ishlaydi va DOM'dagi tayyor havolani ko'rsatadi. lang: ru undan oldin chiqdi — languageChanged hodisasi React chizishidan oldin keladi. Til fayli kelguncha tugma disabled — ikki marta bosib ikkita so'rov yuborib bo'lmaydi.

4-mashq: vazifalar qadami

feat/i18n branch'ida «Vazifalar qadami» ni bajaring: paketlar, src/i18n/ (index.ts, uz/ va ru/ — umumiy, sozlamalar), TilTanlov, main.tsx dagi tilniTayyorla, qobiq/statistika/sozlamalar/404 matnlari t() ga, vitest-sozlash.ts va resolveJsonModule. npm run check toza bo'lsin. Keyin:

  1. Sozlamalarda Русский ni tanlang va DevTools → Network'da qaysi fayllar yuklanganini ko'ring.
  2. Ro'yxatga 21 ta vazifa qo'shing (yoki 5 tasini bajaring) — ruscha hisobni o'qing.
  3. vitest-sozlash.ts dagi i18n importini vaqtincha olib tashlab, npm test ni ishga tushiring.
Yechim
  1. Ikki kichik chunk: umumiy-….js va sozlamalar-….js (gzip 0.64 va 0.34 KB), bir martadan. O'zbekcha sahifada ular umuman yuklanmagan edi.
  2. Осталась 21 задача, … — 21 one kategoriyasi. 5 bajarilsa — 5 выполнено (many).
  3. Hisob matnini tekshiradigan komponent testlari yiqiladi: ekranda «2 ta vazifa qoldi» o'rniga kalit — hisob.matn. i18next sozlanmagan, t() kalitning o'zini qaytaradi. Importni qaytaring.

13. Real ishda

  • Tarjima jarayoni: kodda faqat o'zbekcha (asosiy til) fayl yoziladi. Qolganini tarjimonlar maxsus xizmatlarda (Crowdin, Locize, Tolgee) to'ldiradi, keyin fayllar repoga qaytadi. Kalitlarni koddan avtomatik yig'ish (i18next-cli) va kalit turlari — i18n II da.
  • O'zbekistondagi loyihalar odatda uch tilli: o'zbek (lotin, ba'zan kirill), rus, ingliz. Ruscha ko'plik — har intervyu topshirig'ida "tuzoq" bo'lishi mumkin.
  • Intervyu savollari: "i18n va l10n farqi?", "Ruscha ko'plikni qanday yozasiz?", "Nega escapeValue: false?", "Trans nima uchun kerak?", "Til faylini qanday kech yuklaysiz?", "Tilni qayerda saqlaysiz?".

Xulosa

  • i18n: komponent kalit aytadi (t("hisob.qoldi")), matn — tarjima faylida; fallbackLng — doim o'z asosiy tilingiz.
  • useTranslation til almashganda komponentni qayta chizadi; i18next.t() — faqat React tashqarisida.
  • Interpolatsiya {{ism}}; React bilan escapeValue: false xavfsiz — chunki React ekranlaydi, innerHTML bilan esa yo'q.
  • Ko'plik — Intl.PluralRules kategoriyasi qo'shimcha bo'lib: o'zbekcha _one/_other, ruscha _one/_few/_many/_other, nol uchun _zero.
  • Namespace — fayl bo'yicha guruh (useTranslation("sozlamalar"), t("umumiy:…")); havolali gap — Trans + components.
  • vazifalar: o'zbekcha build'da va sinxron, ruscha — import() bilan kech chunk; birinchi yuklash +17.1 KB (o'z t() +0.1) — halol, qabul qilingan.

Keyingi dars: i18n II: o'zbek lotin/kirill, URL va type-safe kalitlar — til manzilga ko'chadi (/ru/…), kirill varianti skript bilan yasaladi. Noto'g'ri kalit tsc xatosi beradi, sana va foiz Intl bilan formatlanadi.

Manbalar

  • i18next: "Configuration Options" (fallbackLng, partialBundledLanguages, supportedLngs), "Plurals", "Interpolation", "Namespaces", "Add or Load Translations" — i18next.com
  • react-i18next: "useTranslation (hook)", "Trans Component", "Using with Suspense" — react.i18next.com
  • i18next-browser-languagedetector, i18next-http-backend — github.com/i18next
  • Unicode CLDR: "Language Plural Rules" — unicode.org/cldr
  • MDN: Intl.PluralRules, lang global atribut — developer.mozilla.org
  • Kanon: vazifalar holat 02 (src/i18n/, TilTanlov.tsx, Statistika.tsx), natijalar/tekshiruv-02.txt, 02-axe.txt, brauzer-tekshiruv/natija-21-02.txt, natijalar/tajriba/kutubxonalar-21.txt, TEXNIK-QARZ 60–61 (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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