ГлавнаяПесочницаВитринаПриложениеДокументБлог
    • EnglishАнглийский
      EN
    • РусскийРусский
      RU
    • 日本語Японский
      JA
    • françaisФранцузский
      FR
    • 한국어Корейский
      KO
    • 中文Китайский
      ZH
    • EspañolИспанский
      ES
    • DeutschНемецкий
      DE
    • العربيةАрабский
      AR
    • ItalianoИтальянский
      IT
    • British EnglishБританский английский
      EN-GB
    • PortuguêsПортугальский
      PT
    • हिन्दीХинди
      HI
    • TürkçeТурецкий
      TR
    • polskiПольский
      PL
    • IndonesiaИндонезийский
      ID
    • Tiếng ViệtВьетнамский
      VI
    • УкраїнськаУкраинский
      UK
    /
    Alt+←
    Что такое интернационализация (i18n)?
    SEO и Интернационализация
    Руководство
    • i18n с помощью next-i18next
    • i18n с помощью next-intl
    Используйте Intlayer в вашем решении
    • Автоматизировать next-i18next
    • Автоматизировать react-i18next
    • Автоматизировать next-intl
    • Автоматизировать react-intl
    • Автоматизировать vue-i18n
    Сравнения
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    Документация
    1. Blog
    2. Per component vs centralized i18n
    Creation:2025-09-10Last update:2025-09-10
    Ссылайтесь на этот документ на ваш любимый ассистент AI
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    Спросите свой вопрос и получите сводку документа, используя эту страницу и выбранного вами поставщика AI

    Содержимое этой страницы было переведено с помощью ИИ.

    Смотреть последнюю версию оригинального контента на английском
    Edit this doc

    If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.

    GitHub link to the documentation
    Copy

    Copy doc Markdown to clipboard

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

    Подход по компонентам, не новое понятие. Например, в экосистеме Vue vue-i18n поддерживает i18n SFC (Single File Component). Nuxt также предлагает переводы на уровне компонента, а Angular использует похожий паттерн через свои Feature Modules.

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

    bash
    Копировать код

    Копировать код в буфер обмена

    lib/└── features/    └── login/        ├── login_screen.dart        └── login_screen.i18n.dart  # <- Переводы находятся здесь
    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 мы в основном видим разные подходы, которые я сгруппирую в три категории:

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

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

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

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

    В этом блоге я не буду фокусироваться на решениях, основанных на компиляции, которые я уже рассмотрел здесь: Компилятор vs декларативный i18n. Обратите внимание, что 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 оптимизированный bundle

    Таким образом, благодаря 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, чтобы увидеть, какие вызовы представляет собой (даже) следование лучшим практикам). Вследствие этого такие проекты в конечном итоге сталкиваются с проблемой массовой загрузки 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.tsxexport const MyComponent = () => {  const content = useIntlayer("my-key");  return <h1>{content.title}</h1>;};
    tsx
    Копировать код

    Копировать код в буфер обмена

    // src/myComponent.content.tsexport 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 syntaxexport 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 syntaxexport 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 для сопоставления контента с их специфичными форматами.

    Что такое интернационализация (i18n)?
    Alt+→

    На этой странице

      Обсуждения анонимны и регулярно просматриваются для решения распространённых проблем. Не стесняйтесь делиться идеями функций, отзывами о документации или чем-либо, связанным с Intlayer, мы используем эту информацию для формирования нашей дорожной карты и улучшения продукта.

      lib/└── features/    └── login/        ├── login_screen.dart        └── 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);}
      locale/├── en.json├── fr.json└── es.json
      locale/├── en/│   ├── common.json│   ├── navbar.json│   ├── footer.json│   ├── home.json│   └── about.json├── fr/│   └── ...└── es/    └── ...
      // src/MyComponent.tsxexport const MyComponent = () => {  const content = useIntlayer("my-key");  return <h1>{content.title}</h1>;};
      // src/myComponent.content.tsexport const {  key: "my-key",  content: t({    ru: { title: "Мой заголовок" },    en: { title: "My title" },    fr: { title: "Mon titre" }  })}
      // .intlayer/dynamic_dictionary/en/my-key.json{  "key": "my-key",  "content": { "title": "Мой заголовок" },}
      // Representation of the component in JSX-like syntaxexport const MyComponent = () => {  const content = useDictionary({    key: "my-key",    content: {      nodeType: "translation",      translation: {        en: { title: "Мой заголовок" },        fr: { title: "Мой заголовок" },      },    },  });  return <h1>{content.title}</h1>;};
      // Representation of the component in JSX-like syntaxexport 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>;};