UNPKG

@ogs-gmbh/ngx-translate

Version:

A lightweight, REST-based Angular i18n library designed for seamless internationalization with minimal setup. It supports dynamic language switching & flexible translation management via RESTful APIs.

1 lines 93.6 kB
{"version":3,"file":"ogs-gmbh-ngx-translate.mjs","sources":["../../../src/enums/collecting-strategies.enum.ts","../../../src/enums/preloading-strategies.enum.ts","../../../src/errors/locale-not-defined.error.ts","../../../src/errors/source-locale-not-defined.error.ts","../../../src/tokens/config.token.ts","../../../src/services/translation-store.service.ts","../../../src/interceptors/translation.interceptor.ts","../../../src/tokens/scope.token.ts","../../../src/errors/scope-not-defined.error.ts","../../../src/errors/translation-not-defined.error.ts","../../../src/utils/file.util.ts","../../../src/utils/translate.util.ts","../../../src/tokens/http.token.ts","../../../src/services/translation-http.serivce.ts","../../../src/services/translation.service.ts","../../../src/pipes/translation.pipe.ts","../../../src/tokens/preload.token.ts","../../../src/providers/scope.provider.ts","../../../src/providers/preload.provider.ts","../../../src/providers/config.provider.ts","../../../src/providers/interceptor.provider.ts","../../../src/pipe.module.ts","../../../src/providers/http.provider.ts","../../../src/lib.module.ts"],"sourcesContent":["export enum CollectingStrategies {\n ALL = \"all\",\n CURRENT = \"current\"\n}\n","export enum PreloadingStrategies {\n INITIALIZATION = \"initialization\",\n RUNTIME = \"runtime\"\n}\n","import { LocaleConfig } from \"../public-api\";\n\nexport class LocaleNotDefinedError extends Error {\n public override readonly name: string;\n\n constructor (locale: LocaleConfig) {\n super(`Locale \"${ locale.value }\" does not exists. Please check if the locale is provided in the translation config.`);\n this.name = \"LocaleNotDefinedError\";\n }\n}\n\n","export class SourceLocaleNotDefinedError extends Error {\n public override readonly name: string;\n\n constructor () {\n super(`A source locale does not exists. Please check if a source locale is provided in the translation config.`);\n this.name = \"SourceLocaleNotDefinedError\";\n }\n}\n\n","import { InjectionToken } from \"@angular/core\";\nimport { SpecificTranslateConfig } from \"../types/config.type\";\n\nexport const TRANSLATION_CONFIG_TOKEN: InjectionToken<SpecificTranslateConfig> = new InjectionToken<SpecificTranslateConfig>(\"translation-config-token\");\n","import { BehaviorSubject, Observable, Subject, distinctUntilChanged } from \"rxjs\";\nimport { Injectable, inject } from \"@angular/core\";\nimport { LoadedScope, LoadedScopes, LocaleLoadedScopes, NotifierScope, NotifierScopes, ScopedFile } from \"../types/store.type\";\nimport { LocaleConfig, SpecificTranslateConfig } from \"../types/config.type\";\nimport { CollectingStrategies } from \"../enums/collecting-strategies.enum\";\nimport { LocaleNotDefinedError } from \"../errors/locale-not-defined.error\";\nimport { SourceLocaleNotDefinedError } from \"../errors/source-locale-not-defined.error\";\nimport { TRANSLATION_CONFIG_TOKEN } from \"../tokens/config.token\";\n\ntype StorageAttributes = {\n collectingStrategy: CollectingStrategies;\n locale: {\n type: Storage;\n key: string;\n };\n translations: {\n type: Storage;\n key: string;\n };\n};\n\ntype StoreTranslations = LoadedScopes | LocaleLoadedScopes;\n\n/*\n * A scope name of null is the default scope, otherwise, it is the identifier for a translation file.\n */\n@Injectable({\n providedIn: \"root\"\n})\nexport class TranslationStoreService {\n private _loadedScopes: LoadedScopes | LocaleLoadedScopes | null;\n\n private _notifierScopes: NotifierScopes | null = null;\n\n private readonly _translationConfig: SpecificTranslateConfig = inject(TRANSLATION_CONFIG_TOKEN);\n\n private readonly _locale: BehaviorSubject<LocaleConfig>;\n\n private readonly _locale$: Observable<LocaleConfig>;\n\n private readonly _storageAttributes: StorageAttributes = {\n collectingStrategy: this._translationConfig.storageConfig?.collectingStrategy ?? CollectingStrategies.CURRENT,\n locale: {\n type: this._translationConfig.storageConfig?.locale?.type ?? window.localStorage,\n key: this._translationConfig.storageConfig?.locale?.key ?? \"locale\"\n },\n translations: {\n type: this._translationConfig.storageConfig?.translations?.type ?? window.sessionStorage,\n key: this._translationConfig.storageConfig?.translations?.key ?? \"translations\"\n }\n };\n\n constructor () {\n const sourceLocale: LocaleConfig | undefined = this.getSourceLocale();\n\n if (sourceLocale === undefined)\n throw new SourceLocaleNotDefinedError();\n\n if (this._translationConfig.storageConfig?.locale?.store) {\n const localeStorageValue: LocaleConfig | null = this._getStorageValue(\"locale\") as LocaleConfig | null;\n const localeExists: boolean = localeStorageValue ? this._checkIfLocaleExists(localeStorageValue) : false;\n const isSourceLocale: boolean = localeStorageValue ? this._checkIfLocaleIsSourceLocale(localeStorageValue) : false;\n\n if (!localeExists || isSourceLocale)\n this._setStorageValue(\"locale\", null);\n\n this._locale = new BehaviorSubject<LocaleConfig>(localeStorageValue ?? sourceLocale);\n } else\n this._locale = new BehaviorSubject<LocaleConfig>(sourceLocale);\n\n if (this._translationConfig.storageConfig?.translations?.store) {\n const translationsStorageValue: StoreTranslations | null = this._getStorageValue(\"translations\") as StoreTranslations | null;\n\n this._loadedScopes = translationsStorageValue ?? null;\n } else\n this._loadedScopes = null;\n\n this._locale$ = this._locale.asObservable();\n }\n\n public getCurrentLocale (): LocaleConfig {\n return this._locale.value;\n }\n\n public getLocale$ (): Observable<LocaleConfig> {\n return this._locale$.pipe(distinctUntilChanged());\n }\n\n public setLocale (locale: LocaleConfig): void {\n if (!this._checkIfLocaleExists(locale))\n throw new LocaleNotDefinedError(locale);\n\n if (this._translationConfig.storageConfig?.locale?.store) {\n this._checkIfLocaleIsSourceLocale(locale)\n ? this._setStorageValue(\"locale\", null)\n : this._setStorageValue(\"locale\", locale);\n }\n\n if (this._translationConfig.storageConfig?.translations?.store && this._translationConfig.storageConfig.collectingStrategy === CollectingStrategies.CURRENT) this._setStorageValue(\"translations\", null);\n\n this._locale.next(locale);\n }\n\n public getLocales (): LocaleConfig[] {\n return this._translationConfig.locales;\n }\n\n public setScopedFile (scopeName: string | null, file: ScopedFile): void {\n const existingScope: LoadedScope | undefined = scopeName ? this._getScopeByScopeName(scopeName) : undefined;\n\n if (existingScope === undefined)\n this._appendScope(scopeName, file);\n else\n existingScope.file = file;\n\n\n if (this._translationConfig.storageConfig?.translations?.store) this._setStorageValue(\"translations\", this._loadedScopes);\n }\n\n public getScopedFile (scopeName: string | null): ScopedFile | undefined {\n const existingScope: LoadedScope | undefined = this._getScopeByScopeName(scopeName);\n\n return existingScope ? existingScope.file : undefined;\n }\n\n public hasScopedFile (scopeName: string | null): boolean {\n const existingScope: LoadedScope | undefined = this._getScopeByScopeName(scopeName);\n\n return Boolean(existingScope);\n }\n\n public hasScopedFiles (scopeNames: ReadonlyArray<string | null>): boolean {\n const existingScopes: LoadedScopes | undefined = this._getScopeByScopeNames(scopeNames);\n\n return Boolean(existingScopes);\n }\n\n public isScopeExisting (scopeName: string | null): boolean {\n const existingScope: LoadedScope | undefined = this._getScopeByScopeName(scopeName);\n\n return existingScope !== undefined;\n }\n\n public getExistingScopes (scopeNames: ReadonlyArray<string | null>): Array<string | null> {\n return scopeNames.filter((scopeName: string | null): boolean => this.isScopeExisting(scopeName));\n }\n\n public isScopeNameInNotifierScopes (scopeName: string | null): boolean {\n return Boolean(this._notifierScopes?.find((notifierScope: Readonly<NotifierScope>): boolean => notifierScope.scope === scopeName));\n }\n\n public areScopeNamesInNotifierScopes (scopeNames: ReadonlyArray<string | null>): boolean {\n return scopeNames.map((scopeName: string | null): boolean => this.isScopeNameInNotifierScopes(scopeName))\n .reduce((previous: boolean, current: boolean): boolean => previous && current);\n }\n\n public getExistingNotifierScopes (scopeNames: ReadonlyArray<string | null>): Array<string | null> {\n return scopeNames.filter((scopeName: string | null): boolean => this.isScopeNameInNotifierScopes(scopeName));\n }\n\n public getNotiferScope (scopeName: string | null): NotifierScope | undefined {\n return this._notifierScopes?.find((notiferScope: NotifierScope): boolean => notiferScope.scope === scopeName);\n }\n\n public getNotifierScopes (scopeNames: ReadonlyArray<string | null>): NotifierScopes | undefined {\n const filteredScopes: NotifierScopes | [] = scopeNames.map((scopeName: string | null): NotifierScope | undefined => this.getNotiferScope(scopeName))\n .filter((notifierScope: Readonly<NotifierScope> | undefined): boolean => notifierScope !== undefined) as NotifierScopes | [];\n\n return filteredScopes.length === 0 ? filteredScopes : undefined;\n }\n\n public addNotifierScope (notifierScope: Readonly<NotifierScope>): void {\n this._notifierScopes === null\n ? this._notifierScopes = [ notifierScope ]\n : this._notifierScopes.push(notifierScope);\n }\n\n public addNotifierScopes (notifierScopes: NotifierScopes): void {\n this._notifierScopes === null\n ? this._notifierScopes = notifierScopes\n : this._notifierScopes.push(...notifierScopes);\n }\n\n public addNotifierToNotifierScope (scopeName: string | null, notifier: Subject<ScopedFile>): void {\n this._notifierScopes = this._notifierScopes?.map((notifierScope: NotifierScope): NotifierScope => {\n if (notifierScope.scope !== scopeName) return notifierScope;\n\n notifierScope.notifiers === null\n ? notifierScope.notifiers = [ notifier ]\n : notifierScope.notifiers.push(notifier);\n\n return notifierScope;\n }) ?? null;\n }\n\n public removeNotifierScope (scopeName: string | null): void {\n if (this._notifierScopes === null)\n return;\n\n this._notifierScopes = this._notifierScopes.filter((notifierScope: Readonly<NotifierScope>): boolean => notifierScope.scope !== scopeName);\n }\n\n public getSourceLocale (): LocaleConfig | undefined {\n return this._translationConfig.locales.find((localeConfig: LocaleConfig) => localeConfig.isSource);\n }\n\n private _checkIfLocaleExists (locale: LocaleConfig): boolean {\n return this._translationConfig.locales.some((configLocale: LocaleConfig): boolean => configLocale.value === locale.value);\n }\n\n private _checkIfLocaleIsSourceLocale (locale: LocaleConfig): boolean {\n return this._translationConfig.locales.some((localeConfig: LocaleConfig) => localeConfig.value === locale.value && localeConfig.isSource);\n }\n\n private _setStorageValue (dataType: \"locale\" | \"translations\", storageValue: unknown): void {\n storageValue === null\n ? this._storageAttributes[ dataType ].type.removeItem(this._storageAttributes[ dataType ].key)\n : this._storageAttributes[ dataType ].type.setItem(this._storageAttributes[ dataType ].key, JSON.stringify(storageValue));\n }\n\n private _getStorageValue (dataType: \"locale\" | \"translations\"): unknown {\n const serializedStorageValue: string | null = this._storageAttributes[ dataType ].type.getItem(this._storageAttributes[ dataType ].key);\n\n return serializedStorageValue ? JSON.parse(serializedStorageValue) : null;\n }\n\n private _getScopeByScopeName (scopeName: string | null): LoadedScope | undefined {\n if (this._storageAttributes.collectingStrategy === CollectingStrategies.ALL) {\n const localeLoadedScopes: LocaleLoadedScopes | null = this._loadedScopes as LocaleLoadedScopes;\n\n if (this._loadedScopes === null) return undefined;\n\n const _loadedScopes: LoadedScopes | undefined = localeLoadedScopes[ this._locale.value.value ];\n\n return _loadedScopes?.find((loadedScope: LoadedScope): boolean => loadedScope.scope === scopeName);\n }\n\n const loadedScopes: LoadedScopes | null = this._loadedScopes as LoadedScopes | null;\n\n return loadedScopes?.find((loadedScope: LoadedScope): boolean => loadedScope.scope === scopeName);\n }\n\n private _getScopeByScopeNames (scopeNames: ReadonlyArray<string | null>): LoadedScopes | undefined {\n if (this._storageAttributes.collectingStrategy === CollectingStrategies.ALL) {\n const localeLoadedScopes: LocaleLoadedScopes | null = this._loadedScopes as LocaleLoadedScopes;\n\n if (this._loadedScopes === null) return undefined;\n\n const _loadedScopes: LoadedScopes | undefined = localeLoadedScopes[ this._locale.value.value ];\n\n return _loadedScopes?.filter((loadedScope: LoadedScope): boolean => scopeNames.includes(loadedScope.scope));\n }\n\n const loadedScopes: LoadedScopes = this._loadedScopes as LoadedScopes;\n\n return loadedScopes.filter((loadedScope: LoadedScope): boolean => scopeNames.includes(loadedScope.scope));\n }\n\n private _appendScope (scopeName: string | null, file: ScopedFile): void {\n if (this._storageAttributes.collectingStrategy === CollectingStrategies.ALL) {\n const localeLoadedScopes: LocaleLoadedScopes | null = this._loadedScopes as LocaleLoadedScopes | null;\n\n if (localeLoadedScopes === null) {\n this._loadedScopes = { [ this._locale.value.value ]: [ { scope: scopeName, file } ] };\n\n return;\n }\n\n const loadedScope: LoadedScopes | undefined = localeLoadedScopes[ this._locale.value.value ];\n\n if (loadedScope === undefined) {\n localeLoadedScopes[ this._locale.value.value ] = [ { scope: scopeName, file } ];\n\n return;\n }\n\n loadedScope.push({ scope: scopeName, file });\n\n return;\n }\n\n this._loadedScopes === null\n ? this._loadedScopes = [ { scope: scopeName, file } ]\n : (this._loadedScopes as LoadedScopes).push({ scope: scopeName, file });\n }\n}\n","import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from \"@angular/common/http\";\nimport { Injectable, inject } from \"@angular/core\";\nimport { LocaleConfig } from \"../types/config.type\";\nimport { Observable } from \"rxjs\";\nimport { TranslationStoreService } from \"../services/translation-store.service\";\n\n@Injectable({\n providedIn: \"root\"\n})\nexport class TranslationInterceptor implements HttpInterceptor {\n private readonly _translationStoreService: TranslationStoreService = inject(TranslationStoreService);\n\n public intercept (httpRequest: Readonly<HttpRequest<unknown>>, httpHandler: Readonly<HttpHandler>): Observable<HttpEvent<unknown>> {\n const currentLocale: LocaleConfig = this._translationStoreService.getCurrentLocale();\n const clonedHttpRequest: HttpRequest<unknown> = httpRequest.clone({\n setHeaders: {\n language: currentLocale.value\n }\n });\n\n return httpHandler.handle(clonedHttpRequest);\n }\n}\n","import { InjectionToken } from \"@angular/core\";\n\nexport const TRANSLATION_SCOPE_TOKEN: InjectionToken<string> = new InjectionToken<string>(\"translation-scope-token\");\n","export class ScopeNotDefinedError extends Error {\n public override readonly name: string;\n\n constructor (isDefaultScope: boolean, scopeName?: string) {\n super(\n isDefaultScope\n ? `The default scope does not exists. Please check if the given scope is provided.`\n : `Scope \"${ scopeName }\" does not exists. Please check if the given scope is provided.`\n );\n this.name = \"ScopeNotDefinedError\";\n }\n}\n","export class TranslationNotDefinedError extends Error {\n public override name: string;\n\n constructor (token: string, value?: string) {\n super(`Translation for token \"${ token }\" ${ value && `and its default value \"${ value }\"` } does not exists. Please check if the translation for the given token is provided in the translations.`);\n this.name = \"TranslationNotDefinedError\";\n }\n}\n","import { MultiScopedFile, ParsedMultiScopedFiles, ScopedFile } from \"../types/store.type\";\nimport { ScopeNotDefinedError } from \"../errors/scope-not-defined.error\";\nimport { TranslationNotDefinedError } from \"../errors/translation-not-defined.error\";\n\n/**\n * Parse a multi scoped file by the defined scopes\n * @param {MultiScopedFile} multiScopedFile - The multi scoped file, that should be parsed\n * @returns {ParsedMultiScopedFiles} - A partial scoped file for processing it further\n */\nexport const parseMultiScopedFile = (multiScopedFile: MultiScopedFile): ParsedMultiScopedFiles => {\n const splittedMultiScopedFile: ParsedMultiScopedFiles = [];\n const globalScopedFile: Record<string, string> = {};\n\n Object.keys(multiScopedFile).forEach((key: string): void => {\n const currentItem: ScopedFile | undefined = multiScopedFile[ key ];\n\n if (!(currentItem !== undefined && !Array.isArray(currentItem)))\n return;\n\n if (typeof currentItem === \"object\") {\n return void splittedMultiScopedFile.push({\n scopeName: key,\n file: currentItem\n });\n }\n\n globalScopedFile[ key ] = currentItem;\n });\n splittedMultiScopedFile.push({\n scopeName: null,\n file: globalScopedFile\n });\n\n return splittedMultiScopedFile;\n};\nexport const splitMultiScopedFile = (multiScopedFile: MultiScopedFile): ScopedFile[] => {\n const scopedFile: ScopedFile[] = [];\n const globalScopedFile: Record<string, string> = {};\n\n Object.keys(multiScopedFile).forEach((key: string): void => {\n const currentItem: Readonly<Record<string, string>> | undefined = multiScopedFile[ key ];\n\n if (!(currentItem !== undefined && !Array.isArray(currentItem)))\n return;\n\n if (typeof currentItem === \"object\")\n return void scopedFile.push(currentItem);\n\n globalScopedFile[ key ] = currentItem;\n });\n scopedFile.push(globalScopedFile);\n\n return scopedFile;\n};\n/**\n * Find a scope inside a multi scoped file\\\n * Throws an error if the scope could not be resolved out of the multi scoped file\n * @param {MultiScopedFile} multiScopedFile - The multi scoped file, that should include the scope\n * @param {string | null} scopeName - The scope name, that'll be searched\n * @returns {ScopedFile} - The found scoped file\n */\nexport const findScopeInMultiScopedFile = (multiScopedFile: MultiScopedFile, scopeName: string | null): ScopedFile => {\n // Filter all not-nested elements to represent the global scope.\n if (scopeName === null) {\n const newScopedFile: Record<string, string> = {};\n\n Object.keys(multiScopedFile).forEach((key: string): void => {\n const currentItem: Readonly<Record<string, string>> | undefined = multiScopedFile[ key ];\n\n if (!(currentItem !== undefined && typeof currentItem !== \"object\" && !Array.isArray(currentItem)))\n return;\n\n newScopedFile[ key ] = currentItem;\n });\n\n return newScopedFile;\n }\n\n // Get the specific scoped file\n const scopedFile: ScopedFile | undefined = multiScopedFile[ scopeName ];\n\n if (scopedFile === undefined) throw new ScopeNotDefinedError(false, scopeName);\n\n return scopedFile;\n};\n/**\n * Parse a multi scoped file\n * @param {MultiScopedFile} multiScopedFile - The multi scoped file, that'll be parsed\n * @param {string[] | string} scopeName - The scope name, that'll be resolved\n * @returns {ScopedFile[]} - All scoped files, that match the scope name\n */\nexport const parseMultiScopedFileByScope = (multiScopedFile: MultiScopedFile, scopeName: string[] | string): ScopedFile[] => {\n if (Array.isArray(scopeName))\n return scopeName.map((_scopeName: string): ScopedFile => findScopeInMultiScopedFile(multiScopedFile, _scopeName));\n\n\n return [ findScopeInMultiScopedFile(multiScopedFile, scopeName) ];\n};\nexport const findTokenInScopedFiles = (scopedFiles: readonly ScopedFile[], token: string): ScopedFile => {\n const foundScopedFile: ScopedFile | undefined = scopedFiles.find((scopedFile: ScopedFile): boolean => Object.keys(scopedFile).includes(token));\n\n if (foundScopedFile === undefined) throw new TranslationNotDefinedError(token);\n\n return foundScopedFile;\n};\n\n","import { SpecificTranslateConfig } from \"../public-api\";\nimport { MultiScopedFile, ScopedFile } from \"../types/store.type\";\nimport { findScopeInMultiScopedFile } from \"./file.util\";\n\n/**\n * Translate token by a scoped file\n * @param {ScopedFile} scopedFile - The scoped file, that should include the translation\n * @param {string} token - The token, that'll be translated\n * @returns {string | undefined} - string, that represents the translated token. If no translation was found undefined.\n */\nexport const translateTokenByScopedFile = (scopedFile: ScopedFile, token: string): string | undefined => {\n const isTokenValid: boolean = Object.keys(scopedFile).includes(token);\n\n if (!isTokenValid) return;\n\n return scopedFile[ token ];\n};\nexport const translateTokenByScopedFiles = (scopedFiles: ScopedFile[], token: string): string | undefined => {\n let translation: string | undefined;\n\n scopedFiles.some((scopedFile: ScopedFile): boolean => {\n const isTokenValid: boolean = Object.keys(scopedFile).includes(token);\n\n if (!isTokenValid) return false;\n\n translation = scopedFile[ token ];\n\n return true;\n });\n\n return translation;\n};\n/**\n * Translate token by a multi scoped file by first resolving the scope out of the multi scoped file\n * @param {MultiScopedFile} multiScopedFile - The MultiScopedFile, that should includes the scope to search and the token\n * @param {string | null} scopeName - The scope name which should include the translation for the token\n * @param {string} token - The token, that'll be translated\n * @returns {string | undefined} - string, that represents the translated token. If no translation was found undefined.\n */\nexport const translateTokenByMultiScopedFile = (multiScopedFile: MultiScopedFile, scopeName: string | null, token: string): string | undefined => {\n const scopedFile: ScopedFile = findScopeInMultiScopedFile(multiScopedFile, scopeName);\n\n return translateTokenByScopedFile(scopedFile, token);\n};\nexport function resolveScope<T extends ReadonlyArray<string | null> | string | null> (translationConfig: SpecificTranslateConfig | null, scopeName?: T): T {\n return (scopeName ?? translationConfig?.defaultScope ?? null) as T;\n}\n\n","import { InjectionToken } from \"@angular/core\";\nimport { HttpHeadersOption, HttpOptions } from \"@ogs-gmbh/ngx-http\";\n\nexport const TRANSLATION_HTTP_CONFIG: InjectionToken<string> = new InjectionToken<string>(\"translation-http-config\");\nexport const TRANSLATION_HTTP_OPTIONS: InjectionToken<HttpOptions<never, HttpHeadersOption, never>> = new InjectionToken<HttpOptions<never, HttpHeadersOption, never>>(\"translation-http-options\");\n","import { HttpHeadersOption, HttpOptions, mergeHttpHeaders } from \"@ogs-gmbh/ngx-http\";\nimport { HttpClient, HttpHeaders } from \"@angular/common/http\";\nimport { Injectable, inject } from \"@angular/core\";\nimport { Observable, retry, timeout } from \"rxjs\";\nimport { SpecificTranslateConfig } from \"../types/config.type\";\nimport { TRANSLATION_CONFIG_TOKEN } from \"../tokens/config.token\";\nimport { TRANSLATION_HTTP_CONFIG, TRANSLATION_HTTP_OPTIONS } from \"../tokens/http.token\";\n\n@Injectable({\n providedIn: \"root\"\n})\nexport class TranslationHttpSerivce {\n private readonly _translationHttpConfig: string = inject(TRANSLATION_HTTP_CONFIG);\n\n private readonly _translationConfig: SpecificTranslateConfig | null = inject(TRANSLATION_CONFIG_TOKEN, { optional: true });\n\n private readonly _translationHttpOptions: HttpOptions<never, HttpHeadersOption, never> | null = inject(TRANSLATION_HTTP_OPTIONS, { optional: true });\n\n public getWithRef$<T>(httpClientRef: Readonly<HttpClient>, scopeName: ReadonlyArray<string | null> | string | null, httpOptions?: HttpOptions<never, HttpHeadersOption, never>): Observable<T> {\n let path: string = this._translationHttpConfig;\n\n if (typeof scopeName === \"string\" || scopeName === null) {\n if (scopeName !== null)\n path += `?scope=${ encodeURIComponent(scopeName) }`;\n } else {\n const urlSearchParams: URLSearchParams = new URLSearchParams();\n\n scopeName.forEach((scopeNameItem: string | null): void => {\n if (scopeNameItem === null) return;\n\n urlSearchParams.append(\"scope\", encodeURIComponent(scopeNameItem));\n });\n path += `?${ urlSearchParams.toString() }`;\n }\n\n let headers: HttpHeaders = new HttpHeaders();\n\n if (this._translationHttpOptions?.headers !== undefined)\n headers = mergeHttpHeaders(headers, this._translationHttpOptions.headers);\n\n if (httpOptions?.headers !== undefined)\n headers = mergeHttpHeaders(headers, httpOptions.headers);\n\n return httpClientRef.get<T>(path, {\n responseType: \"json\",\n observe: \"body\",\n headers\n }).pipe(\n timeout(this._translationConfig?.timeout ?? 3000),\n retry(0)\n );\n }\n}\n","import { HttpRequestStatus, HttpOptions, HttpHeadersOption } from \"@ogs-gmbh/ngx-http\";\nimport { BehaviorSubject, EMPTY, Observable, Subject, catchError, combineLatestWith, distinctUntilChanged, filter, map, of, switchMap, tap } from \"rxjs\";\nimport { Injectable, SkipSelf, inject } from \"@angular/core\";\nimport { LocaleConfig, SpecificTranslateConfig } from \"../types/config.type\";\nimport { MultiScopedFile, NotifierScope, ParsedMultiScopedFile, ParsedMultiScopedFiles, ScopedFile } from \"../types/store.type\";\nimport { findScopeInMultiScopedFile, findTokenInScopedFiles, parseMultiScopedFile, splitMultiScopedFile } from \"../utils/file.util\";\nimport { resolveScope, translateTokenByScopedFile, translateTokenByScopedFiles } from \"../utils/translate.util\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { TRANSLATION_CONFIG_TOKEN } from \"../tokens/config.token\";\nimport { TranslationHttpSerivce } from \"./translation-http.serivce\";\nimport { TranslationNotDefinedError } from \"../errors/translation-not-defined.error\";\nimport { TranslationStoreService } from \"./translation-store.service\";\n\n@Injectable({\n providedIn: \"root\"\n})\nexport class TranslationService {\n @SkipSelf()\n private readonly _httpClient: HttpClient = inject(HttpClient);\n\n private readonly _translationConfig: SpecificTranslateConfig = inject(TRANSLATION_CONFIG_TOKEN);\n\n private readonly _translationStoreService: TranslationStoreService = inject(TranslationStoreService);\n\n private readonly _translationHttpService: TranslationHttpSerivce = inject(TranslationHttpSerivce);\n\n private readonly _isHttpLoading: BehaviorSubject<HttpRequestStatus | null> = new BehaviorSubject<HttpRequestStatus | null>(null);\n\n private readonly _isHttpLoading$: Observable<HttpRequestStatus | null> = this._isHttpLoading.asObservable();\n\n /**\n * Getter for getting an Observable, that will emit only when a HTTP-Request is made\n * @returns {Observable<HttpRequestStatus | null>} - An observable with the HttpRequestStatus if HTTP is currently under use. Otherwise an observable with null inside.\n */\n public isHttpLoading$ (): Observable<HttpRequestStatus | null> {\n return this._isHttpLoading$.pipe(\n distinctUntilChanged(),\n filter((isHttpLoading: HttpRequestStatus | null): boolean => isHttpLoading !== null)\n );\n }\n\n /**\n * Preload multiple translations with references to the required services\n * @param {HttpClient} httpClient - The HttpClient, that'll be used\n * @param {TranslationStoreService} translationStoreService - The TranslationStoreService, that'll be used\n * @param {TranslationHttpSerivce} translationHttpService - The TranslationHttpService, that'll be used\n * @param {string} _locale - The locale, that should be preloaded\n * @param {string | null} scopeName - The scope name for the lookup of the translation\n * @param {HttpOptions | undefined} httpOptions - Additional HTTP Options for the request\n * @returns {Observable<void>} - An observable to handle the status\n */\n /* eslint-disable-next-line @tseslint/class-methods-use-this, @tseslint/max-params */\n public _preloadWithRef$ (\n httpClient: Readonly<HttpClient>,\n translationStoreService: Readonly<TranslationStoreService>,\n translationHttpService: Readonly<TranslationHttpSerivce>,\n _locale: LocaleConfig,\n scopeName: ReadonlyArray<string | null> | string | null,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<void> {\n /*\n * Handle resolve by store\n * Check if all scopes are in store\n */\n if ((typeof scopeName === \"string\" || scopeName === null) && translationStoreService.hasScopedFile(scopeName))\n return EMPTY;\n\n if (Array.isArray(scopeName) && translationStoreService.hasScopedFiles(scopeName))\n return EMPTY;\n\n // Check if scopes are in notifer scopes\n if ((typeof scopeName === \"string\" || scopeName === null) && translationStoreService.isScopeNameInNotifierScopes(scopeName))\n return EMPTY;\n\n if (Array.isArray(scopeName) && translationStoreService.areScopeNamesInNotifierScopes(scopeName as ReadonlyArray<string | null>))\n return EMPTY;\n\n // Check if scopes are not in store\n if (typeof scopeName === \"string\" || scopeName === null) {\n return translationHttpService.getWithRef$<ScopedFile>(\n httpClient,\n scopeName,\n httpOptions\n ).pipe(\n tap((scopedFile: ScopedFile): void => {\n translationStoreService.setScopedFile(scopeName, scopedFile);\n }),\n map((): void => void 0)\n );\n } else {\n const existingScopes: Array<string | null> = translationStoreService.getExistingScopes(scopeName);\n\n if (existingScopes.length === scopeName.length)\n return EMPTY;\n\n const extinctScopes: Array<string | null> = scopeName.filter((_scopeName: string | null): boolean => !existingScopes.includes(_scopeName));\n\n if (extinctScopes.length === 0)\n return EMPTY;\n\n return translationHttpService.getWithRef$<MultiScopedFile>(\n httpClient,\n extinctScopes,\n httpOptions\n ).pipe(\n tap((multiScopedFile: MultiScopedFile): void => {\n const parsedMultiScopedFiles: ParsedMultiScopedFiles = parseMultiScopedFile(multiScopedFile);\n\n parsedMultiScopedFiles.forEach((parsedScopedFile: ParsedMultiScopedFile): void => {\n translationStoreService.setScopedFile(parsedScopedFile.scopeName, parsedScopedFile.file);\n });\n }),\n map((): void => void 0)\n );\n }\n }\n\n /**\n * Translates a token by the locale reactive\\\n * If the locale changes, a new translation based on the new locale will be emitted.\n * @param {string} token - The token to resolve the translation\n * @param {string | undefined} scopeName - A scope name to resolve the lookup\n * @param {string | undefined} value - The default value of the translation\n * @param {HttpOptions | undefined} httpOptions - Additional HTTP Options for the request\n * @returns {Observable<string>} - An observable with the current translation as string\n */\n public translateTokenByLocale$ (\n token: string,\n value: string,\n scopeName?: ReadonlyArray<string | null> | string | null,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<string> {\n return this._translationStoreService.getLocale$().pipe(switchMap((locale: LocaleConfig): Observable<string> => this._translateWithRef$(\n this._httpClient,\n this._translationStoreService,\n this._translationHttpService,\n locale,\n token,\n value,\n this._resolveScope(scopeName),\n httpOptions\n )));\n }\n\n /**\n * Translates a token by the current locale\\\n * If the locale changes, no new translation will be emitted.\n * @param {string} token - The token to resolve the translation\n * @param {string | undefined} scopeName - A scope name to resolve the lookup\n * @param {string | undefined} value - The default value of the translation\n * @param {HttpOptions | undefined} httpOptions - Additional HTTP Options for the request\n * @returns {Observable<string>} - An observable with the current translation as string\n */\n public translateTokenByCurrentLocale$ (\n token: string,\n value: string,\n scopeName?: ReadonlyArray<string | null> | string | null,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<string> {\n const currentLocale: LocaleConfig = this._translationStoreService.getCurrentLocale();\n\n return this._translateWithRef$(\n this._httpClient,\n this._translationStoreService,\n this._translationHttpService,\n currentLocale,\n token,\n value,\n scopeName,\n httpOptions\n );\n }\n\n /**\n * Preload translations by locale reactive.\n */\n /* eslint-disable-next-line @tseslint/max-params */\n public preloadByLocale (\n httpClient: Readonly<HttpClient>,\n translationStoreService: Readonly<TranslationStoreService>,\n translationHttpService: Readonly<TranslationHttpSerivce>,\n scopeName: string | null | ReadonlyArray<string | null>,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<void> {\n return translationStoreService.getLocale$().pipe(switchMap((locale: LocaleConfig): Observable<void> => this._preloadWithRef$(\n httpClient,\n translationStoreService,\n translationHttpService,\n locale,\n scopeName,\n httpOptions\n )));\n }\n\n /**\n * Preload translations by current locale.\n */\n /* eslint-disable-next-line @tseslint/max-params */\n public preloadByCurrentLocale (\n httpClient: Readonly<HttpClient>,\n translationStoreService: Readonly<TranslationStoreService>,\n translationHttpService: Readonly<TranslationHttpSerivce>,\n scopeName: string | null | ReadonlyArray<string | null>,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<void> {\n const currentLocale: LocaleConfig = translationStoreService.getCurrentLocale();\n\n return this._preloadWithRef$(\n httpClient,\n translationStoreService,\n translationHttpService,\n currentLocale,\n scopeName,\n httpOptions\n );\n }\n\n /**\n * Resolve scope(s) hierarchically\n * @param {Array<string | null> | string | null | undefined} scopeName - Scope name(s), that should be resolved\n * @returns {Array<string | null> | string | null} - based on type\n */\n private _resolveScope<T extends ReadonlyArray<string | null> | string | null>(scopeName?: T): T {\n return resolveScope(this._translationConfig, scopeName);\n }\n\n /**\n * Translate token by source locale\n * @param {string | undefined} value - The value, that should be translated\n * @param {string} token - The token, that is defined for the value\n * @returns {Observable<string>} - An observable with the translation as value\n */\n /* eslint-disable-next-line @tseslint/class-methods-use-this */\n private _translateBySourceLocale (token: string, value?: string): Observable<string> {\n if (!value)\n throw new TranslationNotDefinedError(token);\n\n return of(value);\n }\n\n /**\n * Translate token by resolving it from the storage\\\n * The first matching translation will be used if scope is provided here as an Array.\n * @param {string | null} scope - The scope, that will be looked up\n * @param {string} token - The token, that is defined for the value\n * @returns {Observable<string>} - An observable with the translation as value\n */\n private _translateByStore (scope: ReadonlyArray<string | null> | string | null, token: string): Observable<string> {\n if (typeof scope === \"string\" || scope === null) {\n /* eslint-disable-next-line @tseslint/no-non-null-assertion */\n const scopedFile: ScopedFile = this._translationStoreService.getScopedFile(scope)!;\n const _translatedToken: string | undefined = translateTokenByScopedFile(scopedFile, token);\n\n if (_translatedToken === undefined) throw new TranslationNotDefinedError(token);\n\n return of(_translatedToken);\n }\n\n let translatedToken: string | undefined;\n\n scope.map((_scope: string | null): ScopedFile | undefined => this._translationStoreService.getScopedFile(_scope))\n .some((scopedFile: ScopedFile | undefined): boolean => {\n if (scopedFile === undefined) return false;\n\n const _translatedToken: string | undefined = translateTokenByScopedFile(scopedFile, token);\n\n if (_translatedToken === undefined) return false;\n\n translatedToken = _translatedToken;\n\n return true;\n });\n\n if (translatedToken === undefined) throw new TranslationNotDefinedError(token);\n\n return of(translatedToken);\n }\n\n /**\n * Get scoped file by notifier\\\n * The resolved scoped got requested by HTTP\n * @param {string|null} scope - The scope to identify the notifier by. string is an explicit scope and null is the global scope\n * @returns {Observable<ScopedFile>} - An observable as the new notifier.\n */\n private _getScopedFileByNotifier (scope: string | null): Observable<ScopedFile> {\n const currentNotifier: Subject<ScopedFile> = new Subject<ScopedFile>();\n\n this._translationStoreService.addNotifierToNotifierScope(scope, currentNotifier);\n\n return currentNotifier.asObservable();\n }\n\n /**\n * Get scoped file by adding a notifier\\\n * The resolved scope should be requested by HTTP\n * @param {string | null} scope - The scope to identify the notifier by. string is an explicit scope and null is the global socpe.\n * @returns {Observable<ScopedFile>} - An observable as the new notifier.\n */\n private _getScopedFileByAddingNotifier (scope: string | null): Observable<ScopedFile> {\n const currentNotifier: Subject<ScopedFile> = new Subject<ScopedFile>();\n\n this._translationStoreService.addNotifierScope({\n scope,\n notifiers: [ currentNotifier ]\n });\n\n return currentNotifier;\n }\n\n /**\n * Get a (multi-)scoped file by HTTP\n * @param {HttpClient} httpClient - The HttpClient, that'll be used\n * @param {TranslationStoreService} translationStoreService - The TranslationStoreService, that'll be used\n * @param {TranslationHttpSerivce} translationHttpService - The TranslationHttpService, that'll be used\n * @param {Array<string | null>} scope - The scope as Array, where string is an explicit scope and null is the global scope\n * @param {HttpOptions | undefined} httpOptions - Additional HTTP Options for the request\n * @returns {Observable} - An observable with the (multi-)scoped file inside\n */\n /* eslint-disable-next-line @tseslint/max-params */\n private _getScopedFileByHttpWithRef$<T extends ReadonlyArray<string | null> | string | null>(\n httpClient: Readonly<HttpClient>,\n translationStoreService: Readonly<TranslationStoreService>,\n translationHttpService: Readonly<TranslationHttpSerivce>,\n scope: T,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<T extends ReadonlyArray<string | null> ? MultiScopedFile : ScopedFile> {\n if (typeof scope === \"string\" || scope === null)\n translationStoreService.addNotifierScope({ scope, notifiers: null });\n else\n translationStoreService.addNotifierScopes(scope.map((_scope: string | null): NotifierScope => ({ scope: _scope, notifiers: null })));\n\n\n this._isHttpLoading.next(HttpRequestStatus.PENDING);\n\n return translationHttpService.getWithRef$<T extends ReadonlyArray<string | null> ? MultiScopedFile : ScopedFile>(httpClient, scope, httpOptions).pipe(\n catchError((): Observable<never> => {\n this._isHttpLoading.next(HttpRequestStatus.ERROR);\n this._isHttpLoading.next(null);\n\n if (typeof scope === \"string\" || scope === null)\n translationStoreService.isScopeNameInNotifierScopes(scope) && translationStoreService.removeNotifierScope(scope);\n else {\n scope.forEach((_scope: string | null): void => {\n if (!translationStoreService.isScopeNameInNotifierScopes(_scope))\n return;\n\n translationStoreService.removeNotifierScope(_scope);\n });\n }\n\n return of();\n }),\n tap((genericScopedFile: T extends ReadonlyArray<string | null> ? MultiScopedFile : ScopedFile): void => {\n this._isHttpLoading.next(HttpRequestStatus.SUCCESS);\n this._isHttpLoading.next(null);\n\n if (typeof scope === \"string\" || scope === null) {\n translationStoreService.setScopedFile(scope, genericScopedFile as ScopedFile);\n\n const notifierScope: NotifierScope | undefined = translationStoreService.getNotiferScope(scope);\n\n if (notifierScope === undefined) return;\n\n notifierScope.notifiers?.forEach((notifier: Subject<ScopedFile>): void => {\n notifier.next(genericScopedFile as ScopedFile);\n });\n translationStoreService.removeNotifierScope(scope);\n\n return;\n }\n\n scope.forEach((_scope: string | null): void => {\n const parsedScopedFile: ScopedFile = findScopeInMultiScopedFile(genericScopedFile as MultiScopedFile, _scope);\n const notifierScope: NotifierScope | undefined = translationStoreService.getNotiferScope(_scope);\n\n if (notifierScope === undefined) return;\n\n notifierScope.notifiers?.forEach((notifier: Subject<ScopedFile>): void => {\n notifier.next(parsedScopedFile);\n });\n translationStoreService.removeNotifierScope(_scope);\n });\n })\n );\n }\n\n /**\n * Translate one token by a scope\n * @param {HttpClient} httpClient - The HttpClient, that'll be used\n * @param {TranslationStoreService} translationStoreService - The TranslationStoreService, that'll be used\n * @param {TranslationHttpSerivce} translationHttpService - The TranslationHttpService, that'll be used\n * @param {string} locale - The locale for resolving the translation\n * @param {string} scopeName - The scope name for resolving the translation\n * @param {string} token - The token for resolving the translation\n * @param {string | undefined} value - The default value for the token\n * @param {HttpOptions | undefined} httpOptions - Additional HTTP Options for the request\n * @returns {Observable<string>} - An observable with the translation as string inside\n *\n * Resolving steps:\n * 1. Check if it should not be translated\n * => If the current locale is the source locale\n * 2. Check if it can be translated by store\n * => If translation is stored in store\n * 3. Resolve translations by notifiers\n * => Exisiting scopes are resolved automatically\n * => Extinct scopes are resolved by HTTP\n * 4. Resolve by HTTP\n * => Every scope gets resolved by HTTP\n */\n /* eslint-disable-next-line @tseslint/max-params */\n private _translateWithRef$ (\n httpClient: Readonly<HttpClient>,\n translationStoreService: Readonly<TranslationStoreService>,\n translationHttpService: Readonly<TranslationHttpSerivce>,\n locale: LocaleConfig,\n token: string,\n value: string,\n scopeName?: ReadonlyArray<string | null> | string | null,\n httpOptions?: HttpOptions<never, HttpHeadersOption, never>\n ): Observable<string> {\n let resolvedScopes: ReadonlyArray<string | null> | string | null | undefined;\n\n const sourceLocale: LocaleConfig | undefined = this._translationStoreService.getSourceLocale();\n\n // Handle direct resolving\n if (sourceLocale !== undefined && sourceLocale === this._translationStoreService.getCurrentLocale())\n return this._translateBySourceLocale(token, value);\n\n // Handle store resolving\n if (typeof scopeName === \"string\" || scopeName === null) {\n resolvedScopes = this._resolveScope(scopeName);\n\n if (translationStoreService.hasScopedFile(resolvedScopes))\n return this._translateByStore(resolvedScopes, token);\n } else {\n resolvedScopes = this._resolveScope(scopeName);\n\n if (translationStoreService.hasScopedFiles(resolvedScopes))\n return this._translateByStore(resolvedScopes, token);\n }\n\n // Handle notifier resolving\n if (typeof resolvedScopes === \"string\" || resolvedScopes === null) {\n if (translationStoreService.isScopeNameInNotifierScopes(resolvedScopes)) {\n return this._getScopedFileByNotifier(resolvedScopes)\n .pipe(map((scopedFile: ScopedFile): string => {\n const translatedToken: string | undefined = translateTokenByScopedFile(scopedFile, token);\n\n if (translatedToken === undefined) throw new TranslationNotDefinedError(token, value);\n\n return translatedToken;\n }));\n }\n } else {\n const existingScopes: Array<string | null> = translationStoreService.getExistingNotifierScopes(resolvedScopes);\n const extinctScopes: Array<string | null> = resolvedScopes.filter((resolvedScope: string | null): boolean => !existingScopes.includes(resolvedScope));\n const existingNotifiers: Array<Observable<ScopedFile>> = existingScopes.map((existingScope: string | null): Observable<ScopedFile> => this._getScopedFileByNotifier(existingScope));\n const extinctScopesHttp: Observable<ScopedFile> = this._getScopedFileByHttpWithRef$(httpClient, translationStoreService, translationHttpService, extinctScopes, httpOptions)\n .pipe(\n tap((multiScopedFile: ScopedFile | MultiScopedFile): void => {\n const castedMultiScopedFile: MultiScopedFile = multiScopedFile as MultiScopedFile;\n const parsedMultiScopedFile: ParsedMultiScopedFiles = parseMultiScopedFile(castedMultiScopedFile);\n\n parsedMultiScopedFile.forEach((_parsedMultiScopedFile: ParsedMultiScopedFile): void => {\n translationStoreService.setScopedFile(\n _parsedMultiScopedFile.scopeName,\n _parsedMultiScopedFile.file\n );\n });\n }),\n map((multiScopedFile: ScopedFile | MultiScopedFile): ScopedFile => {\n const castedMultiScopedFile: MultiScopedFile = multiScopedFile as MultiScopedFile;\n const splittedMultiScopedFiles: ScopedFile[] = splitMultiScopedFile(castedMultiScopedFile);\n\n return findTokenInScopedFiles(splittedMultiScopedFiles, token);\n })\n );\n\n return extinctScopesHttp.pipe(\n combineLatestWith(...existingNotifiers),\n map((scopedFiles: ScopedFile[]): string => {\n const translatedToken: string | undefined = translateTokenByScopedFiles(scopedFiles, token);\n\n if (translatedToken === undefined) throw new TranslationNotDefinedError(token, value);\n\n return translatedToken;\n })\n );\n }\n\n // Handle HTTP resolving\n return of(locale).pipe(switchMap((): Observable<string> => (\n /* eslint-disable-next-line @tseslint/no-unnecessary-condition */\n typeof resolvedScopes === \"string\" || resolvedScopes === null\n ? this._getScopedFileByHttpWithRef$(\n httpClient,\n translationStoreService,\n translationHttpService,\n resolvedScopes,\n httpOptions\n ).pipe(map((scopedFile: ScopedFile): string => {\n translationStoreService.setScopedFile(\n resolvedScopes,\n scopedFile\n );\n\n const translatedToken: string | undefined = translateTokenByScopedFile(scopedFile, token);\n\n if (translatedToken === undefined)\n throw new TranslationNotDefinedError(token, value);\n\n return translatedToken;\n }))\n // Handle array\n : this._getScopedFileByHttpWithRef$(\n httpClient,\n translationStoreService,\n translationHttpService,\n resolvedScopes,\n httpOptions\n ).pipe(\n tap((multiScopedFile: MultiScopedFile): void => {\n const parsedMultiScopedFile: ParsedMultiScopedFiles = parseMultiScopedFile(multiScopedFile);\n\n parsedMultiScopedFile.forEach((_parsedMultiScopedFile: ParsedMultiScopedFile): void => {\n translationStoreService.setScopedFile(\n _parsedMultiScopedFile.scopeName,\n _parsedMultiScopedFile.file\n );\n });\n }),\n map((multiScopedFile: MultiScopedFile): string => {\n const splittedMultiScopedFiles: ScopedFile[] = splitMultiScopedFile(multiScopedFile);\n const foundScopedFile: ScopedFile = findTokenInScopedFiles(splittedMultiScopedFiles, token);\n const translatedToken: string | undefined = translateTokenByScopedFile(foundScopedFile, token);\n\n if (translatedToken === undefined)\n throw new TranslationNotDefinedError(token, value);\n\n return translatedToken;\n })\n )\n )));\n }\n}\n\n","import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform, inject