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,224 lines (1,208 loc) 60.8 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, Injectable, DestroyRef, ChangeDetectorRef, Pipe, provideAppInitializer, NgModule } from '@angular/core'; import { BehaviorSubject, distinctUntilChanged, timeout, retry, filter, EMPTY, tap, map, switchMap, of, Subject, catchError, combineLatestWith, throwError } from 'rxjs'; import { mergeHttpHeaders, HttpRequestStatus, buildHttpConnectionString } from '@ogs-gmbh/ngx-http'; import { HttpHeaders, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isEqual } from 'es-toolkit'; import { CommonModule } from '@angular/common'; /** * Strategies for collecting translations in the application, so that they don't have to be fetched multiple times. * @remarks Each option describes which {@link LocaleConfig} should be collected. * @deprecated Use {@link CollectingStrategy} instead. It'll be removed in upcoming releases. * @category NG config * * @since 1.0.0 * @author Simon Kovtyk */ var CollectingStrategies; (function (CollectingStrategies) { /** * Collect every translation, regardless of the current {@link LocaleConfig} * * @since 1.0.0 * @author Simon Kovtyk */ CollectingStrategies["ALL"] = "all"; /** * Collect only translations of the current locale * * @since 1.0.0 * @author Simon Kovtyk */ CollectingStrategies["CURRENT"] = "current"; })(CollectingStrategies || (CollectingStrategies = {})); /** * Strategies for collecting translations in the application, so that they don't have to be fetched multiple times. * @remarks Each option describes which {@link LocaleConfig} should be collected. * @category NG config * * @since 1.0.0 * @author Simon Kovtyk */ var CollectingStrategy; (function (CollectingStrategy) { /** * Collect every translation, regardless of the current locale * * @since 1.0.0 * @author Simon Kovtyk */ CollectingStrategy["ALL"] = "all"; /** * Collect only translations of the current locale * * @since 1.0.0 * @author Simon Kovtyk */ CollectingStrategy["CURRENT"] = "current"; })(CollectingStrategy || (CollectingStrategy = {})); /** * Strategies for preloading translations * @remarks Each option describes when the preload should happen. * @category NG config * @deprecated Use {@link PreloadingStrategy} instead. It'll be removed in upcoming releases. * * @since 1.0.0 * @author Simon Kovtyk */ var PreloadingStrategies; (function (PreloadingStrategies) { /** * Preload translations during application initialization * * @since 1.0.0 * @author Simon Kovtyk */ PreloadingStrategies["INITIALIZATION"] = "initialization"; /** * Preload translations during application runtime * * @since 1.0.0 * @author Simon Kovtyk */ PreloadingStrategies["RUNTIME"] = "runtime"; })(PreloadingStrategies || (PreloadingStrategies = {})); /** * Strategies for preloading translations * @remarks Each option describes when the preload should happen. * @category NG config * * @since 2.0.0 * @author Simon Kovtyk */ var PreloadingStrategy; (function (PreloadingStrategy) { /** * Preload translations during application initialization * * @since 2.0.0 * @author Simon Kovtyk */ PreloadingStrategy["INITIALIZATION"] = "initialization"; /** * Preload translations during application runtime * * @since 2.0.0 * @author Simon Kovtyk */ PreloadingStrategy["RUNTIME"] = "runtime"; })(PreloadingStrategy || (PreloadingStrategy = {})); /** * Error thrown when a scope is not defined * @category Errors * * @since 1.0.0 * @author Simon Kovtyk */ class ScopeNotDefinedError extends Error { name; constructor(snapshot) { super(snapshot.isDefaultScope ? `The default scope does not exists. Please check if the given scope is provided.` : `Scope does not exists. Please check if the given scope is provided.\n` + `State: ${JSON.stringify(snapshot)}`); this.name = "ScopeNotDefinedError"; } } /** * Error thrown when a requested {@link LocaleConfig} is not defined * @category Errors * * @since 1.0.0 * @author Simon Kovtyk */ class LocaleNotDefinedError extends Error { name; constructor(locale) { super(`Locale "${locale.value}" does not exists. Please check if the locale is provided in the translation config.`); this.name = "LocaleNotDefinedError"; } } /** * Error thrown when a translation for a given token is not defined * @category Errors * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationNotDefinedError extends Error { name; constructor(snapshot) { super(`Translation does not exists. Please check if the translation for the given token is provided in the translations. \n State: ${JSON.stringify(snapshot)}`); this.name = "TranslationNotDefinedError"; } } /** * Error thrown when a {@link LocaleConfig} as source locale is not defined * @category Errors * * @since 1.0.0 * @author Simon Kovtyk */ class SourceLocaleNotDefinedError extends Error { name; constructor(snapshot) { super(`A source locale does not exists. Please check if a source locale is provided in the translation config.\n` + `State: ${JSON.stringify(snapshot)}`); this.name = "SourceLocaleNotDefinedError"; } } /** * Injection token for {@link SpecificTranslateConfig} * @readonly * @category NG config * * @since 1.0.0 * @author Simon Kovtyk */ const TRANSLATION_CONFIG_TOKEN = new InjectionToken("translation-config-token"); /* eslint-disable-next-line @tseslint/no-shadow */ /** * Core service as abstraction layer for translaton storage and state management * @category NG services * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationStoreService { _loadedScopes; _notifierScopes = null; _translationConfig = inject(TRANSLATION_CONFIG_TOKEN); _locale; _locale$; _storageAttributes = { collectingStrategy: this._translationConfig.storageConfig?.collectingStrategy ?? CollectingStrategy.CURRENT, locale: { type: this._translationConfig.storageConfig?.locale?.type ?? window.localStorage, key: this._translationConfig.storageConfig?.locale?.key ?? "locale" }, translations: { type: this._translationConfig.storageConfig?.translations?.type ?? window.sessionStorage, key: this._translationConfig.storageConfig?.translations?.key ?? "translations" } }; constructor() { const sourceLocale = this.getSourceLocale(); if (sourceLocale === undefined) throw new SourceLocaleNotDefinedError(); if (this._translationConfig.storageConfig?.locale?.store) { const localeStorageValue = this._getStorageValue("locale"); const localeExists = localeStorageValue ? this._checkIfLocaleExists(localeStorageValue) : false; const isSourceLocale = localeStorageValue ? this._checkIfLocaleIsSourceLocale(localeStorageValue) : false; if (!localeExists || isSourceLocale) this._setStorageValue("locale", null); this._locale = new BehaviorSubject(localeStorageValue ?? sourceLocale); } else this._locale = new BehaviorSubject(sourceLocale); if (this._translationConfig.storageConfig?.translations?.store) { const translationsStorageValue = this._getStorageValue("translations"); this._loadedScopes = translationsStorageValue ?? null; } else this._loadedScopes = null; this._locale$ = this._locale.asObservable(); } /** * Get the current {@link LocaleConfig} * * @returns The current {@link LocaleConfig} * * @since 1.0.0 * @author Simon Kovtyk */ getCurrentLocale() { return this._locale.value; } /** * Get an `Observable` of the current {@link LocaleConfig} * * @returns An `Observable` of the current {@link LocaleConfig} * * @since 1.0.0 * @author Simon Kovtyk */ getLocale$() { return this._locale$.pipe(distinctUntilChanged()); } /** * Set the current {@link LocaleConfig} * * @param locale - The locale to set * @throws {@link LocaleNotDefinedError} if the provided locale is not defined * * @since 1.0.0 * @author Simon Kovtyk */ setLocale(locale) { if (!this._checkIfLocaleExists(locale)) throw new LocaleNotDefinedError(locale); if (this._translationConfig.storageConfig?.locale?.store) { this._checkIfLocaleIsSourceLocale(locale) ? this._setStorageValue("locale", null) : this._setStorageValue("locale", locale); } if (this._translationConfig.storageConfig?.translations?.store && this._translationConfig.storageConfig.collectingStrategy === CollectingStrategy.CURRENT) this._setStorageValue("translations", null); this._locale.next(locale); } /** * Get all defined locales * * @returns An `Array` of all defined locales * * @since 1.0.0 * @author Simon Kovtyk */ getLocales() { return this._translationConfig.locales; } /** * Set a scoped file * * @param scopeName - The name of the scope * @param file - The scoped file to set * * @since 1.0.0 * @author Simon Kovtyk */ setScopedFile(scopeName, file) { const existingScope = scopeName ? this._getScopeByScopeName(scopeName) : undefined; if (existingScope === undefined) this._appendScope(scopeName, file); else existingScope.file = file; if (this._translationConfig.storageConfig?.translations?.store) this._setStorageValue("translations", this._loadedScopes); } /** * Get a scoped file * * @param scopeName - The name of the scope, the {@link ScopedFile} belongs to * @returns if found {@link ScopedFile}, otherwise `undefined` * * @since 1.0.0 * @author Simon Kovtyk */ getScopedFile(scopeName) { const existingScope = this._getScopeByScopeName(scopeName); return existingScope ? existingScope.file : undefined; } /** * Check if a scoped file exists * * @param scopeName - The name of the scope, the {@link ScopedFile} belongs to * @returns `true` if found, otherwise `false` * * @since 1.0.0 * @author Simon Kovtyk */ hasScopedFile(scopeName) { const existingScope = this._getScopeByScopeName(scopeName); return Boolean(existingScope); } /** * Check if scoped files exist * * @param scopeNames - An `Array` of scope names, the ScopedFiles belong to * @returns `true` if all could be found, otherwise `false` * * @since 1.0.0 * @author Simon Kovtyk */ hasScopedFiles(scopeNames) { const existingScopes = this._getScopeByScopeNames(scopeNames); return Boolean(existingScopes); } /** * Check if a scope exists * * @param scopeName - The name of the scope * @returns `true` if found, otherwise `false` * * @since 1.0.0 * @author Simon Kovtyk */ isScopeExisting(scopeName) { const existingScope = this._getScopeByScopeName(scopeName); return existingScope !== undefined; } /** * Check which scopes exist * * @param scopeNames - An `Array` of scope names, that should be checked * @returns An `Array` of existing scope names * * @since 1.0.0 * @author Simon Kovtyk */ getExistingScopes(scopeNames) { return scopeNames.filter((scopeName) => this.isScopeExisting(scopeName)); } /** * Check if scope name is in notifier scopes * * @param scopeName - The name of the scope * @returns `true` if found, otherwise `false` * * @since 1.0.0 * @author Simon Kovtyk */ isScopeNameInNotifierScopes(scopeName) { return Boolean(this._notifierScopes?.find((notifierScope) => notifierScope.scope === scopeName)); } /** * Check if scope names are in notifier scopes * * @param scopeNames - An `Array` of scope names, that should be checked * @returns `true` if all are found, otherwise `false` * * @since 1.0.0 * @author Simon Kovtyk */ areScopeNamesInNotifierScopes(scopeNames) { return scopeNames.map((scopeName) => this.isScopeNameInNotifierScopes(scopeName)) .reduce((previous, current) => previous && current); } /** * Check which scope names are in notifier scopes * * @param scopeNames - An `Array` of scope names, that should be checked * @returns An `Array` of existing scope names * * @since 1.0.0 * @author Simon Kovtyk */ getExistingNotifierScopes(scopeNames) { return scopeNames.filter((scopeName) => this.isScopeNameInNotifierScopes(scopeName)); } /** * Get {@link NotifierScope} by scope name * * @param scopeName - The name of the scope * @returns if found {@link NotifierScope}, otherwise `undefined` * * @since 1.0.0 * @author Simon Kovtyk */ getNotiferScope(scopeName) { return this._notifierScopes?.find((notiferScope) => notiferScope.scope === scopeName); } /** * Get {@link NotifierScopes} by scope names * * @param scopeNames - An `Array` of scope names * @returns if found {@link NotifierScopes}, otherwise `undefined` * * @since 1.0.0 * @author Simon Kovtyk */ getNotifierScopes(scopeNames) { const filteredScopes = scopeNames.map((scopeName) => this.getNotiferScope(scopeName)) .filter((notifierScope) => notifierScope !== undefined); return filteredScopes.length === 0 ? filteredScopes : undefined; } /** * Add a notifier scope * * @param notifierScope - The notifier scope to add * * @since 1.0.0 * @author Simon Kovtyk */ addNotifierScope(notifierScope) { this._notifierScopes === null ? this._notifierScopes = [notifierScope] : this._notifierScopes.push(notifierScope); } /** * Add multiple notifier scopes * * @param notifierScopes - The notifier scopes to add * * @since 1.0.0 * @author Simon Kovtyk */ addNotifierScopes(notifierScopes) { this._notifierScopes === null ? this._notifierScopes = notifierScopes : this._notifierScopes.push(...notifierScopes); } /** * Add a notifier to a notifier scope * * @param scopeName - The name of the scope * @param notifier - The notifier to add * * @since 1.0.0 * @author Simon Kovtyk */ addNotifierToNotifierScope(scopeName, notifier) { this._notifierScopes = this._notifierScopes?.map((notifierScope) => { if (notifierScope.scope !== scopeName) return notifierScope; notifierScope.notifiers === null ? notifierScope.notifiers = [notifier] : notifierScope.notifiers.push(notifier); return notifierScope; }) ?? null; } /** * Remove a notifier scope * * @param scopeName - The name of the scope * * @since 1.0.0 * @author Simon Kovtyk */ removeNotifierScope(scopeName) { if (this._notifierScopes === null) return; this._notifierScopes = this._notifierScopes.filter((notifierScope) => notifierScope.scope !== scopeName); } /** * Get the source {@link LocaleConfig} * * @returns if defined {@link LocaleConfig}, otherwise `undefined` * * @since 1.0.0 * @author Simon Kovtyk */ getSourceLocale() { return this._translationConfig.locales.find((localeConfig) => localeConfig.isSource); } _checkIfLocaleExists(locale) { return this._translationConfig.locales.some((configLocale) => configLocale.value === locale.value); } _checkIfLocaleIsSourceLocale(locale) { return this._translationConfig.locales.some((localeConfig) => localeConfig.value === locale.value && localeConfig.isSource); } _setStorageValue(dataType, storageValue) { storageValue === null ? this._storageAttributes[dataType].type.removeItem(this._storageAttributes[dataType].key) : this._storageAttributes[dataType].type.setItem(this._storageAttributes[dataType].key, JSON.stringify(storageValue)); } _getStorageValue(dataType) { const serializedStorageValue = this._storageAttributes[dataType].type.getItem(this._storageAttributes[dataType].key); return serializedStorageValue ? JSON.parse(serializedStorageValue) : null; } _getScopeByScopeName(scopeName) { if (this._storageAttributes.collectingStrategy === CollectingStrategy.ALL) { const localeLoadedScopes = this._loadedScopes; if (this._loadedScopes === null) return undefined; const _loadedScopes = localeLoadedScopes[this._locale.value.value]; return _loadedScopes?.find((loadedScope) => loadedScope.scope === scopeName); } const loadedScopes = this._loadedScopes; return loadedScopes?.find((loadedScope) => loadedScope.scope === scopeName); } _getScopeByScopeNames(scopeNames) { if (this._storageAttributes.collectingStrategy === CollectingStrategy.ALL) { const localeLoadedScopes = this._loadedScopes; if (this._loadedScopes === null) return undefined; const _loadedScopes = localeLoadedScopes[this._locale.value.value]; return _loadedScopes?.filter((loadedScope) => scopeNames.includes(loadedScope.scope)); } const loadedScopes = this._loadedScopes; return loadedScopes.filter((loadedScope) => scopeNames.includes(loadedScope.scope)); } _appendScope(scopeName, file) { if (this._storageAttributes.collectingStrategy === CollectingStrategy.ALL) { const localeLoadedScopes = this._loadedScopes; if (localeLoadedScopes === null) { this._loadedScopes = { [this._locale.value.value]: [{ scope: scopeName, file }] }; return; } const loadedScope = localeLoadedScopes[this._locale.value.value]; if (loadedScope === undefined) { localeLoadedScopes[this._locale.value.value] = [{ scope: scopeName, file }]; return; } loadedScope.push({ scope: scopeName, file }); return; } this._loadedScopes === null ? this._loadedScopes = [{ scope: scopeName, file }] : this._loadedScopes.push({ scope: scopeName, file }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationStoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationStoreService, providedIn: "root" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationStoreService, decorators: [{ type: Injectable, args: [{ providedIn: "root" }] }], ctorParameters: () => [] }); /** * Interceptor to add the current locale language header to HTTP requests. * @remarks Adds a `language` header with `value` of the current {@link LocaleConfig} to each outgoing HTTP request. * @category NG interceptors * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationInterceptor { _translationStoreService = inject(TranslationStoreService); intercept(httpRequest, httpHandler) { const currentLocale = this._translationStoreService.getCurrentLocale(); const clonedHttpRequest = httpRequest.clone({ setHeaders: { language: currentLocale.value } }); return httpHandler.handle(clonedHttpRequest); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationInterceptor, providedIn: "root" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationInterceptor, decorators: [{ type: Injectable, args: [{ providedIn: "root" }] }] }); /** * Injection token that holds a translation scope * @readonly * @category NG config * * @since 1.0.0 * @author Simon Kovtyk */ const TRANSLATION_SCOPE_TOKEN = new InjectionToken("translation-scope-token"); /** * Parse a multi scoped file by the defined scopes * @param multiScopedFile - The multi scoped file, that should be parsed * @returns A partial scoped file for processing it further */ const parseMultiScopedFile = (multiScopedFile) => { const splittedMultiScopedFile = []; const globalScopedFile = {}; Object.keys(multiScopedFile).forEach((key) => { const currentItem = multiScopedFile[key]; if (!(currentItem !== undefined && !Array.isArray(currentItem))) return; if (typeof currentItem === "object") { return void splittedMultiScopedFile.push({ scopeName: key, file: currentItem }); } globalScopedFile[key] = currentItem; }); splittedMultiScopedFile.push({ scopeName: null, file: globalScopedFile }); return splittedMultiScopedFile; }; const splitMultiScopedFile = (multiScopedFile) => { const scopedFile = []; const globalScopedFile = {}; Object.keys(multiScopedFile).forEach((key) => { const currentItem = multiScopedFile[key]; if (!(currentItem !== undefined && !Array.isArray(currentItem))) return; if (typeof currentItem === "object") return void scopedFile.push(currentItem); globalScopedFile[key] = currentItem; }); scopedFile.push(globalScopedFile); return scopedFile; }; /** * Find a scope inside a multi scoped file\ * Throws an error if the scope could not be resolved out of the multi scoped file * @param multiScopedFile - The multi scoped file, that should include the scope * @param scopeName - The scope name, that'll be searched * @returns The found scoped file * @throws {@link ScopeNotDefinedError} When the scope could not be found in the multi scoped file */ const findScopeInMultiScopedFile = (multiScopedFile, scopeName) => { // Filter all not-nested elements to represent the global scope. if (scopeName === null) { const newScopedFile = {}; Object.keys(multiScopedFile).forEach((key) => { const currentItem = multiScopedFile[key]; if (!(currentItem !== undefined && typeof currentItem !== "object" && !Array.isArray(currentItem))) return; newScopedFile[key] = currentItem; }); return newScopedFile; } // Get the specific scoped file const scopedFile = multiScopedFile[scopeName]; if (scopedFile === undefined) { throw new ScopeNotDefinedError({ isDefaultScope: false, scope: scopeName }); } return scopedFile; }; /** * Parse a multi scoped file * @param multiScopedFile - The multi scoped file, that'll be parsed * @param scopeName - The scope name, that'll be resolved * @returns All scoped files, that match the scope name */ const parseMultiScopedFileByScope = (multiScopedFile, scopeName) => { if (Array.isArray(scopeName)) return scopeName.map((_scopeName) => findScopeInMultiScopedFile(multiScopedFile, _scopeName)); return [findScopeInMultiScopedFile(multiScopedFile, scopeName)]; }; /** * Find a token in the provided scoped file * @param scopedFiles - The scoped file, that should include the token * @param token - The token, that'll be searched * @returns The scoped file with the found token * @throws {@link TranslationNotDefinedError} When the token could not be found in the scoped file */ const findTokenInScopedFiles = (scopedFiles, token) => { const foundScopedFile = scopedFiles.find((scopedFile) => Object.keys(scopedFile).includes(token)); if (foundScopedFile === undefined) { throw new TranslationNotDefinedError({ token }); } return foundScopedFile; }; /** * Translate token by a scoped file * @param scopedFile - The scoped file, that should include the translation * @param token - The token, that'll be translated * @returns A `string`, that represents the translated token. If no translation was found `undefined`. */ const translateTokenByScopedFile = (scopedFile, token) => { const isTokenValid = Object.keys(scopedFile).includes(token); if (!isTokenValid) return; return scopedFile[token]; }; const translateTokenByScopedFiles = (scopedFiles, token) => { let translation; scopedFiles.some((scopedFile) => { const isTokenValid = Object.keys(scopedFile).includes(token); if (!isTokenValid) return false; translation = scopedFile[token]; return true; }); return translation; }; /** * Translate token by a multi scoped file by first resolving the scope out of the multi scoped file * @param multiScopedFile - The MultiScopedFile, that should includes the scope to search and the token * @param scopeName - The scope name which should include the translation for the token * @param token - The token, that'll be translated * @returns A `string`, that represents the translated token. If no translation was found `undefined`. */ const translateTokenByMultiScopedFile = (multiScopedFile, scopeName, token) => { const scopedFile = findScopeInMultiScopedFile(multiScopedFile, scopeName); return translateTokenByScopedFile(scopedFile, token); }; function resolveScope(translationConfig, scopeName) { return (scopeName ?? translationConfig?.defaultScope ?? null); } /** * Injection token for HTTP configuration * @readonly * @category NG config * * @since 1.0.0 * @author Simon Kovtyk */ const TRANSLATION_HTTP_CONFIG = new InjectionToken("translation-http-config"); /** * Injection token for additional HTTP configuration * @readonly * * @since 1.0.0 * @author Simon Kovtyk */ const TRANSLATION_HTTP_OPTIONS = new InjectionToken("translation-http-options"); /** * Core service as abstraction layer for HTTP handling * @category NG services * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationHttpService { _translationHttpConfig = inject(TRANSLATION_HTTP_CONFIG); _translationConfig = inject(TRANSLATION_CONFIG_TOKEN, { optional: true }); _translationHttpOptions = inject(TRANSLATION_HTTP_OPTIONS, { optional: true }); /** * Gets translations with the provided `HttpClient` reference. * * @param httpClientRef - The `HttpClient` reference to use for the request. * @param scopeName - The scope name(s) for the translations. * @param httpOptions - Optional `HttpOptions` to customize the request. * @returns An `Observable` of the requested translations. * * @since 1.0.0 * @author Simon Kovtyk */ getWithRef$(httpClientRef, scopeName, httpOptions) { let path = this._translationHttpConfig; if (typeof scopeName === "string" || scopeName === null) { if (scopeName !== null) path += `?scope=${encodeURIComponent(scopeName)}`; } else { const urlSearchParams = new URLSearchParams(); scopeName.forEach((scopeNameItem) => { if (scopeNameItem === null) return; urlSearchParams.append("scope", encodeURIComponent(scopeNameItem)); }); path += `?${urlSearchParams.toString()}`; } let headers = new HttpHeaders(); if (this._translationHttpOptions?.headers !== undefined) headers = mergeHttpHeaders(headers, this._translationHttpOptions.headers); if (httpOptions?.headers !== undefined) headers = mergeHttpHeaders(headers, httpOptions.headers); return httpClientRef.get(path, { responseType: "json", observe: "body", headers }).pipe(timeout(this._translationConfig?.timeout ?? 3000), retry(0)); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationHttpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationHttpService, providedIn: "root" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationHttpService, decorators: [{ type: Injectable, args: [{ providedIn: "root" }] }] }); /** * Core service for translation handling * @category NG services * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationService { _httpClient = inject(HttpClient, { host: true }); _translationConfig = inject(TRANSLATION_CONFIG_TOKEN); _translationStoreService = inject(TranslationStoreService); _translationHttpService = inject(TranslationHttpService); _isHttpLoading = new BehaviorSubject(null); _isHttpLoading$ = this._isHttpLoading.asObservable(); /** * An `Observable`, that will emit only when a HTTP-Request is made * @returns An `Observable` with the `HttpRequestStatus` if HTTP is currently under use. Otherwise an `Observable` with null inside. * * @since 1.0.0 * @author Simon Kovtyk */ isHttpLoading$() { return this._isHttpLoading$.pipe(distinctUntilChanged(), filter((isHttpLoading) => isHttpLoading !== null)); } /** * Preload multiple translations with references * @param httpClient - The `HttpClient`, that'll be used * @param translationStoreService - The {@link TranslationStoreService}, that'll be used * @param translationHttpService - The {@link TranslationHttpService}, that'll be used * @param _locale - The {@link LocaleConfig}, that should be preloaded * @param scopeName - The scope name for the lookup of the translation * @param httpOptions - Additional HTTP Options for the request * @returns An `Observable` to handle the status * * @since 1.0.0 * @author Simon Kovtyk */ /* eslint-disable-next-line @tseslint/class-methods-use-this, @tseslint/max-params */ _preloadWithRef$(httpClient, translationStoreService, translationHttpService, _locale, scopeName, httpOptions) { /* * Handle resolve by store * Check if all scopes are in store */ if ((typeof scopeName === "string" || scopeName === null) && translationStoreService.hasScopedFile(scopeName)) return EMPTY; if (Array.isArray(scopeName) && translationStoreService.hasScopedFiles(scopeName)) return EMPTY; // Check if scopes are in notifer scopes if ((typeof scopeName === "string" || scopeName === null) && translationStoreService.isScopeNameInNotifierScopes(scopeName)) return EMPTY; if (Array.isArray(scopeName) && translationStoreService.areScopeNamesInNotifierScopes(scopeName)) return EMPTY; // Check if scopes are not in store if (typeof scopeName === "string" || scopeName === null) { return translationHttpService.getWithRef$(httpClient, scopeName, httpOptions).pipe(tap((scopedFile) => { translationStoreService.setScopedFile(scopeName, scopedFile); }), map(() => void 0)); } else { const existingScopes = translationStoreService.getExistingScopes(scopeName); if (existingScopes.length === scopeName.length) return EMPTY; const extinctScopes = scopeName.filter((_scopeName) => !existingScopes.includes(_scopeName)); if (extinctScopes.length === 0) return EMPTY; return translationHttpService.getWithRef$(httpClient, extinctScopes, httpOptions).pipe(tap((multiScopedFile) => { const parsedMultiScopedFiles = parseMultiScopedFile(multiScopedFile); parsedMultiScopedFiles.forEach((parsedScopedFile) => { translationStoreService.setScopedFile(parsedScopedFile.scopeName, parsedScopedFile.file); }); }), map(() => void 0)); } } /** * Translates a token by the locale reactive * @param token - The token to resolve the translation * @param value - The default value of the translation * @param scopeName - A scope name to resolve the lookup * @param httpOptions - Additional HTTP Options for the request * @returns An `Observable` with the current translation as `string` * * @remarks * If the {@link LocaleConfig}, a new translation based on the new locale will be emitted. * * @since 1.0.0 * @author Simon Kovtyk */ translateTokenByLocale$(token, value, scopeName, httpOptions) { return this._translationStoreService.getLocale$().pipe(switchMap((locale) => this._translateWithRef$(this._httpClient, this._translationStoreService, this._translationHttpService, locale, token, value, this._resolveScope(scopeName), httpOptions))); } /** * Translates a token by the current locale * @param token - The token to resolve the translation * @param value - The default value of the translation * @param scopeName - A scope name to resolve the lookup * @param httpOptions - Additional HTTP Options for the request * @returns An `Observable` with the current translation as `string` * * @remarks * If the {@link LocaleConfig}, no new translation will be emitted. * * @since 1.0.0 * @author Simon Kovtyk */ translateTokenByCurrentLocale$(token, value, scopeName, httpOptions) { const currentLocale = this._translationStoreService.getCurrentLocale(); return this._translateWithRef$(this._httpClient, this._translationStoreService, this._translationHttpService, currentLocale, token, value, scopeName, httpOptions); } /** * Preload translations by locale * @param httpClient - The `HttpClient`, that'll be used * @param translationStoreService - The {@link TranslationStoreService}, that'll be used * @param translationHttpService - The {@link TranslationHttpService}, that'll be used * @param scopeName - The scope name for the lookup of the translation * @param httpOptions - Additional HTTP Options for the request * @returns An `Observable` to handle the status * * @remarks * If the {@link LocaleConfig} changes, the scope will be preloaded again. * * @since 1.0.0 * @author Simon Kovtyk */ /* eslint-disable-next-line @tseslint/max-params */ preloadByLocale(httpClient, translationStoreService, translationHttpService, scopeName, httpOptions) { return translationStoreService.getLocale$().pipe(switchMap((locale) => this._preloadWithRef$(httpClient, translationStoreService, translationHttpService, locale, scopeName, httpOptions))); } /** * Preload translations by locale * @param httpClient - The `HttpClient`, that'll be used * @param translationStoreService - The {@link TranslationStoreService}, that'll be used * @param translationHttpService - The {@link TranslationHttpService}, that'll be used * @param scopeName - The scope name for the lookup of the translation * @param httpOptions - Additional HTTP Options for the request * @returns An `Observable` to handle the status * * @remarks * If the {@link LocaleConfig} changes, no new preloading will be made. * * @since 1.0.0 * @author Simon Kovtyk */ /* eslint-disable-next-line @tseslint/max-params */ preloadByCurrentLocale(httpClient, translationStoreService, translationHttpService, scopeName, httpOptions) { const currentLocale = translationStoreService.getCurrentLocale(); return this._preloadWithRef$(httpClient, translationStoreService, translationHttpService, currentLocale, scopeName, httpOptions); } _resolveScope(scopeName) { return resolveScope(this._translationConfig, scopeName); } /* eslint-disable-next-line @tseslint/class-methods-use-this */ _translateBySourceLocale(token, value) { if (!value) { throw new TranslationNotDefinedError({ token }); } return of(value); } _translateByStore(scope, token) { if (typeof scope === "string" || scope === null) { /* eslint-disable-next-line @tseslint/no-non-null-assertion */ const scopedFile = this._translationStoreService.getScopedFile(scope); const _translatedToken = translateTokenByScopedFile(scopedFile, token); if (_translatedToken === undefined) { throw new TranslationNotDefinedError({ token, scope }); } return of(_translatedToken); } let translatedToken; scope.map((_scope) => this._translationStoreService.getScopedFile(_scope)) .some((scopedFile) => { if (scopedFile === undefined) return false; const _translatedToken = translateTokenByScopedFile(scopedFile, token); if (_translatedToken === undefined) return false; translatedToken = _translatedToken; return true; }); if (translatedToken === undefined) { throw new TranslationNotDefinedError({ token, scope }); } ; return of(translatedToken); } _getScopedFileByNotifier(scope) { const currentNotifier = new Subject(); this._translationStoreService.addNotifierToNotifierScope(scope, currentNotifier); return currentNotifier.asObservable(); } _getScopedFileByAddingNotifier(scope) { const currentNotifier = new Subject(); this._translationStoreService.addNotifierScope({ scope, notifiers: [currentNotifier] }); return currentNotifier; } /* eslint-disable-next-line @tseslint/max-params */ _getScopedFileByHttpWithRef$(httpClient, translationStoreService, translationHttpService, scope, httpOptions) { if (typeof scope === "string" || scope === null) translationStoreService.addNotifierScope({ scope, notifiers: null }); else translationStoreService.addNotifierScopes(scope.map((_scope) => ({ scope: _scope, notifiers: null }))); this._isHttpLoading.next(HttpRequestStatus.PENDING); return translationHttpService.getWithRef$(httpClient, scope, httpOptions).pipe(catchError(() => { this._isHttpLoading.next(HttpRequestStatus.ERROR); this._isHttpLoading.next(null); if (typeof scope === "string" || scope === null) translationStoreService.isScopeNameInNotifierScopes(scope) && translationStoreService.removeNotifierScope(scope); else { scope.forEach((_scope) => { if (!translationStoreService.isScopeNameInNotifierScopes(_scope)) return; translationStoreService.removeNotifierScope(_scope); }); } return of(); }), tap((genericScopedFile) => { this._isHttpLoading.next(HttpRequestStatus.SUCCESS); this._isHttpLoading.next(null); if (typeof scope === "string" || scope === null) { translationStoreService.setScopedFile(scope, genericScopedFile); const notifierScope = translationStoreService.getNotiferScope(scope); if (notifierScope === undefined) return; notifierScope.notifiers?.forEach((notifier) => { notifier.next(genericScopedFile); }); translationStoreService.removeNotifierScope(scope); return; } scope.forEach((_scope) => { const parsedScopedFile = findScopeInMultiScopedFile(genericScopedFile, _scope); const notifierScope = translationStoreService.getNotiferScope(_scope); if (notifierScope === undefined) return; notifierScope.notifiers?.forEach((notifier) => { notifier.next(parsedScopedFile); }); translationStoreService.removeNotifierScope(_scope); }); })); } /* eslint-disable-next-line @tseslint/max-params */ _translateWithRef$(httpClient, translationStoreService, translationHttpService, locale, token, value, scopeName, httpOptions) { let resolvedScopes; const sourceLocale = this._translationStoreService.getSourceLocale(); // Handle direct resolving if (sourceLocale !== undefined && sourceLocale === this._translationStoreService.getCurrentLocale()) return this._translateBySourceLocale(token, value); // Handle store resolving if (typeof scopeName === "string" || scopeName === null) { resolvedScopes = this._resolveScope(scopeName); if (translationStoreService.hasScopedFile(resolvedScopes)) return this._translateByStore(resolvedScopes, token); } else { resolvedScopes = this._resolveScope(scopeName); if (translationStoreService.hasScopedFiles(resolvedScopes)) return this._translateByStore(resolvedScopes, token); } // Handle notifier resolving if (typeof resolvedScopes === "string" || resolvedScopes === null) { if (translationStoreService.isScopeNameInNotifierScopes(resolvedScopes)) { return this._getScopedFileByNotifier(resolvedScopes) .pipe(map((scopedFile) => { const translatedToken = translateTokenByScopedFile(scopedFile, token); if (translatedToken === undefined) { throw new TranslationNotDefinedError({ token, locale, scope: resolvedScopes }); } return translatedToken; })); } } else { const existingScopes = translationStoreService.getExistingNotifierScopes(resolvedScopes); const extinctScopes = resolvedScopes.filter((resolvedScope) => !existingScopes.includes(resolvedScope)); const existingNotifiers = existingScopes.map((existingScope) => this._getScopedFileByNotifier(existingScope)); const extinctScopesHttp = this._getScopedFileByHttpWithRef$(httpClient, translationStoreService, translationHttpService, extinctScopes, httpOptions) .pipe(tap((multiScopedFile) => { const castedMultiScopedFile = multiScopedFile; const parsedMultiScopedFile = parseMultiScopedFile(castedMultiScopedFile); parsedMultiScopedFile.forEach((_parsedMultiScopedFile) => { translationStoreService.setScopedFile(_parsedMultiScopedFile.scopeName, _parsedMultiScopedFile.file); }); }), map((multiScopedFile) => { const castedMultiScopedFile = multiScopedFile; const splittedMultiScopedFiles = splitMultiScopedFile(castedMultiScopedFile); return findTokenInScopedFiles(splittedMultiScopedFiles, token); })); return extinctScopesHttp.pipe(combineLatestWith(...existingNotifiers), map((scopedFiles) => { const translatedToken = translateTokenByScopedFiles(scopedFiles, token); if (translatedToken === undefined) { throw new TranslationNotDefinedError({ token, value, scope: resolvedScopes, locale }); } return translatedToken; })); } // Handle HTTP resolving return of(locale).pipe(switchMap(() => ( /* eslint-disable-next-line @tseslint/no-unnecessary-condition */ typeof resolvedScopes === "string" || resolvedScopes === null ? this._getScopedFileByHttpWithRef$(httpClient, translationStoreService, translationHttpService, resolvedScopes, httpOptions).pipe(map((scopedFile) => { translationStoreService.setScopedFile(resolvedScopes, scopedFile); const translatedToken = translateTokenByScopedFile(scopedFile, token); if (translatedToken === undefined) { throw new TranslationNotDefinedError({ value, token, scope: resolvedScopes, locale }); } return translatedToken; })) // Handle array : this._getScopedFileByHttpWithRef$(httpClient, translationStoreService, translationHttpService, resolvedScopes, httpOptions).pipe(tap((multiScopedFile) => { const parsedMultiScopedFile = parseMultiScopedFile(multiScopedFile); parsedMultiScopedFile.forEach((_parsedMultiScopedFile) => { translationStoreService.setScopedFile(_parsedMultiScopedFile.scopeName, _parsedMultiScopedFile.file); }); }), map((multiScopedFile) => { const splittedMultiScopedFiles = splitMultiScopedFile(multiScopedFile); const foundScopedFile = findTokenInScopedFiles(splittedMultiScopedFiles, token); const translatedToken = translateTokenByScopedFile(foundScopedFile, token); if (translatedToken === undefined) { throw new TranslationNotDefinedError({ locale, value, token, scope: resolvedScopes }); } return translatedToken; }))))); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationService, providedIn: "root" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TranslationService, decorators: [{ type: Injectable, args: [{ providedIn: "root" }] }] }); /** * Core pipe to translate content inside an Angular Template * @category NG pipes * * @since 1.0.0 * @author Simon Kovtyk */ class TranslationPipe { _translationSnapshot$ = new Subject(); _destroyRef = inject(DestroyRef); _lastValue = null; _translationService = inject(TranslationService); _changeDetectorRef = inject(ChangeDetectorRef); _translationScopeToken = inject(TRANSLATION_SCOPE_TOKEN, { optional: true }); _translationConfig = inject(TRANSLATION_CONFIG_TOKEN, { optional: true }); constructor() { this._translationSnapshot$ .pipe(takeUntilDestroyed(this._destroyRef), distinctUntilChanged(isEqual), switchMap(({ token, value, scope, shouldFallback }) => this._translationService .translateTokenByLocale$(token, value, this._resolveScope(scope)) .pipe(catchError((_httpErrorResponse, _) => { const fallbackToSourceLocale = shouldFallback ?? this._translationConfig?.fallbackToSourceLocale; return fallbackToSourceLocale ? of(value) : throwError(() => _httpErrorResponse); }), distinctUntilChanged()))).subscribe((translatedValue) => { this._updateLastValue(translatedValue); }); } /** * Transforms a token into its translated value * * @param value - The possible fallback value, if no translation was found. Check the `fallback` parameter * @param token - The token to be translated * @param scope - Optional scope(s) to narrow down the translation search * @param shouldFallback - Optional flag to determine if it should fallback to the source locale when no translation was found. If not provided, {@link TranslationNotDefinedError} will be thrown instead. * @returns The translated token or the fallback value * * @since 1.0.0 * @author Simon Kovtyk */ transform