@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
JavaScript
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