Catalog alignment
[sdc.git] / catalog-ui / src / app / ng2 / shared / translator / translate.service.ts
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 import { Injectable, Inject } from "@angular/core";
22 import { ITranslateServiceConfig, TranslateServiceConfigToken } from "./translate.service.config";
23 import { Observer, Subscription, Observable, ConnectableObservable } from 'rxjs/Rx';
24 import { HttpClient } from "@angular/common/http";
25
26 export { ITranslateServiceConfig, TranslateServiceConfigToken };
27
28 export interface ITranslateLanguageJson {
29     [index:string]: string;
30 }
31
32 export interface ITranslateArgs {
33     [index:string]: any;
34 }
35
36 export class PhraseTranslator {
37     private _observable:ConnectableObservable<string>;
38     private _observer:Observer<string>;
39     private _languageChangeSubscription:Subscription;
40
41     private _phraseKey:string;
42     private _args:ITranslateArgs;
43     private _language:string;
44
45     private _markForCheck:boolean = false;
46     private _lastParams: {
47         phraseKey: string;
48         args: {[index: string]: any};
49         language: string;
50     } = {
51         phraseKey: undefined,
52         args: undefined,
53         language: undefined
54     };
55
56     constructor(private translateService:TranslateService) {
57         this._observable = Observable.create(observer => {
58             this._observer = observer;
59             this._languageChangeSubscription = this.translateService.languageChangedObservable.subscribe(language => {
60                 // using the active language, then force update
61                 if (!this._language) {
62                     this.update(true);
63                 }
64             });
65         }).publishReplay(1).refCount();
66     }
67
68     public get observable() {
69         return this._observable;
70     }
71
72     public destroy() {
73         this._observer.complete();
74         this._languageChangeSubscription.unsubscribe();
75
76         delete this._observable;
77         delete this._observer;
78         delete this._languageChangeSubscription;
79     }
80
81     public shouldUpdate() : boolean {
82         if (!this._markForCheck) {
83             return false;
84         }
85         this._markForCheck = false;
86         return (
87             this._language !== this._lastParams.language ||
88             this._args !== this._lastParams.args ||
89             this._phraseKey !== this._lastParams.phraseKey
90         );
91     }
92
93     public update(forceUpdate:boolean=false) : void {
94         // only update translation when having subscriptions connected.
95         if (this._observer && !this._observer.closed) {
96             if (forceUpdate || this.shouldUpdate()) {
97                 this._lastParams = {
98                     phraseKey: this._phraseKey,
99                     args: this._args,
100                     language: this._language
101                 };
102                 this._markForCheck = false;
103
104                 const translated = this.translateService.translate(this._phraseKey, this._args, this._language);
105                 this._observer.next(translated);
106             }
107         }
108     }
109
110     private _changeParam(paramKey:string, value:any, update:boolean) : void {
111         this[`_${paramKey}`] = value;
112         this._markForCheck = true;
113         if (update) {
114             this.update();
115         }
116     }
117
118     public changePhraseKey(phraseKey:string, update:boolean=true) : void {
119         this._changeParam('phraseKey', phraseKey, update);
120     }
121
122     public changeArgs(args:ITranslateArgs, update:boolean=true) :void {
123         this._changeParam('args', args, update);
124     }
125
126     public changeLangauge(language:string, update:boolean=true) :void {
127         this._changeParam('language', language, update);
128     }
129
130     public changeParams(phraseKey:string, args:ITranslateArgs={}, language?:string, forceUpdate:boolean=false) {
131         this._phraseKey = phraseKey;
132         this._args = args;
133         this._language = language;
134         this._markForCheck = true;
135         this.update(forceUpdate);
136     }
137 }
138
139
140 @Injectable()
141 export class TranslateService {
142     private _activeLanguage:string;
143     private _languageChangedObservable:ConnectableObservable<string>;
144     private _languageChangedObserver:Observer<string>;
145     private _cacheLanguagesJsons:{[index:string]:ITranslateLanguageJson} = {};
146     private _cacheLanguagesLoaders:{[index:string]:Observable<ITranslateLanguageJson>} = {};
147
148     constructor(@Inject(TranslateServiceConfigToken) private config:ITranslateServiceConfig, private http: HttpClient) {
149         this.initLanguageObservable();
150         this.loadAndActivateLanguage(this.config.defaultLanguage);
151     }
152
153     public get languageChangedObservable() : Observable<string> {
154         return this._languageChangedObservable;
155     }
156
157     public get activeLanguage() {
158         return this._activeLanguage;
159     }
160
161     private initLanguageObservable() {
162         this._languageChangedObservable = ConnectableObservable.create(observer => {
163             this._languageChangedObserver = observer;
164         }).publishReplay(1);  // replay last emitted change on subscribe
165         this._languageChangedObservable.connect();
166     }
167
168     private loadLanguageJsonFile(language:string, emitOnLoad:boolean=true) : Observable<ITranslateLanguageJson> {
169         if (this.config.allowedLanguages.indexOf(language) === -1) {
170             return Observable.throw(`Language "${language}" is not available.`);
171         }
172
173         if (this._cacheLanguagesJsons[language]) {
174             return Observable.of(this._cacheLanguagesJsons[language]);
175         }
176
177         if (!(language in this._cacheLanguagesLoaders)) {
178             const filePath = `${this.config.filePrefix}${language}${this.config.fileSuffix}`;
179             this._cacheLanguagesLoaders[language] = this.http.get<ITranslateLanguageJson>(filePath)
180                 .catch(() => Observable.throw(`Failed to load language file for "${language}"`))
181                 .publish();
182             (<ConnectableObservable<ITranslateLanguageJson>>this._cacheLanguagesLoaders[language]).connect();
183             this._cacheLanguagesLoaders[language].subscribe(languageJson => {
184                     this._cacheLanguagesJsons[language] = languageJson;
185                     delete this._cacheLanguagesLoaders[language];
186                     if (emitOnLoad) {
187                         this._languageChangedObserver.next(language);
188                     }
189                     return languageJson;
190                 });
191         }
192         return this._cacheLanguagesLoaders[language];
193     }
194
195     public activateLanguage(language:string) : boolean {
196         if (this._cacheLanguagesJsons[language]) {
197             if (language !== this._activeLanguage) {
198                 this._activeLanguage = language;
199                 this._languageChangedObserver.next(this._activeLanguage);
200             }
201             return true;
202         }
203         return false;
204     }
205
206     public loadAndActivateLanguage(language:string) : void {
207
208         const loadLanguageObservable = this.loadLanguageJsonFile(language, false);
209         loadLanguageObservable.subscribe(() => {
210             this.activateLanguage(language);
211         }, () => {});
212     }
213
214     public translate(phraseKey:string, args:ITranslateArgs={}, language:string=this._activeLanguage) : string {
215         const phrase:string = (this._cacheLanguagesJsons[language] || {})[phraseKey] || '';
216         let translated:string;
217         if (typeof(phrase) === 'string') {
218             translated = phrase
219                 .replace(
220                     /(^|[^\\]|\\\\)\{\{(\w+)\}\}/g,
221                     (m, p1, p2) => `${p1}${args[p2]||''}`
222                 )
223                 .replace('\\{{', '{{')
224                 .replace('\\\\', '\\');
225         }
226         return translated;
227     }
228
229     public createPhraseTranslator(phraseKey?:string, args?:ITranslateArgs, language?:string) : PhraseTranslator {
230         const phraseTranslator = new PhraseTranslator(this);
231         phraseTranslator.changeParams(phraseKey, args, language);
232         return phraseTranslator;
233     }
234 }