angular-l10n
Version:
Angular library to translate texts, dates and numbers
1 lines • 137 kB
Source Map (JSON)
{"version":3,"file":"angular-l10n.mjs","sources":["../../../projects/angular-l10n/src/lib/models/l10n-config.ts","../../../projects/angular-l10n/src/lib/models/l10n-error.ts","../../../projects/angular-l10n/src/lib/models/utils.ts","../../../projects/angular-l10n/src/lib/services/l10n-cache.ts","../../../projects/angular-l10n/src/lib/services/l10n-storage.ts","../../../projects/angular-l10n/src/lib/services/l10n-locale-resolver.ts","../../../projects/angular-l10n/src/lib/services/l10n-translation-loader.ts","../../../projects/angular-l10n/src/lib/services/l10n-translation-fallback.ts","../../../projects/angular-l10n/src/lib/services/l10n-translation-handler.ts","../../../projects/angular-l10n/src/lib/services/l10n-missing-translation-handler.ts","../../../projects/angular-l10n/src/lib/services/l10n-translation.service.ts","../../../projects/angular-l10n/src/lib/models/l10n-async-pipe.ts","../../../projects/angular-l10n/src/lib/models/bfs.ts","../../../projects/angular-l10n/src/lib/models/l10n-directive.ts","../../../projects/angular-l10n/src/lib/functions/resolveL10n.ts","../../../projects/angular-l10n/src/lib/services/l10n-loader.ts","../../../projects/angular-l10n/src/lib/services/l10n-intl.service.ts","../../../projects/angular-l10n/src/lib/services/l10n-validation.ts","../../../projects/angular-l10n/src/lib/functions/initL10n.ts","../../../projects/angular-l10n/src/lib/functions/provideL10n.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-translate.pipe.ts","../../../projects/angular-l10n/src/lib/directives/l10n-translate.directive.ts","../../../projects/angular-l10n/src/lib/modules/l10n-translation.module.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-date.pipe.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-number.pipe.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-time-ago.pipe.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-plural.pipe.ts","../../../projects/angular-l10n/src/lib/pipes/l10n-display-names.pipe.ts","../../../projects/angular-l10n/src/lib/directives/l10n-date.directive.ts","../../../projects/angular-l10n/src/lib/directives/l10n-number.directive.ts","../../../projects/angular-l10n/src/lib/directives/l10n-time-ago.directive.ts","../../../projects/angular-l10n/src/lib/directives/l10n-plural.directive.ts","../../../projects/angular-l10n/src/lib/directives/l10n-display-names.directive.ts","../../../projects/angular-l10n/src/lib/modules/l10n-intl.module.ts","../../../projects/angular-l10n/src/lib/directives/l10n-validate-number.directive.ts","../../../projects/angular-l10n/src/lib/directives/l10n-validate-date.directive.ts","../../../projects/angular-l10n/src/lib/modules/l10n-validation.module.ts","../../../projects/angular-l10n/src/public-api.ts","../../../projects/angular-l10n/src/angular-l10n.ts"],"sourcesContent":["import { InjectionToken, Type } from '@angular/core';\r\n\r\nimport { L10nFormat, L10nProvider, L10nLocale, L10nSchema } from './types';\r\nimport { L10nStorage } from '../services/l10n-storage';\r\nimport { L10nLocaleResolver } from '../services/l10n-locale-resolver';\r\nimport { L10nTranslationFallback } from '../services/l10n-translation-fallback';\r\nimport { L10nTranslationLoader } from '../services/l10n-translation-loader';\r\nimport { L10nTranslationHandler } from '../services/l10n-translation-handler';\r\nimport { L10nMissingTranslationHandler } from '../services/l10n-missing-translation-handler';\r\nimport { L10nValidation } from '../services/l10n-validation';\r\nimport { L10nLoader } from '../services/l10n-loader';\r\n\r\nexport interface L10nConfig {\r\n /**\r\n * Format of the translation language. Pattern: 'language[-script][-region]'\r\n * E.g.\r\n * format: 'language-region';\r\n */\r\n format: L10nFormat;\r\n /**\r\n * The providers of the translations data.\r\n */\r\n providers: L10nProvider[];\r\n /**\r\n * Translation fallback.\r\n */\r\n fallback?: boolean;\r\n /**\r\n * Caching for providers.\r\n */\r\n cache?: boolean;\r\n /**\r\n * Sets key separator.\r\n */\r\n keySeparator: string;\r\n /**\r\n * Defines the default locale to be used.\r\n * E.g.\r\n * defaultLocale: { language: 'en-US', currency: 'USD };\r\n */\r\n defaultLocale: L10nLocale;\r\n /**\r\n * Provides the schema of the supported locales.\r\n */\r\n schema: L10nSchema[];\r\n}\r\n\r\n/**\r\n * L10n configuration token.\r\n */\r\nexport const L10N_CONFIG = new InjectionToken<L10nConfig>('L10N_CONFIG');\r\n\r\n/**\r\n * L10n locale token.\r\n */\r\nexport const L10N_LOCALE = new InjectionToken<L10nLocale>('L10N_LOCALE');\r\n\r\nexport interface L10nTranslationToken {\r\n /**\r\n * Defines the storage to be used.\r\n */\r\n storage?: Type<L10nStorage>;\r\n /**\r\n * Defines the locale to be used.\r\n */\r\n localeResolver?: Type<L10nLocaleResolver>;\r\n /**\r\n * Defines the translation fallback to be used.\r\n */\r\n translationFallback?: Type<L10nTranslationFallback>;\r\n /**\r\n * Defines the translation loader to be used.\r\n */\r\n translationLoader?: Type<L10nTranslationLoader>;\r\n /**\r\n * Defines the translation handler to be used.\r\n */\r\n translationHandler?: Type<L10nTranslationHandler>;\r\n /**\r\n * Defines the missing translation handler to be used.\r\n */\r\n missingTranslationHandler?: Type<L10nMissingTranslationHandler>;\r\n /**\r\n * Defines the loader to be used.\r\n */\r\n loader?: Type<L10nLoader>;\r\n}\r\n\r\nexport interface L10nValidationToken {\r\n /**\r\n * Defines the validation service to be used.\r\n */\r\n validation?: Type<L10nValidation>;\r\n}\r\n","import { Type } from '@angular/core';\r\n\r\nexport function l10nError(type: Type<any> | any, value: string): Error {\r\n return new Error(`angular-l10n (${type.name}): ${value}`);\r\n}\r\n","import { L10nFormat, L10nSchema } from './types';\r\nimport { l10nError } from './l10n-error';\r\n\r\nexport function validateLanguage(language: string): boolean {\r\n const regExp = new RegExp(/^([a-z]{2,3})(\\-[A-Z][a-z]{3})?(\\-[A-Z]{2})?(-u.+)?$/);\r\n return regExp.test(language);\r\n}\r\n\r\nexport function formatLanguage(language: string, format: L10nFormat): string {\r\n if (language == null || language === '') return '';\r\n if (!validateLanguage(language)) throw l10nError(formatLanguage, 'Invalid language');\r\n\r\n const [, LANGUAGE = '', SCRIPT = '', REGION = ''] = language.match(/^([a-z]{2,3})(\\-[A-Z][a-z]{3})?(\\-[A-Z]{2})?/) || [];\r\n switch (format) {\r\n case 'language':\r\n return LANGUAGE;\r\n case 'language-script':\r\n return LANGUAGE + SCRIPT;\r\n case 'language-region':\r\n return LANGUAGE + REGION;\r\n case 'language-script-region':\r\n return LANGUAGE + SCRIPT + REGION;\r\n }\r\n}\r\n\r\nexport function parseLanguage(language: string) {\r\n const groups = language.match(/^([a-z]{2,3})(\\-([A-Z][a-z]{3}))?(\\-([A-Z]{2}))?(-u.+)?$/);\r\n if (groups == null) throw l10nError(parseLanguage, 'Invalid language');\r\n\r\n return {\r\n language: groups[1],\r\n script: groups[3],\r\n region: groups[5],\r\n extension: groups[6]\r\n };\r\n}\r\n\r\nexport function getBrowserLanguage(format: L10nFormat): string | null {\r\n let browserLanguage = null;\r\n if (typeof navigator !== 'undefined' && navigator.language) {\r\n switch (format) {\r\n case 'language-region':\r\n case 'language-script-region':\r\n browserLanguage = navigator.language;\r\n break;\r\n default:\r\n browserLanguage = navigator.language.split('-')[0];\r\n }\r\n }\r\n return browserLanguage;\r\n}\r\n\r\nexport function getSchema(schema: L10nSchema[], language: string, format: L10nFormat): L10nSchema | undefined {\r\n const element = schema.find(item => formatLanguage(item.locale.language, format) === language);\r\n return element;\r\n}\r\n\r\nexport function getValue(key: string, data: { [key: string]: any }, keySeparator: string): string | any | null {\r\n if (data) {\r\n if (keySeparator) {\r\n return key.split(keySeparator).reduce((acc, cur) => (acc && acc[cur]) != null ? acc[cur] : null, data);\r\n }\r\n return data[key] != null ? data[key] : null;\r\n }\r\n return null;\r\n}\r\n\r\nexport function handleParams(value: string, params: any): string {\r\n const replacedValue = value.replace(/{{\\s?([^{}\\s]*)\\s?}}/g, (substring: string, parsedKey: string) => {\r\n const replacer = params[parsedKey];\r\n return replacer !== undefined ? replacer : substring;\r\n });\r\n return replacedValue;\r\n}\r\n\r\nexport function mergeDeep(target: { [key: string]: any }, source: { [key: string]: any }): any {\r\n const output = Object.assign({}, target);\r\n\r\n if (isObject(target) && isObject(source)) {\r\n Object.keys(source).forEach((key) => {\r\n if (isObject(source[key])) {\r\n if (!(key in target)) {\r\n Object.assign(output, { [key]: source[key] });\r\n } else {\r\n output[key] = mergeDeep(target[key], source[key]);\r\n }\r\n } else {\r\n Object.assign(output, { [key]: source[key] });\r\n }\r\n });\r\n }\r\n\r\n return output;\r\n}\r\n\r\nexport function toNumber(value: any): number {\r\n const parsedValue = typeof value === 'string' && !isNaN(+value - parseFloat(value)) ? +value : value;\r\n return parsedValue;\r\n}\r\n\r\nexport function toDate(value: any): Date {\r\n if (isDate(value)) {\r\n return value;\r\n }\r\n\r\n if (typeof value === 'number' && !isNaN(value)) {\r\n return new Date(value);\r\n }\r\n if (typeof value === 'string') {\r\n value = value.trim();\r\n if (!isNaN(value - parseFloat(value))) {\r\n return new Date(parseFloat(value));\r\n }\r\n if (/^(\\d{4}-\\d{1,2}-\\d{1,2})$/.test(value)) {\r\n const [y, m, d] = value.split('-').map((val: string) => +val);\r\n return new Date(y, m - 1, d);\r\n }\r\n const match = value.match(/^(\\d{4})-?(\\d\\d)-?(\\d\\d)(?:T(\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:\\.(\\d+))?)?)?(Z|([+-])(\\d\\d):?(\\d\\d))?)?$/);\r\n if (match) {\r\n return isoStringToDate(match);\r\n }\r\n }\r\n\r\n const date = new Date(value as any);\r\n if (!isDate(date)) {\r\n throw l10nError(toDate, 'Invalid date');\r\n }\r\n return date;\r\n}\r\n\r\nexport const PARSE_DATE_STYLE: { [format: string]: any } = {\r\n full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },\r\n long: { year: 'numeric', month: 'long', day: 'numeric' },\r\n medium: { year: 'numeric', month: 'short', day: 'numeric' },\r\n short: { year: '2-digit', month: 'numeric', day: 'numeric' }\r\n};\r\n\r\nexport const PARSE_TIME_STYLE: { [format: string]: any } = {\r\n full: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' },\r\n long: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' },\r\n medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' },\r\n short: { hour: 'numeric', minute: 'numeric' }\r\n};\r\n\r\nexport function parseDigits(digits: string) {\r\n const groups = digits.match(/^(\\d+)?\\.((\\d+)(\\-(\\d+))?)?$/);\r\n if (groups == null) throw l10nError(parseDigits, 'Invalid digits');\r\n\r\n return {\r\n minimumIntegerDigits: groups[1] ? parseInt(groups[1]) : undefined,\r\n minimumFractionDigits: groups[3] ? parseInt(groups[3]) : undefined,\r\n maximumFractionDigits: groups[5] ? parseInt(groups[5]) : undefined,\r\n };\r\n}\r\n\r\nfunction isObject(item: any): boolean {\r\n return typeof item === 'object' && !Array.isArray(item);\r\n}\r\n\r\nfunction isDate(value: any): value is Date {\r\n return value instanceof Date && !isNaN(value.valueOf());\r\n}\r\n\r\n/**\r\n * Converts a date in ISO 8601 to a Date.\r\n */\r\nfunction isoStringToDate(match: RegExpMatchArray): Date {\r\n const date = new Date(0);\r\n let tzHour = 0;\r\n let tzMin = 0;\r\n const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear;\r\n const timeSetter = match[8] ? date.setUTCHours : date.setHours;\r\n if (match[9]) {\r\n tzHour = Number(match[9] + match[10]);\r\n tzMin = Number(match[9] + match[11]);\r\n }\r\n dateSetter.call(date, Number(match[1]), Number(match[2]) - 1, Number(match[3]));\r\n const h = Number(match[4] || 0) - tzHour;\r\n const m = Number(match[5] || 0) - tzMin;\r\n const s = Number(match[6] || 0);\r\n const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);\r\n timeSetter.call(date, h, m, s, ms);\r\n return date;\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Observable } from 'rxjs';\r\nimport { shareReplay } from 'rxjs/operators';\r\n\r\n@Injectable() export class L10nCache {\r\n\r\n private cache: { [key: string]: Observable<any> } = {};\r\n\r\n public read(key: string, request: Observable<any>): Observable<any> {\r\n if (this.cache[key]) return this.cache[key];\r\n\r\n const response = request.pipe(\r\n shareReplay(1)\r\n );\r\n\r\n this.cache[key] = response;\r\n return response;\r\n }\r\n\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\nimport { L10nLocale } from '../models/types';\r\n\r\n/**\r\n * Implement this class-interface to create a storage for the locale.\r\n */\r\n@Injectable() export abstract class L10nStorage {\r\n\r\n /**\r\n * This method must contain the logic to read the storage.\r\n * @return A promise with the value of the locale\r\n */\r\n public abstract read(): Promise<L10nLocale | null>;\r\n\r\n /**\r\n * This method must contain the logic to write the storage.\r\n * @param locale The current locale\r\n */\r\n public abstract write(locale: L10nLocale): Promise<void>;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultStorage implements L10nStorage {\r\n\r\n public async read(): Promise<L10nLocale | null> {\r\n return Promise.resolve(null);\r\n }\r\n\r\n public async write(locale: L10nLocale): Promise<void> { }\r\n\r\n}\r\n","import { Inject, Injectable } from '@angular/core';\r\n\r\nimport { L10N_CONFIG, L10nConfig } from '../models/l10n-config';\r\nimport { getBrowserLanguage, getSchema } from '../models/utils';\r\nimport { L10nLocale } from '../models/types';\r\n\r\n/**\r\n * Implement this class-interface to resolve the locale.\r\n */\r\n@Injectable() export abstract class L10nLocaleResolver {\r\n\r\n /**\r\n * This method must contain the logic to get the locale.\r\n * @return The locale\r\n */\r\n public abstract get(): Promise<L10nLocale | null>;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultLocaleResolver implements L10nLocaleResolver {\r\n\r\n constructor(@Inject(L10N_CONFIG) private config: L10nConfig) { }\r\n\r\n public async get(): Promise<L10nLocale | null> {\r\n const browserLanguage = getBrowserLanguage(this.config.format);\r\n if (browserLanguage) {\r\n const schema = getSchema(this.config.schema, browserLanguage, this.config.format);\r\n if (schema) {\r\n return Promise.resolve(schema.locale);\r\n }\r\n }\r\n return Promise.resolve(null);\r\n }\r\n\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Observable, of, throwError } from 'rxjs';\r\n\r\nimport { L10nProvider } from '../models/types';\r\nimport { l10nError } from '../models/l10n-error';\r\n\r\n/**\r\n * Implement this class-interface to create a loader of translation data.\r\n */\r\n@Injectable() export abstract class L10nTranslationLoader {\r\n\r\n /**\r\n * This method must contain the logic to get translation data.\r\n * @param language The current language\r\n * @param provider The provider of the translations data\r\n * @return An object of translation data for the language: {key: value}\r\n */\r\n public abstract get(language: string, provider: L10nProvider): Observable<{ [key: string]: any }>;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultTranslationLoader implements L10nTranslationLoader {\r\n\r\n public get(language: string, provider: L10nProvider): Observable<{ [key: string]: any }> {\r\n return provider.asset[language] ?\r\n of(provider.asset[language]) :\r\n throwError(() => l10nError(L10nDefaultTranslationLoader, 'Asset not found'));\r\n }\r\n\r\n}\r\n","import { Injectable, Inject } from '@angular/core';\r\nimport { Observable } from 'rxjs';\r\n\r\nimport { L10nProvider } from '../models/types';\r\nimport { L10N_CONFIG, L10nConfig } from '../models/l10n-config';\r\nimport { L10nCache } from './l10n-cache';\r\nimport { L10nTranslationLoader } from './l10n-translation-loader';\r\n\r\n/**\r\n * Implement this class-interface to create a translation fallback.\r\n */\r\n@Injectable() export abstract class L10nTranslationFallback {\r\n\r\n /**\r\n * This method must contain the logic to get the ordered loaders.\r\n * @param language The current language\r\n * @param provider The provider of the translations data\r\n * @return An array of loaders\r\n */\r\n public abstract get(language: string, provider: L10nProvider): Observable<any>[];\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultTranslationFallback implements L10nTranslationFallback {\r\n\r\n constructor(\r\n @Inject(L10N_CONFIG) private config: L10nConfig,\r\n private cache: L10nCache,\r\n private translationLoader: L10nTranslationLoader\r\n ) { }\r\n\r\n /**\r\n * Translation data will be merged in the following order:\r\n * 'language'\r\n * 'language[-script]'\r\n * 'language[-script][-region]'\r\n */\r\n public get(language: string, provider: L10nProvider): Observable<any>[] {\r\n const loaders: Observable<any>[] = [];\r\n const keywords = language.match(/-?[a-zA-z]+/g) || [];\r\n let fallbackLanguage = '';\r\n for (const keyword of keywords) {\r\n fallbackLanguage += keyword;\r\n if (this.config.cache) {\r\n loaders.push(\r\n this.cache.read(`${provider.name}-${fallbackLanguage}`,\r\n this.translationLoader.get(fallbackLanguage, provider))\r\n );\r\n } else {\r\n loaders.push(this.translationLoader.get(fallbackLanguage, provider));\r\n }\r\n }\r\n return loaders;\r\n }\r\n\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\nimport { handleParams } from '../models/utils';\r\n\r\n/**\r\n * Implement this class-interface to create an handler for translated values.\r\n */\r\n@Injectable() export abstract class L10nTranslationHandler {\r\n\r\n /**\r\n * This method must contain the logic to parse the translated value.\r\n * @param key The key that has been requested\r\n * @param params The parameters passed along with the key\r\n * @param value The translated value\r\n * @return The parsed value\r\n */\r\n public abstract parseValue(key: string, params: any, value: any): string | any;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultTranslationHandler implements L10nTranslationHandler {\r\n\r\n public parseValue(key: string, params: any, value: any): string | any {\r\n if (params) return handleParams(value, params);\r\n return value;\r\n }\r\n\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\n/**\r\n * Implement this class-interface to create an handler for missing values.\r\n */\r\n@Injectable() export abstract class L10nMissingTranslationHandler {\r\n\r\n /**\r\n * This method must contain the logic to handle missing values.\r\n * @param key The key that has been requested\r\n * @param value Null or empty string\r\n * @param params Optional parameters contained in the key\r\n * @return The value\r\n */\r\n public abstract handle(key: string, value?: string, params?: any): string | any;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultMissingTranslationHandler implements L10nMissingTranslationHandler {\r\n\r\n public handle(key: string, value?: string, params?: any): string | any {\r\n return key;\r\n }\r\n\r\n}\r\n","import { Injectable, Inject, Optional } from '@angular/core';\r\nimport { Observable, BehaviorSubject, merge, concat } from 'rxjs';\r\n\r\nimport { L10nLocale, L10nProvider } from '../models/types';\r\nimport { L10N_CONFIG, L10nConfig, L10N_LOCALE } from '../models/l10n-config';\r\nimport { formatLanguage, getSchema, getValue, mergeDeep } from '../models/utils';\r\nimport { L10nCache } from './l10n-cache';\r\nimport { L10nStorage } from './l10n-storage';\r\nimport { L10nLocaleResolver } from './l10n-locale-resolver';\r\nimport { L10nTranslationFallback } from './l10n-translation-fallback';\r\nimport { L10nTranslationLoader } from './l10n-translation-loader';\r\nimport { L10nTranslationHandler } from './l10n-translation-handler';\r\nimport { L10nMissingTranslationHandler } from './l10n-missing-translation-handler';\r\n\r\n@Injectable() export class L10nTranslationService {\r\n\r\n /**\r\n * The translation data: {language: {key: value}}\r\n */\r\n public data: { [key: string]: any } = {};\r\n\r\n private translation = new BehaviorSubject<L10nLocale>(this.locale);\r\n\r\n private error = new BehaviorSubject<any>(null);\r\n\r\n constructor(\r\n @Inject(L10N_CONFIG) private config: L10nConfig,\r\n @Inject(L10N_LOCALE) private locale: L10nLocale,\r\n private cache: L10nCache,\r\n private storage: L10nStorage,\r\n private resolveLocale: L10nLocaleResolver,\r\n private translationFallback: L10nTranslationFallback,\r\n private translationLoader: L10nTranslationLoader,\r\n private translationHandler: L10nTranslationHandler,\r\n private missingTranslationHandler: L10nMissingTranslationHandler\r\n ) { }\r\n\r\n /**\r\n * Gets the current locale.\r\n */\r\n public getLocale(): L10nLocale {\r\n return this.locale;\r\n }\r\n\r\n /**\r\n * Changes the current locale and load the translation data.\r\n * @param locale The new locale\r\n */\r\n public async setLocale(locale: L10nLocale): Promise<void> {\r\n await this.loadTranslations(this.config.providers, locale);\r\n }\r\n\r\n /**\r\n * Fired every time the translation data has been loaded. Returns the locale.\r\n */\r\n public onChange(): Observable<L10nLocale> {\r\n return this.translation.asObservable();\r\n }\r\n\r\n /**\r\n * Fired when the translation data could not been loaded. Returns the error.\r\n */\r\n public onError(): Observable<any> {\r\n return this.error.asObservable();\r\n }\r\n\r\n /**\r\n * Translates a key or an array of keys.\r\n * @param keys The key or an array of keys to be translated\r\n * @param params Optional parameters contained in the key\r\n * @param language The current language\r\n * @return The translated value or an object: {key: value}\r\n */\r\n public translate(\r\n keys: string | string[],\r\n params?: any,\r\n language = this.locale.language\r\n ): string | any {\r\n language = formatLanguage(language, this.config.format);\r\n\r\n if (Array.isArray(keys)) {\r\n const data: { [key: string]: any } = {};\r\n for (const key of keys) {\r\n data[key] = this.translate(key, params, language);\r\n }\r\n return data;\r\n }\r\n\r\n const value = getValue(keys, this.data[language], this.config.keySeparator);\r\n\r\n return value ? this.translationHandler.parseValue(keys, params, value) : this.missingTranslationHandler.handle(keys, value, params);\r\n }\r\n\r\n /**\r\n * Checks if a translation exists.\r\n * @param key The key to be tested\r\n * @param language The current language\r\n */\r\n public has(key: string, language = this.locale.language): boolean {\r\n language = formatLanguage(language, this.config.format);\r\n\r\n return getValue(key, this.data[language], this.config.keySeparator) !== null;\r\n }\r\n\r\n /**\r\n * Gets the language direction.\r\n */\r\n public getLanguageDirection(language = this.locale.language): 'ltr' | 'rtl' | undefined {\r\n const schema = getSchema(this.config.schema, language, this.config.format);\r\n return schema ? schema.dir : undefined;\r\n }\r\n\r\n /**\r\n * Gets available languages.\r\n */\r\n public getAvailableLanguages(): string[] {\r\n const languages = this.config.schema.map(item => formatLanguage(item.locale.language, this.config.format));\r\n return languages;\r\n }\r\n\r\n /**\r\n * Initializes the service\r\n * @param providers An array of L10nProvider\r\n */\r\n public async init(providers: L10nProvider[] = this.config.providers): Promise<void> {\r\n let locale: L10nLocale | null = null;\r\n\r\n // Tries to get locale from storage.\r\n if (locale == null) {\r\n locale = await this.storage.read();\r\n }\r\n // Tries resolved locale.\r\n if (locale == null) {\r\n locale = await this.resolveLocale.get();\r\n }\r\n // Uses default locale.\r\n if (locale == null) {\r\n locale = this.config.defaultLocale;\r\n }\r\n\r\n // Loads translation data.\r\n await this.loadTranslations(providers, locale);\r\n }\r\n\r\n /**\r\n * Can be called at every translation change.\r\n * @param providers An array of L10nProvider\r\n * @param locale The current locale\r\n */\r\n public async loadTranslations(providers: L10nProvider[] = this.config.providers, locale = this.locale): Promise<void> {\r\n const language = formatLanguage(locale.language, this.config.format);\r\n\r\n return new Promise((resolve) => {\r\n concat(...this.getTranslation(providers, language)).subscribe({\r\n next: (data) => this.addData(data, language),\r\n error: (error) => {\r\n this.handleError(error);\r\n resolve();\r\n },\r\n complete: () => {\r\n this.releaseTranslation(locale);\r\n resolve();\r\n }\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Can be called to add translation data.\r\n * @param data The translation data {key: value}\r\n * @param language The language to add data\r\n */\r\n public addData(data: { [key: string]: any }, language: string): void {\r\n this.data[language] = this.data[language] !== undefined\r\n ? mergeDeep(this.data[language], data)\r\n : data;\r\n }\r\n\r\n /**\r\n * Adds providers to configuration\r\n * @param providers The providers of the translations data\r\n */\r\n public addProviders(providers: L10nProvider[]): void {\r\n providers.forEach(provider => {\r\n if (!this.config.providers.find(p => p.name === provider.name)) {\r\n this.config.providers.push(provider);\r\n }\r\n });\r\n }\r\n\r\n private getTranslation(providers: L10nProvider[], language: string): Observable<any>[] {\r\n const lazyLoaders: Observable<any>[] = [];\r\n let loaders: Observable<any>[] = [];\r\n\r\n for (const provider of providers) {\r\n if (this.config.fallback) {\r\n loaders = loaders.concat(this.translationFallback.get(language, provider));\r\n } else {\r\n if (this.config.cache) {\r\n lazyLoaders.push(\r\n this.cache.read(`${provider.name}-${language}`, this.translationLoader.get(language, provider))\r\n );\r\n } else {\r\n lazyLoaders.push(this.translationLoader.get(language, provider));\r\n }\r\n }\r\n }\r\n loaders.push(merge(...lazyLoaders));\r\n\r\n return loaders;\r\n }\r\n\r\n private handleError(error: any): void {\r\n this.error.next(error);\r\n }\r\n\r\n private releaseTranslation(locale: L10nLocale): void {\r\n Object.assign(this.locale, locale);\r\n this.translation.next(this.locale);\r\n this.storage.write(this.locale);\r\n }\r\n\r\n}\r\n","import { Injectable, OnDestroy, ChangeDetectorRef, inject } from '@angular/core';\r\nimport { Subscription } from 'rxjs';\r\n\r\nimport { L10nTranslationService } from '../services/l10n-translation.service';\r\n\r\n@Injectable()\r\nexport class L10nAsyncPipe implements OnDestroy {\r\n\r\n protected onChanges: Subscription;\r\n\r\n protected translation = inject(L10nTranslationService);\r\n protected cdr = inject(ChangeDetectorRef);\r\n\r\n constructor() {\r\n this.onChanges = this.translation.onChange().subscribe({\r\n next: () => this.cdr.markForCheck()\r\n });\r\n }\r\n\r\n ngOnDestroy() {\r\n if (this.onChanges) this.onChanges.unsubscribe();\r\n }\r\n\r\n}\r\n","/**\r\n * Breadth First Search (BFS) algorithm for traversing & searching tree data structure of DOM\r\n * explores the neighbor nodes first, before moving to the next level neighbors.\r\n * Time complexity: between O(1) and O(|V|^2).\r\n */\r\nexport function getTargetNode(rootNode: HTMLElement): HTMLElement {\r\n return walk(rootNode);\r\n}\r\n\r\nconst MAX_DEPTH = 10;\r\n\r\nfunction walk(rootNode: HTMLElement): HTMLElement {\r\n const queue: HTMLElement[] = [];\r\n\r\n let iNode: HTMLElement;\r\n let depth = 0;\r\n let nodeToDepthIncrease = 1;\r\n\r\n queue.push(rootNode);\r\n while (queue.length > 0 && depth <= MAX_DEPTH) {\r\n iNode = queue.splice(0, 1)[0];\r\n if (isTargetNode(iNode)) return iNode;\r\n if (depth < MAX_DEPTH && iNode.childNodes) {\r\n for (const child of Array.from(iNode.childNodes)) {\r\n if (isValidNode(child as HTMLElement)) {\r\n queue.push(child as HTMLElement);\r\n }\r\n }\r\n }\r\n if (--nodeToDepthIncrease === 0) {\r\n depth++;\r\n nodeToDepthIncrease = queue.length;\r\n }\r\n }\r\n return rootNode;\r\n}\r\n\r\nfunction isTargetNode(node: HTMLElement): boolean {\r\n return typeof node !== 'undefined' && node.nodeType === 3 && node.nodeValue != null && node.nodeValue.trim() !== '';\r\n}\r\n\r\n/**\r\n * A valid node is not marked for translation.\r\n */\r\nfunction isValidNode(node: HTMLElement): boolean {\r\n if (typeof node !== 'undefined' && node.nodeType === 1 && node.attributes) {\r\n for (const attr of Array.from(node.attributes)) {\r\n if (attr && /^l10n|translate/.test(attr.name)) return false;\r\n }\r\n }\r\n return true;\r\n}\r\n","import { Directive, Input, AfterViewInit, OnChanges, OnDestroy, ElementRef, Renderer2, inject, Injectable } from '@angular/core';\r\nimport { Subject } from 'rxjs';\r\nimport { takeUntil } from 'rxjs/operators';\r\n\r\nimport { getTargetNode } from './bfs';\r\nimport { L10nTranslationService } from '../services/l10n-translation.service';\r\n\r\n@Directive()\r\nexport abstract class L10nDirective implements AfterViewInit, OnChanges, OnDestroy {\r\n\r\n @Input() public value?: string;\r\n\r\n @Input() set innerHTML(content: any) {\r\n // Handle TrustedHTML\r\n this.content = content.toString();\r\n }\r\n\r\n @Input() public language?: string;\r\n\r\n protected el = inject(ElementRef);\r\n protected renderer = inject(Renderer2);\r\n protected translation = inject(L10nTranslationService);\r\n\r\n private content?: string;\r\n\r\n private text?: string;\r\n private attributes: any[] = [];\r\n\r\n private element?: HTMLElement;\r\n private renderNode?: HTMLElement;\r\n private nodeValue?: string;\r\n\r\n private textObserver?: MutationObserver;\r\n\r\n private destroy = new Subject<boolean>();\r\n\r\n public ngAfterViewInit(): void {\r\n if (this.el && this.el.nativeElement) {\r\n this.element = this.el.nativeElement;\r\n this.renderNode = getTargetNode(this.el.nativeElement);\r\n this.text = this.getText();\r\n this.attributes = this.getAttributes();\r\n this.addTextListener();\r\n\r\n if (this.language) {\r\n this.replaceText();\r\n this.replaceAttributes();\r\n } else {\r\n this.addTranslationListener();\r\n }\r\n }\r\n }\r\n\r\n public ngOnChanges(): void {\r\n if (this.text) {\r\n if (this.nodeValue == null || this.nodeValue === '') {\r\n if (this.value) {\r\n this.text = this.value;\r\n } else if (this.content) {\r\n this.text = this.content;\r\n }\r\n }\r\n this.replaceText();\r\n }\r\n if (this.attributes && this.attributes.length > 0) {\r\n this.replaceAttributes();\r\n }\r\n }\r\n\r\n public ngOnDestroy(): void {\r\n this.destroy.next(true);\r\n this.removeTextListener();\r\n }\r\n\r\n protected abstract getValue(text: string): string;\r\n\r\n private getText(): string {\r\n let text = '';\r\n if (this.element && this.element.childNodes.length > 0) {\r\n text = this.getNodeValue();\r\n } else if (this.value) {\r\n text = this.value;\r\n } else if (this.content) {\r\n text = this.content;\r\n }\r\n return text;\r\n }\r\n\r\n private getNodeValue(): string {\r\n this.nodeValue = this.renderNode != null && this.renderNode.nodeValue != null ? this.renderNode.nodeValue : '';\r\n return this.nodeValue ? this.nodeValue.trim() : '';\r\n }\r\n\r\n private getAttributes(): any[] {\r\n const attributes: any[] = [];\r\n if (this.element && this.element.attributes) {\r\n for (const attr of Array.from(this.element.attributes)) {\r\n if (attr && attr.name) {\r\n const [, name = ''] = attr.name.match(/^l10n-(.+)$/) || [];\r\n if (name) {\r\n const targetAttr = Array.from(this.element.attributes).find(a => a.name === name);\r\n if (targetAttr) attributes.push({ name: targetAttr.name, value: targetAttr.value });\r\n }\r\n }\r\n }\r\n }\r\n return attributes;\r\n }\r\n\r\n private addTextListener(): void {\r\n if (typeof MutationObserver !== 'undefined') {\r\n this.textObserver = new MutationObserver(() => {\r\n if (this.element) {\r\n this.renderNode = getTargetNode(this.element);\r\n this.text = this.getText();\r\n this.replaceText();\r\n }\r\n });\r\n if (this.renderNode) {\r\n this.textObserver.observe(this.renderNode, { subtree: true, characterData: true });\r\n }\r\n }\r\n }\r\n\r\n private removeTextListener(): void {\r\n if (this.textObserver) {\r\n this.textObserver.disconnect();\r\n }\r\n }\r\n\r\n private addTranslationListener(): void {\r\n this.translation.onChange().pipe(takeUntil(this.destroy)).subscribe({\r\n next: () => {\r\n this.replaceText();\r\n this.replaceAttributes();\r\n }\r\n });\r\n }\r\n\r\n private replaceText(): void {\r\n if (this.text) {\r\n this.setText(this.getValue(this.text));\r\n }\r\n }\r\n\r\n private replaceAttributes(): void {\r\n if (this.attributes.length > 0) {\r\n this.setAttributes(this.getAttributesValues());\r\n }\r\n }\r\n\r\n private setText(value: string): void {\r\n if (value) {\r\n if (this.nodeValue && this.text) {\r\n this.removeTextListener();\r\n this.renderer.setValue(this.renderNode, this.nodeValue.replace(this.text, value));\r\n this.addTextListener();\r\n } else if (this.value) {\r\n this.renderer.setAttribute(this.element, 'value', value);\r\n } else if (this.content) {\r\n this.renderer.setProperty(this.element, 'innerHTML', value);\r\n }\r\n }\r\n }\r\n\r\n private setAttributes(data: any): void {\r\n for (const attr of this.attributes) {\r\n this.renderer.setAttribute(this.element, attr.name, data[attr.value]);\r\n }\r\n }\r\n\r\n private getAttributesValues(): any {\r\n const values = this.attributes.map(attr => attr.value);\r\n const data: any = {};\r\n for (const value of values) {\r\n data[value] = this.getValue(value);\r\n }\r\n return data;\r\n }\r\n\r\n}\r\n","import { inject } from '@angular/core';\r\nimport { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';\r\n\r\nimport { L10nTranslationService } from '../services/l10n-translation.service';\r\n\r\nexport const resolveL10n: ResolveFn<void> = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {\r\n const translation = inject(L10nTranslationService);\r\n\r\n const providers = route.data['l10nProviders'];\r\n translation.addProviders(providers);\r\n\r\n await translation.loadTranslations(providers);\r\n};\r\n","import { Injectable } from '@angular/core';\r\n\r\nimport { L10nTranslationService } from './l10n-translation.service';\r\n\r\n/**\r\n * Implement this class-interface to init L10n.\r\n */\r\n@Injectable() export abstract class L10nLoader {\r\n\r\n /**\r\n * This method must contain the logic to init L10n.\r\n */\r\n public abstract init(): Promise<void>;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultLoader implements L10nLoader {\r\n\r\n constructor(private translation: L10nTranslationService) { }\r\n\r\n public async init(): Promise<void> {\r\n await this.translation.init();\r\n }\r\n\r\n}\r\n","import { Injectable, Inject } from '@angular/core';\r\n\r\nimport { L10nLocale, L10nDateTimeFormatOptions, L10nNumberFormatOptions } from '../models/types';\r\nimport { L10nConfig, L10N_CONFIG, L10N_LOCALE } from '../models/l10n-config';\r\nimport {\r\n toDate,\r\n toNumber,\r\n PARSE_DATE_STYLE,\r\n PARSE_TIME_STYLE,\r\n parseDigits\r\n} from '../models/utils';\r\nimport { L10nTranslationService } from './l10n-translation.service';\r\n\r\n@Injectable() export class L10nIntlService {\r\n\r\n constructor(\r\n @Inject(L10N_CONFIG) private config: L10nConfig,\r\n @Inject(L10N_LOCALE) private locale: L10nLocale,\r\n private translation: L10nTranslationService\r\n ) { }\r\n\r\n /**\r\n * Formats a date.\r\n * @param value A date, a number (milliseconds since UTC epoch) or an ISO 8601 string\r\n * @param options A L10n or Intl DateTimeFormatOptions object\r\n * @param language The current language\r\n * @param timeZone The current time zone\r\n */\r\n public formatDate(\r\n value: any,\r\n options?: L10nDateTimeFormatOptions,\r\n language = this.locale.dateLanguage || this.locale.language,\r\n timeZone = this.locale.timeZone\r\n ): string {\r\n value = toDate(value);\r\n\r\n let dateTimeFormatOptions: Intl.DateTimeFormatOptions = {};\r\n if (options) {\r\n if (options) {\r\n const { dateStyle, timeStyle, ...rest } = options;\r\n if (dateStyle) {\r\n dateTimeFormatOptions = { ...dateTimeFormatOptions, ...PARSE_DATE_STYLE[dateStyle] };\r\n }\r\n if (timeStyle) {\r\n dateTimeFormatOptions = { ...dateTimeFormatOptions, ...PARSE_TIME_STYLE[timeStyle] };\r\n }\r\n dateTimeFormatOptions = { ...dateTimeFormatOptions, ...rest };\r\n }\r\n }\r\n if (timeZone) {\r\n dateTimeFormatOptions.timeZone = timeZone;\r\n }\r\n\r\n return new Intl.DateTimeFormat(language, dateTimeFormatOptions).format(value);\r\n }\r\n\r\n /**\r\n * Formats a number.\r\n * @param value A number or a string\r\n * @param options A L10n or Intl NumberFormatOptions object\r\n * @param language The current language\r\n * @param currency The current currency\r\n * @param convert An optional function to convert the value, with value and locale in the signature. \r\n * For example:\r\n * ```\r\n * const convert = (value: number, locale: L10nLocale) => { return ... };\r\n * ```\r\n * @param convertParams Optional parameters for the convert function\r\n */\r\n public formatNumber(\r\n value: any,\r\n options?: L10nNumberFormatOptions,\r\n language = this.locale.numberLanguage || this.locale.language,\r\n currency = this.locale.currency,\r\n convert?: (value: number, locale: L10nLocale, params: any) => number,\r\n convertParams?: any\r\n ): string {\r\n if (options && options['style'] === 'unit' && !options['unit']) return value;\r\n\r\n value = toNumber(value);\r\n\r\n // Optional conversion.\r\n if (typeof convert === 'function') {\r\n value = convert(value, this.locale, Object.values(convertParams || {})); // Destructures params\r\n }\r\n\r\n let numberFormatOptions: Intl.NumberFormatOptions = {};\r\n if (options) {\r\n const { digits, ...rest } = options;\r\n if (digits) {\r\n numberFormatOptions = { ...numberFormatOptions, ...parseDigits(digits) };\r\n }\r\n numberFormatOptions = { ...numberFormatOptions, ...rest };\r\n }\r\n if (currency) numberFormatOptions.currency = currency;\r\n\r\n return new Intl.NumberFormat(language, numberFormatOptions).format(value);\r\n }\r\n\r\n /**\r\n * Formats a relative time.\r\n * @param value A negative (or positive) number\r\n * @param unit An Intl RelativeTimeFormatUnit value\r\n * @param options An Intl RelativeTimeFormatOptions object\r\n * @param language The current language\r\n */\r\n public formatRelativeTime(\r\n value: any,\r\n unit: Intl.RelativeTimeFormatUnit,\r\n options?: Intl.RelativeTimeFormatOptions,\r\n language = this.locale.dateLanguage || this.locale.language\r\n ): string {\r\n value = toNumber(value);\r\n\r\n return new Intl.RelativeTimeFormat(language, options).format(value, unit);\r\n }\r\n\r\n /**\r\n * Gets the plural by a number.\r\n * The 'value' is passed as a parameter to the translation function.\r\n * @param value The number to get the plural\r\n * @param prefix Optional prefix for the key\r\n * @param options An Intl PluralRulesOptions object\r\n * @param language The current language\r\n */\r\n public plural(value: any, prefix = '', options?: Intl.PluralRulesOptions, language = this.locale.language): string {\r\n value = toNumber(value);\r\n\r\n const rule = new Intl.PluralRules(language, options).select(value);\r\n\r\n const key = prefix ? `${prefix}${this.config.keySeparator}${rule}` : rule;\r\n\r\n return this.translation.translate(key, { value });\r\n }\r\n\r\n /**\r\n * Returns translation of language, region, script or currency display names\r\n * @param code ISO code of language, region, script or currency\r\n * @param options An Intl DisplayNamesOptions object\r\n * @param language The current language\r\n */\r\n public displayNames(code: string, options: Intl.DisplayNamesOptions, language = this.locale.language): string {\r\n return new Intl.DisplayNames(language, options).of(code) || code;\r\n }\r\n\r\n public getCurrencySymbol(locale = this.locale): string | undefined {\r\n const decimal = this.formatNumber(0, { digits: '1.0-0' }, locale.numberLanguage || locale.language);\r\n const currency = this.formatNumber(\r\n 0,\r\n { digits: '1.0-0', style: 'currency', currencyDisplay: 'symbol' },\r\n locale.numberLanguage || locale.language,\r\n locale.currency\r\n );\r\n let symbol = currency.replace(decimal, '');\r\n symbol = symbol.trim();\r\n\r\n return symbol;\r\n }\r\n\r\n /**\r\n * Compares two keys by the value of translation.\r\n * @param key1 First key to compare\r\n * @param key1 Second key to compare\r\n * @param options An Intl CollatorOptions object\r\n * @param language The current language\r\n * @return A negative value if the value of translation of key1 comes before the value of translation of key2;\r\n * a positive value if key1 comes after key2;\r\n * 0 if they are considered equal or Intl.Collator is not supported\r\n */\r\n public compare(key1: string, key2: string, options?: Intl.CollatorOptions, language = this.locale.language): number {\r\n const value1 = this.translation.translate(key1);\r\n const value2 = this.translation.translate(key2);\r\n\r\n return new Intl.Collator(language, options).compare(value1, value2);\r\n }\r\n\r\n /**\r\n * Returns the representation of a list.\r\n * @param list An array of keys\r\n * @param options An Intl ListFormatOptions object\r\n * @param language The current language\r\n */\r\n public list(list: string[], options?: Intl.ListFormatOptions, language = this.locale.language): string {\r\n const values = list.map(key => this.translation.translate(key));\r\n if (language == null || language === '') return values.join(', ');\r\n\r\n return new Intl.ListFormat(language, options).format(values);\r\n }\r\n\r\n}\r\n","import { Injectable, Inject } from '@angular/core';\r\n\r\nimport { L10nNumberFormatOptions, L10nDateTimeFormatOptions, L10nLocale } from '../models/types';\r\nimport { L10N_LOCALE } from '../models/l10n-config';\r\n\r\n/**\r\n * Implement this class-interface to create a validation service.\r\n */\r\n@Injectable() export abstract class L10nValidation {\r\n\r\n /**\r\n * This method must contain the logic to convert a string to a number.\r\n * @param value The string to be parsed\r\n * @param options A L10n or Intl NumberFormatOptions object\r\n * @param language The current language\r\n * @return The parsed number\r\n */\r\n public abstract parseNumber(\r\n value: string,\r\n options?: L10nNumberFormatOptions,\r\n language?: string\r\n ): number | null;\r\n\r\n /**\r\n * This method must contain the logic to convert a string to a date.\r\n * @param value The string to be parsed\r\n * @param options A L10n or Intl DateTimeFormatOptions object\r\n * @param language The current language\r\n * @return The parsed date\r\n */\r\n public abstract parseDate(\r\n value: string,\r\n options?: L10nDateTimeFormatOptions,\r\n language?: string\r\n ): Date | null;\r\n\r\n}\r\n\r\n@Injectable() export class L10nDefaultValidation {\r\n\r\n constructor(@Inject(L10N_LOCALE) private locale: L10nLocale) { }\r\n\r\n public parseNumber(\r\n value: string,\r\n options?: L10nNumberFormatOptions,\r\n language = this.locale.numberLanguage || this.locale.language\r\n ): number | null {\r\n return null;\r\n }\r\n\r\n public parseDate(\r\n value: string,\r\n options?: L10nDateTimeFormatOptions,\r\n language = this.locale.dateLanguage || this.locale.language\r\n ): Date | null {\r\n return null;\r\n }\r\n\r\n}\r\n","import { L10nTranslationService } from '../services/l10n-translation.service';\r\n\r\nexport function initL10n(translation: L10nTranslationService): () => Promise<void> {\r\n return () => translation.init();\r\n}\r\n","import { APP_INITIALIZER, EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\r\n\r\nimport { L10N_CONFIG, L10N_LOCALE, L10nConfig, L10nTranslationToken, L10nValidationToken } from '../models/l10n-config';\r\nimport { L10nCache } from '../services/l10n-cache';\r\nimport { L10nLoader, L10nDefaultLoader } from '../services/l10n-loader';\r\nimport { L10nMissingTranslationHandler, L10nDefaultMissingTranslationHandler } from '../services/l10n-missing-translation-handler';\r\nimport { L10nLocaleResolver, L10nDefaultLocaleResolver } from '../services/l10n-locale-resolver';\r\nimport { L10nStorage, L10nDefaultStorage } from '../services/l10n-storage';\r\nimport { L10nTranslationFallback, L10nDefaultTranslationFallback } from '../services/l10n-translation-fallback';\r\nimport { L10nTranslationHandler, L1