Перейти к основному содержимому

i18n & Localization

Цели

  • Единые правила для ключей, namespaces и структуры переводов.
  • Предсказуемая загрузка переводов в SSR/SSG (Next.js / Astro).
  • Минимум “магии”: понятные фолбэки, контроль missing keys.
  • Корректные plural, интерполяция и форматирование дат/чисел/валют.

Организция ключей

MUST

  • Ключи стабильные и не зависят от конкретного текста на языке.
  • Формат ключей: lowercase + разделитель . (точка):
    profile.settings.title
  • Не использовать пробелы, camelCase, кириллицу, длинные предложения как ключи.

SHOULD

  • Ключи отражают структуру UI/домена:
    • common.* - общие слова/кнопки
    • errors.* - ошибки
    • validation.* - сообщения валидации
    • feature.<name> или pages.<route> — фича/страница

Примеры

✅ Хорошо
{
"common": {
"save": "Save"
},
"profile": {
"settings": {
"title": "Settings"
}
},
"errors": {
"network_error": "Network error"
}
}

❌ Плохо
{
"Save button": "Save",
"PROFILE_SETTINGS_TITLE": "Settings",
"Настройки": "Settings"
}

Стратегии Namespace'ов

MUST

  • Переводы делятся на namespaces по доменам/фичам, чтобы:
    • Не раздувать один огромный словарь
    • Грузить только нужное
    • Избегать конфликтов ключей

Рекомендуемая схема

  • common - общие слова и простые действия (ok/cancel/save)
  • errors - универсальные ошибки
  • validation - сообщения валидации
  • feature.<name> или pages.<route> — фича/страница

Пример структуры

  • common.json
  • errors.json
  • validation.json
  • pages/settings.json (для страницы настроек)
i18n/
locales/
en/
_meta.json # мета-информация (версия, дата обновления, статус перевода)
common.json # кнопки/короткие действия/универсальные слова
errors.json # универсальные ошибки и сетевые проблемы
validation.json # сообщения валидатора
ui.json # тексты UI-kit (пустые состояния, generic labels)
navigation.json # меню, хлебные крошки, названия разделов
pages/ # тексты страниц
home.json
settings.json
profile.json
entities/ # статусы/типы/лейблы сущностей
user.json
order.json
subscription.json
domains/ # если домен большой и живет долго
billing.json
notifications.json
analytics.json
emails/ # тексты email-шаблонов, если есть
welcome.json
password-reset.json
en.ts # сборка всех namespace'ов в один объект
es/
...

Внутри namespace ключи остаются читабельными и без избыточной глубины. Не делайте 6–7 уровней вложенности.


Множественное число и интерполяция (Plurals & interpolation)

MUST

  • Плюралы делать через механизм i18n-библиотеки, а не через ручные if.
  • Не склеивать строки через + и не собирать предложения из кусочков (ломает грамматику).
✅ Хорошо
t('common.items_count', { count: items.length })

❌ Плохо
'${count} items'
t('common.items') + ': ' + count

Правила интерполяции

  • Используйте именованные параметры: {{ name }}, {{ count }}.
  • Значения для интерполяции не должны быть HTML/React-элементами, если библиотека не поддерживает безопасное форматирование.
  • Для "жирного текста/ссылок" используйте официальные механизмы форматирования библиотеки (если есть), а не HTML в переводах.

Форматирование dates/numbers/currency

MUST

  • Даты/время/числа/валюта форматируются через Intl.* (или внутреннюю обертку), а не вручную.
  • Формат зависит от locale, а не от "как привыкли".

Рекомендуемые утилиты (через обертку проекта)

  • formatDate(date, locale, options)
  • formatNumber(value, locale, options)
  • formatCurrency(value, locale, currency)

Примеры (Intl)

✅ Хорошо
new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value)
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)

❌ Плохо
date.toLocaleString('en-US') // захардкожено
amount.toFixed(2) + ' €'
`${day}.${month}.${year}` // ручная сборка

Обработка отсутствующих ключей (Missing keys)

MUST

  • Missing key - это ошибка качества, а не "нормально".
  • development missing keys должны быть заметны:
    • лог/ошибка в консоль
    • тест/линт, который падает
    • или визуальный маркер вроде MISSING_KEY

SHOULD

  • В production:
    • показываем fallback (например default locale)
    • но собираем telemetry (Sentry/logging) для missing keys

Policy (пример)

  • defaultLocale: en (или выбранный в проекте)
  • fallback chain: userLocale -> defaultLocale
  • отображение при missing key: key или MISSING_KEY - только в dev (в prod избегать "мусора" в UI).

Пример

// i18n/t.ts
import { useTranslations } from 'next-intl';

export function useT(namespace?: string) {
const t = useTranslations(namespace);

return (key: string, values?: Record<string, any>) => {
try {
return t(key, values);
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.error('[i18n missing key]', namespace ? `${namespace}.${key}` : key, e);
return `__MISSING: ${namespace ? `${namespace}.${key}` : key}__`;
}
// Production: безопасный fallback
return '';
}
};
}

Загрузка переводов с помощь SSG/SSR (Next.js / Astro)

Общий принцип

Переводы должны быть доступны до рендера страницы (SSR/SSG), чтобы избежать:

  • "мигания" текста
  • лишних запросов с клиента

Next.js

MUST

  1. Указать поддерживаемые locales и default locale в конфиге приложения.
  2. На сервере: загрузка нужных namespaces для текущего route.
    • Использовать getStaticProps / getServerSideProps для передачи переводов в страницу.
  3. На клиенте: не делать lazy-fetch переводов для критического UI (кроме редких, оправданных кейсов).

SHOULD

  • Поддерживать "route-based namespaces":
    • страница -> список namespaces, которые ей нужны.

Пример (Next.js)

// pages/profile.tsx (пример для Pages Router + next-intl + getServerSideProps)
// Важно: ниже показан корректный паттерн, SSR грузит словарь, затем "режет" его до нужных namespaces.

import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import { pick } from 'lodash-es'; // или ваша кастомная утилита pick (см. комментарий ниже)

/**
* pick - это может быть:
* 1) lodash-es: import { pick } from 'lodash-es'
* 2) кастомная утилита проекта (например "@/shared/lib/pick") с тем же поведением:
* - pick(object, keys[]) -> returns new object with only selected keys
*/

interface PageProps {
withoutFooterOnDesktop: boolean;
messages: Record<string, unknown>;
};

export const getServerSideProps: GetServerSideProps<PageProps> = async (ctx) => {
const locale = ctx.locale ?? ctx.defaultLocale ?? 'en';

// Явно фиксируем какие "части переводов" нужны странице.
// Обычно это namespaces: common/errors + page/feature namespaces.
const messageNamespaces = ['Layout', 'ProfilePage'] as const;

/**
* next-intl ожидает объект в качестве `messages`.
* Мы импортируем общий словарь локали и выбираем нужные namespaces.
* Путь импорта - нужно подстроить под ваш репозиторий:
* - "src/..." может не работать в рантайме SSR без настроенного alias.
* - Предпочтительнее использовать alias типа "@/..." или относительный путь.
*/
const allMessagesModule = await import(`src/fsd-app/intl/${locale}/${locale}.ts`);
const allMessages = allMessagesModule.default as Record<string, unknown>;
// В pickedMessages будет объект вида:
// { ProfilePage: {...}, Layout: {...} }
const pickedMessages = pick(allMessages, messageNamespaces as unknown as string[]);

return {
props: {
messages: pickedMessages
// ...другие пропсы страницы
}
};
};

Astro (SSG)

MUST

  • Генерировать страницы для каждой локали (если локаль в URL/роутинге).
  • Переводы должны быть доступны на build-time:
    • импорт JSON/TS словарей
    • или сборка словаря в процессе build

SHOULD

  • Делать split по namespaces и подключать только нужные для конкретной страницы.

TypeScript (next-intl) — типобезопасные locales, keys, аргументы и formats

Этот подраздел фиксирует рекомендованный workflow типизации для next-intl, чтобы получить:

  • автокомплит по locale
  • типобезопасные namespaces и keys
  • типобезопасные аргументы интерполяции (optional, experimental)
  • типобезопасные имена глобальных formats

Источник: документация next-intl “TypeScript augmentation”. next-intl.dev

1) Type augmentation через AppConfig (обязательная база)

Создаём декларацию (обычно global.d.ts или global.ts) и расширяем модуль next-intl:

// global.d.ts (или global.ts)
// Важно: файл должен попадать в include вашего tsconfig.
import { routing } from '@/i18n/routing';
import { formats } from '@/i18n/request';
import messages from '@/i18n/locales/en/common.json'; // или единый messages/en.json (см. выше)

declare module 'next-intl' {
interface AppConfig {
// 1) Locale: типизируем доступные локали
Locale: (typeof routing.locales)[number];
// 2) Messages: типизируем структуру переводов (ключи/неймспейсы)
Messages: typeof messages;
// 3) Formats: типизируем глобальные форматы (date/number/list)
Formats: typeof formats;
}
}

2) Типобезопасные namespaces и keys

Если Messages типизированы, то:

  • useTranslations('About') валидирует namespace
  • t('title') валидирует ключ внутри namespace

Пример

// messages/en.json
{
"About": {
"title": "Hello"
}
}

function About() {
const t = useTranslations('About');

// unknown key (будет TS error)
t('description');

// okay
t('title');
}

Content guidelines

MUST

  • Любой UI-текст - через t(), без хардкода в компонентах.
  • Избегать длинных строк в кнопках/табах (ломают верстку).
  • Не хранить HTML в переводах без крайней необходимости.

SHOULD

  • Термины/названия функций продукта — через глоссарий (единообразно по всему приложению).

MAY

  • Добавлять description/комментарии к ключам (если библиотека/процесс переводов это поддерживает).

Процедуры контроля (Code Review)

  • Новый UI-текст добавлен через t(), нет хардкода.
  • Для новой фичи/страницы выбран корректный namespace.
  • Нет склейки строк и ручных плюралов.
  • Даты/числа/валюта форматируются через Intl или утилиты проекта.
  • Missing keys заметны в dev и отслеживаются в prod.

TODO

  • Добавить способы и рекомендации по работе с методами t.raw, t.rich, t.markup