UNPKG

@ngneat/transloco

Version:

The internationalization (i18n) library for Angular

1,639 lines (1,621 loc) 70.8 kB
import flat from 'flat'; import { take, map, catchError, retry, shareReplay, switchMap, tap } from 'rxjs/operators'; import { InjectionToken, Inject, Injectable, Injector, Optional, Component, Input, APP_INITIALIZER, NgModule, ChangeDetectorRef, Directive, ElementRef, TemplateRef, ViewContainerRef, Pipe, ComponentFactoryResolver, defineInjectable, inject } from '@angular/core'; import { of, from, BehaviorSubject, combineLatest, EMPTY, forkJoin, Subject } from 'rxjs'; /** * @fileoverview added by tsickle * Generated from: lib/transloco.loader.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class DefaultLoader { /** * @param {?} translations */ constructor(translations) { this.translations = translations; } /** * @param {?} lang * @return {?} */ getTranslation(lang) { return of(this.translations.get(lang) || {}); } } /** @type {?} */ const TRANSLOCO_LOADER = new InjectionToken('TRANSLOCO_LOADER'); /** * @fileoverview added by tsickle * Generated from: lib/helpers.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} obj * @param {?} path * @return {?} */ function getValue(obj, path) { /* For cases where the key is like: 'general.something.thing' */ if (obj && obj.hasOwnProperty(path)) { return obj[path]; } return path.split('.').reduce((/** * @param {?} p * @param {?} c * @return {?} */ (p, c) => p && p[c]), obj); } /** * @param {?} obj * @param {?} prop * @param {?} val * @return {?} */ function setValue(obj, prop, val) { obj = Object.assign({}, obj); /** @type {?} */ const split = prop.split('.'); /** @type {?} */ const lastIndex = split.length - 1; split.reduce((/** * @param {?} acc * @param {?} part * @param {?} index * @return {?} */ (acc, part, index) => { if (index === lastIndex) { acc[part] = val; } else { acc[part] = Array.isArray(acc[part]) ? acc[part].slice() : Object.assign({}, acc[part]); } return acc && acc[part]; }), obj); return obj; } /** * @param {?} collection * @return {?} */ function size(collection) { if (!collection) { return 0; } if (Array.isArray(collection)) { return collection.length; } if (isObject(collection)) { return Object.keys(collection).length; } return !!collection ? collection.length : 0; } /** * @param {?} collection * @return {?} */ function isEmpty(collection) { return size(collection) === 0; } /** * @param {?} val * @return {?} */ function isFunction(val) { return typeof val === 'function'; } /** * @param {?} val * @return {?} */ function isString(val) { return typeof val === 'string'; } /** * @param {?} val * @return {?} */ function isNumber(val) { return typeof val === 'number'; } /** * @param {?} item * @return {?} */ function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } /** * @param {?} val * @return {?} */ function coerceArray(val) { return Array.isArray(val) ? val : [val]; } /* * @example * * given: path-to-happiness => pathToHappiness * given: path_to_happiness => pathToHappiness * given: path-to_happiness => pathToHappiness * */ /** * @param {?} str * @return {?} */ function toCamelCase(str) { return str .replace(/(?:^\w|[A-Z]|\b\w)/g, (/** * @param {?} word * @param {?} index * @return {?} */ (word, index) => (index == 0 ? word.toLowerCase() : word.toUpperCase()))) .replace(/\s+|_|-|\//g, ''); } /** * @return {?} */ function isBrowser() { return typeof window !== 'undefined'; } /** * @param {?} value * @return {?} */ function isNil(value) { return value === null || value === undefined; } /** * @param {?} value * @return {?} */ function isDefined(value) { return isNil(value) === false; } /** * @param {?} value * @return {?} */ function toNumber(value) { if (isNumber(value)) return value; if (isString(value) && !isNaN(Number(value) - parseFloat(value))) { return Number(value); } return null; } /** * @param {?} item * @return {?} */ function isScopeObject(item) { return item && typeof item.scope === 'string'; } /** * @param {?} item * @return {?} */ function hasInlineLoader(item) { return item && isObject(item.loader); } /** * @param {?} obj * @return {?} */ function unflatten(obj) { return flat.unflatten(obj, { safe: true }); } /** * @param {?} obj * @return {?} */ function flatten(obj) { return flat(obj, { safe: true }); } /** * @fileoverview added by tsickle * Generated from: lib/transloco.config.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_CONFIG = new InjectionToken('TRANSLOCO_CONFIG', { providedIn: 'root', factory: (/** * @return {?} */ () => { return {}; }) }); /** @type {?} */ const defaultConfig = { defaultLang: 'en', reRenderOnLangChange: false, prodMode: false, failedRetries: 2, availableLangs: [], missingHandler: { logMissingKey: true, useFallbackTranslation: false, allowEmpty: false }, flatten: { aot: false }, interpolation: ['{{', '}}'] }; /** * @deprecated * @param {?=} config * @return {?} */ function provideTranslocoConfig(config = defaultConfig) { return { provide: TRANSLOCO_CONFIG, useValue: Object.assign({}, defaultConfig, config) }; } /** * Sets up TranslocoConfig object. * * @param {?=} config The partial config object to load, this is optional, * will be spread after defaultConfig. * @return {?} */ function translocoConfig(config = defaultConfig) { return Object.assign({}, defaultConfig, config); } /** * @fileoverview added by tsickle * Generated from: lib/transloco.transpiler.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_TRANSPILER = new InjectionToken('TRANSLOCO_TRANSPILER'); class DefaultTranspiler { /** * @param {?=} userConfig */ constructor(userConfig) { this.interpolationMatcher = resolveMatcher(userConfig); } /** * @param {?} value * @param {?=} params * @param {?=} translation * @return {?} */ transpile(value, params = {}, translation) { if (isString(value)) { return value.replace(this.interpolationMatcher, (/** * @param {?} _ * @param {?} match * @return {?} */ (_, match) => { match = match.trim(); if (isDefined(params[match])) { return params[match]; } return isDefined(translation[match]) ? this.transpile(translation[match], params, translation) : ''; })); } else if (params) { if (isObject(value)) { value = this.handleObject(value, params, translation); } else if (Array.isArray(value)) { value = this.handleArray(value, params, translation); } } return value; } /** * * \@example * * const en = { * a: { * b: { * c: "Hello {{ value }}" * } * } * } * * const params = { * "b.c": { value: "Transloco "} * } * * service.selectTranslate('a', params); * * // the first param will be the result of `en.a`. * // the second param will be `params`. * parser.transpile(value, params, {}); * * * @protected * @param {?} value * @param {?=} params * @param {?=} translation * @return {?} */ handleObject(value, params = {}, translation) { /** @type {?} */ let result = value; Object.keys(params).forEach((/** * @param {?} p * @return {?} */ p => { // get the value of "b.c" inside "a" => "Hello {{ value }}" /** @type {?} */ const v = getValue(result, p); // get the params of "b.c" => { value: "Transloco" } /** @type {?} */ const getParams = getValue(params, p); // transpile the value => "Hello Transloco" /** @type {?} */ const transpiled = this.transpile(v, getParams, translation); // set "b.c" to `transpiled` result = setValue(result, p, transpiled); })); return result; } /** * @protected * @param {?} value * @param {?=} params * @param {?=} translation * @return {?} */ handleArray(value, params = {}, translation) { return value.map((/** * @param {?} v * @return {?} */ v => this.transpile(v, params, translation))); } } /** @nocollapse */ DefaultTranspiler.ctorParameters = () => [ { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [TRANSLOCO_CONFIG,] }] } ]; /** * @param {?=} userConfig * @return {?} */ function resolveMatcher(userConfig) { const [start, end] = userConfig && userConfig.interpolation ? userConfig.interpolation : defaultConfig.interpolation; return new RegExp(`${start}(.*?)${end}`, 'g'); } /** * @param {?} argsString * @return {?} */ function getFunctionArgs(argsString) { /** @type {?} */ const splitted = argsString ? argsString.split(',') : []; /** @type {?} */ const args = []; for (let i = 0; i < splitted.length; i++) { /** @type {?} */ let value = splitted[i].trim(); while (value[value.length - 1] === '\\') { i++; value = value.replace('\\', ',') + splitted[i]; } args.push(value); } return args; } class FunctionalTranspiler extends DefaultTranspiler { /** * @param {?} injector */ constructor(injector) { super(); this.injector = injector; } /** * @param {?} value * @param {?=} params * @param {?=} translation * @return {?} */ transpile(value, params = {}, translation) { /** @type {?} */ let transpiled = value; if (isString(value)) { transpiled = value.replace(/\[\[\s*(\w+)\((.*)\)\s*]]/g, (/** * @param {?} match * @param {?} functionName * @param {?} args * @return {?} */ (match, functionName, args) => { try { /** @type {?} */ const func = this.injector.get(functionName); return func.transpile(...getFunctionArgs(args)); } catch (e) { /** @type {?} */ let message = `There is an error in: '${value}'. Check that the you used the right syntax in your translation and that the implementation of ${functionName} is correct.`; if (e.message.includes('NullInjectorError')) { message = `You are using the '${functionName}' function in your translation but no provider was found!`; } throw new Error(message); } })); } return super.transpile(transpiled, params, translation); } } FunctionalTranspiler.decorators = [ { type: Injectable } ]; /** @nocollapse */ FunctionalTranspiler.ctorParameters = () => [ { type: Injector } ]; /** * @fileoverview added by tsickle * Generated from: lib/transloco-missing-handler.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_MISSING_HANDLER = new InjectionToken('TRANSLOCO_MISSING_HANDLER'); class DefaultHandler { /** * @param {?} key * @param {?} config * @return {?} */ handle(key, config) { if (config.missingHandler.logMissingKey && !config.prodMode) { /** @type {?} */ const msg = `Missing translation for '${key}'`; console.warn(`%c ${msg}`, 'font-size: 12px; color: red'); } return key; } } /** * @fileoverview added by tsickle * Generated from: lib/transloco.interceptor.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_INTERCEPTOR = new InjectionToken('TRANSLOCO_INTERCEPTOR'); class DefaultInterceptor { /** * @param {?} translation * @param {?} lang * @return {?} */ preSaveTranslation(translation, lang) { return translation; } /** * @param {?} key * @param {?} value * @param {?} lang * @return {?} */ preSaveTranslationKey(key, value, lang) { return value; } } /** * @fileoverview added by tsickle * Generated from: lib/transloco-fallback-strategy.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_FALLBACK_STRATEGY = new InjectionToken('TRANSLOCO_FALLBACK_STRATEGY'); class DefaultFallbackStrategy { /** * @param {?} userConfig */ constructor(userConfig) { this.userConfig = userConfig; } /** * @param {?} failedLang * @return {?} */ getNextLangs(failedLang) { /** @type {?} */ const fallbackLang = this.userConfig.fallbackLang; if (!fallbackLang) { throw new Error('When using the default fallback, a fallback language must be provided in the config!'); } return Array.isArray(fallbackLang) ? fallbackLang : [fallbackLang]; } } /** @nocollapse */ DefaultFallbackStrategy.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_CONFIG,] }] } ]; /** * @fileoverview added by tsickle * Generated from: lib/merge-config.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} defaultConfig * @param {?} userConfig * @return {?} */ function mergeConfig(defaultConfig, userConfig) { return Object.assign({}, defaultConfig, userConfig, { missingHandler: Object.assign({}, defaultConfig.missingHandler, userConfig.missingHandler), flatten: Object.assign({}, defaultConfig.flatten, userConfig.flatten) }); } /** * @fileoverview added by tsickle * Generated from: lib/shared.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /* * @example * * given: lazy-page/en => lazy-page * */ /** * @param {?} lang * @return {?} */ function getScopeFromLang(lang) { if (!lang) { return ''; } /** @type {?} */ const split = lang.split('/'); split.pop(); return split.join('/'); } /* * @example * * given: lazy-page/en => en * */ /** * @param {?} lang * @return {?} */ function getLangFromScope(lang) { if (!lang) { return ''; } /** @type {?} */ const split = lang.split('/'); return split.pop(); } /** * \@example * * getPipeValue('todos|scoped', 'scoped') [true, 'todos'] * getPipeValue('en|static', 'static') [true, 'en'] * getPipeValue('en', 'static') [false, 'en'] * @param {?} str * @param {?} value * @param {?=} char * @return {?} */ function getPipeValue(str, value, char = '|') { if (isString(str)) { /** @type {?} */ const splitted = str.split(char); /** @type {?} */ const lastItem = splitted.pop(); return lastItem === value ? [true, splitted.toString()] : [false, lastItem]; } return [false, '']; } /** * @param {?} service * @param {?} lang * @return {?} */ function shouldListenToLangChanges(service, lang) { const [hasStatic] = getPipeValue(lang, 'static'); if (hasStatic === false) { // If we didn't get 'lang|static' check if it's set in the global level return service.config.reRenderOnLangChange; } // We have 'lang|static' so don't listen to lang changes return false; } /** * @param {?} listenToLangChange * @return {?} */ function listenOrNotOperator(listenToLangChange) { return listenToLangChange ? (/** * @param {?} source * @return {?} */ source => source) : take(1); } /** * @param {?} inlineLoader * @param {?} scope * @return {?} */ function prependScope(inlineLoader, scope) { return Object.keys(inlineLoader).reduce((/** * @param {?} acc * @param {?} lang * @return {?} */ (acc, lang) => { acc[`${scope}/${lang}`] = inlineLoader[lang]; return acc; }), {}); } /** * @param {?} providerScope * @param {?} scope * @return {?} */ function resolveInlineLoader(providerScope, scope) { return hasInlineLoader(providerScope) ? prependScope(providerScope.loader, scope) : null; } /** * @param {?} lang * @return {?} */ function getEventPayload(lang) { return { scope: getScopeFromLang(lang) || null, langName: getLangFromScope(lang), lang }; } /** * @fileoverview added by tsickle * Generated from: lib/resolve-loader.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} path * @param {?} mainLoader * @param {?} inlineLoader * @param {?} data * @return {?} */ function resolveLoader(path, mainLoader, inlineLoader, data) { if (inlineLoader) { /** @type {?} */ const pathLoader = inlineLoader[path]; if (isFunction(pathLoader) === false) { throw `You're using an inline loader but didn't provide a loader for ${path}`; } return inlineLoader[path]().then((/** * @param {?} res * @return {?} */ res => (res.default ? res.default : res))); } return mainLoader.getTranslation(path, data); } /** * @fileoverview added by tsickle * Generated from: lib/get-fallbacks-loaders.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} mainPath * @param {?} fallbackPath * @param {?} mainLoader * @param {?} inlineLoader * @param {?} data * @return {?} */ function getFallbacksLoaders(mainPath, fallbackPath, mainLoader, inlineLoader, data) { return [mainPath, fallbackPath].map((/** * @param {?} path * @return {?} */ path => { /** @type {?} */ const loader = resolveLoader(path, mainLoader, inlineLoader, data); return from(loader).pipe(map((/** * @param {?} translation * @return {?} */ translation => ({ translation, lang: path })))); })); } /** * @fileoverview added by tsickle * Generated from: lib/transloco.service.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ let service; /** * @template T * @param {?} key * @param {?=} params * @param {?=} lang * @return {?} */ function translate(key, params = {}, lang) { return service.translate(key, params, lang); } class TranslocoService { /** * @param {?} loader * @param {?} parser * @param {?} missingHandler * @param {?} interceptor * @param {?} userConfig * @param {?} fallbackStrategy */ constructor(loader, parser, missingHandler, interceptor, userConfig, fallbackStrategy) { this.loader = loader; this.parser = parser; this.missingHandler = missingHandler; this.interceptor = interceptor; this.userConfig = userConfig; this.fallbackStrategy = fallbackStrategy; this.translations = new Map(); this.cache = new Map(); this.firstFallbackLang = null; this.availableLangs = []; this.isResolvedMissingOnce = false; this.events = new Subject(); this.events$ = this.events.asObservable(); this.failedLangs = new Set(); if (!this.loader) { this.loader = new DefaultLoader(this.translations); } service = this; this.mergedConfig = mergeConfig(defaultConfig, userConfig); this.setAvailableLangs(this.mergedConfig.availableLangs); this.setFallbackLangForMissingTranslation(this.mergedConfig); this.setDefaultLang(this.mergedConfig.defaultLang); this.lang = new BehaviorSubject(this.getDefaultLang()); // Don't use distinctUntilChanged as we need the ability to update // the value when using setTranslation or setTranslationKeys this.langChanges$ = this.lang.asObservable(); /** * When we have a failure, we want to define the next language that succeeded as the active */ this.subscription = this.events$.subscribe((/** * @param {?} e * @return {?} */ e => { if (e.type === 'translationLoadSuccess' && e.wasFailure) { // Handle scoped lang /** @type {?} */ const lang = getLangFromScope(e.payload.lang); this.setActiveLang(lang); } })); } /** * @return {?} */ get config() { return this.mergedConfig; } /** * @return {?} */ getDefaultLang() { return this.defaultLang; } /** * @param {?} lang * @return {?} */ setDefaultLang(lang) { this.defaultLang = lang; } /** * @return {?} */ getActiveLang() { return this.lang.getValue(); } /** * @template THIS * @this {THIS} * @param {?} lang * @return {THIS} */ setActiveLang(lang) { (/** @type {?} */ (this)).parser.onLangChanged && (/** @type {?} */ (this)).parser.onLangChanged(lang); (/** @type {?} */ (this)).lang.next(lang); return (/** @type {?} */ (this)); } /** * @param {?} langs * @return {?} */ setAvailableLangs(langs) { this.availableLangs = langs; } /** * Gets the available languages. * * @return {?} * An array of the available languages. Can be either a `string[]` or a `{ id: string; label: string }[]` * depending on how the available languages are set in your module. */ getAvailableLangs() { return this.availableLangs; } /** * @param {?} path * @param {?=} options * @return {?} */ load(path, options = {}) { if (this.cache.has(path) === false) { /** @type {?} */ let loadTranslation; /** @type {?} */ const isScope = this._isLangScoped(path); /** @type {?} */ const scope = isScope ? getScopeFromLang(path) : null; if (this.useFallbackTranslation(path)) { // if the path is scope the fallback should be `scope/fallbackLang`; /** @type {?} */ const fallback = isScope ? `${scope}/${this.firstFallbackLang}` : this.firstFallbackLang; /** @type {?} */ const loaders = getFallbacksLoaders(path, fallback, this.loader, options.inlineLoader, { scope }); loadTranslation = forkJoin(loaders); } else { /** @type {?} */ const loader = resolveLoader(path, this.loader, options.inlineLoader, { scope }); loadTranslation = from(loader); } /** @type {?} */ const load$ = loadTranslation.pipe(retry(this.config.failedRetries), tap((/** * @param {?} translation * @return {?} */ translation => { if (Array.isArray(translation)) { translation.forEach((/** * @param {?} t * @return {?} */ t => { this.handleSuccess(t.lang, t.translation); // Save the fallback in cache so we'll not create a redundant request if (t.lang !== path) { this.cache.set(t.lang, of({})); } })); return; } this.handleSuccess(path, translation); })), catchError((/** * @return {?} */ () => this.handleFailure(path, options))), shareReplay(1)); this.cache.set(path, load$); } return this.cache.get(path); } /** * Gets the instant translated value of a key * * \@example * * translate<string>('hello') * translate('hello', { value: 'value' }) * translate<string[]>(['hello', 'key']) * translate('hello', { }, 'en') * translate('scope.someKey', { }, 'en') * @template T * @param {?} key * @param {?=} params * @param {?=} lang * @return {?} */ translate(key, params = {}, lang = this.getActiveLang()) { if (!key) return (/** @type {?} */ (key)); const { scope, resolveLang } = this.resolveLangAndScope(lang); if (Array.isArray(key)) { return (/** @type {?} */ (key.map((/** * @param {?} k * @return {?} */ k => this.translate(scope ? `${scope}.${k}` : k, params, resolveLang))))); } key = scope ? `${scope}.${key}` : key; /** @type {?} */ const translation = this.getTranslation(resolveLang); /** @type {?} */ const value = translation[key]; if (!value) { return this._handleMissingKey(key, value, params); } return this.parser.transpile(value, params, translation); } /** * Gets the translated value of a key as observable * * \@example * * selectTranslate<string>('hello').subscribe(value => ...) * selectTranslate<string>('hello', {}, 'es').subscribe(value => ...) * selectTranslate<string>('hello', {}, 'todos').subscribe(value => ...) * selectTranslate<string>('hello', {}, { scope: 'todos' }).subscribe(value => ...) * * @template T * @param {?} key * @param {?=} params * @param {?=} lang * @param {?=} _isObject * @return {?} */ selectTranslate(key, params, lang, _isObject = false) { /** @type {?} */ let inlineLoader = null; /** @type {?} */ const load = (/** * @param {?} lang * @param {?=} options * @return {?} */ (lang, options) => this.load(lang, options).pipe(map((/** * @return {?} */ () => (_isObject ? this.translateObject(key, params, lang) : this.translate(key, params, lang)))))); if (isNil(lang)) { return this.langChanges$.pipe(switchMap((/** * @param {?} lang * @return {?} */ lang => load(lang)))); } if (isScopeObject(lang)) { // it's a scope object. /** @type {?} */ const providerScope = (/** @type {?} */ (lang)); lang = providerScope.scope; inlineLoader = resolveInlineLoader(providerScope, providerScope.scope); } lang = (/** @type {?} */ (lang)); if (this.isLang(lang) || this.isScopeWithLang(lang)) { return load(lang); } // it's a scope /** @type {?} */ const scope = lang; return this.langChanges$.pipe(switchMap((/** * @param {?} lang * @return {?} */ lang => load(`${scope}/${lang}`, { inlineLoader })))); } /** * Whether the scope with lang * * \@example * * todos/en => true * todos => false * @private * @param {?} lang * @return {?} */ isScopeWithLang(lang) { return this.isLang(getLangFromScope(lang)); } /** * @template T * @param {?} key * @param {?=} params * @param {?=} lang * @return {?} */ translateObject(key, params, lang = this.getActiveLang()) { if (isString(key) || Array.isArray(key)) { if (Array.isArray(key)) { return (/** @type {?} */ (key.map((/** * @param {?} k * @return {?} */ k => this.translateObject(scope ? `${scope}.${k}` : k, params, resolveLang))))); } const { resolveLang, scope } = this.resolveLangAndScope(lang); /** @type {?} */ const translation = this.getTranslation(resolveLang); key = scope ? `${scope}.${key}` : key; /** @type {?} */ const value = unflatten(this.getObjectByKey(translation, key)); /* If an empty object was returned we want to try and translate the key as a string and not an object */ return isEmpty(value) ? this.translate(key, params, lang) : this.parser.transpile(value, params, translation); } /** @type {?} */ const translations = []; for (const [_key, _params] of this.getEntries(key)) { translations.push(this.translateObject(_key, _params, lang)); } return translations; } /** * @template T * @param {?} key * @param {?=} params * @param {?=} lang * @return {?} */ selectTranslateObject(key, params, lang) { if (isString(key) || Array.isArray(key)) { return this.selectTranslate(key, params, lang, true); } const [[firstKey, firstParams], ...rest] = this.getEntries(key); /* In order to avoid subscribing multiple times to the load language event by calling selectTranslateObject for each pair, * we listen to when the first key has been translated (the language is loaded) and translate the rest synchronously */ return this.selectTranslateObject(firstKey, firstParams, lang).pipe(map((/** * @param {?} value * @return {?} */ value => { /** @type {?} */ const translations = [value]; for (const [_key, _params] of rest) { translations.push(this.translateObject(_key, _params, lang)); } return translations; }))); } /** * @param {?=} langOrScope * @return {?} */ getTranslation(langOrScope) { if (langOrScope) { if (this.isLang(langOrScope)) { return this.translations.get(langOrScope) || {}; } else { // This is a scope, build the scope value from the translation object const { scope, resolveLang } = this.resolveLangAndScope(langOrScope); /** @type {?} */ const translation = this.translations.get(resolveLang) || {}; return this.getObjectByKey(translation, scope); } } return this.translations; } /** * Gets an object of translations for a given language * * \@example * * selectTranslation().subscribe() - will return the current lang translation * selectTranslation('es').subscribe() * selectTranslation('admin-page').subscribe() - will return the current lang scope translation * selectTranslation('admin-page/es').subscribe() * @param {?=} lang * @return {?} */ selectTranslation(lang) { /** @type {?} */ let language$ = this.langChanges$; if (lang) { /** @type {?} */ const scopeLangSpecified = getLangFromScope(lang) !== lang; if (this.isLang(lang) || scopeLangSpecified) { language$ = of(lang); } else { language$ = this.langChanges$.pipe(map((/** * @param {?} currentLang * @return {?} */ currentLang => `${lang}/${currentLang}`))); } } return language$.pipe(switchMap((/** * @param {?} language * @return {?} */ language => this.load(language).pipe(map((/** * @return {?} */ () => this.getTranslation(language))))))); } /** * Sets or merge a given translation object to current lang * * \@example * * setTranslation({ ... }) * setTranslation({ ... }, 'en') * setTranslation({ ... }, 'es', { merge: false } ) * setTranslation({ ... }, 'todos/en', { merge: false } ) * @param {?} translation * @param {?=} lang * @param {?=} options * @return {?} */ setTranslation(translation, lang = this.getActiveLang(), options = {}) { /** @type {?} */ const defaults = { merge: true, emitChange: true }; /** @type {?} */ const mergedOptions = Object.assign({}, defaults, options); /** @type {?} */ const scope = getScopeFromLang(lang); /** * If this isn't a scope we use the whole translation as is * otherwise we need to flat the scope and use it * @type {?} */ let flattenScopeOrTranslation = translation; // Merged the scoped language into the active language if (scope) { /** @type {?} */ const key = this.getMappedScope(scope); flattenScopeOrTranslation = flatten({ [key]: translation }); } /** @type {?} */ const currentLang = scope ? getLangFromScope(lang) : lang; /** @type {?} */ const mergedTranslation = Object.assign({}, (mergedOptions.merge && this.getTranslation(currentLang)), flattenScopeOrTranslation); /** @type {?} */ const flattenTranslation = this.mergedConfig.flatten.aot ? mergedTranslation : flatten(mergedTranslation); /** @type {?} */ const withHook = this.interceptor.preSaveTranslation(flattenTranslation, currentLang); this.translations.set(currentLang, withHook); mergedOptions.emitChange && this.setActiveLang(this.getActiveLang()); } /** * Sets translation key with given value * * \@example * * setTranslationKey('key', 'value') * setTranslationKey('key.nested', 'value') * setTranslationKey('key.nested', 'value', 'en') * setTranslationKey('key.nested', 'value', 'en', { emitChange: false } ) * @param {?} key * @param {?} value * @param {?=} lang * @param {?=} options * @return {?} */ setTranslationKey(key, value, lang = this.getActiveLang(), // Todo: Use Omit and merge options and lang to one object in v3 options = {}) { /** @type {?} */ const withHook = this.interceptor.preSaveTranslationKey(key, value, lang); /** @type {?} */ const newValue = { [key]: withHook }; this.setTranslation(newValue, lang, Object.assign({}, options, { merge: true })); } /** * Sets the fallback lang for the currently active language * @param {?} __0 * @return {?} */ setFallbackLangForMissingTranslation({ fallbackLang }) { /** @type {?} */ const lang = Array.isArray(fallbackLang) ? fallbackLang[0] : fallbackLang; if (this.useFallbackTranslation(lang) && fallbackLang) { this.firstFallbackLang = lang; } } /** * \@internal * @param {?} key * @param {?} value * @param {?=} params * @return {?} */ _handleMissingKey(key, value, params) { if (this.config.missingHandler.allowEmpty && value === '') { return ''; } if (this.useFallbackTranslation() && !this.isResolvedMissingOnce) { // We need to set it to true to prevent a loop this.isResolvedMissingOnce = true; /** @type {?} */ const fallbackValue = this.translate(key, params, this.firstFallbackLang); this.isResolvedMissingOnce = false; return fallbackValue; } return this.missingHandler.handle(key, this.getMissingHandlerData(), params); } /** * \@internal * @param {?} lang * @return {?} */ _isLangScoped(lang) { return this.getAvailableLangsIds().indexOf(lang) === -1; } /** * Checks if a given string is one of the specified available languages. * @param {?} lang * @return {?} * True if the given string is an available language. * False if the given string is not an available language. */ isLang(lang) { return this.getAvailableLangsIds().indexOf(lang) !== -1; } /** * \@internal * * We always want to make sure the global lang is loaded * before loading the scope since you can access both via the pipe/directive. * @param {?} path * @param {?=} inlineLoader * @return {?} */ _loadDependencies(path, inlineLoader) { /** @type {?} */ const mainLang = getLangFromScope(path); if (this._isLangScoped(path) && !this.isLoadedTranslation(mainLang)) { return combineLatest(this.load(mainLang), this.load(path, { inlineLoader })); } return this.load(path, { inlineLoader }); } /** * \@internal * @param {?} langOrScope * @return {?} */ _completeScopeWithLang(langOrScope) { if (this._isLangScoped(langOrScope) && !this.isLang(getLangFromScope(langOrScope))) { return `${langOrScope}/${this.getActiveLang()}`; } return langOrScope; } /** * \@internal * @param {?} scope * @param {?} alias * @return {?} */ _setScopeAlias(scope, alias) { if (!this.mergedConfig.scopeMapping) { this.mergedConfig.scopeMapping = {}; } this.mergedConfig.scopeMapping[scope] = alias; } /** * @return {?} */ ngOnDestroy() { this.subscription.unsubscribe(); // Caretaker note: since this is the root provider, it'll be destroyed when the `NgModuleRef.destroy()` is run. // Cached values capture `this`, thus leading to a circular reference and preventing the `TranslocoService` from // being GC'd. This would lead to a memory leak when server-side rendering is used since the service is created // and destroyed per each HTTP request, but any service is not getting GC'd. this.cache.clear(); } /** * @private * @param {?} lang * @return {?} */ isLoadedTranslation(lang) { return size(this.getTranslation(lang)); } /** * @private * @return {?} */ getAvailableLangsIds() { /** @type {?} */ const first = this.getAvailableLangs()[0]; if (isString(first)) { return (/** @type {?} */ (this.getAvailableLangs())); } return ((/** @type {?} */ (this.getAvailableLangs()))).map((/** * @param {?} l * @return {?} */ l => l.id)); } /** * @private * @return {?} */ getMissingHandlerData() { return Object.assign({}, this.config, { activeLang: this.getActiveLang(), availableLangs: this.availableLangs, defaultLang: this.defaultLang }); } /** * Use a fallback translation set for missing keys of the primary language * This is unrelated to the fallback language (which changes the active language) * @private * @param {?=} lang * @return {?} */ useFallbackTranslation(lang) { return this.config.missingHandler.useFallbackTranslation && lang !== this.firstFallbackLang; } /** * @private * @param {?} lang * @param {?} translation * @return {?} */ handleSuccess(lang, translation) { this.setTranslation(translation, lang, { emitChange: false }); this.events.next({ wasFailure: !!this.failedLangs.size, type: 'translationLoadSuccess', payload: getEventPayload(lang) }); this.failedLangs.forEach((/** * @param {?} l * @return {?} */ l => this.cache.delete(l))); this.failedLangs.clear(); } /** * @private * @param {?} lang * @param {?} loadOptions * @return {?} */ handleFailure(lang, loadOptions) { // When starting to load a first choice language, initialize // the failed counter and resolve the fallback langs. if (isNil(loadOptions.failedCounter)) { loadOptions.failedCounter = 0; if (!loadOptions.fallbackLangs) { loadOptions.fallbackLangs = this.fallbackStrategy.getNextLangs(lang); } } /** @type {?} */ const splitted = lang.split('/'); /** @type {?} */ const fallbacks = loadOptions.fallbackLangs; /** @type {?} */ const nextLang = fallbacks[loadOptions.failedCounter]; this.failedLangs.add(lang); // This handles the case where a loaded fallback language is requested again if (this.cache.has(nextLang)) { this.handleSuccess(nextLang, this.getTranslation(nextLang)); return EMPTY; } /** @type {?} */ const isFallbackLang = nextLang === splitted[splitted.length - 1]; if (!nextLang || isFallbackLang) { /** @type {?} */ let msg = `Unable to load translation and all the fallback languages`; if (splitted.length > 1) { msg += `, did you misspelled the scope name?`; } throw new Error(msg); } /** @type {?} */ let resolveLang = nextLang; // if it's scoped lang if (splitted.length > 1) { // We need to resolve it to: // todos/langNotExists => todos/nextLang splitted[splitted.length - 1] = nextLang; resolveLang = splitted.join('/'); } loadOptions.failedCounter++; this.events.next({ type: 'translationLoadFailure', payload: getEventPayload(lang) }); return this.load(resolveLang, loadOptions); } /** * @private * @param {?} scope * @return {?} */ getMappedScope(scope) { const { scopeMapping = {} } = this.config; return scopeMapping[scope] || toCamelCase(scope); } /** * If lang is scope we need to check the following cases: * todos/es => in this case we should take `es` as lang * todos => in this case we should set the active lang as lang * @private * @param {?} lang * @return {?} */ resolveLangAndScope(lang) { /** @type {?} */ let resolveLang = lang; /** @type {?} */ let scope; if (this._isLangScoped(lang)) { // en for example /** @type {?} */ const langFromScope = getLangFromScope(lang); // en is lang /** @type {?} */ const hasLang = this.isLang(langFromScope); // take en resolveLang = hasLang ? langFromScope : this.getActiveLang(); // find the scope scope = this.getMappedScope(hasLang ? getScopeFromLang(lang) : lang); } return { scope, resolveLang }; } /** * @private * @param {?} translation * @param {?} key * @return {?} */ getObjectByKey(translation, key) { /** @type {?} */ const result = {}; /** @type {?} */ const prefix = `${key}.`; for (const currentKey in translation) { if (currentKey.startsWith(prefix)) { result[currentKey.replace(prefix, '')] = translation[currentKey]; } } return result; } /** * @private * @param {?} key * @return {?} */ getEntries(key) { return key instanceof Map ? key.entries() : Object.entries(key); } } TranslocoService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ TranslocoService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [TRANSLOCO_LOADER,] }] }, { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_TRANSPILER,] }] }, { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_MISSING_HANDLER,] }] }, { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_INTERCEPTOR,] }] }, { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_CONFIG,] }] }, { type: undefined, decorators: [{ type: Inject, args: [TRANSLOCO_FALLBACK_STRATEGY,] }] } ]; /** @nocollapse */ TranslocoService.ngInjectableDef = defineInjectable({ factory: function TranslocoService_Factory() { return new TranslocoService(inject(TRANSLOCO_LOADER, 8), inject(TRANSLOCO_TRANSPILER), inject(TRANSLOCO_MISSING_HANDLER), inject(TRANSLOCO_INTERCEPTOR), inject(TRANSLOCO_CONFIG), inject(TRANSLOCO_FALLBACK_STRATEGY)); }, token: TranslocoService, providedIn: "root" }); /** * @fileoverview added by tsickle * Generated from: lib/loader-component.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TranslocoLoaderComponent { } TranslocoLoaderComponent.decorators = [ { type: Component, args: [{ template: ` <div class="transloco-loader-template" [innerHTML]="html"></div> ` }] } ]; TranslocoLoaderComponent.propDecorators = { html: [{ type: Input }] }; /** * @fileoverview added by tsickle * Generated from: lib/template-handler.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TemplateHandler { /** * @param {?} view * @param {?} vcr */ constructor(view, vcr) { this.view = view; this.vcr = vcr; this.injector = this.vcr.injector; } /** * @return {?} */ attachView() { if (this.view instanceof TemplateRef) { this.vcr.createEmbeddedView(this.view); } else if (isString(this.view)) { /** @type {?} */ const componentRef = this.createComponent(TranslocoLoaderComponent); componentRef.instance.html = this.view; componentRef.hostView.detectChanges(); } else { this.createComponent(this.view); } } /** * @return {?} */ detachView() { this.vcr.clear(); } /** * @private * @template T * @param {?} cmp * @return {?} */ createComponent(cmp) { /** @type {?} */ const cfr = this.injector.get(ComponentFactoryResolver); /** @type {?} */ const factory = cfr.resolveComponentFactory(cmp); return this.vcr.createComponent(factory); } } /** * @fileoverview added by tsickle * Generated from: lib/transloco-lang.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_LANG = new InjectionToken('TRANSLOCO_LANG'); /** * @fileoverview added by tsickle * Generated from: lib/transloco-loading-template.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_LOADING_TEMPLATE = new InjectionToken('TRANSLOCO_LOADING_TEMPLATE'); /** * @fileoverview added by tsickle * Generated from: lib/transloco-scope.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const TRANSLOCO_SCOPE = new InjectionToken('TRANSLOCO_SCOPE'); /** * @fileoverview added by tsickle * Generated from: lib/lang-resolver.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class LangResolver { constructor() { this.initialized = false; } // inline => provider => active /** * @param {?=} __0 * @return {?} */ resolve({ inline, provider, active } = { inline: undefined, provider: undefined, active: undefined }) { /** @type {?} */ let lang = active; /** * When the user changes the lang we need to update * the view. Otherwise, the lang will remain the inline/provided lang */ if (this.initialized) { lang = active; return lang; } if (provider) { const [_, extracted] = getPipeValue(provider, 'static'); lang = extracted; } if (inline) { const [_, extracted] = getPipeValue(inl