Qadam Roadmap
проектdocs/Agents/rules/patterns-i18n.md

patterns-i18n.md

Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев


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 документы для инженерной команды
  • Связанные документы:

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:

// 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.

// 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:

{
  "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):

{
  "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:

{
  "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}!"
}
// 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.

// 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. Defaultuz (Uzbek)
// 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)

// 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)

// 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:

// 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