Динамические переводы в 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.
Недостатки
- Поскольку эти файлы перевода не будут включены в активы, их фактически следует транспилировать, что немного увеличит время сборки;
- Это требует от вас создания собственной утилиты или использования стороннего решения для обмена вашими переводами с платформой локализации;
- Это может не очень хорошо работать с поисковыми системами без надлежащего рендеринга на стороне сервера.
Гитхаб
Не стесняйтесь экспериментировать с полностью рабочим примером, который доступен в этом репозитории.
Оставайтесь позитивными и создавайте отличные приложения!