@ngx-translate/core
Version:
Translation library (i18n) for Angular
1,223 lines (1,212 loc) • 65.9 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, computed, Injectable, inject, DestroyRef, InjectionToken, untracked, TemplateRef, ViewContainerRef, Directive, ElementRef, ChangeDetectorRef, Injector, effect, Input, Pipe } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { of, Subject, isObservable, merge, EMPTY, tap, finalize, forkJoin, concat, defer } from 'rxjs';
import { filter, map, take, shareReplay, concatMap, switchMap } from 'rxjs/operators';
function _(key) {
return key;
}
function omit(map, key) {
const next = {};
for (const k of Object.keys(map)) {
if (k !== key)
next[k] = map[k];
}
return next;
}
/**
* In-flight load registry — one entry per language currently being loaded.
* Backed by a signal so `isLoading` can derive reactively from its contents.
*
* Invariants:
* - Entries added by `loadAndCompileTranslations` before subscribe.
* - Entries removed by `loadAndCompileTranslations`'s `tap` on the success
* path (synchronously between store update and subscriber notification, so
* subscribers observe this language gone from the registry — and
* `isLoading()` flips false if no other load is in flight) and by the
* `shareReplay`'d Observable's `finalize` for error / sync-throw /
* last-subscriber-unsubscribe paths. The success-path finalize is a safe
* no-op (idempotent `clearIfOwner`).
* - Same-language back-to-back loads dedup via the existing `shareReplay`; the
* registry stays at one entry per language regardless of subscriber count.
* `isLoading` therefore toggles `0 -> 1 -> 0` once per language load, not
* once per subscriber. Set-based, not count-based — matches user intent
* ("a language is loading" not "N callers asked").
* - `clearIfOwner(lang, token)` only removes the entry if it still matches the
* original Observable token captured at `set()` time. Prevents stale-finalize
* races where an old load's `finalize` would clobber a newer load's entry
* (e.g. `resetLang` followed by `reloadLang` while the old load is still
* mid-flight).
* - `clear(lang)` is unconditional — used by `resetLang` to forcibly drop
* ownership regardless of which load owns the entry.
*
* Internal to the package — not re-exported from `public-api.ts`.
*/
class LoadingTranslationsRegistry {
state = signal({}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
/** Reactive — `true` while at least one load is in flight. */
hasAny = computed(() => Object.keys(this.state()).length > 0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hasAny" }] : /* istanbul ignore next */ []));
/** `true` while THIS language is being loaded on this instance. */
isLoading(lang) {
return this.state()[lang] !== undefined;
}
get(lang) {
return this.state()[lang];
}
set(lang, obs) {
this.state.update((m) => ({ ...m, [lang]: obs }));
}
/** Unconditional clear. Used by `resetLang` to forcibly drop the entry. */
clear(lang) {
this.state.update((m) => omit(m, lang));
}
/**
* Token-aware clear. Used by `loadAndCompileTranslations`'s `finalize` so
* an old load's `finalize` cannot clobber a newer load's entry.
*/
clearIfOwner(lang, token) {
this.state.update((m) => (m[lang] === token ? omit(m, lang) : m));
}
}
class MissingTranslationHandler {
}
/**
* This handler is just a placeholder that does nothing; in case you don't need a missing translation handler at all
*/
class DefaultMissingTranslationHandler {
handle(params) {
return params.key;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DefaultMissingTranslationHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DefaultMissingTranslationHandler });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DefaultMissingTranslationHandler, decorators: [{
type: Injectable
}] });
class TranslateCompiler {
}
/**
* This compiler is just a placeholder that does nothing; in case you don't need a compiler at all
*/
class TranslateNoOpCompiler extends TranslateCompiler {
compile(value, lang) {
void lang;
return value;
}
compileTranslations(translations, lang) {
void lang;
return translations;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpCompiler, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpCompiler });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpCompiler, decorators: [{
type: Injectable
}] });
class TranslateLoader {
}
/**
* This loader is just a placeholder that does nothing; in case you don't need a loader at all
*/
class TranslateNoOpLoader extends TranslateLoader {
getTranslation(lang) {
void lang;
return of({});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpLoader, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpLoader });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateNoOpLoader, decorators: [{
type: Injectable
}] });
/**
* Determines if two objects or two values are equivalent.
*
* Two objects or values are considered equivalent if at least one of the following is true:
*
* * Both objects or values pass `===` comparison.
* * Both objects or values are of the same type and all of their properties are equal by
* comparing them with `equals`.
*
* @param o1 Object or value to compare.
* @param o2 Object or value to compare.
* @returns true if arguments are equal.
*/
function equals(o1, o2) {
if (o1 === o2)
return true;
if (o1 === null || o2 === null)
return false;
if (o1 !== o1 && o2 !== o2)
return true; // NaN === NaN
const t1 = typeof o1, t2 = typeof o2;
let length;
if (t1 == t2 && t1 == "object") {
if (Array.isArray(o1)) {
if (!Array.isArray(o2))
return false;
if ((length = o1.length) == o2.length) {
for (let key = 0; key < length; key++) {
if (!equals(o1[key], o2[key]))
return false;
}
return true;
}
}
else {
if (Array.isArray(o2)) {
return false;
}
if (isDict(o1) && isDict(o2)) {
const keySet = Object.create(null);
for (const key in o1) {
if (!equals(o1[key], o2[key])) {
return false;
}
keySet[key] = true;
}
for (const key in o2) {
if (!(key in keySet) && typeof o2[key] !== "undefined") {
return false;
}
}
return true;
}
}
}
return false;
}
function isDefinedAndNotNull(value) {
return typeof value !== "undefined" && value !== null;
}
function isDefined(value) {
return value !== undefined;
}
function isDict(value) {
return isObject(value) && !isArray(value) && value !== null;
}
function isObject(value) {
return typeof value === "object" && value !== null;
}
function isArray(value) {
return Array.isArray(value);
}
function isString(value) {
return typeof value === "string";
}
function isFunction(value) {
return typeof value === "function";
}
function cloneDeep(value) {
if (isArray(value)) {
return value.map((item) => cloneDeep(item));
}
else if (isDict(value)) {
const cloned = {};
Object.keys(value).forEach((key) => {
cloned[key] = cloneDeep(value[key]);
});
return cloned;
}
else {
return value;
}
}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
function mergeDeep(target, source) {
if (!isObject(target)) {
return cloneDeep(source);
}
const output = cloneDeep(target);
if (isObject(output) && isObject(source)) {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
Object.keys(source).forEach((key) => {
if (isDict(source[key])) {
if (key in target) {
output[key] = mergeDeep(target[key], source[key]);
}
else {
Object.assign(output, { [key]: source[key] });
}
}
else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
/**
* Retrieves a value from a nested object using a dot-separated key path.
*
* Example usage:
* ```ts
* getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA'); // returns 'valueI'
* ```
*
* @param target The source object from which to retrieve the value.
* @param key Dot-separated key path specifying the value to retrieve.
* @returns The value at the specified key path, or `undefined` if not found.
*/
function getValue(target, key) {
const keys = key.split(".");
key = "";
do {
key += keys.shift();
const isLastKey = !keys.length;
if (isDefinedAndNotNull(target)) {
if (isDict(target) &&
isDefined(target[key]) &&
(isDict(target[key]) || isArray(target[key]) || isLastKey)) {
target = target[key];
key = "";
continue;
}
if (isArray(target)) {
if (key === "length" && isLastKey) {
target = target.length;
key = "";
continue;
}
if (/^\d+$/.test(key)) {
const index = parseInt(key, 10);
if (isDefined(target[index]) &&
(isDict(target[index]) || isArray(target[index]) || isLastKey)) {
target = target[index];
key = "";
continue;
}
}
}
}
if (isLastKey) {
target = undefined;
continue;
}
key += ".";
} while (keys.length);
return target;
}
/**
* Sets a value on object using a dot separated key.
* Returns a clone of the object without modifying it
* insertValue({a:{b:{c: "test"}}}, 'a.b.c', "test2") ==> {a:{b:{c: "test2"}}}
* @param target an object
* @param key E.g. "a.b.c"
* @param value to set
*/
function insertValue(target, key, value) {
return mergeDeep(target, createNestedObject(key, value));
}
function createNestedObject(dotSeparatedKey, value) {
return dotSeparatedKey.split(".").reduceRight((acc, key) => ({ [key]: acc }), value);
}
class TranslateParser {
}
class TranslateDefaultParser extends TranslateParser {
templateMatcher = /{{\s?([^{}\s]*)\s?}}/g;
interpolate(expr, params) {
if (isString(expr)) {
return this.interpolateString(expr, params);
}
else if (isFunction(expr)) {
return this.interpolateFunction(expr, params);
}
return undefined;
}
interpolateFunction(fn, params) {
return fn(params);
}
interpolateString(expr, params) {
if (!params) {
return expr;
}
return expr.replace(this.templateMatcher, (substring, key) => {
const replacement = this.getInterpolationReplacement(params, key);
return replacement !== undefined ? replacement : substring;
});
}
/**
* Returns the replacement for an interpolation parameter
* @params:
*/
getInterpolationReplacement(params, key) {
return this.formatValue(getValue(params, key));
}
/**
* Converts a value into a useful string representation.
* @param value The value to format.
* @returns A string representation of the value.
*/
formatValue(value) {
if (isString(value)) {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return value.toString();
}
if (value === null) {
return "null";
}
if (isArray(value)) {
return value.join(", ");
}
if (isObject(value)) {
if (typeof value.toString === "function" &&
value.toString !== Object.prototype.toString) {
return value.toString();
}
return JSON.stringify(value); // Pretty-print JSON if no meaningful toString()
}
return undefined;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateDefaultParser, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateDefaultParser });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateDefaultParser, decorators: [{
type: Injectable
}] });
class TranslateStore {
_translations = signal({}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_translations" }] : /* istanbul ignore next */ []));
translations = this._translations.asReadonly();
_languages = signal([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_languages" }] : /* istanbul ignore next */ []));
languages = this._languages.asReadonly();
_lastTranslationChange = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_lastTranslationChange" }] : /* istanbul ignore next */ []));
lastTranslationChange = this._lastTranslationChange.asReadonly();
_translationChange$ = new Subject();
translationChange$ = this._translationChange$.asObservable();
constructor() {
// Complete the Subject when the owning injector tears down. Without
// this, child-service stores on lazy routes leak `translationChange$`
// subscribers across navigations.
inject(DestroyRef).onDestroy(() => {
this._translationChange$.complete();
});
}
getTranslations(language) {
return this.translations()[language];
}
setTranslations(language, translations, extend) {
this._translations.update((current) => ({
...current,
[language]: extend && this.hasTranslationFor(language)
? mergeDeep(current[language], translations)
: translations,
}));
this.addLanguages([language]);
const event = {
lang: language,
translations: this.getTranslations(language),
};
this._lastTranslationChange.set(event);
this._translationChange$.next(event);
}
getLanguages() {
return this.languages();
}
addLanguages(langs) {
this._languages.update((current) => Array.from(new Set([...current, ...langs])));
}
hasTranslationFor(lang) {
return typeof this.translations()[lang] !== "undefined";
}
deleteTranslations(lang) {
this._translations.update((current) => {
const { [lang]: _, ...rest } = current;
return rest;
});
}
getTranslationValue(language, key) {
return getValue(this.getTranslations(language), key);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateStore });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateStore, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
const TRANSLATE_SERVICE_CONFIG = new InjectionToken("TRANSLATE_CONFIG");
const makeObservable = (value) => {
return isObservable(value) ? value : of(value);
};
class TranslateService {
loadingTranslations = new LoadingTranslationsRegistry();
lastUseLanguage = null;
currentLoader = inject(TranslateLoader);
compiler = inject(TranslateCompiler);
parser = inject(TranslateParser);
missingTranslationHandler = inject(MissingTranslationHandler);
store = inject(TranslateStore);
destroyRef = inject(DestroyRef);
parent;
get isRoot() {
return this.parent === null;
}
_onLangChange = new Subject();
_onFallbackLangChange = new Subject();
_currentLang = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_currentLang" }] : /* istanbul ignore next */ []));
_fallbackLang = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_fallbackLang" }] : /* istanbul ignore next */ []));
_onTranslationRefresh = null;
// Downward-inheritance: `true` if THIS service has loads in flight, OR any
// ancestor does. Walks the parent chain via the public `parent.isLoading()`
// getter (not by reaching into `parent.loadingTranslations`) so the
// encapsulation boundary holds. Angular signals re-collect dependencies on
// each evaluation; short-circuit is safe — the only signal whose flip
// could change the result is the one returned by short-circuit, and it IS
// tracked. Parent chain is acyclic (DI tree is acyclic; `skipSelf` only
// walks up), so no cycle guard needed.
//
// Set-based composition at each level: `loadingTranslations.hasAny()`
// stays true while ANY language has an in-flight load on this service,
// flips false only when the last entry is cleared.
_isLoading = computed(() => this.loadingTranslations.hasAny() || (this.parent?.isLoading() ?? false), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
/**
* Returns the root of this service's hierarchy — the topmost service in
* the `getParent()` chain. For an isolated subtree, returns the subtree's
* root (since `parent === null` at the isolation boundary).
*
* A root service returns itself. Equivalent to walking `getParent()` until
* it returns `null`, but provided as a convenience.
*/
getRoot() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let svc = this;
while (svc.parent)
svc = svc.parent;
return svc;
}
/**
* Returns the service this one inherits translations from, or `null` if
* this is a root (a top-level service or an isolated subtree root).
*
* A `null` return means the service is the terminus of its translation
* fallback chain — equivalent to "is this a root?".
*/
getParent() {
return this.parent;
}
/**
* The language most recently requested via `use()`. Always read from the
* root, because `use()` delegates to the root (`parent!.use(lang)`) and only
* the root ever assigns `lastUseLanguage`. A child's own `lastUseLanguage`
* stays `null`, so reading it directly would make `get()` miss the child's
* in-flight load. `null` until the first `use()` runs.
*/
getActiveRequestedLang() {
return this.getRoot().lastUseLanguage;
}
hasTranslationInChain(lang) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let svc = this; svc; svc = svc.parent) {
if (svc.store.hasTranslationFor(lang))
return true;
}
return false;
}
chainTranslationChange$() {
const streams = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let svc = this; svc; svc = svc.parent) {
streams.push(svc.store.translationChange$);
}
return streams.length === 1 ? streams[0] : merge(...streams);
}
/**
* An Observable to listen to translation change events
* onTranslationChange.subscribe((params: TranslationChangeEvent) => {
* // do something
* });
*/
get onTranslationChange() {
return this.store.translationChange$;
}
/**
* An Observable to listen to lang change events
* onLangChange.subscribe((params: LangChangeEvent) => {
* // do something
* });
*/
get onLangChange() {
if (this.isRoot) {
return this._onLangChange.asObservable();
}
return this.parent ? this.parent.onLangChange : EMPTY;
}
/**
* An Observable to listen to fallback lang change events
* onFallbackLangChange.subscribe((params: FallbackLangChangeEvent) => {
* // do something
* });
*/
get onFallbackLangChange() {
if (this.isRoot) {
return this._onFallbackLangChange.asObservable();
}
return this.parent ? this.parent.onFallbackLangChange : EMPTY;
}
/**
* A combined Observable that emits whenever translations might need to be refreshed.
* This includes: language changes, translation updates for the current or fallback language,
* and fallback language changes.
*/
get onTranslationRefresh() {
if (!this._onTranslationRefresh) {
const refresh$ = merge(this.onTranslationChange.pipe(filter((event) => event.lang === this.getCurrentLang() ||
event.lang === this.getFallbackLang())), this.onLangChange, this.onFallbackLangChange).pipe(map(() => void 0));
if (this.isRoot) {
this._onTranslationRefresh = refresh$;
}
else {
this._onTranslationRefresh = this.parent
? merge(refresh$, this.parent.onTranslationRefresh)
: refresh$;
}
}
return this._onTranslationRefresh;
}
constructor() {
const config = {
isRoot: true,
fallbackLang: null,
...inject(TRANSLATE_SERVICE_CONFIG, {
optional: true,
}),
};
// parent === null exactly means "I am a root" (including isolated-subtree roots).
// isRoot is now a getter derived from this single source of truth.
this.parent = config.isRoot
? null
: inject(TranslateService, { optional: true, skipSelf: true });
const destroyRef = inject(DestroyRef);
if (this.isRoot) {
if (config.lang) {
this.use(config.lang);
}
if (config.fallbackLang) {
this.setFallbackLang(config.fallbackLang);
}
}
else {
// Child services should initially load the root's current and fallback languages.
// Loader failures are warned contextually here — the internal no-op subscribe
// inside loadAndCompileTranslations would otherwise swallow them silently.
// Best-effort: takeUntilDestroyed tears down the subscription on host destroy,
// so a destroy-then-error race silently drops the warn.
const currentLang = this.getCurrentLang();
if (currentLang) {
this.loadOrExtendLanguage(currentLang)
?.pipe(takeUntilDestroyed(destroyRef))
.subscribe({
error: (err) => {
console.warn(`@ngx-translate/core: child failed to load "${currentLang}". Cause:`, err);
},
});
}
const fallbackLang = this.getFallbackLang();
// Dedup guard: currentLang === fallbackLang means both branches would
// resolve to the same in-flight observable (via the loading-translations
// registry's get-or-create) and double-log on error.
if (fallbackLang && fallbackLang !== currentLang) {
this.loadOrExtendLanguage(fallbackLang)
?.pipe(takeUntilDestroyed(destroyRef))
.subscribe({
error: (err) => {
console.warn(`@ngx-translate/core: child failed to load "${fallbackLang}". Cause:`, err);
},
});
}
}
// Child services should load translations when the language changes on the root
this.onLangChange.pipe(takeUntilDestroyed(destroyRef)).subscribe((event) => {
if (!this.isRoot) {
this.loadOrExtendLanguage(event.lang)
?.pipe(takeUntilDestroyed(destroyRef))
.subscribe({
error: (err) => {
console.warn(`@ngx-translate/core: child failed to load "${event.lang}". Cause:`, err);
},
});
}
});
this.onFallbackLangChange.pipe(takeUntilDestroyed(destroyRef)).subscribe((event) => {
if (!this.isRoot) {
this.loadOrExtendLanguage(event.lang)
?.pipe(takeUntilDestroyed(destroyRef))
.subscribe({
error: (err) => {
console.warn(`@ngx-translate/core: child failed to load "${event.lang}". Cause:`, err);
},
});
}
});
// Complete this service's Subjects when its injector tears down.
// Root singletons live as long as the app, but child services on
// lazy routes would otherwise pin their Subjects until GC.
destroyRef.onDestroy(() => {
this._onLangChange.complete();
this._onFallbackLangChange.complete();
});
}
/**
* Sets the fallback language to use if a translation is not found in the
* current language
*/
setFallbackLang(lang) {
if (!this.isRoot) {
return this.parent.setFallbackLang(lang);
}
if (!this._fallbackLang()) {
// on init set the fallbackLang immediately, but do not emit a change yet
this._fallbackLang.set(lang);
}
const pending = this.loadOrExtendLanguage(lang);
if (isObservable(pending)) {
pending.pipe(take(1)).subscribe({
next: () => {
this._fallbackLang.set(lang);
this._onFallbackLangChange.next({
lang: lang,
translations: this.store.getTranslations(lang),
});
},
error: (err) => {
console.warn(`@ngx-translate/core: failed to load fallback "${lang}". Cause:`, err);
},
});
return pending;
}
this._fallbackLang.set(lang);
this._onFallbackLangChange.next({
lang: lang,
translations: this.store.getTranslations(lang),
});
return of(this.store.getTranslations(lang));
}
/**
* Signal that is `true` while one or more language loads are in flight at
* this service or any of its ancestors in the service hierarchy.
*
* Loading scope propagates DOWNWARD: a load triggered at the root marks
* the root and all descendants as loading. A load triggered at a child
* (e.g. a lazy-route bootstrap fetching its translations) marks only that
* child's subtree. Siblings and ancestors are unaffected by a descendant's
* loads.
*
* Drive a spinner by reading it from the service injected at the scope
* where the spinner should live: root for an app-shell spinner, the
* nearest child for a local spinner inside a lazy-loaded subtree.
*/
get isLoading() {
return this._isLoading;
}
/**
* Changes the lang currently used
*/
use(lang) {
if (!this.isRoot) {
return this.parent.use(lang);
}
// Snapshot prior state so we can roll back if the loader fails.
const prevLang = this._currentLang();
const prevLastUseLang = this.lastUseLanguage;
// Remember the language that was called — used by changeLang() to discard
// late-arriving completions from superseded calls.
this.lastUseLanguage = lang;
if (!this._currentLang()) {
// on init set the currentLang immediately, but do not emit a change yet
this._currentLang.set(lang);
}
const pending = this.loadOrExtendLanguage(lang);
if (!isObservable(pending)) {
// Defensive: loadOrExtendLanguage is typed `Observable | undefined`.
// The undefined branch means "nothing to load" — synchronously activate.
this.changeLang(lang);
return of(this.store.getTranslations(lang));
}
pending.pipe(take(1)).subscribe({
next: () => {
this.changeLang(lang);
},
error: (err) => {
// Only roll back if THIS call is still the most-recent one.
// A later use() may have superseded it (symmetric with the
// changeLang() guard).
if (this.lastUseLanguage === lang) {
this._currentLang.set(prevLang);
this.lastUseLanguage = prevLastUseLang;
}
console.warn(`@ngx-translate/core: failed to load "${lang}". ` +
`currentLang was NOT changed; remains ` +
`"${prevLang ?? "null"}". Cause:`, err);
},
});
return pending;
}
/**
* Retrieves the given translations
*/
loadOrExtendLanguage(lang) {
// if this language is unavailable, ask for it
if (!this.store.hasTranslationFor(lang)) {
return this.loadAndCompileTranslations(lang);
}
return of(this.store.getTranslations(lang));
}
/**
* @returns The loaded translations for the given language
*/
getTranslations(language) {
return this.store.getTranslations(language);
}
/**
* Changes the current lang
*/
changeLang(lang) {
if (lang !== this.lastUseLanguage) {
// received new language data,
// but this was not the one requested last
return;
}
this._currentLang.set(lang);
this._onLangChange.next({ lang: lang, translations: this.store.getTranslations(lang) });
}
getCurrentLang() {
return this.isRoot ? this._currentLang() : (this.parent?.getCurrentLang() ?? null);
}
/**
* Loads translations for `lang` via the configured `TranslateLoader`,
* compiles them, and stores the result. Tracking via the protected
* `loadingTranslations` registry happens automatically.
*
* Subclasses that override this method bypass `isLoading` tracking
* unless they call `this.loadingTranslations.set(lang, obs)` and arrange
* a token-aware finalize (`this.loadingTranslations.clearIfOwner(lang, obs)`)
* from the override.
*/
loadAndCompileTranslations(lang) {
const existing = this.loadingTranslations.get(lang);
if (existing) {
return existing;
}
const translations$ = this.currentLoader.getTranslation(lang).pipe(map((res) => this.compiler.compileTranslations(res, lang)), tap((compiled) => {
this.store.setTranslations(lang, compiled, false);
// Clear synchronously on success — this `tap` runs before
// `shareReplay` forwards `next` to subscribers, so subscribers
// receiving `next` observe `isLoading()` reflecting that this
// language is no longer in flight. The finalize below covers
// error / sync-throw / unsubscribe paths; clearIfOwner is
// idempotent so the double-clear on success is a safe no-op.
// Invariant: `tap` MUST stay before `shareReplay` in the pipe.
this.loadingTranslations.clearIfOwner(lang, translations$);
}),
// Token-aware clear: if `resetLang` + `reloadLang` raced between
// set() and finalize(), this load's finalize must NOT clobber the
// newer load's entry. clearIfOwner compares by reference identity.
finalize(() => this.loadingTranslations.clearIfOwner(lang, translations$)),
// cache the single result & share it across all subscribers
shareReplay({ bufferSize: 1, refCount: true }));
this.loadingTranslations.set(lang, translations$);
// Trigger loading if nobody subscribes from outside. The error callback
// is intentionally a no-op: use() and setFallbackLang() already emit a
// console.warn on loader failure for their own paths. Warning here
// would double-log for those callers. Bound to the service lifetime so a
// non-completing/hot custom loader cannot pin this subscription (and the
// upstream loader subscription) past service teardown.
translations$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
// eslint-disable-next-line @typescript-eslint/no-empty-function
error: () => { },
});
return translations$;
}
/**
* Manually sets an object of translations for a given language,
* passing it through the configured {@link TranslateCompiler} first.
*
* If you already have translations in their final compiled form
* (e.g. interpolation functions produced at build time), use
* {@link setCompiledTranslation} instead — it stores the data
* directly and skips the compiler.
*/
setTranslation(lang, translations, shouldMerge = false) {
const interpolatableTranslations = this.compiler.compileTranslations(translations, lang);
this.store.setTranslations(lang, interpolatableTranslations, shouldMerge);
}
/**
* Stores an already-compiled translation object for the given language,
* bypassing the configured {@link TranslateCompiler}.
*
* Use this when you have translations in their final, interpolator-ready
* form — e.g. interpolation functions produced at build time. For raw
* translations that still need to go through the compiler, use
* {@link setTranslation} instead.
*/
setCompiledTranslation(lang, translations, shouldMerge = false) {
this.store.setTranslations(lang, translations, shouldMerge);
}
getLangs() {
return this.store.getLanguages();
}
/**
* Add available languages
*/
addLangs(languages) {
this.store.addLanguages(languages);
}
getParsedResultForKey(key, interpolateParams, lang) {
const textToInterpolate = this.getTextToInterpolate(key, lang);
if (isDefinedAndNotNull(textToInterpolate)) {
return this.runInterpolation(textToInterpolate, interpolateParams);
}
const handler = this.getMissingTranslationHandler();
const res = handler.handle({
key,
translateService: this,
...(interpolateParams !== undefined && { interpolateParams }),
});
return res !== undefined ? res : key;
}
getMissingTranslationHandler() {
return this.missingTranslationHandler;
}
/**
* Gets the fallback language. null if none is defined
*/
getFallbackLang() {
return this.isRoot ? this._fallbackLang() : (this.parent?.getFallbackLang() ?? null);
}
getTextToInterpolate(key, lang) {
if (lang) {
const res = this.store.getTranslationValue(lang, key);
if (res !== undefined) {
return res;
}
return this.parent?.getTextToInterpolate(key, lang);
}
const currentLang = this.getCurrentLang();
const fallbackLang = this.getFallbackLang();
// 1. Try own store (currentLang)
let res;
if (currentLang) {
res = this.store.getTranslationValue(currentLang, key);
}
// 2. Try own store (fallbackLang) - null values also trigger fallback
if (!isDefinedAndNotNull(res) && fallbackLang && fallbackLang !== currentLang) {
res = this.store.getTranslationValue(fallbackLang, key);
}
if (res !== undefined) {
return res;
}
// 3. Try parent
return this.parent?.getTextToInterpolate(key);
}
runInterpolation(translations, interpolateParams) {
if (!isDefinedAndNotNull(translations)) {
return;
}
if (isArray(translations)) {
return this.runInterpolationOnArray(translations, interpolateParams);
}
if (isDict(translations)) {
return this.runInterpolationOnDict(translations, interpolateParams);
}
return this.parser.interpolate(translations, interpolateParams);
}
runInterpolationOnArray(translations, interpolateParams) {
return translations.map((translation) => this.runInterpolation(translation, interpolateParams));
}
runInterpolationOnDict(translations, interpolateParams) {
const result = {};
for (const key in translations) {
const res = this.runInterpolation(translations[key], interpolateParams);
if (res !== undefined) {
result[key] = res;
}
}
return result;
}
/**
* Returns the parsed result of the translations
*/
getParsedResult(key, interpolateParams, lang) {
return key instanceof Array
? this.getParsedResultForArray(key, interpolateParams, lang)
: this.getParsedResultForKey(key, interpolateParams, lang);
}
getParsedResultForArray(key, interpolateParams, lang) {
const result = {};
let observables = false;
for (const k of key) {
result[k] = this.getParsedResultForKey(k, interpolateParams, lang);
observables = observables || isObservable(result[k]);
}
if (!observables) {
return result;
}
const sources = key.map((k) => makeObservable(result[k]));
return forkJoin(sources).pipe(map((arr) => {
const obj = {};
arr.forEach((value, index) => {
obj[key[index]] = value;
});
return obj;
}));
}
/**
* Gets the translated value of a key (or an array of keys)
* @returns the translated key, or an object of translated keys
*/
get(key, interpolateParams, lang) {
if (!isDefinedAndNotNull(key) || !key.length) {
return of("");
}
// check if we are loading a new translation to use.
// Use the ROOT's last-requested language (not this service's own
// lastUseLanguage, which a child never sets) so a child's in-flight
// load is found in its registry; fall back to the current language when
// no use() has run yet.
const effectiveLang = lang ?? this.getActiveRequestedLang() ?? this.getCurrentLang();
const pending = effectiveLang ? this.loadingTranslations.get(effectiveLang) : undefined;
if (pending) {
return pending.pipe(concatMap(() => {
return makeObservable(this.getParsedResult(key, interpolateParams, lang));
}));
}
return makeObservable(this.getParsedResult(key, interpolateParams, lang));
}
/**
* Returns a stream of translated values of a key (or an array of keys) which updates
* whenever the translation changes.
* @returns A stream of the translated key, or an object of translated keys
*/
getStreamOnTranslationChange(key, interpolateParams, lang) {
if (!isDefinedAndNotNull(key) || !key.length) {
throw new Error(`Parameter "key" is required and cannot be empty`);
}
return concat(defer(() => this.get(key, interpolateParams, lang)), this.onTranslationChange.pipe(switchMap(() => {
const res = this.getParsedResult(key, interpolateParams, lang);
return makeObservable(res);
})));
}
/**
* Returns a stream of translated values of a key (or an array of keys) which updates
* whenever the language changes, the requested language's translations are
* (re)loaded, or the explicitly-requested `lang` argument's translations change.
*
* Without `lang`: re-emits on `onLangChange` (active-language switches via
* {@link use}). With `lang`: also re-emits when translations for that specific
* language are loaded or updated via `store.translationChange$`, so an
* explicit `stream("KEY", undefined, "de")` updates once "de" finishes
* loading.
*
* @returns A stream of the translated key, or an object of translated keys
*/
stream(key, interpolateParams, lang) {
if (!isDefinedAndNotNull(key) || !key.length) {
throw new Error(`Parameter "key" required`);
}
const reemit$ = lang
? merge(this.onLangChange, this.chainTranslationChange$().pipe(filter((e) => e.lang === lang)))
: this.onLangChange;
return concat(defer(() => this.get(key, interpolateParams, lang)), reemit$.pipe(switchMap(() => {
const res = this.getParsedResult(key, interpolateParams, lang);
return makeObservable(res);
})));
}
/**
* Returns a translation instantly from the internal state of loaded translation.
* All rules regarding the current language, the preferred language of even fallback languages
* will be used except any promise handling.
*
* When `lang` is provided, the lookup goes directly to the specified language,
* bypassing the current language and fallback chain.
*/
instant(key, interpolateParams, lang) {
if (!isDefinedAndNotNull(key) || key.length === 0) {
return "";
}
if (lang && !this.hasTranslationInChain(lang)) {
this.warnUnloadedInstantLang(lang);
}
const result = this.getParsedResult(key, interpolateParams, lang);
return isObservable(result) ? this.keyToObject(key) : result;
}
warnedUnloadedInstantLangs = new Set();
warnUnloadedInstantLang(lang) {
// Delegate to the root so the warn budget is shared across the isolated
// subtree. With parent === null at isolated boundaries, getRoot() stops
// at the right place — one warn per (isolated subtree, lang) pair.
const root = this.getRoot();
if (root !== this) {
root.warnUnloadedInstantLang(lang);
return;
}
if (this.warnedUnloadedInstantLangs.has(lang))
return;
// instant() runs inside the translate() computed; keep the Set write and
// console.warn out of the reactive graph so they never register as a
// producer during signal evaluation.
untracked(() => {
this.warnedUnloadedInstantLangs.add(lang);
console.warn(`@ngx-translate/core: instant() called with lang="${lang}" but no ` +
`translations are loaded for that language. Returning the key as ` +
`fallback. Load with use("${lang}") or setTranslation("${lang}", ...) first.`);
});
}
/**
* Returns a Signal that provides the translated value and automatically
* updates when the language changes or translations are reloaded.
*
* Parameters accept plain values or arrow functions. Signal reads inside
* the function are tracked reactively. Signals themselves are also
* accepted directly, since Signal<T> is callable.
*
* @param key The translation key (or array of keys), a function returning one
* @param params Optional interpolation parameters, or a function returning them
* @param lang Optional language override, or a function returning one
* @returns A Signal that emits the translated value(s)
*
* @example
* // Static key
* greeting = this.translate.translate('HELLO');
*
* @example
* // Derived key from another signal (no separate computed needed)
* model = signal({ currentKey: 'HELLO' });
* greeting = this.translate.translate(() => this.model().currentKey);
*
* @example
* // Multi-key lookup
* labels = this.translate.translate(['SAVE', 'CANCEL']);
*/
translate(key, params, lang) {
return computed(() => {
const currentKey = typeof key === "function" ? key() : key;
const currentParams = typeof params === "function" ? params() : params;
const currentLang = typeof lang === "function" ? lang() : lang;
return this.instant(currentKey, currentParams, currentLang);
});
}
keyToObject(key) {
if (Array.isArray(key)) {
return key.reduce((acc, currKey) => {
acc[currKey] = currKey;
return acc;
}, {});
}
return key;
}
/**
* Sets the translated value of a key, after compiling it
*/
set(key, translation, lang = this.getCurrentLang()) {
this.store.setTranslations(lang, insertValue(this.store.getTranslations(lang), key, isString(translation)
? this.compiler.compile(translation, lang)
: this.compiler.compileTranslations(translation, lang)), false);
}
/**
* Allows reloading the lang file from the file
*/
reloadLang(lang) {
this.resetLang(lang);
return this.loadAndCompileTranslations(lang);
}
/**
* Deletes stored translations for `lang` and clears the in-flight registry
* entry — `isLoading()` flips to `false` immediately on this service.
*
* Does NOT cancel the underlying network call: if the loader is mid-flight
* when this method returns, the request can still complete and `tap()`
* translations back into the store. To replace state and re-fetch
* deterministically, follow with `reloadLang(lang)`.
*/
resetLang(lang) {
// Unconditional clear — `resetLang`'s contract is "forget this entry
// NOW", regardless of which load owns it.
this.loadingTranslations.clear(lang);
this.store.deleteTranslations(lang);
}
/**
* Returns the language code name from the browser, e.g. "de"
*/
static getBrowserLang() {
if (typeof window === "undefined" || !window.navigator) {
return undefined;
}
const browserLang = this.getBrowserCultureLang();
return browserLang ? browserLang.split(/[-_]/)[0] : undefined;
}
/**
* Returns the culture language code name from the browser, e.g. "de-DE"
*/
static getBrowserCultureLang() {
if (typeof window === "undefined" || typeof window.navigator === "undefined") {
return undefined;
}
return window.navigator.languages
? window.navigator.languages[0]
: window.navigator.language ||
window.navigator.browserLanguage ||
window.navigator.userLanguage;
}
getBrowserLang() {
return TranslateService.getBrowserLang();
}
getBrowserCultureLang() {
return TranslateService.getBrowserCultureLang();
}
/**
* The current language as a reactive Signal.
* Use `getCurrentLang()` for a non-reactive snapshot.
*/
get currentLang() {
return this.isRoot ? this._currentLang.asReadonly() : this.parent.currentLang;
}
/**
* The fallback language as a reactive Signal.
* Use `getFallbackLang()` for a non-reactive snapshot.
*/
get fallbackLang() {
return this.isRoot ? this._fallbackLang.asReadonly() : this.parent.fallbackLang;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TranslateService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/**
* Returns a Signal with the translation for the given key, resolved against
* the current language. Must be called in an Angular injection context.
*
* Parameters accept plain values or arrow functions. Signal reads inside
* the function are tracked reactively. Signals themselves are also accepted
* directly, since Signal<T> is callable.
*
* @example
* greeting = translate('HELLO');
*
* @example
* model = signal({ currentKey: 'HELLO' });
* greeting = translate(() => this.model().currentKey);
*/
function translate(key, params, lang) {
return inject(TranslateService).translate(key, params, lang);
}
class TranslateBlockContext {
$implicit;
constructor($implicit) {
this.$implicit = $implicit;
}
}
class TranslateBlockDirective {
templateRef = inject((TemplateRef));
viewContainer = inject(ViewContainerRef);
t