1 | import {EventEmitter, Inject, Injectable, InjectionToken} from "@angular/core";
|
2 | import {concat, forkJoin, isObservable, Observable, of, defer} from "rxjs";
|
3 | import {concatMap, map, shareReplay, switchMap, take} from "rxjs/operators";
|
4 | import {MissingTranslationHandler, MissingTranslationHandlerParams} from "./missing-translation-handler";
|
5 | import {TranslateCompiler} from "./translate.compiler";
|
6 | import {TranslateLoader} from "./translate.loader";
|
7 | import {TranslateParser} from "./translate.parser";
|
8 |
|
9 | import {TranslateStore} from "./translate.store";
|
10 | import {isDefined, mergeDeep} from "./util";
|
11 |
|
12 | export const USE_STORE = new InjectionToken<string>('USE_STORE');
|
13 | export const USE_DEFAULT_LANG = new InjectionToken<string>('USE_DEFAULT_LANG');
|
14 | export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
15 | export const USE_EXTEND = new InjectionToken<string>('USE_EXTEND');
|
16 |
|
17 | export interface TranslationChangeEvent {
|
18 | translations: any;
|
19 | lang: string;
|
20 | }
|
21 |
|
22 | export interface LangChangeEvent {
|
23 | lang: string;
|
24 | translations: any;
|
25 | }
|
26 |
|
27 | export interface DefaultLangChangeEvent {
|
28 | lang: string;
|
29 | translations: any;
|
30 | }
|
31 |
|
32 | declare interface Window {
|
33 | navigator: any;
|
34 | }
|
35 |
|
36 | declare const window: Window;
|
37 |
|
38 | @Injectable()
|
39 | export 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 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | get onTranslationChange(): EventEmitter<TranslationChangeEvent> {
|
58 | return this.isolate ? this._onTranslationChange : this.store.onTranslationChange;
|
59 | }
|
60 |
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | get onLangChange(): EventEmitter<LangChangeEvent> {
|
68 | return this.isolate ? this._onLangChange : this.store.onLangChange;
|
69 | }
|
70 |
|
71 | |
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | get onDefaultLangChange() {
|
78 | return this.isolate ? this._onDefaultLangChange : this.store.onDefaultLangChange;
|
79 | }
|
80 |
|
81 | |
82 |
|
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 |
|
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 |
|
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 |
|
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 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
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 |
|
163 | if (defaultLanguage) {
|
164 | this.setDefaultLang(defaultLanguage);
|
165 | }
|
166 | }
|
167 |
|
168 | |
169 |
|
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 |
|
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 {
|
189 | this.changeDefaultLang(lang);
|
190 | }
|
191 | }
|
192 |
|
193 | |
194 |
|
195 |
|
196 | public getDefaultLang(): string {
|
197 | return this.defaultLang;
|
198 | }
|
199 |
|
200 | |
201 |
|
202 |
|
203 | public use(lang: string): Observable<any> {
|
204 |
|
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 |
|
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 {
|
224 | this.changeLang(lang);
|
225 |
|
226 | return of(this.translations[lang]);
|
227 | }
|
228 | }
|
229 |
|
230 | |
231 |
|
232 |
|
233 | private retrieveTranslations(lang: string): Observable<any> | undefined {
|
234 | let pending: Observable<any> | undefined;
|
235 |
|
236 |
|
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 |
|
247 |
|
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 |
|
279 |
|
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 |
|
294 |
|
295 | public getLangs(): Array<string> {
|
296 | return this.langs;
|
297 | }
|
298 |
|
299 | |
300 |
|
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 |
|
312 |
|
313 | private updateLangs(): void {
|
314 | this.addLangs(Object.keys(this.translations));
|
315 | }
|
316 |
|
317 | |
318 |
|
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 |
|
368 |
|
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 |
|
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 |
|
390 |
|
391 |
|
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 |
|
415 |
|
416 |
|
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 |
|
435 |
|
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 |
|
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 |
|
468 |
|
469 | private changeLang(lang: string): void {
|
470 | this.currentLang = lang;
|
471 | this.onLangChange.emit({lang: lang, translations: this.translations[lang]});
|
472 |
|
473 |
|
474 | if (this.defaultLang == null) {
|
475 | this.changeDefaultLang(lang);
|
476 | }
|
477 | }
|
478 |
|
479 | |
480 |
|
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 |
|
489 |
|
490 | public reloadLang(lang: string): Observable<any> {
|
491 | this.resetLang(lang);
|
492 | return this.getTranslation(lang);
|
493 | }
|
494 |
|
495 | |
496 |
|
497 |
|
498 | public resetLang(lang: string): void {
|
499 | this._translationRequests[lang] = undefined;
|
500 | this.translations[lang] = undefined;
|
501 | }
|
502 |
|
503 | |
504 |
|
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 |
|
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 | }
|