angular-l10n
Version:
Angular library to translate texts, dates and numbers
1,156 lines (1,132 loc) • 80.6 kB
JavaScript
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