@o3r/localization
Version:
This module provides a runtime dynamic language/translation support and debug tools.
1,061 lines (1,036 loc) • 57.4 kB
JavaScript
import { immutablePrimitive, deepFill, otterComponentInfoPropertyName, sendOtterMessage, filterMessageContent } from '@o3r/core';
import * as i0 from '@angular/core';
import { InjectionToken, inject, Injectable, NgModule, ElementRef, ChangeDetectorRef, Input, Directive, Pipe, RendererFactory2, Optional, LOCALE_ID, Injector, ApplicationRef, DestroyRef } from '@angular/core';
import { TranslateCompiler, TranslateService, TranslateDirective, TranslatePipe, TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { IntlMessageFormat } from 'intl-messageformat';
import { BehaviorSubject, combineLatest, firstValueFrom, of, from, lastValueFrom, fromEvent } from 'rxjs';
import * as i1 from '@ngrx/store';
import { createAction, props, on, createReducer, StoreModule, createFeatureSelector, createSelector, Store, select } from '@ngrx/store';
import { LoggerService } from '@o3r/logger';
import { startWith, switchMap, map, distinctUntilChanged, shareReplay, catchError } from 'rxjs/operators';
import { Directionality, DIR_DOCUMENT, BidiModule } from '@angular/cdk/bidi';
import { CurrencyPipe, DatePipe, DecimalPipe, CommonModule } from '@angular/common';
import { DynamicContentModule, DynamicContentService } from '@o3r/dynamic-content';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
/**
* Decorator to pass localization url
* @param _url
*/
// eslint-disable-next-line @typescript-eslint/naming-convention -- decorator should start with a capital letter
function Localization(_url) {
return (target, key) => {
const privateField = _url || `_${key}`;
const privateValue = target[key];
if (delete target[key]) {
Object.defineProperty(target, key, {
get: function () {
return this[privateField];
},
set: function (value) {
const currentField = this[privateField] || privateValue;
this[privateField] = typeof currentField === 'undefined' ? immutablePrimitive(value) : deepFill(currentField, value);
if (this[otterComponentInfoPropertyName]) {
this[otterComponentInfoPropertyName].translations = this[privateField];
}
},
enumerable: true,
configurable: true
});
}
};
}
/**
* Default configuration for LocalizationModule
*/
const DEFAULT_LOCALIZATION_CONFIGURATION = {
supportedLocales: [],
endPointUrl: '',
useDynamicContent: false,
rtlLanguages: ['ar', 'he'],
fallbackLanguage: 'en',
bundlesOutputPath: '',
debugMode: false,
enableTranslationDeactivation: false,
mergeWithLocalTranslations: false
};
/**
* Message format configuration default value
*/
const lazyMessageDefaultConfig = {
enableCache: true,
ignoreTag: true
};
/** Message Format configuration Token */
const MESSAGE_FORMAT_CONFIG = new InjectionToken('Message Format configuration');
/**
* This compiler expects ICU syntax and compiles the expressions with messageformat.js
* Compare to ngx-translate-messageformat-compiler package, the compilation of the translation is done only on demand
*/
class TranslateMessageFormatLazyCompiler extends TranslateCompiler {
constructor() {
const config = inject(MESSAGE_FORMAT_CONFIG, { optional: true });
super();
/** Cache of compiled translations */
this.cache = {};
this.config = config ? { ...lazyMessageDefaultConfig, ...config } : lazyMessageDefaultConfig;
}
/**
* Clear the cache of the compiled translations
*/
clearCache() {
this.cache = {};
}
/** @inheritDoc */
compile(value, lang) {
return (params) => new IntlMessageFormat(value, lang, undefined, this.config).format(params);
}
/** @inheritDoc */
compileTranslations(translations, lang) {
const compilingStrategy = this.config.enableCache
? (acc, key) => {
acc[key] = (params) => {
const cached = this.cache[`${lang}_${key}`];
if (cached) {
return cached.format(params);
}
const newCachedItem = new IntlMessageFormat(translations[key], lang, undefined, this.config);
this.cache[`${lang}_${key}`] = newCachedItem;
return newCachedItem.format(params);
};
return acc;
}
: (acc, key) => {
acc[key] = (params) => new IntlMessageFormat(translations[key], lang, undefined, this.config).format(params);
return acc;
};
return Object.keys(translations).reduce((acc, key) => compilingStrategy(acc, key), {});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslateMessageFormatLazyCompiler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslateMessageFormatLazyCompiler }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslateMessageFormatLazyCompiler, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/** Actions */
const ACTION_SET = '[LocalizationOverride] set';
/**
* Clear all overrides and fill the store with the payload
*/
const setLocalizationOverride = createAction(ACTION_SET, props());
/**
* LocalizationOverride Store initial value
*/
const localizationOverrideInitialState = { localizationOverrides: {} };
/**
* List of basic actions for LocalizationOverride Store
*/
const localizationOverrideReducerFeatures = [
on(setLocalizationOverride, (_state, payload) => ({ ...payload.state }))
];
/**
* LocalizationOverride Store reducer
*/
const localizationOverrideReducer = createReducer(localizationOverrideInitialState, ...localizationOverrideReducerFeatures);
/**
* Name of the LocalizationOverride Store
*/
const LOCALIZATION_OVERRIDE_STORE_NAME = 'localizationOverride';
/** Token of the LocalizationOverride reducer */
const LOCALIZATION_OVERRIDE_REDUCER_TOKEN = new InjectionToken('Feature LocalizationOverride Reducer');
/** Provide default reducer for LocalizationOverride store */
function getDefaultLocalizationOverrideReducer() {
return localizationOverrideReducer;
}
class LocalizationOverrideStoreModule {
static forRoot(reducerFactory) {
return {
ngModule: LocalizationOverrideStoreModule,
providers: [
{ provide: LOCALIZATION_OVERRIDE_REDUCER_TOKEN, useFactory: reducerFactory }
]
};
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationOverrideStoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: LocalizationOverrideStoreModule, imports: [i1.StoreFeatureModule] }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationOverrideStoreModule, providers: [
{ provide: LOCALIZATION_OVERRIDE_REDUCER_TOKEN, useFactory: getDefaultLocalizationOverrideReducer }
], imports: [StoreModule.forFeature(LOCALIZATION_OVERRIDE_STORE_NAME, LOCALIZATION_OVERRIDE_REDUCER_TOKEN)] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationOverrideStoreModule, decorators: [{
type: NgModule,
args: [{
imports: [
StoreModule.forFeature(LOCALIZATION_OVERRIDE_STORE_NAME, LOCALIZATION_OVERRIDE_REDUCER_TOKEN)
],
providers: [
{ provide: LOCALIZATION_OVERRIDE_REDUCER_TOKEN, useFactory: getDefaultLocalizationOverrideReducer }
]
}]
}] });
/** Select LocalizationOverride State */
const selectLocalizationOverrideState = createFeatureSelector(LOCALIZATION_OVERRIDE_STORE_NAME);
/** Select all localization override map */
const selectLocalizationOverride = createSelector(selectLocalizationOverrideState, (state) => state?.localizationOverrides || {});
const localizationOverrideStorageDeserializer = (rawObject) => {
if (!rawObject) {
return localizationOverrideInitialState;
}
return rawObject;
};
const localizationOverrideStorageSync = {
deserialize: localizationOverrideStorageDeserializer
};
/** Localization Configuration Token */
const LOCALIZATION_CONFIGURATION_TOKEN = new InjectionToken('Localization Configuration injection token');
/**
* Service which is wrapping the configuration logic of TranslateService from ngx-translate
* Any application willing to use localization just needs to inject LocalizationService
* in the root component and call its configure() method.
*/
class LocalizationService {
constructor() {
this.translateService = inject(TranslateService);
this.logger = inject(LoggerService);
this.configuration = inject(LOCALIZATION_CONFIGURATION_TOKEN);
this.store = inject(Store, { optional: true });
this.localeSplitIdentifier = '-';
/**
* Internal subject that we use to track changes between keys only and translation mode
*/
this._showKeys$ = new BehaviorSubject(false);
/**
* _showKeys$ exposed as an Observable
*/
this.showKeys$ = this._showKeys$.asObservable();
void this.configure();
if (this.store) {
this.keyMapping$ = this.store.pipe(select(selectLocalizationOverride));
}
}
/**
* This will handle the fallback language hierarchy to find out fallback language.
* supportedLocales language has highest priority, next priority goes to fallbackLocalesMap and default would be
* fallbackLanguage.
* @param language Selected language.
* @returns selected language if supported, fallback language otherwise.
*/
checkFallbackLocalesMap(language) {
if (language && !this.configuration.supportedLocales.includes(language)) {
const closestSupportedLanguageCode = this.getFirstClosestSupportedLanguageCode(language);
const fallbackForLanguage = this.getFallbackMapLangCode(language);
const fallbackStrategyDebug = (fallbackForLanguage && ' associated fallback language ')
|| (closestSupportedLanguageCode && ' closest supported language ')
|| (this.configuration.fallbackLanguage && ' configured default language ');
const fallbackLang = fallbackForLanguage || closestSupportedLanguageCode || this.configuration.fallbackLanguage || language;
if (language !== fallbackLang) {
this.logger.debug(`Non supported languages ${language} will fallback to ${fallbackStrategyDebug} ${fallbackLang}`);
}
return fallbackLang;
}
else if (!language) {
this.logger.debug('Language is not defined');
}
return language;
}
/**
* This function checks if fallback language can be provided from fallbackLocalesMap.
* supportedLocales: ['en-GB', 'en-US', 'fr-FR'], fallbackLocalesMap: {'en-CA': 'en-US', 'de': 'fr-FR'}
* translate to en-CA -> fallback to en-US, translate to de-DE -> fallback to fr-FR
* translate to en-NZ -> fallback to en-GB
* @param language Selected language.
* @returns Fallback language if available, undefined otherwise.
*/
getFallbackMapLangCode(language) {
const fallbackLocalesMap = this.configuration.fallbackLocalesMap;
const [locale] = language.split(this.localeSplitIdentifier);
return fallbackLocalesMap && (fallbackLocalesMap[language] || fallbackLocalesMap[locale]);
}
/**
* This function checks if closest supported language available incase of selected language is not
* supported language.
* supportedLocales: ['en-GB', 'en-US', 'fr-FR']
* translate to en-CA -> fallback to en-GB
* @param language Selected language.
* @returns Closest supported language if available, undefined otherwise.
*/
getFirstClosestSupportedLanguageCode(language) {
const [locale] = language.split(this.localeSplitIdentifier);
const firstClosestRegx = new RegExp(`^${locale}${this.localeSplitIdentifier}?`, 'i');
return this.configuration.supportedLocales.find((supportedLang) => firstClosestRegx.test(supportedLang));
}
/**
* Returns a stream of translated values of a key which updates whenever the language changes.
* @param translationKey Key to translate
* @param interpolateParams Object to use in translation binding
* @returns A stream of the translated key
*/
getTranslationStream(translationKey, interpolateParams) {
const translation$ = this.translateService.onTranslationChange.pipe(startWith(undefined), switchMap(() => this.translateService.stream(translationKey, interpolateParams)), map((value) => this.configuration.debugMode ? `${translationKey} - ${value}` : value), distinctUntilChanged());
if (!this.configuration.enableTranslationDeactivation) {
return translation$;
}
return combineLatest([
translation$,
this.showKeys$
]).pipe(map(([value, showKeys]) => showKeys ? translationKey : value));
}
/**
* Configures TranslateService and registers locales. This method is called from the application level.
*/
async configure() {
const language = this.checkFallbackLocalesMap(this.configuration.language || this.configuration.fallbackLanguage);
this.translateService.addLangs(this.configuration.supportedLocales);
this.translateService.setDefaultLang(language);
await firstValueFrom(this.useLanguage(language));
}
/**
* Is the translation deactivation enabled
*/
isTranslationDeactivationEnabled() {
return this.configuration.enableTranslationDeactivation;
}
/**
* Wrapper to call the ngx-translate service TranslateService method getLangs().
*/
getLanguages() {
return this.translateService.getLangs();
}
/**
* Wrapper to call the ngx-translate service TranslateService method use(language).
* @param language
*/
useLanguage(language) {
language = this.checkFallbackLocalesMap(language);
return this.translateService.use(language);
}
/**
* Wrapper to get the ngx-translate service TranslateService currentLang.
*/
getCurrentLanguage() {
return this.translateService.currentLang;
}
/**
* Get the instance of the ngx-translate TranslateService used by LocalizationService.
*/
getTranslateService() {
return this.translateService;
}
/**
* Toggle the ShowKeys mode between active and inactive.
* @param value if specified, set the ShowKeys mode to value. If not specified, toggle the ShowKeys mode.
*/
toggleShowKeys(value) {
if (!this.configuration.enableTranslationDeactivation) {
throw new Error('Translation deactivation is not enabled. Please set the LocalizationConfiguration property "enableTranslationDeactivation" accordingly.');
}
const newValue = value === undefined ? !this.showKeys : value;
this._showKeys$.next(newValue);
}
/**
* Return the current value of debug show/hide translation keys.
*/
get showKeys() {
return this._showKeys$.value;
}
/**
* Get an observable of translation key after global mapping
* @param requestedKey Original translation key
*/
getKey(requestedKey) {
return this.keyMapping$
? this.keyMapping$.pipe(map((keyMapping) => keyMapping?.[requestedKey] || requestedKey), distinctUntilChanged())
: of(requestedKey);
}
/**
* Returns a stream of translated values of a key which updates whenever the language changes.
* @param key Key to translate
* @param interpolateParams Object to use in translation binding
* @returns A stream of the translated key
*/
translate(key, interpolateParams) {
return this.getKey(key).pipe(switchMap((translationKey) => this.getTranslationStream(translationKey, interpolateParams)), shareReplay({ refCount: true, bufferSize: 1 }));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/**
* TranslateDirective class adding debug functionality
*/
class LocalizationTranslateDirective extends TranslateDirective {
/** @inheritdoc */
set translate(key) {
if (key && key !== this.key) {
if (this.onKeyChange) {
this.onKeyChange.unsubscribe();
}
this.onKeyChange = this.localizationService.getKey(key).subscribe((newKey) => {
this.key = newKey;
this.checkNodes();
});
}
}
constructor() {
const translateService = inject(TranslateService);
const element = inject(ElementRef);
const ref = inject(ChangeDetectorRef);
super(translateService, element, ref);
this.localizationService = inject(LocalizationService);
this.localizationConfig = inject(LOCALIZATION_CONFIGURATION_TOKEN);
/**
* Should we display keys instead of translations
*/
this.showKeys = false;
const localizationService = this.localizationService;
const localizationConfig = this.localizationConfig;
if (localizationConfig.enableTranslationDeactivation) {
this.onShowKeysChange = localizationService.showKeys$.subscribe((showKeys) => {
this.showKeys = showKeys;
this.checkNodes(true);
});
}
}
/**
* Overriding parent's setContent to plug debugging feature
* @param node
* @param content
*/
setContent(node, content) {
const key = node.originalContent;
const newContent = this.showKeys ? key : (this.localizationConfig.debugMode && key ? `${key} - ${content}` : content);
if (typeof node.textContent !== 'undefined' && node.textContent !== null) {
node.textContent = newContent;
}
else {
node.data = newContent;
}
}
ngOnDestroy() {
super.ngOnDestroy();
if (this.onShowKeysChange) {
this.onShowKeysChange.unsubscribe();
}
if (this.onKeyChange) {
this.onKeyChange?.unsubscribe();
}
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationTranslateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.7", type: LocalizationTranslateDirective, isStandalone: false, selector: "[translate],[ngx-translate]", inputs: { translate: "translate" }, usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationTranslateDirective, decorators: [{
type: Directive,
args: [{
selector: '[translate],[ngx-translate]',
standalone: false
}]
}], ctorParameters: () => [], propDecorators: { translate: [{
type: Input
}] } });
/**
* TranslatePipe class adding debug functionality
*/
class O3rLocalizationTranslatePipe extends TranslatePipe {
constructor() {
super(inject(TranslateService), inject(ChangeDetectorRef));
/** Localization service instance */
this.localizationService = inject(LocalizationService);
/** Change detector service instance */
this.changeDetector = inject(ChangeDetectorRef);
/** Localization config token */
this.localizationConfig = inject(LOCALIZATION_CONFIGURATION_TOKEN);
/**
* Should we display keys instead of translations
*/
this.showKeys = false;
if (this.localizationConfig.enableTranslationDeactivation) {
this.onShowKeysChange = this.localizationService.showKeys$.subscribe((showKeys) => {
this.showKeys = showKeys;
this.changeDetector.markForCheck();
});
}
}
/**
* Calls original transform method and eventually outputs the key if debugMode (in LocalizationConfiguration) is enabled
* @inheritdoc
*/
transform(query, ...args) {
if (this.showKeys) {
return query;
}
if (query !== this.lastQueryKey) {
this.lastQueryKey = query;
if (this.onKeyChange) {
this.onKeyChange.unsubscribe();
}
this.onKeyChange = this.localizationService.getKey(query).subscribe((key) => {
this.lastResolvedKey = key;
this.changeDetector.markForCheck();
});
}
if (this.lastResolvedKey) {
const value = super.transform(this.lastResolvedKey, ...args);
if (this.localizationConfig.debugMode) {
return `${this.lastResolvedKey} - ${value}`;
}
return value;
}
return this.value;
}
ngOnDestroy() {
super.ngOnDestroy();
if (this.onShowKeysChange) {
this.onShowKeysChange.unsubscribe();
}
if (this.onKeyChange) {
this.onKeyChange.unsubscribe();
}
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: O3rLocalizationTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: O3rLocalizationTranslatePipe, isStandalone: false, name: "o3rTranslate", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: O3rLocalizationTranslatePipe, decorators: [{
type: Pipe,
args: [{
name: 'o3rTranslate',
pure: false,
standalone: false
}]
}], ctorParameters: () => [] });
/**
* Native angular CurrencyPipe taking the current lang into consideration
*/
class LocalizedCurrencyPipe extends CurrencyPipe {
constructor() {
super(inject(LocalizationService).getCurrentLanguage());
this.localizationService = inject(LocalizationService);
this.changeDetectorRef = inject(ChangeDetectorRef);
this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() => this.changeDetectorRef.markForCheck());
}
transform(value, currencyCode, display, digitsInfo, locale) {
return super.transform(value, currencyCode, display, digitsInfo, locale || this.localizationService.getCurrentLanguage());
}
ngOnDestroy() {
this.onLangChange.unsubscribe();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedCurrencyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: LocalizedCurrencyPipe, isStandalone: false, name: "currency", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedCurrencyPipe, decorators: [{
type: Pipe,
args: [{
name: 'currency',
pure: false,
standalone: false
}]
}], ctorParameters: () => [] });
/**
* Native angular DatePipe taking the current lang into consideration
*/
class LocalizedDatePipe extends DatePipe {
constructor() {
super(inject(LocalizationService).getCurrentLanguage());
this.localizationService = inject(LocalizationService);
this.changeDetectorRef = inject(ChangeDetectorRef);
this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() => this.changeDetectorRef.markForCheck());
}
transform(value, format = 'mediumDate', timezone, locale) {
return this.localizationService.showKeys ? format : super.transform(value, format, timezone, locale || this.localizationService.getCurrentLanguage());
}
ngOnDestroy() {
this.onLangChange.unsubscribe();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDatePipe, isStandalone: false, name: "date", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDatePipe, decorators: [{
type: Pipe,
args: [{
name: 'date',
pure: false,
standalone: false
}]
}], ctorParameters: () => [] });
/**
* Native angular DecimalPipe taking the current lang into consideration
*/
class LocalizedDecimalPipe extends DecimalPipe {
constructor() {
super(inject(LocalizationService).getCurrentLanguage());
this.localizationService = inject(LocalizationService);
this.changeDetectorRef = inject(ChangeDetectorRef);
this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() => this.changeDetectorRef.markForCheck());
}
transform(value, digitsInfo, locale) {
return super.transform(value, digitsInfo, locale || this.localizationService.getCurrentLanguage());
}
ngOnDestroy() {
this.onLangChange.unsubscribe();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDecimalPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDecimalPipe, isStandalone: false, name: "decimal", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizedDecimalPipe, decorators: [{
type: Pipe,
args: [{
name: 'decimal',
pure: false,
standalone: false
}]
}], ctorParameters: () => [] });
/**
* Service for handling the text direction based on the LocalizationConfiguration
*/
class TextDirectionService {
constructor() {
this.translateService = inject(TranslateService);
this.configuration = inject(LOCALIZATION_CONFIGURATION_TOKEN);
this.rendererFactory = inject(RendererFactory2);
this.directionality = inject(Directionality);
this.renderer = this.rendererFactory.createRenderer(null, null);
}
/**
* Updates the dir attribute on body HTML tag.
* @returns a subscription that updates the dir attribute
*/
onLangChangeSubscription() {
if (this.subscription && !this.subscription.closed) {
return this.subscription;
}
this.subscription = this.translateService.onLangChange.subscribe((event) => {
const direction = this.configuration.rtlLanguages.includes(event.lang.split('-')[0]) ? 'rtl' : 'ltr';
this.renderer.setAttribute(document.body, 'dir', direction);
this.directionality.change.emit(direction);
});
return this.subscription;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/**
* @deprecated The value of Directionality is no longer readonly and can be updated, this class will be removed in v16
*/
class TextDirectionality extends Directionality {
get value() {
return this._value;
}
set value(value) {
this._value = value;
}
constructor() {
super(inject(DIR_DOCUMENT, { optional: true }));
/**
* The current 'ltr' or 'rtl' value.
* @override
*/
this._value = 'ltr';
this.change
.pipe(startWith(this._value))
.subscribe((value) => this._value = value);
}
ngOnDestroy() {
this.change.complete();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionality, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionality }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TextDirectionality, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/**
* creates LocalizationConfiguration, which is used if the application
* @param configuration Localization configuration
*/
function createLocalizationConfiguration(configuration) {
return {
...DEFAULT_LOCALIZATION_CONFIGURATION,
...configuration
};
}
/**
* Factory to inject the LOCALE_ID token with the current language into Angular context
* @param localizationService Localization service
*/
function localeIdNgBridge(localizationService) {
return localizationService.getCurrentLanguage();
}
/** Custom Localization Configuration Token to override default localization configuration */
const CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN = new InjectionToken('Partial Localization configuration');
class LocalizationModule {
/**
* forRoot method should be called only once from the application index.ts
* It will do several things:
* - provide the configuration for the whole application
* - register all locales specified in the LocalizationConfiguration
* - configure TranslateService
* - inject LOCALE_ID token
* @param configuration LocalizationConfiguration
*/
static forRoot(configuration) {
return {
ngModule: LocalizationModule,
providers: [
LocalizationService,
...(configuration
? [{
provide: CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN,
useFactory: configuration
}]
: [])
]
};
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: LocalizationModule, declarations: [O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe], imports: [TranslateModule, BidiModule, DynamicContentModule, CommonModule], exports: [TranslateModule, O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe] }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationModule, providers: [
{ provide: LOCALIZATION_CONFIGURATION_TOKEN, useFactory: createLocalizationConfiguration, deps: [[new Optional(), CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN]] },
{ provide: LOCALE_ID, useFactory: localeIdNgBridge, deps: [LocalizationService] },
{ provide: Directionality, useClass: TextDirectionality },
{ provide: DatePipe, useClass: LocalizedDatePipe },
{ provide: DecimalPipe, useClass: LocalizedDecimalPipe },
{ provide: CurrencyPipe, useClass: LocalizedCurrencyPipe },
TextDirectionService
], imports: [TranslateModule, BidiModule, DynamicContentModule, CommonModule, TranslateModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationModule, decorators: [{
type: NgModule,
args: [{
declarations: [O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe],
imports: [TranslateModule, BidiModule, DynamicContentModule, CommonModule],
exports: [TranslateModule, O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe],
providers: [
{ provide: LOCALIZATION_CONFIGURATION_TOKEN, useFactory: createLocalizationConfiguration, deps: [[new Optional(), CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN]] },
{ provide: LOCALE_ID, useFactory: localeIdNgBridge, deps: [LocalizationService] },
{ provide: Directionality, useClass: TextDirectionality },
{ provide: DatePipe, useClass: LocalizedDatePipe },
{ provide: DecimalPipe, useClass: LocalizedDecimalPipe },
{ provide: CurrencyPipe, useClass: LocalizedCurrencyPipe },
TextDirectionService
]
}]
}] });
const JSON_EXT = '.json';
/**
* This class is responsible for loading translation bundles from remote or local endpoints depending on the LocalizationConfiguration.
* Fallback mechanism ensures that if a bundle in some language cannot be fetched remotely
* we try to fetch the same language bundle locally (bundles stored inside the application)
* and finally load the fallback language bundle (if all previous fetches failed)
*/
class TranslationsLoader {
constructor() {
this.localizationConfiguration = inject(LOCALIZATION_CONFIGURATION_TOKEN);
this.logger = inject(LoggerService, { optional: true });
this.dynamicContentService = inject(DynamicContentService, { optional: true });
}
/**
* Download a language bundle file
* @param url Url to the bundle file
*/
downloadLanguageBundle$(url) {
const queryParams = this.localizationConfiguration.queryParams;
let queryString = '';
if (queryParams) {
queryString = '?' + Object.keys(queryParams).map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(queryParams[key])).join('&');
}
return from(fetch(url + queryString, this.localizationConfiguration.fetchOptions)).pipe(switchMap((response) => from(response.json())));
}
/**
* @inheritdoc
*/
getTranslation(lang) {
const fallback = this.localizationConfiguration.fallbackLanguage;
let localizationPath$ = of(this.localizationConfiguration.endPointUrl);
if (this.localizationConfiguration.useDynamicContent) {
if (!this.dynamicContentService) {
throw new Error('Dynamic Content is not available. Please verify you have imported the module DynamicContentModule in your application');
}
localizationPath$ = this.dynamicContentService.getContentPathStream(this.localizationConfiguration.endPointUrl);
}
return localizationPath$.pipe(switchMap((localizationPath) => {
if (localizationPath) {
const localizationBundle$ = this.downloadLanguageBundle$(localizationPath + lang + JSON_EXT);
if (this.localizationConfiguration.mergeWithLocalTranslations) {
return combineLatest([
localizationBundle$.pipe(catchError(() => of({}))),
this.getTranslationFromLocal(lang, fallback).pipe(map((translations) => {
Object.keys(translations).forEach((key) => translations[key] = `[local] ${translations[key]}`);
return translations;
}))
]).pipe(map(([dynamicTranslations, localTranslations]) => ({ ...localTranslations, ...dynamicTranslations })));
}
/*
* if endPointUrl is specified by the configuration then:
* 1. try to load lang from endPointUrl
* 2. if 1 fails then try to load from the app (local file)
*/
return localizationBundle$.pipe(catchError(() => {
this.logger?.warn(`Failed to load the localization resource from ${localizationPath + lang + JSON_EXT}, trying from the application resources`);
return this.getTranslationFromLocal(lang, fallback);
}));
}
/*
* else if endPointUrl NOT specified by then configuration then:
* 1. try to load from the app (local file)
*/
this.logger?.warn('No localization endpoint specified, localization fetch from application resources');
return this.getTranslationFromLocal(lang, fallback);
}));
}
/**
*
*Fetches localization bundles from published folder (internal to application)
*
*1. try to load lang from local
*2. if 1 fails try to load fallback lang but only if it's different from lang in 1
* @param lang - language of the bundle
* @param fallbackLanguage - fallback language in case bundle in language not found
*/
getTranslationFromLocal(lang, fallbackLanguage) {
const pathPrefix = this.localizationConfiguration.bundlesOutputPath;
return this.downloadLanguageBundle$(pathPrefix + lang + JSON_EXT).pipe(catchError(() => {
if (lang === fallbackLanguage) {
this.logger?.warn(`Failed to load ${lang} from ${pathPrefix + lang + JSON_EXT}.`);
return of({});
}
else {
this.logger?.warn(`Failed to load ${lang} from ${pathPrefix + lang + JSON_EXT}. Application will fallback to ${fallbackLanguage}`);
return this.downloadLanguageBundle$(pathPrefix + fallbackLanguage + JSON_EXT).pipe(catchError(() => of({})));
}
}));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslationsLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslationsLoader }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: TranslationsLoader, decorators: [{
type: Injectable
}] });
/**
* Creates a loader of translations bundles based on the configuration
* (endPointUrl and language determine which bundle we load and where do we fetch it from)
* @param localizationConfiguration
* @param logger service to handle the log of warning and errors
* @param dynamicContentService (optional)
*/
function createTranslateLoader(localizationConfiguration, logger, dynamicContentService) {
const injector = Injector.create({
providers: [
{ provide: LOCALIZATION_CONFIGURATION_TOKEN, useValue: localizationConfiguration },
{ provide: LoggerService, useValue: logger },
{ provide: DynamicContentService, useValue: dynamicContentService },
{
provide: TranslationsLoader,
deps: [[LoggerService, new Optional()], [DynamicContentService, new Optional()], LOCALIZATION_CONFIGURATION_TOKEN]
}
]
});
return injector.get(TranslationsLoader);
}
/**
* TranslateLoader provider, using framework's TranslationsLoader class
*/
const translateLoaderProvider = {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [LOCALIZATION_CONFIGURATION_TOKEN, [new Optional(), LoggerService], [new Optional(), DynamicContentService]]
};
class OtterLocalizationDevtools {
constructor() {
this.localizationService = inject(LocalizationService);
this.translateCompiler = inject(TranslateCompiler);
this.appRef = inject(ApplicationRef);
}
/**
* Is the translation deactivation enabled
*/
isTranslationDeactivationEnabled() {
return this.localizationService.isTranslationDeactivationEnabled();
}
/**
* Show localization keys
* @param value value enforced by the DevTools extension
*/
showLocalizationKeys(value) {
this.localizationService.toggleShowKeys(value);
this.appRef.tick();
}
/**
* Returns the current language
*/
getCurrentLanguage() {
return this.localizationService.getCurrentLanguage();
}
/**
* Setup a listener on language change
* @param fn called when the language is changed in the app
*/
onLanguageChange(fn) {
return this.localizationService
.getTranslateService()
.onLangChange
.subscribe(({ lang }) => {
fn(lang);
});
}
/**
* Switch the current language to the specified value
* @param language new language to switch to
*/
async switchLanguage(language) {
if (!language) {
return;
}
await lastValueFrom(this.localizationService.useLanguage(language));
this.appRef.tick();
}
/**
* Updates the specified localization key/values for the current language.
*
* Recommendation: To be used with a small number of keys to update to avoid performance issues.
* @param keyValues key/values to update
* @param language if not provided, the current language value
*/
updateLocalizationKeys(keyValues, language) {
const lang = language || this.getCurrentLanguage();
const translateService = this.localizationService.getTranslateService();
Object.entries(keyValues).forEach(([key, value]) => {
translateService.set(key, value, lang);
});
this.appRef.tick();
}
/**
* Reload a language from the language file
* @see https://github.com/ngx-translate/core/blob/master/packages/core/lib/translate.service.ts#L490
* @param language language to reload
*/
async reloadLocalizationKeys(language) {
const lang = language || this.getCurrentLanguage();
if (this.translateCompiler.clearCache) {
this.translateCompiler.clearCache();
}
const initialLocs = await lastValueFrom(this.localizationService
.getTranslateService()
.reloadLang(lang));
this.localizationService.getTranslateService().setTranslation(language || this.getCurrentLanguage(), initialLocs);
this.appRef.tick();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: OtterLocalizationDevtools, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: OtterLocalizationDevtools }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: OtterLocalizationDevtools, decorators: [{
type: Injectable
}] });
const OTTER_LOCALIZATION_DEVTOOLS_DEFAULT_OPTIONS = {
isActivatedOnBootstrap: false,
isActivatedOnBootstrapWhenCMSContext: true,
metadataFilePath: './metadata/localisation.metadata.json'
};
const OTTER_LOCALIZATION_DEVTOOLS_OPTIONS = new InjectionToken('Otter Localization Devtools options');
/* eslint-disable no-console -- This is the purpose of this service */
class LocalizationDevtoolsConsoleService {
/** Name of the Window property to access to the devtools */
static { this.windowModuleName = 'localization'; }
constructor() {
this.localizationDevtools = inject(OtterLocalizationDevtools);
this.options = inject(OTTER_LOCALIZATION_DEVTOOLS_OPTIONS, { optional: true }) ?? OTTER_LOCALIZATION_DEVTOOLS_DEFAULT_OPTIONS;
if (this.options.isActivatedOnBootstrap
|| (this.options.isActivatedOnBootstrapWhenCMSContext
&& document.body.dataset.cmscontext === 'true')) {
this.activate();
}
}
/** @inheritDoc */
activate() {
const windowWithDevtools = window;
windowWithDevtools._OTTER_DEVTOOLS_ ||= {};
windowWithDevtools._OTTER_DEVTOOLS_[LocalizationDevtoolsConsoleService.windowModuleName] = this;
console.info(`Otter localization Devtools is now accessible via the _OTTER_DEVTOOLS_.${LocalizationDevtoolsConsoleService.windowModuleName} variable`);
}
/**
* @inheritdoc
*/
isTranslationDeactivationEnabled() {
return this.localizationDevtools.isTranslationDeactivationEnabled();
}
/**
* @inheritdoc
*/
showLocalizationKeys(value) {
this.localizationDevtools.showLocalizationKeys(value);
}
/**
* @inheritdoc
*/
getCurrentLanguage() {
const currentLanguage = this.localizationDevtools.getCurrentLanguage();
return currentLanguage;
}
/**
* @inheritdoc
*/
async switchLanguage(language) {
const previous = this.localizationDevtools.getCurrentLanguage();
await this.localizationDevtools.switchLanguage(language);
const current = this.localizationDevtools.getCurrentLanguage();
return {
requested: language,
previous,
current
};
}
/**
* @inheritdoc
*/
onLanguageChange(fn) {
return this.localizationDevtools.onLanguageChange(fn);
}
/**
* @inheritdoc
*/
updateLocalizationKeys(keyValues, language) {
return this.localizationDevtools.updateLocalizationKeys(keyValues, language);
}
/**
* @inheritdoc
*/
reloadLocalizationKeys(language) {
return this.localizationDevtools.reloadLocalizationKeys(language);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationDevtoolsConsoleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationDevtoolsConsoleService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: LocalizationDevtoolsConsoleService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
const isLocalizationMessage = (message) => {
return message && (message.dataType === 'displayLocalizationKeys'
|| message.dataType === 'languages'
|| message.dataType === 'switchLanguage'
|| message.dataType === 'localizations'
|| message.dataType === 'updateLocalization'
|| message.dataType === 'requestMessages'
|| message.dataType === 'connect'
|| message.dataType === 'reloadLocalizationKeys'
|| message.dataType === 'isTranslationDeactivationEnabled'
|| message.dataType === 'getTranslationValuesContentMessage');
};
class LocalizationDevtoolsMessageService {
constructor() {
this.logger = inject(LoggerService);
this.localizationDevTools = inject(OtterLocalizationDevtools);
this.localizationService = inject(LocalizationService);
this.options = inject(OTTER_LOCALIZATION_DEVTOOLS_OPTIONS, { optional: true }) ?? OTTER_LOCALIZATION_DEVTOOLS_DEFAULT_OPTIONS;
this.sendMessage = (sendOtterMessage);
this.destroyRef = inject(DestroyRef);
this.options = {
...OTTER_LOCALIZATION_DEVTOOLS_DEFAULT_OPTIONS,
...this.options
};
if (this.options.isActivatedOnBootstrap) {
this.activate();
}
}
async sendLocalizationsMetadat