UNPKG

17.4 kBPlain TextView Raw
1import {EventEmitter, Inject, Injectable, InjectionToken} from "@angular/core";
2import {concat, forkJoin, isObservable, Observable, of, defer} from "rxjs";
3import {concatMap, map, shareReplay, switchMap, take} from "rxjs/operators";
4import {MissingTranslationHandler, MissingTranslationHandlerParams} from "./missing-translation-handler";
5import {TranslateCompiler} from "./translate.compiler";
6import {TranslateLoader} from "./translate.loader";
7import {TranslateParser} from "./translate.parser";
8
9import {TranslateStore} from "./translate.store";
10import {isDefined, mergeDeep} from "./util";
11
12export const USE_STORE = new InjectionToken<string>('USE_STORE');
13export const USE_DEFAULT_LANG = new InjectionToken<string>('USE_DEFAULT_LANG');
14export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
15export const USE_EXTEND = new InjectionToken<string>('USE_EXTEND');
16
17export interface TranslationChangeEvent {
18 translations: any;
19 lang: string;
20}
21
22export interface LangChangeEvent {
23 lang: string;
24 translations: any;
25}
26
27export interface DefaultLangChangeEvent {
28 lang: string;
29 translations: any;
30}
31
32declare interface Window {
33 navigator: any;
34}
35
36declare const window: Window;
37
38@Injectable()
39export class TranslateService {
40 private loadingTranslations!: Observable<any>;
41 private pending: boolean = false;
42 private _onTranslationChange: EventEmitter<TranslationChangeEvent> = new EventEmitter<TranslationChangeEvent>();
43 private _onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>();
44 private _onDefaultLangChange: EventEmitter<DefaultLangChangeEvent> = new EventEmitter<DefaultLangChangeEvent>();
45 private _defaultLang!: string;
46 private _currentLang!: string;
47 private _langs: Array<string> = [];
48 private _translations: any = {};
49 private _translationRequests: any = {};
50
51 /**
52 * An EventEmitter to listen to translation change events
53 * onTranslationChange.subscribe((params: TranslationChangeEvent) => {
54 * // do something
55 * });
56 */
57 get onTranslationChange(): EventEmitter<TranslationChangeEvent> {
58 return this.isolate ? this._onTranslationChange : this.store.onTranslationChange;
59 }
60
61 /**
62 * An EventEmitter to listen to lang change events
63 * onLangChange.subscribe((params: LangChangeEvent) => {
64 * // do something
65 * });
66 */
67 get onLangChange(): EventEmitter<LangChangeEvent> {
68 return this.isolate ? this._onLangChange : this.store.onLangChange;
69 }
70
71 /**
72 * An EventEmitter to listen to default lang change events
73 * onDefaultLangChange.subscribe((params: DefaultLangChangeEvent) => {
74 * // do something
75 * });
76 */
77 get onDefaultLangChange() {
78 return this.isolate ? this._onDefaultLangChange : this.store.onDefaultLangChange;
79 }
80
81 /**
82 * The default lang to fallback when translations are missing on the current lang
83 */
84 get defaultLang(): string {
85 return this.isolate ? this._defaultLang : this.store.defaultLang;
86 }
87
88 set defaultLang(defaultLang: string) {
89 if (this.isolate) {
90 this._defaultLang = defaultLang;
91 } else {
92 this.store.defaultLang = defaultLang;
93 }
94 }
95
96 /**
97 * The lang currently used
98 */
99 get currentLang(): string {
100 return this.isolate ? this._currentLang : this.store.currentLang;
101 }
102
103 set currentLang(currentLang: string) {
104 if (this.isolate) {
105 this._currentLang = currentLang;
106 } else {
107 this.store.currentLang = currentLang;
108 }
109 }
110
111 /**
112 * an array of langs
113 */
114 get langs(): string[] {
115 return this.isolate ? this._langs : this.store.langs;
116 }
117
118 set langs(langs: string[]) {
119 if (this.isolate) {
120 this._langs = langs;
121 } else {
122 this.store.langs = langs;
123 }
124 }
125
126 /**
127 * a list of translations per lang
128 */
129 get translations(): any {
130 return this.isolate ? this._translations : this.store.translations;
131 }
132
133 set translations(translations: any) {
134 if (this.isolate) {
135 this._translations = translations;
136 } else {
137 this.store.translations = translations;
138 }
139 }
140
141 /**
142 *
143 * @param store an instance of the store (that is supposed to be unique)
144 * @param currentLoader An instance of the loader currently used
145 * @param compiler An instance of the compiler currently used
146 * @param parser An instance of the parser currently used
147 * @param missingTranslationHandler A handler for missing translations.
148 * @param useDefaultLang whether we should use default language translation when current language translation is missing.
149 * @param isolate whether this service should use the store or not
150 * @param extend To make a child module extend (and use) translations from parent modules.
151 * @param defaultLanguage Set the default language using configuration
152 */
153 constructor(public store: TranslateStore,
154 public currentLoader: TranslateLoader,
155 public compiler: TranslateCompiler,
156 public parser: TranslateParser,
157 public missingTranslationHandler: MissingTranslationHandler,
158 @Inject(USE_DEFAULT_LANG) private useDefaultLang: boolean = true,
159 @Inject(USE_STORE) private isolate: boolean = false,
160 @Inject(USE_EXTEND) private extend: boolean = false,
161 @Inject(DEFAULT_LANGUAGE) defaultLanguage: string) {
162 /** set the default language from configuration */
163 if (defaultLanguage) {
164 this.setDefaultLang(defaultLanguage);
165 }
166 }
167
168 /**
169 * Sets the default language to use as a fallback
170 */
171 public setDefaultLang(lang: string): void {
172 if (lang === this.defaultLang) {
173 return;
174 }
175
176 let pending = this.retrieveTranslations(lang);
177
178 if (typeof pending !== "undefined") {
179 // on init set the defaultLang immediately
180 if (this.defaultLang == null) {
181 this.defaultLang = lang;
182 }
183
184 pending.pipe(take(1))
185 .subscribe((res: any) => {
186 this.changeDefaultLang(lang);
187 });
188 } else { // we already have this language
189 this.changeDefaultLang(lang);
190 }
191 }
192
193 /**
194 * Gets the default language used
195 */
196 public getDefaultLang(): string {
197 return this.defaultLang;
198 }
199
200 /**
201 * Changes the lang currently used
202 */
203 public use(lang: string): Observable<any> {
204 // don't change the language if the language given is already selected
205 if (lang === this.currentLang) {
206 return of(this.translations[lang]);
207 }
208
209 let pending = this.retrieveTranslations(lang);
210
211 if (typeof pending !== "undefined") {
212 // on init set the currentLang immediately
213 if (!this.currentLang) {
214 this.currentLang = lang;
215 }
216
217 pending.pipe(take(1))
218 .subscribe((res: any) => {
219 this.changeLang(lang);
220 });
221
222 return pending;
223 } else { // we have this language, return an Observable
224 this.changeLang(lang);
225
226 return of(this.translations[lang]);
227 }
228 }
229
230 /**
231 * Retrieves the given translations
232 */
233 private retrieveTranslations(lang: string): Observable<any> | undefined {
234 let pending: Observable<any> | undefined;
235
236 // if this language is unavailable or extend is true, ask for it
237 if (typeof this.translations[lang] === "undefined" || this.extend) {
238 this._translationRequests[lang] = this._translationRequests[lang] || this.getTranslation(lang);
239 pending = this._translationRequests[lang];
240 }
241
242 return pending;
243 }
244
245 /**
246 * Gets an object of translations for a given language with the current loader
247 * and passes it through the compiler
248 */
249 public getTranslation(lang: string): Observable<any> {
250 this.pending = true;
251 const loadingTranslations = this.currentLoader.getTranslation(lang).pipe(
252 shareReplay(1),
253 take(1),
254 );
255
256 this.loadingTranslations = loadingTranslations.pipe(
257 map((res: Object) => this.compiler.compileTranslations(res, lang)),
258 shareReplay(1),
259 take(1),
260 );
261
262 this.loadingTranslations
263 .subscribe({
264 next: (res: Object) => {
265 this.translations[lang] = this.extend && this.translations[lang] ? { ...res, ...this.translations[lang] } : res;
266 this.updateLangs();
267 this.pending = false;
268 },
269 error: (err: any) => {
270 this.pending = false;
271 }
272 });
273
274 return loadingTranslations;
275 }
276
277 /**
278 * Manually sets an object of translations for a given language
279 * after passing it through the compiler
280 */
281 public setTranslation(lang: string, translations: Object, shouldMerge: boolean = false): void {
282 translations = this.compiler.compileTranslations(translations, lang);
283 if ((shouldMerge || this.extend) && this.translations[lang]) {
284 this.translations[lang] = mergeDeep(this.translations[lang], translations);
285 } else {
286 this.translations[lang] = translations;
287 }
288 this.updateLangs();
289 this.onTranslationChange.emit({lang: lang, translations: this.translations[lang]});
290 }
291
292 /**
293 * Returns an array of currently available langs
294 */
295 public getLangs(): Array<string> {
296 return this.langs;
297 }
298
299 /**
300 * Add available langs
301 */
302 public addLangs(langs: Array<string>): void {
303 langs.forEach((lang: string) => {
304 if (this.langs.indexOf(lang) === -1) {
305 this.langs.push(lang);
306 }
307 });
308 }
309
310 /**
311 * Update the list of available langs
312 */
313 private updateLangs(): void {
314 this.addLangs(Object.keys(this.translations));
315 }
316
317 /**
318 * Returns the parsed result of the translations
319 */
320 public getParsedResult(translations: any, key: any, interpolateParams?: Object): any {
321 let res: string | Observable<string> | undefined;
322
323 if (key instanceof Array) {
324 let result: any = {},
325 observables: boolean = false;
326 for (let k of key) {
327 result[k] = this.getParsedResult(translations, k, interpolateParams);
328 if (isObservable(result[k])) {
329 observables = true;
330 }
331 }
332 if (observables) {
333 const sources = key.map(k => isObservable(result[k]) ? result[k] : of(result[k] as string));
334 return forkJoin(sources).pipe(
335 map((arr: Array<string>) => {
336 let obj: any = {};
337 arr.forEach((value: string, index: number) => {
338 obj[key[index]] = value;
339 });
340 return obj;
341 })
342 );
343 }
344 return result;
345 }
346
347 if (translations) {
348 res = this.parser.interpolate(this.parser.getValue(translations, key), interpolateParams);
349 }
350
351 if (typeof res === "undefined" && this.defaultLang != null && this.defaultLang !== this.currentLang && this.useDefaultLang) {
352 res = this.parser.interpolate(this.parser.getValue(this.translations[this.defaultLang], key), interpolateParams);
353 }
354
355 if (typeof res === "undefined") {
356 let params: MissingTranslationHandlerParams = {key, translateService: this};
357 if (typeof interpolateParams !== 'undefined') {
358 params.interpolateParams = interpolateParams;
359 }
360 res = this.missingTranslationHandler.handle(params);
361 }
362
363 return typeof res !== "undefined" ? res : key;
364 }
365
366 /**
367 * Gets the translated value of a key (or an array of keys)
368 * @returns the translated key, or an object of translated keys
369 */
370 public get(key: string | Array<string>, interpolateParams?: Object): Observable<string | any> {
371 if (!isDefined(key) || !key.length) {
372 throw new Error(`Parameter "key" required`);
373 }
374 // check if we are loading a new translation to use
375 if (this.pending) {
376 return this.loadingTranslations.pipe(
377 concatMap((res: any) => {
378 res = this.getParsedResult(res, key, interpolateParams);
379 return isObservable(res) ? res : of(res);
380 }),
381 );
382 } else {
383 let res = this.getParsedResult(this.translations[this.currentLang], key, interpolateParams);
384 return isObservable(res) ? res : of(res);
385 }
386 }
387
388 /**
389 * Returns a stream of translated values of a key (or an array of keys) which updates
390 * whenever the translation changes.
391 * @returns A stream of the translated key, or an object of translated keys
392 */
393 public getStreamOnTranslationChange(key: string | Array<string>, interpolateParams?: Object): Observable<string | any> {
394 if (!isDefined(key) || !key.length) {
395 throw new Error(`Parameter "key" required`);
396 }
397
398 return concat(
399 defer(() => this.get(key, interpolateParams)),
400 this.onTranslationChange.pipe(
401 switchMap((event: TranslationChangeEvent) => {
402 const res = this.getParsedResult(event.translations, key, interpolateParams);
403 if (typeof res.subscribe === 'function') {
404 return res;
405 } else {
406 return of(res);
407 }
408 })
409 )
410 );
411 }
412
413 /**
414 * Returns a stream of translated values of a key (or an array of keys) which updates
415 * whenever the language changes.
416 * @returns A stream of the translated key, or an object of translated keys
417 */
418 public stream(key: string | Array<string>, interpolateParams?: Object): Observable<string | any> {
419 if (!isDefined(key) || !key.length) {
420 throw new Error(`Parameter "key" required`);
421 }
422
423 return concat(
424 defer(() => this.get(key, interpolateParams)),
425 this.onLangChange.pipe(
426 switchMap((event: LangChangeEvent) => {
427 const res = this.getParsedResult(event.translations, key, interpolateParams);
428 return isObservable(res) ? res : of(res);
429 })
430 ));
431 }
432
433 /**
434 * Returns a translation instantly from the internal state of loaded translation.
435 * All rules regarding the current language, the preferred language of even fallback languages will be used except any promise handling.
436 */
437 public instant(key: string | Array<string>, interpolateParams?: Object): string | any {
438 if (!isDefined(key) || !key.length) {
439 throw new Error(`Parameter "key" required`);
440 }
441
442 let res = this.getParsedResult(this.translations[this.currentLang], key, interpolateParams);
443 if (isObservable(res)) {
444 if (key instanceof Array) {
445 let obj: any = {};
446 key.forEach((value: string, index: number) => {
447 obj[key[index]] = key[index];
448 });
449 return obj;
450 }
451 return key;
452 } else {
453 return res;
454 }
455 }
456
457 /**
458 * Sets the translated value of a key, after compiling it
459 */
460 public set(key: string, value: string, lang: string = this.currentLang): void {
461 this.translations[lang][key] = this.compiler.compile(value, lang);
462 this.updateLangs();
463 this.onTranslationChange.emit({lang: lang, translations: this.translations[lang]});
464 }
465
466 /**
467 * Changes the current lang
468 */
469 private changeLang(lang: string): void {
470 this.currentLang = lang;
471 this.onLangChange.emit({lang: lang, translations: this.translations[lang]});
472
473 // if there is no default lang, use the one that we just set
474 if (this.defaultLang == null) {
475 this.changeDefaultLang(lang);
476 }
477 }
478
479 /**
480 * Changes the default lang
481 */
482 private changeDefaultLang(lang: string): void {
483 this.defaultLang = lang;
484 this.onDefaultLangChange.emit({lang: lang, translations: this.translations[lang]});
485 }
486
487 /**
488 * Allows to reload the lang file from the file
489 */
490 public reloadLang(lang: string): Observable<any> {
491 this.resetLang(lang);
492 return this.getTranslation(lang);
493 }
494
495 /**
496 * Deletes inner translation
497 */
498 public resetLang(lang: string): void {
499 this._translationRequests[lang] = undefined;
500 this.translations[lang] = undefined;
501 }
502
503 /**
504 * Returns the language code name from the browser, e.g. "de"
505 */
506 public getBrowserLang(): string | undefined {
507 if (typeof window === 'undefined' || typeof window.navigator === 'undefined') {
508 return undefined;
509 }
510
511 let browserLang: any = window.navigator.languages ? window.navigator.languages[0] : null;
512 browserLang = browserLang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage;
513
514 if (typeof browserLang === 'undefined') {
515 return undefined
516 }
517
518 if (browserLang.indexOf('-') !== -1) {
519 browserLang = browserLang.split('-')[0];
520 }
521
522 if (browserLang.indexOf('_') !== -1) {
523 browserLang = browserLang.split('_')[0];
524 }
525
526 return browserLang;
527 }
528
529 /**
530 * Returns the culture language code name from the browser, e.g. "de-DE"
531 */
532 public getBrowserCultureLang(): string | undefined {
533 if (typeof window === 'undefined' || typeof window.navigator === 'undefined') {
534 return undefined;
535 }
536
537 let browserCultureLang: any = window.navigator.languages ? window.navigator.languages[0] : null;
538 browserCultureLang = browserCultureLang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage;
539
540 return browserCultureLang;
541 }
542}