---
title: Internationalization Patterns (uz, ru, en)
impact: HIGH
impactDescription: Ensures consistent multi-language support across the entire platform
tags: patterns, i18n, localization, nextjs, next-intl, translations
---
## Паспорт документа

- Статус документа: living standard
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении инженерной практики, CI/CD, архитектурных правил или локального workflow
- Область применения: внутренние rule/reference-card документы для инженерной команды
- Связанные документы:
  - [Индекс Agents](../README.md)
  - [Команды разработки](../commands.md)
  - [Инженерные принципы](../../governance/engineering-principles.md)

## Internationalization Patterns (uz, ru, en)

**Impact: HIGH**

The platform supports three languages: Uzbek (uz), Russian (ru), and English (en). All user-facing strings must be translated. Uzbek is the source/primary language.

---

### 1. Configuration — Single Source of Truth

All locale configuration lives in one root file:

```json
// i18n.json (project root)
{
  "version": "1.0",
  "locale": {
    "source": "uz",
    "targets": ["ru", "en"]
  }
}
```

This file is imported by the Next.js i18n config and drives all locale-related behavior. Never hardcode locale lists — always derive from `i18n.json`.

```typescript
// apps/web/i18n.config.ts
import i18nJson from '../../i18n.json';

export const i18nConfig = {
  defaultLocale: i18nJson.locale.source,
  locales: [i18nJson.locale.source, ...i18nJson.locale.targets],
};
```

---

### 2. Translation File Structure

```
apps/web/messages/
  uz.json     # Uzbek (source — always complete)
  ru.json     # Russian
  en.json     # English
```

**File format — flat key structure with namespace prefixes:**

```json
{
  "catalog.title": "Barcha kurslar",
  "catalog.filters.subject": "Fan",
  "catalog.filters.location": "Joylashuv",
  "catalog.filters.price": "Narx",
  "catalog.filters.type": "Turi",
  "catalog.noResults": "Hech narsa topilmadi",
  "catalog.loadMore": "Ko'proq ko'rsatish",
  "item.leaveRequest": "So'rov qoldirish",
  "item.reviews": "Sharhlar",
  "item.similar": "O'xshash kurslar",
  "common.save": "Saqlash",
  "common.cancel": "Bekor qilish",
  "common.delete": "O'chirish",
  "common.loading": "Yuklanmoqda...",
  "common.error": "Xatolik yuz berdi"
}
```

**Alternative — nested structure (if using next-intl namespaces):**

```json
{
  "catalog": {
    "title": "Barcha kurslar",
    "filters": {
      "subject": "Fan",
      "location": "Joylashuv"
    }
  }
}
```

Pick one format and use it consistently. Both work with next-intl.

---

### 3. Interpolation and Pluralization

Use ICU message format for dynamic values:

```json
{
  "catalog.resultsCount": "{count, plural, one {# kurs topildi} other {# ta kurs topildi}}",
  "item.price": "Narxi: {price} so'm",
  "seller.rating": "Reyting: {rating}/5 ({reviewCount, plural, one {# sharh} other {# ta sharh}})",
  "common.greeting": "Salom, {name}!"
}
```

```typescript
// Usage in component
t('catalog.resultsCount', { count: items.length })
// → "3 ta kurs topildi"

t('item.price', { price: '500 000' })
// → "Narxi: 500 000 so'm"
```

---

### 4. Server-Side Translation Loading

Load translations on the server for SSR pages. Cache translation files to avoid re-reading on every request.

```typescript
// apps/web/lib/i18n-server.ts
// Uses next-intl's built-in getTranslations() for Server Components.
// This helper is only needed if you require manual translation loading
// outside of next-intl's standard flow.

import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
}));
```

For most cases, use `getTranslations()` from `next-intl/server` directly in Server Components — no manual loading needed.

**Loading rules:**
- `next-intl` handles caching and loading automatically via `getRequestConfig`
- All locales fall back to Uzbek — missing keys show Uzbek text, not raw keys
- Translations change only on deploy — no runtime reloading needed

---

### 5. Locale Detection

Detect user's preferred language in this priority order:

1. **User preference** (stored in JWT token / cookie) — highest priority
2. **URL parameter** (`?lang=ru`) — for explicit switching
3. **Accept-Language header** — browser preference
4. **Default** — `uz` (Uzbek)

```typescript
// apps/web/lib/get-locale.ts
import { cookies } from 'next/headers';
import { i18nConfig } from './i18n.config';

export async function getLocale(): Promise<string> {
  const cookieStore = await cookies();

  // 1. User's saved preference
  const savedLocale = cookieStore.get('locale')?.value;
  if (savedLocale && i18nConfig.locales.includes(savedLocale)) {
    return savedLocale;
  }

  // 2. Accept-Language header (handled by next-intl middleware)
  // 3. Default
  return i18nConfig.defaultLocale;
}
```

---

### 6. Usage in Components

#### Server Component (preferred — no JS bundle impact)

```typescript
// app/page.tsx (Server Component)
import { getTranslations } from 'next-intl/server';

export default async function CatalogPage() {
  const t = await getTranslations('catalog');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('resultsCount', { count: 42 })}</p>
    </div>
  );
}
```

#### Client Component (only when interactivity is needed)

```typescript
// components/FilterSidebar.tsx
"use client";
import { useTranslations } from 'next-intl';

export function FilterSidebar() {
  const t = useTranslations('catalog.filters');

  return (
    <div>
      <label>{t('subject')}</label>
      <label>{t('location')}</label>
    </div>
  );
}
```

---

### 7. Right-to-Left (RTL) Support

Not needed for uz/ru/en, but set `dir` attribute correctly on `<html>` for future-proofing:

```typescript
// app/layout.tsx
export default async function RootLayout({ children }) {
  const locale = await getLocale();
  return (
    <html lang={locale} dir="ltr">
      <body>{children}</body>
    </html>
  );
}
```

---

### 8. Translation Key Naming Convention

```
{namespace}.{feature}.{element}
```

Examples:
```
catalog.title                    # Page title
catalog.filters.subject          # Filter label
catalog.noResults                # Empty state
item.leaveRequest                # CTA button
seller.dashboard.title           # Dashboard heading
admin.moderation.approve         # Admin action
common.save                      # Shared UI element
common.cancel                    # Shared UI element
error.networkError               # Error message
error.validationFailed           # Error message
```

**Rules:**
- Use `camelCase` for key segments
- Prefix with namespace (`catalog`, `item`, `seller`, `admin`, `common`, `error`)
- `common.*` for strings shared across multiple pages
- `error.*` for error messages

---

### 9. Rules Summary

**Always:**
- Use translation keys for ALL user-facing strings — never hardcode text
- Add translations to ALL three locale files (uz, ru, en) when adding new strings
- Use Server Component `getTranslations()` over Client Component `useTranslations()` when possible
- Use ICU message format for pluralization and interpolation
- Merge with Uzbek fallback so missing keys show Uzbek text, not raw keys
- Cache translations on the server

**Never:**
- Hardcode user-facing strings in components
- Use template literals for translated strings (`t('greeting') + name` — use interpolation instead)
- Add keys to only one locale file — always add to all three
- Skip pluralization for countable nouns (Uzbek and Russian have different plural rules)
- Store UI translations in the database — they belong in JSON files

**Content vs UI translations:**
- **UI strings** (buttons, labels, headings, error messages) → translation files, always translated
- **Content** (item titles, descriptions, reviews) → database, in the seller's/user's language, NOT translated via i18n system
