Skip to content

Commit 96f354d

Browse files
SF-3537 Connect quotation denormalization to frontend UI (#3336)
1 parent 965ea4c commit 96f354d

19 files changed

+292
-138
lines changed

src/RealtimeServer/scriptureforge/models/translate-config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export enum ParagraphBreakFormat {
3636
}
3737

3838
export enum QuoteFormat {
39-
Automatic = 'automatic',
40-
Straight = 'straight'
39+
Denormalized = 'denormalized',
40+
Normalized = 'normalized'
4141
}
4242

4343
export interface BaseProject {
@@ -55,6 +55,7 @@ export interface ProjectScriptureRange {
5555

5656
export interface DraftUsfmConfig {
5757
paragraphFormat: ParagraphBreakFormat;
58+
quoteFormat: QuoteFormat;
5859
}
5960

6061
export interface DraftConfig {

src/RealtimeServer/scriptureforge/services/sf-project-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ export class SFProjectService extends ProjectService<SFProject> {
317317
properties: {
318318
paragraphFormat: {
319319
enum: ['best_guess', 'remove', 'move_to_end']
320+
},
321+
quoteFormat: {
322+
enum: ['denormalized', 'normalized']
320323
}
321324
},
322325
additionalProperties: false

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { Canon } from '@sillsdev/scripture';
55
import { saveAs } from 'file-saver';
66
import JSZip from 'jszip';
77
import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data';
8+
import {
9+
DraftUsfmConfig,
10+
ParagraphBreakFormat,
11+
QuoteFormat
12+
} from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
13+
import { DeltaOperation } from 'rich-text';
814
import { of } from 'rxjs';
915
import { first } from 'rxjs/operators';
1016
import { anything, mock, verify } from 'ts-mockito';
@@ -59,6 +65,33 @@ describe('DraftGenerationService', () => {
5965
queueDepth: 0
6066
};
6167

68+
function getTestDeltaOps(): DeltaOperation[] {
69+
return [
70+
{
71+
insert: {
72+
chapter: {
73+
number: '1',
74+
style: 'c'
75+
}
76+
}
77+
},
78+
{
79+
insert: {
80+
verse: {
81+
number: '1',
82+
style: 'v'
83+
}
84+
}
85+
},
86+
{
87+
insert: 'Verse 1 Contents',
88+
attributes: {
89+
segment: 'verse_1_1'
90+
}
91+
}
92+
];
93+
}
94+
6295
beforeEach(() => {
6396
service = TestBed.inject(DraftGenerationService);
6497
httpTestingController = TestBed.inject(HttpTestingController);
@@ -381,30 +414,7 @@ describe('DraftGenerationService', () => {
381414
it('should get the pre-translation ops for the specified book/chapter and return an observable', fakeAsync(() => {
382415
const book = 43;
383416
const chapter = 3;
384-
const ops = [
385-
{
386-
insert: {
387-
chapter: {
388-
number: '1',
389-
style: 'c'
390-
}
391-
}
392-
},
393-
{
394-
insert: {
395-
verse: {
396-
number: '1',
397-
style: 'v'
398-
}
399-
}
400-
},
401-
{
402-
insert: 'Verse 1 Contents',
403-
attributes: {
404-
segment: 'verse_1_1'
405-
}
406-
}
407-
];
417+
const ops = getTestDeltaOps();
408418
const preTranslationDeltaData = {
409419
id: `${projectId}:${Canon.bookNumberToId(book)}:${chapter}:target`,
410420
version: 0,
@@ -432,30 +442,7 @@ describe('DraftGenerationService', () => {
432442
const book = 43;
433443
const chapter = 3;
434444
const timestamp = new Date();
435-
const ops = [
436-
{
437-
insert: {
438-
chapter: {
439-
number: '1',
440-
style: 'c'
441-
}
442-
}
443-
},
444-
{
445-
insert: {
446-
verse: {
447-
number: '1',
448-
style: 'v'
449-
}
450-
}
451-
},
452-
{
453-
insert: 'Verse 1 Contents',
454-
attributes: {
455-
segment: 'verse_1_1'
456-
}
457-
}
458-
];
445+
const ops = getTestDeltaOps();
459446
const preTranslationDeltaData = {
460447
id: `${projectId}:${Canon.bookNumberToId(book)}:${chapter}:target`,
461448
version: 0,
@@ -481,6 +468,41 @@ describe('DraftGenerationService', () => {
481468
tick();
482469
}));
483470

471+
it('should get the pretranslation ops with a specific USFM config and return an observable', fakeAsync(() => {
472+
const book = 43;
473+
const chapter = 3;
474+
const ops = getTestDeltaOps();
475+
const preTranslationDeltaData = {
476+
id: `${projectId}:${Canon.bookNumberToId(book)}:${chapter}:target`,
477+
version: 0,
478+
data: {
479+
ops
480+
}
481+
};
482+
483+
const config: DraftUsfmConfig = {
484+
paragraphFormat: ParagraphBreakFormat.MoveToEnd,
485+
quoteFormat: QuoteFormat.Normalized
486+
};
487+
488+
// SUT
489+
service.getGeneratedDraftDeltaOperations(projectId, book, chapter, undefined, config).subscribe(result => {
490+
expect(result).toEqual(ops);
491+
});
492+
tick();
493+
494+
const queryParams = new URLSearchParams();
495+
queryParams.append('paragraphFormat', config.paragraphFormat);
496+
queryParams.append('quoteFormat', config.quoteFormat);
497+
// Setup the HTTP request
498+
const req = httpTestingController.expectOne(
499+
`${MACHINE_API_BASE_URL}translation/engines/project:${projectId}/actions/pretranslate/${book}_${chapter}/delta?${queryParams.toString()}`
500+
);
501+
expect(req.request.method).toEqual('GET');
502+
req.flush(preTranslationDeltaData);
503+
tick();
504+
}));
505+
484506
it('should return an empty array for missing data', fakeAsync(() => {
485507
const book = 43;
486508
const chapter = 3;

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export class DraftGenerationService {
224224
}
225225
if (usfmConfig != null) {
226226
params.append('paragraphFormat', usfmConfig.paragraphFormat);
227+
params.append('quoteFormat', usfmConfig.quoteFormat);
227228
}
228229
if (params.size > 0) {
229230
url += `?${params.toString()}`;

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.html

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,23 @@ <h1>{{ t("formatting_options") }}</h1>
3434
</mat-card>
3535
<mat-card class="options">
3636
<mat-card-header>
37-
<mat-card-title> {{ t("quote_style") }} </mat-card-title>
37+
<mat-card-title>{{ t("quote_style_title") }}</mat-card-title>
3838
</mat-card-header>
3939
<mat-card-content class="format-options">
40-
{{ t("choose_how_quotes_appear") }}
40+
{{ t("quote_style_description") }}
4141

4242
<!-- TODO make this notice conditional -->
4343
<app-notice type="warning" mode="fill-dark"> {{ t("no_quote_convention_detected") }} </app-notice>
4444

4545
<mat-radio-group formControlName="quoteFormat">
46-
<mat-radio-button [value]="quoteStyleFormat.Automatic">
47-
<span class="option-title"> {{ t("automatic_quote_style") }} </span>
48-
<span class="new"> {{ t("new") }} </span>
49-
<span class="description"> {{ t("automatic_quote_style_description") }} </span>
46+
<mat-radio-button [value]="quoteStyle.Denormalized">
47+
<span class="option-title">{{ t("quote_style_automatic") }}</span>
48+
<span class="new">{{ t("new") }}</span>
49+
<span class="description">{{ t("quote_style_automatic_description") }}</span>
5050
</mat-radio-button>
51-
<mat-radio-button [value]="quoteStyleFormat.Straight">
52-
<span class="option-title"> {{ t("straight_quotes") }} </span>
53-
<span class="description"> {{ t("straight_quotes_description") }} </span>
51+
<mat-radio-button [value]="quoteStyle.Normalized">
52+
<span class="option-title">{{ t("quote_style_straight") }}</span>
53+
<span class="description">{{ t("quote_style_straight_description") }}</span>
5454
</mat-radio-button>
5555
</mat-radio-group>
5656
</mat-card-content>

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
display: flex;
1313
flex-direction: row;
1414
column-gap: 8px;
15-
height: 90%;
15+
height: 100%;
1616
margin-top: 8px;
1717

1818
@include breakpoints.media-breakpoint-down(sm) {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.spec.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ describe('DraftUsfmFormatComponent', () => {
6767
}));
6868

6969
it('shows message if user is not online', fakeAsync(async () => {
70-
const env = new TestEnvironment({ config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd } });
70+
const env = new TestEnvironment({
71+
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
72+
});
7173
expect(env.offlineMessage).toBeNull();
7274

7375
env.onlineStatusService.setIsOnline(false);
@@ -81,7 +83,9 @@ describe('DraftUsfmFormatComponent', () => {
8183

8284
// Book and chapter changed
8385
it('navigates to a different book and chapter', fakeAsync(() => {
84-
const env = new TestEnvironment({ config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd } });
86+
const env = new TestEnvironment({
87+
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
88+
});
8589
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
8690
expect(env.component.chapters.length).toEqual(1);
8791
expect(env.component.booksWithDrafts.length).toEqual(2);
@@ -101,17 +105,22 @@ describe('DraftUsfmFormatComponent', () => {
101105
it('should initialize and default to best guess and automatic quotes', fakeAsync(async () => {
102106
const env = new TestEnvironment();
103107
expect(env.component.paragraphFormat.value).toBe(ParagraphBreakFormat.BestGuess);
104-
expect(env.component.quoteFormat.value).toBe(QuoteFormat.Automatic);
108+
expect(env.component.quoteFormat.value).toBe(QuoteFormat.Denormalized);
105109
expect(await env.component.confirmLeave()).toBe(true);
106110
}));
107111

108112
it('should show the currently selected format options', fakeAsync(() => {
109-
const env = new TestEnvironment({ config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd } });
113+
const env = new TestEnvironment({
114+
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
115+
});
110116
expect(env.component.paragraphFormat.value).toBe(ParagraphBreakFormat.MoveToEnd);
117+
expect(env.component.quoteFormat.value).toBe(QuoteFormat.Normalized);
111118
}));
112119

113120
it('goes back if user chooses different configurations and then goes back', fakeAsync(async () => {
114-
const env = new TestEnvironment({ config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd } });
121+
const env = new TestEnvironment({
122+
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
123+
});
115124
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
116125
expect(env.harnesses?.length).toEqual(5);
117126
await env.harnesses![0].check();
@@ -130,14 +139,17 @@ describe('DraftUsfmFormatComponent', () => {
130139
}));
131140

132141
it('should save changes to the draft format', fakeAsync(async () => {
133-
const env = new TestEnvironment({ config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd } });
142+
const env = new TestEnvironment({
143+
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
144+
});
134145
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
135146
expect(env.harnesses?.length).toEqual(5);
136147
await env.harnesses![0].check();
137148
tick();
138149
env.fixture.detectChanges();
139150
const config: DraftUsfmConfig = {
140-
paragraphFormat: ParagraphBreakFormat.BestGuess
151+
paragraphFormat: ParagraphBreakFormat.BestGuess,
152+
quoteFormat: QuoteFormat.Denormalized
141153
};
142154
verify(mockedProjectService.onlineSetUsfmConfig(env.projectId, anything())).never();
143155
verify(mockedDraftHandlingService.getDraft(anything(), anything())).twice();

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
6363
chapters: number[] = [];
6464
isInitializing: boolean = true;
6565
paragraphBreakFormat = ParagraphBreakFormat;
66-
quoteStyleFormat = QuoteFormat;
66+
quoteStyle = QuoteFormat;
6767

6868
paragraphFormat = new FormControl<ParagraphBreakFormat>(ParagraphBreakFormat.BestGuess);
69-
quoteFormat = new FormControl<QuoteFormat>(QuoteFormat.Automatic);
69+
quoteFormat = new FormControl<QuoteFormat>(QuoteFormat.Denormalized);
7070
usfmFormatForm: FormGroup = new FormGroup({
7171
paragraphFormat: this.paragraphFormat,
7272
quoteFormat: this.quoteFormat
@@ -111,7 +111,10 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
111111

112112
private get currentFormat(): DraftUsfmConfig | undefined {
113113
const paragraphFormat = this.paragraphFormat.value;
114-
return paragraphFormat == null ? undefined : { paragraphFormat };
114+
const quoteFormat = this.quoteFormat.value;
115+
// both values must be set to be valid
116+
if (paragraphFormat == null || quoteFormat == null) return undefined;
117+
return { paragraphFormat, quoteFormat };
115118
}
116119

117120
ngAfterViewInit(): void {
@@ -191,7 +194,12 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
191194
}
192195

193196
async confirmLeave(): Promise<boolean> {
194-
if (this.lastSavedState?.paragraphFormat === this.currentFormat?.paragraphFormat) return true;
197+
if (
198+
this.lastSavedState?.paragraphFormat === this.currentFormat?.paragraphFormat &&
199+
this.lastSavedState?.quoteFormat === this.currentFormat?.quoteFormat
200+
) {
201+
return true;
202+
}
195203
return this.dialogService.confirm(
196204
this.i18n.translate('draft_sources.discard_changes_confirmation'),
197205
this.i18n.translate('draft_sources.leave_and_discard'),
@@ -202,7 +210,7 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
202210
private setUsfmConfig(config?: DraftUsfmConfig): void {
203211
this.usfmFormatForm.setValue({
204212
paragraphFormat: config?.paragraphFormat ?? ParagraphBreakFormat.BestGuess,
205-
quoteFormat: QuoteFormat.Automatic
213+
quoteFormat: config?.quoteFormat ?? QuoteFormat.Denormalized
206214
});
207215
this.lastSavedState = this.currentFormat;
208216

src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -350,30 +350,29 @@
350350
"unknown_language_code": "unknown"
351351
},
352352
"draft_usfm_format": {
353-
"automatic_quote_style_description": "Use the same quote style as the rest of the project, if possible. Recommended for most projects.",
354-
"automatic_quote_style": "Automatic quote style",
355353
"available_for_each_draft": "You can do this for every new draft if you wish.",
356354
"cancel": "Cancel",
357-
"choose_how_quotes_appear": "Choose how quotes should appear",
358355
"connect_to_the_internet": "Please connect to the internet to change the draft formatting options.",
359356
"draft_format_description": "The draft has been created. Here are some formatting options for the text. You can try each option to see the difference it makes.",
360-
"draft_format_options": "Format draft",
361357
"failed_to_save": "Failed to save changes. Try again later.",
362358
"formatting_options": "Formatting options",
363359
"line_breaks_description": "Scripture Forge translates complete verses, without line breaks, because this gives the best translation. Choose where to place the line breaks from the source.",
364360
"line_breaks_title": "Line breaks",
365361
"new": "new",
366362
"no_quote_convention_detected": "No quote convention could be detected for your project. Straight quotes will be used in the draft regardless of which option you choose.",
367-
"option_best_guess_description": "Line breaks in the source will be placed at the best guess of where they should be in the draft. Recommended for most projects.",
368363
"option_best_guess": "Best guess",
364+
"option_best_guess_description": "Line breaks in the source will be placed at the best guess of where they should be in the draft. Recommended for most projects.",
369365
"option_end_description": "Line breaks in the source will be moved to the end of the verse.",
370366
"option_end": "Move to end",
371367
"option_remove_description": "Line breaks in the source will be omitted from the draft.",
372368
"option_remove": "Remove",
373-
"quote_style": "Quote style",
374-
"save_changes": "Save",
375-
"straight_quotes_description": "Use only straight quotes in the draft.",
376-
"straight_quotes": "Straight quotes"
369+
"quote_style_automatic": "Automatic quote style",
370+
"quote_style_automatic_description": "Use the same quote style as the rest of the project, if possible. Recommended for most projects.",
371+
"quote_style_description": "Choose how quotes should appear.",
372+
"quote_style_straight": "Straight quotes",
373+
"quote_style_straight_description": "Use only straight quotes in the draft.",
374+
"quote_style_title": "Quote style",
375+
"save_changes": "Save"
377376
},
378377
"editor": {
379378
"add_comment": "Add Comment",

0 commit comments

Comments
 (0)