
Динамические переводы в Angular стали возможными
Практическое руководство по реализации переводов с ленивой загрузкой
Если вы когда-либо имели дело с интернационализацией (или для краткости i18n) в Angular или собираетесь ее реализовать, вы можете придерживаться официального руководства, которое является замечательным, использовать сторонние пакеты, которые могут быть сложными для отладки, или выбрать альтернативный путь, который я опишу ниже.
Одной из распространенных ошибок при использовании i18n является большой размер файлов перевода и невозможность разделить их, чтобы скрыть части вашего приложения от посторонних глаз. Некоторые решения, такие как встроенная реализация Angular, действительно мощные и SEO-совместимые, но требуют большой подготовки и не поддерживают переключение языков на лету в режиме разработки (что доставляло проблемы по крайней мере в версии 9); другие решения, такие как ngx-translate, требуют установки нескольких пакетов и по-прежнему не поддерживают разделение одного языка (обновление: на самом деле ngx-translate поддерживает это).
Хотя для этой сложной функции, которая поддерживает все и подходит всем, не существует «волшебной палочки», вот еще один способ реализации переводов, который может соответствовать вашим потребностям.
Достаточно введения, я обещал, что это будет практическое руководство, так что давайте прыгать прямо в него.
Подготовка основы
Первый шаг — создать тип для языков, которые будут использоваться в приложении:
export type LanguageCode = 'en' | 'de';
Одной из любимых функций Angular является внедрение зависимостей, которое очень много делает для нас — давайте использовать его для наших нужд. Я также хотел бы немного оживить ситуацию, используя NgRx для этого руководства, но если вы не используете его в своем проекте, не стесняйтесь заменить его простым BehaviorSubject.
В качестве дополнительного шага, который упростит дальнейшую разработку с NgRx, создайте тип для фабрик DI:
export type Ti18nFactory<Part> = (store: Store) => Observable<Part>;
Создание файлов перевода
Общие строки
Предположим, у нас есть несколько основных строк, которые мы хотели бы использовать в приложении. Некоторые простые, но общие вещи, которые никогда не связаны с конкретным модулем, функцией или библиотекой, такие как кнопки «ОК» или «Назад».
Мы поместим эти строки в «основной» модуль и начнем делать это с помощью простого интерфейса. это поможет нам не забыть ни одной строки в наших переводах:
export interface I18nCore {
errorDefault: string;
language: string;
}
Просто для ясности: этот интерфейс не гарантирует, что все строки будут действительно переведены, но компилятор TypeScript (и ваша IDE) выдаст ошибку «TS2741», если вы забудете включить какую-либо строку в свои «языковые» файлы.
Переходя к реализации интерфейса и для этого фрагмента жизненно важно предоставить пример пути к файлу, который в данном случае будет libs/core/src/lib/i18n/lang-en.lang.ts:
export const lang: I18nCore = {
errorDefault: 'An error has occurred',
language: 'Language',
};
Чтобы уменьшить дублирование кода и получить максимальную отдачу от процесса разработки, мы также создадим фабрику внедрения зависимостей. Вот рабочий пример с использованием 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"
И, при желании, создать фабрику внедрения зависимостей, которая будет выглядеть буквально так же, как предыдущая, но с другим интерфейсом.
Предоставление переводов
Я предпочитаю уменьшить количество провайдеров, поэтому «основные» переводы будут перечислены только в 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:



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