Skip to content

Commit a77bd89

Browse files
Add free translation input to segment view
1 parent 3d68073 commit a77bd89

24 files changed

Lines changed: 994 additions & 23 deletions

contributions/localizedStrings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
"%interlinearizer_viewOption_showMorphology%": "Show morphology",
3131
"%interlinearizer_projectSettings_showMorphology%": "Show Morphology",
3232
"%interlinearizer_projectSettings_showMorphologyDescription%": "Display morpheme breakdown and per-morpheme glosses beneath each word token",
33+
"%interlinearizer_viewOption_showFreeTranslation%": "Show free translation",
34+
"%interlinearizer_projectSettings_showFreeTranslation%": "Show Free Translation",
35+
"%interlinearizer_projectSettings_showFreeTranslationDescription%": "Display a free-translation input beneath each segment's tokens or baseline text",
36+
"%interlinearizer_glossInput_placeholder%": "gloss",
37+
"%interlinearizer_freeTranslationInput_placeholder%": "Free translation",
3338
"%interlinearizer_morphemeEditor_splitLabel%": "Split into morphemes",
3439
"%interlinearizer_morphemeEditor_delete%": "Delete",
3540
"%interlinearizer_morphemeEditor_cancel%": "Cancel",

contributions/projectSettings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"label": "%interlinearizer_projectSettings_showMorphology%",
2727
"description": "%interlinearizer_projectSettings_showMorphologyDescription%",
2828
"default": false
29+
},
30+
"interlinearizer.showFreeTranslation": {
31+
"label": "%interlinearizer_projectSettings_showFreeTranslation%",
32+
"description": "%interlinearizer_projectSettings_showFreeTranslationDescription%",
33+
"default": false
2934
}
3035
}
3136
}

src/__tests__/components/AnalysisStore.test.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
usePhraseGloss,
2323
usePhraseGlossDispatch,
2424
useReportGlossEditing,
25+
useSegmentFreeTranslation,
26+
useSegmentFreeTranslationDispatch,
2527
} from '../../components/AnalysisStore';
2628

2729
// ---------------------------------------------------------------------------
@@ -819,6 +821,144 @@ describe('usePhraseGlossDispatch', () => {
819821
});
820822
});
821823

824+
// ---------------------------------------------------------------------------
825+
// useSegmentFreeTranslation
826+
// ---------------------------------------------------------------------------
827+
828+
/** A `TextAnalysis` with an approved segment analysis carrying a free translation in `'und'`. */
829+
const SEGMENT_ANALYSIS_WITH_TRANSLATION: TextAnalysis = {
830+
segmentAnalyses: [
831+
{ id: 'sa-1', surfaceText: 'In the beginning', freeTranslation: { und: 'au commencement' } },
832+
],
833+
segmentAnalysisLinks: [{ analysisId: 'sa-1', status: 'approved', segmentId: 'seg-1' }],
834+
tokenAnalyses: [],
835+
tokenAnalysisLinks: [],
836+
phraseAnalyses: [],
837+
phraseAnalysisLinks: [],
838+
};
839+
840+
/**
841+
* Renders the free translation for a given segmentId, used to assert on
842+
* `useSegmentFreeTranslation`.
843+
*
844+
* @param props - Component props.
845+
* @param props.segmentId - Segment id to look up.
846+
* @returns JSX element.
847+
*/
848+
function SegmentTranslationReader({ segmentId }: Readonly<{ segmentId: string }>) {
849+
const value = useSegmentFreeTranslation(segmentId);
850+
return <span data-testid="segment-translation">{value}</span>;
851+
}
852+
853+
/**
854+
* Renders a component that calls `useSegmentFreeTranslation` without a provider, to assert it
855+
* throws.
856+
*
857+
* @returns Nothing — only mounted to trigger the throw.
858+
*/
859+
function SegmentTranslationUser() {
860+
useSegmentFreeTranslation('seg-1');
861+
return undefined;
862+
}
863+
864+
describe('useSegmentFreeTranslation', () => {
865+
it('returns empty string when the segment has no approved analysis', () => {
866+
render(
867+
<AnalysisStoreProvider analysisLanguage="und">
868+
<SegmentTranslationReader segmentId="seg-1" />
869+
</AnalysisStoreProvider>,
870+
);
871+
expect(screen.getByTestId('segment-translation')).toHaveTextContent('');
872+
});
873+
874+
it('returns the free translation for the active analysis language', () => {
875+
render(
876+
<AnalysisStoreProvider
877+
initialAnalysis={SEGMENT_ANALYSIS_WITH_TRANSLATION}
878+
analysisLanguage="und"
879+
>
880+
<SegmentTranslationReader segmentId="seg-1" />
881+
</AnalysisStoreProvider>,
882+
);
883+
expect(screen.getByTestId('segment-translation')).toHaveTextContent('au commencement');
884+
});
885+
886+
it('throws when called outside an AnalysisStoreProvider', () => {
887+
jest.spyOn(console, 'error').mockImplementation(() => {});
888+
expect(() => render(<SegmentTranslationUser />)).toThrow(
889+
'useSegmentFreeTranslation must be used inside an AnalysisStoreProvider',
890+
);
891+
});
892+
});
893+
894+
// ---------------------------------------------------------------------------
895+
// useSegmentFreeTranslationDispatch
896+
// ---------------------------------------------------------------------------
897+
898+
/**
899+
* Renders a button that writes a segment free translation via `useSegmentFreeTranslationDispatch`.
900+
*
901+
* @param props - Component props.
902+
* @param props.segmentId - Segment id to write.
903+
* @param props.surfaceText - Segment baseline text to store.
904+
* @param props.value - Free-translation value to write.
905+
* @returns JSX element.
906+
*/
907+
function SegmentTranslationWriter({
908+
segmentId,
909+
surfaceText,
910+
value,
911+
}: Readonly<{ segmentId: string; surfaceText: string; value: string }>) {
912+
const dispatch = useSegmentFreeTranslationDispatch();
913+
return (
914+
<button onClick={() => dispatch(segmentId, surfaceText, value)} type="button">
915+
write
916+
</button>
917+
);
918+
}
919+
920+
/**
921+
* Renders a component that calls `useSegmentFreeTranslationDispatch` without a provider, to assert
922+
* it throws.
923+
*
924+
* @returns Nothing — only mounted to trigger the throw.
925+
*/
926+
function SegmentTranslationDispatchUser() {
927+
useSegmentFreeTranslationDispatch();
928+
return undefined;
929+
}
930+
931+
describe('useSegmentFreeTranslationDispatch', () => {
932+
it('writes the segment free translation and triggers onSave', async () => {
933+
const onSave = jest.fn();
934+
render(
935+
<AnalysisStoreProvider analysisLanguage="und" onSave={onSave}>
936+
<SegmentTranslationWriter
937+
segmentId="seg-1"
938+
surfaceText="In the beginning"
939+
value="au commencement"
940+
/>
941+
</AnalysisStoreProvider>,
942+
);
943+
944+
await userEvent.click(screen.getByRole('button', { name: 'write' }));
945+
946+
expect(onSave).toHaveBeenCalledTimes(1);
947+
const saved: TextAnalysis = onSave.mock.calls[0][0];
948+
expect(saved.segmentAnalyses[0]).toMatchObject({
949+
surfaceText: 'In the beginning',
950+
freeTranslation: { und: 'au commencement' },
951+
});
952+
});
953+
954+
it('throws when called outside an AnalysisStoreProvider', () => {
955+
jest.spyOn(console, 'error').mockImplementation(() => {});
956+
expect(() => render(<SegmentTranslationDispatchUser />)).toThrow(
957+
'useSegmentFreeTranslationDispatch must be used inside an AnalysisStoreProvider',
958+
);
959+
});
960+
});
961+
822962
// ---------------------------------------------------------------------------
823963
// Morpheme hooks
824964
// ---------------------------------------------------------------------------

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ function requiredProps(
436436
simplifyPhrases: false,
437437
chapterLabelInVerse: false,
438438
showMorphology: false,
439+
showFreeTranslation: false,
439440
},
440441
};
441442
}
@@ -906,6 +907,7 @@ describe('ContinuousView scroll behavior', () => {
906907
simplifyPhrases: false,
907908
chapterLabelInVerse: false,
908909
showMorphology: false,
910+
showFreeTranslation: false,
909911
}}
910912
/>
911913
);
@@ -956,6 +958,7 @@ describe('ContinuousView scroll behavior', () => {
956958
simplifyPhrases: false,
957959
chapterLabelInVerse: false,
958960
showMorphology: false,
961+
showFreeTranslation: false,
959962
}}
960963
/>
961964
);

src/__tests__/components/Interlinearizer.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ function renderInterlinearizer({
354354
simplifyPhrases = false,
355355
chapterLabelInVerse = false,
356356
showMorphology = false,
357+
showFreeTranslation = false,
357358
}: {
358359
book?: Book;
359360
continuousScroll?: boolean;
@@ -363,6 +364,7 @@ function renderInterlinearizer({
363364
simplifyPhrases?: boolean;
364365
chapterLabelInVerse?: boolean;
365366
showMorphology?: boolean;
367+
showFreeTranslation?: boolean;
366368
} = {}) {
367369
return render(
368370
withNav(
@@ -378,6 +380,7 @@ function renderInterlinearizer({
378380
simplifyPhrases,
379381
chapterLabelInVerse,
380382
showMorphology,
383+
showFreeTranslation,
381384
}}
382385
/>,
383386
navigate,
@@ -521,6 +524,7 @@ describe('Interlinearizer', () => {
521524
simplifyPhrases: false,
522525
chapterLabelInVerse: false,
523526
showMorphology: false,
527+
showFreeTranslation: false,
524528
}}
525529
/>,
526530
),
@@ -636,6 +640,7 @@ describe('Interlinearizer', () => {
636640
simplifyPhrases: false,
637641
chapterLabelInVerse: false,
638642
showMorphology: false,
643+
showFreeTranslation: false,
639644
}}
640645
/>,
641646
),
@@ -675,6 +680,7 @@ describe('Interlinearizer', () => {
675680
simplifyPhrases: false,
676681
chapterLabelInVerse: false,
677682
showMorphology: false,
683+
showFreeTranslation: false,
678684
}}
679685
/>,
680686
),
@@ -719,6 +725,7 @@ describe('Interlinearizer', () => {
719725
simplifyPhrases: false,
720726
chapterLabelInVerse: false,
721727
showMorphology: false,
728+
showFreeTranslation: false,
722729
}}
723730
/>,
724731
),
@@ -740,6 +747,7 @@ describe('Interlinearizer', () => {
740747
simplifyPhrases: false,
741748
chapterLabelInVerse: false,
742749
showMorphology: false,
750+
showFreeTranslation: false,
743751
}}
744752
/>,
745753
),
@@ -786,6 +794,7 @@ describe('Interlinearizer', () => {
786794
simplifyPhrases: false,
787795
chapterLabelInVerse: false,
788796
showMorphology: false,
797+
showFreeTranslation: false,
789798
}}
790799
/>,
791800
),
@@ -826,6 +835,7 @@ describe('Interlinearizer', () => {
826835
simplifyPhrases: false,
827836
chapterLabelInVerse: false,
828837
showMorphology: false,
838+
showFreeTranslation: false,
829839
}}
830840
/>,
831841
),
@@ -925,6 +935,7 @@ describe('Interlinearizer', () => {
925935
simplifyPhrases: false,
926936
chapterLabelInVerse: false,
927937
showMorphology: false,
938+
showFreeTranslation: false,
928939
}}
929940
/>,
930941
),
@@ -953,6 +964,7 @@ describe('Interlinearizer', () => {
953964
simplifyPhrases: false,
954965
chapterLabelInVerse: false,
955966
showMorphology: false,
967+
showFreeTranslation: false,
956968
}}
957969
/>,
958970
),
@@ -975,6 +987,7 @@ describe('Interlinearizer', () => {
975987
simplifyPhrases: false,
976988
chapterLabelInVerse: false,
977989
showMorphology: false,
990+
showFreeTranslation: false,
978991
}}
979992
/>,
980993
),
@@ -999,6 +1012,7 @@ describe('Interlinearizer', () => {
9991012
simplifyPhrases: false,
10001013
chapterLabelInVerse: false,
10011014
showMorphology: false,
1015+
showFreeTranslation: false,
10021016
}}
10031017
/>,
10041018
),
@@ -1023,6 +1037,7 @@ describe('Interlinearizer', () => {
10231037
simplifyPhrases: false,
10241038
chapterLabelInVerse: false,
10251039
showMorphology: false,
1040+
showFreeTranslation: false,
10261041
}}
10271042
/>,
10281043
),
@@ -1046,6 +1061,7 @@ describe('Interlinearizer', () => {
10461061
simplifyPhrases: false,
10471062
chapterLabelInVerse: false,
10481063
showMorphology: false,
1064+
showFreeTranslation: false,
10491065
},
10501066
};
10511067
const { container, rerender } = render(
@@ -1117,6 +1133,7 @@ describe('Interlinearizer', () => {
11171133
simplifyPhrases: false,
11181134
chapterLabelInVerse: false,
11191135
showMorphology: false,
1136+
showFreeTranslation: false,
11201137
}}
11211138
/>
11221139
);
@@ -1151,6 +1168,7 @@ describe('Interlinearizer', () => {
11511168
simplifyPhrases: false,
11521169
chapterLabelInVerse: false,
11531170
showMorphology: false,
1171+
showFreeTranslation: false,
11541172
},
11551173
};
11561174
const { container, rerender } = render(

0 commit comments

Comments
 (0)