@o3r/localization
Version:
This module provides a runtime dynamic language/translation support and debug tools.
1 lines • 93.7 kB
Source Map (JSON)
{"version":3,"file":"o3r-localization.mjs","sources":["../../src/annotations/localization.ts","../../src/core/localization.configuration.ts","../../src/core/translate-messageformat-lazy.compiler.ts","../../src/stores/localization-override/localization-override.actions.ts","../../src/stores/localization-override/localization-override.reducer.ts","../../src/stores/localization-override/localization-override.state.ts","../../src/stores/localization-override/localization-override.module.ts","../../src/stores/localization-override/localization-override.selectors.ts","../../src/stores/localization-override/localization-override.sync.ts","../../src/tools/localization.token.ts","../../src/tools/localization.service.ts","../../src/tools/localization-translate.directive.ts","../../src/tools/localization-translate.pipe.ts","../../src/tools/localized-currency.pipe.ts","../../src/tools/localized-date.pipe.ts","../../src/tools/localized-decimal.pipe.ts","../../src/tools/text-direction.service.ts","../../src/tools/text-directionality.service.ts","../../src/tools/localization.module.ts","../../src/tools/translations-loader.ts","../../src/tools/localization.provider.ts","../../src/devkit/localization-devtools.service.ts","../../src/devkit/localization-devtools.token.ts","../../src/devkit/localization-devtools.console.service.ts","../../src/devkit/localization-devtools.message.service.ts","../../src/devkit/localization-devtools.module.ts","../../src/o3r-localization.ts"],"sourcesContent":["import {\n deepFill,\n immutablePrimitive,\n otterComponentInfoPropertyName,\n} from '@o3r/core';\n\n/**\n * Decorator to pass localization url\n * @param _url\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention -- decorator should start with a capital letter\nexport function Localization(_url: string) {\n return (target: any, key: string) => {\n const privateField = _url || `_${key}`;\n const privateValue = target[key];\n\n if (delete target[key]) {\n Object.defineProperty(target, key, {\n get: function (this: any) {\n return this[privateField];\n },\n set: function (this: any, value: Record<string, unknown>) {\n const currentField = this[privateField] || privateValue;\n this[privateField] = typeof currentField === 'undefined' ? immutablePrimitive(value) : deepFill(currentField, value);\n if (this[otterComponentInfoPropertyName]) {\n this[otterComponentInfoPropertyName].translations = this[privateField];\n }\n },\n enumerable: true,\n configurable: true\n });\n }\n };\n}\n","/**\n * Describes configuration for LocalizationModule\n */\nexport interface LocalizationConfiguration {\n /** List of available languages */\n supportedLocales: string[];\n /** Application display language */\n language?: string;\n /** Url to fetch translation bundles from */\n endPointUrl: string;\n /** Prefix endPoinrUrl with dynamicContentPath provided by DynamicContentPath */\n useDynamicContent: boolean;\n /** List of RTL language codes */\n rtlLanguages: string[];\n /**\n * Fallback language map of resource in case translation in language does not exist.\n * translate to unsupported language will try to map to supportedLocales from below property.\n * @example\n * ```typescript\n * {\n * supportedLocales: ['en-GB', 'en-US', 'fr-FR'],\n * fallbackLocalesMap: {'en-CA': 'en-US', 'de': 'fr-FR'}\n * }\n * // translate to en-CA -> fallback to en-US, translate to de-DE -> fallback to fr-FR,\n * // translate to en-NZ -> fallback to en-GB, translate to en -> fallback to en-GB.\n * ```\n */\n fallbackLocalesMap?: {\n [supportedLocale: string]: string;\n };\n /** Fallback language of resource in case translation in language does not exist */\n fallbackLanguage: string;\n /** Path relative to published folder where webpack will copy translation bundles */\n bundlesOutputPath: string;\n /** Debug mode switch */\n debugMode: boolean;\n /** Query parameters for fetching the localization resources */\n queryParams?: { [key: string]: string };\n /** Fetch options object as per https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters */\n fetchOptions?: RequestInit;\n /** Enable the ability to switch the translations on and off at runtime. */\n enableTranslationDeactivation: boolean;\n /**\n * Merge the translations from DynamicContentPath with the local translations\n * Warning: Enable this option will download two localization bundles and can delay the display of the text on application first page\n * @default false\n */\n mergeWithLocalTranslations: boolean;\n}\n\n/**\n * Default configuration for LocalizationModule\n */\nexport const DEFAULT_LOCALIZATION_CONFIGURATION: Readonly<LocalizationConfiguration> = {\n supportedLocales: [],\n endPointUrl: '',\n useDynamicContent: false,\n rtlLanguages: ['ar', 'he'],\n fallbackLanguage: 'en',\n bundlesOutputPath: '',\n debugMode: false,\n enableTranslationDeactivation: false,\n mergeWithLocalTranslations: false\n} as const;\n","import {\n inject,\n Injectable,\n InjectionToken,\n} from '@angular/core';\nimport {\n TranslateCompiler,\n} from '@ngx-translate/core';\nimport {\n IntlMessageFormat,\n Options,\n} from 'intl-messageformat';\n\n/**\n * Options for Lazy Message Format compiler\n */\nexport interface LazyMessageFormatConfig extends Options {\n /**\n * Enables compiled translation caching\n * @default true\n */\n enableCache?: boolean;\n\n /**\n * Enables HTML in translation\n * @default true\n */\n ignoreTag?: boolean;\n}\n\n/**\n * Message format configuration default value\n */\nexport const lazyMessageDefaultConfig: Readonly<LazyMessageFormatConfig> = {\n enableCache: true,\n ignoreTag: true\n} as const;\n\n/** Message Format configuration Token */\nexport const MESSAGE_FORMAT_CONFIG = new InjectionToken<LazyMessageFormatConfig>('Message Format configuration');\n\n/**\n * This compiler expects ICU syntax and compiles the expressions with messageformat.js\n * Compare to ngx-translate-messageformat-compiler package, the compilation of the translation is done only on demand\n */\n@Injectable()\nexport class TranslateMessageFormatLazyCompiler extends TranslateCompiler {\n /** Configuration */\n private readonly config: LazyMessageFormatConfig;\n\n /** Cache of compiled translations */\n private cache: { [x: string]: IntlMessageFormat } = {};\n\n constructor() {\n const config = inject<LazyMessageFormatConfig>(MESSAGE_FORMAT_CONFIG, { optional: true });\n\n super();\n\n this.config = config ? { ...lazyMessageDefaultConfig, ...config } : lazyMessageDefaultConfig;\n }\n\n /**\n * Clear the cache of the compiled translations\n */\n public clearCache() {\n this.cache = {};\n }\n\n /** @inheritDoc */\n public compile(value: string, lang: string): (params: any) => string {\n return (params: any) => (new IntlMessageFormat(value, lang, undefined, this.config).format(params) as string);\n }\n\n /** @inheritDoc */\n public compileTranslations(translations: { [x: string]: any }, lang: string) {\n type CompiledTranslationMap = { [key in keyof typeof translations]: (params: any) => string };\n\n const compilingStrategy = this.config.enableCache\n ? (acc: CompiledTranslationMap, key: string) => {\n acc[key] = (params: any) => {\n const cached = this.cache[`${lang}_${key}`];\n if (cached) {\n return cached.format(params) as string;\n }\n\n const newCachedItem = new IntlMessageFormat(translations[key], lang, undefined, this.config);\n this.cache[`${lang}_${key}`] = newCachedItem;\n return newCachedItem.format(params) as string;\n };\n return acc;\n }\n\n : (acc: CompiledTranslationMap, key: string) => {\n acc[key] = (params: any) => new IntlMessageFormat(translations[key], lang, undefined, this.config).format(params) as string;\n return acc;\n };\n\n return Object.keys(translations).reduce<CompiledTranslationMap>((acc, key) => compilingStrategy(acc, key), {});\n }\n}\n","import {\n createAction,\n props,\n} from '@ngrx/store';\nimport {\n SetStateActionPayload,\n} from '@o3r/core';\nimport {\n LocalizationOverrideState,\n} from './localization-override.state';\n\n/** Actions */\nconst ACTION_SET = '[LocalizationOverride] set';\n\n/**\n * Clear all overrides and fill the store with the payload\n */\nexport const setLocalizationOverride = createAction(ACTION_SET, props<SetStateActionPayload<LocalizationOverrideState>>());\n","import {\n ActionCreator,\n createReducer,\n on,\n ReducerTypes,\n} from '@ngrx/store';\nimport * as actions from './localization-override.actions';\nimport {\n LocalizationOverrideState,\n} from './localization-override.state';\n\n/**\n * LocalizationOverride Store initial value\n */\nexport const localizationOverrideInitialState: LocalizationOverrideState = { localizationOverrides: {} };\n\n/**\n * List of basic actions for LocalizationOverride Store\n */\nexport const localizationOverrideReducerFeatures: ReducerTypes<LocalizationOverrideState, ActionCreator[]>[] = [\n on(actions.setLocalizationOverride, (_state, payload) => ({ ...payload.state }))\n];\n\n/**\n * LocalizationOverride Store reducer\n */\nexport const localizationOverrideReducer = createReducer(\n localizationOverrideInitialState,\n ...localizationOverrideReducerFeatures\n);\n","/**\n * LocalizationOverride store state\n */\nexport interface LocalizationOverrideState {\n /** Mapping of initial localization keys to the one they are replaced with */\n localizationOverrides: Record<string, string>;\n}\n\n/**\n * Name of the LocalizationOverride Store\n */\nexport const LOCALIZATION_OVERRIDE_STORE_NAME = 'localizationOverride';\n\n/**\n * LocalizationOverride Store Interface\n */\nexport interface LocalizationOverrideStore {\n /** LocalizationOverride state */\n [LOCALIZATION_OVERRIDE_STORE_NAME]: LocalizationOverrideState;\n}\n","import {\n InjectionToken,\n ModuleWithProviders,\n NgModule,\n} from '@angular/core';\nimport {\n Action,\n ActionReducer,\n StoreModule,\n} from '@ngrx/store';\nimport {\n localizationOverrideReducer,\n} from './localization-override.reducer';\nimport {\n LOCALIZATION_OVERRIDE_STORE_NAME,\n LocalizationOverrideState,\n} from './localization-override.state';\n\n/** Token of the LocalizationOverride reducer */\nexport const LOCALIZATION_OVERRIDE_REDUCER_TOKEN = new InjectionToken<ActionReducer<LocalizationOverrideState, Action>>('Feature LocalizationOverride Reducer');\n\n/** Provide default reducer for LocalizationOverride store */\nexport function getDefaultLocalizationOverrideReducer() {\n return localizationOverrideReducer;\n}\n\n@NgModule({\n imports: [\n StoreModule.forFeature(LOCALIZATION_OVERRIDE_STORE_NAME, LOCALIZATION_OVERRIDE_REDUCER_TOKEN)\n ],\n providers: [\n { provide: LOCALIZATION_OVERRIDE_REDUCER_TOKEN, useFactory: getDefaultLocalizationOverrideReducer }\n ]\n})\nexport class LocalizationOverrideStoreModule {\n public static forRoot<T extends LocalizationOverrideState>(reducerFactory: () => ActionReducer<T, Action>): ModuleWithProviders<LocalizationOverrideStoreModule> {\n return {\n ngModule: LocalizationOverrideStoreModule,\n providers: [\n { provide: LOCALIZATION_OVERRIDE_REDUCER_TOKEN, useFactory: reducerFactory }\n ]\n };\n }\n}\n","import {\n createFeatureSelector,\n createSelector,\n} from '@ngrx/store';\nimport {\n LOCALIZATION_OVERRIDE_STORE_NAME,\n LocalizationOverrideState,\n} from './localization-override.state';\n\n/** Select LocalizationOverride State */\nexport const selectLocalizationOverrideState = createFeatureSelector<LocalizationOverrideState>(LOCALIZATION_OVERRIDE_STORE_NAME);\n\n/** Select all localization override map */\nexport const selectLocalizationOverride = createSelector(selectLocalizationOverrideState, (state) => state?.localizationOverrides || {});\n","import {\n Serializer,\n} from '@o3r/core';\nimport {\n localizationOverrideInitialState,\n} from './localization-override.reducer';\nimport {\n LocalizationOverrideState,\n} from './localization-override.state';\n\nexport const localizationOverrideStorageDeserializer = (rawObject: any) => {\n if (!rawObject) {\n return localizationOverrideInitialState;\n }\n return rawObject;\n};\n\nexport const localizationOverrideStorageSync: Serializer<LocalizationOverrideState> = {\n deserialize: localizationOverrideStorageDeserializer\n};\n","import {\n InjectionToken,\n} from '@angular/core';\nimport {\n LocalizationConfiguration,\n} from '../core';\n\n/** Localization Configuration Token */\nexport const LOCALIZATION_CONFIGURATION_TOKEN = new InjectionToken<LocalizationConfiguration>('Localization Configuration injection token');\n","import {\n inject,\n Injectable,\n} from '@angular/core';\nimport {\n select,\n Store,\n} from '@ngrx/store';\nimport {\n TranslateService,\n} from '@ngx-translate/core';\nimport {\n LoggerService,\n} from '@o3r/logger';\nimport {\n BehaviorSubject,\n combineLatest,\n firstValueFrom,\n Observable,\n of,\n} from 'rxjs';\nimport {\n distinctUntilChanged,\n map,\n shareReplay,\n startWith,\n switchMap,\n} from 'rxjs/operators';\nimport {\n LocalizationConfiguration,\n} from '../core/localization.configuration';\nimport {\n LocalizationOverrideStore,\n selectLocalizationOverride,\n} from '../stores/index';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\n\n/**\n * Service which is wrapping the configuration logic of TranslateService from ngx-translate\n * Any application willing to use localization just needs to inject LocalizationService\n * in the root component and call its configure() method.\n */\n@Injectable()\nexport class LocalizationService {\n private readonly translateService = inject(TranslateService);\n private readonly logger = inject(LoggerService);\n private readonly configuration = inject<LocalizationConfiguration>(LOCALIZATION_CONFIGURATION_TOKEN);\n private readonly store = inject<Store<LocalizationOverrideStore>>(Store, { optional: true });\n\n private readonly localeSplitIdentifier: string = '-';\n\n /**\n * Internal subject that we use to track changes between keys only and translation mode\n */\n private readonly _showKeys$ = new BehaviorSubject(false);\n\n /**\n * Map of localization keys to replace a key to another\n */\n private readonly keyMapping$?: Observable<Record<string, any>>;\n\n /**\n * _showKeys$ exposed as an Observable\n */\n public showKeys$ = this._showKeys$.asObservable();\n\n constructor() {\n void this.configure();\n if (this.store) {\n this.keyMapping$ = this.store.pipe(\n select(selectLocalizationOverride)\n );\n }\n }\n\n /**\n * This will handle the fallback language hierarchy to find out fallback language.\n * supportedLocales language has highest priority, next priority goes to fallbackLocalesMap and default would be\n * fallbackLanguage.\n * @param language Selected language.\n * @returns selected language if supported, fallback language otherwise.\n */\n private checkFallbackLocalesMap<T extends string | undefined>(language: T) {\n if (language && !this.configuration.supportedLocales.includes(language)) {\n const closestSupportedLanguageCode = this.getFirstClosestSupportedLanguageCode(language);\n const fallbackForLanguage = this.getFallbackMapLangCode(language);\n const fallbackStrategyDebug = (fallbackForLanguage && ' associated fallback language ')\n || (closestSupportedLanguageCode && ' closest supported language ')\n || (this.configuration.fallbackLanguage && ' configured default language ');\n const fallbackLang = fallbackForLanguage || closestSupportedLanguageCode || this.configuration.fallbackLanguage || language;\n if (language !== fallbackLang) {\n this.logger.debug(`Non supported languages ${language} will fallback to ${fallbackStrategyDebug} ${fallbackLang}`);\n }\n return fallbackLang;\n } else if (!language) {\n this.logger.debug('Language is not defined');\n }\n return language;\n }\n\n /**\n * This function checks if fallback language can be provided from fallbackLocalesMap.\n * supportedLocales: ['en-GB', 'en-US', 'fr-FR'], fallbackLocalesMap: {'en-CA': 'en-US', 'de': 'fr-FR'}\n * translate to en-CA -> fallback to en-US, translate to de-DE -> fallback to fr-FR\n * translate to en-NZ -> fallback to en-GB\n * @param language Selected language.\n * @returns Fallback language if available, undefined otherwise.\n */\n private getFallbackMapLangCode(language: string): string | undefined {\n const fallbackLocalesMap = this.configuration.fallbackLocalesMap;\n const [locale] = language.split(this.localeSplitIdentifier);\n\n return fallbackLocalesMap && (fallbackLocalesMap[language] || fallbackLocalesMap[locale]);\n }\n\n /**\n * This function checks if closest supported language available incase of selected language is not\n * supported language.\n * supportedLocales: ['en-GB', 'en-US', 'fr-FR']\n * translate to en-CA -> fallback to en-GB\n * @param language Selected language.\n * @returns Closest supported language if available, undefined otherwise.\n */\n private getFirstClosestSupportedLanguageCode(language: string): string | undefined {\n const [locale] = language.split(this.localeSplitIdentifier);\n const firstClosestRegx = new RegExp(`^${locale}${this.localeSplitIdentifier}?`, 'i');\n\n return this.configuration.supportedLocales.find((supportedLang) => firstClosestRegx.test(supportedLang));\n }\n\n /**\n * Returns a stream of translated values of a key which updates whenever the language changes.\n * @param translationKey Key to translate\n * @param interpolateParams Object to use in translation binding\n * @returns A stream of the translated key\n */\n private getTranslationStream(translationKey: string, interpolateParams?: object) {\n const translation$ = this.translateService.onTranslationChange.pipe(\n startWith(undefined),\n switchMap(() => this.translateService.stream(translationKey, interpolateParams)),\n map((value) => this.configuration.debugMode ? `${translationKey} - ${value}` : value),\n distinctUntilChanged()\n );\n\n if (!this.configuration.enableTranslationDeactivation) {\n return translation$;\n }\n\n return combineLatest([\n translation$,\n this.showKeys$\n ]).pipe(\n map(([value, showKeys]) => showKeys ? translationKey : value)\n );\n }\n\n /**\n * Configures TranslateService and registers locales. This method is called from the application level.\n */\n public async configure() {\n const language = this.checkFallbackLocalesMap(this.configuration.language || this.configuration.fallbackLanguage);\n this.translateService.addLangs(this.configuration.supportedLocales);\n this.translateService.setDefaultLang(language);\n await firstValueFrom(this.useLanguage(language));\n }\n\n /**\n * Is the translation deactivation enabled\n */\n public isTranslationDeactivationEnabled() {\n return this.configuration.enableTranslationDeactivation;\n }\n\n /**\n * Wrapper to call the ngx-translate service TranslateService method getLangs().\n */\n public getLanguages() {\n return this.translateService.getLangs();\n }\n\n /**\n * Wrapper to call the ngx-translate service TranslateService method use(language).\n * @param language\n */\n public useLanguage(language: string): Observable<any> {\n language = this.checkFallbackLocalesMap(language);\n return this.translateService.use(language);\n }\n\n /**\n * Wrapper to get the ngx-translate service TranslateService currentLang.\n */\n public getCurrentLanguage() {\n return this.translateService.currentLang;\n }\n\n /**\n * Get the instance of the ngx-translate TranslateService used by LocalizationService.\n */\n public getTranslateService() {\n return this.translateService;\n }\n\n /**\n * Toggle the ShowKeys mode between active and inactive.\n * @param value if specified, set the ShowKeys mode to value. If not specified, toggle the ShowKeys mode.\n */\n public toggleShowKeys(value?: boolean) {\n if (!this.configuration.enableTranslationDeactivation) {\n throw new Error('Translation deactivation is not enabled. Please set the LocalizationConfiguration property \"enableTranslationDeactivation\" accordingly.');\n }\n const newValue = value === undefined ? !this.showKeys : value;\n this._showKeys$.next(newValue);\n }\n\n /**\n * Return the current value of debug show/hide translation keys.\n */\n public get showKeys() {\n return this._showKeys$.value;\n }\n\n /**\n * Get an observable of translation key after global mapping\n * @param requestedKey Original translation key\n */\n public getKey(requestedKey: string) {\n return this.keyMapping$\n ? this.keyMapping$.pipe(\n map((keyMapping) => keyMapping?.[requestedKey] || requestedKey),\n distinctUntilChanged()\n )\n : of(requestedKey);\n }\n\n /**\n * Returns a stream of translated values of a key which updates whenever the language changes.\n * @param key Key to translate\n * @param interpolateParams Object to use in translation binding\n * @returns A stream of the translated key\n */\n public translate(key: string, interpolateParams?: object) {\n return this.getKey(key).pipe(\n switchMap((translationKey) => this.getTranslationStream(translationKey, interpolateParams)),\n shareReplay({ refCount: true, bufferSize: 1 })\n );\n }\n}\n","import {\n ChangeDetectorRef,\n Directive,\n ElementRef,\n inject,\n Input,\n OnDestroy,\n} from '@angular/core';\nimport {\n TranslateDirective,\n TranslateService,\n} from '@ngx-translate/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationConfiguration,\n} from '../core';\nimport {\n LocalizationService,\n} from './localization.service';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\n/**\n * TranslateDirective class adding debug functionality\n */\n@Directive({\n selector: '[translate],[ngx-translate]',\n standalone: false\n})\nexport class LocalizationTranslateDirective extends TranslateDirective implements OnDestroy {\n private readonly localizationService = inject(LocalizationService);\n private readonly localizationConfig = inject<LocalizationConfiguration>(LOCALIZATION_CONFIGURATION_TOKEN);\n\n /**\n * Internal subscription to the LocalizationService showKeys mode changes\n */\n private readonly onShowKeysChange?: Subscription;\n\n /**\n * Should we display keys instead of translations\n */\n private showKeys = false;\n\n /**\n * Internal subscription to the LocalizationService key mapping\n */\n private onKeyChange?: Subscription;\n\n /** @inheritdoc */\n @Input()\n public set translate(key: string) {\n if (key && key !== this.key) {\n if (this.onKeyChange) {\n this.onKeyChange.unsubscribe();\n }\n this.onKeyChange = this.localizationService.getKey(key).subscribe((newKey) => {\n this.key = newKey;\n this.checkNodes();\n });\n }\n }\n\n constructor() {\n const translateService = inject(TranslateService);\n const element = inject(ElementRef);\n const ref = inject(ChangeDetectorRef);\n\n super(translateService, element, ref);\n const localizationService = this.localizationService;\n const localizationConfig = this.localizationConfig;\n\n if (localizationConfig.enableTranslationDeactivation) {\n this.onShowKeysChange = localizationService.showKeys$.subscribe((showKeys) => {\n this.showKeys = showKeys;\n this.checkNodes(true);\n });\n }\n }\n\n /**\n * Overriding parent's setContent to plug debugging feature\n * @param node\n * @param content\n */\n public setContent(node: any, content: string): void {\n const key = node.originalContent;\n const newContent = this.showKeys ? key : (this.localizationConfig.debugMode && key ? `${key as string} - ${content}` : content);\n if (typeof node.textContent !== 'undefined' && node.textContent !== null) {\n node.textContent = newContent;\n } else {\n node.data = newContent;\n }\n }\n\n public ngOnDestroy() {\n super.ngOnDestroy();\n if (this.onShowKeysChange) {\n this.onShowKeysChange.unsubscribe();\n }\n if (this.onKeyChange) {\n this.onKeyChange?.unsubscribe();\n }\n }\n}\n","import {\n ChangeDetectorRef,\n inject,\n OnDestroy,\n Pipe,\n PipeTransform,\n} from '@angular/core';\nimport {\n TranslatePipe,\n TranslateService,\n} from '@ngx-translate/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationConfiguration,\n} from '../core';\nimport {\n LocalizationService,\n} from './localization.service';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\n\n/**\n * TranslatePipe class adding debug functionality\n */\n@Pipe({\n name: 'o3rTranslate',\n pure: false,\n standalone: false\n})\nexport class O3rLocalizationTranslatePipe extends TranslatePipe implements PipeTransform, OnDestroy {\n /** Localization service instance */\n protected readonly localizationService = inject(LocalizationService);\n /** Change detector service instance */\n protected readonly changeDetector = inject(ChangeDetectorRef);\n /** Localization config token */\n protected readonly localizationConfig: LocalizationConfiguration = inject(LOCALIZATION_CONFIGURATION_TOKEN);\n /**\n * Internal subscription to the LocalizationService showKeys mode changes\n */\n protected readonly onShowKeysChange?: Subscription;\n\n /**\n * Internal subscription to the LocalizationService key mapping\n */\n protected onKeyChange?: Subscription;\n\n /**\n * Should we display keys instead of translations\n */\n protected showKeys = false;\n\n /** last key queried */\n protected lastQueryKey?: string;\n\n /** last key resolved */\n protected lastResolvedKey?: string;\n\n constructor() {\n super(inject(TranslateService), inject(ChangeDetectorRef));\n if (this.localizationConfig.enableTranslationDeactivation) {\n this.onShowKeysChange = this.localizationService.showKeys$.subscribe((showKeys) => {\n this.showKeys = showKeys;\n this.changeDetector.markForCheck();\n });\n }\n }\n\n /**\n * Calls original transform method and eventually outputs the key if debugMode (in LocalizationConfiguration) is enabled\n * @inheritdoc\n */\n public transform(query: string, ...args: any[]): any {\n if (this.showKeys) {\n return query;\n }\n\n if (query !== this.lastQueryKey) {\n this.lastQueryKey = query;\n if (this.onKeyChange) {\n this.onKeyChange.unsubscribe();\n }\n this.onKeyChange = this.localizationService.getKey(query).subscribe((key) => {\n this.lastResolvedKey = key;\n this.changeDetector.markForCheck();\n });\n }\n\n if (this.lastResolvedKey) {\n const value = super.transform(this.lastResolvedKey, ...args);\n\n if (this.localizationConfig.debugMode) {\n return `${this.lastResolvedKey} - ${value as string}`;\n }\n\n return value;\n }\n\n return this.value;\n }\n\n public ngOnDestroy() {\n super.ngOnDestroy();\n if (this.onShowKeysChange) {\n this.onShowKeysChange.unsubscribe();\n }\n if (this.onKeyChange) {\n this.onKeyChange.unsubscribe();\n }\n }\n}\n","import {\n CurrencyPipe,\n} from '@angular/common';\nimport {\n ChangeDetectorRef,\n inject,\n OnDestroy,\n Pipe,\n PipeTransform,\n} from '@angular/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationService,\n} from './localization.service';\n\n/**\n * Native angular CurrencyPipe taking the current lang into consideration\n */\n@Pipe({\n name: 'currency',\n pure: false,\n standalone: false\n})\nexport class LocalizedCurrencyPipe extends CurrencyPipe implements OnDestroy, PipeTransform {\n private readonly onLangChange: Subscription;\n private readonly localizationService = inject(LocalizationService);\n private readonly changeDetectorRef = inject(ChangeDetectorRef);\n\n constructor() {\n super(inject(LocalizationService).getCurrentLanguage());\n this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() =>\n this.changeDetectorRef.markForCheck()\n );\n }\n\n /**\n * @inheritdoc\n */\n public transform(value: number | string, currencyCode?: string, display?: string | boolean, digitsInfo?: string, locale?: string): string | null;\n public transform(value: null | undefined, currencyCode?: string, display?: string | boolean, digitsInfo?: string, locale?: string): null;\n public transform(\n // eslint-disable-next-line @typescript-eslint/unified-signatures -- Expose same signatures as angular CurrencyPipe\n value: number | string | null | undefined, currencyCode?: string, display?: string | boolean, digitsInfo?: string, locale?: string): string | null;\n public transform(\n value: number | string | null | undefined, currencyCode?: string, display?: string | boolean, digitsInfo?: string, locale?: string): string | null {\n return super.transform(value, currencyCode, display, digitsInfo, locale || this.localizationService.getCurrentLanguage());\n }\n\n public ngOnDestroy(): void {\n this.onLangChange.unsubscribe();\n }\n}\n","import {\n DatePipe,\n} from '@angular/common';\nimport {\n ChangeDetectorRef,\n inject,\n OnDestroy,\n Pipe,\n PipeTransform,\n} from '@angular/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationService,\n} from './localization.service';\n\n/**\n * Native angular DatePipe taking the current lang into consideration\n */\n@Pipe({\n name: 'date',\n pure: false,\n standalone: false\n})\nexport class LocalizedDatePipe extends DatePipe implements OnDestroy, PipeTransform {\n private readonly onLangChange: Subscription;\n private readonly localizationService = inject(LocalizationService);\n private readonly changeDetectorRef = inject(ChangeDetectorRef);\n\n constructor() {\n super(inject(LocalizationService).getCurrentLanguage());\n this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() =>\n this.changeDetectorRef.markForCheck()\n );\n }\n\n /**\n * @inheritdoc\n */\n public transform(value: Date | string | number, format?: string, timezone?: string, locale?: string): string\n | null;\n public transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null;\n public transform(\n value: Date | string | number | null | undefined, format?: string, timezone?: string,\n locale?: string): string | null;\n public transform(\n value: Date | string | number | null | undefined, format = 'mediumDate', timezone?: string,\n locale?: string): string | null {\n return this.localizationService.showKeys ? format : super.transform(value, format, timezone, locale || this.localizationService.getCurrentLanguage());\n }\n\n public ngOnDestroy(): void {\n this.onLangChange.unsubscribe();\n }\n}\n","import {\n DecimalPipe,\n} from '@angular/common';\nimport {\n ChangeDetectorRef,\n inject,\n OnDestroy,\n Pipe,\n PipeTransform,\n} from '@angular/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationService,\n} from './localization.service';\n\n/**\n * Native angular DecimalPipe taking the current lang into consideration\n */\n@Pipe({\n name: 'decimal',\n pure: false,\n standalone: false\n})\nexport class LocalizedDecimalPipe extends DecimalPipe implements OnDestroy, PipeTransform {\n private readonly onLangChange: Subscription;\n private readonly localizationService = inject(LocalizationService);\n private readonly changeDetectorRef = inject(ChangeDetectorRef);\n\n constructor() {\n super(inject(LocalizationService).getCurrentLanguage());\n this.onLangChange = this.localizationService.getTranslateService().onLangChange.subscribe(() =>\n this.changeDetectorRef.markForCheck()\n );\n }\n\n /**\n * @inheritdoc\n */\n public transform(value: number | string, digitsInfo?: string, locale?: string): string | null;\n public transform(value: null | undefined, digitsInfo?: string, locale?: string): null;\n public transform(value: number | string | null | undefined, digitsInfo?: string, locale?: string): string | null {\n return super.transform(value, digitsInfo, locale || this.localizationService.getCurrentLanguage());\n }\n\n public ngOnDestroy(): void {\n this.onLangChange.unsubscribe();\n }\n}\n","import {\n Directionality,\n} from '@angular/cdk/bidi';\nimport {\n inject,\n Injectable,\n Renderer2,\n RendererFactory2,\n} from '@angular/core';\nimport {\n LangChangeEvent,\n TranslateService,\n} from '@ngx-translate/core';\nimport {\n Subscription,\n} from 'rxjs';\nimport {\n LocalizationConfiguration,\n} from '../core';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\n\n/**\n * Service for handling the text direction based on the LocalizationConfiguration\n */\n@Injectable()\nexport class TextDirectionService {\n private readonly translateService = inject(TranslateService);\n private readonly configuration = inject<LocalizationConfiguration>(LOCALIZATION_CONFIGURATION_TOKEN);\n private readonly rendererFactory = inject(RendererFactory2);\n private readonly directionality = inject(Directionality);\n\n private subscription?: Subscription;\n private readonly renderer: Renderer2;\n\n constructor() {\n this.renderer = this.rendererFactory.createRenderer(null, null);\n }\n\n /**\n * Updates the dir attribute on body HTML tag.\n * @returns a subscription that updates the dir attribute\n */\n public onLangChangeSubscription() {\n if (this.subscription && !this.subscription.closed) {\n return this.subscription;\n }\n this.subscription = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {\n const direction = this.configuration.rtlLanguages.includes(event.lang.split('-')[0]) ? 'rtl' : 'ltr';\n this.renderer.setAttribute(document.body, 'dir', direction);\n this.directionality.change.emit(direction);\n });\n return this.subscription;\n }\n}\n","import {\n DIR_DOCUMENT,\n Direction,\n Directionality,\n} from '@angular/cdk/bidi';\nimport {\n inject,\n Injectable,\n OnDestroy,\n} from '@angular/core';\nimport {\n startWith,\n} from 'rxjs/operators';\n\n/**\n * @deprecated The value of Directionality is no longer readonly and can be updated, this class will be removed in v16\n */\n@Injectable()\nexport class TextDirectionality extends Directionality implements OnDestroy {\n public get value(): Direction {\n return this._value;\n }\n\n public set value(value: Direction) {\n this._value = value;\n }\n\n /**\n * The current 'ltr' or 'rtl' value.\n * @override\n */\n private _value: Direction = 'ltr';\n\n constructor() {\n super(inject(DIR_DOCUMENT, { optional: true }));\n this.change\n .pipe(startWith(this._value))\n .subscribe((value: Direction) => this._value = value);\n }\n\n public ngOnDestroy() {\n this.change.complete();\n }\n}\n","import {\n BidiModule,\n Directionality,\n} from '@angular/cdk/bidi';\nimport {\n CommonModule,\n CurrencyPipe,\n DatePipe,\n DecimalPipe,\n} from '@angular/common';\nimport {\n InjectionToken,\n LOCALE_ID,\n ModuleWithProviders,\n NgModule,\n Optional,\n} from '@angular/core';\nimport {\n TranslateModule,\n} from '@ngx-translate/core';\nimport {\n DynamicContentModule,\n} from '@o3r/dynamic-content';\nimport {\n DEFAULT_LOCALIZATION_CONFIGURATION,\n LocalizationConfiguration,\n} from '../core';\nimport {\n LocalizationTranslateDirective,\n} from './localization-translate.directive';\nimport {\n O3rLocalizationTranslatePipe,\n} from './localization-translate.pipe';\nimport {\n LocalizationService,\n} from './localization.service';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\nimport {\n LocalizedCurrencyPipe,\n} from './localized-currency.pipe';\nimport {\n LocalizedDatePipe,\n} from './localized-date.pipe';\nimport {\n LocalizedDecimalPipe,\n} from './localized-decimal.pipe';\nimport {\n TextDirectionService,\n} from './text-direction.service';\nimport {\n TextDirectionality,\n} from './text-directionality.service';\n\n/**\n * creates LocalizationConfiguration, which is used if the application\n * @param configuration Localization configuration\n */\nexport function createLocalizationConfiguration(configuration?: Partial<LocalizationConfiguration>): LocalizationConfiguration {\n return {\n ...DEFAULT_LOCALIZATION_CONFIGURATION,\n ...configuration\n };\n}\n\n/**\n * Factory to inject the LOCALE_ID token with the current language into Angular context\n * @param localizationService Localization service\n */\nexport function localeIdNgBridge(localizationService: LocalizationService) {\n return localizationService.getCurrentLanguage();\n}\n\n/** Custom Localization Configuration Token to override default localization configuration */\nexport const CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN = new InjectionToken<Partial<LocalizationConfiguration>>('Partial Localization configuration');\n\n@NgModule({\n declarations: [O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe],\n imports: [TranslateModule, BidiModule, DynamicContentModule, CommonModule],\n exports: [TranslateModule, O3rLocalizationTranslatePipe, LocalizationTranslateDirective, LocalizedDatePipe, LocalizedDecimalPipe, LocalizedCurrencyPipe],\n providers: [\n { provide: LOCALIZATION_CONFIGURATION_TOKEN, useFactory: createLocalizationConfiguration, deps: [[new Optional(), CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN]] },\n { provide: LOCALE_ID, useFactory: localeIdNgBridge, deps: [LocalizationService] },\n { provide: Directionality, useClass: TextDirectionality },\n { provide: DatePipe, useClass: LocalizedDatePipe },\n { provide: DecimalPipe, useClass: LocalizedDecimalPipe },\n { provide: CurrencyPipe, useClass: LocalizedCurrencyPipe },\n TextDirectionService\n ]\n})\nexport class LocalizationModule {\n /**\n * forRoot method should be called only once from the application index.ts\n * It will do several things:\n * - provide the configuration for the whole application\n * - register all locales specified in the LocalizationConfiguration\n * - configure TranslateService\n * - inject LOCALE_ID token\n * @param configuration LocalizationConfiguration\n */\n public static forRoot(\n configuration?: () => Partial<LocalizationConfiguration>\n ): ModuleWithProviders<LocalizationModule> {\n return {\n ngModule: LocalizationModule,\n providers: [\n LocalizationService,\n ...(configuration\n ? [{\n provide: CUSTOM_LOCALIZATION_CONFIGURATION_TOKEN,\n useFactory: configuration\n }]\n : [])\n ]\n };\n }\n}\n","import {\n inject,\n Injectable,\n} from '@angular/core';\nimport {\n TranslateLoader,\n} from '@ngx-translate/core';\nimport {\n DynamicContentService,\n} from '@o3r/dynamic-content';\nimport {\n LoggerService,\n} from '@o3r/logger';\nimport {\n combineLatest,\n from,\n Observable,\n of,\n} from 'rxjs';\nimport {\n catchError,\n map,\n switchMap,\n} from 'rxjs/operators';\nimport {\n LocalizationConfiguration,\n} from '../core';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\n\nconst JSON_EXT = '.json';\n\n/**\n * This class is responsible for loading translation bundles from remote or local endpoints depending on the LocalizationConfiguration.\n * Fallback mechanism ensures that if a bundle in some language cannot be fetched remotely\n * we try to fetch the same language bundle locally (bundles stored inside the application)\n * and finally load the fallback language bundle (if all previous fetches failed)\n */\n@Injectable()\nexport class TranslationsLoader implements TranslateLoader {\n private readonly localizationConfiguration: LocalizationConfiguration = inject(LOCALIZATION_CONFIGURATION_TOKEN);\n private readonly logger? = inject(LoggerService, { optional: true });\n private readonly dynamicContentService? = inject(DynamicContentService, { optional: true });\n\n /**\n * Download a language bundle file\n * @param url Url to the bundle file\n */\n private downloadLanguageBundle$(url: string) {\n const queryParams = this.localizationConfiguration.queryParams;\n\n let queryString = '';\n if (queryParams) {\n queryString = '?' + Object.keys(queryParams).map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(queryParams[key])).join('&');\n }\n return from(fetch(url + queryString, this.localizationConfiguration.fetchOptions)).pipe(\n switchMap((response) => from(response.json()))\n );\n }\n\n /**\n * @inheritdoc\n */\n public getTranslation(lang: string): Observable<any> {\n const fallback = this.localizationConfiguration.fallbackLanguage;\n let localizationPath$ = of(this.localizationConfiguration.endPointUrl);\n\n if (this.localizationConfiguration.useDynamicContent) {\n if (!this.dynamicContentService) {\n throw new Error('Dynamic Content is not available. Please verify you have imported the module DynamicContentModule in your application');\n }\n localizationPath$ = this.dynamicContentService.getContentPathStream(this.localizationConfiguration.endPointUrl);\n }\n\n return localizationPath$.pipe(\n switchMap((localizationPath: string) => {\n if (localizationPath) {\n const localizationBundle$ = this.downloadLanguageBundle$(localizationPath + lang + JSON_EXT);\n\n if (this.localizationConfiguration.mergeWithLocalTranslations) {\n return combineLatest([\n localizationBundle$.pipe(catchError(() => of({}))),\n this.getTranslationFromLocal(lang, fallback).pipe(\n map((translations) => {\n Object.keys(translations).forEach((key) => translations[key] = `[local] ${translations[key] as string}`);\n return translations;\n })\n )\n ]).pipe(map(([dynamicTranslations, localTranslations]) => ({ ...localTranslations, ...dynamicTranslations })));\n }\n\n /*\n * if endPointUrl is specified by the configuration then:\n * 1. try to load lang from endPointUrl\n * 2. if 1 fails then try to load from the app (local file)\n */\n return localizationBundle$.pipe(\n catchError(() => {\n this.logger?.warn(`Failed to load the localization resource from ${localizationPath + lang + JSON_EXT}, trying from the application resources`);\n return this.getTranslationFromLocal(lang, fallback);\n })\n );\n }\n /*\n * else if endPointUrl NOT specified by then configuration then:\n * 1. try to load from the app (local file)\n */\n this.logger?.warn('No localization endpoint specified, localization fetch from application resources');\n return this.getTranslationFromLocal(lang, fallback);\n })\n );\n }\n\n /**\n *\n *Fetches localization bundles from published folder (internal to application)\n *\n *1. try to load lang from local\n *2. if 1 fails try to load fallback lang but only if it's different from lang in 1\n * @param lang - language of the bundle\n * @param fallbackLanguage - fallback language in case bundle in language not found\n */\n public getTranslationFromLocal(lang: string, fallbackLanguage: string): Observable<any> {\n const pathPrefix: string = this.localizationConfiguration.bundlesOutputPath;\n return this.downloadLanguageBundle$(pathPrefix + lang + JSON_EXT).pipe(\n catchError(() => {\n if (lang === fallbackLanguage) {\n this.logger?.warn(`Failed to load ${lang} from ${pathPrefix + lang + JSON_EXT}.`);\n return of({});\n } else {\n this.logger?.warn(`Failed to load ${lang} from ${pathPrefix + lang + JSON_EXT}. Application will fallback to ${fallbackLanguage}`);\n return this.downloadLanguageBundle$(pathPrefix + fallbackLanguage + JSON_EXT).pipe(\n catchError(() => of({}))\n );\n }\n })\n );\n }\n}\n","import {\n FactoryProvider,\n Injector,\n Optional,\n} from '@angular/core';\nimport {\n TranslateLoader,\n} from '@ngx-translate/core';\nimport {\n DynamicContentService,\n} from '@o3r/dynamic-content';\nimport {\n LoggerService,\n} from '@o3r/logger';\nimport {\n LocalizationConfiguration,\n} from '../core';\nimport {\n LOCALIZATION_CONFIGURATION_TOKEN,\n} from './localization.token';\nimport {\n TranslationsLoader,\n} from './translations-loader';\n\n/**\n * Creates a loader of translations bundles based on the configuration\n * (endPointUrl and language determine which bundle we load and where do we fetch it from)\n * @param localizationConfiguration\n * @param logger service to handle the log of warning and errors\n * @param dynamicContentService (optional)\n */\nexport function createTranslateLoader(localizationConfiguration: LocalizationConfiguration, logger?: LoggerService, dynamicContentService?: DynamicContentService) {\n const injector = Injector.create({\n providers: [\n { provide: LOCALIZATION_CONFIGURATION_TOKEN, useValue: localizationConfiguration },\n { provide: LoggerService, useValue: logger },\n { provide: DynamicContentService, useValue: dynamicContentService },\n {\n provide: TranslationsLoader,\n deps: [[LoggerService, new Optional()], [DynamicContentService, new Optional()], LOCALIZATION_CONFIGURATION_TOKEN]\n }\n ]\n });\n return injector.get(TranslationsLoader);\n}\n\n/**\n * TranslateLoader provider, using framework's TranslationsLoader class\n */\nexport const translateLoaderProvider: Readonly<FactoryProvider> = {\n provide: TranslateLoader,\n useFactory: createTranslateLoader,\n deps: [LOCALIZATION_CONFIGURATION_TOKEN, [new Optional(), LoggerService], [new Optional(), DynamicContentService]]\n} as const;\n","import {\n ApplicationRef,\n inject,\n Injectable,\n} from '@angular/core';\nimport {\n TranslateCompiler,\n} from '@ngx-translate/core';\nimport {\n lastValueFrom,\n Subscription,\n} from 'rxjs';\nimport type {\n TranslateMessageFormatLazyCompiler,\n} from '../core';\nimport {\n LocalizationService,\n} from '../tools';\n\n@Injectable()\nexport class OtterLocalizationDevtools {\n private readonly localizationService = inject(LocalizationService);\n private readonly translateCompiler = inject(TranslateCompiler);\n private readonly appRef = inject(ApplicationRef);\n\n /**\n * Is the translation deactivation enabled\n */\n public isTranslationDeactivationEnabled() {\n return this.localizationService.isTranslationDeactivationEnabled();\n }\n\n /**\n * Show localization keys\n * @param value value enforced by the DevTools extension\n */\n public showLocalizationKeys(value?: boolean): void {\n this.localizationService.toggleShowKeys(value);\n this.appRef.tick();\n }\n\n /**\n * Returns the current language\n */\n public getCurrentLanguage() {\n return this.localizationService.getCurrentLanguage();\n }\n\n /**\n * Setup a listener on language change\n * @param fn called when the language is changed in the app\n */\n public onLanguageChange(fn: (language: string) => any): Subscription {\n return this.localizationService\n .getTranslateService()\n .onLangChange\n .subscribe(({ lang }) => {\n fn(lang);\n });\n }\n\n /**\n * Switch the current language to the specified value\n * @param language new language to switch to\n */\n public async switchLanguage(language: string | undefined) {\n if (!language) {\n return;\n }\n await lastValueFrom(this.localizationService.useLanguage(language));\n this.appRef.tick();\n }\n\n /**\n * Updates the specified localization key/values for the current language.\n *\n * Recommendation: To be used with a small number of keys to update to avoid performance issues.\n * @param keyValues key/values to update\n * @param language if not provided, the current language value\n */\n public updateLocalizationKeys(keyValues: { [key: string]: string }, language?: string): void | Promise<void> {\n const lang = language || this.getCurrentLanguage();\n const translateService = this.localizationService.getTranslateService();\n Object.entries(keyValues).forEach(([key, value]) => {\n translateService.set(key, value, lang);\n });\n this.appRef.tick();\n }\n\n /**\n * Reload a language from the language file\n * @see https://github.com/ngx-translate/core/blob/master/packages/core/lib/translate.service.ts#L490\n * @param language language to reload\n */\n public async reloadLocalizationKeys(language?: string) {\n const lang = language || this.getCurrentLanguage();\n if ((this.translateCompiler as TranslateMessageFormatLazyCompiler).clearCache) {\n (this.translateCompiler as TranslateMessageFormatLazyCompiler).clearCache();\n }\n const initialLocs = await lastValueFrom(\n this.localizationService\n .getTranslateService()\