Связка Angular и i18next строится на разделении ответственности: Angular управляет жизненным циклом приложения, DI-контейнером и реактивностью, тогда как i18next отвечает за хранение переводов, выбор языка, интерполяцию и правила локализации. В результате интеграция почти всегда реализуется через прослойку — сервисы, пайпы и директивы, которые адаптируют API i18next к Angular-экосистеме.
Ключевая особенность заключается в том, что i18next не является «Angular-библиотекой» в прямом смысле. Он работает на уровне JavaScript-ядра, поэтому Angular требует явной обвязки для:
Экосистема i18next для Angular обычно строится из нескольких пакетов:
i18next — ядро библиотекиi18next-http-backend — загрузка переводов по HTTPi18next-browser-languagedetector — определение языка
пользователяrxjs — для реактивной интеграцииИнициализация выполняется один раз на уровне приложения, до рендера компонентов.
Типовая конфигурация:
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-приложениях (standalone API) инициализация
выполняется через APP_INITIALIZER или
provideAppInitializer.
import { APP_INITIALIZER } from '@angular/core';
export const appConfig = {
providers: [
{
provide: APP_INITIALIZER,
useFactory: () => () => initI18next(),
multi: true
}
]
};
Такой подход гарантирует, что приложение не стартует до завершения загрузки конфигурации 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;
}
}
Сервис выполняет две задачи:
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();
}
}
Непуристый пайп используется намеренно: изменение языка должно автоматически перерендеривать значения без пересоздания компонентов.
В шаблонах:
<h1>{{ 'home.title' | t }}</h1>
<p>{{ 'home.description' | t:{ count: items.length } }}</p>
В компонентах:
constructor(private i18n: I18nService) {}
setGerman() {
this.i18n.changeLanguage('de');
}
Angular-модули часто загружаются лениво, что требует аккуратного обращения с переводами.
Lazy-loaded module может:
i18next поддерживает разделение переводов:
i18next.init({
ns: ['common', 'dashboard', 'admin'],
defaultNS: 'common'
});
В feature-модуле:
i18next.loadNamespaces('dashboard');
В 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>
i18next автоматически обрабатывает множественные формы:
{
"item_one": "{{count}} item",
"item_other": "{{count}} items"
}
В шаблоне:
<p>{{ 'item' | t:{ count: items.length } }}</p>
Angular-пайп не требует дополнительной логики: выбор формы выполняется внутри i18next.
Загрузка переводов через HTTP:
backend: {
loadPath: '/assets/locales/{{lng}}/{{ns}}.json'
}
Angular HTTP interceptor обычно не требуется, так как i18next использует собственный fetch/XHR слой, но в корпоративных приложениях может подключаться авторизация через кастомный backend.
При серверном рендеринге возникают дополнительные требования:
Инициализация на сервере:
i18next.init({
lng: request.headers['accept-language'] || 'en'
});
Критично избегать глобального singleton без клонирования инстанса:
const instance = i18next.createInstance();
Компоненты с ChangeDetectionStrategy.OnPush уменьшают
количество проверок, но требуют явного уведомления при смене языка.
i18next автоматически кэширует загруженные namespaces, но при больших приложениях полезно:
Реактивный слой позволяет связывать язык с состоянием приложения:
language$ = this.i18n.languageChanges.pipe(
distinctUntilChanged(),
shareReplay(1)
);
Это позволяет:
Изменение языка триггерит:
i18next.changeLanguage('ru');
Angular UI обновляется через подписки и
markForCheck.
В масштабируемых Angular-приложениях i18next обычно занимает уровень:
Такое разделение позволяет: