---
createdAt: 2025-09-10
updatedAt: 2025-09-10
title: Per-Component проти централізованого i18n: новий підхід з Intlayer
description: Детальний огляд стратегій інтернаціоналізації в React: порівняння централізованого, per-key і per-component підходів та презентація Intlayer.
keywords:
  - i18n
  - React
  - Internationalization
  - Intlayer
  - Optimization
  - Bundle Size
slugs:
  - blog
  - per-component-vs-centralized-i18n
---

# Підхід per-component проти централізованого i18n

Підхід per-component не є новим поняттям. Наприклад, в екосистемі Vue `vue-i18n` підтримує [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt також пропонує [переклади per-component](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>

> У цьому блозі я не зосереджуватимусь на рішеннях на основі компілятора, які я вже розглянув тут: [Компілятор проти декларативного i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/uk/compiler_vs_declarative_i18n.md).
> Зауважте, що компіляторні i18n-рішення (наприклад, Lingui) лише автоматизують витяг та завантаження контенту. Під капотом вони часто мають ті самі обмеження, що й інші підходи.

> Зауважте, що чим детальніше ви налаштовуєте спосіб отримання контенту, тим більший ризик додати додатковий стан і логіку в компоненти.

Гранулярні підходи є більш гнучкими, ніж централізовані, але часто це компроміс. Навіть якщо ці бібліотеки рекламують "tree shaking", на практиці ви часто в кінцевому підсумку завантажуєте сторінку на всіх мовах.

Отже, в загальних рисах рішення зводиться до наступного:

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

Звісно, автори бібліотек усвідомлюють ці обмеження і пропонують обхідні шляхи.
Серед них: розподіл на namespaces, динамічне завантаження JSON-файлів (`await import()`), або очищення контенту під час збірки.

Водночас потрібно знати, що коли ви динамічно завантажуєте свій вміст, ви вводите додаткові запити до сервера. Кожен додатковий `useState` або хук означає ще один запит до сервера.

> Щоб вирішити це питання, Intlayer пропонує групувати кілька визначень контенту під одним ключем — Intlayer потім об'єднає цей контент.

Але з-поміж усіх цих рішень очевидно, що найпопулярнішим є централізований підхід.

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

- По-перше, i18next було першим рішенням, яке стало широко використовуваним, і воно слідувало філософії, запозиченій із PHP та Java-архітектур (MVC), які базуються на суворому розділенні обов'язків (триманні контенту окремо від коду). Воно з'явилося в 2011 році, встановивши свої стандарти ще до масового переходу до архітектур на основі компонентів (наприклад, React).
- Далі, коли бібліотека широко прийнята, змінити екосистему на інші патерни стає складно.
- Використання централізованого підходу також спрощує роботу в Translation Management Systems, таких як Crowdin, Phrase або Localized.
- Логіка підходу per-component складніша за централізований і вимагає додаткового часу на розробку, особливо коли потрібно вирішувати задачі на кшталт визначення місця розташування контенту.

### Добре, але чому б просто не дотримуватися централізованого підходу?

Дозвольте пояснити, чому це може бути проблематично для вашого додатка:

- **Невикористані дані:**
  Коли завантажується сторінка, часто підвантажується контент з усіх інших сторінок. (У додатку з 10 сторінок це означає 90% невикористаного контенту.) Відкриваєте модальне вікно з відкладеним завантаженням? Бібліотека i18n байдуже — вона все одно спочатку підвантажує рядки.
- **Продуктивність:**
  При кожному перерендері кожен ваш компонент отримує велике JSON-навантаження, що погіршує реактивність додатка в міру його зростання.
- **Підтримка:**
  Підтримка великих JSON-файлів болюча. Потрібно переходити між файлами, щоб додати переклад, переконуючись, що не бракує перекладів і не залишилося **orphan keys**.
- **Дизайн-система:**
  Воно створює несумісність із дизайн-системами (наприклад, компонентом `LoginForm`) і обмежує дублювання компонентів між різними додатками.

**"Але ми винайшли Namespaces!"**

Звісно, і це величезний крок уперед. Погляньмо на порівняння розміру основного бандла для налаштування Vite + React + React Router v7 + Intlayer. Ми змоделювали 20-сторінковий додаток.

Перший приклад не включає lazy-loaded переклади за локалями і не використовує розбиття на неймспейси. Другий включає content purging + динамічне завантаження перекладів.

| Оптимізований бандл                                                                                                        | Бандл без оптимізації                                                                                    |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| ![неоптимізований бандл](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) | ![оптимізований бандл](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/uk/i18n_using_next-i18next.md), щоб побачити виклики, які становить (лише) дотримання найкращих практик).
Внаслідок цього ті проєкти опиняються з проблемою масивного завантаження JSON, описаною раніше.

> Зверніть увагу, що ця проблема не є специфічною для i18next, а характерна для всіх централізованих підходів, перелічених вище.

Проте хочу нагадати, що не всі гранулярні підходи вирішують цю проблему. Наприклад, підходи `vue-i18n SFC` або `inlang` за замовчуванням не виконують відкладене завантаження перекладів за локалями (lazy load), тому ви просто замінюєте проблему розміру бандла на іншу.

Більше того, без належного розділення обов'язків (separation of concerns) стає значно складніше витягувати та надавати ваші переклади перекладачам для перегляду.

### Як підхід Intlayer на рівні компонентів вирішує це

Intlayer виконує кілька кроків:

1. **Declaration:** Оголосіть ваш контент будь-де у кодовій базі, використовуючи файли `*.content.{ts|jsx|cjs|json|json5|...}`. Це забезпечує розділення обов'язків, зберігаючи контент поруч із компонентами. Файл контенту може бути на одну локаль або мультимовним.
2. **Опрацювання:** Intlayer запускає крок збірки для обробки JS-логіки, обробки fallback-значень для відсутніх перекладів, генерації типів 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({
    uk: { title: "Мій заголовок" },
    en: { title: "My title" },
    fr: { title: "Mon titre" }
  })
}

```

**Опрацювання:** Intlayer будує словник на основі файлу `.content` та генерує:

```json5
// .intlayer/dynamic_dictionary/en/my-key.json
{
  "key": "my-key",
  "content": { "title": "My title" },
}
```

**Заміна:** Intlayer перетворює ваш компонент під час збірки застосунку.

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

```tsx
// Представлення компонента у синтаксисі, подібному до JSX
export const MyComponent = () => {
  const content = useDictionary({
    key: "my-key",
    content: {
      nodeType: "translation",
      translation: {
        en: { title: "My title" },
        fr: { title: "Mon titre" },
      },
    },
  });

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

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

```tsx
// Представлення компонента у синтаксисі, подібному до JSX
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 лише за потреби.

**Ключові переваги цього компонентного підходу:**

- Тримання оголошення контенту поруч із компонентами забезпечує кращу підтримуваність (наприклад, при переміщенні компонентів до іншого app або design system. Видалення папки компонента також видаляє пов'язаний контент, як ви, ймовірно, вже робите для ваших `.test`, `.stories`)

- Підхід на рівні компоненту запобігає необхідності AI-агентам переходити по всіх ваших різних файлах. Він обробляє всі переклади в одному місці, зменшуючи складність завдання та кількість використовуваних токенів.

### Обмеження

Звісно, цей підхід має свої компроміси:

- Важче підключатися до інших l10n-систем та додаткових інструментів.
- Ви потрапляєте у залежність (що, по суті, вже відбувається з будь-яким i18n-рішенням через їхній специфічний синтаксис).

Саме тому Intlayer намагається надати повний набір інструментів для i18n (100% безкоштовний і з відкритим вихідним кодом — OSS), включно з AI-перекладом за допомогою вашого власного AI-провайдера та API-ключів. Intlayer також надає інструменти для синхронізації ваших JSON-файлів, що працюють подібно до message formatters ICU / vue-i18n / i18next, щоб відобразити контент у їхніх специфічних форматах.
