Commit ef21b7d3 authored by Steven Zou's avatar Steven Zou
Browse files

Refactor i18n methods and update readme

parent 5e5a93d5
......@@ -51,6 +51,7 @@ If no parameters are passed to **'forRoot'**, the module will be initialized wit
**Enable components via tags**
* **Registry log view**
```
//No @Input properties
......@@ -58,27 +59,32 @@ If no parameters are passed to **'forRoot'**, the module will be initialized wit
```
* **Replication Management View**
Support two different display scope mode: under specific project or whole system.
If **projectId** is set to the id of specified project, then only show the replication rules bound with the project. Otherwise, show all the rules of the whole system.
**withReplicationJob** is used to determine whether or not show the replication jobs which are relevant with the selected replication rule.
```
<hbr-replication [projectId]="..."></hbr-replication>
<hbr-replication [projectId]="..." [withReplicationJob]='...'></hbr-replication>
```
* **Endpoint Management View**
```
//No @Input properties
<hbr-endpoint></hbr-endpoint>
```
* **Repository and Tag Management View**
```
/*
export interface SessionInfo {
withNotary?: boolean;
hasProjectAdminRole?: boolean;
hasSignedIn?: boolean;
registryUrl?: string;
}
*/
* **Repository and Tag Management View[updating]**
**projectId** is used to specify which projects the repositories are from.
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
<hbr-repository [projectId]="..." [sessionInfo]="..."></hbr-repository>
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
```
<hbr-repository-stackview [projectId]="..." [hasSignedIn]="..." [hasProjectAdminRole]="..."></hbr-repository-stackview>
```
## Configurations
......@@ -88,14 +94,20 @@ All the related configurations are defined in the **HarborModuleConfig** interfa
The base configuration for the module. Mainly used to define the relevant endpoints of services which are in charge of retrieving data from backend APIs. It's a 'OpaqueToken' and defined by 'IServiceConfig' interface. If **config** is not set, the default value will be used.
```
export const DefaultServiceConfig: IServiceConfig = {
systemInfoEndpoint: "/api/system",
repositoryBaseEndpoint: "/api/repositories",
logBaseEndpoint: "/api/logs",
targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication",
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
supportedLangs: DEFAULT_SUPPORTING_LANGS,
enablei18Support: false
enablei18Support: false,
defaultLang: DEFAULT_LANG, //'en-us'
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
supportedLangs: DEFAULT_SUPPORTING_LANGS,//['en-us','zh-cn','es-es']
langMessageLoader: "local",
langMessagePathForHttpLoader: "i18n/langs/",
langMessageFileSuffixForHttpLoader: "-lang.json",
localI18nMessageVariableMap: {}
};
```
If you want to override the related items, declare your own 'IServiceConfig' interface and define the configuration value. E.g: Override 'repositoryBaseEndpoint'
......@@ -113,14 +125,52 @@ HarborLibraryModule.forRoot({
```
It supports partially overriding. For the items not overridden, default values will be adopted. The items contained in **config** are:
* **repositoryBaseEndpoint:** The base endpoint of the service used to handle the repositories of registry and/or tags of repository. Default value is "/api/repositories".
* **logBaseEndpoint:** The base endpoint of the service used to handle the recent access logs. Default is "/api/logs".
* **targetBaseEndpoint:** The base endpoint of the service used to handle the registry endpoints. Default is "/api/targets".
* **replicationRuleEndpoint:** The base endpoint of the service used to handle the replication rules. Default is "/api/policies/replication".
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
* **enablei18Support:** To determine whether or not to enable the i18 multiple languages supporting. Default is false.
* **langMessageLoader:** To determine which loader will be used to load the required lang messages. Support two loaders: One is **'http'**, use async http to load json files with the specified url/path. Another is **'local'**, use local json variable to store the lang message.
* **langMessagePathForHttpLoader:** Define the basic url/path prefix for the loader to find the json files if the 'langMessageLoader' is set to **'http'**. E.g: 'src/i18n/langs'.
* **langMessageFileSuffixForHttpLoader:** Define the suffix of the json file names without lang name if 'langMessageLoader' is set to **'http'**. For example, '-lang.json' is suffix of message file 'en-us-lang.json'.
* **localI18nMessageVariableMap:** If configuration property 'langMessageLoader' is set to **'local'** to load the i18n messages, this property must be defined to tell local JSON loader where to get the related messages. E.g: If declare the following messages storage variables,
```
export const EN_US_LANG: any = {
"APP_TITLE": {
"VMW_HARBOR": "VMware Harbor",
"HARBOR": "Harbor"
}
}
export const ZH_CN_LANG: any = {
"APP_TITLE": {
"VMW_HARBOR": "VMware Harbor中文版",
"HARBOR": "Harbor"
}
}
```
then this property should be set to:
```
{
"en-us": EN_US_LANG,
"zh-cn": ZH_CN_LANG
};
```
**2. errorHandler**
UI components in the library use this interface to pass the errors/warnings/infos/logs to the top component or page. The top component or page can display those information in their message panel or notification system.
If not set, the console will be used as default output approach.
......
......@@ -10,7 +10,7 @@
"pree2e": "webdriver-manager update",
"e2e": "protractor",
"cleanup": "rimraf dist",
"copy": "copyfiles -f LICENSE AUTHORS pkg/package.json dist",
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
"transpile": "ngc -p tsconfig.json",
"package": "rollup -c",
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
......
{
"name": "harbor-ui",
"version": "0.1.1",
"version": "0.1.24",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "Harbor",
"module": "index.js",
......
......@@ -39,9 +39,10 @@ import {
DefaultErrorHandler
} from './error-handler/index';
import { SharedModule } from './shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { TranslateServiceInitializer } from './i18n/index';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
import { TranslateService } from '@ngx-translate/core';
import { CookieService } from 'ngx-cookie';
/**
* Declare default service configuration; all the endpoints will be defined in
......@@ -54,9 +55,14 @@ export const DefaultServiceConfig: IServiceConfig = {
targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication",
enablei18Support: false,
defaultLang: DEFAULT_LANG,
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
supportedLangs: DEFAULT_SUPPORTING_LANGS,
enablei18Support: false
langMessageLoader: "local",
langMessagePathForHttpLoader: "i18n/langs/",
langMessageFileSuffixForHttpLoader: "-lang.json",
localI18nMessageVariableMap: {}
};
/**
......@@ -98,31 +104,15 @@ export interface HarborModuleConfig {
* @param {AppConfigService} configService
* @returns
*/
export function initConfig(translateService: TranslateService, config: IServiceConfig, cookie: CookieService) {
export function initConfig(translateInitializer: TranslateServiceInitializer, config: IServiceConfig) {
return (init);
function init() {
let selectedLang: string = DEFAULT_LANG;
translateService.addLangs(config.supportedLangs ? config.supportedLangs : [DEFAULT_LANG]);
translateService.setDefaultLang(DEFAULT_LANG);
if (config.enablei18Support) {
//If user has selected lang, then directly use it
let langSetting: string = cookie.get(config.langCookieKey ? config.langCookieKey : DEFAULT_LANG_COOKIE_KEY);
if (!langSetting || langSetting.trim() === "") {
//Use browser lang
langSetting = translateService.getBrowserCultureLang().toLowerCase();
}
if (config.supportedLangs && config.supportedLangs.length > 0) {
if (config.supportedLangs.find(lang => lang === langSetting)) {
selectedLang = langSetting;
}
}
}
translateService.use(selectedLang);
console.log('initConfig => ', translateService.currentLang);
translateInitializer.init({
enablei18Support: config.enablei18Support,
supportedLangs: config.supportedLangs,
defaultLang: config.defaultLang,
langCookieKey: config.langCookieKey
});
};
}
......@@ -160,7 +150,8 @@ export function initConfig(translateService: TranslateService, config: IServiceC
LIST_REPLICATION_RULE_DIRECTIVES,
CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES
VULNERABILITY_DIRECTIVES,
TranslateModule
],
providers: []
})
......@@ -179,13 +170,13 @@ export class HarborLibraryModule {
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
//Do initializing
TranslateService,
TranslateServiceInitializer,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [TranslateService, SERVICE_CONFIG],
deps: [TranslateServiceInitializer, SERVICE_CONFIG],
multi: true
},
}
]
};
}
......
export interface i18nConfig {
/**
* The cookie key used to store the current used language preference.
*
* @type {string}
* @memberOf IServiceConfig
*/
langCookieKey?: string,
/**
* Declare what languages are supported.
*
* @type {string[]}
* @memberOf IServiceConfig
*/
supportedLangs?: string[],
/**
* Define the default language the translate service uses.
*
* @type {string}
* @memberOf i18nConfig
*/
defaultLang?: string;
/**
* To determine whether or not to enable the i18 multiple languages supporting.
*
* @type {boolean}
* @memberOf IServiceConfig
*/
enablei18Support?: boolean;
}
\ No newline at end of file
export * from './translate-init.service';
export * from './i18n-config';
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import { TranslateLoader } from '@ngx-translate/core';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Observable';
import { EN_US_LANG } from './lang/en-us-lang';
import { ES_ES_LANG } from './lang/es-es-lang';
import { ZH_CN_LANG } from './lang/zh-cn-lang';
/**
* Define language mapping
*/
export const langs: { [key: string]: any } = {
"en-us": EN_US_LANG,
"es-es": ES_ES_LANG,
"zh-cn": ZH_CN_LANG
};
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
/**
* Declare a translation loader with local json object
......@@ -24,8 +12,15 @@ export const langs: { [key: string]: any } = {
* @extends {TranslateLoader}
*/
export class TranslatorJsonLoader extends TranslateLoader {
constructor(private config: IServiceConfig) {
super();
}
getTranslation(lang: string): Observable<any> {
let dict: any = langs[lang] ? langs[lang] : {};
let dict: any = this.config &&
this.config.localI18nMessageVariableMap &&
this.config.localI18nMessageVariableMap[lang] ?
this.config.localI18nMessageVariableMap[lang] : {};
return Observable.of(dict);
}
}
\ No newline at end of file
import { Injectable } from "@angular/core";
import { i18nConfig } from "./i18n-config";
import { TranslateService } from '@ngx-translate/core';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from '../utils';
import { CookieService } from 'ngx-cookie';
@Injectable()
export class TranslateServiceInitializer {
constructor(
private translateService: TranslateService,
private cookie: CookieService
) { }
public init(config: i18nConfig = {}): void {
let selectedLang: string = config.defaultLang ? config.defaultLang : DEFAULT_LANG;
this.translateService.addLangs(config.supportedLangs ? config.supportedLangs : DEFAULT_SUPPORTING_LANGS);
this.translateService.setDefaultLang(selectedLang);
if (config.enablei18Support) {
//If user has selected lang, then directly use it
let langSetting: string = this.cookie.get(config.langCookieKey ? config.langCookieKey : DEFAULT_LANG_COOKIE_KEY);
if (!langSetting || langSetting.trim() === "") {
//Use browser lang
langSetting = this.translateService.getBrowserCultureLang().toLowerCase();
}
if (config.supportedLangs && config.supportedLangs.length > 0) {
if (config.supportedLangs.find(lang => lang === langSetting)) {
selectedLang = langSetting;
}
}
}
this.translateService.use(selectedLang);
}
}
\ No newline at end of file
......@@ -3,13 +3,36 @@ import { SharedModule } from '../shared/shared.module';
import { TranslateService } from '@ngx-translate/core';
import { DEFAULT_LANG } from '../utils';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
const EN_US_LANG: any = {
"SIGN_UP": {
"TITLE": "Sign Up"
},
}
const ZH_CN_LANG: any = {
"SIGN_UP": {
"TITLE": "注册"
},
}
describe('TranslateService', () => {
let testConfig: IServiceConfig = {
langMessageLoader: 'local',
localI18nMessageVariableMap: {
"en-us": EN_US_LANG,
"zh-cn": ZH_CN_LANG
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
providers: []
providers: [{
provide: SERVICE_CONFIG, useValue: testConfig
}]
});
});
......@@ -25,8 +48,8 @@ describe('TranslateService', () => {
it('should translate key to text [en-us]', inject([TranslateService], (service: TranslateService) => {
service.use(DEFAULT_LANG);
service.get('APP_TITLE.HARBOR').subscribe(text => {
expect(text).toEqual('Harbor');
service.get('SIGN_UP.TITLE').subscribe(text => {
expect(text).toEqual('Sign Up');
});
}));
......
......@@ -9,4 +9,5 @@ export * from './endpoint/index';
export * from './repository/index';
export * from './tag/index';
export * from './replication/index';
export * from './vulnerability-scanning/index';
\ No newline at end of file
export * from './vulnerability-scanning/index';
export * from './i18n/index';
\ No newline at end of file
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
......@@ -8,33 +8,35 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { ListRepositoryComponent } from './list-repository.component';
import { Repository } from '../service/interface';
describe('ListRepositoryComponent (inline template)', ()=> {
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('ListRepositoryComponent (inline template)', () => {
let comp: ListRepositoryComponent;
let fixture: ComponentFixture<ListRepositoryComponent>;
let mockData: Repository[] = [
{
"id": 11,
"name": "library/busybox",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
"id": 11,
"name": "library/busybox",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
},
{
"id": 12,
"name": "library/nginx",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
"id": 12,
"name": "library/nginx",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
}
];
];
beforeEach(async(()=>{
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
......@@ -43,19 +45,19 @@ describe('ListRepositoryComponent (inline template)', ()=> {
ListRepositoryComponent,
ConfirmationDialogComponent
],
providers: []
providers: [{ provide: SERVICE_CONFIG, useValue: {} }]
});
}));
beforeEach(()=>{
beforeEach(() => {
fixture = TestBed.createComponent(ListRepositoryComponent);
comp = fixture.componentInstance;
});
it('should load and render data', async(()=>{
it('should load and render data', async(() => {
fixture.detectChanges();
comp.repositories = mockData;
fixture.whenStable().then(()=>{
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(comp.repositories).toBeTruthy();
let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell'));
......
......@@ -3,6 +3,16 @@ import { OpaqueToken } from '@angular/core';
export let SERVICE_CONFIG = new OpaqueToken("service.config");
export interface IServiceConfig {
/**
* The base endpoint of service used to retrieve the system configuration information.
* The configurations may include but not limit:
* Notary configurations
* Registry configuration
* Volume information
*
* @type {string}
* @memberOf IServiceConfig
*/
systemInfoEndpoint?: string;
/**
......@@ -62,6 +72,22 @@ export interface IServiceConfig {
*/
replicationJobEndpoint?: string;
/**
* The base endpoint of the service used to handle vulnerability scanning.
*
* @type {string}
* @memberOf IServiceConfig
*/
vulnerabilityScanningBaseEndpoint?: string;
/**
* To determine whether or not to enable the i18 multiple languages supporting.
*
* @type {boolean}
* @memberOf IServiceConfig
*/
enablei18Support?: boolean;
/**
* The cookie key used to store the current used language preference.
*
......@@ -79,18 +105,71 @@ export interface IServiceConfig {
supportedLangs?: string[],
/**
* To determine whether to not enable the i18 multiple languages supporting.
* Define the default language the translate service uses.
*
* @type {boolean}
* @type {string}
* @memberOf i18nConfig
*/
defaultLang?: string;
/**
* To determine which loader will be used to load the required lang messages.
* Support two loaders:
* One is 'http', use async http to load json files with the specified url/path.
* Another is 'local', use local json variable to store the lang message.
*
* @type {string}
* @memberOf IServiceConfig
*/
enablei18Support?: boolean;
langMessageLoader?: string;
/**
* The base endpoint of the service used to handle vulnerability scanning.
* Define the basic url/path prefix for the loader to find the json files if the 'langMessageLoader' is 'http'.
* For example, 'src/i18n/langs'.
*
* @type {string}
* @memberOf IServiceConfig
*/
vulnerabilityScanningBaseEndpoint?: string;
langMessagePathForHttpLoader?: string;
/**
* Define the suffix of the json file names without lang name if 'langMessageLoader' is 'http'.
* For example, '-lang.json' is suffix of message file 'en-us-lang.json'.
*
* @type {string}
* @memberOf IServiceConfig
*/
langMessageFileSuffixForHttpLoader?: string;
/**
* If set 'local' loader in configuration property 'langMessageLoader' to load the i18n messages,
* this property must be defined to tell local JSON loader where to get the related messages.
* E.g:
* If declare the following messages storage variables,
*
* export const EN_US_LANG: any = {
* "APP_TITLE": {
* "VMW_HARBOR": "VMware Harbor",
* "HARBOR": "Harbor"
* }
* }
*
* export const ZH_CN_LANG: any = {
* "APP_TITLE": {
* "VMW_HARBOR": "VMware Harbor中文版",
* "HARBOR": "Harbor"
* }
* }
*
* then this property should be set to: