Практическое руководство по реализации лениво загружаемых переводов
Если вы когда-либо имели дело с интернационализацией (или сокращенно «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