Рутовое приложение
Локализация

Локализация

Язык интерфейса один на сессию, и выбирает его пользователь в настройках рутового приложения. Рутовое приложение хранит выбор, переводит собственный интерфейс и меню и сообщает о смене языка веб-компонентам. Веб-компонент переводит свой интерфейс сам — на Transloco (opens in a new tab) (пакет @jsverse/transloco), а подключает переводы через provideQLocalization и withTransloco из @diasoft/qpalette-visual: они берут язык из рутового приложения, настраивают Transloco, локаль Angular и PrimeNG и заголовок Accept-Language в запросах.

Собственный модуль transloco-root.module.ts, который раньше создавался в каждом проекте, больше не нужен — см. Устаревшее.

С чего начать

Положите переводы в src/assets/i18n/ своей сборки — по файлу на язык, ru.json и en.json. Ключи — фразы на русском, поэтому ru.json может оставаться пустым; если ключи уникальные ("I18N_HOME_PAGE"), заполняются оба файла.

{
  "Поиск": "Search",
  "Конфигурирование поддерживаемых языков": "Configuring supported languages"
}

В main.ts добавьте provideQLocalization с withTransloco, указав каталог переводов:

// main.ts
import { createApplication, provideQLocalization, withTransloco } from '@diasoft/qpalette-visual';
import { AppComponent } from './app/app.component';
 
(async () => {
  const app = createApplication();
 
  await app.bootstrap(AppComponent, [
    // ...
    provideQLocalization(
      withTransloco({
        translationsUrl: 'i18n'   // каталог в ассетах вашей сборки
      })
    )
  ]);
})();

В компонентах переводы используются штатными средствами Transloco; в standalone-компоненте директиву *transloco и pipe transloco подключает импорт TranslocoModule:

import { Component } from '@angular/core';
import { TranslocoModule } from '@jsverse/transloco';
 
@Component({
  selector: 'app-home',
  imports: [TranslocoModule],
  template: `
    <ng-container *transloco="let t">
      <h1>{{ t('Домашняя страница') }}</h1>
    </ng-container>
  `
})
export class HomeComponent {}

При старте веб-компонент получит текущий язык рутового приложения и загрузит /api/<сервис>/<компонент>/assets/i18n/<lang>.json — путь без ведущего assets/ ведёт в ассеты вашей сборки, см. Адрес переводов: строка или функция.

Как это устроено

Список языков и язык по умолчанию задаёт рутовое приложение в assets/data/i18n/languages.json, выбор пользователя оно хранит в настройках браузера. Веб-компонент при старте читает и то и другое из общего состояния страницы — отдельного запроса за языком не нужно.

provideQLocalization на основании этих данных:

  • регистрирует конфигурацию Transloco: список доступных языков, язык по умолчанию и текущий язык;
  • устанавливает локаль Angular (LOCALE_ID, registerLocaleData) и переводы PrimeNG;
  • подключает HTTP-перехватчик LanguageInterceptor: к каждому запросу добавляется заголовок Accept-Language с текущим языком;
  • регистрирует провайдеры TranslocoModule.

withTransloco добавляет загрузчик переводов: для активного языка он запрашивает файл по адресу из translationsUrl и передаёт его Transloco.

Смена языка в настройках рутового приложения страницу не перезагружает: рутовое приложение переключает собственные переводы, заново запрашивает меню (assets/data/menu.<lang>.json, а если его нет — assets/data/menu.json) и заново создаёт открытые вкладки. Веб-компонент во вкладке стартует ещё раз уже с новым языком — подписываться на смену языка ему не нужно. Веб-компоненты, которые вкладками не являются и не пересоздаются (например, плагины), получают событие onLanguageChange и переключают переводы сами — см. Переключать язык вслед за рутовым приложением.

Локализованные компоненты @diasoft/qpalette-visual (например, q-search-box) подгружают свои переводы отдельно — из файлов assets/data/i18n/<lang>.json, которые отдаёт рутовое приложение, — и объединяют их с переводами продукта. Настраивать для них ничего не нужно.

Настройка

Языки рутового приложения: languages.json

Файл rootapp/assets/data/i18n/languages.json задаёт язык по умолчанию и список языков, доступных в настройках рутового приложения:

{
  "default" : "ru",
  "list" : [
    {
      "code" : "ru",
      "name" : "Русский"
    },
    {
      "code" : "en",
      "name" : "English"
    }
  ]
}

Поддерживаются языки ru и en.

Меню на нескольких языках

При переключении языка рутовое приложение запрашивает меню assets/data/menu.<lang>.json; если файла по этому адресу нет, загружается assets/data/menu.json. Чтобы меню переводилось, положите рядом с menu.json его переводы: menu.en.json для английского.

Адрес переводов: строка или функция

translationsUrl принимает либо строку — каталог переводов, к которому загрузчик добавляет /<lang>.json, либо функцию (language: string) => string, которая возвращает полный адрес файла переводов для языка. Функция нужна, когда адрес зависит не только от языка — например, переводы отдаёт сервер по своей схеме:

withTransloco({
  translationsUrl: language => `/api/<service>/<component>/translations?lang=${ language }`
})

Функция обязана вернуть строку, иначе загрузчик завершится ошибкой.

Строка разбирается по общему правилу адресов ассетов (Адреса собственных ассетов): путь без ведущего assets/ — каталог в ассетах вашей сборки, у веб-компонента это /api/<сервис>/<компонент>/assets/…. Так переводы не зависят от имени пакета в адресе:

withTransloco({
  translationsUrl: 'i18n'   // → /api/<сервис>/<компонент>/assets/i18n/<lang>.json
})

assets/…, путь от корня и полный URL используются как есть — assets/… браузер отсчитает от корня сайта, то есть от ассетов рутового приложения. Если файл не нашёлся, ошибка загрузки называет адрес, по которому его искали. Правило действует с 8.2.0.

Заголовок Accept-Language

LanguageInterceptor из @diasoft/qpalette-core подключается автоматически — регистрировать его в HTTP_INTERCEPTORS не нужно. Единственное условие: HTTP-клиент должен принимать перехватчики из DI — provideHttpClient(withInterceptorsFromDi()).

Как решать типовые задачи

Переключать язык вслед за рутовым приложением

Веб-компоненту во вкладке это не нужно: при смене языка рутовое приложение создаёт вкладки заново, и он стартует с новым языком. Подписка нужна веб-компоненту, который рутовое приложение не пересоздаёт, — например, плагину шапки или боковой панели. Подпишитесь на onLanguageChange и передайте язык в Transloco:

// main.ts
import { inject, provideAppInitializer } from '@angular/core';
import { QEventsService } from '@diasoft/qpalette-visual';
import { TranslocoService } from '@jsverse/transloco';
 
await app.bootstrap(AppComponent, [
  // ...
  provideAppInitializer(() => {
    const events = inject(QEventsService);
    const transloco = inject(TranslocoService);
 
    events.onLanguageChange().subscribe(data => {
      transloco.setActiveLang(data.payload.language);
    });
  })
]);

Так переключаются переводы Transloco. Локаль Angular (форматы дат и чисел) и переводы PrimeNG задаются при старте и по событию не меняются — они обновятся при следующем создании веб-компонента.

Менять язык из веб-компонента (QEventsService.changeLanguage) можно только в портальном режиме — когда рутовое приложение скрыто и веб-компонент занимает всё окно. Во встроенном режиме языком управляет рутовое приложение.

Получать адрес переводов извне: библиотечные проекты

Библиотечный проект не знает, где его разместит продукт-потребитель: его файлы копируются в assets продукта, а собственного микросервиса у библиотеки нет. Жёстко заданный адрес вида /api/<library-service>/<library-component>/assets/... ведёт на несуществующий микросервис, и переводы не загружаются. Адрес переводов библиотека должна получать от потребителя — через properties своего endpoint.

Конфигурация задаётся провайдером QTranslocoConfig с зависимостью QWebComponentProperties, в которой лежат параметры, переданные библиотеке извне. Провайдер регистрируется после provideQLocalization и переопределяет конфигурацию из withTransloco:

// main.ts библиотечного проекта
import {
  createApplication,
  provideQLocalization,
  withTransloco,
  QTranslocoConfig,
  QWebComponentProperties
} from '@diasoft/qpalette-visual';
 
const DEFAULT_TRANSLATIONS_URL = 'i18n';   // ассеты своей сборки, когда библиотека запущена сама
 
const app = createApplication();
 
await app.bootstrap(AppComponent, [
  // ...
  provideQLocalization(withTransloco({ translationsUrl: DEFAULT_TRANSLATIONS_URL })),
  {
    provide: QTranslocoConfig,
    useFactory: (properties: QWebComponentProperties): QTranslocoConfig => ({
      translationsUrl: properties?.translationsUrl || DEFAULT_TRANSLATIONS_URL
    }),
    deps: [QWebComponentProperties]
  }
]);

Потребитель передаёт адрес в properties при монтировании библиотеки:

const endpoint: QEndpoint = {
  service: '<service>',
  component: '<component>',
  route: '',
  properties: {
    // каталог переводов внутри скопированного в assets пакета библиотеки
    translationsUrl: '/api/<service>/<component>/assets/<library>/<путь до i18n внутри пакета>'
  }
};

Имя свойства (translationsUrl в примере) — часть контракта библиотеки: опишите его в README.md пакета вместе с остальными properties.

Брать переводы компонентов Q.Palette из другого каталога

По умолчанию компоненты @diasoft/qpalette-visual читают переводы из assets/data/i18n/<lang>.json рутового приложения. Если они должны браться из другого каталога, добавьте в провайдеры ...provideVisualLocalization('<каталог>') — файл <каталог>/<lang>.json объединится с переводами продукта.

Ограничения и типичные ошибки

  • Вместо перевода на экране ключ с приставкой языка — en.Отмена. Активный язык не входит в список доступных, и Transloco считает его не языком, а scope'ом. Начиная с 8.1.0 provideQLocalization добавляет текущий язык в список сам; в старых версиях — проверьте availableLangs в конфигурации Transloco.
  • В консоли Не удалось получить файл с переводами по URL: <адрес>. Файл по итоговому адресу недоступен; по адресу видно, из чего он собрался. Для библиотечного проекта, если адрес ведёт в микросервис самой библиотеки, — см. Получать адрес переводов извне.
  • Ошибка Неизвестный язык. В languages.json указан язык, кроме ru и en; другие языки не поддерживаются.
  • Плагин не переключается вслед за рутовым приложением. Плагин, в отличие от вкладки, при смене языка не пересоздаётся — ему нужна подписка на onLanguageChange, см. Переключать язык вслед за рутовым приложением.
  • После смены языка даты и числа в прежнем формате. Локаль Angular задаётся при старте веб-компонента и по событию не меняется; обновится при следующем его создании.
  • changeLanguage не действует. Из веб-компонента язык меняется только в портальном режиме.
  • Переводы грузятся не по тому адресу или не на том языке. В проекте одновременно provideQLocalization и старый transloco-root.module.ts с собственными загрузчиком и конфигурацией; какой из них окажется активным, зависит от порядка провайдеров. Удалите модуль — см. Устаревшее.

Справочник

Всё из @diasoft/qpalette-visual, если не указано иное.

provideQLocalization(...features): EnvironmentProviders

Настраивает локализацию веб-компонента по языку рутового приложения. Принимает одну или несколько фич; пока единственная — withTransloco.

withTransloco(config: QTranslocoConfig)

ПолеТипОписание
translationsUrlstring | ((language: string) => string)Строка — каталог переводов, файл <каталог>/<lang>.json; функция — полный адрес файла для языка

QTranslocoConfig можно переопределить провайдером после provideQLocalization — см. библиотечные проекты.

provideVisualLocalization(translationsUrl = 'assets/data/i18n'): Provider[]

Переводы компонентов @diasoft/qpalette-visual: загружает <translationsUrl>/<lang>.json и объединяет с переводами активного языка. По умолчанию уже подключено компонентами.

QEventsService

МетодЧто делает
onLanguageChange(): Observable<QLanguageChangeContract>Событие смены языка рутового приложения; язык — в payload.language
changeLanguage(language: string): voidПросит рутовое приложение сменить язык; только в портальном режиме

LanguageInterceptor (@diasoft/qpalette-core)

HTTP-перехватчик, добавляющий заголовок Accept-Language с текущим языком. Подключается provideQLocalization, требует provideHttpClient(withInterceptorsFromDi()).

languages.json

ПолеТипОписание
default'ru' | 'en'Язык по умолчанию
list{ code: string; name: string }[]Языки, доступные в настройках рутового приложения; name — подпись в списке

Совместимость версий

ВерсияЧто изменилось
8.1.0Текущий язык и язык по умолчанию всегда входят в список доступных — ключи вида en.Отмена больше не появляются; Transloco в производственной сборке работает в производственном режиме
7.5.1provideQLocalization и withTransloco; собственный transloco-root.module.ts больше не нужен

Устаревшее

transloco-root.module.ts — собственный модуль локализации веб-компонента: загрузчик переводов с методом getTranslation, конфигурация translocoConfig со списком языков, подписка на onLanguageChange и ручная регистрация LanguageInterceptor. Всё это делает provideQLocalization, а модуль остался в проектах как наследие.

⚠️

Держать оба способа одновременно не стоит: модуль регистрирует собственные конфигурацию и загрузчик Transloco, и какой из них окажется активным, зависит от порядка провайдеров.

Удаление вручную:

  1. Подключите локализацию, как описано в разделе С чего начать: provideQLocalization(withTransloco({ translationsUrl })) с адресом каталога переводов, который раньше был указан в getTranslation.
  2. Подписку модуля на onLanguageChange веб-компоненту во вкладке переносить не нужно — рутовое приложение пересоздаёт вкладки при смене языка. Плагину перенесите её в provideAppInitializer — см. Переключать язык вслед за рутовым приложением.
  3. Уберите TranslocoRootModule из imports корневого модуля или из importProvidersFrom(...), а также ручную регистрацию LanguageInterceptor в HTTP_INTERCEPTORS, LOCALE_ID и вызовы registerLocaleData — их выполняет provideQLocalization.
  4. Удалите файл transloco-root.module.ts.

Мигратор qpalette migrate при переходе на 8.x (см. Миграция на 8.x) переводит такой модуль на пакет @jsverse/transloco и регистрирует в нём provideTransloco с языками ru и en, чтобы старый способ продолжил работать после обновления. Сам модуль он не удаляет — переход на provideQLocalization и удаление файла выполняются вручную по шагам выше.