Skip to content

Commit

Permalink
feat(Edit Project Info): Multiple languages setting (#1546)
Browse files Browse the repository at this point in the history
Allow authors to select default language and supported languages
  • Loading branch information
hirokiterashima authored Dec 5, 2023
1 parent 63f86ee commit 6eaebf0
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 47 deletions.
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@angular/platform-browser": "^16.2.9",
"@angular/platform-browser-dynamic": "^16.2.9",
"@angular/router": "^16.2.9",
"@ng-select/ng-select": "^11.2.0",
"@ng-web-apis/common": "^2.0.1",
"@ng-web-apis/intersection-observer": "^3.0.0",
"@stomp/rx-stomp": "^1.1.4",
Expand Down
12 changes: 9 additions & 3 deletions src/app/domain/localeToLanguage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export const localeToLanguage = {
export const localeToLanguage: { [locale: string]: string } = {
zh_CN: $localize`Chinese (Simplified)`,
zh_TW: $localize`Chinese (Traditional)`,
nl: $localize`Dutch`,
en_US: $localize`English`,
es: $localize`Spanish`,
de: $localize`German`,
it: $localize`Italian`,
ja: $localize`Japanese`,
it: $localize`Italian`
ko: $localize`Korean`,
es: $localize`Spanish`,
vi: $localize`Vietnamese`
};
39 changes: 26 additions & 13 deletions src/app/domain/projectLocale.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
import { Language } from './language';
import { localeToLanguage } from './localeToLanguage';

const defaultLocales = {
default: 'en_US',
supported: []
};
export class ProjectLocale {
private default: string;
private supported: string[];
private locale: { default: string; supported: string[] };

constructor(locales: any = defaultLocales) {
this.default = locales.default;
this.supported = locales.supported;
constructor(locale: any) {
this.locale = locale;
}

getAvailableLanguages(): Language[] {
return [this.getDefaultLanguage()].concat(this.getSupportedLanguages());
}

getDefaultLanguage(): Language {
return { language: localeToLanguage[this.locale.default], locale: this.locale.default };
}

setDefaultLocale(locale: string): void {
this.locale.default = locale;
this.locale.supported = this.locale.supported.filter(
(supportedLocale) => supportedLocale != locale
);
}

getSupportedLanguages(): Language[] {
return this.supported.map((locale) => ({
return this.locale.supported.map((locale) => ({
language: localeToLanguage[locale],
locale: locale
}));
}

setSupportedLanguages(languages: Language[]): void {
this.locale.supported = languages.map((language) => language.locale);
}

private hasLocale(locale: string): boolean {
return this.supported.includes(locale);
return this.locale.supported.includes(locale);
}

hasTranslations(): boolean {
return this.supported.length > 1;
return this.locale.supported.length > 0;
}

hasTranslationsToApply(locale: string): boolean {
return !this.isDefaultLocale(locale) && this.hasLocale(locale);
}

isDefaultLocale(locale: string): boolean {
return this.default === locale;
return this.locale.default === locale;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"startGroupId": "group0",
"startNodeId": "node1",
"metadata": {},
"nodes": [
{
"id": "group0",
Expand Down
8 changes: 5 additions & 3 deletions src/app/services/translateProjectService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ describe('TranslateProjectService', () => {
describe('translate()', () => {
describe('has no translations to apply', () => {
beforeEach(() => {
spyOn(projectService, 'getLocale').and.returnValue(new ProjectLocale());
spyOn(projectService, 'getLocale').and.returnValue(
new ProjectLocale({ default: 'en_US', supported: [] })
);
});
it('should keep original project in tact', () => {
service.translate().subscribe(() => {
service.translate().then(() => {
expect(projectService.getProjectTitle()).toEqual('Demo Project');
});
});
Expand All @@ -44,7 +46,7 @@ describe('TranslateProjectService', () => {
spyOn(configService, 'getConfigParam').and.returnValue('/123/project.json');
});
it('should retrieve translation mapping file and translate project', () => {
service.translate('es').subscribe(() => {
service.translate('es').then(() => {
expect(projectService.getProjectTitle()).toEqual('Proyecto de demostración');
});
http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { StudentTeacherCommonServicesModule } from '../../student-teacher-common
import { ProjectService } from '../../../assets/wise5/services/projectService';
import { ProjectLocale } from '../../domain/projectLocale';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';

let projectService: ProjectService;
describe('ProjectLanguageChooserComponent', () => {
let component: ProjectLanguageChooserComponent;
let fixture: ComponentFixture<ProjectLanguageChooserComponent>;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
Expand All @@ -24,13 +28,23 @@ describe('ProjectLanguageChooserComponent', () => {
});

beforeEach(() => {
spyOn(projectService, 'getLocale').and.returnValue(new ProjectLocale());
spyOn(projectService, 'getLocale').and.returnValue(
new ProjectLocale({ default: 'en_US', supported: ['ja', 'es'] })
);
fixture = TestBed.createComponent(ProjectLanguageChooserComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
it('shows available languages', async () => {
const selects = await loader.getAllHarnesses(MatSelectHarness);
const selectComponent = selects[0];
await selectComponent.open();
const options = await selectComponent.getOptions();
expect(options.length).toEqual(3);
expect(await options[0].getText()).toMatch('English');
expect(await options[1].getText()).toMatch('Japanese');
expect(await options[2].getText()).toMatch('Spanish');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ProjectLanguageChooserComponent implements OnInit {

ngOnInit(): void {
const unitLocale = this.projectService.getLocale();
this.availableLanguages = unitLocale.getSupportedLanguages();
this.availableLanguages = unitLocale.getAvailableLanguages();
this.selectedLanguage = this.availableLanguages.find((language) =>
unitLocale.isDefaultLocale(language.locale)
);
Expand Down
4 changes: 4 additions & 0 deletions src/app/teacher/authoring-tool.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select';
import { AddYourOwnNode } from '../../assets/wise5/authoringTool/addNode/add-your-own-node/add-your-own-node.component';
import { ChooseNewNodeLocation } from '../../assets/wise5/authoringTool/addNode/choose-new-node-location/choose-new-node-location.component';
import { ChooseNewNodeTemplate } from '../../assets/wise5/authoringTool/addNode/choose-new-node-template/choose-new-node-template.component';
Expand Down Expand Up @@ -56,6 +57,7 @@ import { ComponentTypeButtonComponent } from '../../assets/wise5/authoringTool/c
import { ComponentInfoDialogComponent } from '../../assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component';
import { ComponentTypeSelectorComponent } from '../../assets/wise5/authoringTool/components/component-type-selector/component-type-selector.component';
import { EditNodeTitleComponent } from '../../assets/wise5/authoringTool/node/edit-node-title/edit-node-title.component';
import { EditProjectLanguageSettingComponent } from '../../assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component';
import { AddComponentButtonComponent } from '../../assets/wise5/authoringTool/node/add-component-button/add-component-button.component';

@NgModule({
Expand Down Expand Up @@ -87,6 +89,7 @@ import { AddComponentButtonComponent } from '../../assets/wise5/authoringTool/no
ConcurrentAuthorsMessageComponent,
ConfigureAutomatedAssessmentComponent,
EditNodeTitleComponent,
EditProjectLanguageSettingComponent,
InsertNodeAfterButtonComponent,
InsertNodeInsideButtonComponent,
MilestonesAuthoringComponent,
Expand All @@ -113,6 +116,7 @@ import { AddComponentButtonComponent } from '../../assets/wise5/authoringTool/no
MatBadgeModule,
MatChipsModule,
ImportComponentModule,
NgSelectModule,
NodeAdvancedAuthoringModule,
PreviewComponentModule,
ProjectAssetAuthoringModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
(ngModelChange)="metadataChanged.next()"
/>
</mat-form-field>
<ng-container *ngIf="metadataField.type === 'radio'">
<edit-project-language-setting
*ngIf="metadataField.name === 'Language'"
></edit-project-language-setting>
<ng-container *ngIf="metadataField.type === 'radio' && metadataField.name !== 'Language'">
<label class="bold">{{ metadataField.name }}:</label>
<mat-radio-group class="radio-group margin-bottom-20" fxLayout="column">
<ng-container *ngFor="let choice of metadataField.choices">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<h3 i18n>Language</h3>
<span i18n>Default language:</span>
<ng-select
[items]="availableLanguages"
bindLabel="language"
[(ngModel)]="defaultLanguage"
[clearable]="false"
(change)="updateDefaultLanguage()"
>
</ng-select>
<span i18n>Additional languages:</span>
<ng-select
[items]="availableLanguages"
bindLabel="language"
[(ngModel)]="supportedLanguages"
[closeOnSelect]="false"
[multiple]="true"
(change)="updateSupportedLanguages()"
>
</ng-select>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgSelectModule } from '@ng-select/ng-select';
import { EditProjectLanguageSettingComponent } from './edit-project-language-setting.component';
import { TeacherProjectService } from '../../../services/teacherProjectService';
import { ProjectLocale } from '../../../../../app/domain/projectLocale';
import { FormsModule } from '@angular/forms';

class MockTeacherProjectService {
getLocale() {}
saveProject() {}
}
describe('EditProjectLanguageSettingComponent', () => {
let component: EditProjectLanguageSettingComponent;
let fixture: ComponentFixture<EditProjectLanguageSettingComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [EditProjectLanguageSettingComponent],
imports: [FormsModule, NgSelectModule],
providers: [{ provide: TeacherProjectService, useClass: MockTeacherProjectService }]
});
fixture = TestBed.createComponent(EditProjectLanguageSettingComponent);
component = fixture.componentInstance;
spyOn(TestBed.inject(TeacherProjectService), 'getLocale').and.returnValue(
new ProjectLocale({ default: 'en_US', supported: [] })
);
fixture.detectChanges();
});

it('creates', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component } from '@angular/core';
import { Language } from '../../../../../app/domain/language';
import { TeacherProjectService } from '../../../services/teacherProjectService';
import { ProjectLocale } from '../../../../../app/domain/projectLocale';
import { localeToLanguage } from '../../../../../app/domain/localeToLanguage';

@Component({
selector: 'edit-project-language-setting',
templateUrl: './edit-project-language-setting.component.html'
})
export class EditProjectLanguageSettingComponent {
protected availableLanguages: Language[];
protected defaultLanguage: Language;
private projectLocale: ProjectLocale;
protected supportedLanguages: Language[];

constructor(private projectService: TeacherProjectService) {}

ngOnInit(): void {
this.updateModel();
}

private updateModel(): void {
this.projectLocale = this.projectService.getLocale();
this.defaultLanguage = this.projectLocale.getDefaultLanguage();
this.supportedLanguages = this.projectLocale.getSupportedLanguages();
this.availableLanguages = Object.entries(localeToLanguage)
.map(([locale, language]) => ({
locale: locale,
language: language
}))
.filter((language) => language.locale != this.defaultLanguage.locale);
}

protected updateDefaultLanguage(): void {
this.projectLocale.setDefaultLocale(this.defaultLanguage.locale);
this.projectService.saveProject();
this.updateModel();
}

protected updateSupportedLanguages(): void {
this.projectLocale.setSupportedLanguages(this.supportedLanguages);
this.projectService.saveProject();
this.updateModel();
}
}
4 changes: 4 additions & 0 deletions src/assets/wise5/services/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ export class ProjectService {
instantiateDefaults(): void {
this.project.nodes = this.project.nodes ? this.project.nodes : [];
this.project.inactiveNodes = this.project.inactiveNodes ? this.project.inactiveNodes : [];
this.project.metadata.locale = this.project.metadata.locale ?? {
default: 'en_US',
supported: []
};
}

private calculateNodeOrderOfProject(): void {
Expand Down
Loading

0 comments on commit 6eaebf0

Please sign in to comment.