Internationalization — next-intl bilan ko'p tilli ilova

~20 daq · Next.js · Ilg'or mavzular

Muammodan boshlaylik

Do'kon o'zbek tilida. Endi rus va ingliz tillari ham kerak. Talablar:

  • Har bir til — o'z URL i: /uz/katalog, /ru/katalog, /en/katalog — Google har birini alohida indekslashi uchun
  • Matnlar Server Component larda ham tarjima qilinsin (JavaScript siz)
  • Sana, son, valyuta — har tilning o'z formatida
  • Foydalanuvchi birinchi kirganda — brauzer tiliga qarab yo'naltirish

React SPA uchun react-i18next bor, lekin App Router, Server Component va middleware bilan next-intl tabiiyroq ishlaydi.


Hayotiy o'xshatma

Ko'p tilli muzey. Har bir zal (sahifa) bir xil, lekin kirishda tilni tanlaysiz — va barcha yozuvlar, audio-gid, xaritalar shu tilda. Har bir til uchun alohida kirish eshigi (/uz, /ru) — chet ellik turist darhol o'z tilidagi eshikni topadi.


Tuzilma

npm install next-intl
messages/
├── uz.json
├── ru.json
└── en.json
i18n/
├── routing.ts
├── request.ts
└── navigation.ts
middleware.ts
app/
└── [locale]/
    ├── layout.tsx
    └── page.tsx

Barcha sahifalar app/[locale]/ ichida — til URL ning birinchi segmenti.

Tarjimalar

// messages/uz.json
{
  "Bosh": {
    "sarlavha": "Xush kelibsiz!",
    "salom": "Salom, {ism}!"
  },
  "Savat": {
    "soni": "{count, plural, =0 {Savat bo'sh} other {Savatda # ta mahsulot}}",
    "jami": "Jami: {summa, number} so'm"
  }
}
// messages/en.json
{
  "Bosh": {
    "sarlavha": "Welcome!",
    "salom": "Hello, {ism}!"
  },
  "Savat": {
    "soni": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}",
    "jami": "Total: {summa, number} UZS"
  }
}

ICU formati: {ism} — o'zgaruvchi, plural — ko'plik shakllari (har tilda o'z qoidalari: inglizchada one/other, ruschada one/few/many/other), number — mahalliy son formati.


Sozlash

// i18n/routing.ts
import { defineRouting } from 'next-intl/routing'

export const routing = defineRouting({
  locales: ['uz', 'ru', 'en'],
  defaultLocale: 'uz',
})
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { hasLocale } from 'next-intl'
import { routing } from './routing'

export default getRequestConfig(async ({ requestLocale }) => {
  const soralgan = await requestLocale
  const locale = hasLocale(routing.locales, soralgan) ? soralgan : routing.defaultLocale

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  }
})
// i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'

export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing)
// middleware.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'

export default createMiddleware(routing)

export const config = {
  matcher: '/((?!api|_next|_vercel|.*\\..*).*)',
}
// next.config.ts
import createNextIntlPlugin from 'next-intl/plugin'

const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
export default withNextIntl({})

Middleware: /katalog → brauzer tili (Accept-Language) yoki cookie bo'yicha /uz/katalog ga yo'naltiradi.


Layout

// app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from 'next-intl'
import { setRequestLocale } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { routing } from '@/i18n/routing'

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }))
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ locale: string }>
}) {
  const { locale } = await params
  if (!hasLocale(routing.locales, locale)) notFound()
  setRequestLocale(locale)

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  )
}
  • <html lang={locale}> — ekran o'quvchilar va SEO uchun
  • generateStaticParams + setRequestLocale — sahifalar har bir til uchun statik render bo'lishi mumkin

Server Component da

// app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server'

export default async function Bosh({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params
  setRequestLocale(locale)
  const t = await getTranslations('Bosh')

  return <h1>{t('sarlavha')}</h1>
}

Client Component da

'use client'

import { useTranslations, useFormatter } from 'next-intl'

export function SavatXulosa({ soni, summa }: { soni: number; summa: number }) {
  const t = useTranslations('Savat')
  const format = useFormatter()

  return (
    <div>
      <p>{t('soni', { count: soni })}</p>
      <p>{t('jami', { summa })}</p>
      <time>{format.dateTime(new Date(), { dateStyle: 'long' })}</time>
    </div>
  )
}

Til almashtirish va havolalar

'use client'

import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/navigation'

export function TilTanlash() {
  const locale = useLocale()
  const pathname = usePathname()          // tilsiz yo'l: '/katalog'
  const router = useRouter()

  return (
    <select value={locale} onChange={(e) => router.replace(pathname, { locale: e.target.value })} aria-label="Til">
      <option value="uz">O'zbekcha</option>
      <option value="ru">Ruscha</option>
      <option value="en">English</option>
    </select>
  )
}
import { Link } from '@/i18n/navigation'

<Link href="/katalog">...</Link>     // avtomatik /uz/katalog yoki /ru/katalog

[!WARNING] Ichki havolalar uchun next/link emas, @/i18n/navigation dagi Link ni ishlating. Aks holda havola joriy til prefiksini yo'qotadi va har bosishda middleware qayta yo'naltiradi.


SEO

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Bosh' })

  return {
    title: t('sarlavha'),
    alternates: {
      canonical: `https://dokon.uz/${locale}`,
      languages: { uz: 'https://dokon.uz/uz', ru: 'https://dokon.uz/ru', en: 'https://dokon.uz/en' },
    },
  }
}

alternates.languages — hreflang teglari: Google har bir til versiyasini to'g'ri auditoriyaga ko'rsatadi.

[!TIP] Ma'lumotlar bazasidagi kontent (mahsulot nomi va tavsifi) ham tarjima talab qiladi — JSON fayllar faqat interfeys matnlari uchun. Mahsulotlar uchun: nomi: { uz, ru, en } kabi maydonlar yoki alohida tarjimalar jadvali.


Tez-tez uchraydigan xatolar

1. Sahifalar app/[locale] dan tashqarida

Til segmenti yo'q — middleware yo'naltiradi, sahifa topilmaydi.

2. Tarjima kaliti faqat bitta faylda

ru.json da kalit yo'q — ekranda Savat.jami kabi kalit nomi chiqadi. TypeScript bilan kalitlarni tekshirish yoki tarjimalarni sinxronlash vositalaridan foydalaning.

3. Matnlarni bo'laklab birlashtirish

t('salom') + ism + '!' — har tilda so'z tartibi boshqa. O'zgaruvchilar bilan: t('salom', { ism }).


Xulosa

next-intl: messages/{til}.json, ICU formati (o'zgaruvchi, plural, number)
i18n/routing.ts — tillar; request.ts — tarjimalarni yuklash; navigation.ts — Link, useRouter
middleware — til bo'yicha yo'naltirish; app/[locale]/ — barcha sahifalar
Server: getTranslations + setRequestLocale; Client: useTranslations, useFormatter
<html lang={locale}>; Link — @/i18n/navigation dan
SEO: alternates.languages (hreflang)
Bazadagi kontent — alohida tarjima maydonlari

Keyingi dars

71-dars: next.config.js — redirects, rewrites, headers, env

Next.js ning asosiy konfiguratsiya fayli va eng foydali sozlamalari.