diff --git a/src/app/services/teacherProjectTranslationService.spec.ts b/src/app/services/teacherProjectTranslationService.spec.ts index d7ddb632fa6..9509e8eb58c 100644 --- a/src/app/services/teacherProjectTranslationService.spec.ts +++ b/src/app/services/teacherProjectTranslationService.spec.ts @@ -48,4 +48,19 @@ describe('TeacherProjectTranslationService', () => { expect(request.request.body).toEqual({}); }); }); + describe('getTranslationSuggestion()', () => { + it('makes a POST request to backend with do not translate tags', () => { + service + .getTranslationSuggestion('srcLang', 'targetLang', 'srcText untagged') + .subscribe(); + const request = http.expectOne(`/api/author/project/translate/suggest`); + expect(request.request.method).toEqual('POST'); + expect(request.request.body).toEqual({ + srcLang: 'srcLang', + targetLang: 'targetLang', + srcText: + 'srcText untagged' + }); + }); + }); }); diff --git a/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.scss b/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.scss index 2f11ee88776..86ccac6b154 100644 --- a/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.scss +++ b/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.scss @@ -1,21 +1,45 @@ +@reference "tailwindcss"; + mat-form-field.translatable-form-field { width: 100%; + + .mat-mdc-form-field-bottom-align::before { + @apply -ms-2; + } } .translatable { .mat-mdc-form-field-hint-wrapper { position: relative; - margin: -20px 0 4px; + margin: 0; + padding: 0; flex-direction: column; align-items: flex-start; } + .mat-mdc-form-field-bottom-align::before { + display: none; + } + .mat-mdc-form-field-hint-spacer { - display: none; + display: none; } .source-language { - padding: 4px 8px; - margin-top: 4px; + @apply px-2 py-1; } } + +.translation-tools { + @apply flex flex-wrap items-center gap-2 text-sm; +} + +.translation-link { + @apply flex items-center gap-1; + + .mat-icon { + font-size: inherit; + height: 1rem; + width: 1rem; + } +} \ No newline at end of file diff --git a/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.ts b/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.ts index e58bde1ec7f..2197ca43032 100644 --- a/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.ts +++ b/src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.ts @@ -1,12 +1,15 @@ +import { ConfigService } from '../../../services/configService'; +import { copy } from '../../../common/object/object'; +import { generateRandomKey } from '../../../common/string/string'; import { Input, Signal, Output, computed, Directive } from '@angular/core'; -import { Subject, Subscription, debounceTime } from 'rxjs'; import { Language } from '../../../../../app/domain/language'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Subject, Subscription, debounceTime } from 'rxjs'; import { TeacherProjectTranslationService } from '../../../services/teacherProjectTranslationService'; import { TeacherProjectService } from '../../../services/teacherProjectService'; -import { generateRandomKey } from '../../../common/string/string'; import { toObservable } from '@angular/core/rxjs-interop'; import { Translations } from '../../../../../app/domain/translations'; -import { copy } from '../../../common/object/object'; +import { TranslationSuggestionsDialogComponent } from '../translation-suggestions-dialog/translation-suggestions-dialog.component'; @Directive() export abstract class AbstractTranslatableFieldComponent { @@ -27,6 +30,8 @@ export abstract class AbstractTranslatableFieldComponent { protected translationText: string; protected translationTextChanged: Subject = new Subject(); constructor( + protected configService: ConfigService, + protected dialog: MatDialog, protected projectService: TeacherProjectService, protected projectTranslationService: TeacherProjectTranslationService ) {} @@ -71,8 +76,57 @@ export abstract class AbstractTranslatableFieldComponent { } protected saveTranslationText(text: string): void { + if (this.i18nId === undefined) { + this.createI18NField(); + } const currentTranslations = copy(this.projectTranslationService.currentTranslations()); currentTranslations[this.i18nId] = { value: text, modified: new Date().getTime() }; this.projectTranslationService.saveCurrentTranslations(currentTranslations).subscribe(); } + + protected isTranslationServiceEnabled(): boolean { + return this.configService.getConfigParam('translationServiceEnabled'); + } + + protected async translateText(event: Event): Promise { + event.preventDefault(); + if (this.translationText) { + this.openDialog(); + } else { + this.projectTranslationService + .getTranslationSuggestion( + this.defaultLanguage.language, + this.currentLanguage().language, + this.content[this.key] + ) + .subscribe({ + next: (translation) => this.saveTranslationText(translation), + error: () => + alert( + $localize`There was an error translating the text. Please contact WISE staff if the error persists.` + ) + }); + } + } + + private openDialog(): void { + const dialogRef = this.createDialogRef(); + dialogRef.afterClosed().subscribe((result: string) => { + if (result) { + this.saveTranslationText(result); + } + }); + } + + private createDialogRef(): MatDialogRef { + return this.dialog.open(TranslationSuggestionsDialogComponent, { + panelClass: 'dialog-md', + data: { + defaultLanguage: this.defaultLanguage.language, + currentLanguage: this.currentLanguage().language, + defaultLanguageContent: this.content[this.key], + currentLanguageContent: this.translationText + } + }); + } } diff --git a/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.spec.ts b/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.spec.ts index 56b20343fe7..8d3dd60f57c 100644 --- a/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.spec.ts +++ b/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfigService } from '../../../services/configService'; import { TranslatableAssetChooserComponent } from './translatable-asset-chooser.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -21,7 +22,7 @@ describe('TranslatableAssetChooserComponent', () => { StudentTeacherCommonServicesModule, TranslatableAssetChooserComponent ], - providers: [TeacherProjectService, TeacherProjectTranslationService] + providers: [ConfigService, TeacherProjectService, TeacherProjectTranslationService] }); spyOn(TestBed.inject(TeacherProjectService), 'getLocale').and.returnValue( new ProjectLocale({ default: 'en-US' }) diff --git a/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.ts b/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.ts index 610a9f945bf..390790affb1 100644 --- a/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.ts +++ b/src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { ConfigService } from '../../../services/configService'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -22,11 +23,12 @@ export class TranslatableAssetChooserComponent extends AbstractTranslatableField }; constructor( - private dialog: MatDialog, + protected configService: ConfigService, + protected dialog: MatDialog, protected projectService: TeacherProjectService, protected projectTranslationService: TeacherProjectTranslationService ) { - super(projectService, projectTranslationService); + super(configService, dialog, projectService, projectTranslationService); } protected chooseAsset(): void { diff --git a/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.html b/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.html index 0e418991fa8..ef89f7a2f34 100644 --- a/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.html +++ b/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.html @@ -11,9 +11,17 @@ @if (hint) { {{ hint }} } - - translate {{ defaultLanguage.language }}: - {{ content[key] }} + +
+ translate {{ defaultLanguage.language }}: + {{ content[key] }} +
+ @if (content[key] && isTranslationServiceEnabled()) { + + auto_awesome + Translate with AI + + }
} @else { diff --git a/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.spec.ts b/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.spec.ts index de8a10a8ef8..913b95b8c7b 100644 --- a/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.spec.ts +++ b/src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfigService } from '../../../services/configService'; import { MockProviders } from 'ng-mocks'; import { ProjectLocale } from '../../../../../app/domain/projectLocale'; import { TeacherProjectService } from '../../../services/teacherProjectService'; @@ -12,7 +13,9 @@ describe('TranslatableInputComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [TranslatableInputComponent], - providers: [MockProviders(TeacherProjectService, TeacherProjectTranslationService)] + providers: [ + MockProviders(ConfigService, TeacherProjectService, TeacherProjectTranslationService) + ] }); const projectService = TestBed.inject(TeacherProjectService); spyOn(projectService, 'getLocale').and.returnValue(new ProjectLocale({ default: 'en-US' })); diff --git a/src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html b/src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html index 900e80f181f..5378f0fdd91 100644 --- a/src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html +++ b/src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html @@ -2,7 +2,7 @@ -
+
+ @if (content[key] && isTranslationServiceEnabled()) { + + auto_awesome + Translate with AI + + }
-
+
Note: Editing is disabled. Please switch back to {{ defaultLanguage.language }} if you want to edit.{{ hint }} } - - translate {{ defaultLanguage.language }}: - {{ content[key] }} + +
+ translate {{ defaultLanguage.language }}: + {{ content[key] }} +
+ @if (content[key] && isTranslationServiceEnabled()) { + + auto_awesome + Translate with AI + + }
} @else { diff --git a/src/assets/wise5/authoringTool/components/translatable-textarea/translatable-textarea.component.spec.ts b/src/assets/wise5/authoringTool/components/translatable-textarea/translatable-textarea.component.spec.ts index 11aad65d97f..5aaf3f50f86 100644 --- a/src/assets/wise5/authoringTool/components/translatable-textarea/translatable-textarea.component.spec.ts +++ b/src/assets/wise5/authoringTool/components/translatable-textarea/translatable-textarea.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfigService } from '../../../services/configService'; import { TranslatableTextareaComponent } from './translatable-textarea.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -19,7 +20,7 @@ describe('TranslatableTextareaComponent', () => { StudentTeacherCommonServicesModule, TranslatableTextareaComponent ], - providers: [TeacherProjectTranslationService, TeacherProjectService] + providers: [ConfigService, TeacherProjectTranslationService, TeacherProjectService] }); spyOn(TestBed.inject(TeacherProjectService), 'getLocale').and.returnValue( new ProjectLocale({ default: 'en-US' }) diff --git a/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html new file mode 100644 index 00000000000..ef1cc80dc74 --- /dev/null +++ b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html @@ -0,0 +1,18 @@ +

Replace Translation

+ + +

{{ this.data.defaultLanguage }} text:

+

{{ this.data.defaultLanguageContent }}

+

{{ this.data.currentLanguage }} text (current translation):

+

{{ this.data.currentLanguageContent }}

+

{{ this.data.currentLanguage }} text (AI suggested translation):

+

{{ this.translation }}

+

+ Do you want to replace the current translation with the AI suggested translation? +

+
+ + + + + diff --git a/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.scss b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.scss new file mode 100644 index 00000000000..2991245a003 --- /dev/null +++ b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.scss @@ -0,0 +1,7 @@ +.text-content { + margin-left: 15px; +} + +.replace-translation { + margin-top: 20px; +} \ No newline at end of file diff --git a/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.spec.ts b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.spec.ts new file mode 100644 index 00000000000..5a129a2df31 --- /dev/null +++ b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TeacherProjectTranslationService } from '../../../services/teacherProjectTranslationService'; +import { TranslationSuggestionsDialogComponent } from './translation-suggestions-dialog.component'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; + +describe('TranslationSuggestionsDialogComponent', () => { + let component: TranslationSuggestionsDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslationSuggestionsDialogComponent], + providers: [ + MockProvider(TeacherProjectTranslationService), + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} } + ] + }).compileComponents(); + + spyOn( + TestBed.inject(TeacherProjectTranslationService), + 'getTranslationSuggestion' + ).and.returnValue(of('Example translated text')); + + fixture = TestBed.createComponent(TranslationSuggestionsDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.ts b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.ts new file mode 100644 index 00000000000..ce4ac16ebb7 --- /dev/null +++ b/src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.ts @@ -0,0 +1,54 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatInputModule } from '@angular/material/input'; +import { TeacherProjectTranslationService } from '../../../services/teacherProjectTranslationService'; + +interface TranslationSuggestionsDialogData { + defaultLanguage: string; + currentLanguage: string; + defaultLanguageContent: string; + currentLanguageContent?: string; +} + +@Component({ + imports: [MatDividerModule, MatInputModule, FormsModule, MatButtonModule, MatDialogModule], + selector: 'translation-suggestions-dialog', + styles: [ + ` + .mat-divider { + margin: 0; + } + ` + ], + templateUrl: './translation-suggestions-dialog.component.html' +}) +export class TranslationSuggestionsDialogComponent { + readonly dialogRef = inject(MatDialogRef); + readonly data = inject(MAT_DIALOG_DATA); + protected translation; + + constructor(protected projectTranslationService: TeacherProjectTranslationService) { + this.generateTranslationSuggestion(); + } + + private generateTranslationSuggestion(): void { + this.projectTranslationService + .getTranslationSuggestion( + this.data.defaultLanguage, + this.data.currentLanguage, + this.data.defaultLanguageContent + ) + .subscribe({ + next: (suggestedTranslation) => (this.translation = suggestedTranslation), + error: () => + alert($localize`There was an error translating the text. Please talk to WISE staff.`) + }); + } + + protected onClose(saveTranslation: boolean): void { + this.dialogRef.close(saveTranslation && this.translation); + } +} diff --git a/src/assets/wise5/components/animation/animation-authoring/animation-authoring.component.html b/src/assets/wise5/components/animation/animation-authoring/animation-authoring.component.html index 593078a7cb5..08e877460c4 100644 --- a/src/assets/wise5/components/animation/animation-authoring/animation-authoring.component.html +++ b/src/assets/wise5/components/animation/animation-authoring/animation-authoring.component.html @@ -105,7 +105,7 @@ let isLast = $last ) {
-
+
@if (object.type === 'image') { -
+
} @if (object.type === 'image') { -
+
+
Time -
+
-
+
-
+
-
+
X Axis @if (enableMultipleYAxes) { @for (y of [].constructor(componentContent.yAxis.length); track y; let yAxisIndex = $index) { -
+
Y Axis
} -
+
-
+
-
+
-
+
-
+
}
-
+
}
-
+
+
@for (cell of row; track cell) { -
+
{ + return this.http + .post( + `/api/author/project/translate/suggest`, + { + srcLang: defaultLanguage, + targetLang: currentLanguage, + srcText: this.addDoNotTranslateTags(defaultLanguageText) + }, + { responseType: 'text' } + ) + .pipe(map(this.removeDoNotTranslateTags)); + } + + private addDoNotTranslateTags(textToTranslate: string): string { + return textToTranslate + .replaceAll(/<.*?>/g, (match) => '' + match + '') + .replaceAll( + /link-text='.*?'/g, + (match) => match.slice(0, 11) + '' + match.slice(11, -1) + '\'' + ); + } + + private removeDoNotTranslateTags(translatedText: string): string { + return translatedText.replaceAll(/<.*?><\/span>/g, (match) => + match.slice(21, -7) + ); + } } diff --git a/src/messages.xlf b/src/messages.xlf index 3a55e3f5715..4d3fe9d9a6c 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -491,6 +491,10 @@ src/assets/wise5/authoringTool/choose-node-location/choose-move-node-location/choose-move-node-location.component.html 3,6 + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 16,17 + src/assets/wise5/authoringTool/create-branch/create-branch.component.html 50,54 @@ -11063,6 +11067,13 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.5,9 + + There was an error translating the text. Please contact WISE staff if the error persists. + + src/assets/wise5/authoringTool/components/abstract-translatable-field/abstract-translatable-field.component.ts + 106 + + Selected @@ -11257,7 +11268,22 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Choose image src/assets/wise5/authoringTool/components/translatable-asset-chooser/translatable-asset-chooser.component.ts - 19 + 20 + + + + Translate with AI + + src/assets/wise5/authoringTool/components/translatable-input/translatable-input.component.html + 22,27 + + + src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html + 18,22 + + + src/assets/wise5/authoringTool/components/translatable-textarea/translatable-textarea.component.html + 23,28 @@ -11271,21 +11297,70 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Note: Editing is disabled. Please switch back to if you want to edit. src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html - 27,28 + 33,34 Copy content to src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.html - 37,38 + 43,44 Are you sure you want to replace the content in with content in for this item? src/assets/wise5/authoringTool/components/translatable-rich-text-editor/translatable-rich-text-editor.component.ts - 50,52 + 59,61 + + + + Replace Translation + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 1,4 + + + + text: + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 4,5 + + + + text (current translation): + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 6,7 + + + + text (AI suggested translation): + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 8,9 + + + + Do you want to replace the current translation with the AI suggested translation? + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 11,15 + + + + Replace + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.html + 17,19 + + + + There was an error translating the text. Please talk to WISE staff. + + src/assets/wise5/authoringTool/components/translation-suggestions-dialog/translation-suggestions-dialog.component.ts + 47 @@ -20067,7 +20142,7 @@ Category Name: src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html - 297,301 + 297,302