@bespunky/angular-zen
Version:
The Angular tools you always wished were there.
1 lines • 59.3 kB
Source Map (JSON)
{"version":3,"file":"bespunky-angular-zen-language.mjs","sources":["../../../../libs/angular-zen/language/src/config/language-integration-config.ts","../../../../libs/angular-zen/language/src/services/language-integration.service.ts","../../../../libs/angular-zen/language/src/url-localization/localizers/url-localizer.ts","../../../../libs/angular-zen/language/src/url-localization/config/url-localization-config.ts","../../../../libs/angular-zen/language/src/url-localization/localizers/route-position-url-localizer.ts","../../../../libs/angular-zen/language/src/url-localization/localizers/query-params-url-localizer.ts","../../../../libs/angular-zen/language/src/url-localization/localizers/noop-url-localizer.ts","../../../../libs/angular-zen/language/src/url-localization/config/url-localization.provider.ts","../../../../libs/angular-zen/language/src/config/language-integration.provider.ts","../../../../libs/angular-zen/language/src/language-integration.module.ts","../../../../libs/angular-zen/language/src/services/localized-route-aware.service.ts","../../../../libs/angular-zen/language/src/url-localization/services/url-localization.service.ts","../../../../libs/angular-zen/language/src/bespunky-angular-zen-language.ts"],"sourcesContent":["import { Observable } from 'rxjs';\nimport { InjectionToken } from '@angular/core';\n\nimport { SupportedLanguagesFactory, DefaultLanguageFactory, TranslationFn, ObservableLike } from './language-integration-types';\n\n/**\n * Configuration for integrating language services into a library.\n * Used in `LanguageIntegrationModule.forRoot()`.\n *\n * @export\n * @interface LanguageIntegrationConfig\n */\nexport interface LanguageIntegrationConfig\n{\n /**\n * The languages supported by the integrated app (e.g. 'en', 'fr', 'en-US', 'es-CL').\n * This can be a local array or a resolvable async factory which returns the language names.\n * \n * When providing a factory returning an observable, it is the app's responsability to provide an auto completing observable.\n * A common way would be to pipe `take(1)` to make the observable complete after one emission.\n * \n * Observables left open will cause memory leaks in the language integration service.\n * \n * @type {(string[] | SupportedLanguagesFactory)}\n */\n supported: string[] | SupportedLanguagesFactory;\n /**\n * The default language used by the integrated app (e.g. 'en', 'fr', 'en-US', 'es-CL').\n * This can be a local array or a resolvable async factory which returns the language names.\n *\n * When providing a factory returning an observable, it is the app's responsability to provide an auto completing observable.\n * A common way would be to pipe `take(1)` to make the observable complete after one emission.\n * \n * Observables left open will cause memory leaks in the language integration service.\n *\n * @type {(string | DefaultLanguageFactory)}\n */\n default: string | DefaultLanguageFactory;\n /**\n * (Optional) A resolvable async object which emits once, when the intgrated language services are ready for operation.\n * \n * Provide a value if you need to control and delay the execution of language related operations.\n * \n * @type {ObservableLike<any>}\n */\n ready?: ObservableLike<any>;\n /**\n * A subscribable object which emits every time the integrated app has changed the current language.\n * The observable should emit the new language name (e.g. 'en', 'fr', 'en-US', 'es-CL').\n *\n * @type {Observable<string>}\n */\n changed: Observable<string>;\n /**\n * The function used for live translation.\n *\n * @type {TranslationFn}\n */\n translate: TranslationFn;\n}\n\n/**\n * An injection token representing the the global language integration configuration provided by an app to its libraries.\n * Provided by `LanguageIntegrationModule.forRoot()`.\n */\nexport const LanguageIntegration = new InjectionToken<LanguageIntegrationConfig>('LanguageIntegration.Config');\n","/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport { from, of, Observable } from 'rxjs';\nimport { Inject, Injectable, Optional } from '@angular/core';\n\nimport { Destroyable } from '@bespunky/angular-zen/core';\nimport { access } from '@bespunky/angular-zen/utils';\nimport { LanguageIntegrationConfig, LanguageIntegration } from '../config/language-integration-config';\n\n/**\n * Uses the language integration configuration provided by an app to provide language services for a library.\n * @see `LanguageIntegrationModule.forRoot()`.\n * \n * @export\n * @class LanguageIntegrationService\n * @extends {Destroyable}\n */\n@Injectable({ providedIn: 'root' })\nexport class LanguageIntegrationService extends Destroyable\n{\n private $ready! : Observable<void>;\n private defaultLang? : string;\n private supportedLangs?: string[];\n private currentLang? : string;\n\n /**\n * Creates an instance of LanguageIntegrationService.\n * \n * @param {LanguageIntegrationConfig} [config] The language integration configuration provided using `LanguageIntegrationModule.forRoot()`.\n */\n constructor(@Optional() @Inject(LanguageIntegration) public readonly config?: LanguageIntegrationConfig)\n {\n super();\n\n this.initReadyObservable();\n\n if (config) this.initMultiLanguageSupport();\n }\n \n private initReadyObservable(): void\n {\n const ready = this.config?.ready;\n\n // Using of() instead of EMPTY as EMPTY only calls `complete` but not `next`.\n // This allows users to subscribe more intuitively.\n this.$ready = ready ? from(ready) : of();\n }\n\n private initMultiLanguageSupport(): void\n {\n this.subscribe(this.config!.changed, lang => this.currentLang = lang);\n\n // User's responsability to provide a completing observables.\n this.loadDefaultLanguage ().subscribe(defaultLang => this.defaultLang = defaultLang);\n this.loadSupportedLanguages().subscribe(languages => this.supportedLangs = languages);\n }\n\n private loadDefaultLanguage(): Observable<string>\n {\n const defaultLang = this.config!.default;\n \n return typeof defaultLang === 'string' ? of(defaultLang) : from(defaultLang());\n }\n\n private loadSupportedLanguages(): Observable<string[]>\n {\n const supported = this.config!.supported;\n \n return Array.isArray(supported) ? of(supported) : from(supported());\n }\n\n /**\n * A subscribable event emitting every time the integrated app changes a language.\n * The new language name is emitted with each change.\n *\n * This will be `undefined` if the language integration module hasn't been imported by the app.\n * \n * @readonly\n * @type {Observable<string> | undefined}\n */\n public get changed(): Observable<string> | undefined\n {\n return this.config?.changed;\n }\n\n /**\n * The default language used by the integrated app.\n *\n * This will be `undefined` in the following cases:\n * 1. The language integration module hasn't been imported by the app.\n * 2. The default language hasn't resolved yet.\n * \n * @readonly\n * @type {string | undefined}\n */\n public get default(): string | undefined\n {\n return this.defaultLang;\n }\n\n /**\n * The languages supported by the integrated app.\n *\n * This will be `undefined` in the following cases:\n * 1. The language integration module hasn't been imported by the app.\n * 2. Supported languages haven't resolved yet.\n *\n * @readonly\n * @type {string[] | undefined}\n */\n public get supported(): string[] | undefined\n {\n return this.supportedLangs;\n }\n\n /**\n * The current language used by the integrated app.\n *\n * This will be `undefined` in the following cases:\n * 1. The language integration module hasn't been imported by the app.\n * 2. The `changed` event hasn't emitted yet.\n *\n * @readonly\n * @type {string | undefined}\n */\n public get current(): string | undefined\n {\n return this.currentLang;\n }\n \n /**\n * Indicated whether the language integration module has been imported into the app.\n * If this is `false`, this service will serve no purpose.\n *\n * @readonly\n * @type {boolean}\n */\n public get enabled()\n {\n return !!(this.config);\n }\n\n /**\n * A resolvable async object which emits once when the intgrated language services are ready for operation.\n * \n * This will complete immediately if the the language integration module hasn't been imported, or a `ready` observable hasn't\n * been provided when importing the module.\n *\n * @readonly\n * @type {Observable<void>}\n */\n public get ready(): Observable<void>\n {\n return this.$ready;\n }\n\n /**\n * Retrieves the list of alternative languages to the specified language supported by the integrated app.\n *\n * @param {string} lang The language for which to get the alternative languages.\n * @returns {string[]} An array of alternative languages supported by the integrated app.\n * @throws If the language integration module hasn't been imported into the app.\n */\n public alternateLanguagesFor(lang: string): string[]\n {\n this.ensureEnabled();\n \n return this.supported!.filter(supportedLocale => supportedLocale !== lang);\n }\n \n /**\n * Translates a value (typically a translation id) into the current language used by the integrated app.\n *\n * @param {string} value The value to translate (typically a translation id).\n * @param {Record<string, unknown>} [params] (Optional) Any params needed for translating the value.\n * @returns {string} The translation of the specified value and params in the current language used by the integrated app.\n * @throws If the language integration module hasn't been imported into the app.\n */\n public translate(value: string, params?: Record<string, unknown>): string\n {\n this.ensureEnabled();\n\n return this.config!.translate(value, params);\n }\n\n /**\n * Dives deep into an object or an array and replaces the indicated properties in-place with their translation. \n *\n * The `paths` argument is an array of paths representing deep properties which should be translated.\n * For example:\n * \n * ```typescript\n * // If we have a user object, we can translate its city and role properties\n * {\n * id: 15,\n * addresses: [\n * { city: 'Tel Aviv', ... },\n * { city: 'Rishon LeTzion' }\n * ],\n * system: {\n * role: 'Admin'\n * }\n * }\n * \n * // Our paths would be:\n * `addresses[0].city`\n * `addresses[1].city`\n * `system.role`\n * ```\n * \n * @param {Record<string, unknown>} data The object which holds the translatable properties. Can be a deeply nested object.\n * @param {string[]} paths The paths of the translatable properties to translate and replace.\n * @throws If the language integration module hasn't been imported into the app.\n */\n public translateProperties(data: Record<string, unknown>, paths: string[]): void\n {\n this.ensureEnabled();\n \n paths.forEach(path =>\n {\n const valueAccessor = access<string>(data, path);\n const value = valueAccessor.get();\n\n if (typeof value !== 'string') return;\n\n valueAccessor.set(this.translate(value));\n });\n }\n \n /**\n * Ensures that the language integration module has been imported and a configuration object has been provided.\n * \n * @throws {Error} The language integration module hasn't been imported by the app.\n */\n public ensureEnabled(): this is { config: LanguageIntegrationConfig }\n {\n if (this.enabled) return true;\n\n throw new Error(`\n Language integration hasn't been enabled.\n Did you import the language integration module in your app module using 'LanguageIntegrationModule.forRoot()'?\n `);\n }\n}","import { Injectable } from '@angular/core';\n\nimport { UrlReflectionService } from '@bespunky/angular-zen/router-x';\n\n/**\n * The base class for url localization implementors. This can be used as an injectable token to get a hold of the currently\n * configured url localizer class.\n *\n * @export\n * @abstract\n * @class UrlLocalizer\n */\n@Injectable()\nexport abstract class UrlLocalizer\n{\n /**\n * Creates an instance of UrlLocalizer.\n * \n * @param {UrlReflectionService} urlReflection The url reflection service.\n */\n constructor(protected urlReflection: UrlReflectionService) { }\n\n /**\n * Reads the currently navigated url and localizes it to the specified language.\n * If the url is already localized with a different language, updates the language.\n * If the url is already localized with the specified language, returns the url unchanged.\n * \n * @abstract\n * @param {string} lang The language code to use for localization (e.g. 'en', 'fr', 'en-US', 'es-CL').\n * @returns {string} The localized url.\n */\n public abstract localize(lang: string): string;\n\n /**\n * Reads the Currently navigated url and delocalizes it.\n * If the url is already delocalized, return the url unchanged.\n *\n * @abstract\n * @returns {string} The delocalized url.\n */\n public abstract delocalize(): string;\n}\n","import { ClassProvider, FactoryProvider, InjectionToken, Type } from '@angular/core';\n\nimport { UrlLocalizer } from '../localizers/url-localizer';\n\n/** Represents a factory function for building a `UrlLocalizer` using dependency injection. */\nexport type UrlLocalizerFactory = (...deps: any[]) => UrlLocalizer;\n\n/**\n * A strongly-typed factory provider for creating a `UrlLocalizer` using dependency injection.\n *\n * @export\n * @interface UrlLocalizerFactoryProvider\n * @extends {(Omit<FactoryProvider, 'provide' | 'multi'>)}\n */\nexport interface UrlLocalizerFactoryProvider extends Omit<FactoryProvider, 'provide' | 'multi'>\n{\n /**\n * The factory to use for instantiating a `UrlLocalizer` object.\n *\n * @type {UrlLocalizerFactory}\n */\n useFactory: UrlLocalizerFactory;\n}\n\n/**\n * A strongly-typed class provider for instantiating a `UrlLocalizer`.\n *\n * @export\n * @interface UrlLocalizerClassProvider\n * @extends {(Omit<ClassProvider, 'provide' | 'multi'>)}\n */\nexport interface UrlLocalizerClassProvider extends Omit<ClassProvider, 'provide' | 'multi'>\n{\n /**\n * The class to use as a url localizer.\n *\n * @type {Type<UrlLocalizer>}\n */\n useClass: Type<UrlLocalizer>;\n}\n\n/** Represents the supported types of strategies for url localization. */\nexport type UrlLocalizationStrategy = number | string | UrlLocalizerFactoryProvider | UrlLocalizerClassProvider;\n\n/** Represents the configutaration for url localization tools. */\nexport interface UrlLocalizationConfig\n{\n /**\n * The strategy to use for url localization. Can be a: \n * `Number`\n * - Positive numbers express the position of the language param from the start of the route.\n * - Negative numbers express the position of the language param from the end of the route.\n * - Zero is ignored and will cause no change to the url.\n * \n * `String`\n * - The name of the query param specifying the current language.\n * \n * `UrlLocalizerFactoryProvider`\n * - A provider for a factory for instantiating a `UrlLocalizer` object using dependency injection.\n * \n * `UrlLocalizerClassProvider`\n * - A provider for an extending `UrlLocalizer` class.\n * @type {UrlLocalizationStrategy}\n */\n strategy : UrlLocalizationStrategy;\n /**\n * (Optional) Indicates whether localization and delocalization of a url should always return an https prefix.\n * Default is `false`.\n * @type {boolean}\n */\n forceHttps?: boolean;\n}\n\n/**\n * An injection token for the provided url localization configuration.\n * `LanguageIntegrationModule.forRoot()` facilitates the injection of this token. No need to inject directly.\n */\nexport const UrlLocalization = new InjectionToken<UrlLocalizationConfig>('LanguageIntegration.UrlLocalizationConfig');","import { Inject, Injectable } from '@angular/core';\n\nimport { UrlReflectionService } from '@bespunky/angular-zen/router-x';\nimport { LanguageIntegrationService } from '../../services/language-integration.service';\nimport { UrlLocalization, UrlLocalizationConfig } from '../config/url-localization-config';\nimport { UrlLocalizer } from './url-localizer';\n\n/**\n * Provides tools for localization and delocalization of the currently navigated url by adding or removing\n * a route segment dedicated for language.\n *\n * @export\n * @class RoutePositionUrlLocalizer\n * @extends {UrlLocalizer}\n */\n@Injectable({ providedIn: 'root'})\nexport class RoutePositionUrlLocalizer extends UrlLocalizer\n{\n /**\n * The position of the language segment in the route of the currently navigated url.\n * Positive numbers indicate position from the beginning of the route.\n * Negative numbers indicate position from the end of the route.\n * Zero is ignored and will cause methods to return an unchanged url.\n * \n * @type {number}\n */\n public readonly position: number;\n\n constructor(\n @Inject(UrlLocalization) { strategy } : UrlLocalizationConfig,\n urlReflection: UrlReflectionService,\n private language : LanguageIntegrationService\n )\n {\n super(urlReflection);\n\n this.position = strategy as number;\n }\n\n /**\n * Localizes the currently navigated url by adding or updating the language segment of the route.\n * If `position` is positive, language lookup will be performed from the beginning of route.\n * If `position` is negative, language lookup will be performed from the end of route.\n * If `position` points to an index out of bounds, the last/first element will be treated as the language.\n *\n * #### Example\n * Position 1 - /en/some/route - first segment from the left. \n * Position 2 - /some/en/route - second segment from the left. \n * Position 5 - /some/route/en - out of bounds. last segment from the left. \n * \n * Position -1 - /some/route/en - first segment from the right. \n * Position -2 - /some/en/route - second segment from the right. \n * Position -5 - /en/some/route - out of bounds. last segment from the right. \n * \n * @param {string} lang\n * @returns {string} The currently navigated url localized to the specified language.\n */\n localize(lang: string): string\n {\n return this.transformUrl(\n (segments, langIndex, isLanguage) => this.insertOrReplaceLanguage(lang, segments, langIndex, isLanguage)\n );\n }\n\n /**\n * Delocalizes the currently navigated url by removing the language segment from the route.\n * If `position` is positive, language lookup will be performed from the beginning of route.\n * If `position` is negative, language lookup will be performed from the end of route.\n * If `position` points to an index out of bounds, the last/first element will be treated as the language.\n * If no language exists at the language position, returns the url unchanged.\n *\n * #### Example\n * Position 1 - /en/some/route - first segment from the left. \n * Position 2 - /some/en/route - second segment from the left. \n * Position 5 - /some/route/en - out of bounds. last segment from the left. \n * \n * Position -1 - /some/route/en - first segment from the right. \n * Position -2 - /some/en/route - second segment from the right. \n * Position -5 - /en/some/route - out of bounds. last segment from the right. \n * \n * @returns {string} The delocalized currently navigated url.\n */\n delocalize(): string\n {\n return this.transformUrl(\n (segments, langIndex, isLanguage) => this.removeLanguage(segments, langIndex, isLanguage),\n // When removing the language segment, the index should always be in range. Unlike when inserting, an overflowing index\n // will not be converted to the last index automatically by `.splice()`.\n (segments, langIndex ) => langIndex >= segments.length ? segments.length - 1: langIndex\n );\n }\n \n private transformUrl(transform: (segments: string[], langIndex: number, isLanguage: boolean) => void, sanitizeIndex?: (segments: string[], langIndex: number) => number): string\n {\n // Position of zero will not touch the url as zero's functionality is not defined\n if (this.position === 0) return this.urlReflection.fullUrl;\n\n const segments = this.urlReflection.routeSegments;\n // Convert the position to replace/add to an index (might result in large negatives or positives, exceeding array bounds)\n let langIndex = this.indexOfPosition();\n \n this.accessSegmentsSafely(segments, () =>\n {\n if (sanitizeIndex) langIndex = sanitizeIndex(segments, langIndex);\n // Determine if a language segment exists at the specified index\n const isLanguage = this.isLanguage(segments[langIndex]);\n\n return transform(segments, langIndex, isLanguage);\n });\n\n return this.composeUrl(segments);\n }\n\n /**\n * Updates the specified route segments array with the specified language.\n *\n * @protected\n * @param {string} lang The new language to set to the route.\n * @param {string[]} segments The current route segments.\n * @param {number} langIndex The index of the expected language segment.\n * @param {boolean} isLanguage `true` if the current value at `langIndex` is a supported language; otherwise `false`.\n */\n protected insertOrReplaceLanguage(lang: string, segments: string[], langIndex: number, isLanguage: boolean): void\n {\n // Define how many items to remove. If the segment is already a language and should be replaced, zero items should be removed.\n const deleteCount = isLanguage ? 1 : 0;\n\n // Replace existing language segment or add a new one. If the specified index exceeds the length of the array, splice will add a new item at the end/beginning of the array.\n segments.splice(langIndex, deleteCount, lang);\n }\n\n /**\n * Removes the language segment from a route segments array.\n * If the language index points to a non-language segment, returns without changing the segments.\n *\n * @protected\n * @param {string[]} segments The current route segments.\n * @param {number} langIndex The index of the expected langauge segment.\n * @param {boolean} isLanguage `true` if the current value at `langIndex` is a supported language; otherwise `false`.\n */\n protected removeLanguage(segments: string[], langIndex: number, isLanguage: boolean): void\n {\n if (!isLanguage) return;\n\n segments.splice(langIndex, 1);\n }\n\n /**\n * Accessing segments by index requires the `this.position` to be translated into an index.\n * As the position can either be positive or negative, there are two different formulas for index calculation.\n * In turn, this means two different scenarios with different edge cases.\n * \n * To unify the cases and reduce complexity, when position is negative, this method reverses the segments array, runs the segments manipulation, then reverses it again to restore the original order.\n * This way the indexing is always done from one side of the array.\n * \n * @protected\n * @param {string[]} segments The segments about to be manipulated.\n * @param {() => void} accessSegments The function that needs safe access by index to the \n */\n protected accessSegmentsSafely(segments: string[], accessSegments: () => void): void\n {\n if (this.isNegativeLookup) segments.reverse();\n \n accessSegments();\n \n if (this.isNegativeLookup) segments.reverse();\n }\n\n /**\n * Indicates whether the configured language position is positive, resulting in a lookup from the left (positive lookup).\n * `true` if positive lookup should be performed; otherwise `false`.\n * \n * @readonly\n * @protected\n * @type {boolean}\n */\n protected get isPositiveLookup(): boolean\n {\n return this.position > 0;\n }\n\n /**\n * Indicates whether the configured language position is negative, resulting in a lookup from the right (negative lookup).\n * `true` if negative lookup should be performed; otherwise `false`.\n *\n * @readonly\n * @protected\n * @type {boolean}\n */\n protected get isNegativeLookup(): boolean\n {\n return this.position < 0;\n }\n\n /**\n * Calculates the absolute index for the configured language position.\n *\n * @protected\n * @returns {number}\n */\n protected indexOfPosition(): number\n {\n return Math.abs(this.position) - 1;\n }\n\n /**\n * Checks whether the specified value is a language supported by the language integration services.\n *\n * @protected\n * @param {string} value The value to check.\n * @returns {boolean} `true` if the value is a supported language; otherwise `false`.\n */\n protected isLanguage(value: string): boolean\n {\n return this.language.supported?.includes(value) || false;\n }\n\n /**\n * Concats the host url as given by the url reflection service with the segments and the current query string to create\n * a fully qualified url.\n *\n * @protected\n * @param {string[]} segments The route segments to place in the url.\n * @returns {string} The fully qualified url composed of the host url as given by the url reflection service, the specified route segments, and the current query params.\n */\n protected composeUrl(segments: string[]): string\n {\n const { hostUrl, queryString } = this.urlReflection;\n\n return `${hostUrl}/${segments.join('/')}${queryString}`\n }\n}","import { Inject, Injectable } from '@angular/core';\nimport { UrlTree } from '@angular/router';\n\nimport { UrlReflectionService } from '@bespunky/angular-zen/router-x';\nimport { UrlLocalization, UrlLocalizationConfig } from '../config/url-localization-config';\nimport { UrlLocalizer } from './url-localizer';\n\n/**\n * Provides tools for localization and delocalization of the currently navigated url by adding or removing\n * a named query param dedicated for language.\n *\n * @export\n * @class QueryParamsUrlLocalizer\n * @extends {UrlLocalizer}\n */\n@Injectable({ providedIn: 'root'})\nexport class QueryParamsUrlLocalizer extends UrlLocalizer\n{\n /**\n * The name of the query parameter specifying the language.\n *\n * @type {string}\n */\n public readonly paramName: string;\n\n constructor(@Inject(UrlLocalization) { strategy }: UrlLocalizationConfig, urlReflection: UrlReflectionService)\n {\n super(urlReflection);\n\n this.paramName = strategy as string;\n }\n\n /**\n * Localizes the currently navigated url by adding or updating the query param specifying the language.\n *\n * @param {string} lang The new language of the url.\n * @returns {string} The currently navigated url localized into the specified language.\n */\n localize(lang: string): string\n {\n const currentUrlTree = this.parseUrlTree();\n const localizedParams = this.replaceLanguageParam(this.urlReflection.queryParams, lang);\n const localizedRoute = this.replaceQueryParamsInUrlTree(currentUrlTree, localizedParams);\n const localizedRouteUrl = this.urlReflection.router.serializeUrl(localizedRoute);\n\n return this.composeUrl(localizedRouteUrl);\n }\n \n /**\n * Delocalizes the currently navigated url by removing the query param specifying the language.\n *\n * @returns {string} The currently navigated url without the query param for language.\n */\n delocalize(): string\n {\n return this.localize('');\n }\n\n /**\n * Returns the `UrlTree` representing the currently navigated url.\n *\n * @protected\n * @returns {UrlTree} The `UrlTree` representing the currently navigated url.\n */\n protected parseUrlTree(): UrlTree\n {\n const { router } = this.urlReflection;\n\n // Parsing the url seems dumb as the router should have it parsed already, but the route object doesn't hold\n // the tree and the router SOMETIMES holds it in getCurrentNavigation().\n return router.parseUrl(router.url);\n }\n\n /**\n * Updates the language param in a query params object.\n *\n * @protected\n * @param {*} params The object representing the query params.\n * @param {string} lang The new language to set to the language query param. If `null` or `undefined`, the language query param will be deleted from the object.\n * @returns {*} The updated query params object.\n */\n protected replaceLanguageParam(params: any, lang: string): any\n {\n if (lang)\n params[this.paramName] = lang;\n else\n delete params[this.paramName];\n\n return params;\n }\n\n /**\n * Replaces the query params in a url tree object.\n *\n * @protected\n * @param {UrlTree} url The url tree in which query params should be replaced.\n * @param {Object} newParams The new query params object to set to the url tree.\n * @returns {UrlTree} The updated url tree object.\n */\n protected replaceQueryParamsInUrlTree(url: UrlTree, newParams: { [k: string]: any }): UrlTree\n {\n return Object.assign(url, { queryParams: newParams });\n }\n\n /**\n * Concats the host url and the specified route url to compose a fully qualified url.\n * Uses the host url provided by the url reflection service.\n * \n * @protected\n * @param {string} routeUrl The route url to concat to the host url. Should be prefixed with '/'.\n * @returns {string} The fully qualified url composed of the host url defined by the url reflection service and the specified route url.\n */\n protected composeUrl(routeUrl: string): string\n { \n const { hostUrl } = this.urlReflection;\n\n return `${hostUrl}${routeUrl}`;\n }\n}","import { Injectable } from '@angular/core';\n\nimport { UrlLocalizer } from './url-localizer';\n\n/**\n * A noop implemetation for the `UrlLocalizer` class.\n * Always returns unchanged urls.\n *\n * @export\n * @class NoopUrlLocalizer\n * @extends {UrlLocalizer}\n */\n@Injectable({ providedIn: 'root'})\nexport class NoopUrlLocalizer extends UrlLocalizer\n{\n /**\n * Returns the currently navigated url as is.\n *\n * @param {string} lang Ignore.\n * @returns {string} The currently navigated url as is.\n */\n localize(lang: string): string\n {\n return this.urlReflection.fullUrl;\n }\n \n /**\n * Returns the currently navigated url as is.\n *\n * @returns {string} The currently navigated url as is.\n */\n delocalize(): string\n {\n return this.urlReflection.fullUrl;\n }\n}","import { ClassProvider, FactoryProvider, Provider } from '@angular/core';\n\nimport { UrlReflectionService } from '@bespunky/angular-zen/router-x';\nimport { LanguageIntegrationService } from '../../services/language-integration.service';\nimport { UrlLocalizer } from '../localizers/url-localizer';\nimport { RoutePositionUrlLocalizer } from '../localizers/route-position-url-localizer';\nimport { QueryParamsUrlLocalizer } from '../localizers/query-params-url-localizer';\nimport { NoopUrlLocalizer } from '../localizers/noop-url-localizer';\nimport { UrlLocalizationConfig, UrlLocalization } from './url-localization-config';\n\n/**\n * The default configuration for url localization when loading the language integration module.\n * Uses the `NoopUrlLocalizer` as strategy and does not force https. Localization and delocalization will always return an unchanged url url.\n */\nexport const DefaultUrlLocalizationConfig: UrlLocalizationConfig = {\n strategy : { useClass: NoopUrlLocalizer },\n forceHttps: false\n};\n\n/**\n * Creates the appropriate DI compatible provider for the `UrlLocalizer` class, depending on the strategy specified in the url localization configuration. \n * If the configured strategy is a number, `RoutePositionUrlLocalizer` will be used. \n * If the configured strategy is a string, `QueryParamsUrlLocalizer` will be used. \n * If the configured strategy is a valid `UrlLocalizer` provider, the provider will be used as is. \n * Otherwise, `NoopUrlLocalizer` will be used.\n * \n * @export\n * @param {UrlLocalizationConfig} config The url localization configuration holding the strategy.\n * @returns {(ClassProvider | FactoryProvider)} A DI compatible provider for the `UrlLocalizer` class with the implementation appropriate for the specified strategy.\n */\nexport function provideUrlLocalizer(config: UrlLocalizationConfig): ClassProvider | FactoryProvider\n{\n const strategy = config?.strategy;\n\n const strategies: { [type: string]: () => Partial<Provider>; } = {\n // Use route position strategy for numbers\n number : () => ({ useFactory: (urlReflect: UrlReflectionService, language: LanguageIntegrationService) => new RoutePositionUrlLocalizer(config, urlReflect, language), deps: [UrlReflectionService, LanguageIntegrationService] }),\n // Use query params strategy for strings\n string : () => ({ useFactory: (urlReflect: UrlReflectionService) => new QueryParamsUrlLocalizer(config, urlReflect), deps: [UrlReflectionService] }),\n // Use the user's factory or class provider\n object : () => strategy,\n // Use the noop localizer when nothing provided (in case url localization config is not present)\n undefined: () => ({ useClass: NoopUrlLocalizer })\n };\n\n // Create a basic provider to which the strategy will be assigned\n const provider: Partial<Provider> = { provide: UrlLocalizer };\n\n // Override the useClass or useFactory with the detected strategy\n return Object.assign(provider, strategies[typeof strategy]()) as (ClassProvider | FactoryProvider);\n}\n\n/**\n * Creates the providers for the `UrlLocalization` token and the `UrlLocalizer` class.\n *\n * @export\n * @param {UrlLocalizationConfig} [config] (Optional) The configuration for url localization tools. Default is `DefaultUrlLocalizationConfig`.\n * @returns {Provider[]} The providers for the `UrlLocalization` token and the `UrlLocalizer` class.\n */\nexport function provideUrlLocalization(config?: UrlLocalizationConfig): Provider[]\n{\n config = Object.assign({}, DefaultUrlLocalizationConfig, config);\n\n return [\n { provide: UrlLocalization, useValue: config },\n provideUrlLocalizer(config)\n ];\n}\n","import { FactoryProvider, Provider } from '@angular/core';\n\nimport { UrlLocalizationConfig } from '../url-localization/config/url-localization-config';\nimport { provideUrlLocalization } from '../url-localization/config/url-localization.provider';\nimport { LanguageIntegrationConfig, LanguageIntegration } from './language-integration-config';\n\n/** Represents a factory that receives dependencies and produces the language integration configuration for an app. */\nexport type LanguageIntegrationConfigFactory = (...deps: any[]) => LanguageIntegrationConfig;\n\n/**\n * A strongly-typed factory provider for providing language integration config.\n *\n * @export\n * @interface LanguageIntegrationProvider\n * @extends {(Omit<FactoryProvider, 'provide' | 'multi'>)}\n */\nexport interface LanguageIntegrationProvider extends Omit<FactoryProvider, 'provide' | 'multi'>\n{\n /**\n * The factory to use for creating the language integration configuration.\n *\n * @type {LanguageIntegrationConfigFactory}\n */\n useFactory : LanguageIntegrationConfigFactory;\n /**\n * (Optional) The configuration for the `UrlLocalizationService`.\n *\n * @type {UrlLocalizationConfig}\n */\n urlLocalization?: UrlLocalizationConfig;\n}\n\n/**\n * Generates language integration tokens and services to be provided in a module.\n * Used by `LanguageIntegrationModule.forRoot()`.\n *\n * @export\n * @param {LanguageIntegrationProvider} { useFactory, deps, urlLocalization } The language integration provider configuration.\n * @returns {Provider[]} An array of providers for language integration.\n */\nexport function provideLanguageIntegration({ useFactory, deps, urlLocalization }: LanguageIntegrationProvider): Provider[]\n{\n return [\n { provide: LanguageIntegration, useFactory, deps },\n ...provideUrlLocalization(urlLocalization)\n ];\n}","import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';\n\nimport { CoreModule } from '@bespunky/angular-zen/core';\nimport { LanguageIntegrationProvider, provideLanguageIntegration } from './config/language-integration.provider';\n\n/**\n * Provides services for libraries requiring integration with their user's language services.\n *\n * @export\n * @class LanguageIntegrationModule\n */\n@NgModule({\n imports: [CoreModule]\n})\nexport class LanguageIntegrationModule\n{\n constructor(@Optional() @SkipSelf() parentModule: LanguageIntegrationModule)\n {\n if (parentModule) throw new Error('`LanguageIntegrationModule` has already been loaded. Import it only once, in your app module, using `forRoot()`.');\n }\n\n /**\n * Generates the language integration modules with the appropriate providers for the app to share its language services with\n * libraries and supporting languages.\n *\n * @static\n * @param {LanguageIntegrationProvider} configProvider The integration configuration. Tells the module how to operate with your language services.\n */\n static forRoot(configProvider: LanguageIntegrationProvider): ModuleWithProviders<LanguageIntegrationModule>\n {\n return {\n ngModule : LanguageIntegrationModule,\n providers: provideLanguageIntegration(configProvider)\n };\n }\n}\n","import { Directive, Injectable } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\n\nimport { RouteAware, RouterOutletComponentBus } from '@bespunky/angular-zen/router-x';\nimport { LanguageIntegrationService } from './language-integration.service';\n\n/**\n * Integrates with the `LanguageIntegrationService` and facilitates language related work in route-aware services.\n *\n * @export\n * @abstract\n * @class LocalizedRouteAware\n * @extends {RouteAware}\n */\n@Directive() // Originally this was decorated with `Directive` only so angular accepts it as base for both services and components.\n@Injectable() // However, compodoc fails to collect abstract classes marked with `Directive` so I marked it as both. Tests pass, POC stackblitz doesn't show side effects.\n// eslint-disable-next-line @angular-eslint/directive-class-suffix\nexport abstract class LocalizedRouteAware extends RouteAware\n{\n /**\n * Creates an instance of LocalizedRouteAware.\n * \n * @param {LanguageIntegrationService} language The instance of the language integration service.\n * @param {Router} router The instance of Angular's router service.\n * @param {ActivatedRoute} route The instance of Angular's activated route service.\n * @param {RouterOutletComponentBus} [componentBus] (Optional) The component bus for router-x functionality.\n * Provide this when you want your route-aware service to have access to the instance(s) of the activated component(s).\n */\n constructor(\n protected language : LanguageIntegrationService,\n router : Router,\n route : ActivatedRoute,\n componentBus?: RouterOutletComponentBus\n )\n {\n super(router, route, componentBus);\n \n if (this.language.enabled) this.initLanguageSupport();\n }\n\n private initLanguageSupport(): void\n {\n this.subscribe(this.language.ready , this.onLanguageServicesReady.bind(this));\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.subscribe(this.language.changed!, this.onLanguageChanged.bind(this));\n }\n \n /**\n * Called when the app's language services have initialized and are ready for use.\n * When language integration is disabled, or no ready observable have been provided by the app\n * this will execute immediatelly on construction time.\n * \n * Override to implement.\n *\n * @virtual\n * @protected\n */\n protected onLanguageServicesReady(): void { void 0; }\n\n /**\n * Called when the current language used by the integrated app has changed. Override to implement.\n * \n * @virtual\n * @protected\n * @param {*} lang The language code of the new language.\n */\n protected onLanguageChanged(lang: string): void { void 0; }\n}\n","import { Inject, Injectable } from '@angular/core';\n\nimport { UrlReflectionService } from '@bespunky/angular-zen/router-x';\nimport { UrlLocalization, UrlLocalizationConfig } from '../config/url-localization-config';\nimport { UrlLocalizer } from '../localizers/url-localizer';\n\n/**\n * Provides tools for localization and delocalization of the currently navigated url taking into\n * account the url localization configuration provided when importing the language integration module.\n * \n * @export\n * @class UrlLocalizationService\n */\n@Injectable({ providedIn: 'root' })\nexport class UrlLocalizationService\n{\n /**\n * Creates an instance of UrlLocalizationService.\n *\n * @param {UrlLocalizationConfig} config The url localization configuration provided for the `UrlLocalization` token.\n * @param {UrlReflectionService} urlReflection The url reflection service.\n * @param {UrlLocalizer} localizer The url localizer which will actually do the localization work.\n * The instance and implementation depend on the strategy configured for url localization when importing the language integration module.\n */\n constructor(\n @Inject(UrlLocalization) private config : UrlLocalizationConfig,\n private urlReflection: UrlReflectionService,\n public readonly localizer : UrlLocalizer\n ) { }\n \n /**\n * Localizes the currently navigated url using the configured localization strategy and forces https if needed.\n *\n * @param {string} lang The langugae to localize the currently navigated url to.\n * @returns {string} The localized currently navigated url.\n */\n public localize(lang: string): string\n {\n return this.replaceHttpIfRequired(this.localizer.localize(lang));\n }\n \n /**\n * Delocalizes the currently navigated url using the configured localization strategy and forces https if needed.\n *\n * @returns {string} The delocalized currently navigated url.\n */\n public delocalize(): string\n { \n return this.replaceHttpIfRequired(this.localizer.delocalize());\n }\n\n /**\n * Generates a localized version of the currently navigate url for each of the specified languages using the configured localization strategy.\n * \n * @param {string[]} langs The languages for which to generate the localized urls.\n * @returns {{ [lang: string]: string }[]} An array of { [lang]: url } containing an object for each language and its corresponding localized url.\n */\n public generateLocalizedUrls(langs: string[]): { [lang: string]: string }[]\n {\n return langs.map(lang => ({ [lang]: this.localizer.localize(lang) }));\n }\n\n private replaceHttpIfRequired(url: string): string\n {\n return this.config?.forceHttps ? this.urlReflection.forceHttps(url) : url;\n }\n}","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["i2.LanguageIntegrationService","i1.LanguageIntegrationService","i3","i2.UrlLocalizer"],"mappings":"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