Концепция i18n в веб-разработке

i18n (internationalization) — процесс подготовки приложения к поддержке нескольких языков, регионов и культурных особенностей без изменения исходного кода. Число 18 обозначает количество букв между i и n в слове internationalization.

В веб-разработке интернационализация включает:

  • перевод текстового интерфейса;
  • поддержку различных форматов дат и времени;
  • локализацию чисел и валют;
  • обработку множественного числа;
  • поддержку направлений письма (LTR и RTL);
  • адаптацию единиц измерения;
  • форматирование списков и относительного времени.

i18n не равен переводу. Перевод — лишь часть локализации. Полноценная интернационализация затрагивает архитектуру приложения, структуру компонентов и работу с пользовательскими данными.


Разница между i18n и l10n

Internationalization (i18n)

Подготовка приложения к локализации:

  • отделение текста от логики;
  • внедрение системы сообщений;
  • поддержка разных форматов;
  • переключение локалей;
  • работа с ICU MessageFormat.

Localization (l10n)

Адаптация под конкретный язык или регион:

  • перевод строк;
  • изменение форматов;
  • адаптация валют;
  • культурные особенности интерфейса.

Пример:

Регион Формат даты Валюта
США 12/31/2026 USD
Германия 31.12.2026 EUR
Казахстан 31.12.2026 KZT

Почему интернационализация важна

Современные веб-приложения редко ограничиваются одной страной. Даже внутренние корпоративные системы часто используются международными командами.

Без i18n возникают проблемы:

  • строки жёстко встроены в код;
  • перевод невозможен без пересборки;
  • даты отображаются некорректно;
  • валюты форматируются неправильно;
  • множественное число работает только для английского;
  • интерфейс ломается при длинных переводах.

Интернационализация решает эти проблемы на уровне архитектуры.


Основные задачи i18n

Перевод интерфейса

Вместо:

<button>Save</button>

используется система сообщений:

<button>{intl.formatMessage({ id: 'button.save' })}</button>

Форматирование чисел

Разные страны используют разные разделители:

Локаль Число
en-US 1,234.56
de-DE 1.234,56
fr-FR 1 234,56

Пример:

new Intl.NumberFormat('de-DE').format(1234.56)

Результат:

1.234,56

Форматирование валют

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(1000)

Результат:

$1,000.00

Для Казахстана:

new Intl.NumberFormat('kk-KZ', {
  style: 'currency',
  currency: 'KZT'
}).format(1000)

Форматирование дат

new Intl.DateTimeFormat('ru-RU').format(new Date())

Пример результата:

28.05.2026

Множественное число

Английский:

1 file
2 files

Русский:

1 файл
2 файла
5 файлов

Правила множественного числа различаются между языками. Простая конкатенация строк не подходит.


Проблемы наивной локализации

Конкатенация строк

Плохой пример:

const text = count + ' files uploaded'

Для русского языка такая конструкция не работает.

Правильный подход:

intl.formatMessage(
  {
    id: 'files.uploaded',
    defaultMessage:
      '{count, plural, one {# файл} few {# файла} many {# файлов}}'
  },
  { count }
)

Жёстко зашитые строки

Плохой пример:

alert('User created')

Правильный:

alert(
  intl.formatMessage({
    id: 'user.created'
  })
)

Игнорирование локали браузера

Плохая практика:

const locale = 'en'

Лучше:

const locale = navigator.language

Стандарт Intl в Javascript

Основа большинства i18n-библиотек — встроенный API Intl.

Основные возможности:

API Назначение
Intl.NumberFormat Форматирование чисел
Intl.DateTimeFormat Форматирование дат
Intl.RelativeTimeFormat Относительное время
Intl.ListFormat Форматирование списков
Intl.PluralRules Правила множественного числа

Intl.NumberFormat

Форматирование чисел

const formatter = new Intl.NumberFormat('ru-RU')

formatter.format(1234567.89)

Результат:

1 234 567,89

Форматирование процентов

new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(0.56)

Результат:

56%

Intl.DateTimeFormat

const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
})

formatter.format(new Date())

Результат:

28 мая 2026 г.

Intl.RelativeTimeFormat

const rtf = new Intl.RelativeTimeFormat('ru', {
  numeric: 'auto'
})

rtf.format(-1, 'day')

Результат:

вчера

Intl.PluralRules

const plural = new Intl.PluralRules('ru-RU')

plural.select(5)

Результат:

many

Что такое FormatJS

FormatJS — набор библиотек для интернационализации Javascript-приложений.

Основные задачи:

  • перевод сообщений;
  • ICU MessageFormat;
  • работа с React;
  • форматирование дат и чисел;
  • pluralization;
  • extraction переводов;
  • поддержка TypeScript.

FormatJS построен поверх API Intl.


Основные пакеты FormatJS

Пакет Назначение
react-intl i18n для React
intl-messageformat ICU MessageFormat
@formatjs/cli CLI-инструменты
@formatjs/intl polyfill и утилиты

Архитектура локализации

Обычно структура выглядит так:

src/
 ├── locales/
 │    ├── en.json
 │    ├── ru.json
 │    └── kk.json
 │
 ├── i18n/
 │    └── config.js
 │
 └── components/

Сообщения и message descriptors

FormatJS использует descriptors:

{
  id: 'app.title',
  defaultMessage: 'Dashboard'
}

Поля descriptor

Поле Назначение
id уникальный идентификатор
defaultMessage текст по умолчанию
description описание для переводчика

ICU MessageFormat

FormatJS основан на стандарте ICU.

Пример:

Hello {name}

Подстановка:

intl.formatMessage(
  {
    id: 'greeting',
    defaultMessage: 'Hello {name}'
  },
  {
    name: 'John'
  }
)

Параметры сообщений

User {name} has {count} messages

Использование:

intl.formatMessage(
  {
    id: 'messages'
  },
  {
    name: 'Alex',
    count: 10
  }
)

Множественное число в ICU

{count, plural,
  one {# file}
  other {# files}
}

Для русского:

{count, plural,
  one {# файл}
  few {# файла}
  many {# файлов}
}

Select expressions

Позволяют выбирать текст по условию.

{gender, select,
  male {He}
  female {She}
  other {They}
}

Форматирование дат через ICU

Today is {date, date, short}

Форматирование чисел через ICU

Price: {price, number, ::currency/USD}

React и react-intl

react-intl — основной пакет FormatJS для React.

Установка:

npm install react-intl

IntlProvider

Главный провайдер локализации.

<IntlProvider locale="ru" messages={messages}>
  <App />
</IntlProvider>

FormattedMessage

Компонент для отображения перевода.

<FormattedMessage
  id="app.welcome"
  defaultMessage="Welcome"
/>

Передача параметров

<FormattedMessage
  id="user.greeting"
  defaultMessage="Hello, {name}"
  values={{
    name: 'Alex'
  }}
/>

Форматирование чисел в React

<FormattedNumber value={1000} />

Валюты

<FormattedNumber
  value={1000}
  style="currency"
  currency="USD"
/>

Форматирование дат

<FormattedDate
  value={new Date()}
  year="numeric"
  month="long"
  day="2-digit"
/>

Relative time

<FormattedRelativeTime
  value={-1}
  unit="day"
/>

Использование useIntl

Хук предоставляет доступ к API.

const intl = useIntl()

Пример:

const title = intl.formatMessage({
  id: 'dashboard.title'
})

defineMessages

Позволяет централизовать сообщения.

const messages = defineMessages({
  title: {
    id: 'app.title',
    defaultMessage: 'Dashboard'
  }
})

Хранение переводов

en.json

{
  "app.title": "Dashboard"
}

ru.json

{
  "app.title": "Панель управления"
}

Динамическая загрузка локалей

const messages = await import(`./locales/${locale}.json`)

Lazy loading переводов

Позволяет уменьшить размер bundle.

async function loadLocale(locale) {
  return import(`./locales/${locale}.json`)
}

Определение локали пользователя

Из браузера

navigator.language

Из массива языков

navigator.languages

Fallback locale

Если перевод отсутствует:

locale = 'ru'
fallback = 'en'

RTL-языки

Некоторые языки используют направление справа налево:

  • арабский;
  • иврит;
  • персидский.

Поддержка:

<html dir="rtl">

Проблемы длины текста

Переводы могут быть длиннее оригинала.

Пример:

Английский Немецкий
Save Änderungen speichern

Интерфейс должен учитывать это.


Извлечение сообщений

FormatJS CLI позволяет автоматически извлекать строки.

Пример:

formatjs extract "src/**/*.{js,jsx,ts,tsx}"

Компиляция переводов

formatjs compile-folder locales compiled-locales

Типичные ошибки

Использование строк вместо id

Плохо:

id: 'Save button text'

Хорошо:

id: 'button.save'

Отсутствие namespace

Плохо:

id: 'title'

Хорошо:

id: 'dashboard.title'

Конкатенация текста

Плохо:

'Hello ' + name

Хорошо:

Hello {name}

Производительность

i18n может влиять на производительность:

  • большие JSON-файлы;
  • лишние locale-data;
  • повторное форматирование;
  • отсутствие memoization.

Кэширование formatter-объектов

Плохой вариант:

new Intl.NumberFormat('ru-RU')

при каждом рендере.

Лучше:

const formatter = useMemo(
  () => new Intl.NumberFormat('ru-RU'),
  []
)

Polyfills

Старые браузеры могут не поддерживать часть API.

FormatJS предоставляет polyfills:

npm install @formatjs/intl-pluralrules

Server Side Rendering

При SSR локаль должна быть известна заранее.

Пример:

<IntlProvider
  locale={locale}
  messages={messages}
>
  <App />
</IntlProvider>

SEO и мультиязычность

Для SEO важно:

  • разные URL для языков;
  • корректный lang;
  • hreflang;
  • серверный рендеринг.

Пример:

<html lang="ru">

Стратегии хранения переводов

JSON

Самый распространённый вариант.

Преимущества:

  • простота;
  • совместимость;
  • удобство CI/CD.

CMS

Переводы хранятся во внешней системе:

  • Contentful;
  • Strapi;
  • Phrase;
  • Lokalise.

Namespace-подход

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

auth.login.title
auth.login.button
dashboard.header.title

Преимущества:

  • отсутствие конфликтов;
  • удобный поиск;
  • масштабируемость.

Версионирование переводов

При удалении сообщений появляются:

  • неиспользуемые ключи;
  • устаревшие переводы;
  • конфликты.

Обычно используются:

  • extraction;
  • автоматическая проверка;
  • CI-валидация.

Форматы локалей

Примеры:

Локаль Значение
en английский
en-US английский США
en-GB английский Великобритания
ru-RU русский
kk-KZ казахский Казахстан

BCP 47

Стандарт кодов локалей:

language-REGION

Пример:

kk-KZ

Переключение языка

Пример:

setLocale('ru')

Часто используется Context API:

<LocaleContext.Provider value={locale}>

Локализация маршрутов

Пример:

/ru/dashboard
/en/dashboard
/kk/dashboard

Форматирование списков

new Intl.ListFormat('ru').format([
  'JavaScript',
  'React',
  'FormatJS'
])

Результат:

JavaScript, React и FormatJS

Форматирование единиц измерения

new Intl.NumberFormat('ru', {
  style: 'unit',
  unit: 'kilometer'
}).format(10)

Тестирование локализации

Проверяются:

  • отсутствие пропущенных переводов;
  • корректность plural rules;
  • RTL;
  • длина текста;
  • fallback-механизмы.

Псевдолокализация

Техника искусственного удлинения текста.

Пример:

[Šávíñĝ…]

Помогает выявлять проблемы интерфейса.


Continuous Localization

Современные проекты автоматизируют процесс:

  1. extraction сообщений;
  2. отправка переводчикам;
  3. проверка CI;
  4. автоматическая доставка переводов.

Best Practices

Не хранить HTML внутри переводов

Плохо:

Click <b>here</b>

Использовать semantic ids

Хорошо:

profile.settings.title

Не дублировать сообщения

Плохо:

saveButtonText
saveLabel
saveCaption

Использовать defaultMessage

Это помогает:

  • разработке;
  • extraction;
  • fallback;
  • переводчикам.

Масштабирование i18n

В крупных приложениях появляются:

  • тысячи ключей;
  • десятки локалей;
  • команды переводчиков;
  • CI-интеграции;
  • автоматическая синхронизация.

Поэтому архитектура i18n должна проектироваться заранее.


Роль FormatJS в современной разработке

FormatJS стал стандартным решением для React-экосистемы благодаря:

  • соответствию стандартам ICU;
  • использованию Intl;
  • хорошей производительности;
  • поддержке TypeScript;
  • зрелой экосистеме;
  • гибкой архитектуре;
  • поддержке SSR;
  • удобной работе с pluralization и форматированием.