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-intlhandles caching and loading automatically viagetRequestConfig- 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:
- User preference (stored in JWT token / cookie) — highest priority
- URL parameter (
?lang=ru) — for explicit switching - Accept-Language header — browser preference
- Default —
uz(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
camelCasefor key segments - Prefix with namespace (
catalog,item,seller,admin,common,error) common.*for strings shared across multiple pageserror.*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 ComponentuseTranslations()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