Refactor portal language code (#16795)

* Refactor portal language code

This PR makes the following improvements:

  * The language code is DRYed up by defining `supportedLangs` in terms of `LANGUAGES` (previously `languageNames`).

  * The language selection dropdown code is DRYed up similarly.

  * The Angular locale registration code is DRYed up similarly: the omission of a supported language is now a static type error.

The above improvements mean that it's now impossible to forget to include a supported language in any of those contexts.

Furthermore:

  * The type of supported languages is replaced by a more accurate one than `string`, namely `SupportedLanguage`.

  * The value acquired from localStorage will never be used unless it is in fact a supported language. (Today, the GUI breaks pretty badly and errors are spammed in the console if localStorage contains an invalid value.)

  * Redundant implicit existence checks such as `localStorage &&` and `browserCultureLang &&` are removed.

  * The implementation of `initLangauge` is generally simplified and clarified.

Signed-off-by: Simon Alling <alling.simon@gmail.com>

* Restore accidentally deleted date check

Signed-off-by: Simon Alling <alling.simon@gmail.com>
This commit is contained in:
Simon Alling 2022-05-05 07:16:41 +02:00 committed by GitHub
parent ca019f4030
commit bb007f70bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 60 additions and 53 deletions

View File

@ -18,9 +18,9 @@ import { AppConfigService } from './services/app-config.service';
import { ThemeService } from './services/theme.service';
import { CustomStyle, HAS_STYLE_MODE, THEME_ARRAY, ThemeInterface } from './services/theme';
import { clone } from './shared/units/utils';
import { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang, supportedLangs } from "./shared/entities/shared.const";
import { forkJoin, Observable } from "rxjs";
import { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang, supportedLangs, SupportedLanguage } from "./shared/entities/shared.const";
import { SkinableConfig } from "./services/skinable-config.service";
import { isSupportedLanguage } from './shared/units/shared.utils';
@ -75,20 +75,18 @@ export class AppComponent {
initLanguage() {
this.translate.addLangs(supportedLangs);
this.translate.setDefaultLang(DeFaultLang);
let selectedLang: string = DeFaultLang;
if (localStorage && localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY)) {// If user has selected lang, then directly use it
selectedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
let selectedLang: SupportedLanguage;
const savedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
if (isSupportedLanguage(savedLang)) {// If user has selected lang, then directly use it
selectedLang = savedLang;
} else if (savedLang !== null) { // If there is a saved value, but it is not a supported language, warn and use the default language.
console.warn(`Invalid saved language setting ${JSON.stringify(savedLang)}; defaulting to ${JSON.stringify(DeFaultLang)}.`);
selectedLang = DeFaultLang;
} else {// If user has not selected lang, then use browser language(if contained in supportedLangs)
const browserCultureLang: string = this.translate
.getBrowserCultureLang()
.toLowerCase();
if (browserCultureLang && browserCultureLang.trim() !== "") {
if (supportedLangs && supportedLangs.length > 0) {
if (supportedLangs.find(lang => lang === browserCultureLang)) {
selectedLang = browserCultureLang;
}
}
}
selectedLang = isSupportedLanguage(browserCultureLang) ? browserCultureLang : DeFaultLang;
}
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, selectedLang);
// use method will load related language json from backend server

View File

@ -20,14 +20,7 @@
<clr-icon size="10" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("en-us")' [class.lang-selected]='matchLang("en-us")'>English</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("zh-cn")' [class.lang-selected]='matchLang("zh-cn")'>中文简体</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("zh-tw")' [class.lang-selected]='matchLang("zh-tw")'>中文繁體</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("es-es")' [class.lang-selected]='matchLang("es-es")'>Español</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("fr-fr")' [class.lang-selected]='matchLang("fr-fr")'>Français</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("pt-br")' [class.lang-selected]='matchLang("pt-br")'>Português do Brasil</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("tr-tr")' [class.lang-selected]='matchLang("tr-tr")'>Türkçe</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("de-de")' [class.lang-selected]='matchLang("de-de")'>Deutsch</a>
<a *ngFor="let lang of guiLanguages" href="javascript:void(0)" clrDropdownItem (click)='switchLanguage(lang[0])' [class.lang-selected]='matchLang(lang[0])'>{{lang[1]}}</a>
</clr-dropdown-menu>
</clr-dropdown>
<div class="nav-divider"></div>

View File

@ -26,7 +26,8 @@ import {
CommonRoutes,
DEFAULT_LANG_LOCALSTORAGE_KEY,
DeFaultLang,
languageNames,
LANGUAGES,
SupportedLanguage,
} from "../../entities/shared.const";
import { CustomStyle, HAS_STYLE_MODE, StyleMode } from "../../../services/theme";
@ -41,7 +42,8 @@ export class NavigatorComponent implements OnInit {
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
@Output() showDialogModalAction = new EventEmitter<ModalEvent>();
selectedLang: string = DeFaultLang;
readonly guiLanguages = Object.entries(LANGUAGES);
selectedLang: SupportedLanguage = DeFaultLang;
appTitle: string = 'APP_TITLE.HARBOR';
customStyle: CustomStyle;
constructor(
@ -58,7 +60,7 @@ export class NavigatorComponent implements OnInit {
ngOnInit(): void {
// custom skin
this.customStyle = this.skinableConfig.getSkinConfig();
this.selectedLang = this.translate.currentLang;
this.selectedLang = this.translate.currentLang as SupportedLanguage;
if (this.appConfigService.isIntegrationMode()) {
this.appTitle = 'APP_TITLE.VIC';
}
@ -77,7 +79,7 @@ export class NavigatorComponent implements OnInit {
}
public get currentLang(): string {
return languageNames[this.selectedLang];
return LANGUAGES[this.selectedLang];
}
public get admiralLink(): string {
@ -103,8 +105,8 @@ export class NavigatorComponent implements OnInit {
|| config.auth_mode === "oidc_auth")) || (user.user_id === 1 && user.username === "admin"));
}
matchLang(lang: string): boolean {
return lang.trim() === this.selectedLang;
matchLang(lang: SupportedLanguage): boolean {
return lang === this.selectedLang;
}
// Open the account setting dialog
@ -146,7 +148,7 @@ export class NavigatorComponent implements OnInit {
}
// Switch languages
switchLanguage(lang: string): void {
switchLanguage(lang: SupportedLanguage): void {
this.selectedLang = lang;
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang);
// due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module

View File

@ -178,9 +178,9 @@ export const REFRESH_TIME_DIFFERENCE = 10000;
//
export const supportedLangs = ['en-us', 'zh-cn', 'zh-tw', 'es-es', 'fr-fr', 'pt-br', 'tr-tr', 'de-de'];
export const DeFaultLang = "en-us";
export const languageNames = {
export type SupportedLanguage = keyof typeof LANGUAGES;
export const LANGUAGES = {
"en-us": "English",
"zh-cn": "中文简体",
"zh-tw": "中文繁體",
@ -189,7 +189,8 @@ export const languageNames = {
"pt-br": "Português do Brasil",
"tr-tr": "Türkçe",
"de-de": "Deutsch"
};
} as const;
export const supportedLangs = Object.keys(LANGUAGES) as SupportedLanguage[];
/**
* The default cookie key used to store current used language preference.
*/

View File

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DatePipe } from "@angular/common";
import { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang } from "../entities/shared.const";
import { isSupportedLanguage } from '../units/shared.utils';
const baseTimeLine: Date = new Date('1970-1-1');
@ -11,13 +12,11 @@ const baseTimeLine: Date = new Date('1970-1-1');
export class HarborDatetimePipe implements PipeTransform {
transform(value: any, format?: string): string {
let lang: string = DeFaultLang;
if (localStorage && localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY)) {
lang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
}
if (value && value <= baseTimeLine) {// invalid date
return '-';
}
const savedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
const lang = isSupportedLanguage(savedLang) ? savedLang : DeFaultLang;
// default format medium
return new DatePipe(lang).transform(value, format ? format : 'medium');
}

View File

@ -25,8 +25,8 @@ import {
} from "../units/utils";
import { FlushAll } from "../units/cache-util";
import { SignInCredential } from "../../account/sign-in/sign-in-credential";
import { DeFaultLang } from "../entities/shared.const";
import { ProjectMemberEntity } from "../../../../ng-swagger-gen/models/project-member-entity";
import { DeFaultLang } from "../entities/shared.const";
const signInUrl = '/c/login';

View File

@ -69,25 +69,32 @@ import { HttpClientTestingModule } from "@angular/common/http/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HarborDatetimePipe } from './pipes/harbor-datetime.pipe';
import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component';
import { registerLocaleData } from "@angular/common";
import zh_cn from '@angular/common/locales/zh-Hans';
import zh_tw from '@angular/common/locales/zh-Hans-HK';
import es from '@angular/common/locales/es';
import localeFr from '@angular/common/locales/fr';
import localePt from '@angular/common/locales/pt-PT';
import localeTr from '@angular/common/locales/tr';
import localeDe from '@angular/common/locales/de';
import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component';
// add locale data for supported languages ['en-us', 'zh-cn', 'zh-tw', 'es-es', 'fr-fr', 'pt-br', 'tr-tr', 'de-de'];
// en-us defaulted supported
registerLocaleData(zh_cn, 'zh-cn');
registerLocaleData(zh_tw, 'zh-tw');
registerLocaleData(es, 'es-es');
registerLocaleData(localeFr, 'fr-fr');
registerLocaleData(localePt, 'pt-br');
registerLocaleData(localeTr, 'tr-tr');
registerLocaleData(localeDe, 'de-de');
import locale_en from "@angular/common/locales/en";
import locale_zh_CN from "@angular/common/locales/zh-Hans";
import locale_zh_TW from "@angular/common/locales/zh-Hans-HK";
import locale_es from "@angular/common/locales/es";
import locale_fr from "@angular/common/locales/fr";
import locale_pt from "@angular/common/locales/pt-PT";
import locale_tr from "@angular/common/locales/tr";
import locale_de from "@angular/common/locales/de";
import { SupportedLanguage } from "./entities/shared.const";
const localesForSupportedLangs: Record<SupportedLanguage, unknown[]> = {
"en-us": locale_en,
"zh-cn": locale_zh_CN,
"zh-tw": locale_zh_TW,
"es-es": locale_es,
"fr-fr": locale_fr,
"pt-br": locale_pt,
"tr-tr": locale_tr,
"de-de": locale_de,
};
for (const [ lang, locale ] of Object.entries(localesForSupportedLangs)) {
registerLocaleData(locale, lang);
}
// ClarityIcons is publicly accessible from the browser's window object.

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { NgForm } from '@angular/forms';
import { MessageService } from '../components/global-message/message.service';
import { AlertType, httpStatusCode } from "../entities/shared.const";
import { AlertType, httpStatusCode, SupportedLanguage, LANGUAGES } from "../entities/shared.const";
/**
* To check if form is empty
@ -34,6 +34,13 @@ export const isEmptyForm = function (ngForm: NgForm): boolean {
return true;
};
/**
* A type guard that checks if the given value is a supported language.
*/
export function isSupportedLanguage(x: unknown): x is SupportedLanguage {
return Object.keys(LANGUAGES).some(lang => x === lang);
}
/**
* Hanlde the 401 and 403 code
*