mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-12 17:04:57 +00:00
[ADF-3784] support for browser cultures (i18n) (#4066)
* support language cultures * update docs * fix typo * fix tests * correctly replace fallback translations * login dialog fixes * fix error component * [denys-i18n-cultures] Fix error content unit tests
This commit is contained in:
parent
d6df5bc862
commit
b5f9036545
@ -249,6 +249,9 @@ The table below illustrates how the selection is made:
|
||||
| X | fr | jp | en | fr |
|
||||
| it | fr | jp | en | it |
|
||||
|
||||
The translation service probes the browser culture first, for example `en-GB`.
|
||||
If the `en-GB.json` file does not exist, the service falls back to the language id: `en`.
|
||||
|
||||
Once the locale language is determined, it is saved to the user preferences and this saved value
|
||||
will be used from that point on, regardless of the `app.config.json` and browser settings.
|
||||
|
||||
|
@ -46,8 +46,8 @@
|
||||
tabindex="1">
|
||||
</mat-form-field>
|
||||
|
||||
<span class="adf-login-validation" for="username" *ngIf="formError.username">
|
||||
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError.username | translate }}</span>
|
||||
<span class="adf-login-validation" for="username" *ngIf="formError['username']">
|
||||
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError['username'] | translate }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<div class="adf-login__field">
|
||||
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
|
||||
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
|
||||
type="password"
|
||||
[type]="isPasswordShow ? 'text' : 'password'"
|
||||
[formControl]="form.controls['password']"
|
||||
id="password"
|
||||
data-automation-id="password"
|
||||
@ -69,9 +69,9 @@
|
||||
visibility_off
|
||||
</mat-icon>
|
||||
</mat-form-field>
|
||||
<span class="adf-login-validation" for="password" *ngIf="formError.password">
|
||||
<span class="adf-login-validation" for="password" *ngIf="formError['password']">
|
||||
<span id="password-required" class="adf-login-error"
|
||||
data-automation-id="password-required">{{formError.password | translate }}</span>
|
||||
data-automation-id="password-required">{{formError['password'] | translate }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, EventEmitter,
|
||||
import { Component, EventEmitter,
|
||||
Input, OnInit, Output, TemplateRef, ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@ -42,6 +42,11 @@ enum LoginSteps {
|
||||
Welcome = 2
|
||||
}
|
||||
|
||||
interface ValidationMessage {
|
||||
value: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-login',
|
||||
templateUrl: './login.component.html',
|
||||
@ -134,7 +139,7 @@ export class LoginComponent implements OnInit {
|
||||
headerTemplate: TemplateRef<any>;
|
||||
data: any;
|
||||
|
||||
private _message: { [id: string]: { [id: string]: string } };
|
||||
private _message: { [id: string]: { [id: string]: ValidationMessage } };
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@ -147,7 +152,6 @@ export class LoginComponent implements OnInit {
|
||||
private authService: AuthenticationService,
|
||||
private translateService: TranslationService,
|
||||
private logService: LogService,
|
||||
private elementRef: ElementRef,
|
||||
private router: Router,
|
||||
private appConfig: AppConfigService,
|
||||
private userPreferences: UserPreferencesService,
|
||||
@ -220,8 +224,11 @@ export class LoginComponent implements OnInit {
|
||||
if (hasError) {
|
||||
for (let key in this.form.controls[field].errors) {
|
||||
if (key) {
|
||||
this.formError[field] +=
|
||||
this._message[field][key] + '';
|
||||
const message = this._message[field][key];
|
||||
if (message && message.value) {
|
||||
const translated = this.translateService.instant(message.value, message.params);
|
||||
this.formError[field] += translated;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +263,7 @@ export class LoginComponent implements OnInit {
|
||||
(err: any) => {
|
||||
this.actualLoginStep = LoginSteps.Landing;
|
||||
this.displayErrorMessage(err);
|
||||
this.enableError();
|
||||
this.isError = true;
|
||||
this.error.emit(new LoginErrorEvent(err));
|
||||
},
|
||||
() => this.logService.info('Login done')
|
||||
@ -310,13 +317,10 @@ export class LoginComponent implements OnInit {
|
||||
msg: string,
|
||||
params?: any
|
||||
) {
|
||||
if (params) {
|
||||
this.translateService.get(msg, params).subscribe((res: string) => {
|
||||
this._message[field][ruleId] = res;
|
||||
});
|
||||
} else {
|
||||
this._message[field][ruleId] = msg;
|
||||
}
|
||||
this._message[field][ruleId] = {
|
||||
value: msg,
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -324,10 +328,6 @@ export class LoginComponent implements OnInit {
|
||||
*/
|
||||
toggleShowPassword() {
|
||||
this.isPasswordShow = !this.isPasswordShow;
|
||||
this.elementRef.nativeElement.querySelector('#password').type = this
|
||||
.isPasswordShow
|
||||
? 'text'
|
||||
: 'password';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -371,18 +371,25 @@ export class LoginComponent implements OnInit {
|
||||
private initFormFieldsMessagesDefault() {
|
||||
this._message = {
|
||||
username: {
|
||||
required: 'LOGIN.MESSAGES.USERNAME-REQUIRED'
|
||||
required: {
|
||||
value: 'LOGIN.MESSAGES.USERNAME-REQUIRED'
|
||||
},
|
||||
minLength: {
|
||||
value: 'LOGIN.MESSAGES.USERNAME-MIN',
|
||||
params: {
|
||||
get minLength() {
|
||||
return this.minLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
password: {
|
||||
required: 'LOGIN.MESSAGES.PASSWORD-REQUIRED'
|
||||
required: {
|
||||
value: 'LOGIN.MESSAGES.PASSWORD-REQUIRED'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.translateService
|
||||
.get('LOGIN.MESSAGES.USERNAME-MIN', { minLength: this.minLength })
|
||||
.subscribe((res: string) => {
|
||||
this._message['username']['minlength'] = res;
|
||||
});
|
||||
}
|
||||
|
||||
private initFormFieldsDefault() {
|
||||
@ -400,13 +407,6 @@ export class LoginComponent implements OnInit {
|
||||
this.initFormError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the error flag
|
||||
*/
|
||||
private enableError() {
|
||||
this.isError = true;
|
||||
}
|
||||
|
||||
private hasCustomFieldsValidation(): boolean {
|
||||
return this.fieldsValidation !== undefined;
|
||||
}
|
||||
|
@ -33,10 +33,15 @@ export class TranslateLoaderService implements TranslateLoader {
|
||||
private suffix: string = '.json';
|
||||
private providers: ComponentTranslationModel[] = [];
|
||||
private queue: string [][] = [];
|
||||
private defaultLang: string = 'en';
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
setDefaultLang(value: string) {
|
||||
this.defaultLang = value || 'en';
|
||||
}
|
||||
|
||||
registerProvider(name: string, path: string) {
|
||||
let registered = this.providers.find((provider) => provider.name === name);
|
||||
if (registered) {
|
||||
@ -50,6 +55,29 @@ export class TranslateLoaderService implements TranslateLoader {
|
||||
return this.providers.find((x) => x.name === name) ? true : false;
|
||||
}
|
||||
|
||||
fetchLanguageFile(lang: string, component: ComponentTranslationModel, fallbackUrl?: string): Observable<void> {
|
||||
const translationUrl = fallbackUrl || `${component.path}/${this.prefix}/${lang}${this.suffix}?v=${Date.now()}`;
|
||||
|
||||
return this.http.get(translationUrl).pipe(
|
||||
map((res: Response) => {
|
||||
component.json[lang] = res;
|
||||
}),
|
||||
retry(3),
|
||||
catchError(() => {
|
||||
if (!fallbackUrl && lang.includes('-')) {
|
||||
const [langId] = lang.split('-');
|
||||
|
||||
if (langId && langId !== this.defaultLang) {
|
||||
const url = `${component.path}/${this.prefix}/${langId}${this.suffix}?v=${Date.now()}`;
|
||||
|
||||
return this.fetchLanguageFile(lang, component, url);
|
||||
}
|
||||
}
|
||||
return throwError(`Failed to load ${translationUrl}`);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getComponentToFetch(lang: string): Array<Observable<any>> {
|
||||
const observableBatch = [];
|
||||
if (!this.queue[lang]) {
|
||||
@ -59,16 +87,8 @@ export class TranslateLoaderService implements TranslateLoader {
|
||||
if (!this.isComponentInQueue(lang, component.name)) {
|
||||
this.queue[lang].push(component.name);
|
||||
|
||||
const translationUrl = `${component.path}/${this.prefix}/${lang}${this.suffix}?v=${Date.now()}`;
|
||||
|
||||
observableBatch.push(
|
||||
this.http.get(translationUrl).pipe(
|
||||
map((res: Response) => {
|
||||
component.json[lang] = res;
|
||||
}),
|
||||
retry(3),
|
||||
catchError(() => throwError(`Failed to load ${translationUrl}`))
|
||||
)
|
||||
this.fetchLanguageFile(lang, component)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -43,6 +43,7 @@ export class TranslationService {
|
||||
|
||||
this.defaultLang = 'en';
|
||||
translate.setDefaultLang(this.defaultLang);
|
||||
this.customLoader.setDefaultLang(this.defaultLang);
|
||||
|
||||
if (providers && providers.length > 0) {
|
||||
for (let provider of providers) {
|
||||
|
@ -117,24 +117,24 @@ describe('UserPreferencesService', () => {
|
||||
|
||||
it('should return as default locale the app.config locate as first', () => {
|
||||
appConfig.config.locale = 'fake-locate-config';
|
||||
spyOn(translate, 'getBrowserLang').and.returnValue('fake-locate-browser');
|
||||
spyOn(translate, 'getBrowserCultureLang').and.returnValue('fake-locate-browser');
|
||||
expect(preferences.getDefaultLocale()).toBe('fake-locate-config');
|
||||
});
|
||||
|
||||
it('should return as default locale the browser locale as second', () => {
|
||||
spyOn(translate, 'getBrowserLang').and.returnValue('fake-locate-browser');
|
||||
spyOn(translate, 'getBrowserCultureLang').and.returnValue('fake-locate-browser');
|
||||
expect(preferences.getDefaultLocale()).toBe('fake-locate-browser');
|
||||
});
|
||||
|
||||
it('should return as default locale the component property as third ', () => {
|
||||
spyOn(translate, 'getBrowserLang').and.stub();
|
||||
spyOn(translate, 'getBrowserCultureLang').and.stub();
|
||||
expect(preferences.getDefaultLocale()).toBe('en');
|
||||
});
|
||||
|
||||
it('should return as locale the store locate', () => {
|
||||
preferences.locale = 'fake-store-locate';
|
||||
appConfig.config.locale = 'fake-locate-config';
|
||||
spyOn(translate, 'getBrowserLang').and.returnValue('fake-locate-browser');
|
||||
spyOn(translate, 'getBrowserCultureLang').and.returnValue('fake-locate-browser');
|
||||
expect(preferences.locale).toBe('fake-store-locate');
|
||||
});
|
||||
|
||||
|
@ -183,7 +183,7 @@ export class UserPreferencesService {
|
||||
* @returns Default locale language code
|
||||
*/
|
||||
public getDefaultLocale(): string {
|
||||
return this.appConfig.get<string>('locale') || this.translate.getBrowserLang() || 'en';
|
||||
return this.appConfig.get<string>('locale') || this.translate.getBrowserCultureLang() || 'en';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,16 +31,6 @@ describe('ErrorContentComponent', () => {
|
||||
let element: HTMLElement;
|
||||
let translateService: TranslationService;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
CoreTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: TranslationService, useClass: TranslationMock },
|
||||
{ provide: ActivatedRoute, useValue: { params: of({id: '404'})}}
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ErrorContentComponent);
|
||||
element = fixture.nativeElement;
|
||||
@ -53,84 +43,109 @@ describe('ErrorContentComponent', () => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('should create error component', async(() => {
|
||||
fixture.detectChanges();
|
||||
expect(errorContentComponent).toBeTruthy();
|
||||
}));
|
||||
describe(' with an undefined error', () => {
|
||||
|
||||
it('should render error code', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-code');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should render error title', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-title');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should render error description', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should render error description', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should hide secondary button if this one has no value', async(() => {
|
||||
spyOn(translateService, 'instant').and.callFake((inputString) => {
|
||||
return '';
|
||||
setupTestBed({
|
||||
imports: [
|
||||
CoreTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: TranslationService, useClass: TranslationMock },
|
||||
{ provide: ActivatedRoute, useValue: { params: of() } }
|
||||
]
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description-link');
|
||||
expect(errorContentElement).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should render secondary button with its value from the translate file', async(() => {
|
||||
spyOn(translateService, 'instant').and.callFake((inputString) => {
|
||||
return 'Secondary Button';
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const errorContentElement = element.querySelector('#adf-secondary-button');
|
||||
it('should create error component', async(() => {
|
||||
fixture.detectChanges();
|
||||
expect(errorContentComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should render error code', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-code');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
expect(errorContentElement.textContent).toContain('ERROR_CONTENT.UNKNOWN.SECONDARY_BUTTON.TEXT');
|
||||
}));
|
||||
|
||||
it('should render error title', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-title');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should render error description', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should render error description', async(() => {
|
||||
fixture.detectChanges();
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should hide secondary button if this one has no value', async(() => {
|
||||
spyOn(translateService, 'instant').and.callFake((inputString) => {
|
||||
return '';
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const errorContentElement = element.querySelector('.adf-error-content-description-link');
|
||||
expect(errorContentElement).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should render secondary button with its value from the translate file', async(() => {
|
||||
spyOn(translateService, 'instant').and.callFake((inputString) => {
|
||||
return 'Secondary Button';
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const errorContentElement = element.querySelector('#adf-secondary-button');
|
||||
expect(errorContentElement).not.toBeNull();
|
||||
expect(errorContentElement).toBeDefined();
|
||||
expect(errorContentElement.textContent).toContain('ERROR_CONTENT.UNKNOWN.SECONDARY_BUTTON.TEXT');
|
||||
|
||||
});
|
||||
}));
|
||||
|
||||
it('should the default value of return button be /', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.returnButtonUrl).toBe('/');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate to the default error UNKNOWN if it does not find the error', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.errorCode).toBe('UNKNOWN');
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe(' with a specific error', () => {
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
CoreTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: TranslationService, useClass: TranslationMock },
|
||||
{ provide: ActivatedRoute, useValue: { params: of({ id: '404' }) } }
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
it('should the default value of return button be /', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.returnButtonUrl).toBe('/');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate to an error given by the route params', async(() => {
|
||||
spyOn(translateService, 'get').and.returnValue(of('404'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.errorCode).toBe('404');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate to the default error UNKNOWN if it does not find the error', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.errorCode).toBe('UNKNOWN');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate to an error given by the route params', async(() => {
|
||||
spyOn(translateService, 'get').and.returnValue(of('404'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorContentComponent.errorCode).toBe('404');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ export class ErrorContentComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
/** Error code associated with this error. */
|
||||
@Input()
|
||||
errorCode: string;
|
||||
errorCode: string = 'UNKNOWN';
|
||||
|
||||
hasSecondButton: boolean;
|
||||
|
||||
@ -58,15 +58,8 @@ export class ErrorContentComponent implements OnInit, AfterContentChecked {
|
||||
ngOnInit() {
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
if (params['id'] && !this.errorCode) {
|
||||
if (params['id']) {
|
||||
this.errorCode = params['id'];
|
||||
let unknown = '';
|
||||
this.translateService.get('ERROR_CONTENT.' + this.errorCode + '.TITLE').subscribe((errorTranslation: string) => {
|
||||
unknown = errorTranslation;
|
||||
});
|
||||
if (unknown === 'ERROR_CONTENT.' + this.errorCode + '.TITLE') {
|
||||
this.errorCode = 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user