Skip to content

Commit

Permalink
SHIBUI-814 Fixed tests, implemented pipe for i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
rmathis committed Sep 12, 2018
1 parent a09f5ad commit 19d96f4
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 33 deletions.
2 changes: 2 additions & 0 deletions backend/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ action.logout=Logout
action.add-new=Add New
action.clear=Clear

heading.shibboleth=Shibboleth { foo }

label.metadata-sources=Metadata Sources
label.metadata-source=Metadata Source
label.metadata-providers=Metadata Providers
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<nav class="navbar navbar-expand-md fixed-top">
<a class="navbar-brand" href="https://www.shibboleth.net/" target="_blank">
<img src="/assets/shibboleth_icon_color_130x130.png" width="30" height="30" class="d-inline-block align-top" alt="Shibboleth Logo - Click to be directed to www.shibboleth.net">
<span class="d-lg-inline d-none">Shibboleth</span>
<span class="d-lg-inline d-none">{{ 'heading.shibboleth' | i18n:{foo: 'bar'} }}</span>
</a>
<span class="navbar-text" i18n="@@label--app">Source Management</span>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar"
Expand Down
12 changes: 8 additions & 4 deletions ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule, Store, combineReducers } from '@ngrx/store';
import { AppComponent } from './app.component';

import { User } from './core/model/user';
import * as fromRoot from './core/reducer';
import * as fromVersion from './core/reducer/version.reducer';
import { NotificationModule } from './notification/notification.module';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { MockI18nPipe, MockI18nService } from '../testing/i18n.stub';
import { I18nService } from './i18n/service/i18n.service';

@Component({
template: `
Expand All @@ -29,6 +29,9 @@ describe('AppComponent', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
{provide: I18nService, useClass: MockI18nService }
],
imports: [
NgbDropdownModule.forRoot(),
RouterTestingModule,
Expand All @@ -39,7 +42,8 @@ describe('AppComponent', () => {
],
declarations: [
AppComponent,
TestHostComponent
TestHostComponent,
MockI18nPipe
],
}).compileComponents();

Expand All @@ -54,7 +58,7 @@ describe('AppComponent', () => {

it('should create the app', async(() => {
expect(app).toBeTruthy();
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledTimes(2);
}));

it(`should have as title 'Shib-UI'`, async(() => {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Store } from '@ngrx/store';
import * as fromRoot from './core/reducer';
import { VersionInfo } from './core/model/version';
import { VersionInfoLoadRequestAction } from './core/action/version.action';
import { I18nService } from './core/service/i18n.service';
import { SetLocale } from './core/action/message.action';
import { I18nService } from './i18n/service/i18n.service';
import { SetLocale } from './i18n/action/message.action';

@Component({
selector: 'app-root',
Expand Down
3 changes: 3 additions & 0 deletions ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { WizardModule } from './wizard/wizard.module';
import { FormModule } from './schema-form/schema-form.module';
import { environment } from '../environments/environment.prod';
import { getCurrentLocale } from './shared/util';
import { I18nModule } from './i18n/i18n.module';

@NgModule({
declarations: [
Expand Down Expand Up @@ -52,6 +53,8 @@ import { getCurrentLocale } from './shared/util';
HttpClientModule,
ContentionModule,
SharedModule,
I18nModule.forRoot(),
I18nModule,
AppRoutingModule
],
providers: [
Expand Down
7 changes: 2 additions & 5 deletions ui/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { UserEffects } from './effect/user.effect';
import { HttpClientModule } from '@angular/common/http';
import { ModalService } from './service/modal.service';
import { DifferentialService } from './service/differential.service';
import { I18nService } from './service/i18n.service';
import { MessageEffects } from './effect/message.effect';

export const COMPONENTS = [];

Expand All @@ -38,8 +36,7 @@ export class CoreModule {
FileService,
ModalService,
DifferentialService,
CanDeactivateGuard,
I18nService
CanDeactivateGuard
]
};
}
Expand All @@ -48,7 +45,7 @@ export class CoreModule {
@NgModule({
imports: [
StoreModule.forFeature('core', reducers),
EffectsModule.forFeature([UserEffects, VersionEffects, MessageEffects]),
EffectsModule.forFeature([UserEffects, VersionEffects]),
],
})
export class RootCoreModule { }
22 changes: 2 additions & 20 deletions ui/src/app/core/reducer/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import {
ActionReducerMap,
createSelector,
createFeatureSelector,
createSelectorFactory,
ActionReducer,
MetaReducer,
combineReducers
createFeatureSelector
} from '@ngrx/store';

import * as fromUser from './user.reducer';
import * as fromVersion from './version.reducer';
import * as fromMessages from './message.reducer';
import * as fromRoot from '../../app.reducer';
import { getCurrentLanguage, getCurrentCountry } from '../../shared/util';

export interface CoreState {
user: fromUser.UserState;
version: fromVersion.VersionState;
messages: fromMessages.MessageState;
}

export interface State extends fromRoot.State {
Expand All @@ -26,14 +18,12 @@ export interface State extends fromRoot.State {

export const reducers = {
user: fromUser.reducer,
version: fromVersion.reducer,
messages: fromMessages.reducer
version: fromVersion.reducer
};

export const getCoreFeature = createFeatureSelector<CoreState>('core');
export const getUserStateFn = (state: CoreState) => state.user;
export const getVersionStateFn = (state: CoreState) => state.version;
export const getMessageStateFn = (state: CoreState) => state.messages;

export const getUserState = createSelector(getCoreFeature, getUserStateFn);
export const getUser = createSelector(getUserState, fromUser.getUser);
Expand All @@ -44,11 +34,3 @@ export const getVersionState = createSelector(getCoreFeature, (state: CoreState)
export const getVersionInfo = createSelector(getVersionState, fromVersion.getVersionInfo);
export const getVersionLoading = createSelector(getVersionState, fromVersion.getVersionIsLoading);
export const getVersionError = createSelector(getVersionState, fromVersion.getVersionError);

export const getMessageState = createSelector(getCoreFeature, getMessageStateFn);
export const getLocale = createSelector(getMessageState, fromMessages.getLocale);
export const getLanguage = createSelector(getLocale, locale => getCurrentLanguage(locale));
export const getCountry = createSelector(getLocale, locale => getCurrentCountry(locale));

export const getMessages = createSelector(getMessageState, fromMessages.getMessages);
export const getFetchingMessages = createSelector(getMessageState, fromMessages.isFetching);
File renamed without changes.
File renamed without changes.
53 changes: 53 additions & 0 deletions ui/src/app/i18n/i18n.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

import { reducers } from './reducer';
import { HttpClientModule } from '@angular/common/http';
import { I18nService } from './service/i18n.service';
import { MessageEffects } from './effect/message.effect';
import { I18nPipe } from './pipe/i18n.pipe';
import { CoreModule } from '../core/core.module';

export const COMPONENTS = [];
export const DIRECTIVES = [];
export const PIPES = [
I18nPipe
];

@NgModule({
imports: [
CommonModule,
HttpClientModule,
CoreModule
],
declarations: [
...PIPES,
...COMPONENTS,
...DIRECTIVES
],
exports: [
...PIPES,
...COMPONENTS,
...DIRECTIVES
],
})
export class I18nModule {
static forRoot() {
return {
ngModule: RootI18nModule,
providers: [
I18nService
]
};
}
}

@NgModule({
imports: [
StoreModule.forFeature('i18n', reducers),
EffectsModule.forFeature([MessageEffects]),
],
})
export class RootI18nModule { }
72 changes: 72 additions & 0 deletions ui/src/app/i18n/pipe/i18n.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { I18nPipe } from './i18n.pipe';
import { I18nService } from '../service/i18n.service';
import { CommonModule } from '@angular/common';
import { StoreModule, combineReducers, Store } from '@ngrx/store';

import * as fromI18n from '../reducer';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Component } from '@angular/core';
import { MessagesLoadSuccessAction } from '../action/message.action';

@Component({
template: `
<span>{{ foo | i18n:{ foo: 'bar' } }}</span>
`
})
class TestHostComponent {
private _foo: string;

public get foo(): string {
return this._foo;
}

public set foo(val: string) {
this._foo = val;
}
}

describe('Pipe: I18n translation', () => {
let fixture: ComponentFixture<TestHostComponent>;
let instance: TestHostComponent;
let store: Store<fromI18n.State>;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: I18nService, useValue: {
interpolate: (value: string, interpolated: { [prop: string]: string } = {}): string => {
return Object.entries(interpolated).reduce((current, interpolate) => {
let reg = new RegExp(`{\\s*${interpolate[0]}\\s*}`, 'gm');
return current.replace(reg, interpolate[1]);
}, value);
}
} }
],
imports: [
CommonModule,
StoreModule.forRoot({
'message': combineReducers(fromI18n.reducers),
})
],
declarations: [
I18nPipe,
TestHostComponent
],
});
store = TestBed.get(Store);

spyOn(store, 'dispatch').and.callThrough();

fixture = TestBed.createComponent(TestHostComponent);
instance = fixture.componentInstance;
});

it('should set the correct text', () => {
store.dispatch(new MessagesLoadSuccessAction({ foo: 'hi there' }));

store.select(fromI18n.getMessages).subscribe(() => {
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('hi there');
});
});
});
40 changes: 40 additions & 0 deletions ui/src/app/i18n/pipe/i18n.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { PipeTransform, Pipe, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromI18n from '../reducer';
import { Subscription } from 'rxjs';
import { I18nService } from '../service/i18n.service';

@Pipe({
name: 'i18n',
pure: false
})
export class I18nPipe implements PipeTransform, OnDestroy {

sub: Subscription;

messages: { [propName: string]: string } = {};

value: string;

constructor(
private store: Store<fromI18n.State>,
private i18nService: I18nService,
private _ref: ChangeDetectorRef
) {
this.sub = this.store.select(fromI18n.getMessages).subscribe(messages => {
this.messages = messages ? messages : {};
this._ref.markForCheck();
});
}

transform(value: string, interpolated: { [prop: string]: string } = {}): any {
interpolated = interpolated || {};
let val = this.messages.hasOwnProperty(value) ? this.messages[value] : value;
this.value = this.i18nService.interpolate(val, interpolated);
return this.value;
}

ngOnDestroy(): void {
this.sub.unsubscribe();
}
}
31 changes: 31 additions & 0 deletions ui/src/app/i18n/reducer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
createSelector,
createFeatureSelector
} from '@ngrx/store';

import * as fromMessages from './message.reducer';
import * as fromRoot from '../../app.reducer';
import { getCurrentLanguage, getCurrentCountry } from '../../shared/util';

export interface I18nState {
messages: fromMessages.MessageState;
}

export interface State extends fromRoot.State {
core: I18nState;
}

export const reducers = {
messages: fromMessages.reducer
};

export const getCoreFeature = createFeatureSelector<I18nState>('i18n');
export const getMessageStateFn = (state: I18nState) => state.messages;

export const getMessageState = createSelector(getCoreFeature, getMessageStateFn);
export const getLocale = createSelector(getMessageState, fromMessages.getLocale);
export const getLanguage = createSelector(getLocale, locale => getCurrentLanguage(locale));
export const getCountry = createSelector(getLocale, locale => getCurrentCountry(locale));

export const getMessages = createSelector(getMessageState, fromMessages.getMessages);
export const getFetchingMessages = createSelector(getMessageState, fromMessages.isFetching);
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
import { NavigatorService } from './navigator.service';
import { NavigatorService } from '../../core/service/navigator.service';
import { getCurrentLanguage, getCurrentCountry, getCurrentLocale } from '../../shared/util';

@Injectable()
Expand Down Expand Up @@ -34,4 +34,11 @@ export class I18nService {
getCurrentLocale(): string {
return getCurrentLocale(this.navigator.native);
}

interpolate(value: string, interpolated: { [prop: string]: string } = {}): string {
return Object.entries(interpolated).reduce((current, interpolate) => {
let reg = new RegExp(`{\\s*${interpolate[0]}\\s*}`, 'gm');
return current.replace(reg, interpolate[1]);
}, value);
}
}
Loading

0 comments on commit 19d96f4

Please sign in to comment.