WisarWisar
Dasturlash kitobi/8-QISM — NestJS34 daqiqa

8.30-bob: i18n (ko'p til) backend

8-QISM — NestJS (chuqur) · 30-mavzu · Amaliy real mavzu


1. Kirish va motivatsiya

Bu — 8-QISM va "real amaliy mavzular" turkumining yakuniy bobi: i18n (internationalization — xalqarolashtirish, ko'p til). O'zbekistonda deyarli har ilova kamida uch tilda ishlashi kerak: o'zbek, rus, ingliz. Va xalqaro bozorga chiqsangiz — yana ko'proq. Ko'p dasturchi i18n'ni faqat frontend masalasi deb o'ylaydi (tugma, menyu tarjimasi), lekin backend ham ko'p tilni qo'llab-quvvatlashi kerak: xato xabarlari ("Email band" / "Email zanyat" / "Email taken"), email/SMS matni (8.10, 5.18), bildirishnoma 8.23-bob, kontent (mahsulot nomi/tavsifi ko'p tilda), va valyuta/sana/raqam formati.

Backend i18n'ning ikki qismi bor: statik tarjima (xato xabarlari, email shablonlari — kod bilan birga, kalit-qiymat fayllar) va dinamik kontent (mahsulot nomi, maqola — DB'da ko'p tilda saqlanadi). Birinchisi — nestjs-i18n kabi kutubxona (til faylllari); ikkinchisi — DB dizayni (har til uchun ustun yoki JSON). Va eng muhim — tilni aniqlash (foydalanuvchi qaysi tilni xohlaydi — header, profil, parametr) va format (sana, raqam, valyuta — har til/mamlakatda boshqacha).

Bu bob: nega backend i18n (faqat frontend emas), tilni aniqlash (Accept-Language/header/profil), statik tarjima (nestjs-i18n — xato/email), dinamik kontent (DB ko'p til — ustun vs JSON vs jadval), format (sana/raqam/valyuta — Intl), pluralization (ko'plik shakllari), va fallback (til topilmasa). Bu bob 8.5 (validatsiya xabari), 8.10 (email), 8.12 (bot i18n), 11 (frontend i18n) bilan bog'liq. i18n — keng auditoriya, professional ilova belgisi.

O'xshatish: i18n — xalqaro mehmonxona xizmati. Mehmon (foydalanuvchi) qaysi tilda gaplashishini aytadi (Accept-Language — "men ingliz tilida"), va mehmonxona unga o'sha tilda xizmat ko'rsatadi: belgilar (statik tarjima — xato xabarlari), menyu (dinamik kontent — taom nomlari), narx (valyuta formati — $/so'm), sana (format — 06/22 yoki 22.06). Yaxshi mehmonxona — har tilni biladi (yoki kamida asosiy tillarni — fallback). Til bilmaslik — mehmonni yo'qotish (foydalanuvchi tushunmaydi ketadi). Backend — ko'p tilli xizmatchi.

Nega muhim?

  • O'zbekiston — o'zbek/rus/ingliz (deyarli har ilova).
  • Backend ham — xato, email, kontent (faqat frontend emas).
  • Keng auditoriya — ko'p til = ko'p foydalanuvchi.
  • Professional — xalqaro, sifatli ilova belgisi.

2. Nazariya — chuqur tushuntirish

2.0. i18n vs l10n vs g11n — atamalar farqi

Ko'p manbada bir-biriga yaqin uchta atama uchraydi va ular ko'pincha aralashtiriladi. Aslida ular jarayonning turli bosqichlari:

text
  i18n (internationalization — xalqarolashtirish):
    Kodni har qanday til/mintaqaga MOSLASHUVCHAN qilib yozish.
     Matnni koddan ajratish (kalit-qiymat), tilni parametr qilish,
      formatlarni Intl'ga topshirish. "Kod tayyor, endi til qo'shsa bo'ladi".
     18 harf "internationalization" so'zining i...n orasida (18 harf)  i18n.

  l10n (localization — lokalizatsiya):
    Aniq bir til/mintaqa uchun MAZMUN tayyorlash.
     uz.json'ni to'ldirish, rus tarjimoniga bermoq, so'm valyutasini
      sozlash, sanani "22-iyun" ko'rinishida. "Endi rus tilini qo'shdik".
     "localization" — 10 harf o...n orasida  l10n.

  g11n (globalization — globallashtirish):
    i18n + l10n + biznes jarayoni (yuridik, to'lov, qo'llab-quvvatlash).

  ┌─────────────┬──────────────────────────┬─────────────────────┐
  │ Atama       │ Nima qiladi              │ Kim qiladi          │
  ├─────────────┼──────────────────────────┼─────────────────────┤
  │ i18n        │ Kodni moslashuvchan qil  │ Dasturchi (bir mart)│
  │ l10n        │ Til/mintaqa mazmuni      │ Tarjimon (har til)  │
  │ g11n        │ Butun global biznes      │ Jamoa               │
  └─────────────┴──────────────────────────┴─────────────────────┘

i18n vs l10n: i18n (internationalization) — kodni tilga bog'liq bo'lmagan, moslashuvchan qilish (matn koddan ajratilgan, format Intl'da) — dasturchi bir marta qiladi. l10n (localization) — aniq til/mintaqa uchun mazmun (tarjima, valyuta, sana ko'rinishi) — har til uchun qilinadi (tarjimon). Oddiy qilib: i18n — "til qo'shsa bo'ladigan" idish tayyorlash; l10n — o'sha idishga aniq tilni quyish. Bu bobda asosan i18n (idish — kod, nestjs-i18n, resolverlar), lekin l10n mazmuni (uz/ru/en fayllari, so'm formati) ham ko'riladi. Raqamlar — so'z harflari soni (i·18·n = "i" + 18 harf + "n").

2.1. Nega backend i18n (faqat frontend emas)

text
  FRONTEND i18n yetmaydi — BACKEND ham ko'p til kerak:
   Xato xabarlari (validatsiya, biznes — 8.5): "Email band"/"Email zanyat"
   Email/SMS matni (8.10, 5.18): xabar foydalanuvchi tilida
   Push bildirishnoma 8.23-bob
   Dinamik kontent (mahsulot, maqola — DB ko'p til)
   Format (sana, raqam, valyuta — server generatsiya: PDF, hisobot)
   Bot (8.12 — to'liq backend i18n)

   Backend foydalanuvchiga matn yuboradi  u tilni bilishi kerak

Nega backend i18n: frontend i18n (tugma/menyu) yetmaydi — backend ham foydalanuvchiga matn yuboradi: xato xabarlari 8.5-bob, email/SMS (8.10, 5.18), push 8.23-bob, dinamik kontent (DB), format (PDF/hisobot — 8.21). Bot 8.12-bobto'liq backend i18n (frontend yo'q). Backend foydalanuvchi tilini bilishi kerak (aks holda inglizcha xato o'zbek foydalanuvchiga — yomon UX). Backend i18n — to'liq ko'p tilning yarmi.

2.2. Tilni aniqlash (qaysi til)

text
  FOYDALANUVCHI TILINI ANIQLASH (prioritet):
  1. So'rov parametri: ?lang=uz (aniq — eng yuqori)
  2. Foydalanuvchi profili: user.til (kirgan — saqlangan tanlov)
  3. Header: Accept-Language: uz-UZ,ru;q=0.9 (brauzer)
  4. Custom header: X-Lang: uz
  5. Default: o'zbek (fallback — 2.7)

   Aniq (parametr) > saqlangan (profil) > brauzer (header) > default

Tilni aniqlash (prioritet bilan): so'rov parametri (?lang=uz — eng aniq), foydalanuvchi profili (saqlangan tanlov — kirgan), Accept-Language header (brauzer afzalligi), custom header, default (fallback — 2.7). Prioritet: aniq tanlov > saqlangan > brauzer > default. nestjs-i18n — resolverlar (har manbadan til). Til aniqlanmasa — default (hech qachon xato emas). Bu — i18n'ning birinchi qadami.

Resolverlar to'liq va prioritet mexanizmi

nestjs-i18n tilni bir nechta resolver orqali aniqlaydi. Har so'rovga resolverlar massivdagi tartib bo'yicha (yuqoridan pastga) yuriladi: birinchi resolver null bo'lmagan qiymat qaytarsa — shu til ishlatiladi, qolganlari tekshirilmaydi. Hech biri qiymat qaytarmasa — fallbackLanguage.

typescript
import {
  QueryResolver,          // ?lang=uz — so'rov parametri
  HeaderResolver,         // x-lang: uz — custom header
  AcceptLanguageResolver, // Accept-Language: uz-UZ,ru;q=0.9 — brauzer
  CookieResolver,         // lang=uz — cookie (brauzer sessiyasi saqlaydi)
} from "nestjs-i18n";

resolvers: [
  { use: QueryResolver, options: ["lang", "l"] },   // 1. ?lang=uz yoki ?l=uz
  new HeaderResolver(["x-lang"]),                     // 2. x-lang header (mobil app)
  new CookieResolver(["lang"]),                       // 3. lang cookie
  AcceptLanguageResolver,                             // 4. brauzer afzalligi
  // 5. hech biri  fallbackLanguage (forRoot'da)
],

Har resolverning hayotiy holati:

  • QueryResolver?lang=uz. Foydalanuvchi tilni ataylab tanlaganda (masalan, ilova ichida til almashtirgichdan link). Eng ustun — chunki bu aniq, joriy niyat.
  • HeaderResolverx-lang: uz. Mobil ilova/API mijozi har so'rovga o'z tilini qo'yadi (brauzer emas — dastur boshqaradi).
  • CookieResolverlang=uz. Brauzer sessiyasida til saqlanadi (foydalanuvchi bir marta tanlagach, keyingi so'rovlarga avtomatik qo'shiladi).
  • AcceptLanguageResolverAccept-Language: uz-UZ,ru;q=0.9,en;q=0.8. Brauzer OT tilidan avtomatik yuboradi. q= (quality) — afzallik og'irligi (uz > ru > en). Yangi mehmon uchun yaxshi taxmin (hali tanlamagan).

Custom resolver — foydalanuvchi profilidan. Ko'p ilovada eng ishonchli manba — login qilgan foydalanuvchi profilidagi til (user.til). Buni maxsus resolver bilan qo'shamiz:

typescript
import { Injectable, ExecutionContext } from "@nestjs/common";
import { I18nResolver } from "nestjs-i18n";

@Injectable()
export class UserLangResolver implements I18nResolver {
  resolve(context: ExecutionContext): string | undefined {
    const req = context.switchToHttp().getRequest();
    // JwtAuthGuard req.user'ni to'ldiradi 8.7-bob — undagi til
    return req.user?.til;   // topilmasa undefined  keyingi resolver
  }
}

// forRoot resolvers ro'yxatida (prioritet joyiga qo'ying):
resolvers: [
  { use: QueryResolver, options: ["lang"] },  // 1. aniq tanlov (link bosdi)
  UserLangResolver,                            // 2. profil (kirgan foydalanuvchi)
  AcceptLanguageResolver,                      // 3. brauzer (mehmon)
],

Resolver prioriteti: resolverlar massivdagi tartib bo'yicha tekshiriladi — birinchi null bo'lmagan qiymat g'olib. Shuning uchun QueryResolver (aniq, joriy niyat) yuqorida, AcceptLanguageResolver (taxmin) pastda. CookieResolver — brauzer til tanlovini eslab qoladi; custom UserLangResolver — eng ishonchli (profil, user.til). Custom resolver I18nResolver'ni implement qiladi va undefined qaytarsa keyingi resolver tekshiriladi (zanjir). Tartibni siz belgilaysiz — bu i18n xatti-harakatining o'zagi.

@I18n() dekorator va I18nContext

Xatoni tarjima qilishda tilni qo'lda uzatish shart emas — nestjs-i18n joriy so'rov tilini avtomatik biladi. Uni olishning ikki yo'li:

1) @I18n() dekorator — kontroller/method argumentiga joriy tarjima kontekstini oladi:

typescript
import { I18n, I18nContext } from "nestjs-i18n";

@Get("salom")
salom(@I18n() i18n: I18nContext) {
  return i18n.t("umumiy.salom");   // joriy tilda (resolver aniqlagan)
}

2) I18nContext.current() — dekoratorsiz, chuqur servisda (masalan, subscriber, exception filter — argument uzatib bo'lmaydigan joyda) joriy kontekstni statik chaqiradi:

typescript
import { I18nContext } from "nestjs-i18n";

@Injectable()
export class NarxService {
  hisobla() {
    const lang = I18nContext.current()?.lang ?? "uz";   // joriy til
    // ... shu til bilan format
  }
}

@I18n() vs I18nContext.current(): @I18n() — kontroller/method argumenti, o'qish oson va tavsiya etiladi. I18nContext.current() — argument uzatib bo'lmaydigan chuqur joy (filter, subscriber, guard) uchun statik kirish; u so'rov konteksti (AsyncLocalStorage) orqali joriy tilni topadi. I18nContext.current() faqat so'rov oqimida ishlaydi (masalan cron/queue ishchisida — 8.16 — so'rov yo'q, shuning uchun til undefined; unda tilni qo'lda { lang } bilan uzating). I18nService (DI) — universal, ammo tilni oshkora bermasangiz fallback ishlatadi.

2.3. Statik tarjima (nestjs-i18n)

typescript
// nestjs-i18n — til fayllaridan tarjima (xato, email)
import { I18nModule, AcceptLanguageResolver, QueryResolver } from "nestjs-i18n";

I18nModule.forRoot({
  fallbackLanguage: "uz",                             // default (2.7)
  loaderOptions: { path: "src/i18n/" },               // til fayllari
  resolvers: [                                        // til aniqlash (2.2)
    { use: QueryResolver, options: ["lang"] },        // ?lang=uz
    AcceptLanguageResolver,                            // header
  ],
});
json
// src/i18n/uz/xato.json
{ "email_band": "Bu email allaqachon ro'yxatdan o'tgan", "topilmadi": "{{narsa}} topilmadi" }
// src/i18n/ru/xato.json
{ "email_band": "Etot email uje zaregistrirovan", "topilmadi": "{{narsa}} ne nayden" }
// src/i18n/en/xato.json
{ "email_band": "This email is already registered", "topilmadi": "{{narsa}} not found" }

Statik tarjima (nestjs-i18n): til fayllari (uz/ru/en — kalit-qiymat JSON), forRoot (fallback, resolverlar — 2.2). Xato/email matnlari kalit bilan (email_band) — har tilda tarjima. {{narsa}} — o'zgaruvchi (interpolyatsiya). Kod tilga bog'liq emas (kalit ishlatadi). Tarjimalar fayl/DB'da (kodda emas — qo'shish/o'zgartirish oson). Bu — statik matnlar (xato, email — 2.4).

Fayl tuzilishi, nested kalitlar va t() / translate()

Til fayllari loaderOptions.path katalogida til kodi bo'yicha papkalarga bo'linadi; har papkadagi fayl nomi — kalitning birinchi bo'lagi (namespace):

text
  src/i18n/
  ├── uz/
  │   ├── xato.json       kalit: "xato.email_band"
  │   ├── email.json      kalit: "email.tasdiqlash_subject"
  │   └── umumiy.json     kalit: "umumiy.salom"
  ├── ru/  (aynan shu fayllar, ruscha qiymat)
  └── en/  (aynan shu fayllar, inglizcha qiymat)

Kalitlarni guruhlab (nested — ichma-ich) yozish tavsiya etiladi — bu tartibni saqlaydi:

json
// src/i18n/uz/xato.json — nested (guruhlangan) kalitlar
{
  "email": {
    "band": "Bu email allaqachon ro'yxatdan o'tgan",
    "notogri": "Email formati noto'g'ri"
  },
  "auth": {
    "ruxsat_yoq": "Bu amalga ruxsatingiz yo'q",
    "parol_qisqa": "Parol kamida {{min}} ta belgidan iborat bo'lsin"
  }
}

Bunda kalit nuqta bilan bo'linadi: xato.email.band, xato.auth.parol_qisqa. Fayl kalibga aylanmaydi — chuqurroq daraxt tuziladi, bu 50+ kalitni boshqarishni osonlashtiradi.

I18nService'ning tarjima metodi ikki nom bilan mavjud — ular bir xil: translate() — to'liq nom, t() — qisqa taxallus (alias). Ikkalasi ham { lang, args } opsiyalarini oladi:

typescript
this.i18n.translate("xato.email.band");                          // to'liq
this.i18n.t("xato.email.band");                                  // qisqa (alias) — bir xil
this.i18n.t("xato.auth.parol_qisqa", { args: { min: 8 } });      // interpolyatsiya
this.i18n.t("email.subject", { lang: "ru" });                    // tilni majburlash

Nested kalitlar va t/translate: til fayllari til/fayl.json sxemasi bilan joylashadi; fayl nomi — kalitning namespace'i (xato.json xato.email.band). Kalitlarni guruhlab (nested obyekt) yozish tartibni saqlaydi. t() va translate() — bir xil metod (biri taxallus); { lang } bilan tilni majburlash mumkin (masalan cron ishchisida so'rov konteksti yo'q — 2.2). Til fayllari barcha tilda bir xil kalit tuzilishiga ega bo'lishi kerak — birida kalit yo'q bo'lsa, o'sha tilda fallback ishga tushadi 2.7-bob.

Interpolatsiya — {{o'zgaruvchi}} vs ICU {o'zgaruvchi}

Tarjima matniga dinamik qiymat (ism, son, narsa nomi) qo'yishni interpolatsiya deyiladi. nestjs-i18n'da ikki xil qavs sintaksisi bor va ular aralashtirilmasligi kerak:

json
// src/i18n/uz/umumiy.json — ikki xil qavs
{
  "salom": "Salom, {{ism}}!",                          // 1) ODATIY interpolatsiya — ikki qavs
  "qoldiq": "Sizda {count, plural, one {# ta} other {# ta}} xabar bor"  // 2) ICU — bitta qavs
}
  • {{ism}} (ikki jingalak qavs) — oddiy o'rniga qo'yish. Qiymat args orqali uzatiladi. Matn ichiga to'g'ridan-to'g'ri joylashadi. Aynan shu sintaksis xato/email kalitlarida ishlatiladi ({{narsa}}, {{min}}).
  • {ism} (bitta jingalak qavs)ICU MessageFormat sintaksisi. Faqat plural/select ichida ishlatiladi 2.8-bob; u yerda # — joriy son, {count, plural, ...} — bitta qavs. Buni oddiy o'rniga qo'yishda ishlatib bo'lmaydi.

Ikki holatda ham qiymat bir xilargs obyekti orqali uzatiladi:

typescript
this.i18n.t("umumiy.salom", { args: { ism: "Dilnoza" } });   //  "Salom, Dilnoza!"
this.i18n.t("umumiy.qoldiq", { args: { count: 3 } });        // ICU  "Sizda 3 ta xabar bor"

Interpolatsiya sintaksisi: oddiy o'rniga qo'yish — {{ism}} (ikki qavs, args bilan uzatiladi — xato/email matnlarida); ICU (plural/select) — {count, plural, ...} (bitta qavs, ichida # joriy son). Ikkisini aralashtirmang: oddiy matnda {{}}, ICU qoidalarida {}. Ikkalasiga ham qiymat t(kalit, { args: {...} }) orqali beriladi. Til fayldagi qavsni noto'g'ri yozish — eng ko'p uchraydigan i18n xatosi (qiymat o'rniga literal {{ism}} ko'rinadi).

2.4. Xato va validatsiya tarjimasi (8.5)

typescript
@Injectable()
export class UsersService {
  constructor(private i18n: I18nService) {}

  async yarat(dto: CreateUserDto) {
    const mavjud = await this.repo.findOneBy({ email: dto.email });
    if (mavjud) {
      throw new ConflictException(this.i18n.t("xato.email_band"));   // joriy tilda (2.3)
    }
    // ...
  }
}

// Validatsiya xabarlari ham (8.5 — class-validator i18n)
class CreateUserDto {
  @IsEmail({}, { message: i18nValidationMessage("xato.email_notogri") })
  email: string;
}

Xato/validatsiya tarjimasi: i18n.t("xato.email_band") — joriy tilda xato (8.5 — ConflictException). Validatsiya (class-validator) ham — i18nValidationMessage (DTO xabarlari ko'p tilda — 8.5). Foydalanuvchi o'z tilida xato ko'radi (inglizcha "Conflict" emas — o'zbekcha "Email band"). Bu — UX uchun muhim (foydalanuvchi xatoni tushunadi). Exception filter 8.6-bob bilan ham.

I18nValidationPipe va I18nValidationExceptionFilter — mexanizm

DTO validatsiyasi (class-validator, 8.5) main.ts'da global ValidationPipe bilan ishlaydi. i18n uchun uni I18nValidationPipe bilan almashtiramiz — u xato kalitlarini (message'dagi i18nValidationMessage) joriy tilga tarjima qiladi, so'ng I18nValidationExceptionFilter javobni chiroyli JSON'ga formatlaydi:

typescript
// main.ts
import { I18nValidationPipe, I18nValidationExceptionFilter } from "nestjs-i18n";

app.useGlobalPipes(new I18nValidationPipe());   // xato message kalitini tarjima
app.useGlobalFilters(
  new I18nValidationExceptionFilter({
    detailedErrors: false,   // false  sodda { property, errors: [...] } ; true  to'liq
  }),
);

i18nValidationMessage("xato.parol_qisqa", { min: 8 }) argumentlari class-validator konstraintidan (masalan @MinLength(8)) avtomatik uzatiladi — min qiymati til faylidagi {{min}} o'rniga qo'yiladi. Natija (?lang=uz):

json
{ "statusCode": 400,
  "message": [ { "property": "parol",
      "errors": ["Parol kamida 8 ta belgidan iborat bo'lsin"] } ] }

Validatsiya pipe/filter: I18nValidationPipe — DTO xato kalitini (i18nValidationMessage) joriy tilga tarjima qiladi; I18nValidationExceptionFilter — javobni formatlaydi (detailedErrors bilan batafsillik darajasi). Konstraint qiymatlari (@MinLength(8)'dagi 8) {{min}}'ga avtomatik uzatiladi. Bu ikkisi birga ishlatiladi va odatiy ValidationPipe'ni almashtiradi (aks holda xato kaliti tarjima qilinmay ketadi — foydalanuvchi xato.parol_qisqa'ni ko'radi).

Biznes xatolarni tarjima — exception filter (8.6)

Validatsiya emas, biznes exception'lari (ConflictException, NotFoundException — 8.6) uchun ikki yondashuv bor:

  1. Xatoni service'da tarjima qilib tashlash (Misol 3) — throw new ConflictException(this.i18n.t("xato.email.band")). Sodda, lekin har joyda i18n'ni inject qilish kerak.
  2. Kalitni tashlash, filter'da tarjima qilish — service faqat kalit tashlaydi (throw new ConflictException("xato.email.band")), global exception filter (8.6) I18nContext.current() orqali joriy tilni oladi va xabarni tarjima qiladi. Bu — DRY (bir joyda tarjima), lekin filter murakkabroq:
typescript
@Catch(HttpException)
export class I18nHttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    const i18n = I18nContext.current();                 // joriy til (2.2)
    const kalit = exception.message;                    // service tashlagan kalit
    const xabar = i18n?.t(kalit) ?? kalit;              // tarjima (yoki kalit — fallback)
    res.status(exception.getStatus()).json({ message: xabar });
  }
}

Biznes xato tarjimasi: ikki usul — (1) service'da darhol tarjima (i18n.t bilan throw — sodda, oshkora); (2) service kalit tashlaydi, filter (8.6) I18nContext.current() orqali tilni olib tarjima qiladi (DRY — bir markazda). 2-usul katta loyihada afzal (tarjima mantiqi bir joyda), lekin filter joriy tilni topolmasa (so'rovdan tashqari) kalitni o'zi qaytaradi — fallback 2.7-bob.

2.5. Dinamik kontent (DB ko'p til)

text
  DINAMIK KONTENT (mahsulot, maqola — DB'da ko'p til):

  USUL 1 — JSON ustun (moslashuvchan — tavsiya):
  @Column("jsonb") nom: { uz: "Telefon", ru: "Telefon", en: "Phone" }
   bir ustun, barcha til (oson qo'shish)

  USUL 2 — Til ustunlari (oddiy, lekin qat'iy):
  nom_uz, nom_ru, nom_en (har til alohida ustun)

  USUL 3 — Tarjima jadvali (normalizatsiya — ko'p til):
  products + product_translations (productId, lang, nom) — N til

   JSON (oz til, moslashuvchan); jadval (ko'p til, qidiruv)

Dinamik kontent (DB ko'p til): JSON ustun ({ uz, ru, en } — bir ustun, moslashuvchan, oson qo'shish — tavsiya oz til uchun); til ustunlari (nom_uz, nom_ru — oddiy, qat'iy); tarjima jadvali (product_translations — normalizatsiya — ko'p til, qidiruv oson — 6.9). Tanlov: oz til (3-4) JSON; ko'p til jadval. Mahsulot nomi/tavsifi, maqola — DB'da. Statik 2.3-bob bilan farqi: kontent o'zgaradi (admin kiritadi).

Uch strategiya — afzallik/kamchilik jadvali

text
  ┌──────────────┬──────────────────────┬──────────────────────────┐
  │ Strategiya   │ Afzallik             │ Kamchilik                │
  ├──────────────┼──────────────────────┼──────────────────────────┤
  │ 1. JSONB     │ • Bir ustun, bir     │ • Til bo'yicha qidiruv   │
  │   ustun      │   qator (JOIN yo'q)  │   qiyin (JSONB indeks)   │
  │ {uz,ru,en}   │ • Til qo'shish —     │ • Har qatorda barcha til │
  │              │   JSON kalit qo'shish│   (bo'sh til = bo'sh key) │
  │              │ • Migratsiya yengil  │ • ORM tip nazorati zaif  │
  ├──────────────┼──────────────────────┼──────────────────────────┤
  │ 2. Til       │ • Oddiy, tushunarli  │ • Yangi til = migratsiya │
  │   ustunlari  │ • Har ustunga indeks │   (nom_de, nom_fr ...)   │
  │ nom_uz,      │ • Tip qat'iy (ORM)   │ • Jadval kengayadi       │
  │ nom_ru,...   │ • SQL qidiruv oson   │ • Ko'p til = ko'p ustun  │
  ├──────────────┼──────────────────────┼──────────────────────────┤
  │ 3. Tarjima   │ • Cheksiz til (qator │ • Har o'qishda JOIN      │
  │   jadvali    │   qo'shish, sxema    │ • So'rov murakkabroq     │
  │ (entity_id,  │   o'zgarmaydi)       │ • N+1 xavfi 8.4-bob        │
  │  locale,     │ • Normalizatsiya     │ • Ko'proq kod            │
  │  field,val)  │ • Til bo'yicha indeks│                          │
  └──────────────┴──────────────────────┴──────────────────────────┘

  QOIDA:
  • 2-4 barqaror til, kontent kam qidiriladi    JSONB (1)
  • 2-4 til, tez SQL qidiruv/filtr shart        til ustunlari (2)
  • Til soni oshib boradi, cheksiz kengayish     tarjima jadvali (3)

Tarjima jadvali (3-usul) sxemasi — eng normalizatsiyalangan yechim:

typescript
@Entity()
export class ProductTranslation {
  @PrimaryGeneratedColumn("uuid") id: string;

  @ManyToOne(() => Product, (p) => p.translations, { onDelete: "CASCADE" })
  product: Product;

  @Index()                                     // til bo'yicha tez qidiruv
  @Column({ length: 5 }) locale: string;       // "uz" | "ru" | "en" | "de" ...

  @Column() nom: string;
  @Column("text") tavsif: string;

  // (product, locale) — noyob (bir til bir marta)
}
// Yangi til (nemis) qo'shish: yangi QATOR — sxema o'zgarmaydi, migratsiya yo'q.

DB strategiya tanlovi: JSONB — bir ustun/qator, JOIN yo'q, til qo'shish yengil, ammo til bo'yicha qidiruv/indeks zaif oz, barqaror til uchun ideal. Til ustunlari — oddiy, SQL qidiruv oson, tip qat'iy, ammo yangi til = migratsiya 2-4 til, filtr ko'p bo'lsa. Tarjima jadvali — cheksiz til (qator qo'shiladi, sxema o'zgarmaydi), normalizatsiya, ammo JOIN va N+1 xavfi (8.4) til soni o'sadigan loyihada. Ko'pchilik O'zbekiston ilovasi uz/ru/en bilan cheklanadi JSONB eng sodda va yetarli.

2.6. Format (sana, raqam, valyuta — Intl)

2.6. Format (sana, raqam, valyuta — Intl)

typescript
// Intl — til/mamlakatga qarab format (built-in — JS)
const narx = 1500000;
new Intl.NumberFormat("uz-UZ", { style: "currency", currency: "UZS" }).format(narx);
//  "1 500 000 so'm"
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(120);
//  "$120.00"

const sana = new Date();
new Intl.DateTimeFormat("uz-UZ").format(sana);        // "22.06.2026"
new Intl.DateTimeFormat("en-US").format(sana);        // "6/22/2026"

Format (sana/raqam/valyuta — Intl built-in): har til/mamlakatda boshqacha (1 500 000 so'm vs $120.00; 22.06.2026 vs 6/22/2026). Intl.NumberFormat (valyuta, raqam — ming ajratish), Intl.DateTimeFormat (sana). Server generatsiyada muhim (PDF chek — 8.21, email — 8.10, hisobot). Qo'lda formatlamaslik (Intl — to'g'ri, til-aware). Format — i18n'ning ko'p unutiladigan qismi.

2.7. Fallback (til topilmasa)

text
  FALLBACK — til/tarjima topilmasa, nima qilish:
  - Til qo'llab-quvvatlanmasa  default (o'zbek/ingliz)
  - Tarjima kaliti yo'q  fallback til (yoki kalit o'zi — debug)
  - Dinamik kontent: so'ralgan til yo'q  mavjud til (uz)

   HECH QACHON bo'sh/xato ko'rsatma (fallback har doim)
   "email_band" (kalit) ko'rinsa — tarjima yo'q (xato belgisi)

Fallback (til topilmasa — muhim): til qo'llab-quvvatlanmasa default; tarjima kaliti yo'q fallback til (yoki kalit o'zi — debug uchun); dinamik kontent so'ralgan tilda yo'q mavjud til. Hech qachon bo'sh/xato ko'rsatmaslik (foydalanuvchi har doim biror matn ko'rishi kerak). Kalit (email_band) ko'rinsa — tarjima yo'q (xato belgisi — tuzatish kerak). Fallback — ishonchli i18n (har holatda matn bor).

Fallback zanjiri — fallbacks map (uz ru en)

fallbackLanguage — bitta oxirgi tayanch (masalan uz). Ammo real hayotda oraliq tayanch kerak bo'ladi: rus tilida tarjima yo'q bo'lsa, inglizchaga o'tishdan oldin o'zbekchaga urinish mantiqiyroq (o'zbek foydalanuvchisi rus tilini tanlagan bo'lsa ham, o'zbekcha matnni tushunadi). Buning uchun nestjs-i18n fallbacks obyektini beradi — u til (yoki mintaqa) tayanch til mosligini belgilaydi:

typescript
I18nModule.forRoot({
  fallbackLanguage: "uz",                 // eng oxirgi tayanch (hamma narsa qulasa)
  fallbacks: {                            // zanjir: kalit topilmasa qaysi tilga o'tish
    "ru-*": "ru",                         // ru-RU, ru-KZ ...  ru
    "en-*": "en",                         // en-US, en-GB ...  en
    "uz-*": "uz",                         // uz-UZ, uz-Cyrl ...  uz
    ru: "uz",                             // rus tarjimasi yo'q  o'zbekchaga urinadi
    en: "uz",                             // ingliz yo'q  o'zbekchaga
  },
  loaderOptions: { path: "src/i18n/" },
});

Ikki vazifa bir vaqtda hal bo'ladi:

  • Mintaqa normalizatsiyasi: Accept-Language: ru-RU kelsa, ru-* ru faylini ishlatadi (har mintaqa uchun alohida fayl kerak emas).
  • Kalit-darajali tayanch: ru/xato.json'da email.band kaliti yo'q bo'lsa — ru: "uz" qoidasi bo'yicha uz/xato.json'dan olinadi (bo'sh ko'rsatilmaydi). Zanjir shunday davom etadi: ru uz fallbackLanguage.

Fallback zanjiri (fallbacks): fallbackLanguage — bitta oxirgi tayanch; fallbacks map — oraliq tayanch va mintaqa normalizatsiyasi. "ru-*": "ru" — mintaqa variantlarini (ru-RU, ru-KZ) bitta faylga yig'adi; ru: "uz" — rus tarjimasi topilmasa o'zbekchaga o'tadi (uz ru en emas, balki loyihaga mos zanjir: mahalliy auditoriya uchun uz oxirgi tayanch mantiqiyroq). Zanjir har kalit uchun alohida ishlaydi — bitta kalit yo'qligi butun tilni qulatmaydi, faqat o'sha kalit tayanch tildan olinadi.

2.8. Pluralization va kontekst (ko'plik)

text
  PLURALIZATION — ko'plik shakllari (til-bog'liq):
  Ingliz: "1 item" / "2 items" (2 shakl)
  Rus: "1 tovar" / "2 tovara" / "5 tovarov" (3 shakl — murakkab)
  O'zbek: "1 mahsulot" / "5 mahsulot" (oddiy — son + birlik)

  nestjs-i18n / ICU format:
  "{count, plural, one {# mahsulot} other {# mahsulot}}"

   Til qoidasiga mos (har tilda boshqa ko'plik)

Pluralization (ko'plik — til-bog'liq): har til boshqacha (ingliz — 2 shakl; rus — 3 shakl: 1/2/5 har xil; o'zbek — oddiy). Qo'lda if (count === 1) — yetmaydi (rus uchun murakkab). ICU format ({count, plural, one {...} other {...}}) — til qoidasiga mos. nestjs-i18n qo'llab-quvvatlaydi. Ko'plik — i18n'ning nozik qismi (oddiy konkatenatsiya xato). To'g'ri pluralization — tabiiy matn.

ICU plural — til fayllari va t() amalda

ICU MessageFormat kategoriyalari: zero, one, two, few, many, other — har til shu kategoriyalarning qismini ishlatadi (CLDR qoidalari asosida). Ingliz — one/other; rus — one/few/many/other; o'zbek — other (ko'plik shakl o'zgarmaydi, faqat son qo'shiladi). # belgisi — joriy sonning o'rni.

json
// src/i18n/en/umumiy.json — ingliz: 2 shakl (one / other)
{ "xabar": "{count, plural, one {# message} other {# messages}}" }

// src/i18n/ru/umumiy.json — rus: 3 muhim shakl (one / few / many)
{ "xabar": "{count, plural, one {# soobshenie} few {# soobsheniya} many {# soobsheniy} other {# soobsheniya}}" }

// src/i18n/uz/umumiy.json — o'zbek: shakl o'zgarmaydi (other yetarli)
{ "xabar": "{count, plural, other {# ta xabar}}" }

t() chaqiruvi — args ichida count:

typescript
this.i18n.t("umumiy.xabar", { lang: "ru", args: { count: 1 } });   //  "1 soobshenie"
this.i18n.t("umumiy.xabar", { lang: "ru", args: { count: 3 } });   //  "3 soobsheniya" (few)
this.i18n.t("umumiy.xabar", { lang: "ru", args: { count: 5 } });   //  "5 soobsheniy"  (many)
this.i18n.t("umumiy.xabar", { lang: "uz", args: { count: 5 } });   //  "5 ta xabar"

Rus qoidasi (nega 1/3/5 har xil): one — oxiri 1 (11 dan tashqari: 1, 21, 31); few — oxiri 2-4 (12-14 dan tashqari: 2, 3, 22); many — oxiri 0, 5-9 yoki 11-14 (5, 11, 25). ICU bu qoidani avtomatik qo'llaydi — siz faqat to'g'ri kategoriya matnini yozasiz, sonni tekshirmaysiz.

ICU plural amalda: til faylida {count, plural, KATEGORIYA {matn} ...} — har til o'z kategoriyalarini (one/few/many/other) beradi; # — joriy son. t(kalit, { args: { count } }) — nestjs-i18n CLDR qoidasi bo'yicha to'g'ri shaklni tanlaydi. Rus ko'pligini qo'lda (if) yozib bo'lmaydi (1 va 21 — one; 2 va 22 — few; 5 va 11 — many) — ICU'ga topshiring. O'zbek uchun other yetarli, lekin fayl tuzilishi bir xil bo'lishi uchun ICU sintaksisini baribir ishlating.

2.9. Best practices (i18n)

text
   Backend ham i18n (xato, email, kontent — faqat frontend emas — 2.1)
   Tilni aniqlash (parametr > profil > header > default — 2.2)
   Statik (fayl/nestjs-i18n) vs dinamik (DB — 2.3, 2.5)
   Tarjima kodda emas (fayl/DB — qo'shish oson — 2.3)
   Format Intl (sana/raqam/valyuta — 2.6)
   Fallback (til/kalit topilmasa — hech qachon bo'sh — 2.7)
   Pluralization (ICU — til qoidasi — 2.8)
   DB dizayn (JSON oz til; jadval ko'p til — 2.5)
   O'zbekiston: uz/ru/en (deyarli har ilova — 2.1)

2.10. Yangi til qo'shish — bosqichma-bosqich

Loyihaga to'rtinchi til (masalan qozoqkk) qo'shish jarayoni i18n arxitekturasi qanchalik yaxshi qurilganini sinaydi. To'g'ri qurilgan bo'lsa — bu deyarli kodsiz amal:

text
  YANGI TIL (kk) QO'SHISH:
  1. Til papkasi: src/i18n/kk/ — mavjud fayllarni (xato.json, email.json,
     umumiy.json) NUSXALAB, tarjimonga bering (kalitlar bir xil qoladi).
  2. Ruxsat etilgan tillar ro'yxatiga qo'shing:
     ["uz","ru","en","kk"] (til tanlash validatsiyasi — Misol 8).
  3. fallbacks map'ga 2.7-bob: "kk-*": "kk", kk: "uz" (tayanch).
  4. Format locale mapga 2.6-bob: { uz, ru, en, kk: "kk-KZ" }.
  5. Dinamik kontent (DB):
     - JSONB: nom.kk kalitini qo'shing (migratsiya YO'Q — 2.5).
     - Til ustunlari: nom_kk ustuni (migratsiya KERAK).
     - Tarjima jadvali: yangi QATORlar (locale="kk") — sxema o'zgarmaydi.

   Statik (fayl) + JSONB/jadval  kodsiz. Til ustunlari  migratsiya.

Kalitlarning to'liqligini tekshirish (yangi tilda unutilgan kalit — jimgina fallback'ga tushadi, sezilmaydi) uchun nestjs-i18n typesOutputPath bilan TypeScript tiplarini generatsiya qiladi yoki CI'da oddiy skript barcha til fayllari kalitlarini solishtiradi:

typescript
// forRoot — kalitlardan TS tip generatsiyasi (kompilyatsiyada yo'q kalit — xato)
I18nModule.forRoot({
  // ...
  typesOutputPath: path.join(__dirname, "../src/generated/i18n.generated.ts"),
});
//  i18n.t("xato.yoq_kalit") — TypeScript XATO beradi (kalit mavjud emas)

Yangi til qo'shish: to'g'ri qurilgan i18n'da bu kodsiz — (1) til papkasini nusxalab tarjima; (2) ruxsat ro'yxati; (3) fallbacks; (4) format locale; (5) DB — JSONB/jadvalda kodsiz, til ustunlarida migratsiya. typesOutputPath — kalitlardan TS tip generatsiya qiladi, unutilgan/xato kalit kompilyatsiyada ushlanadi (runtime fallback'ga tushib ketmaydi). Yangi til qo'shish osonligi — i18n dizayni sifatining o'lchovi.

2.11. Sana/valyuta chuqur va RTL (o'ngdan-chapga)

Format (2.6) Intl bilan hal bo'ladi, lekin uch nozik jihat bor. Birinchi — valyuta tilga emas, mamlakatga bog'liq: rus tilida narxni ko'rsatganda ham valyuta so'm (UZS) bo'lishi mumkin (O'zbekistondagi rus foydalanuvchi). Shuning uchun locale (matn tili) va currency (valyuta) alohida parametr:

typescript
// til = ko'rsatish tili; valyuta = biznes qarori (aralashtirmang)
new Intl.NumberFormat("ru-RU", { style: "currency", currency: "UZS" }).format(1500000);
//  "1 500 000 UZS" (ruscha ko'rinish, so'm valyutasi)

Ikkinchi — vaqt mintaqasi (timezone): server odatda UTC'da ishlaydi 8.16-bob, lekin foydalanuvchiga sanani uning mintaqasida ko'rsatish kerak:

typescript
new Intl.DateTimeFormat("uz-UZ", {
  timeZone: "Asia/Tashkent",                 // foydalanuvchi mintaqasi (UTC+5)
  dateStyle: "long", timeStyle: "short",
}).format(new Date());
//  "2026-yil 22-iyun, 14:30"

Uchinchi — RTL (right-to-left, o'ngdan-chapga): arab, urdu, ibroniy tillari o'ngdan chapga yoziladi. Backend uchun bu asosan metama'lumot vazifasi — matn yo'nalishini frontend hal qiladi, lekin backend har til uchun dir (direction) ni bilishi va PDF/email HTML'da (8.21, 8.10) dir="rtl" qo'yishi kerak:

typescript
const RTL_TILLAR = new Set(["ar", "he", "fa", "ur"]);
export function matnYonalishi(lang: string): "rtl" | "ltr" {
  return RTL_TILLAR.has(lang) ? "rtl" : "ltr";
}
// Email/PDF shabloniga: <html dir="{{dir}}"> — sana/narx joyi to'g'ri joylashadi

Format chuqur va RTL: valyutani tildan ajrating (ru-RU + UZS — ruscha ko'rinish, so'm); sanaga timeZone bering (server UTC foydalanuvchi mintaqasi — 8.16); RTL tillar (ar/he/fa/ur) o'ngdan chapga — backend dir ni aniqlab email/PDF HTML'ga (dir="rtl") qo'yadi. uz/ru/en — hammasi LTR, shuning uchun O'zbekiston loyihalarida RTL ko'pincha kerak emas, lekin xalqaro bozorga chiqsangiz (arab) — dir metama'lumotini oldindan tayyorlang.

2.12. i18n test qilish

i18n xatolari jimgina kechadi (noto'g'ri til yoki fallback — sin,ovsiz sezilmaydi), shuning uchun test muhim. Uch narsani sinang: (1) resolver to'g'ri tilni tanlaydimi; (2) tarjima kaliti hamma tilda bormi; (3) fallback ishlaydimi.

typescript
// 1) Resolver — ?lang / header to'g'ri tilni beradimi (e2e — 8.14)
it("?lang=ru  ruscha xato qaytaradi", async () => {
  const res = await request(app.getHttpServer())
    .post("/users").query({ lang: "ru" })
    .send({ email: "band@mail.uz" });      // mavjud email
  expect(res.body.message).toContain("uje zaregistrirovan");   // ruscha
});

// 2) Kalit to'liqligi — har til fayli bir xil kalitga egami (unit)
it("barcha til fayllari bir xil kalitga ega", () => {
  const uz = Object.keys(flatten(require("./i18n/uz/xato.json")));
  const ru = Object.keys(flatten(require("./i18n/ru/xato.json")));
  expect(ru).toEqual(uz);                  // ru'da yo'q kalit  test qulaydi
});

// 3) Fallback — qo'llab-quvvatlanmaydigan til  default
it("?lang=de (yo'q til)  o'zbekcha (fallback)", async () => {
  const res = await request(app.getHttpServer())
    .post("/users").query({ lang: "de" }).send({ email: "band@mail.uz" });
  expect(res.body.message).toContain("ro'yxatdan o'tgan");      // o'zbekcha (fallback)
});

i18n test: (1) resolver?lang=ru/header e2e 8.14-bob so'rovi to'g'ri tilda javob beradimi; (2) kalit to'liqligi — har til fayli bir xil kalit to'plamiga egami (unit — yo'q kalit test qulatadi); (3) fallback — qo'llab-quvvatlanmaydigan til default'ga tushadimi. Kalit to'liqligi testi eng qimmatli — u tarjimon unutgan kalitni CI'da ushlaydi (foydalanuvchi fallback tilni ko'rmasdan oldin). i18n testsiz — sifat sekin buziladi (yangi kalit qo'shiladi, faqat bir tilda).

2.13. Frontend bilan til sinxronizatsiyasi (11/12-QISM)

Backend va frontend i18n bir xil tilni ko'rsatishi shart — aks holda menyu inglizcha, xato xabari o'zbekcha bo'lib, chalkashlik chiqadi. Sinxronizatsiya yagona haqiqat manbai (single source of truth) orqali hal bo'ladi — foydalanuvchi profilidagi til 2.2-bob:

text
  TIL SINXRON (frontend  backend):
  1. Foydalanuvchi tilni tanlaydi (frontend til almashtirgich — 11-QISM).
  2. Frontend: (a) o'z UI'sini almashtiradi (i18next/react-intl);
     (b) backend'ga saqlaydi: PATCH /profile/language (Misol 8).
  3. Har API so'rovda frontend tilni yuboradi:
     - Header: Accept-Language: ru  YOKI  x-lang: ru (2.2 resolver);
     - yoki backend profil'dan oladi (UserLangResolver — 2.2).
  4. Backend javoblari (xato, kontent) SHU tilda qaytadi.

   Bitta manba: user.til (profil). Frontend UI + backend javob — mos.

Amaliy naqsh: frontend HTTP mijozida (axios/fetch) interseptor har so'rovga joriy tilni qo'yadi, backend esa HeaderResolver/UserLangResolver bilan o'qiydi:

typescript
// Frontend (11-QISM) — axios interceptor: har so'rovga joriy til
axios.interceptors.request.use((config) => {
  config.headers["x-lang"] = i18next.language;   // frontend joriy tili
  return config;
});
// Backend HeaderResolver(["x-lang"]) buni o'qiydi 2.2-bob  javob shu tilda

Frontend-backend til sinxron: yagona haqiqat manbai — user.til (profil, 2.2). Foydalanuvchi tilni tanlaganda: frontend o'z UI'sini almashtiradi (i18next — 11-QISM) va backend profilga saqlaydi (Misol 8); har API so'rovda til header (x-lang/Accept-Language) orqali yuboriladi — backend HeaderResolver/UserLangResolver bilan o'qiydi. Til bir joyda saqlanmasa (frontend alohida, backend alohida) — rassinxron: UI bir tilda, xato boshqa tilda. Interseptor (axios) har so'rovga tilni avtomatik qo'shadi — qo'lda unutish xavfini yo'qotadi.


3. Sintaksis — tez ma'lumotnoma

typescript
// nestjs-i18n 2.3-bob: I18nModule.forRoot({ fallbackLanguage, loaderOptions, resolvers })
// Tarjima 2.4-bob: i18n.t("xato.email_band", { args: { narsa: "Mijoz" } })
// Til aniqlash 2.2-bob: QueryResolver(["lang"]) / AcceptLanguageResolver / HeaderResolver

// Dinamik 2.5-bob: @Column("jsonb") nom: { uz, ru, en }
// Format 2.6-bob: new Intl.NumberFormat("uz-UZ", { style: "currency", currency: "UZS" })

4. Batafsil kod namunalari

Misol 1 — i18n setup (2.3)

typescript
// app.module.ts
import { I18nModule, QueryResolver, AcceptLanguageResolver, HeaderResolver } from "nestjs-i18n";
import * as path from "path";

@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: "uz",                         // default (2.7)
      loaderOptions: { path: path.join(__dirname, "/i18n/"), watch: true },
      resolvers: [                                    // prioritet tartibida (2.2)
        { use: QueryResolver, options: ["lang"] },    // ?lang=uz (eng yuqori)
        new HeaderResolver(["x-lang"]),               // X-Lang header
        AcceptLanguageResolver,                        // Accept-Language (brauzer)
      ],
    }),
  ],
})
export class AppModule {}

Misol 2 — Til fayllari (2.3)

json
// src/i18n/uz/xato.json
{
  "email_band": "Bu email allaqachon ro'yxatdan o'tgan",
  "topilmadi": "{{narsa}} topilmadi",
  "ruxsat_yoq": "Bu amalga ruxsatingiz yo'q",
  "parol_qisqa": "Parol kamida {{min}} ta belgidan iborat bo'lsin"
}
json
// src/i18n/ru/xato.json
{
  "email_band": "Etot email uje zaregistrirovan",
  "topilmadi": "{{narsa}} ne nayden",
  "ruxsat_yoq": "U vas net prav na eto deystvie",
  "parol_qisqa": "Parol doljen soderjat minimum {{min}} simvolov"
}
json
// src/i18n/en/xato.json
{
  "email_band": "This email is already registered",
  "topilmadi": "{{narsa}} not found",
  "ruxsat_yoq": "You don't have permission for this action",
  "parol_qisqa": "Password must be at least {{min}} characters"
}

Misol 3 — Xato tarjimasi (service — 2.4)

typescript
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private repo: Repository<User>,
    private i18n: I18nService,
  ) {}

  async yarat(dto: CreateUserDto) {
    const mavjud = await this.repo.findOneBy({ email: dto.email });
    if (mavjud) {
      throw new ConflictException(this.i18n.t("xato.email_band"));   // joriy til
    }
    return this.repo.save(dto);
  }

  async bitta(id: string) {
    const user = await this.repo.findOneBy({ id });
    if (!user) {
      throw new NotFoundException(
        this.i18n.t("xato.topilmadi", { args: { narsa: "Foydalanuvchi" } }),   // {{narsa}}
      );
    }
    return user;
  }
}
// ?lang=ru  "Etot email uje zaregistrirovan"; ?lang=en  "This email is already registered"

Misol 4 — Validatsiya i18n (8.5 — 2.4)

typescript
import { i18nValidationMessage } from "nestjs-i18n";

class CreateUserDto {
  @IsEmail({}, { message: i18nValidationMessage("xato.email_notogri") })
  email: string;

  @MinLength(8, { message: i18nValidationMessage("xato.parol_qisqa", { min: 8 }) })
  parol: string;
}

// main.ts — i18n validation pipe (8.5)
app.useGlobalPipes(new I18nValidationPipe());
app.useGlobalFilters(new I18nValidationExceptionFilter({ detailedErrors: false }));
//  validatsiya xatosi foydalanuvchi tilida

Misol 5 — Dinamik kontent JSON (DB — 2.5)

typescript
@Entity()
export class Product {
  @PrimaryGeneratedColumn("uuid") id: string;

  @Column("jsonb")                                    // ko'p til (JSON ustun — 2.5)
  nom: { uz: string; ru: string; en: string };

  @Column("jsonb")
  tavsif: { uz: string; ru: string; en: string };

  @Column("bigint") narx: number;
}

// Service — joriy tilda qaytarish (transformer)
@Injectable()
export class ProductsService {
  constructor(private i18n: I18nService) {}

  async bitta(id: string, lang: string) {
    const p = await this.repo.findOneBy({ id });
    return {
      id: p.id,
      nom: p.nom[lang] || p.nom.uz,                   // fallback (2.7)
      tavsif: p.tavsif[lang] || p.tavsif.uz,
      narx: this.formatNarx(p.narx, lang),            // format (2.6)
    };
  }
}

Misol 6 — Format (Intl — 2.6)

typescript
@Injectable()
export class FormatService {
  // Valyuta (til/mamlakatga qarab)
  narx(miqdor: number, lang: string): string {
    const locale = { uz: "uz-UZ", ru: "ru-RU", en: "en-US" }[lang] || "uz-UZ";
    const valyuta = lang === "en" ? "USD" : "UZS";
    return new Intl.NumberFormat(locale, { style: "currency", currency: valyuta }).format(miqdor);
  }

  // Sana
  sana(date: Date, lang: string): string {
    const locale = { uz: "uz-UZ", ru: "ru-RU", en: "en-US" }[lang] || "uz-UZ";
    return new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(date);
  }

  // Raqam (ming ajratish)
  raqam(n: number, lang: string): string {
    return new Intl.NumberFormat({ uz: "uz-UZ", ru: "ru-RU", en: "en-US" }[lang]).format(n);
  }
}
// narx(1500000, "uz")  "1 500 000 so'm"; sana  "22-iyun, 2026"

Misol 7 — Email i18n (8.10 — 2.1)

typescript
@Injectable()
export class MailService {
  constructor(private i18n: I18nService, private mailer: MailerService) {}

  async tasdiqlashYubor(user: User) {
    const lang = user.til || "uz";                    // foydalanuvchi tili (2.2)
    await this.mailer.sendMail({
      to: user.email,
      subject: this.i18n.t("email.tasdiqlash_subject", { lang }),   // tilga qarab
      template: `verification-${lang}`,               // til shabloni (uz/ru/en)
      context: {
        ism: user.ism,
        sana: this.formatService.sana(new Date(), lang),   // format (2.6)
      },
    });
  }
}
//  o'zbek user — o'zbekcha email; rus user — ruscha

Misol 8 — Foydalanuvchi til tanlovi (2.2)

typescript
// Foydalanuvchi tilini saqlash (profil — eng ishonchli manba)
@Entity()
export class User {
  @Column({ default: "uz" }) til: string;             // saqlangan tanlov
}

@Patch("profile/language")
@UseGuards(JwtAuthGuard)
async tilOzgartir(@Body() dto: { til: string }, @CurrentUser() user) {
  if (!["uz", "ru", "en"].includes(dto.til)) throw new BadRequestException();
  await this.usersService.tilSaqla(user.id, dto.til);
  return { til: dto.til };
}
//  keyingi email/push/xato shu tilda (profil — 2.2)

Misol 9 — Bot i18n (8.12 — to'liq backend i18n)

typescript
// Telegram bot — to'liq backend i18n (frontend yo'q — 8.12)
@Update()
export class BotUpdate {
  constructor(private i18n: I18nService, private botService: BotService) {}

  @Start()
  async start(@Ctx() ctx: Context) {
    const lang = await this.botService.userTili(ctx.from.id);   // saqlangan til
    await ctx.reply(
      this.i18n.t("bot.salom", { lang, args: { ism: ctx.from.first_name } }),
      this.botService.menyu(lang),                    // tugmalar ham tilda (8.12)
    );
  }

  @Action(/lang_(uz|ru|en)/)
  async tilTanla(@Ctx() ctx: Context) {
    const til = ctx.match[1];
    await this.botService.tilSaqla(ctx.from.id, til);
    await ctx.reply(this.i18n.t("bot.til_ozgartirildi", { lang: til }));
  }
}

Misol 10 — i18n modul (to'liq tuzilma)

text
  src/
  ├── i18n/                          (til fayllari — 2.3)
  │   ├── uz/ (xato.json, email.json, bot.json)
  │   ├── ru/ (...)
  │   └── en/ (...)
  ├── common/
  │   └── format.service.ts          (Intl — Misol 6)
  └── (har modul i18n.t ishlatadi)

  Oqim:
  - Til aniqlash (parametr/profil/header — 2.2)  resolver
  - Statik matn (xato/email)  i18n.t (fayl — 2.3, 2.4)
  - Dinamik kontent (mahsulot)  DB JSON [lang] 2.5-bob
  - Format (narx/sana)  Intl 2.6-bob
  - Fallback (til/kalit yo'q  default — 2.7)

   O'zbekiston: uz/ru/en; backend ham (xato/email/bot — 2.1)

5. To'g'ri va noto'g'ri holatlar

1) Faqat frontend i18n

text
 backend xato/email inglizcha (o'zbek user tushunmaydi — 2.1)
 backend ham i18n

2) Tarjima kodda (hardcode)

text
 if (lang === "uz") return "Email band" (qo'shish qiyin — 2.3)
 til fayllari (kalit)

3) Qo'lda format

text
 `${narx} so'm` (til/format e'tiborsiz — 2.6)
 Intl (til-aware)

4) Fallback yo'q

text
 til topilmasa  bo'sh/kalit ko'rinadi (2.7)
 fallback (default til)

5) Qo'lda pluralization

text
 if (n === 1) (rus uchun xato — 2.8)
 ICU plural

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Xato xabar noto'g'ri tilda

Sababi: til aniqlanmagan 2.2-bob. Yechimi: resolver (parametr/profil/header).

Xato 2 — Kalit ko'rinadi (tarjima yo'q)

Sababi: tarjima/fallback yo'q 2.7-bob. Yechimi: tarjima qo'sh; fallback.

Xato 3 — Format noto'g'ri (sana/narx)

Sababi: qo'lda format 2.6-bob. Yechimi: Intl.

Xato 4 — Dinamik kontent bir tilda

Sababi: DB ko'p til dizayn yo'q 2.5-bob. Yechimi: JSON/jadval.

Xato 5 — Email/push noto'g'ri tilda

Sababi: foydalanuvchi tili olinmagan 2.2-bob. Yechimi: user.til (profil).

Xato 6 — Rus ko'plik xato

Sababi: oddiy if 2.8-bob. Yechimi: ICU plural.


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi


8. Eng yaxshi amaliyotlar (best practices)

  • Backend ham i18n (xato, email, kontent — 2.1).
  • Tilni aniqlash (parametr > profil > header > default — 2.2).
  • Statik (fayl) vs dinamik (DB) (2.3, 2.5).
  • Tarjima kodda emas (fayl/DB — 2.3).
  • Format Intl (sana/raqam/valyuta — 2.6).
  • Fallback (hech qachon bo'sh — 2.7).
  • Pluralization (ICU — 2.8).
  • DB dizayn (JSON oz til; jadval ko'p til — 2.5).
  • Foydalanuvchi til tanlovi (profil — 2.2).
  • O'zbekiston: uz/ru/en 2.1-bob.

9. Amaliy loyiha: "Ko'p Tilli Backend"

i18n'ni amalda mustahkamlash.

Maqsad

To'liq ko'p tilli backend: xato, email, dinamik kontent, format — uz/ru/en.

Talablar (requirements)

  1. i18n setup: nestjs-i18n + resolverlar (Misol 1, 2.3).
  2. Til fayllari: uz/ru/en (xato/email — Misol 2).
  3. Xato tarjimasi: service (Misol 3, 2.4).
  4. Validatsiya i18n: DTO (Misol 4, 8.5).
  5. Dinamik kontent: DB JSON (Misol 5, 2.5).
  6. Format: Intl (narx/sana — Misol 6, 2.6).
  7. Email i18n: ko'p til (Misol 7, 8.10).
  8. Til tanlovi: profil (Misol 8, 2.2).
  9. Bot i18n: to'liq (Misol 9, 8.12).
  10. Fallback: til/kalit topilmasa 2.7-bob.

Maslahatlar (hint)

  • Resolver prioritet (2.2, 1-xato).
  • Tarjima fayl (2.3, 2-holat).
  • Intl format (2.6, 3-xato).
  • DB JSON dinamik (2.5, 4-xato).
  • Fallback (2.7, 2-xato).
  • user.til (2.2, 5-xato).

"Tayyor" mezonlari (acceptance criteria)

  • i18n setup.
  • Til fayllari (uz/ru/en).
  • Xato tarjimasi.
  • Validatsiya i18n.
  • Dinamik kontent.
  • Format (Intl).
  • Email i18n.
  • Til tanlovi.
  • Bot i18n.
  • Fallback.

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda backend i18n'ni o'rgandik:

  • Nega backend i18n (xato/email/kontent — 2.1); tilni aniqlash (parametr/profil/header — 2.2).
  • Statik tarjima (nestjs-i18n — fayl — 2.3, 2.4); dinamik kontent (DB ko'p til — 2.5).
  • Format (Intl — sana/raqam/valyuta — 2.6); fallback 2.7-bob; pluralization (ICU — 2.8).

8-QISM (NestJS) VA "REAL AMALIY MAVZULAR" TURKUMI TO'LIQ TUGADI! Endi siz backend'da hayotda uchraydigan deyarli har qanday loyihani qura olasiz: to'lov 8.19-bob, webhook 8.20-bob, hujjat 8.21-bob, qidiruv 8.22-bob, push 8.23-bob, rasm 8.24-bob, multi-tenancy 8.25-bob, audit 8.26-bob, versioning 8.27-bob, geo 8.28-bob, subscription 8.29-bob, i18n 8.30-bob — barchasi NestJS asosida.

Keyingi bob — 9.5: Monolith vs Microservices (9-QISM Arxitektura davomi). Backend amaliy mavzular tugadi; endi arxitektura yo'nalishida davom etamiz.


Foydalanilgan rasmiy/ishonchli manbalar

  • nestjs-i18n — kutubxona hujjati: I18nModule.forRoot, resolverlar (Query/Header/Cookie/AcceptLanguage), fallbacks map, I18nValidationPipe, i18nValidationMessage, typesOutputPath, ICU plural.
  • NestJS — rasmiy hujjat: modullar, DI, exception filter, ValidationPipe (i18n bilan integratsiya asosi).
  • MDN — IntlNumberFormat (valyuta, ming ajratish), DateTimeFormat (sana, timeZone), locale formatlash.
  • ICU MessageFormat (unicode-org) — plural/select sintaksisi va kategoriyalari (one/few/many/other).
  • Unicode CLDR — plural qoidalari (rus 1/2/5 farqi, o'zbek/ingliz ko'plik kategoriyalari).
  • class-validator — validatsiya konstraintlari (@IsEmail, @MinLength) va custom message (i18n kaliti bilan).
  • i18n/l10n best practices — fallback zanjiri, dinamik kontent strategiyalari (JSONB / til ustunlari / tarjima jadvali), RTL yo'nalishi.

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.30-bob: i18n (ko'p til) backend — Wisar