diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index d951f965ee..c34b4a2fab 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -27,6 +27,7 @@ import { CookieLanguageProvider } from './providers/cookie-language.provider'; import { LocaleProvider } from './providers/locale.provider'; import { LocalizationService } from './services/localization.service'; import { oAuthStorage } from './strategies/auth-flow.strategy'; +import { localizationContributor, LOCALIZATIONS } from './tokens/localization.token'; import { coreOptionsFactory, CORE_OPTIONS } from './tokens/options.token'; import { TENANT_KEY } from './tokens/tenant-key.token'; import { noop } from './utils/common-utils'; @@ -176,6 +177,26 @@ export class CoreModule { }, { provide: OAuthStorage, useFactory: storageFactory }, { provide: TENANT_KEY, useValue: options.tenantKey || '__tenant' }, + { + provide: LOCALIZATIONS, + multi: true, + useValue: localizationContributor(options.localizations), + deps: [LocalizationService], + }, + ], + }; + } + + static forChild(options = {} as ABP.Child): ModuleWithProviders { + return { + ngModule: RootCoreModule, + providers: [ + { + provide: LOCALIZATIONS, + multi: true, + useValue: localizationContributor(options.localizations), + deps: [LocalizationService], + }, ], }; } diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index d99d591ad4..a696eb8a00 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -11,6 +11,21 @@ export namespace ABP { skipGetAppConfiguration?: boolean; sendNullsAsQueryParam?: boolean; tenantKey?: string; + localizations?: Localization[]; + } + + export interface Child { + localizations?: Localization[]; + } + + export interface Localization { + culture: string; + resources: LocalizationResource[]; + } + + export interface LocalizationResource { + resourceName: string; + texts: Record; } export interface HasPolicy { diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index f7c0315568..0e67aea119 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -1,10 +1,11 @@ import { registerLocaleData } from '@angular/common'; import { Injectable, Injector, isDevMode, Optional, SkipSelf } from '@angular/core'; -import { from, Observable, Subject } from 'rxjs'; +import { BehaviorSubject, combineLatest, from, Observable, Subject } from 'rxjs'; import { filter, map, mapTo, switchMap } from 'rxjs/operators'; import { ABP } from '../models/common'; import { LocalizationWithDefault } from '../models/localization'; import { ApplicationConfigurationDto } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models'; +import { localizations$ } from '../tokens/localization.token'; import { CORE_OPTIONS } from '../tokens/options.token'; import { createLocalizer, createLocalizerWithFallback } from '../utils/localization-utils'; import { interpolate } from '../utils/string-utils'; @@ -16,6 +17,12 @@ export class LocalizationService { private latestLang = this.sessionState.getLanguage(); private _languageChange$ = new Subject(); + private localLocalizations$ = new BehaviorSubject( + new Map>>(), + ); + + private localizations$ = new BehaviorSubject(new Map>()); + /** * Returns currently selected language */ @@ -38,6 +45,62 @@ export class LocalizationService { if (otherInstance) throw new Error('LocalizationService should have only one instance.'); this.listenToSetLanguage(); + this.initLocalizationValues(); + } + + private initLocalizationValues() { + localizations$.subscribe(val => this.addLocalization(val)); + + const remoteLocalizations$ = this.configState.getDeep$('localization.values') as Observable< + Record> + >; + const currentLanguage$ = this.sessionState.getLanguage$(); + + const localLocalizations$ = combineLatest([currentLanguage$, this.localLocalizations$]).pipe( + map(([currentLang, localizations]) => localizations.get(currentLang)), + ); + + combineLatest([remoteLocalizations$, localLocalizations$]) + .pipe( + map(([remote, local]) => { + if (remote) { + Object.entries(remote).forEach(entry => { + const resourceName = entry[0]; + const remoteTexts = entry[1]; + let resource = local.get(resourceName) || {}; + resource = { ...resource, ...remoteTexts }; + + local.set(resourceName, resource); + }); + } + + return local; + }), + ) + .subscribe(val => this.localizations$.next(val)); + } + + addLocalization(localizations?: ABP.Localization[]) { + if (!localizations) return; + + const localizationMap = this.localLocalizations$.value; + + localizations.forEach(loc => { + const cultureMap = + localizationMap.get(loc.culture) || new Map>(); + + loc.resources.forEach(res => { + let resource: Record = cultureMap.get(res.resourceName) || {}; + + resource = { ...resource, ...res.texts }; + + cultureMap.set(res.resourceName, resource); + }); + + localizationMap.set(loc.culture, cultureMap); + }); + + this.localLocalizations$.next(localizationMap); } private listenToSetLanguage() { @@ -70,15 +133,15 @@ export class LocalizationService { get(key: string | LocalizationWithDefault, ...interpolateParams: string[]): Observable { return this.configState .getAll$() - .pipe(map(state => getLocalization(state, key, ...interpolateParams))); + .pipe(map(state => this.getLocalization(state, key, ...interpolateParams))); } getResource(resourceName: string) { - return this.configState.getDeep(`localization.values.${resourceName}`); + return this.localizations$.value.get(resourceName); } getResource$(resourceName: string) { - return this.configState.getDeep$(`localization.values.${resourceName}`); + return this.localizations$.pipe(map(res => res.get(resourceName))); } /** @@ -87,7 +150,7 @@ export class LocalizationService { * @param interpolateParams Values to intepolate. */ instant(key: string | LocalizationWithDefault, ...interpolateParams: string[]): string { - return getLocalization(this.configState.getAll(), key, ...interpolateParams); + return this.getLocalization(this.configState.getAll(), key, ...interpolateParams); } localize(resourceName: string, key: string, defaultValue: string): Observable { @@ -117,60 +180,62 @@ export class LocalizationService { const localization = this.configState.getOne('localization'); return createLocalizerWithFallback(localization)(resourceNames, keys, defaultValue); } -} -function getLocalization( - state: ApplicationConfigurationDto, - key: string | LocalizationWithDefault, - ...interpolateParams: string[] -) { - if (!key) key = ''; - let defaultValue: string; + private getLocalization( + state: ApplicationConfigurationDto, + key: string | LocalizationWithDefault, + ...interpolateParams: string[] + ) { + if (!key) key = ''; + let defaultValue: string; - if (typeof key !== 'string') { - defaultValue = key.defaultValue; - key = key.key; - } + if (typeof key !== 'string') { + defaultValue = key.defaultValue; + key = key.key; + } - const keys = key.split('::') as string[]; - const warn = (message: string) => { - if (isDevMode) console.warn(message); - }; + const keys = key.split('::') as string[]; + const warn = (message: string) => { + if (isDevMode) console.warn(message); + }; - if (keys.length < 2) { - warn('The localization source separator (::) not found.'); - return defaultValue || (key as string); - } - if (!state.localization) return defaultValue || keys[1]; + if (keys.length < 2) { + warn('The localization source separator (::) not found.'); + return defaultValue || (key as string); + } + if (!state.localization) return defaultValue || keys[1]; - const sourceName = keys[0] || state.localization.defaultResourceName; - const sourceKey = keys[1]; + const sourceName = keys[0] || state.localization.defaultResourceName; + const sourceKey = keys[1]; - if (sourceName === '_') { - return defaultValue || sourceKey; - } + if (sourceName === '_') { + return defaultValue || sourceKey; + } - if (!sourceName) { - warn('Localization source name is not specified and the defaultResourceName was not defined!'); + if (!sourceName) { + warn( + 'Localization source name is not specified and the defaultResourceName was not defined!', + ); - return defaultValue || sourceKey; - } + return defaultValue || sourceKey; + } - const source = state.localization.values[sourceName]; - if (!source) { - warn('Could not find localization source: ' + sourceName); - return defaultValue || sourceKey; - } + const source = this.localizations$.value.get(sourceName); + if (!source) { + warn('Could not find localization source: ' + sourceName); + return defaultValue || sourceKey; + } - let localization = source[sourceKey]; - if (typeof localization === 'undefined') { - return defaultValue || sourceKey; - } + let localization = source[sourceKey]; + if (typeof localization === 'undefined') { + return defaultValue || sourceKey; + } - interpolateParams = interpolateParams.filter(params => params != null); - if (localization) localization = interpolate(localization, interpolateParams); + interpolateParams = interpolateParams.filter(params => params != null); + if (localization) localization = interpolate(localization, interpolateParams); - if (typeof localization !== 'string') localization = ''; + if (typeof localization !== 'string') localization = ''; - return localization || defaultValue || (key as string); + return localization || defaultValue || (key as string); + } } diff --git a/npm/ng-packs/packages/core/src/lib/tokens/index.ts b/npm/ng-packs/packages/core/src/lib/tokens/index.ts index 2b7028ce53..da1ea9bdad 100644 --- a/npm/ng-packs/packages/core/src/lib/tokens/index.ts +++ b/npm/ng-packs/packages/core/src/lib/tokens/index.ts @@ -1,6 +1,7 @@ export * from './app-config.token'; export * from './cookie-language-key.token'; export * from './list.token'; +export * from './localization.token'; export * from './lodaer-delay.token'; export * from './manage-profile.token'; export * from './options.token'; diff --git a/npm/ng-packs/packages/core/src/lib/tokens/localization.token.ts b/npm/ng-packs/packages/core/src/lib/tokens/localization.token.ts new file mode 100644 index 0000000000..e4fd2eeb3f --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tokens/localization.token.ts @@ -0,0 +1,13 @@ +import { InjectionToken } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { ABP } from '../models/common'; + +export const LOCALIZATIONS = new InjectionToken('LOCALIZATIONS'); + +export function localizationContributor(localizations: ABP.Localization[]) { + if (localizations) { + localizations$.next([...localizations$.value, ...localizations]); + } +} + +export const localizations$ = new BehaviorSubject([]);