diff --git a/src/app/app.server.module.ts b/src/app/app.server.module.ts index c2a6a3d1ea..7fcfe08c67 100644 --- a/src/app/app.server.module.ts +++ b/src/app/app.server.module.ts @@ -15,11 +15,13 @@ import { AppComponent } from './app.component'; import { AppModule } from './app.module'; export class UniversalErrorHandler implements ErrorHandler { - handleError(error: Error): void { + handleError(error: unknown): void { if (error instanceof HttpErrorResponse) { console.error('ERROR', error.message); - } else { + } else if (error instanceof Error) { console.error('ERROR', error.name, error.message, error.stack?.split('\n')?.[1]?.trim()); + } else { + console.error('ERROR', error?.toString()); } } } diff --git a/src/app/core/configurations/ngrx-state-transfer.ts b/src/app/core/configurations/ngrx-state-transfer.ts index 3afbe2110b..6e3c9defd1 100644 --- a/src/app/core/configurations/ngrx-state-transfer.ts +++ b/src/app/core/configurations/ngrx-state-transfer.ts @@ -9,7 +9,7 @@ import { mergeDeep } from 'ish-core/utils/functions'; export const NGRX_STATE_SK = makeStateKey('ngrxState'); -const STATE_ACTION_TYPE = '[Internal] Import NgRx State'; +export const STATE_ACTION_TYPE = '[Internal] Import NgRx State'; let transferredState: object; diff --git a/src/app/core/configurations/state-keys.ts b/src/app/core/configurations/state-keys.ts index 810c6723bd..a630d4441b 100644 --- a/src/app/core/configurations/state-keys.ts +++ b/src/app/core/configurations/state-keys.ts @@ -1,11 +1,7 @@ import { makeStateKey } from '@angular/platform-browser'; -import { Translations } from 'ish-core/internationalization.module'; - export const DISPLAY_VERSION = makeStateKey('displayVersion'); export const COOKIE_CONSENT_VERSION = makeStateKey('cookieConsentVersion'); export const SSR_LOCALE = makeStateKey('ssrLocale'); - -export const SSR_TRANSLATIONS = makeStateKey('ssrTranslation'); diff --git a/src/app/core/internationalization.module.ts b/src/app/core/internationalization.module.ts index 09e4f63b96..6d73c51d5b 100644 --- a/src/app/core/internationalization.module.ts +++ b/src/app/core/internationalization.module.ts @@ -1,94 +1,26 @@ -import { isPlatformBrowser, isPlatformServer, registerLocaleData } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; +import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import localeFr from '@angular/common/locales/fr'; -import { Inject, Injectable, LOCALE_ID, NgModule, PLATFORM_ID } from '@angular/core'; +import { Inject, LOCALE_ID, NgModule } from '@angular/core'; import { TransferState } from '@angular/platform-browser'; -import { Store, select } from '@ngrx/store'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { from, of } from 'rxjs'; -import { catchError, map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { MissingTranslationHandler, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { SSR_LOCALE, SSR_TRANSLATIONS } from './configurations/state-keys'; -import { getCurrentLocale, getRestEndpoint } from './store/core/configuration'; -import { whenTruthy } from './utils/operators'; - -export type Translations = Record>; - -function maybeJSON(val: string) { - if (val.startsWith('{')) { - try { - return JSON.parse(val); - } catch { - // default - } - } - return val; -} - -function filterAndTransformKeys(translations: Record): Translations { - return Object.entries(translations) - .filter(([key]) => key.startsWith('pwa-')) - .map(([key, value]) => [key.substring(4), maybeJSON(value)]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); -} - -@Injectable() -class ICMTranslateLoader implements TranslateLoader { - constructor( - private httpClient: HttpClient, - private transferState: TransferState, - @Inject(PLATFORM_ID) private platformId: string, - private store: Store - ) {} - - getTranslation(lang: string) { - if (isPlatformBrowser(this.platformId) && this.transferState.hasKey(SSR_TRANSLATIONS)) { - return of(this.transferState.get(SSR_TRANSLATIONS, undefined)); - } - const local$ = from(import(`../../assets/i18n/${lang}.json`)).pipe( - catchError(() => - this.store.pipe( - select(getCurrentLocale), - whenTruthy(), - take(1), - switchMap(defaultLang => from(import(`../../assets/i18n/${defaultLang}.json`))) - ) - ) - ); - return this.store.pipe( - select(getRestEndpoint), - whenTruthy(), - take(1), - switchMap(url => - this.httpClient.get(`${url};loc=${lang}/localizations`, { - params: { - searchKeys: 'pwa-', - }, - }) - ), - map(filterAndTransformKeys), - withLatestFrom(local$), - map(([translations, localTranslations]) => ({ - ...localTranslations, - ...translations, - })), - tap((translations: Translations) => { - if (isPlatformServer(this.platformId)) { - this.transferState.set(SSR_TRANSLATIONS, translations); - } - }), - catchError(() => local$) - ); - } -} +import { SSR_LOCALE } from './configurations/state-keys'; +import { + FALLBACK_LANG, + FallbackMissingTranslationHandler, +} from './utils/translate/fallback-missing-translation-handler'; +import { ICMTranslateLoader } from './utils/translate/icm-translate-loader'; @NgModule({ imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: ICMTranslateLoader }, + missingTranslationHandler: { provide: MissingTranslationHandler, useClass: FallbackMissingTranslationHandler }, + useDefaultLang: false, }), ], + providers: [{ provide: FALLBACK_LANG, useValue: 'en_US' }], }) export class InternationalizationModule { constructor( diff --git a/src/app/core/services/localizations/localizations.service.spec.ts b/src/app/core/services/localizations/localizations.service.spec.ts new file mode 100644 index 0000000000..a4cb1cc684 --- /dev/null +++ b/src/app/core/services/localizations/localizations.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { provideMockStore } from '@ngrx/store/testing'; + +import { LocalizationsService } from './localizations.service'; + +describe('Localizations Service', () => { + let localizationsService: LocalizationsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [provideMockStore()], + }); + localizationsService = TestBed.inject(LocalizationsService); + }); + + it('should be created', () => { + expect(localizationsService).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/localizations/localizations.service.ts b/src/app/core/services/localizations/localizations.service.ts new file mode 100644 index 0000000000..c5dc9664ad --- /dev/null +++ b/src/app/core/services/localizations/localizations.service.ts @@ -0,0 +1,57 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store, select } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { catchError, concatMap, map, take } from 'rxjs/operators'; + +import { getRestEndpoint } from 'ish-core/store/core/configuration'; +import { whenTruthy } from 'ish-core/utils/operators'; +import { Translations } from 'ish-core/utils/translate/translations.type'; + +function maybeJSON(val: string) { + if (val.startsWith('{')) { + try { + return JSON.parse(val); + } catch { + // default + } + } + return val; +} + +function filterAndTransformKeys(translations: Record): Translations { + return Object.entries(translations) + .filter(([key]) => key.startsWith('pwa-')) + .map(([key, value]) => [key.substring(4), maybeJSON(value)]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); +} + +@Injectable({ providedIn: 'root' }) +export class LocalizationsService { + private icmEndpoint$: Observable; + + constructor(private httpClient: HttpClient, store: Store) { + this.icmEndpoint$ = store.pipe(select(getRestEndpoint), whenTruthy(), take(1)); + } + + getServerTranslations(lang: string) { + return this.icmEndpoint$.pipe( + concatMap(url => + this.httpClient + .get(`${url};loc=${lang}/localizations`, { + params: { + searchKeys: 'pwa-', + }, + }) + .pipe(map(filterAndTransformKeys)) + ) + ); + } + + getSpecificTranslation(lang: string, key: string) { + return this.icmEndpoint$.pipe( + concatMap(url => this.httpClient.get(`${url};loc=${lang}/localizations/${key}`, { responseType: 'text' })), + catchError(() => of('')) + ); + } +} diff --git a/src/app/core/store/core/configuration/configuration.actions.ts b/src/app/core/store/core/configuration/configuration.actions.ts index cce577b344..db5513bf03 100644 --- a/src/app/core/store/core/configuration/configuration.actions.ts +++ b/src/app/core/store/core/configuration/configuration.actions.ts @@ -1,9 +1,35 @@ import { createAction } from '@ngrx/store'; -import { payload } from 'ish-core/utils/ngrx-creators'; +import { httpError, payload } from 'ish-core/utils/ngrx-creators'; +import { Translations } from 'ish-core/utils/translate/translations.type'; import { ConfigurationState } from './configuration.reducer'; type ConfigurationType = Partial; export const applyConfiguration = createAction('[Configuration] Apply Configuration', payload()); + +export const loadServerTranslations = createAction( + '[Configuration] Load Server Translations', + payload<{ lang: string }>() +); + +export const loadServerTranslationsSuccess = createAction( + '[Configuration] Load Server Translations Success', + payload<{ lang: string; translations: Translations }>() +); + +export const loadServerTranslationsFail = createAction( + '[Configuration] Load Server Translations Fail', + httpError<{ lang: string }>() +); + +export const loadSingleServerTranslation = createAction( + '[Configuration] Load Single Server Translation', + payload<{ lang: string; key: string }>() +); + +export const loadSingleServerTranslationSuccess = createAction( + '[Configuration] Load Single Server Translation Success', + payload<{ lang: string; key: string; translation: string }>() +); diff --git a/src/app/core/store/core/configuration/configuration.effects.spec.ts b/src/app/core/store/core/configuration/configuration.effects.spec.ts index 0f5f95f897..03864c88f7 100644 --- a/src/app/core/store/core/configuration/configuration.effects.spec.ts +++ b/src/app/core/store/core/configuration/configuration.effects.spec.ts @@ -6,7 +6,9 @@ import { Action } from '@ngrx/store'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable, Subject, of } from 'rxjs'; import { take } from 'rxjs/operators'; +import { instance, mock } from 'ts-mockito'; +import { LocalizationsService } from 'ish-core/services/localizations/localizations.service'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; import { applyConfiguration } from './configuration.actions'; @@ -24,7 +26,10 @@ describe('Configuration Effects', () => { CoreStoreModule.forTesting(['configuration', 'serverConfig'], [ConfigurationEffects]), TranslateModule.forRoot(), ], - providers: [provideMockActions(() => actions$)], + providers: [ + provideMockActions(() => actions$), + { provide: LocalizationsService, useFactory: () => instance(mock(LocalizationsService)) }, + ], }); effects = TestBed.inject(ConfigurationEffects); diff --git a/src/app/core/store/core/configuration/configuration.effects.ts b/src/app/core/store/core/configuration/configuration.effects.ts index 0c06d71f5a..085fba519b 100644 --- a/src/app/core/store/core/configuration/configuration.effects.ts +++ b/src/app/core/store/core/configuration/configuration.effects.ts @@ -6,10 +6,11 @@ import { Store, select } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { defer, fromEvent, iif, merge } from 'rxjs'; import { - debounceTime, + distinct, distinctUntilChanged, map, mapTo, + mergeMap, shareReplay, switchMap, take, @@ -21,10 +22,25 @@ import { LARGE_BREAKPOINT_WIDTH, MEDIUM_BREAKPOINT_WIDTH } from 'ish-core/config import { NGRX_STATE_SK } from 'ish-core/configurations/ngrx-state-transfer'; import { SSR_LOCALE } from 'ish-core/configurations/state-keys'; import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; -import { distinctCompareWith, mapToProperty, whenTruthy } from 'ish-core/utils/operators'; +import { LocalizationsService } from 'ish-core/services/localizations/localizations.service'; +import { + distinctCompareWith, + mapErrorToAction, + mapToPayload, + mapToPayloadProperty, + mapToProperty, + whenTruthy, +} from 'ish-core/utils/operators'; import { StatePropertiesService } from 'ish-core/utils/state-transfer/state-properties.service'; -import { applyConfiguration } from './configuration.actions'; +import { + applyConfiguration, + loadServerTranslations, + loadServerTranslationsFail, + loadServerTranslationsSuccess, + loadSingleServerTranslation, + loadSingleServerTranslationSuccess, +} from './configuration.actions'; import { getCurrentLocale, getDeviceType } from './configuration.selectors'; @Injectable() @@ -38,7 +54,8 @@ export class ConfigurationEffects { @Inject(MEDIUM_BREAKPOINT_WIDTH) private mediumBreakpointWidth: number, @Inject(LARGE_BREAKPOINT_WIDTH) private largeBreakpointWidth: number, translateService: TranslateService, - appRef: ApplicationRef + appRef: ApplicationRef, + private localizationsService: LocalizationsService ) { appRef.isStable .pipe(takeWhile(() => isPlatformBrowser(platformId))) @@ -53,12 +70,10 @@ export class ConfigurationEffects { select(getCurrentLocale), mapToProperty('lang'), distinctUntilChanged(), - // https://github.com/ngx-translate/core/issues/1030 - debounceTime(0), whenTruthy(), switchMap(lang => languageChanged$.pipe(mapTo(lang), take(1))) ) - .subscribe((lang: string) => { + .subscribe(lang => { this.transferState.set(SSR_LOCALE, lang); translateService.use(lang); }); @@ -136,4 +151,30 @@ export class ConfigurationEffects { ) ) ); + + loadServerTranslations$ = createEffect(() => + this.actions$.pipe( + ofType(loadServerTranslations), + mapToPayloadProperty('lang'), + distinct(), + mergeMap(lang => + this.localizationsService.getServerTranslations(lang).pipe( + map(translations => loadServerTranslationsSuccess({ lang, translations })), + mapErrorToAction(loadServerTranslationsFail, { lang }) + ) + ) + ) + ); + + loadSingleServerTranslation$ = createEffect(() => + this.actions$.pipe( + ofType(loadSingleServerTranslation), + mapToPayload(), + mergeMap(({ lang, key }) => + this.localizationsService + .getSpecificTranslation(lang, key) + .pipe(map(translation => loadSingleServerTranslationSuccess({ lang, key, translation }))) + ) + ) + ); } diff --git a/src/app/core/store/core/configuration/configuration.integration.spec.ts b/src/app/core/store/core/configuration/configuration.integration.spec.ts index 29159a55a4..b0eeb5b6ca 100644 --- a/src/app/core/store/core/configuration/configuration.integration.spec.ts +++ b/src/app/core/store/core/configuration/configuration.integration.spec.ts @@ -6,12 +6,13 @@ import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { EMPTY } from 'rxjs'; -import { mock, when } from 'ts-mockito'; +import { instance, mock, when } from 'ts-mockito'; import { configurationMeta } from 'ish-core/configurations/configuration.meta'; import { ConfigurationGuard } from 'ish-core/guards/configuration.guard'; import { Locale } from 'ish-core/models/locale/locale.model'; import { ConfigurationService } from 'ish-core/services/configuration/configuration.service'; +import { LocalizationsService } from 'ish-core/services/localizations/localizations.service'; import { applyConfiguration, getFeatures, getRestEndpoint } from 'ish-core/store/core/configuration'; import { ConfigurationEffects } from 'ish-core/store/core/configuration/configuration.effects'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; @@ -45,7 +46,10 @@ describe('Configuration Integration', () => { ]), TranslateModule.forRoot(), ], - providers: [provideStoreSnapshots()], + providers: [ + provideStoreSnapshots(), + { provide: LocalizationsService, useFactory: () => instance(mock(LocalizationsService)) }, + ], }); router = TestBed.inject(Router); diff --git a/src/app/core/store/core/configuration/configuration.reducer.ts b/src/app/core/store/core/configuration/configuration.reducer.ts index 9492b081fe..67e4469038 100644 --- a/src/app/core/store/core/configuration/configuration.reducer.ts +++ b/src/app/core/store/core/configuration/configuration.reducer.ts @@ -2,10 +2,16 @@ import { createReducer, on } from '@ngrx/store'; import { Locale } from 'ish-core/models/locale/locale.model'; import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; +import { Translations } from 'ish-core/utils/translate/translations.type'; import { environment } from '../../../../../environments/environment'; -import { applyConfiguration } from './configuration.actions'; +import { + applyConfiguration, + loadServerTranslationsFail, + loadServerTranslationsSuccess, + loadSingleServerTranslationSuccess, +} from './configuration.actions'; export interface ConfigurationState { baseURL?: string; @@ -20,6 +26,7 @@ export interface ConfigurationState { defaultLocale?: string; locales?: Locale[]; lang?: string; + serverTranslations: { [lang: string]: Translations }; // not synced via state transfer _deviceType?: DeviceType; } @@ -35,10 +42,40 @@ const initialState: ConfigurationState = { defaultLocale: environment.defaultLocale, locales: environment.locales, lang: undefined, + serverTranslations: {}, _deviceType: environment.defaultDeviceType, }; +function setTranslations(state: ConfigurationState, lang: string, translations: Translations): ConfigurationState { + return { + ...state, + serverTranslations: { ...state.serverTranslations, [lang]: translations }, + }; +} + +function addSingleTranslation( + state: ConfigurationState, + lang: string, + key: string, + translation: string +): ConfigurationState { + return { + ...state, + serverTranslations: { + ...state.serverTranslations, + [lang]: { ...state.serverTranslations?.[lang], [key]: translation }, + }, + }; +} + export const configurationReducer = createReducer( initialState, - on(applyConfiguration, (state, action) => ({ ...state, ...action.payload })) + on(applyConfiguration, (state, action) => ({ ...state, ...action.payload })), + on(loadServerTranslationsSuccess, (state, action) => + setTranslations(state, action.payload.lang, action.payload.translations) + ), + on(loadServerTranslationsFail, (state, action) => setTranslations(state, action.payload.lang, {})), + on(loadSingleServerTranslationSuccess, (state, action) => + addSingleTranslation(state, action.payload.lang, action.payload.key, action.payload.translation) + ) ); diff --git a/src/app/core/store/core/configuration/configuration.selectors.ts b/src/app/core/store/core/configuration/configuration.selectors.ts index 5fd0920771..23932ecf7f 100644 --- a/src/app/core/store/core/configuration/configuration.selectors.ts +++ b/src/app/core/store/core/configuration/configuration.selectors.ts @@ -88,3 +88,9 @@ export const getIdentityProvider = createSelectorFactory< state.identityProvider && (state.identityProvider === 'ICM' ? { type: 'ICM' } : state.identityProviders?.[state.identityProvider]) ); + +export const getServerTranslations = (lang: string) => + createSelector(getConfigurationState, state => state.serverTranslations?.[lang]); + +export const getSpecificServerTranslation = (lang: string, key: string) => + createSelector(getServerTranslations(lang), translations => translations?.[key]); diff --git a/src/app/core/store/shopping/shopping-store.spec.ts b/src/app/core/store/shopping/shopping-store.spec.ts index d21d1d6bcf..addf8a6ea8 100644 --- a/src/app/core/store/shopping/shopping-store.spec.ts +++ b/src/app/core/store/shopping/shopping-store.spec.ts @@ -977,7 +977,7 @@ describe('Shopping Store', () => { categories: tree(A.123,A.123.456) @ngrx/router-store/cancel: routerState: {"url":"","params":{},"queryParams":{},"data":{}} - storeState: {"configuration":{"defaultLocale":"en_US","locales":[3],"_de... + storeState: {"configuration":{"defaultLocale":"en_US","locales":[3],"ser... event: {"id":1,"url":"/category/A.123.456/product/P3"} @ngrx/router-store/request: routerState: {"url":"","params":{},"queryParams":{},"data":{}} @@ -1026,7 +1026,7 @@ describe('Shopping Store', () => { error: {"name":"HttpErrorResponse","message":"error loading categor... @ngrx/router-store/cancel: routerState: {"url":"","params":{},"queryParams":{},"data":{}} - storeState: {"configuration":{"defaultLocale":"en_US","locales":[3],"_de... + storeState: {"configuration":{"defaultLocale":"en_US","locales":[3],"ser... event: {"id":1,"url":"/category/A.123.XXX"} @ngrx/router-store/request: routerState: {"url":"","params":{},"queryParams":{},"data":{}} diff --git a/src/app/core/utils/translate/fallback-missing-translation-handler.ts b/src/app/core/utils/translate/fallback-missing-translation-handler.ts new file mode 100644 index 0000000000..ff60b4c833 --- /dev/null +++ b/src/app/core/utils/translate/fallback-missing-translation-handler.ts @@ -0,0 +1,88 @@ +import { isPlatformBrowser } from '@angular/common'; +import { ErrorHandler, Inject, Injectable, InjectionToken, PLATFORM_ID } from '@angular/core'; +import { Store, select } from '@ngrx/store'; +import { + MissingTranslationHandler, + MissingTranslationHandlerParams, + TranslateLoader, + TranslateParser, +} from '@ngx-translate/core'; +import { memoize } from 'lodash-es'; +import { concat, defer, iif, of } from 'rxjs'; +import { filter, first, map, tap } from 'rxjs/operators'; + +import { getSpecificServerTranslation, loadSingleServerTranslation } from 'ish-core/store/core/configuration'; +import { whenTruthy } from 'ish-core/utils/operators'; + +export const FALLBACK_LANG = new InjectionToken('fallbackTranslateLanguage'); + +@Injectable() +export class FallbackMissingTranslationHandler implements MissingTranslationHandler { + constructor( + private parser: TranslateParser, + private translateLoader: TranslateLoader, + private errorHandler: ErrorHandler, + @Inject(FALLBACK_LANG) private fallback: string, + @Inject(PLATFORM_ID) private platformId: string, + private store: Store + ) {} + + private reportMissingTranslation = memoize<(lang: string, key: string) => void>( + (lang, key) => { + this.errorHandler.handleError(`missing translation in ${lang}: ${key}`); + }, + (lang, key) => lang + key + ); + + private retrieveSpecificTranslation(lang: string, key: string) { + return defer(() => + this.store.pipe( + select(getSpecificServerTranslation(lang, key)), + tap(translation => { + if (translation === undefined) { + this.store.dispatch(loadSingleServerTranslation({ lang, key })); + } + }), + filter(translation => translation !== undefined), + first() + ) + ); + } + + handle(params: MissingTranslationHandlerParams) { + if (params.interpolateParams || /^\w+(\.[\w-]+)+$/.test(params.key)) { + this.reportMissingTranslation(params.translateService.currentLang, params.key); + + const doSingleCheck = isPlatformBrowser(this.platformId) && /\berror\b/.test(params.key); + const isFallbackAvailable = params.translateService.currentLang !== this.fallback; + return concat( + // try API call with specific key + iif(() => doSingleCheck, this.retrieveSpecificTranslation(params.translateService.currentLang, params.key)), + // try fallback translations + iif( + () => isFallbackAvailable, + this.translateLoader.getTranslation(this.fallback).pipe( + map(translations => translations?.[params.key]), + tap(translation => { + if (!translation) { + this.reportMissingTranslation(this.fallback, params.key); + } + }), + first() + ) + ), + // try API call with fallback translation + iif(() => isFallbackAvailable && doSingleCheck, this.retrieveSpecificTranslation(this.fallback, params.key)), + // give up + of(params.key) + ).pipe( + // stop after first emission + whenTruthy(), + first(), + map(translation => this.parser.interpolate(translation, params.interpolateParams)), + map(translation => (PRODUCTION_MODE ? translation : 'TRANSLATE_ME ' + translation)) + ); + } + return params.key; + } +} diff --git a/src/app/core/utils/translate/icm-translate-loader.ts b/src/app/core/utils/translate/icm-translate-loader.ts new file mode 100644 index 0000000000..13525ed2af --- /dev/null +++ b/src/app/core/utils/translate/icm-translate-loader.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { TransferState } from '@angular/platform-browser'; +import { Actions, ROOT_EFFECTS_INIT, ofType } from '@ngrx/effects'; +import { Store, select } from '@ngrx/store'; +import { TranslateLoader } from '@ngx-translate/core'; +import { ReplaySubject, combineLatest, defer, from, of } from 'rxjs'; +import { catchError, first, map, switchMap, take, tap } from 'rxjs/operators'; + +import { NGRX_STATE_SK, STATE_ACTION_TYPE } from 'ish-core/configurations/ngrx-state-transfer'; +import { getServerTranslations, loadServerTranslations } from 'ish-core/store/core/configuration'; +import { whenTruthy } from 'ish-core/utils/operators'; + +@Injectable() +export class ICMTranslateLoader implements TranslateLoader { + private initialized$ = new ReplaySubject(1); + + constructor(private store: Store, actions: Actions, transferState: TransferState) { + const actionType = transferState.hasKey(NGRX_STATE_SK) ? STATE_ACTION_TYPE : ROOT_EFFECTS_INIT; + actions.pipe(ofType(actionType), first()).subscribe(() => { + this.initialized$.next(true); + }); + } + + getTranslation(lang: string) { + const local$ = defer(() => from(import(`../../../../assets/i18n/${lang}.json`)).pipe(catchError(() => of({})))); + const server$ = this.store.pipe( + select(getServerTranslations(lang)), + tap(translations => { + if (!translations) { + this.store.dispatch(loadServerTranslations({ lang })); + } + }), + whenTruthy(), + take(1) + ); + return this.initialized$.pipe( + switchMap(() => + combineLatest([local$, server$]).pipe( + map(([localTranslations, serverTranslations]) => ({ + ...localTranslations, + ...serverTranslations, + })) + ) + ) + ); + } +} diff --git a/src/app/core/utils/translate/translations.type.ts b/src/app/core/utils/translate/translations.type.ts new file mode 100644 index 0000000000..ba69d0d147 --- /dev/null +++ b/src/app/core/utils/translate/translations.type.ts @@ -0,0 +1 @@ +export type Translations = Record>;