diff --git a/package-lock.json b/package-lock.json index af51c2deeb7..8241af685ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,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", @@ -5062,6 +5063,23 @@ "tslib": "^2.1.0" } }, + "node_modules/@ng-select/ng-select": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-11.2.0.tgz", + "integrity": "sha512-lTyw93kFdKGecp9eKmOP0PQSCaAJS8DCt4D60ns055+ixvRSp2fuXAuJUvn1e3gAsvpZor37osmYlOJ4LYwYIA==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 16", + "npm": ">= 8" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0" + } + }, "node_modules/@ng-web-apis/common": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-2.2.0.tgz", diff --git a/package.json b/package.json index 1a6965ab2f4..010f7767356 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/domain/localeToLanguage.ts b/src/app/domain/localeToLanguage.ts index c66d368e73d..c18f05d2bbc 100644 --- a/src/app/domain/localeToLanguage.ts +++ b/src/app/domain/localeToLanguage.ts @@ -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` }; diff --git a/src/app/domain/projectLocale.ts b/src/app/domain/projectLocale.ts index 862302fd771..a6e3846cded 100644 --- a/src/app/domain/projectLocale.ts +++ b/src/app/domain/projectLocale.ts @@ -1,32 +1,45 @@ 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 { @@ -34,6 +47,6 @@ export class ProjectLocale { } isDefaultLocale(locale: string): boolean { - return this.default === locale; + return this.locale.default === locale; } } diff --git a/src/app/services/sampleData/curriculum/TeacherProjectServiceSpec.project.json b/src/app/services/sampleData/curriculum/TeacherProjectServiceSpec.project.json index bd80120f958..89021c0d7b9 100644 --- a/src/app/services/sampleData/curriculum/TeacherProjectServiceSpec.project.json +++ b/src/app/services/sampleData/curriculum/TeacherProjectServiceSpec.project.json @@ -1,6 +1,7 @@ { "startGroupId": "group0", "startNodeId": "node1", + "metadata": {}, "nodes": [ { "id": "group0", diff --git a/src/app/services/translateProjectService.spec.ts b/src/app/services/translateProjectService.spec.ts index 80ecff07edd..2476f22d560 100644 --- a/src/app/services/translateProjectService.spec.ts +++ b/src/app/services/translateProjectService.spec.ts @@ -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'); }); }); @@ -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 diff --git a/src/app/student/project-language-chooser/project-language-chooser.component.spec.ts b/src/app/student/project-language-chooser/project-language-chooser.component.spec.ts index 11d6582b8d2..f69b2ad36cd 100644 --- a/src/app/student/project-language-chooser/project-language-chooser.component.spec.ts +++ b/src/app/student/project-language-chooser/project-language-chooser.component.spec.ts @@ -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; + let loader: HarnessLoader; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -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'); }); }); diff --git a/src/app/student/project-language-chooser/project-language-chooser.component.ts b/src/app/student/project-language-chooser/project-language-chooser.component.ts index 451d25a2943..617f03e3433 100644 --- a/src/app/student/project-language-chooser/project-language-chooser.component.ts +++ b/src/app/student/project-language-chooser/project-language-chooser.component.ts @@ -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) ); diff --git a/src/app/teacher/authoring-tool.module.ts b/src/app/teacher/authoring-tool.module.ts index 5cd193731a8..a1912f53f9b 100644 --- a/src/app/teacher/authoring-tool.module.ts +++ b/src/app/teacher/authoring-tool.module.ts @@ -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'; @@ -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({ @@ -87,6 +89,7 @@ import { AddComponentButtonComponent } from '../../assets/wise5/authoringTool/no ConcurrentAuthorsMessageComponent, ConfigureAutomatedAssessmentComponent, EditNodeTitleComponent, + EditProjectLanguageSettingComponent, InsertNodeAfterButtonComponent, InsertNodeInsideButtonComponent, MilestonesAuthoringComponent, @@ -113,6 +116,7 @@ import { AddComponentButtonComponent } from '../../assets/wise5/authoringTool/no MatBadgeModule, MatChipsModule, ImportComponentModule, + NgSelectModule, NodeAdvancedAuthoringModule, PreviewComponentModule, ProjectAssetAuthoringModule, diff --git a/src/assets/wise5/authoringTool/project-info-authoring/project-info-authoring.component.html b/src/assets/wise5/authoringTool/project-info-authoring/project-info-authoring.component.html index 104e922a9d7..204472394ae 100644 --- a/src/assets/wise5/authoringTool/project-info-authoring/project-info-authoring.component.html +++ b/src/assets/wise5/authoringTool/project-info-authoring/project-info-authoring.component.html @@ -52,7 +52,10 @@ (ngModelChange)="metadataChanged.next()" /> - + + diff --git a/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html new file mode 100644 index 00000000000..6fe1e62558c --- /dev/null +++ b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html @@ -0,0 +1,20 @@ +

Language

+Default language: + + +Additional languages: + + diff --git a/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.scss b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.spec.ts b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.spec.ts new file mode 100644 index 00000000000..ecd4a4f58a1 --- /dev/null +++ b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.ts b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.ts new file mode 100644 index 00000000000..4bd26f05117 --- /dev/null +++ b/src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.ts @@ -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(); + } +} diff --git a/src/assets/wise5/services/projectService.ts b/src/assets/wise5/services/projectService.ts index b9b436412d3..354013f666d 100644 --- a/src/assets/wise5/services/projectService.ts +++ b/src/assets/wise5/services/projectService.ts @@ -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 { diff --git a/src/messages.xlf b/src/messages.xlf index 81d3df56e35..7a2fb192dfd 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -2265,32 +2265,74 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.138 - - English + + Chinese (Simplified) src/app/domain/localeToLanguage.ts 2 - - Spanish + + Chinese (Traditional) src/app/domain/localeToLanguage.ts 3 - - Japanese + + Dutch src/app/domain/localeToLanguage.ts 4 + + English + + src/app/domain/localeToLanguage.ts + 5 + + + + German + + src/app/domain/localeToLanguage.ts + 6 + + Italian src/app/domain/localeToLanguage.ts - 5 + 7 + + + + Japanese + + src/app/domain/localeToLanguage.ts + 8 + + + + Korean + + src/app/domain/localeToLanguage.ts + 9 + + + + Spanish + + src/app/domain/localeToLanguage.ts + 10 + + + + Vietnamese + + src/app/domain/localeToLanguage.ts + 11 @@ -7392,6 +7434,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/teacher/account/edit-profile/edit-profile.component.html 109 + + src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html + 1 + Language required @@ -12268,6 +12314,20 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.22,24 + + Default language: + + src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html + 2 + + + + Additional languages: + + src/assets/wise5/authoringTool/project-info/edit-project-language-setting/edit-project-language-setting.component.html + 11 + + Back to Teacher Home @@ -21168,112 +21228,112 @@ If this problem continues, let your teacher know and move on to the next activit Complete <b></b> src/assets/wise5/services/projectService.ts - 1192 + 1196 Visit <b></b> src/assets/wise5/services/projectService.ts - 1198 + 1202 Correctly answer <b></b> src/assets/wise5/services/projectService.ts - 1204 + 1208 Obtain a score of <b></b> on <b></b> src/assets/wise5/services/projectService.ts - 1219 + 1223 You must choose "" on "" src/assets/wise5/services/projectService.ts - 1227 + 1231 Submit <b></b> time on <b></b> src/assets/wise5/services/projectService.ts - 1239 + 1243 Submit <b></b> times on <b></b> src/assets/wise5/services/projectService.ts - 1241 + 1245 Take the branch path from <b></b> to <b></b> src/assets/wise5/services/projectService.ts - 1248 + 1252 Write <b></b> words on <b></b> src/assets/wise5/services/projectService.ts - 1254 + 1258 "" is visible src/assets/wise5/services/projectService.ts - 1260 + 1264 "" is visitable src/assets/wise5/services/projectService.ts - 1266 + 1270 Add <b></b> note on <b></b> src/assets/wise5/services/projectService.ts - 1273 + 1277 Add <b></b> notes on <b></b> src/assets/wise5/services/projectService.ts - 1275 + 1279 You must fill in <b></b> row in the <b>Table</b> on <b></b> src/assets/wise5/services/projectService.ts - 1282 + 1286 You must fill in <b></b> rows in the <b>Table</b> on <b></b> src/assets/wise5/services/projectService.ts - 1284 + 1288 Wait for your teacher to unlock the item src/assets/wise5/services/projectService.ts - 1287 + 1291 diff --git a/src/style/styles.scss b/src/style/styles.scss index a9ff04bd1ed..62cae66dc2f 100644 --- a/src/style/styles.scss +++ b/src/style/styles.scss @@ -63,6 +63,7 @@ @import 'themes/dark'; @import 'themes/author'; @import 'themes/monitor'; +@import "~@ng-select/ng-select/themes/material.theme.css"; // TODO: remove/merge after upgrading apps to Angular @import 'themes/apps';