### File

`src/i18n/i18n.service.ts`

### Description

Represents an "overridable" translation value.

Largely an internal usecase. There are situations where we want an `Observable` that can emit events from a centralized source OR an `Observable` that will emit events from a component local source. The key example being on/off text in a `Toggle` - In some cases we want the `Toggle` to use `I18n`s global translations, but in others we'd prefer to use a local override. We don't ever need to return to a non-overridden state, but we do need the ability to switch to an overridden sate.

### Index

###### Properties
Protected _value Protected $override Protected baseTranslation Protected isOverridden Protected subscription
###### Methods
override
###### Accessors
value subject

### Constructor

`constructor(path: string, i18n: I18n)`
Defined in src/i18n/i18n.service.ts:99
Parameters :
Name | Type | Optional
path | `string` | No
i18n | `I18n` | No

### Properties

Protected _value
`_value: `string | Observable<string>``
Type : `string | Observable<string>`
Defined in src/i18n/i18n.service.ts:82
Overridden value. Accessed by the readonly getter `value` and set through `override`

Protected $override
`$override: `BehaviorSubject<string>``
Type : `BehaviorSubject<string>`
Defined in src/i18n/i18n.service.ts:86
Subject of overridden values. Initialized with our default value.

Protected baseTranslation
`baseTranslation: `Observable<string>``
Type : `Observable<string>`
Default value : `this.i18n.get(this.path)`
Defined in src/i18n/i18n.service.ts:90
Our base non-overridden translation.

Protected isOverridden
`isOverridden: ```
Default value : `false`
Defined in src/i18n/i18n.service.ts:99
A boolean to flip between overridden and non-overridden states.

Protected subscription
`subscription: `Subscription``
Type : `Subscription`
Defined in src/i18n/i18n.service.ts:95
Subscription to the observable provided as an override (if any)

### Methods

override
`override(value: string | Observable)`
Defined in src/i18n/i18n.service.ts:114
Takes a string or an `Observable` that emits strings. Overrides the value provided by the `I18n` service. Parameters :
Name | Type | Optional
value | `string | Observable<string>` | No
Returns : `void`

### Accessors

value
get `value()`
Defined in src/i18n/i18n.service.ts:58
The raw value of the translation. Defaults to the string value, but will return the value passed to `override` Returns : `string | Observable`
set `value(v)`
Defined in src/i18n/i18n.service.ts:62
Parameters :
Name | Optional
v | No
Returns : `void`

subject
get `subject()`
Defined in src/i18n/i18n.service.ts:71
The translation subject. Returns either a stream of overridden values, or our base translation values. Returns : `Observable<string>`

    import { Injectable } from "@angular/core";
    import {
    	BehaviorSubject,
    	Observable,
    	isObservable,
    	iif,
    	Subscription
    } from "rxjs";
    import { map } from "rxjs/operators";
    import { merge } from "../utils/object";

    import EN from "./en";

    /**
     * Takes the `Observable` returned from `i18n.get` and an object of variables to replace.
     *
     * The keys specify the variable name in the string.
     *
     * Example:
     * ```typescript
     * service.set({ "TEST": "{{foo}} {{bar}}" });
     *
     * service.replace(service.get("TEST"), { foo: "test", bar: "asdf" })
     * ```
     *
     * Produces: `"test asdf"`
     *
     * @param subject the translation to replace variables on
     * @param variables object of variables to replace
     */
    export const replace = (subject, variables) => subject.pipe(
    	map<string, void>(str => {
    		const keys = Object.keys(variables);
    		for (const key of keys) {
    			const value = variables[key];
    			str = str.replace(new RegExp(`{{\\s*${key}\\s*}}`, "g"), value);
    		}
    		return str;
    	})
    );

    /**
     * Represents an "overridable" translation value.
     *
     * Largely an internal usecase. There are situations where we want an `Observable` that
     * can emit events from a centralized source **OR** an `Observable` that will emit events
     * from a component local source. The key example being on/off text in a `Toggle` - In some cases
     * we want the `Toggle` to use `I18n`s global translations, but in others we'd prefer to use a local
     * override. We don't ever need to return to a non-overridden state, but we do need the ability to
     * switch _to_ an overridden sate.
     */
    export class Overridable {
    	/**
    	 * The raw value of the translation. Defaults to the string value, but will return the value passed to `override`
    	 *
    	 * @readonly
    	 */
    	public get value(): string | Observable<string> {
    		return this._value;
    	}

    	public set value(v: string | Observable<string>) {
    		this.override(v);
    	}

    	/**
    	 * The translation subject. Returns either a stream of overridden values, or our base translation values.
    	 *
    	 * @readonly
    	 */
    	public get subject(): Observable<string> {
    		/**
    		 * since inputs are bound on template instantiation (and thusly will always have _some_ value)
    		 * We can use a simple boolean and the `iif` function to determine which subject to return on subscription
    		 */
    		return iif(() => this.isOverridden, this.$override, this.baseTranslation);
    	}

    	/**
    	 * Overridden value. Accessed by the readonly getter `value` and set through `override`
    	 */
    	protected _value: string | Observable<string>;
    	/**
    	 * Subject of overridden values. Initialized with our default value.
    	 */
    	protected $override: BehaviorSubject<string>;
    	/**
    	 * Our base non-overridden translation.
    	 */
    	protected baseTranslation: Observable<string> = this.i18n.get(this.path);

    	/**
    	 * Subscription to the observable provided as an override (if any)
    	 */
    	protected subscription: Subscription;
    	/**
    	 * A boolean to flip between overridden and non-overridden states.
    	 */
    	protected isOverridden = false;

    	constructor(protected path: string, protected i18n: I18n) {
    		/**
    		 * ensure `$override` is initialized with the correct default value
    		 * in some cases `_value` can get changed for an `Observable` before `$override` is created
    		 */
    		const value = this.i18n.getValueFromPath(this.path) as string;
    		this.$override = new BehaviorSubject<string>(value);
    		this._value = value;
    	}
    	/**
    	 * Takes a string or an `Observable` that emits strings.
    	 * Overrides the value provided by the `I18n` service.
    	 */
    	override(value: string | Observable<string>) {
    		this.isOverridden = true;
    		// To ensure that there are not multiple subscriptions created for the same observable, we
    		// unsubscribe if a subscription already exists for an observable before creating a new one.
    		if (this.subscription) {
    			this.subscription.unsubscribe();
    			this.subscription = null;
    		}

    		this._value = value;

    		if (isObservable(value)) {
    			this.subscription = value.subscribe(v => {
    				this.$override.next(v);
    			});
    		} else {
    			this.$override.next(value);
    		}
    	}
    }

    /**
     * The I18n service is a minimal internal singleton service used to supply our components with translated strings.
     *
     * All the components that support I18n also support directly passed strings.
     * Usage of I18n is optional, and it is not recommended for application use (libraries like ngx-translate
     * are a better choice)
     *
     */
    @Injectable()
    export class I18n {
    	protected translationStrings = EN;

    	protected translations = new Map();

    	/**
    	 * Set/update the translations from an object. Also notifies all participating components of the update.
    	 *
    	 * @param strings an object of strings, should follow the same format as src/i18n/en.json
    	 */
    	public set(strings: { [key: string]: string }) {
    		this.translationStrings = merge({}, EN, strings);
    		// iterate over all our tracked translations and update each observable
    		const translations = Array.from(this.translations);
    		for (const [path, subject] of translations) {
    			subject.next(this.getValueFromPath(path));
    		}
    	}

    	/**
    	 * When a path is specified returns an observable that will resolve to the translation string value.
    	 *
    	 * Returns the full translations object if path is not specified.
    	 *
    	 * @param path optional, looks like `"NOTIFICATION.CLOSE_BUTTON"`
    	 */
    	public get(path?: string): any {
    		if (!path) {
    			return this.translationStrings;
    		}
    		return this.getSubject(path);
    	}

    	/**
    	 * Returns all descendents of some path fragment as an object.
    	 *
    	 * @param partialPath a path fragment, for example `"NOTIFICATION"`
    	 */
    	public getMultiple(partialPath: string): { [key: string]: Observable<string> } {
    		const values = this.getValueFromPath(partialPath);
    		const subjects = {};
    		for (const key of Object.keys(values)) {
    			if (values[key] === Object(values[key])) {
    				subjects[key] = this.getMultiple(`${partialPath}.${key}`);
    			} else {
    				subjects[key] = this.getSubject(`${partialPath}.${key}`);
    			}
    		}
    		return subjects;
    	}

    	/**
    	 * Returns an instance of `Overridable` that can be used to optionally override the value provided by `I18n`
    	 * @param path looks like `"NOTIFICATION.CLOSE_BUTTON"`
    	 */
    	public getOverridable(path: string) {
    		return new Overridable(path, this);
    	}

    	/**
    	 * Takes the `Observable` returned from `i18n.get` and an object of variables to replace.
    	 *
    	 * The keys specify the variable name in the string.
    	 *
    	 * Example:
    	 * ```
    	 * service.set({ "TEST": "{{foo}} {{bar}}" });
    	 *
    	 * service.replace(service.get("TEST"), { foo: "test", bar: "asdf" })
    	 * ```
    	 *
    	 * Produces: `"test asdf"`
    	 *
    	 * @param subject the translation to replace variables on
    	 * @param variables object of variables to replace
    	 */
    	public replace(subject: Observable<string>, variables: { [key: string]: string }) {
    		return replace(subject, variables);
    	}

    	/**
    	 * Trys to resolve a value from the provided path.
    	 *
    	 * @param path looks like `"NOTIFICATION.CLOSE_BUTTON"`
    	 */
    	public getValueFromPath(path): string | { [key: string]: string } {
    		let value = this.translationStrings;
    		for (const segment of path.split(".")) {
    			if (value[segment]) {
    				value = value[segment];
    			} else {
    				throw new Error(`no key ${segment} at ${path}`);
    			}
    		}
    		return value as any;
    	}

    	/**
    	 * Helper method that returns an observable from the internal cache based on the path
    	 *
    	 * @param path looks like `"NOTIFICATION.CLOSE_BUTTON"`
    	 */
    	protected getSubject(path: string): Observable<string> {
    		try {
    			// we run this here to validate the path exists before adding it to the translation map
    			const value = this.getValueFromPath(path) as string;
    			if (this.translations.has(path)) {
    				return this.translations.get(path);
    			}
    			const translation = new BehaviorSubject(value);
    			this.translations.set(path, translation);
    			return translation;
    		} catch (error) {
    			console.error(error);
    		}
    	}
    }

# result-matching ""

# No results matching ""