Динамические переводы в Angular стали возможными


Практическое руководство по реализации лениво загружаемых переводов

Если вы когда-либо имели дело с интернационализацией (или сокращенно «i18n») в Angular или только собираетесь ее внедрить, вы можете придерживаться официального руководства, которое является потрясающим, использовать сторонние пакеты, которые могут быть трудно отлаживать, или выбрать альтернативный путь, который я опишу ниже.

Одним из распространенных подводных камней при использовании i18n является большой размер файлов перевода и невозможность разделить их для того, чтобы скрыть часть приложения от посторонних глаз. Некоторые решения, такие как встроенная реализация Angular, действительно мощны и совместимы с SEO, но требуют большой подготовки и не поддерживают переключение языков на лету в режиме разработки (что вызывало проблемы, по крайней мере, в версии 9); другие решения, такие как ngx-translate, требуют установки нескольких пакетов и по-прежнему не поддерживают разделение одного языка (обновление: на самом деле, ngx-translate поддерживает это).

Хотя для этой сложной функции не существует «волшебной палочки», которая бы поддерживала все и подходила всем, вот еще один способ реализации переводов, который может подойти для ваших нужд.
Хватит вступления, я обещал, что это будет практическое руководство, так что давайте сразу перейдем к делу.


Подготовка основ

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

export type LanguageCode = 'en' | 'de';
Вход в полноэкранный режим Выход из полноэкранного режима

Одной из любимых функций Angular является Dependency Injection, которая делает многое для нас — давайте используем ее для наших нужд. Я бы также хотел немного приправить ситуацию, используя NgRx в этом руководстве, но если вы не используете его в своем проекте, смело заменяйте его простым BehaviorSubject.

В качестве дополнительного шага, который облегчит дальнейшую разработку с NgRx, создайте тип для DI-фабрик:

export type Ti18nFactory<Part> = (store: Store) => Observable<Part>;
Вход в полноэкранный режим Выход из полноэкранного режима

Создание файлов перевода

Общие строки

Предположим, у нас есть несколько основных строк, которые мы хотели бы использовать во всем приложении. Некоторые простые, но распространенные вещи, которые никогда не связаны с конкретным модулем, функцией или библиотекой, например, кнопки «OK» или «Назад».
Мы поместим эти строки в модуль «core» и начнем с простого интерфейса, который поможет нам не забыть ни одной строки в переводах:

export interface I18nCore {
  errorDefault: string;
  language: string;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Для ясности, этот интерфейс не гарантирует, что все строки будут действительно переведены, но компилятор TypeScript (и ваша IDE) выдаст ошибку «TS2741», если вы забудете включить какую-либо строку в ваши файлы «lang».

Переходим к реализации интерфейса, и для этого фрагмента жизненно важно, чтобы я указал путь к файлу примера, который в данном случае будет libs/core/src/lib/i18n/lang-en.lang.ts:

export const lang: I18nCore = {
  errorDefault: 'An error has occurred',
  language: 'Language',
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Чтобы уменьшить дублирование кода и получить максимальную отдачу от процесса разработки, мы также создадим фабрику DI. Вот рабочий пример, использующий NgRx (опять же, это совершенно необязательно, вы можете использовать для этого BehaviorSubject):

export const I18N_CORE =
  new InjectionToken<Observable<I18nCore>>('I18N_CORE');

export const i18nCoreFactory: Ti18nFactory<I18nCore> =
  (store: Store): Observable<I18nCore> => 
    (store as Store<LocalePartialState>).pipe(
      select(getLocaleLanguageCode),
      distinctUntilChanged(),
      switchMap((code: LanguageCode) =>
        import(`./lang-${code}.lang`)
          .then((l: { lang: I18nCore }) => l.lang)
      ),
    );

export const i18nCoreProvider: FactoryProvider = {
  provide: I18N_CORE,
  useFactory: i18nCoreFactory,
  deps: [Store],
};
Вход в полноэкранный режим Выход из полноэкранного режима

Очевидно, что селектор getLocaleLanguageCode будет выбирать код языка из Store.

Не забудьте включить файлы перевода в вашу компиляцию, так как на них нет прямых ссылок, поэтому они не будут включены автоматически. Для этого найдите соответствующий «tsconfig» (тот, в котором указан «main.ts») и добавьте в массив «include» следующее:

"../../libs/core/src/lib/i18n/*.lang.ts"
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что путь к файлу здесь включает подстановочный знак, так что все ваши переводы будут включены сразу. Кроме того, по вкусу, я люблю ставить префикс для похожих файлов, что в значительной степени объясняет, почему название примера ([prefix]-[langCode].lang.ts) выглядит так странно.

Строки, специфичные для модуля

Давайте сделаем то же самое для любого модуля, чтобы увидеть, как переводы будут загружаться отдельно в браузере. Чтобы не усложнять, этот модуль будет называться «tab1».

Опять же, начнем с интерфейса:

export interface I18nTab1 {
  country: string;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Реализуйте этот интерфейс:

export const lang: I18nTab1 = {
  country: 'Country',
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Включите ваши переводы в компиляцию:

"../../libs/tab1/src/lib/i18n/*.lang.ts"
Войдите в полноэкранный режим Выйти из полноэкранного режима

И, по желанию, создайте фабрику DI, которая будет выглядеть буквально так же, как и предыдущая, но с другим интерфейсом.


Предоставление переводов

Я предпочитаю уменьшить количество провайдеров, поэтому «основные» переводы будут перечислены только в AppModule:

providers: [i18nCoreProvider],
Войти в полноэкранный режим Выход из полноэкранного режима

Любой другой перевод должен быть предоставлен только в соответствующих модулях — либо в лениво загружаемых модулях функций, либо, если вы следуете шаблону SCAM, в модулях компонентов:

@NgModule({
  declarations: [TabComponent],
  imports: [CommonModule, ReactiveFormsModule],
  providers: [i18nTab1Provider],
})
export class TabModule {}
Войти в полноэкранный режим Выход из полноэкранного режима

Также обратите внимание на элегантность использования готовых FactoryProviders вместо добавления объектов здесь.

Вставьте маркеры в component.ts:

constructor(
  @Inject(I18N_CORE)
  public readonly i18nCore$: Observable<I18nCore>,
  @Inject(I18N_TAB1)
  public readonly i18nTab1$: Observable<I18nTab1>,
) {}
Вход в полноэкранный режим Выход из полноэкранного режима

И, наконец, оберните component.html с помощью ng-container и простого оператора ngIf:

<ng-container *ngIf="{
    core: i18nCore$ | async,
    tab1: i18nTab1$ | async
  } as i18n">
    <p>{{ i18n.core?.language }}</p>
    <p>{{ i18n.tab1?.country }}: n/a</p>
</ng-container>
Войти в полноэкранный режим Выйти из полноэкранного режима

Проверяем результат

Давайте запустим это и посмотрим, работает ли это на самом деле и, что более важно, как именно будут загружаться эти переводы. Я создал простое демонстрационное приложение, состоящее из двух лениво загруженных модулей Angular, так что вы можете клонировать его и поэкспериментировать с ним. А пока вот фактические скриншоты DevTools:

Это начальная загрузка страницы в режиме разработки; обратите внимание на два файла .js в самом конце — мы создали их в предыдущем разделе.

Вот как это выглядит при переключении языка. Вкладка Сеть была сброшена в демонстрационных целях.

А это результат переключения на вторую ленивую вкладку.


Преимущества

  • С помощью этого решения вы сможете, но не обязаны, разделить свои переводы на несколько файлов любым удобным для вас способом;
  • Оно реактивное, что означает, что при правильной реализации оно обеспечит вашим пользователям бесперебойную работу;
  • Он не требует установки ничего, что не поставляется с Angular из коробки;
  • Он легко отлаживается и полностью настраивается, поскольку он будет реализован непосредственно в вашем проекте;
  • Поддерживаются сложные разрешения локали, такие как привязка к языку браузера, получение региональных настроек из учетной записи пользователя при авторизации и переопределение с помощью пользовательского языка — и все это без единой перезагрузки страницы;
  • Он также поддерживает завершение кода в современных IDE.

Недостатки

  • Поскольку эти файлы перевода не будут включены в активы, их необходимо транспонировать, что несколько увеличит время сборки;
  • Для обмена переводами с платформой локализации необходимо создать пользовательскую утилиту или использовать стороннее решение;
  • Может не очень хорошо работать с поисковыми системами без надлежащего рендеринга на стороне сервера.

GitHub

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

Фотография на обложке от Nareeta Martin на Unsplash

Оцените статью
devanswers.ru
Добавить комментарий