From 72c275c97b03a4201fae0e6caa8308d843bde1a2 Mon Sep 17 00:00:00 2001 From: Amir Ajorloo Date: Thu, 26 Nov 2020 18:37:38 +0330 Subject: [PATCH] Add change language option. (#5693) * Add change language option. Move available languages to index.config. Make lang value in cookie the most important, then the env variable and the least important is the default browser header. * Update zh translations based on helight suggestions. * Refactor code and fix caching issue * Small optimizations Co-authored-by: Sebastian Florek --- aio/scripts/build.sh | 11 ++++++ angular.json | 1 + i18n/de/messages.de.xlf | 16 ++++++++ i18n/fr/messages.fr.xlf | 16 ++++++++ i18n/ja/messages.ja.xlf | 16 ++++++++ i18n/ko/messages.ko.xlf | 16 ++++++++ i18n/messages.xlf | 14 +++++++ i18n/zh-Hans/messages.zh-Hans.xlf | 16 ++++++++ i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf | 16 ++++++++ i18n/zh-Hant/messages.zh-Hant.xlf | 16 ++++++++ src/app/backend/handler/localehandler.go | 16 +++++--- .../common/components/namespace/component.ts | 26 ++++++------- .../common/services/global/authentication.ts | 27 +++++++------ .../common/services/global/interceptor.ts | 10 ++--- .../common/services/global/namespace.ts | 10 +++-- .../from/form/createnamespace/dialog.ts | 9 ++--- .../create/from/form/createsecret/dialog.ts | 9 ++--- src/app/frontend/index.config.ts | 38 +++++++++++++++++++ src/app/frontend/settings/local/component.ts | 30 ++++++++++++++- src/app/frontend/settings/local/template.html | 15 ++++++++ src/app/frontend/typings/frontendapi.ts | 5 +++ 21 files changed, 279 insertions(+), 54 deletions(-) diff --git a/aio/scripts/build.sh b/aio/scripts/build.sh index 98c5bc2c6..fb54b6018 100755 --- a/aio/scripts/build.sh +++ b/aio/scripts/build.sh @@ -36,6 +36,17 @@ function build::frontend { --prod \ --localize \ --outputPath=${FRONTEND_DIR} + + # Avoid locale caching due to the same output file naming + # We'll add language code prefix to the generated main javascript file. + languages=($(ls ${FRONTEND_DIR})) + for language in "${languages[@]}"; do + localeDir=${FRONTEND_DIR}/${language} + filename=("$(find "${localeDir}" -name 'main.*.js' -exec basename {} \;)") + + mv "${localeDir}/${filename}" "${localeDir}/${language}.${filename}" + sed -i "s/${filename}/${language}.${filename}/" "${localeDir}/index.html" + done } function build::backend { diff --git a/angular.json b/angular.json index 261e620e6..8f2d3d3cb 100644 --- a/angular.json +++ b/angular.json @@ -100,6 +100,7 @@ "extractCss": true, "namedChunks": false, "aot": true, + "localize": true, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, diff --git a/i18n/de/messages.de.xlf b/i18n/de/messages.de.xlf index e86c1c044..031abb919 100644 --- a/i18n/de/messages.de.xlf +++ b/i18n/de/messages.de.xlf @@ -5442,6 +5442,22 @@ 27 + + Language + Sprache + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + Ändern Sie die Sprache des Dashboards + + ../src/app/frontend/settings/local/template.html + 39 + + diff --git a/i18n/fr/messages.fr.xlf b/i18n/fr/messages.fr.xlf index 2abcaf86a..69993283f 100644 --- a/i18n/fr/messages.fr.xlf +++ b/i18n/fr/messages.fr.xlf @@ -5456,6 +5456,22 @@ 27 + + Language + Langue + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + Changer la langue du tableau de bord + + ../src/app/frontend/settings/local/template.html + 39 + + diff --git a/i18n/ja/messages.ja.xlf b/i18n/ja/messages.ja.xlf index 4b976d86c..005a9e5cb 100644 --- a/i18n/ja/messages.ja.xlf +++ b/i18n/ja/messages.ja.xlf @@ -5402,6 +5402,22 @@ 27 + + Language + 言語 + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + ダッシュボードの言語を変更する + + ../src/app/frontend/settings/local/template.html + 39 + + Shell in diff --git a/i18n/ko/messages.ko.xlf b/i18n/ko/messages.ko.xlf index 22acd697e..5c176d308 100644 --- a/i18n/ko/messages.ko.xlf +++ b/i18n/ko/messages.ko.xlf @@ -5514,6 +5514,22 @@ 27 + + Language + 언어 + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + 대시 보드의 언어 변경 + + ../src/app/frontend/settings/local/template.html + 39 + + Shell in diff --git a/i18n/messages.xlf b/i18n/messages.xlf index 6f1f0f367..3c4ff3605 100644 --- a/i18n/messages.xlf +++ b/i18n/messages.xlf @@ -3748,6 +3748,20 @@ 27 + + Language + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + + ../src/app/frontend/settings/local/template.html + 39 + + About diff --git a/i18n/zh-Hans/messages.zh-Hans.xlf b/i18n/zh-Hans/messages.zh-Hans.xlf index 9c7e60162..deec0122f 100644 --- a/i18n/zh-Hans/messages.zh-Hans.xlf +++ b/i18n/zh-Hans/messages.zh-Hans.xlf @@ -5517,6 +5517,22 @@ 27 + + Language + 语言 + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + 更改 dashboard 的语言 + + ../src/app/frontend/settings/local/template.html + 39 + + Shell in diff --git a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf index 4c2e93643..7e22cc842 100644 --- a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf +++ b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf @@ -5517,6 +5517,22 @@ 27 + + Language + 语言 + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + 更改 dashboard 的语言 + + ../src/app/frontend/settings/local/template.html + 39 + + Shell in diff --git a/i18n/zh-Hant/messages.zh-Hant.xlf b/i18n/zh-Hant/messages.zh-Hant.xlf index 1814d4fb3..f8b9a9322 100644 --- a/i18n/zh-Hant/messages.zh-Hant.xlf +++ b/i18n/zh-Hant/messages.zh-Hant.xlf @@ -5517,6 +5517,22 @@ 27 + + Language + 語言 + + ../src/app/frontend/settings/local/template.html + 37 + + + + Change the language of the dashboard + 更改 dashboard 的語言 + + ../src/app/frontend/settings/local/template.html + 39 + + Shell in diff --git a/src/app/backend/handler/localehandler.go b/src/app/backend/handler/localehandler.go index a49bef61d..5286ff04b 100644 --- a/src/app/backend/handler/localehandler.go +++ b/src/app/backend/handler/localehandler.go @@ -89,13 +89,17 @@ func getAssetsDir() string { // LocaleHandler serves different html versions based on the Accept-Language header. func (handler *LocaleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.URL.EscapedPath() == "/" || r.URL.EscapedPath() == "/index.html" { - // Do not store the html page in the cache. If the user is to click on 'switch language', - // we want a different index.html (for the right locale) to be served when the page refreshes. - w.Header().Add("Cache-Control", "no-store") + acceptLanguage := "" + cookie, err := r.Cookie("lang") + if err == nil { + acceptLanguage = cookie.Value } - acceptLanguage := os.Getenv("ACCEPT_LANGUAGE") - if acceptLanguage == "" { + + if len(acceptLanguage) == 0 { + acceptLanguage = os.Getenv("ACCEPT_LANGUAGE") + } + + if len(acceptLanguage) == 0 { acceptLanguage = r.Header.Get("Accept-Language") } diff --git a/src/app/frontend/common/components/namespace/component.ts b/src/app/frontend/common/components/namespace/component.ts index b0c17b1cd..9c3b28543 100644 --- a/src/app/frontend/common/components/namespace/component.ts +++ b/src/app/frontend/common/components/namespace/component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {MatSelect} from '@angular/material/select'; import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; @@ -20,15 +20,14 @@ import {NamespaceList} from '@api/backendapi'; import {Subject} from 'rxjs'; import {distinctUntilChanged, filter, startWith, switchMap, takeUntil} from 'rxjs/operators'; -import {CONFIG} from '../../../index.config'; +import {Config, CONFIG_DI_TOKEN} from '../../../index.config'; import {NAMESPACE_STATE_PARAM} from '../../params/params'; import {HistoryService} from '../../services/global/history'; import {NamespaceService} from '../../services/global/namespace'; -import {NotificationSeverity, NotificationsService} from '../../services/global/notifications'; +import {NotificationsService} from '../../services/global/notifications'; import {KdStateService} from '../../services/global/state'; import {EndpointManager, Resource} from '../../services/resource/endpoint'; import {ResourceService} from '../../services/resource/resource'; - import {NamespaceChangeDialog} from './changedialog/dialog'; @Component({ @@ -57,12 +56,13 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { private readonly dialog_: MatDialog, private readonly kdState_: KdStateService, private readonly notifications_: NotificationsService, - private readonly _activatedRoute: ActivatedRoute, - private readonly _historyService: HistoryService + private readonly activatedRoute_: ActivatedRoute, + private readonly historyService_: HistoryService, + @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config ) {} ngOnInit(): void { - this._activatedRoute.queryParams.pipe(takeUntil(this.unsubscribe_)).subscribe(params => { + this.activatedRoute_.queryParams.pipe(takeUntil(this.unsubscribe_)).subscribe(params => { const namespace = params.namespace; if (!namespace) { this.setDefaultQueryParams_(); @@ -183,12 +183,12 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { if (confirmed) { this.selectedNamespace = this._getCurrentResourceNamespaceParam(); this.router_.navigate([], { - relativeTo: this._activatedRoute, + relativeTo: this.activatedRoute_, queryParams: {[NAMESPACE_STATE_PARAM]: this.selectedNamespace}, queryParamsHandling: 'merge', }); } else { - this._historyService.goToPreviousState('overview'); + this.historyService_.goToPreviousState('overview'); } }); } @@ -205,7 +205,7 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { } else { // Change only the namespace as currently not on details view. this.router_.navigate([], { - relativeTo: this._activatedRoute, + relativeTo: this.activatedRoute_, queryParams: {[NAMESPACE_STATE_PARAM]: namespace}, queryParamsHandling: 'merge', }); @@ -229,7 +229,7 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { } private _getCurrentRoute(): ActivatedRoute { - let route = this._activatedRoute.root; + let route = this.activatedRoute_.root; while (route && route.firstChild) { route = route.firstChild; } @@ -247,8 +247,8 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { } setDefaultQueryParams_() { - this.router_.navigate([this._activatedRoute.snapshot.url], { - queryParams: {[NAMESPACE_STATE_PARAM]: CONFIG.defaultNamespace}, + this.router_.navigate([this.activatedRoute_.snapshot.url], { + queryParams: {[NAMESPACE_STATE_PARAM]: this.appConfig_.defaultNamespace}, queryParamsHandling: 'merge', }); } diff --git a/src/app/frontend/common/services/global/authentication.ts b/src/app/frontend/common/services/global/authentication.ts index 2aaf8b9fa..f0d199d55 100644 --- a/src/app/frontend/common/services/global/authentication.ts +++ b/src/app/frontend/common/services/global/authentication.ts @@ -13,7 +13,7 @@ // limitations under the License. import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {Router} from '@angular/router'; import {CookieService} from 'ngx-cookie-service'; import {of} from 'rxjs'; @@ -21,7 +21,7 @@ import {Observable} from 'rxjs'; import {first, switchMap} from 'rxjs/operators'; import {AuthResponse, CsrfToken, LoginSpec, LoginStatus} from 'typings/backendapi'; -import {CONFIG} from '../../../index.config'; +import {Config, CONFIG_DI_TOKEN} from '../../../index.config'; import {K8SError} from '../../errors/errors'; import {CsrfTokenService} from './csrftoken'; @@ -29,14 +29,13 @@ import {KdStateService} from './state'; @Injectable() export class AuthService { - private readonly _config = CONFIG; - constructor( private readonly cookies_: CookieService, private readonly router_: Router, private readonly http_: HttpClient, private readonly csrfTokenService_: CsrfTokenService, - private readonly stateService_: KdStateService + private readonly stateService_: KdStateService, + @Inject(CONFIG_DI_TOKEN) private readonly config_: Config ) { this.init_(); } @@ -55,17 +54,17 @@ export class AuthService { } if (this.isCurrentProtocolSecure_()) { - this.cookies_.set(this._config.authTokenCookieName, token, null, null, null, true, 'Strict'); + this.cookies_.set(this.config_.authTokenCookieName, token, null, null, null, true, 'Strict'); return; } if (this.isCurrentDomainSecure_()) { - this.cookies_.set(this._config.authTokenCookieName, token, null, null, location.hostname, false, 'Strict'); + this.cookies_.set(this.config_.authTokenCookieName, token, null, null, location.hostname, false, 'Strict'); } } private getTokenCookie_(): string { - return this.cookies_.get(this._config.authTokenCookieName) || ''; + return this.cookies_.get(this.config_.authTokenCookieName) || ''; } private isCurrentDomainSecure_(): boolean { @@ -77,8 +76,8 @@ export class AuthService { } removeAuthCookies(): void { - this.cookies_.delete(this._config.authTokenCookieName); - this.cookies_.delete(this._config.skipLoginPageCookieName); + this.cookies_.delete(this.config_.authTokenCookieName); + this.cookies_.delete(this.config_.skipLoginPageCookieName); } /** @@ -90,7 +89,7 @@ export class AuthService { .pipe( switchMap((csrfToken: CsrfToken) => this.http_.post('api/v1/login', loginSpec, { - headers: new HttpHeaders().set(this._config.csrfHeaderName, csrfToken.token), + headers: new HttpHeaders().set(this.config_.csrfHeaderName, csrfToken.token), }) ) ) @@ -126,7 +125,7 @@ export class AuthService { 'api/v1/token/refresh', {jweToken: token}, { - headers: new HttpHeaders().set(this._config.csrfHeaderName, csrfToken.token), + headers: new HttpHeaders().set(this.config_.csrfHeaderName, csrfToken.token), } ); }) @@ -161,7 +160,7 @@ export class AuthService { skipLoginPage(skip: boolean): void { this.removeAuthCookies(); - this.cookies_.set(this._config.skipLoginPageCookieName, skip.toString(), null, null, null, false, 'Strict'); + this.cookies_.set(this.config_.skipLoginPageCookieName, skip.toString(), null, null, null, false, 'Strict'); } /** @@ -170,7 +169,7 @@ export class AuthService { * In case cookie is not set login page will also be visible. */ isLoginPageEnabled(): boolean { - return !(this.cookies_.get(this._config.skipLoginPageCookieName) === 'true'); + return !(this.cookies_.get(this.config_.skipLoginPageCookieName) === 'true'); } /** diff --git a/src/app/frontend/common/services/global/interceptor.ts b/src/app/frontend/common/services/global/interceptor.ts index 2dc7d827c..209170de3 100644 --- a/src/app/frontend/common/services/global/interceptor.ts +++ b/src/app/frontend/common/services/global/interceptor.ts @@ -13,22 +13,22 @@ // limitations under the License. import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {CookieService} from 'ngx-cookie-service'; import {Observable} from 'rxjs'; -import {CONFIG} from '../../../index.config'; +import {Config, CONFIG_DI_TOKEN} from '../../../index.config'; @Injectable() export class AuthInterceptor implements HttpInterceptor { - constructor(private readonly cookies_: CookieService) {} + constructor(private readonly cookies_: CookieService, @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { - const authCookie = this.cookies_.get(CONFIG.authTokenCookieName); + const authCookie = this.cookies_.get(this.appConfig_.authTokenCookieName); // Filter requests made to our backend starting with 'api/v1' and append request header // with token stored in a cookie. if (req.url.startsWith('api/v1') && authCookie.length) { const authReq = req.clone({ - headers: req.headers.set(CONFIG.authTokenHeaderName, authCookie), + headers: req.headers.set(this.appConfig_.authTokenHeaderName, authCookie), }); return next.handle(authReq); diff --git a/src/app/frontend/common/services/global/namespace.ts b/src/app/frontend/common/services/global/namespace.ts index 66345038c..9e9104f73 100644 --- a/src/app/frontend/common/services/global/namespace.ts +++ b/src/app/frontend/common/services/global/namespace.ts @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {EventEmitter, Injectable} from '@angular/core'; -import {CONFIG} from '../../../index.config'; +import {EventEmitter, Inject, Injectable} from '@angular/core'; +import {Config, CONFIG_DI_TOKEN} from '../../../index.config'; @Injectable() export class NamespaceService { onNamespaceChangeEvent = new EventEmitter(); + constructor(@Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config) {} + /** * Internal key for empty selection. To differentiate empty string from nulls. */ @@ -37,7 +39,7 @@ export class NamespaceService { } current(): string { - return this.currentNamespace_ || CONFIG.defaultNamespace; + return this.currentNamespace_ || this.appConfig_.defaultNamespace; } getAllNamespacesKey(): string { @@ -45,7 +47,7 @@ export class NamespaceService { } getDefaultNamespace(): string { - return CONFIG.defaultNamespace; + return this.appConfig_.defaultNamespace; } isNamespaceValid(namespace: string): boolean { diff --git a/src/app/frontend/create/from/form/createnamespace/dialog.ts b/src/app/frontend/create/from/form/createnamespace/dialog.ts index f8fc4f37b..0547ae65f 100644 --- a/src/app/frontend/create/from/form/createnamespace/dialog.ts +++ b/src/app/frontend/create/from/form/createnamespace/dialog.ts @@ -19,7 +19,7 @@ import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog import {switchMap} from 'rxjs/operators'; import {AlertDialog, AlertDialogConfig} from '../../../../common/dialogs/alert/dialog'; import {CsrfTokenService} from '../../../../common/services/global/csrftoken'; -import {CONFIG} from '../../../../index.config'; +import {Config, CONFIG_DI_TOKEN} from '../../../../index.config'; export interface CreateNamespaceDialogMeta { namespaces: string[]; @@ -35,8 +35,6 @@ export interface CreateNamespaceDialogMeta { export class CreateNamespaceDialog implements OnInit { form: FormGroup; - private readonly config_ = CONFIG; - /** * Max-length validation rule for namespace */ @@ -52,7 +50,8 @@ export class CreateNamespaceDialog implements OnInit { private readonly http_: HttpClient, private readonly csrfToken_: CsrfTokenService, private readonly matDialog_: MatDialog, - private readonly fb_: FormBuilder + private readonly fb_: FormBuilder, + @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config ) {} ngOnInit(): void { @@ -83,7 +82,7 @@ export class CreateNamespaceDialog implements OnInit { this.http_.post<{valid: boolean}>( 'api/v1/namespace', {...namespaceSpec}, - {headers: new HttpHeaders().set(this.config_.csrfHeaderName, csrfToken.token)} + {headers: new HttpHeaders().set(this.appConfig_.csrfHeaderName, csrfToken.token)} ) ) ) diff --git a/src/app/frontend/create/from/form/createsecret/dialog.ts b/src/app/frontend/create/from/form/createsecret/dialog.ts index 866642b4b..4c63904da 100644 --- a/src/app/frontend/create/from/form/createsecret/dialog.ts +++ b/src/app/frontend/create/from/form/createsecret/dialog.ts @@ -19,7 +19,7 @@ import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog import {switchMap} from 'rxjs/operators'; import {AlertDialog, AlertDialogConfig} from '../../../../common/dialogs/alert/dialog'; import {CsrfTokenService} from '../../../../common/services/global/csrftoken'; -import {CONFIG} from '../../../../index.config'; +import {Config, CONFIG_DI_TOKEN} from '../../../../index.config'; export interface CreateSecretDialogMeta { namespace: string; @@ -32,8 +32,6 @@ export interface CreateSecretDialogMeta { export class CreateSecretDialog implements OnInit { form: FormGroup; - private readonly config_ = CONFIG; - /** * Max-length validation rule for secretName. */ @@ -55,7 +53,8 @@ export class CreateSecretDialog implements OnInit { private readonly http_: HttpClient, private readonly csrfToken_: CsrfTokenService, private readonly matDialog_: MatDialog, - private readonly fb_: FormBuilder + private readonly fb_: FormBuilder, + @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config ) {} ngOnInit(): void { @@ -98,7 +97,7 @@ export class CreateSecretDialog implements OnInit { this.http_.post<{valid: boolean}>( 'api/v1/secret/', {...secretSpec}, - {headers: new HttpHeaders().set(this.config_.csrfHeaderName, csrfToken.token)} + {headers: new HttpHeaders().set(this.appConfig_.csrfHeaderName, csrfToken.token)} ) ) ) diff --git a/src/app/frontend/index.config.ts b/src/app/frontend/index.config.ts index 0e2650bbb..f4e6002ed 100644 --- a/src/app/frontend/index.config.ts +++ b/src/app/frontend/index.config.ts @@ -14,9 +14,41 @@ import {InjectionToken} from '@angular/core'; import {MatTooltipDefaultOptions} from '@angular/material/tooltip'; +import {LanguageConfig} from 'typings/frontendapi'; export const CONFIG_DI_TOKEN = new InjectionToken('kd.config'); +const SUPPORTED_LANGUAGES: LanguageConfig[] = [ + { + label: 'German', + value: 'de', + }, + { + label: 'English', + value: 'en', + }, + { + label: 'Japanese', + value: 'ja', + }, + { + label: 'Korean', + value: 'ko', + }, + { + label: 'Chinese Simplified', + value: 'zh-Hans', + }, + { + label: 'Chinese Traditional', + value: 'zh-Hant', + }, + { + label: 'Chinese Traditional Hong Kong', + value: 'zh-Hant-HK', + }, +]; + export interface Config { authTokenCookieName: string; skipLoginPageCookieName: string; @@ -24,6 +56,9 @@ export interface Config { authTokenHeaderName: string; defaultNamespace: string; authModeCookieName: string; + supportedLanguages: LanguageConfig[]; + defaultLanguage: string; + languageCookieName: string; } export const CONFIG: Config = { @@ -33,6 +68,9 @@ export const CONFIG: Config = { skipLoginPageCookieName: 'skipLoginPage', defaultNamespace: 'default', authModeCookieName: 'authMode', + supportedLanguages: SUPPORTED_LANGUAGES, + defaultLanguage: 'en', + languageCookieName: 'lang', }; // Override default material tooltip values. diff --git a/src/app/frontend/settings/local/component.ts b/src/app/frontend/settings/local/component.ts index b3c36532b..c94905453 100644 --- a/src/app/frontend/settings/local/component.ts +++ b/src/app/frontend/settings/local/component.ts @@ -12,21 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Component, OnInit} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {Component, Inject, OnInit, ViewChild} from '@angular/core'; +import {MatSelect} from '@angular/material/select'; import {LocalSettings} from '@api/backendapi'; +import {LanguageConfig} from '@api/frontendapi'; +import {CookieService} from 'ngx-cookie-service'; import {LocalSettingsService} from '../../common/services/global/localsettings'; +import {environment} from '../../environments/environment'; +import {Config, CONFIG_DI_TOKEN} from '../../index.config'; @Component({selector: 'kd-local-settings', templateUrl: './template.html'}) export class LocalSettingsComponent implements OnInit { settings: LocalSettings = {} as LocalSettings; + languages: LanguageConfig[] = []; + selectedLanguage: string; - constructor(private readonly settings_: LocalSettingsService) {} + @ViewChild(MatSelect, {static: true}) private readonly select_: MatSelect; + + constructor( + private readonly settings_: LocalSettingsService, + private readonly cookies_: CookieService, + @Inject(DOCUMENT) private readonly document_: Document, + @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: Config + ) {} ngOnInit(): void { + this.languages = this.appConfig_.supportedLanguages; this.settings = this.settings_.get(); + this.selectedLanguage = this.cookies_.get(this.appConfig_.languageCookieName) || this.appConfig_.defaultLanguage; } onThemeChange(): void { this.settings_.handleThemeChange(this.settings.isThemeDark); } + + onLanguageSelected(selectedLanguage: string) { + this.cookies_.set(this.appConfig_.languageCookieName, selectedLanguage); + this.document_.location.reload(); + } + + isProdMode(): boolean { + return environment.production; + } } diff --git a/src/app/frontend/settings/local/template.html b/src/app/frontend/settings/local/template.html index 7c592f44d..dd7f940a0 100644 --- a/src/app/frontend/settings/local/template.html +++ b/src/app/frontend/settings/local/template.html @@ -33,5 +33,20 @@ limitations under the License. (change)="onThemeChange()"> + + + + + {{language.label}} + + + + diff --git a/src/app/frontend/typings/frontendapi.ts b/src/app/frontend/typings/frontendapi.ts index ab411431e..61881de48 100644 --- a/src/app/frontend/typings/frontendapi.ts +++ b/src/app/frontend/typings/frontendapi.ts @@ -140,3 +140,8 @@ export interface ViewportMetadata { target: HTMLElement; visible: boolean; } + +export interface LanguageConfig { + value: string; + label: string; +} -- GitLab