---
createdAt: 2025-09-10
updatedAt: 2025-09-10
title: i18n на уровне компонента против централизованного: новый подход с Intlayer
description: Глубокое погружение в стратегии интернационализации в React, сравнение централизованного, по-ключевого и по-компонентного подходов, и представление Intlayer.
keywords:
  - i18n
  - React
  - Интернационализация
  - Intlayer
  - Оптимизация
  - Размер бандла
slugs:
  - blog
  - per-component-vs-centralized-i18n
---

# По-компонентный vs централизованный i18n

Подход по компонентам — не новое понятие. Например, в экосистеме Vue `vue-i18n` поддерживает [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt также предлагает [переводы на уровне компонента](https://i18n.nuxtjs.org/docs/guide/per-component-translations), а Angular использует похожий паттерн через свои [Feature Modules](https://v17.angular.io/guide/feature-modules).

Даже в Flutter-приложении часто встречается следующий паттерн:

```bash
lib/
└── features/
    └── login/
        ├── login_screen.dart
        └── login_screen.i18n.dart  # <- Переводы находятся здесь
```

```dart fileName='lib/features/login/login_screen.i18n.dart'
import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {
  static var _t = Translations.byText("en") +
      {
        "Hello": {
          "en": "Hello",
          "fr": "Bonjour",
        },
      };

  String get i18n => localize(this, _t);
}
```

Однако в мире React мы в основном видим разные подходы, которые я сгруппирую в три категории:

<Columns>
  <Column>

**Централизованный подход** (i18next, next-intl, react-intl, lingui)

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

  </Column>
  <Column>

**Гранулярный подход** (intlayer, inlang)

- более тонко настраивает получение контента — по ключу или по компоненту.

  </Column>
</Columns>

> В этом блоге я не буду фокусироваться на решениях, основанных на компиляции, которые я уже рассмотрел здесь: [Компилятор vs декларативный i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ru/compiler_vs_declarative_i18n.md).
> Обратите внимание, что i18n, основанный на компиляции (например, Lingui), просто автоматизирует извлечение и загрузку контента. Под капотом они часто имеют те же ограничения, что и другие подходы.

> Обратите внимание: чем тоньше вы разбиваете механизм получения контента, тем больше риск внедрения дополнительного состояния и логики в ваши компоненты.

Гранулярные подходы более гибкие, чем централизованные, но часто это компромисс. Даже если библиотеки рекламируют "tree shaking", на практике вы часто в итоге будете загружать страницу на каждом языке.

В целом решение сводится к следующему:

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

Разработчики библиотек, разумеется, знают об этих ограничениях и предлагают обходные решения. Среди них: разделение на namespaces, динамическая загрузка JSON-файлов (`await import()`), или очистка контента на этапе сборки.

В то же время следует учитывать, что при динамической подгрузке контента вы создаёте дополнительные запросы к серверу. Каждое дополнительное `useState` или hook означает ещё один запрос к серверу.

> Чтобы решить эту проблему, Intlayer предлагает группировать несколько определений контента под одним и тем же ключом; Intlayer затем объединит этот контент.

Исходя из всего этого, очевидно, что наиболее популярным подходом является централизованный.

### Почему же централизованный подход так популярен?

/// Во-первых, i18next был первым решением, ставшим широко используемым, следуя философии, вдохновлённой архитектурами PHP и Java (MVC), которые опираются на строгую сегрегацию обязанностей (содержание отделено от кода). Он появился в 2011 году, установив свои стандарты ещё до масштабного перехода к компонентно-ориентированным архитектурам (например, React).

- Затем, как только библиотека становится широко принятой, переход экосистемы на другие паттерны становится затруднительным.
- Использование централизованного подхода также упрощает работу с системами управления переводами, такими как Crowdin, Phrase или Localized.
- Логика подхода per-component более сложна, чем у централизованного, и требует дополнительного времени на разработку, особенно когда приходится решать такие задачи, как определение местоположения контента.

### Хорошо, но почему просто не придерживаться централизованного подхода?

Давайте я объясню, почему это может стать проблемой для вашего приложения:

- **Неиспользуемые данные:**
  Когда загружается страница, вы часто загружаете содержимое со всех остальных страниц. (В приложении из 10 страниц это 90% загруженного, но неиспользуемого контента). Ленивая загрузка модального окна? Библиотека i18n всё равно не обращает внимания — она сначала загружает строки.

- **Производительность:**
  При каждом повторном рендере каждый ваш компонент гидратируется массивной JSON‑нагрузкой, что негативно влияет на реактивность приложения по мере его роста.

- **Поддержка:**
  Поддерживать большие JSON‑файлы болезненно. Приходится переходить между файлами, чтобы вставить перевод, при этом нужно следить, чтобы не было отсутствующих переводов и чтобы не осталось **orphan keys**.

- **Дизайн‑система:**
  Это создаёт несовместимость с design systems (например, компонент `LoginForm`) и ограничивает дублирование компонентов между разными приложениями.

**"Но мы придумали Namespaces!"**

Конечно, и это значительный шаг вперёд. Давайте сравним размер основного бандла для конфигурации `Vite + React + React Router v7 + Intlayer`. Мы симулировали 20-страничное приложение.

Первый пример не включает ленивую загрузку переводов по локалям и не содержит разделения на namespaces. Во втором примере используется content purging + динамическая загрузка переводов.

| Оптимизированный бандл                                                                                                         | Неоптимизированный бандл                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| ![неоптимизированный bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) | ![оптимизированный bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png?raw=true) |

Таким образом, благодаря namespaces, мы перешли от этой структуры:

```bash
locale/
├── en.json
├── fr.json
└── es.json
```

To this one:

```bash
locale/
├── en/
│   ├── common.json
│   ├── navbar.json
│   ├── footer.json
│   ├── home.json
│   └── about.json
├── fr/
│   └── ...
└── es/
    └── ...

```

Теперь вам нужно тонко управлять тем, какую часть контента вашего приложения следует загружать и где. В результате подавляющее большинство проектов просто пропускают эту часть из‑за сложности (см. руководство [next-i18next](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ru/i18n_using_next-i18next.md), чтобы увидеть, какие вызовы представляет собой (даже) следование лучшим практикам).
Вследствие этого такие проекты в конечном итоге сталкиваются с проблемой массовой загрузки JSON, описанной ранее.

> Обратите внимание, что эта проблема не является специфичной для i18next, а относится ко всем централизованным подходам, перечисленным выше.

Однако хочу напомнить, что не все гранулярные подходы решают эту проблему. Например, подходы `vue-i18n SFC` или `inlang` по сути не выполняют ленивую подгрузку переводов по локалям, поэтому вы просто меняете проблему размера бандла (bundle size) на другую.

Кроме того, без правильного разделения ответственности становится гораздо сложнее извлечь и передать ваши переводы переводчикам для проверки.

### Как покомпонентный подход Intlayer решает эту проблему

Intlayer работает в несколько шагов:

1. **Declaration:** Объявляйте ваш контент в любом месте codebase, используя файлы `*.content.{ts|jsx|cjs|json|json5|...}`. Это обеспечивает разделение ответственности при одновременном размещении контента рядом с кодом (colocated). Файл контента может быть для одной локали или мультиязычным.
2. **Обработка:** Intlayer выполняет шаг сборки для обработки JS-логики, обработки отсутствующих переводов (fallbacks), генерации типов TypeScript, управления дублирующимся контентом, получения контента из вашей CMS и прочего.
3. **Очистка:** Когда ваше приложение собирается, Intlayer удаляет неиспользуемый контент (немного похожим образом на то, как Tailwind управляет вашими классами), заменяя контент следующим образом:

**Декларация:**

```tsx
// src/MyComponent.tsx
export const MyComponent = () => {
  const content = useIntlayer("my-key");
  return <h1>{content.title}</h1>;
};
```

```tsx
// src/myComponent.content.ts
export const {
  key: "my-key",
  content: t({
    ru: { title: "Мой заголовок" },
    en: { title: "My title" },
    fr: { title: "Mon titre" }
  })
}

```

**Обработка:** Intlayer собирает словарь на основе файла `.content` и генерирует:

```json5
// .intlayer/dynamic_dictionary/en/my-key.json
{
  "key": "my-key",
  "content": { "title": "Мой заголовок" },
}
```

**Замена:** Intlayer преобразует ваш компонент во время сборки приложения.

**- Режим статического импорта:**

```tsx
// Representation of the component in JSX-like syntax
export const MyComponent = () => {
  const content = useDictionary({
    key: "my-key",
    content: {
      nodeType: "translation",
      translation: {
        en: { title: "Мой заголовок" },
        fr: { title: "Мой заголовок" },
      },
    },
  });

  return <h1>{content.title}</h1>;
};
```

**- Режим динамического импорта:**

```tsx
// Representation of the component in JSX-like syntax
export const MyComponent = () => {
  const content = useDictionaryAsync({
    en: () =>
      import(".intlayer/dynamic_dictionary/en/my-key.json", {
        with: { type: "json" },
      }).then((mod) => mod.default),
    // То же самое для других языков
  });

  return <h1>{content.title}</h1>;
};
```

> `useDictionaryAsync` использует механизм, похожий на Suspense, чтобы загружать локализованный JSON только по мере необходимости.

**Ключевые преимущества этого подхода на уровне компонентов:**

- Держать объявление контента рядом с компонентами повышает удобство сопровождения (например, при переносе компонентов в другое приложение или дизайн-систему. Удаление папки компонента также удаляет связанный контент, как вы, вероятно, уже делаете для ваших `.test`, `.stories`)

- Подход «по компонентам» избавляет AI-агентов от необходимости перескакивать между множеством разных файлов. Он собирает все переводы в одном месте, снижая сложность задачи и количество используемых токенов.

### Ограничения

Разумеется, у такого подхода есть компромиссы:

- Сложнее интегрироваться с другими l10n‑системами и дополнительным tooling'ом (инструментариями).
- Возникает привязка к выбранному решению (что, по сути, уже характерно для любых i18n‑решений из‑за их специфического синтаксиса).

Именно поэтому Intlayer стремится предоставить полный набор инструментов для i18n (100% бесплатно и с открытым исходным кодом), включая AI‑перевод с использованием вашего собственного AI Provider и API‑ключей. Intlayer также предоставляет tooling для синхронизации ваших JSON‑файлов, функционируя как message formatters ICU / vue-i18n / i18next для сопоставления контента с их специфичными форматами.
