UNPKG

angular-l10n

Version:

Angular library to translate texts, dates and numbers

1,156 lines (1,132 loc) 80.6 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Inject, inject, ChangeDetectorRef, ElementRef, Renderer2, Directive, Input, APP_INITIALIZER, makeEnvironmentProviders, Pipe, NgModule, forwardRef } from '@angular/core'; import { of, throwError, BehaviorSubject, concat, merge, Subject } from 'rxjs'; import { shareReplay, takeUntil } from 'rxjs/operators'; import { NG_VALIDATORS } from '@angular/forms'; /** * L10n configuration token. */ const L10N_CONFIG = new InjectionToken('L10N_CONFIG'); /** * L10n locale token. */ const L10N_LOCALE = new InjectionToken('L10N_LOCALE'); function l10nError(type, value) { return new Error(`angular-l10n (${type.name}): ${value}`); } function validateLanguage(language) { const regExp = new RegExp(/^([a-z]{2,3})(\-[A-Z][a-z]{3})?(\-[A-Z]{2})?(-u.+)?$/); return regExp.test(language); } function formatLanguage(language, format) { if (language == null || language === '') return ''; if (!validateLanguage(language)) throw l10nError(formatLanguage, 'Invalid language'); const [, LANGUAGE = '', SCRIPT = '', REGION = ''] = language.match(/^([a-z]{2,3})(\-[A-Z][a-z]{3})?(\-[A-Z]{2})?/) || []; switch (format) { case 'language': return LANGUAGE; case 'language-script': return LANGUAGE + SCRIPT; case 'language-region': return LANGUAGE + REGION; case 'language-script-region': return LANGUAGE + SCRIPT + REGION; } } function parseLanguage(language) { const groups = language.match(/^([a-z]{2,3})(\-([A-Z][a-z]{3}))?(\-([A-Z]{2}))?(-u.+)?$/); if (groups == null) throw l10nError(parseLanguage, 'Invalid language'); return { language: groups[1], script: groups[3], region: groups[5], extension: groups[6] }; } function getBrowserLanguage(format) { let browserLanguage = null; if (typeof navigator !== 'undefined' && navigator.language) { switch (format) { case 'language-region': case 'language-script-region': browserLanguage = navigator.language; break; default: browserLanguage = navigator.language.split('-')[0]; } } return browserLanguage; } function getSchema(schema, language, format) { const element = schema.find(item => formatLanguage(item.locale.language, format) === language); return element; } function getValue(key, data, keySeparator) { if (data) { if (keySeparator) { return key.split(keySeparator).reduce((acc, cur) => (acc && acc[cur]) != null ? acc[cur] : null, data); } return data[key] != null ? data[key] : null; } return null; } function handleParams(value, params) { const replacedValue = value.replace(/{{\s?([^{}\s]*)\s?}}/g, (substring, parsedKey) => { const replacer = params[parsedKey]; return replacer !== undefined ? replacer : substring; }); return replacedValue; } function mergeDeep(target, source) { const output = Object.assign({}, target); if (isObject(target) && isObject(source)) { Object.keys(source).forEach((key) => { if (isObject(source[key])) { if (!(key in target)) { Object.assign(output, { [key]: source[key] }); } else { output[key] = mergeDeep(target[key], source[key]); } } else { Object.assign(output, { [key]: source[key] }); } }); } return output; } function toNumber(value) { const parsedValue = typeof value === 'string' && !isNaN(+value - parseFloat(value)) ? +value : value; return parsedValue; } function toDate(value) { if (isDate(value)) { return value; } if (typeof value === 'number' && !isNaN(value)) { return new Date(value); } if (typeof value === 'string') { value = value.trim(); if (!isNaN(value - parseFloat(value))) { return new Date(parseFloat(value)); } if (/^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) { const [y, m, d] = value.split('-').map((val) => +val); return new Date(y, m - 1, d); } const match = value.match(/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/); if (match) { return isoStringToDate(match); } } const date = new Date(value); if (!isDate(date)) { throw l10nError(toDate, 'Invalid date'); } return date; } const PARSE_DATE_STYLE = { full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }, long: { year: 'numeric', month: 'long', day: 'numeric' }, medium: { year: 'numeric', month: 'short', day: 'numeric' }, short: { year: '2-digit', month: 'numeric', day: 'numeric' } }; const PARSE_TIME_STYLE = { full: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' }, long: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' }, medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' }, short: { hour: 'numeric', minute: 'numeric' } }; function parseDigits(digits) { const groups = digits.match(/^(\d+)?\.((\d+)(\-(\d+))?)?$/); if (groups == null) throw l10nError(parseDigits, 'Invalid digits'); return { minimumIntegerDigits: groups[1] ? parseInt(groups[1]) : undefined, minimumFractionDigits: groups[3] ? parseInt(groups[3]) : undefined, maximumFractionDigits: groups[5] ? parseInt(groups[5]) : undefined, }; } function isObject(item) { return typeof item === 'object' && !Array.isArray(item); } function isDate(value) { return value instanceof Date && !isNaN(value.valueOf()); } /** * Converts a date in ISO 8601 to a Date. */ function isoStringToDate(match) { const date = new Date(0); let tzHour = 0; let tzMin = 0; const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear; const timeSetter = match[8] ? date.setUTCHours : date.setHours; if (match[9]) { tzHour = Number(match[9] + match[10]); tzMin = Number(match[9] + match[11]); } dateSetter.call(date, Number(match[1]), Number(match[2]) - 1, Number(match[3])); const h = Number(match[4] || 0) - tzHour; const m = Number(match[5] || 0) - tzMin; const s = Number(match[6] || 0); const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } class L10nCache { constructor() { this.cache = {}; } read(key, request) { if (this.cache[key]) return this.cache[key]; const response = request.pipe(shareReplay(1)); this.cache[key] = response; return response; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nCache, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nCache }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nCache, decorators: [{ type: Injectable }] }); /** * Implement this class-interface to create a storage for the locale. */ class L10nStorage { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nStorage }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nStorage, decorators: [{ type: Injectable }] }); class L10nDefaultStorage { async read() { return Promise.resolve(null); } async write(locale) { } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultStorage }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultStorage, decorators: [{ type: Injectable }] }); /** * Implement this class-interface to resolve the locale. */ class L10nLocaleResolver { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLocaleResolver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLocaleResolver }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLocaleResolver, decorators: [{ type: Injectable }] }); class L10nDefaultLocaleResolver { constructor(config) { this.config = config; } async get() { const browserLanguage = getBrowserLanguage(this.config.format); if (browserLanguage) { const schema = getSchema(this.config.schema, browserLanguage, this.config.format); if (schema) { return Promise.resolve(schema.locale); } } return Promise.resolve(null); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLocaleResolver, deps: [{ token: L10N_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLocaleResolver }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLocaleResolver, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [L10N_CONFIG] }] }] }); /** * Implement this class-interface to create a loader of translation data. */ class L10nTranslationLoader { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationLoader }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationLoader, decorators: [{ type: Injectable }] }); class L10nDefaultTranslationLoader { get(language, provider) { return provider.asset[language] ? of(provider.asset[language]) : throwError(() => l10nError(L10nDefaultTranslationLoader, 'Asset not found')); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationLoader }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationLoader, decorators: [{ type: Injectable }] }); /** * Implement this class-interface to create a translation fallback. */ class L10nTranslationFallback { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationFallback, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationFallback }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationFallback, decorators: [{ type: Injectable }] }); class L10nDefaultTranslationFallback { constructor(config, cache, translationLoader) { this.config = config; this.cache = cache; this.translationLoader = translationLoader; } /** * Translation data will be merged in the following order: * 'language' * 'language[-script]' * 'language[-script][-region]' */ get(language, provider) { const loaders = []; const keywords = language.match(/-?[a-zA-z]+/g) || []; let fallbackLanguage = ''; for (const keyword of keywords) { fallbackLanguage += keyword; if (this.config.cache) { loaders.push(this.cache.read(`${provider.name}-${fallbackLanguage}`, this.translationLoader.get(fallbackLanguage, provider))); } else { loaders.push(this.translationLoader.get(fallbackLanguage, provider)); } } return loaders; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationFallback, deps: [{ token: L10N_CONFIG }, { token: L10nCache }, { token: L10nTranslationLoader }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationFallback }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationFallback, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [L10N_CONFIG] }] }, { type: L10nCache }, { type: L10nTranslationLoader }] }); /** * Implement this class-interface to create an handler for translated values. */ class L10nTranslationHandler { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationHandler, decorators: [{ type: Injectable }] }); class L10nDefaultTranslationHandler { parseValue(key, params, value) { if (params) return handleParams(value, params); return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultTranslationHandler, decorators: [{ type: Injectable }] }); /** * Implement this class-interface to create an handler for missing values. */ class L10nMissingTranslationHandler { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nMissingTranslationHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nMissingTranslationHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nMissingTranslationHandler, decorators: [{ type: Injectable }] }); class L10nDefaultMissingTranslationHandler { handle(key, value, params) { return key; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultMissingTranslationHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultMissingTranslationHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultMissingTranslationHandler, decorators: [{ type: Injectable }] }); class L10nTranslationService { constructor(config, locale, cache, storage, resolveLocale, translationFallback, translationLoader, translationHandler, missingTranslationHandler) { this.config = config; this.locale = locale; this.cache = cache; this.storage = storage; this.resolveLocale = resolveLocale; this.translationFallback = translationFallback; this.translationLoader = translationLoader; this.translationHandler = translationHandler; this.missingTranslationHandler = missingTranslationHandler; /** * The translation data: {language: {key: value}} */ this.data = {}; this.translation = new BehaviorSubject(this.locale); this.error = new BehaviorSubject(null); } /** * Gets the current locale. */ getLocale() { return this.locale; } /** * Changes the current locale and load the translation data. * @param locale The new locale */ async setLocale(locale) { await this.loadTranslations(this.config.providers, locale); } /** * Fired every time the translation data has been loaded. Returns the locale. */ onChange() { return this.translation.asObservable(); } /** * Fired when the translation data could not been loaded. Returns the error. */ onError() { return this.error.asObservable(); } /** * Translates a key or an array of keys. * @param keys The key or an array of keys to be translated * @param params Optional parameters contained in the key * @param language The current language * @return The translated value or an object: {key: value} */ translate(keys, params, language = this.locale.language) { language = formatLanguage(language, this.config.format); if (Array.isArray(keys)) { const data = {}; for (const key of keys) { data[key] = this.translate(key, params, language); } return data; } const value = getValue(keys, this.data[language], this.config.keySeparator); return value ? this.translationHandler.parseValue(keys, params, value) : this.missingTranslationHandler.handle(keys, value, params); } /** * Checks if a translation exists. * @param key The key to be tested * @param language The current language */ has(key, language = this.locale.language) { language = formatLanguage(language, this.config.format); return getValue(key, this.data[language], this.config.keySeparator) !== null; } /** * Gets the language direction. */ getLanguageDirection(language = this.locale.language) { const schema = getSchema(this.config.schema, language, this.config.format); return schema ? schema.dir : undefined; } /** * Gets available languages. */ getAvailableLanguages() { const languages = this.config.schema.map(item => formatLanguage(item.locale.language, this.config.format)); return languages; } /** * Initializes the service * @param providers An array of L10nProvider */ async init(providers = this.config.providers) { let locale = null; // Tries to get locale from storage. if (locale == null) { locale = await this.storage.read(); } // Tries resolved locale. if (locale == null) { locale = await this.resolveLocale.get(); } // Uses default locale. if (locale == null) { locale = this.config.defaultLocale; } // Loads translation data. await this.loadTranslations(providers, locale); } /** * Can be called at every translation change. * @param providers An array of L10nProvider * @param locale The current locale */ async loadTranslations(providers = this.config.providers, locale = this.locale) { const language = formatLanguage(locale.language, this.config.format); return new Promise((resolve) => { concat(...this.getTranslation(providers, language)).subscribe({ next: (data) => this.addData(data, language), error: (error) => { this.handleError(error); resolve(); }, complete: () => { this.releaseTranslation(locale); resolve(); } }); }); } /** * Can be called to add translation data. * @param data The translation data {key: value} * @param language The language to add data */ addData(data, language) { this.data[language] = this.data[language] !== undefined ? mergeDeep(this.data[language], data) : data; } /** * Adds providers to configuration * @param providers The providers of the translations data */ addProviders(providers) { providers.forEach(provider => { if (!this.config.providers.find(p => p.name === provider.name)) { this.config.providers.push(provider); } }); } getTranslation(providers, language) { const lazyLoaders = []; let loaders = []; for (const provider of providers) { if (this.config.fallback) { loaders = loaders.concat(this.translationFallback.get(language, provider)); } else { if (this.config.cache) { lazyLoaders.push(this.cache.read(`${provider.name}-${language}`, this.translationLoader.get(language, provider))); } else { lazyLoaders.push(this.translationLoader.get(language, provider)); } } } loaders.push(merge(...lazyLoaders)); return loaders; } handleError(error) { this.error.next(error); } releaseTranslation(locale) { Object.assign(this.locale, locale); this.translation.next(this.locale); this.storage.write(this.locale); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationService, deps: [{ token: L10N_CONFIG }, { token: L10N_LOCALE }, { token: L10nCache }, { token: L10nStorage }, { token: L10nLocaleResolver }, { token: L10nTranslationFallback }, { token: L10nTranslationLoader }, { token: L10nTranslationHandler }, { token: L10nMissingTranslationHandler }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [L10N_CONFIG] }] }, { type: undefined, decorators: [{ type: Inject, args: [L10N_LOCALE] }] }, { type: L10nCache }, { type: L10nStorage }, { type: L10nLocaleResolver }, { type: L10nTranslationFallback }, { type: L10nTranslationLoader }, { type: L10nTranslationHandler }, { type: L10nMissingTranslationHandler }] }); class L10nAsyncPipe { constructor() { this.translation = inject(L10nTranslationService); this.cdr = inject(ChangeDetectorRef); this.onChanges = this.translation.onChange().subscribe({ next: () => this.cdr.markForCheck() }); } ngOnDestroy() { if (this.onChanges) this.onChanges.unsubscribe(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nAsyncPipe, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nAsyncPipe }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nAsyncPipe, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * Breadth First Search (BFS) algorithm for traversing & searching tree data structure of DOM * explores the neighbor nodes first, before moving to the next level neighbors. * Time complexity: between O(1) and O(|V|^2). */ function getTargetNode(rootNode) { return walk(rootNode); } const MAX_DEPTH = 10; function walk(rootNode) { const queue = []; let iNode; let depth = 0; let nodeToDepthIncrease = 1; queue.push(rootNode); while (queue.length > 0 && depth <= MAX_DEPTH) { iNode = queue.splice(0, 1)[0]; if (isTargetNode(iNode)) return iNode; if (depth < MAX_DEPTH && iNode.childNodes) { for (const child of Array.from(iNode.childNodes)) { if (isValidNode(child)) { queue.push(child); } } } if (--nodeToDepthIncrease === 0) { depth++; nodeToDepthIncrease = queue.length; } } return rootNode; } function isTargetNode(node) { return typeof node !== 'undefined' && node.nodeType === 3 && node.nodeValue != null && node.nodeValue.trim() !== ''; } /** * A valid node is not marked for translation. */ function isValidNode(node) { if (typeof node !== 'undefined' && node.nodeType === 1 && node.attributes) { for (const attr of Array.from(node.attributes)) { if (attr && /^l10n|translate/.test(attr.name)) return false; } } return true; } class L10nDirective { constructor() { this.el = inject(ElementRef); this.renderer = inject(Renderer2); this.translation = inject(L10nTranslationService); this.attributes = []; this.destroy = new Subject(); } set innerHTML(content) { // Handle TrustedHTML this.content = content.toString(); } ngAfterViewInit() { if (this.el && this.el.nativeElement) { this.element = this.el.nativeElement; this.renderNode = getTargetNode(this.el.nativeElement); this.text = this.getText(); this.attributes = this.getAttributes(); this.addTextListener(); if (this.language) { this.replaceText(); this.replaceAttributes(); } else { this.addTranslationListener(); } } } ngOnChanges() { if (this.text) { if (this.nodeValue == null || this.nodeValue === '') { if (this.value) { this.text = this.value; } else if (this.content) { this.text = this.content; } } this.replaceText(); } if (this.attributes && this.attributes.length > 0) { this.replaceAttributes(); } } ngOnDestroy() { this.destroy.next(true); this.removeTextListener(); } getText() { let text = ''; if (this.element && this.element.childNodes.length > 0) { text = this.getNodeValue(); } else if (this.value) { text = this.value; } else if (this.content) { text = this.content; } return text; } getNodeValue() { this.nodeValue = this.renderNode != null && this.renderNode.nodeValue != null ? this.renderNode.nodeValue : ''; return this.nodeValue ? this.nodeValue.trim() : ''; } getAttributes() { const attributes = []; if (this.element && this.element.attributes) { for (const attr of Array.from(this.element.attributes)) { if (attr && attr.name) { const [, name = ''] = attr.name.match(/^l10n-(.+)$/) || []; if (name) { const targetAttr = Array.from(this.element.attributes).find(a => a.name === name); if (targetAttr) attributes.push({ name: targetAttr.name, value: targetAttr.value }); } } } } return attributes; } addTextListener() { if (typeof MutationObserver !== 'undefined') { this.textObserver = new MutationObserver(() => { if (this.element) { this.renderNode = getTargetNode(this.element); this.text = this.getText(); this.replaceText(); } }); if (this.renderNode) { this.textObserver.observe(this.renderNode, { subtree: true, characterData: true }); } } } removeTextListener() { if (this.textObserver) { this.textObserver.disconnect(); } } addTranslationListener() { this.translation.onChange().pipe(takeUntil(this.destroy)).subscribe({ next: () => { this.replaceText(); this.replaceAttributes(); } }); } replaceText() { if (this.text) { this.setText(this.getValue(this.text)); } } replaceAttributes() { if (this.attributes.length > 0) { this.setAttributes(this.getAttributesValues()); } } setText(value) { if (value) { if (this.nodeValue && this.text) { this.removeTextListener(); this.renderer.setValue(this.renderNode, this.nodeValue.replace(this.text, value)); this.addTextListener(); } else if (this.value) { this.renderer.setAttribute(this.element, 'value', value); } else if (this.content) { this.renderer.setProperty(this.element, 'innerHTML', value); } } } setAttributes(data) { for (const attr of this.attributes) { this.renderer.setAttribute(this.element, attr.name, data[attr.value]); } } getAttributesValues() { const values = this.attributes.map(attr => attr.value); const data = {}; for (const value of values) { data[value] = this.getValue(value); } return data; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.4", type: L10nDirective, inputs: { value: "value", innerHTML: "innerHTML", language: "language" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDirective, decorators: [{ type: Directive }], propDecorators: { value: [{ type: Input }], innerHTML: [{ type: Input }], language: [{ type: Input }] } }); const resolveL10n = async (route, state) => { const translation = inject(L10nTranslationService); const providers = route.data['l10nProviders']; translation.addProviders(providers); await translation.loadTranslations(providers); }; /** * Implement this class-interface to init L10n. */ class L10nLoader { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLoader }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nLoader, decorators: [{ type: Injectable }] }); class L10nDefaultLoader { constructor(translation) { this.translation = translation; } async init() { await this.translation.init(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLoader, deps: [{ token: L10nTranslationService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLoader }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultLoader, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: L10nTranslationService }] }); class L10nIntlService { constructor(config, locale, translation) { this.config = config; this.locale = locale; this.translation = translation; } /** * Formats a date. * @param value A date, a number (milliseconds since UTC epoch) or an ISO 8601 string * @param options A L10n or Intl DateTimeFormatOptions object * @param language The current language * @param timeZone The current time zone */ formatDate(value, options, language = this.locale.dateLanguage || this.locale.language, timeZone = this.locale.timeZone) { value = toDate(value); let dateTimeFormatOptions = {}; if (options) { if (options) { const { dateStyle, timeStyle, ...rest } = options; if (dateStyle) { dateTimeFormatOptions = { ...dateTimeFormatOptions, ...PARSE_DATE_STYLE[dateStyle] }; } if (timeStyle) { dateTimeFormatOptions = { ...dateTimeFormatOptions, ...PARSE_TIME_STYLE[timeStyle] }; } dateTimeFormatOptions = { ...dateTimeFormatOptions, ...rest }; } } if (timeZone) { dateTimeFormatOptions.timeZone = timeZone; } return new Intl.DateTimeFormat(language, dateTimeFormatOptions).format(value); } /** * Formats a number. * @param value A number or a string * @param options A L10n or Intl NumberFormatOptions object * @param language The current language * @param currency The current currency * @param convert An optional function to convert the value, with value and locale in the signature. * For example: * ``` * const convert = (value: number, locale: L10nLocale) => { return ... }; * ``` * @param convertParams Optional parameters for the convert function */ formatNumber(value, options, language = this.locale.numberLanguage || this.locale.language, currency = this.locale.currency, convert, convertParams) { if (options && options['style'] === 'unit' && !options['unit']) return value; value = toNumber(value); // Optional conversion. if (typeof convert === 'function') { value = convert(value, this.locale, Object.values(convertParams || {})); // Destructures params } let numberFormatOptions = {}; if (options) { const { digits, ...rest } = options; if (digits) { numberFormatOptions = { ...numberFormatOptions, ...parseDigits(digits) }; } numberFormatOptions = { ...numberFormatOptions, ...rest }; } if (currency) numberFormatOptions.currency = currency; return new Intl.NumberFormat(language, numberFormatOptions).format(value); } /** * Formats a relative time. * @param value A negative (or positive) number * @param unit An Intl RelativeTimeFormatUnit value * @param options An Intl RelativeTimeFormatOptions object * @param language The current language */ formatRelativeTime(value, unit, options, language = this.locale.dateLanguage || this.locale.language) { value = toNumber(value); return new Intl.RelativeTimeFormat(language, options).format(value, unit); } /** * Gets the plural by a number. * The 'value' is passed as a parameter to the translation function. * @param value The number to get the plural * @param prefix Optional prefix for the key * @param options An Intl PluralRulesOptions object * @param language The current language */ plural(value, prefix = '', options, language = this.locale.language) { value = toNumber(value); const rule = new Intl.PluralRules(language, options).select(value); const key = prefix ? `${prefix}${this.config.keySeparator}${rule}` : rule; return this.translation.translate(key, { value }); } /** * Returns translation of language, region, script or currency display names * @param code ISO code of language, region, script or currency * @param options An Intl DisplayNamesOptions object * @param language The current language */ displayNames(code, options, language = this.locale.language) { return new Intl.DisplayNames(language, options).of(code) || code; } getCurrencySymbol(locale = this.locale) { const decimal = this.formatNumber(0, { digits: '1.0-0' }, locale.numberLanguage || locale.language); const currency = this.formatNumber(0, { digits: '1.0-0', style: 'currency', currencyDisplay: 'symbol' }, locale.numberLanguage || locale.language, locale.currency); let symbol = currency.replace(decimal, ''); symbol = symbol.trim(); return symbol; } /** * Compares two keys by the value of translation. * @param key1 First key to compare * @param key1 Second key to compare * @param options An Intl CollatorOptions object * @param language The current language * @return A negative value if the value of translation of key1 comes before the value of translation of key2; * a positive value if key1 comes after key2; * 0 if they are considered equal or Intl.Collator is not supported */ compare(key1, key2, options, language = this.locale.language) { const value1 = this.translation.translate(key1); const value2 = this.translation.translate(key2); return new Intl.Collator(language, options).compare(value1, value2); } /** * Returns the representation of a list. * @param list An array of keys * @param options An Intl ListFormatOptions object * @param language The current language */ list(list, options, language = this.locale.language) { const values = list.map(key => this.translation.translate(key)); if (language == null || language === '') return values.join(', '); return new Intl.ListFormat(language, options).format(values); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nIntlService, deps: [{ token: L10N_CONFIG }, { token: L10N_LOCALE }, { token: L10nTranslationService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nIntlService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nIntlService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [L10N_CONFIG] }] }, { type: undefined, decorators: [{ type: Inject, args: [L10N_LOCALE] }] }, { type: L10nTranslationService }] }); /** * Implement this class-interface to create a validation service. */ class L10nValidation { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nValidation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nValidation }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nValidation, decorators: [{ type: Injectable }] }); class L10nDefaultValidation { constructor(locale) { this.locale = locale; } parseNumber(value, options, language = this.locale.numberLanguage || this.locale.language) { return null; } parseDate(value, options, language = this.locale.dateLanguage || this.locale.language) { return null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultValidation, deps: [{ token: L10N_LOCALE }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultValidation }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nDefaultValidation, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [L10N_LOCALE] }] }] }); function initL10n(translation) { return () => translation.init(); } function provideL10nTranslation(config, token = {}) { return makeEnvironmentProviders([ L10nTranslationService, L10nCache, { provide: L10N_CONFIG, useValue: config }, { provide: L10N_LOCALE, useValue: { language: '', units: {} } }, { provide: L10nStorage, useClass: token.storage || L10nDefaultStorage }, { provide: L10nLocaleResolver, useClass: token.localeResolver || L10nDefaultLocaleResolver }, { provide: L10nTranslationFallback, useClass: token.translationFallback || L10nDefaultTranslationFallback }, { provide: L10nTranslationLoader, useClass: token.translationLoader || L10nDefaultTranslationLoader }, { provide: L10nTranslationHandler, useClass: token.translationHandler || L10nDefaultTranslationHandler }, { provide: L10nMissingTranslationHandler, useClass: token.missingTranslationHandler || L10nDefaultMissingTranslationHandler }, { provide: L10nLoader, useClass: token.loader || L10nDefaultLoader }, { provide: APP_INITIALIZER, useFactory: initL10n, deps: [L10nLoader], multi: true } ]); } function provideL10nIntl() { return makeEnvironmentProviders([ L10nIntlService ]); } function provideL10nValidation(token = {}) { return makeEnvironmentProviders([ { provide: L10nValidation, useClass: token.validation || L10nDefaultValidation } ]); } class L10nTranslatePipe { constructor(translation) { this.translation = translation; } transform(key, language, params) { if (key == null || key === '') return null; return this.translation.translate(key, params, language); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslatePipe, deps: [{ token: L10nTranslationService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslatePipe, isStandalone: true, name: "translate" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslatePipe, decorators: [{ type: Pipe, args: [{ name: 'translate', pure: true, standalone: true }] }], ctorParameters: () => [{ type: L10nTranslationService }] }); class L10nTranslateAsyncPipe extends L10nAsyncPipe { transform(key, params, language) { if (key == null || key === '') return null; return this.translation.translate(key, params, language); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslateAsyncPipe, deps: null, target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslateAsyncPipe, isStandalone: true, name: "translateAsync", pure: false }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslateAsyncPipe, decorators: [{ type: Pipe, args: [{ name: 'translateAsync', pure: false, standalone: true }] }] }); class L10nTranslateDirective extends L10nDirective { set l10nTranslate(params) { if (params) this.params = params; } set translate(params) { if (params) this.params = params; } getValue(text) { return this.translation.translate(text, this.params, this.language); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslateDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.4", type: L10nTranslateDirective, isStandalone: true, selector: "[l10nTranslate],[translate]", inputs: { l10nTranslate: "l10nTranslate", translate: "translate", params: "params" }, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslateDirective, decorators: [{ type: Directive, args: [{ selector: '[l10nTranslate],[translate]', standalone: true }] }], propDecorators: { l10nTranslate: [{ type: Input }], translate: [{ type: Input }], params: [{ type: Input }] } }); class L10nTranslationModule { static forRoot(config, token = {}) { return { ngModule: L10nTranslationModule, providers: [ L10nTranslationService, L10nCache, { provide: L10N_CONFIG, useValue: config }, { provide: L10N_LOCALE, useValue: { language: '', units: {} } }, { provide: L10nStorage, useClass: token.storage || L10nDefaultStorage }, { provide: L10nLocaleResolver, useClass: token.localeResolver || L10nDefaultLocaleResolver }, { provide: L10nTranslationFallback, useClass: token.translationFallback || L10nDefaultTranslationFallback }, { provide: L10nTranslationLoader, useClass: token.translationLoader || L10nDefaultTranslationLoader }, { provide: L10nTranslationHandler, useClass: token.translationHandler || L10nDefaultTranslationHandler }, { provide: L10nMissingTranslationHandler, useClass: token.missingTranslationHandler || L10nDefaultMissingTranslationHandler }, { provide: L10nLoader, useClass: token.loader || L10nDefaultLoader }, { provide: APP_INITIALIZER, useFactory: initL10n, deps: [L10nLoader], multi: true } ] }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationModule, imports: [L10nTranslatePipe, L10nTranslateAsyncPipe, L10nTranslateDirective], exports: [L10nTranslatePipe, L10nTranslateAsyncPipe, L10nTranslateDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: L10nTranslationModule, decorators: [{ type: NgModule, args: [{ imports: [ L10nTranslatePipe, L10nTranslateAsyncPipe, L10nTranslateDirective ], exports: [ L10nTranslatePipe, L10nTranslateAsyncPipe, L10nTranslateDirective ] }] }] }); class L10nDatePipe { constructor(i