Добавление поддержки нескольких языков в NestJS и Angular приложениях
В этой статье я добавлю поддержку нескольких языков в NestJS
и Angular
приложениях, для сообщений в ошибках, уведомлениях и данных полученных из базы данных.
1. Устанавливаем все необходимые библиотеки
Команды
npm install --save @jsverse/transloco nestjs-translates class-validator-multi-lang class-transformer-global-storage @jsverse/transloco-keys-manager
Так как мы используем внешние генераторы, то мы не имеем доступа к сгенерированному коду, но д ля возможности перевода ошибок валидации нам нужно использовать библиотеку class-validator-multi-lang
вместо class-validator
, которую добавляет генератор.
Для подмены импортов в тайпскрипт файлах установим и подключим веб-пак плагин для замены строк.
Команды
npm install --save string-replace-loader
Прописываем правила замены в нашем веб-пак конфиге.
Обновляем файл apps/server/webpack.config.js
const { composePlugins, withNx } = require('@nx/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(
withNx({
sourceMap: true,
target: 'node',
}),
(config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
config.module.rules = [
...config.module.rules,
{
test: /\.(ts)$/,
loader: 'string-replace-loader',
options: {
search: `class-validator`,
replace: `class-validator-multi-lang`,
flags: 'g',
},
},
{
test: /\.(ts)$/,
loader: 'string-replace-loader',
options: {
search: 'class-transformer',
replace: 'class-transformer-global-storage',
flags: 'g',
},
},
];
return config;
}
);
2. Добавляем поддержку переводов в Angular-приложении
Добавляем новый модуль в конфиг фронтенда.
Обновляем файл apps/client/src/app/app.config.ts
import { provideTransloco } from '@jsverse/transloco';
import { marker } from '@jsverse/transloco-keys-manager/marker';
import { AUTHORIZER_URL } from '@nestjs-mod-fullstack/auth-angular';
import { TranslocoHttpLoader } from './integrations/transloco-http.loader';
export const appConfig = ({ authorizerURL, minioURL }: { authorizerURL: string; minioURL: string }): ApplicationConfig => {
return {
providers: [
// ...
provideTransloco({
config: {
availableLangs: [
{
id: marker('en'),
label: marker('app.locale.name.english'),
},
{
id: marker('ru'),
label: marker('app.locale.name.russian'),
},
],
defaultLang: 'en',
fallbackLang: 'en',
reRenderOnLangChange: true,
prodMode: true,
missingHandler: {
logMissingKey: true,
useFallbackTranslation: true,
allowEmpty: true,
},
},
loader: TranslocoHttpLoader,
}),
],
};
};
Для загрузки переводов из интернета необходимо создать специальный загрузчик.
Создаем файл apps/client/src/app/integrations/transloco-http.loader.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Translation, TranslocoLoader } from '@jsverse/transloco';
import { catchError, forkJoin, map, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
constructor(private readonly httpClient: HttpClient) {}
getTranslation(lang: string) {
return forkJoin({
translation: this.httpClient.get<Translation>(`./assets/i18n/${lang}.json`).pipe(
catchError(() => {
return of({});
})
),
vendor: this.httpClient.get(`./assets/i18n/${lang}.vendor.json`).pipe(
catchError(() => {
return of({});
})
),
}).pipe(
map(({ translation, vendor }) => {
const dictionaries = {
...translation,
...Object.keys(vendor).reduce((all, key) => ({ ...all, ...vendor[key] }), {}),
};
for (const key in dictionaries) {
if (Object.prototype.hasOwnProperty.call(dictionaries, key)) {
const value = dictionaries[key];
if (!value && value !== 'empty') {
delete dictionaries[key];
}
}
}
return dictionaries;
})
);
}
}
Загрузка переводов будет происходить при запуске приложения
Обновляем файл apps/client/src/app/app-initializer.ts
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { TranslocoService } from '@jsverse/transloco';
import { AppRestService, AuthorizerRestService, FilesRestService, TimeRestService, WebhookRestService } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { AuthService, TokensService } from '@nestjs-mod-fullstack/auth-angular';
import { catchError, map, merge, mergeMap, of, Subscription, tap, throwError } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AppInitializer {
private subscribeToTokenUpdatesSubscription?: Subscription;
constructor(
// ..
private readonly translocoService: TranslocoService,
private readonly tokensService: TokensService
) {}
resolve() {
this.subscribeToTokenUpdates();
return (
this.authService.getAuthorizerClientID()
? of(null)
: this.authorizerRestService.authorizerControllerGetAuthorizerClientID().pipe(
map(({ clientID }) => {
this.authService.setAuthorizerClientID(clientID);
return null;
})
)
).pipe(
// ..
mergeMap(() => {
const lang = localStorage.getItem('activeLang') || this.translocoService.getDefaultLang();
this.translocoService.setActiveLang(lang);
localStorage.setItem('activeLang', lang);
return this.translocoService.load(lang);
})
// ..
);
}
private subscribeToTokenUpdates() {
if (this.subscribeToTokenUpdatesSubscription) {
this.subscribeToTokenUpdatesSubscription.unsubscribe();
this.subscribeToTokenUpdatesSubscription = undefined;
}
this.subscribeToTokenUpdatesSubscription = merge(this.tokensService.tokens$, this.translocoService.langChanges$)
.pipe(
tap(() => {
// ..
})
)
.subscribe();
}
}
Язык по умолчанию будет стоять Английский
. Для переключения языка в навигационном меню добавим выпадающий список с доступными для переключения языками.
Обновляем файл apps/client/src/app/app.component.ts
import { LangDefinition, TranslocoDirective, TranslocoPipe, TranslocoService } from '@jsverse/transloco';
import { marker } from '@jsverse/transloco-keys-manager/marker';
import { AppRestService, TimeRestService } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
// ...
@UntilDestroy()
@Component({
standalone: true,
imports: [RouterModule, NzMenuModule, NzLayoutModule, NzTypographyModule, AsyncPipe, NgForOf, NgFor, TranslocoPipe, TranslocoDirective],
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
title = marker('client');
serverMessage$ = new BehaviorSubject('');
serverTime$ = new BehaviorSubject('');
authUser$?: Observable<User | undefined>;
lang$ = new BehaviorSubject<string>('');
availableLangs$ = new BehaviorSubject<LangDefinition[]>([]);
constructor(
// ...
private readonly appRestService: AppRestService,
private readonly translocoService: TranslocoService
) {}
ngOnInit() {
this.loadAvailableLangs();
this.subscribeToLangChanges();
this.fillServerMessage().pipe(untilDestroyed(this)).subscribe();
// ...
}
setActiveLang(lang: string) {
this.translocoService.setActiveLang(lang);
localStorage.setItem('activeLang', lang);
}
private loadAvailableLangs() {
this.availableLangs$.next(this.translocoService.getAvailableLangs() as LangDefinition[]);
}
private subscribeToLangChanges() {
this.translocoService.langChanges$
.pipe(
tap((lang) => this.lang$.next(lang)),
mergeMap(() => this.fillServerMessage()),
untilDestroyed(this)
)
.subscribe();
}
// ...
private fillServerMessage() {
return this.appRestService.appControllerGetData().pipe(tap((result) => this.serverMessage$.next(result.message)));
}
}