Интеграция с модулями Angular

Связка Angular и i18next строится на разделении ответственности: Angular управляет жизненным циклом приложения, DI-контейнером и реактивностью, тогда как i18next отвечает за хранение переводов, выбор языка, интерполяцию и правила локализации. В результате интеграция почти всегда реализуется через прослойку — сервисы, пайпы и директивы, которые адаптируют API i18next к Angular-экосистеме.

Ключевая особенность заключается в том, что i18next не является «Angular-библиотекой» в прямом смысле. Он работает на уровне JavaScript-ядра, поэтому Angular требует явной обвязки для:

  • синхронизации изменения языка с Change Detection;
  • реактивного обновления интерфейса;
  • внедрения через Dependency Injection;
  • корректной работы в lazy-loaded модулях и SSR.

Установка и базовая конфигурация

Экосистема i18next для Angular обычно строится из нескольких пакетов:

  • i18next — ядро библиотеки
  • i18next-http-backend — загрузка переводов по HTTP
  • i18next-browser-languagedetector — определение языка пользователя
  • rxjs — для реактивной интеграции
  • Angular-провайдеры и обёртки (создаются вручную или через собственный слой)

Инициализация выполняется один раз на уровне приложения, до рендера компонентов.

Типовая конфигурация:

import i18next from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

export function initI18next() {
  return i18next
    .use(HttpBackend)
    .use(LanguageDetector)
    .init({
      fallbackLng: 'en',
      supportedLngs: ['en', 'ru', 'de'],
      ns: ['common'],
      defaultNS: 'common',
      backend: {
        loadPath: '/assets/locales/{{lng}}/{{ns}}.json'
      },
      interpolation: {
        escapeValue: false
      },
      detection: {
        order: ['localStorage', 'navigator'],
        caches: ['localStorage']
      }
    });
}

Подключение через Angular bootstrap

В современных Angular-приложениях (standalone API) инициализация выполняется через APP_INITIALIZER или provideAppInitializer.

import { APP_INITIALIZER } from '@angular/core';

export const appConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => () => initI18next(),
      multi: true
    }
  ]
};

Такой подход гарантирует, что приложение не стартует до завершения загрузки конфигурации i18next.


Обёртка над i18next: сервис интернационализации

Прямое использование i18next.t() внутри компонентов приводит к жёсткой связности и проблемам с обновлением UI. Поэтому создаётся сервис-адаптер.

import { Injectable } from '@angular/core';
import i18next from 'i18next';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class I18nService {
  private lang$ = new BehaviorSubject<string>(i18next.language);

  languageChanges = this.lang$.asObservable();

  t(key: string, options?: any): string {
    return i18next.t(key, options);
  }

  changeLanguage(lang: string) {
    return i18next.changeLanguage(lang).then(() => {
      this.lang$.next(lang);
    });
  }

  get currentLang(): string {
    return i18next.language;
  }
}

Сервис выполняет две задачи:

  • инкапсуляция API i18next;
  • предоставление реактивного потока изменений языка.

Реактивная интеграция с Angular Change Detection

i18next по умолчанию не вызывает Angular Change Detection, поэтому требуется явная синхронизация.

Обычно используется подписка на событие:

i18next.on('languageChanged', (lng) => {
  // триггер обновления UI через сервис
});

В Angular-слое это превращается в поток:

i18next.on('languageChanged', (lng) => {
  i18nextInstance.lang$.next(lng);
});

При использовании стратегии OnPush обновление интерфейса происходит только при изменении входных данных или Observable, поэтому поток языка становится ключевым механизмом синхронизации.


Пайп для перевода строк

Angular-пайп является наиболее естественным способом использования переводов в шаблонах.

import { Pipe, PipeTransform, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { I18nService } from './i18n.service';
import { Subscription } from 'rxjs';

@Pipe({
  name: 't',
  pure: false
})
export class TranslatePipe implements PipeTransform, OnDestroy {
  private sub: Subscription;

  constructor(
    private i18n: I18nService,
    private cdr: ChangeDetectorRef
  ) {
    this.sub = this.i18n.languageChanges.subscribe(() => {
      this.cdr.markForCheck();
    });
  }

  transform(key: string, params?: any): string {
    return this.i18n.t(key, params);
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

Непуристый пайп используется намеренно: изменение языка должно автоматически перерендеривать значения без пересоздания компонентов.


Использование i18next в Angular компонентах

В шаблонах:

<h1>{{ 'home.title' | t }}</h1>
<p>{{ 'home.description' | t:{ count: items.length } }}</p>

В компонентах:

constructor(private i18n: I18nService) {}

setGerman() {
  this.i18n.changeLanguage('de');
}

Интеграция с Angular модулями (Feature Modules)

Angular-модули часто загружаются лениво, что требует аккуратного обращения с переводами.

Проблема изоляции контекста

Lazy-loaded module может:

  • использовать другой namespace переводов;
  • требовать динамической загрузки ресурсов;
  • не иметь гарантии, что язык уже инициализирован.

Решение через namespaces

i18next поддерживает разделение переводов:

i18next.init({
  ns: ['common', 'dashboard', 'admin'],
  defaultNS: 'common'
});

В feature-модуле:

i18next.loadNamespaces('dashboard');

Lazy-loaded модули и загрузка переводов

В Angular routing:

{
  path: 'dashboard',
  loadChildren: () =>
    import('./dashboard/dashboard.module').then(m => m.DashboardModule)
}

Внутри модуля:

ngOnInit() {
  this.i18n.loadNamespace('dashboard');
}

Сервис-обёртка:

loadNamespace(ns: string) {
  return i18next.loadNamespaces(ns);
}

Организация структуры переводов

Типичная структура:

assets/
  locales/
    en/
      common.json
      dashboard.json
    ru/
      common.json
      dashboard.json

Разделение по namespace снижает нагрузку и улучшает масштабируемость больших приложений.


Интерполяция и динамические параметры

i18next поддерживает параметризацию строк:

{
  "welcome": "Hello {{name}}",
  "items": "You have {{count}} items"
}

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

<p>{{ 'welcome' | t:{ name: user.name } }}</p>

Pluralization в Angular + i18next

i18next автоматически обрабатывает множественные формы:

{
  "item_one": "{{count}} item",
  "item_other": "{{count}} items"
}

В шаблоне:

<p>{{ 'item' | t:{ count: items.length } }}</p>

Angular-пайп не требует дополнительной логики: выбор формы выполняется внутри i18next.


Работа с HTTP backend

Загрузка переводов через HTTP:

backend: {
  loadPath: '/assets/locales/{{lng}}/{{ns}}.json'
}

Angular HTTP interceptor обычно не требуется, так как i18next использует собственный fetch/XHR слой, но в корпоративных приложениях может подключаться авторизация через кастомный backend.


SSR (Angular Universal)

При серверном рендеринге возникают дополнительные требования:

  • язык должен определяться на сервере;
  • переводы должны быть загружены до рендера;
  • состояние i18next должно быть изолировано между запросами.

Инициализация на сервере:

i18next.init({
  lng: request.headers['accept-language'] || 'en'
});

Критично избегать глобального singleton без клонирования инстанса:

const instance = i18next.createInstance();

Оптимизация производительности

OnPush стратегия

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

Кэширование переводов

i18next автоматически кэширует загруженные namespaces, но при больших приложениях полезно:

  • предзагружать ключевые namespaces;
  • использовать bundle splitting переводов;
  • ограничивать глубину интерполяции.

Интеграция с RxJS

Реактивный слой позволяет связывать язык с состоянием приложения:

language$ = this.i18n.languageChanges.pipe(
  distinctUntilChanged(),
  shareReplay(1)
);

Это позволяет:

  • переключать язык как часть глобального state;
  • синхронизировать с NgRx или Akita;
  • обновлять UI без ручных вызовов.

Динамическое изменение языка в рантайме

Изменение языка триггерит:

  • перезагрузку namespace (если требуется);
  • обновление всех подписанных компонентов;
  • пересчёт всех pipe-значений.
i18next.changeLanguage('ru');

Angular UI обновляется через подписки и markForCheck.


Связь с архитектурой больших приложений

В масштабируемых Angular-приложениях i18next обычно занимает уровень:

  • CoreModule (инициализация);
  • SharedModule (pipe и сервис);
  • Feature modules (namespaces).

Такое разделение позволяет:

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