Skip to content

Commit

Permalink
Improve handling of language codes, clean up code
Browse files Browse the repository at this point in the history
  • Loading branch information
Nateowami committed Feb 13, 2025
1 parent 3abca01 commit 7bc01ef
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h3>Translated project {{ parentheses(targetLanguageDisplayName) }}</h3>
<div class="project">
<span class="project-name">{{ project.name }}</span>
<span class="language-code">
{{ t("language_code") }} <strong>{{ project.writingSystem?.tag }}</strong>
{{ t("language_code") }} <strong>{{ project.writingSystem.tag }}</strong>
</span>
</div>
}
Expand Down Expand Up @@ -156,7 +156,7 @@ <h3>Source {{ parentheses(sourceLanguageDisplayName) }}</h3>

<div class="confirm-language-codes">
<ng-container *transloco="let t; read: 'confirm_draft_sources'">
@if (sourceSideLanguageCodes.length > 1) {
@if (multipleSourceSideLanguages) {
<app-notice mode="fill-dark" type="warning">
<h3>All source and reference projects should be in the same language</h3>
<p>
Expand Down Expand Up @@ -187,7 +187,7 @@ <h3>Source and target languages are both {{ i18n.getLanguageDisplayName(targetLa
</app-notice>
}

@if (sourceSideLanguageCodes.length === 1 && !showSourceAndTargetLanguagesIdenticalWarning) {
@if (!multipleSourceSideLanguages && !showSourceAndTargetLanguagesIdenticalWarning) {
<app-notice mode="fill-dark">
<h3>{{ t("incorrect_language_codes_reduce_quality") }}</h3>
<p>Please make sure all the language codes are correct before saving.</p>
Expand All @@ -214,7 +214,7 @@ <h3>{{ t("incorrect_language_codes_reduce_quality") }}</h3>
</mat-card-header>
<mat-card-content>
@if (getControlState("projectSettings") === ElementState.Submitting) {
<div class="saving-indicator"><mat-spinner [diameter]="24" [color]="'primary'"></mat-spinner> Saving</div>
<div class="saving-indicator"><mat-spinner [diameter]="24" color="primary"></mat-spinner> Saving</div>
} @else {
<div class="saving-indicator"><mat-icon class="success">checkmark</mat-icon> All changes saved</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ describe('DraftSourcesComponent', () => {
expect(env.component.resources).toBeDefined();
}));

it('should not save when language codes are not confirmed', fakeAsync(() => {
const env = new TestEnvironment();
env.component.languageCodesConfirmed = false;

env.component.save();
tick();

verify(mockedSFProjectService.onlineUpdateSettings(anything(), anything())).never();
}));

it('updates control state during save', fakeAsync(() => {
const env = new TestEnvironment();
when(mockedSFProjectService.onlineUpdateSettings(anything(), anything())).thenResolve();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MatCardModule } from '@angular/material/card';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Router } from '@angular/router';
import { TranslocoModule } from '@ngneat/transloco';
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
Expand All @@ -27,6 +27,7 @@ import { ParatextService, SelectableProject, SelectableProjectWithLanguageCode }
import { SFProjectService } from '../../../core/sf-project.service';
import { NoticeComponent } from '../../../shared/notice/notice.component';
import { isSFProjectSyncing } from '../../../sync/sync.component';
import { countNonEquivalentLanguageCodes } from '../draft-utils';

function translateSourceToSelectableProjectWithLanguageTag(
project: TranslateSource
Expand Down Expand Up @@ -113,7 +114,7 @@ export function projectToDraftSources(project: SFProjectProfile): DraftSourcesAs
TranslocoModule,
NoticeComponent,
MatCheckboxModule,
MatProgressSpinner
MatProgressSpinnerModule
],
templateUrl: './draft-sources.component.html',
styleUrl: './draft-sources.component.scss'
Expand Down Expand Up @@ -190,17 +191,25 @@ export class DraftSourcesComponent extends DataLoadingComponent implements OnIni
return value ? `(${value})` : '';
}

get multipleSourceSideLanguages(): boolean {
return countNonEquivalentLanguageCodes(this.sourceSideLanguageCodes) > 1;
}

get sourceSideLanguageCodes(): string[] {
return Array.from(
// FIXME Handle language codes that may be equivalent by not identical strings
new Set([...this.draftingSources, ...this.trainingSources].filter(s => s != null).map(s => s.languageTag))
);
}

get showSourceAndTargetLanguagesIdenticalWarning(): boolean {
// Show the warning when there's only one language on the source side, but that one language is equivalent to the
// target language.
const sourceCodes = this.sourceSideLanguageCodes;
// FIXME Handle language codes that may be equivalent by not identical strings
return sourceCodes.length === 1 && sourceCodes[0] === this.trainingTargets[0]!.writingSystem.tag;
return (
sourceCodes.length > 0 &&
countNonEquivalentLanguageCodes(sourceCodes) === 1 &&
countNonEquivalentLanguageCodes([sourceCodes[0], this.targetLanguageTag]) < 2
);
}

get targetLanguageTag(): string {
Expand All @@ -226,18 +235,15 @@ export class DraftSourcesComponent extends DataLoadingComponent implements OnIni
const { trainingSources, trainingTargets, draftingSources } = projectToDraftSources(projectDoc.data);
if (trainingSources.length > 2) throw new Error('More than 2 training sources is not supported');

const mappedTrainingSources = trainingSources
// FIXME No actual nullish elements, but TS doesn't know because of how we set the array length
.filter(s => s != null)
.map(translateSourceToSelectableProjectWithLanguageTag) as SelectableProjectWithLanguageCode[] &
({ length: 0 } | { length: 1 } | { length: 2 });
const mappedTrainingSources = trainingSources.map(
translateSourceToSelectableProjectWithLanguageTag
) as SelectableProjectWithLanguageCode[] & ({ length: 0 } | { length: 1 } | { length: 2 });

this.trainingSources = mappedTrainingSources;
this.trainingTargets = trainingTargets;
const mappedDraftingSources: SelectableProjectWithLanguageCode[] = draftingSources
// FIXME No actual nullish elements, but TS doesn't know because of how we set the array length
.filter(s => s != null)
.map(translateSourceToSelectableProjectWithLanguageTag);
const mappedDraftingSources: SelectableProjectWithLanguageCode[] = draftingSources.map(
translateSourceToSelectableProjectWithLanguageTag
);
this.draftingSources = [mappedDraftingSources[0]];

if (this.draftingSources.length < 1) this.draftingSources.push(undefined);
Expand Down Expand Up @@ -294,8 +300,8 @@ export class DraftSourcesComponent extends DataLoadingComponent implements OnIni
array[index] = undefined;
// When the user clears a project select, if there are now multiple blank project selects, remove the first one
if (array.filter(s => s == null).length > 1) {
const index = array.findIndex(s => s == null);
array.splice(index, 1);
const nullIndex = array.findIndex(s => s == null);
array.splice(nullIndex, 1);
}
}
}
Expand All @@ -306,8 +312,8 @@ export class DraftSourcesComponent extends DataLoadingComponent implements OnIni
const arrays = [this.trainingSources, this.trainingTargets, this.draftingSources];
for (const array of arrays) {
while (array.length > 1 && array.some(s => s == null)) {
const index = array.findIndex(s => s == null);
array.splice(index, 1);
const nullIndex = array.findIndex(s => s == null);
array.splice(nullIndex, 1);
}
}
}
Expand Down Expand Up @@ -349,7 +355,6 @@ export class DraftSourcesComponent extends DataLoadingComponent implements OnIni
}

async save(): Promise<void> {
// TODO verify at least one source and one reference is selected
const definedSources = this.draftingSources.filter(s => s != null);
const definedReferences = this.trainingSources.filter(s => s != null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,6 @@ export default {
]
})
],
// render: args => {
// // setUpMocks(args as DraftSourcesComponentStoryState);
// return {
// // moduleMetadata: {
// // providers: [
// // { provide: ActivatedProjectService, useValue: instance(mockActivatedProjectService) },
// // { provide: DestroyRef, useValue: instance(mockDestroyRef) },
// // { provide: ParatextService, useValue: instance(mockParatextService) },
// // { provide: DialogService, useValue: instance(mockDialogService) },
// // { provide: SFProjectService, useValue: instance(mockProjectService) },
// // { provide: SFUserProjectsService, useValue: instance(mockUserProjectsService) },
// // { provide: Router, useValue: instance(mockRouter) },
// // { provide: FeatureFlagService, useValue: instance(mockFeatureFlags) }
// // ]
// // },
// template: `<app-draft-sources></app-draft-sources>`
// };
// },
args: defaultArgs,
parameters: {
controls: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function englishNameFromCode(code: string): string {
const locale = new Intl.Locale(code);
return new Intl.DisplayNames(['en'], { type: 'language' }).of(locale.language) ?? '';
}

export function areLanguageCodesEquivalent(code1: string, code2: string): boolean {
return code1 === code2 || englishNameFromCode(code1) === englishNameFromCode(code2);
}

/**
* Given a list of language codes, counts the number of codes that are "unique" in the sense that they have different
* English names.
*
* This is a *very rough* way of determining whether languages are "equivalent" for the purposes of training a model.
*
* As an example, zh and cmn both map to "Chinese" in English, so they would be considered equivalent.
*
* TODO Create a more robust way of determining whether languages are equivalent, that isn't browser-dependent.
*/
export function countNonEquivalentLanguageCodes(languageCodes: string[]): number {
const uniqueTags = new Set(languageCodes);
return new Set(Array.from(uniqueTags).map(englishNameFromCode)).size;
}
2 changes: 1 addition & 1 deletion src/SIL.XForge.Scripture/Controllers/ParatextController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ int chapter
/// </summary>
/// <response code="200">
/// The resources were successfully retrieved. A dictionary is returned where the Paratext Id is the key, and the
/// values are an array with the short name followed by the name.
/// values are an array containing: [shortName, name, languageTag].
/// </response>
/// <response code="204">The user does not have permission to access Paratext.</response>
/// <response code="401">The user's Paratext tokens have expired, and the user must log in again.</response>
Expand Down

0 comments on commit 7bc01ef

Please sign in to comment.