Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/localization/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@
"%project_settings_project_group1_label%": "Project properties",
"%redoButton_tooltip%": "Redo",
"%resources_deprecated_any%": "Any",
"%scripture_book_chapter_separator%": " ",
"%scripture_chapter_verse_separator%": ":",
"%scripture_section_dc_long%": "Deuterocanon",
"%scripture_section_dc_short%": "DC",
"%scripture_section_extra_long%": "Extra material",
Expand Down
72 changes: 41 additions & 31 deletions assets/localization/fr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"%some_localization_key%": "Ceci est le texte en français pour %some_localization_key%.",
"%general_button_submit%": "Soumettre",
"%scripture_book_chapter_separator%": " ",
"%scripture_chapter_verse_separator%": ".",
"%scripture_section_nt_long%": "Nouveau Testament",
"%scripture_section_ot_long%": "Ancien Testament",
"%scripture_section_dc_long%": "Deutérocanon",
"%scripture_section_extra_long%": "Suppléments",
"%scripture_section_nt_short%": "NT",
"%scripture_section_ot_short%": "AT",
"%scripture_section_dc_short%": "DC",
"%scripture_section_extra_short%": "Suppl.",
"%Book.GEN%": "Genèse",
"%Book.EXO%": "Exode",
"%Book.LEV%": "Lévitique",
Expand All @@ -9,12 +19,12 @@
"%Book.JOS%": "Josué",
"%Book.JDG%": "Juges",
"%Book.RUT%": "Ruth",
"%Book.1SA%": "1 Samuel",
"%Book.2SA%": "2 Samuel",
"%Book.1KI%": "1 Rois",
"%Book.2KI%": "2 Rois",
"%Book.1CH%": "1 Chroniques",
"%Book.2CH%": "2 Chroniques",
"%Book.1SA%": "1 Samuel",
"%Book.2SA%": "2 Samuel",
"%Book.1KI%": "1 Rois",
"%Book.2KI%": "2 Rois",
"%Book.1CH%": "1 Chroniques",
"%Book.2CH%": "2 Chroniques",
"%Book.EZR%": "Esdras",
"%Book.NEH%": "Néhémie",
"%Book.EST%": "Esther (hébreu)",
Expand Down Expand Up @@ -46,25 +56,25 @@
"%Book.JHN%": "Jean",
"%Book.ACT%": "Actes",
"%Book.ROM%": "Romains",
"%Book.1CO%": "1 Corinthiens",
"%Book.2CO%": "2 Corinthiens",
"%Book.1CO%": "1 Corinthiens",
"%Book.2CO%": "2 Corinthiens",
"%Book.GAL%": "Galates",
"%Book.EPH%": "Éphésiens",
"%Book.PHP%": "Philippiens",
"%Book.COL%": "Colossiens",
"%Book.1TH%": "1 Thessaloniciens",
"%Book.2TH%": "2 Thessaloniciens",
"%Book.1TI%": "1 Timothée",
"%Book.2TI%": "2 Timothée",
"%Book.1TH%": "1 Thessaloniciens",
"%Book.2TH%": "2 Thessaloniciens",
"%Book.1TI%": "1 Timothée",
"%Book.2TI%": "2 Timothée",
"%Book.TIT%": "Tite",
"%Book.PHM%": "Philémon",
"%Book.HEB%": "Hébreux",
"%Book.JAS%": "Jacques",
"%Book.1PE%": "1 Pierre",
"%Book.2PE%": "2 Pierre",
"%Book.1JN%": "1 Jean",
"%Book.2JN%": "2 Jean",
"%Book.3JN%": "3 Jean",
"%Book.1PE%": "1 Pierre",
"%Book.2PE%": "2 Pierre",
"%Book.1JN%": "1 Jean",
"%Book.2JN%": "2 Jean",
"%Book.3JN%": "3 Jean",
"%Book.JUD%": "Jude",
"%Book.REV%": "Apocalypse",
"%Book.TOB%": "Tobit",
Expand All @@ -77,12 +87,12 @@
"%Book.S3Y%": "Cantique des trois jeunes",
"%Book.SUS%": "Suzanne",
"%Book.BEL%": "Bel et le Dragon",
"%Book.1MA%": "1 Maccabées",
"%Book.2MA%": "2 Maccabées",
"%Book.3MA%": "3 Maccabées",
"%Book.4MA%": "4 Maccabées",
"%Book.1ES%": "1 Esdras (grec)",
"%Book.2ES%": "2 Esdras (latin)",
"%Book.1MA%": "1 Maccabées",
"%Book.2MA%": "2 Maccabées",
"%Book.3MA%": "3 Maccabées",
"%Book.4MA%": "4 Maccabées",
"%Book.1ES%": "1 Esdras (grec)",
"%Book.2ES%": "2 Esdras (latin)",
"%Book.MAN%": "Prière de Manassé",
"%Book.PS2%": "Psaume 151",
"%Book.ODA%": "Odes",
Expand All @@ -103,25 +113,25 @@
"%Book.FRT%": "pages liminaires",
"%Book.BAK%": "parties annexes",
"%Book.OTH%": "Autre partie",
"%Book.3ES%": "3 Esdras *obsolète*",
"%Book.3ES%": "3 Esdras *obsolète*",
"%Book.EZA%": "Apocalypse d'Esdras",
"%Book.5EZ%": "5 Esdras (prologue en latin)",
"%Book.6EZ%": "6 Esdras (épilogue en latin)",
"%Book.5EZ%": "5 Esdras (prologue en latin)",
"%Book.6EZ%": "6 Esdras (épilogue en latin)",
"%Book.INT%": "Introduction",
"%Book.CNC%": "Concordance ",
"%Book.GLO%": "Glossaire ",
"%Book.TDX%": "Index thématique",
"%Book.NDX%": "Index de noms",
"%Book.DAG%": "Daniel (grec)",
"%Book.PS3%": "Psaumes 152-155",
"%Book.2BA%": "2 Baruch (Apocalypse)",
"%Book.2BA%": "2 Baruch (Apocalypse)",
"%Book.LBA%": "Lettre de Baruch",
"%Book.JUB%": "Jubilés",
"%Book.ENO%": "Hénoch",
"%Book.1MQ%": "1 Meqabyan",
"%Book.2MQ%": "2 Meqabyan",
"%Book.3MQ%": "3 Meqabyan",
"%Book.1MQ%": "1 Meqabyan",
"%Book.2MQ%": "2 Meqabyan",
"%Book.3MQ%": "3 Meqabyan",
"%Book.REP%": "Reproches (Proverbes 25-31)",
"%Book.4BA%": "4 Baruch (reste de Baruch)",
"%Book.4BA%": "4 Baruch (reste de Baruch)",
"%Book.LAO%": "Laodicéens"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Direction, readDirection } from '@/utils/dir-helper.util';
import { cn } from '@/utils/shadcn-ui.util';
import { Canon, SerializedVerseRef } from '@sillsdev/scripture';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { formatScrRef, getSectionForBook, Section } from 'platform-bible-utils';
import { formatScrRef, formatScrRefRange, getSectionForBook, Section } from 'platform-bible-utils';
import {
getSectionLongName,
getLocalizedBookName,
Expand Down Expand Up @@ -51,6 +51,7 @@ export function BookChapterControl({
recentSearches,
onAddRecentSearch,
id,
hideVerse = false,
}: BookChapterControlProps) {
const direction: Direction = readDirection();

Expand Down Expand Up @@ -214,7 +215,13 @@ export function BookChapterControl({
// #region Navigation and view changes

// Hook that provides navigation buttons for quick chapter/verse navigation
const quickNavButtons = useQuickNavButtons(scrRef, availableBooks, direction, handleSubmit);
const quickNavButtons = useQuickNavButtons(
scrRef,
availableBooks,
direction,
handleSubmit,
hideVerse,
);

const handleBackToBooks = useCallback(() => {
setViewMode('books');
Expand Down Expand Up @@ -260,6 +267,19 @@ export function BookChapterControl({
extraLong: localizedStrings?.['%scripture_section_extra_long%'],
};

// Locale-aware reference separators. useLocalizedStrings echoes the key
// back when a locale lacks an entry (e.g. '%scripture_..._separator%'),
// so we drop any value that still looks like a placeholder and let the
// util's own defaults (' ' / ':') kick in.
const resolveSeparator = (value: string | undefined): string | undefined =>
value && !(value.startsWith('%') && value.endsWith('%')) ? value : undefined;
const bookChapterSeparator = resolveSeparator(
localizedStrings?.['%scripture_book_chapter_separator%'],
);
const chapterVerseSeparator = resolveSeparator(
localizedStrings?.['%scripture_chapter_verse_separator%'],
);

const getSectionLabel = useCallback(
(section: Section): string => {
return getSectionLongName(section, otLong, ntLong, dcLong, extraLong);
Expand All @@ -275,14 +295,23 @@ export function BookChapterControl({
[topMatch],
);

const currentDisplayValue = useMemo(
() =>
formatScrRef(
scrRef,
localizedBookNames ? getLocalizedBookName(scrRef.book, localizedBookNames) : 'English',
),
[scrRef, localizedBookNames],
);
const currentDisplayValue = useMemo(() => {
const bookName = localizedBookNames
? getLocalizedBookName(scrRef.book, localizedBookNames)
: 'English';
if (hideVerse) {
// formatScrRefRange omits the verse part when verseNum is negative, and collapses
// start==end into a single ref. Using it with verseNum: -1 yields "<book> <chapter>"
// in a locale-safe way (no regex on the formatted string).
const refWithoutVerse: SerializedVerseRef = { ...scrRef, verseNum: -1 };
return formatScrRefRange(refWithoutVerse, refWithoutVerse, {
optionOrLocalizedBookName: bookName,
bookChapterSeparator,
chapterVerseSeparator,
});
}
return formatScrRef(scrRef, bookName, chapterVerseSeparator, bookChapterSeparator);
}, [scrRef, localizedBookNames, hideVerse, bookChapterSeparator, chapterVerseSeparator]);

const setChapterRef = useCallback((chapter: number) => {
return (element: HTMLDivElement | null) => {
Expand Down Expand Up @@ -539,7 +568,9 @@ export function BookChapterControl({
<RecentSearches
recentSearches={recentSearches}
onSearchItemSelect={handleRecentItemSelect}
renderItem={(verseRef) => formatScrRef(verseRef, 'English')}
renderItem={(verseRef) =>
formatScrRef(verseRef, 'English', chapterVerseSeparator, bookChapterSeparator)
}
getItemKey={(verseRef) =>
`${verseRef.book}-${verseRef.chapterNum}-${verseRef.verseNum}`
}
Expand Down Expand Up @@ -644,6 +675,8 @@ export function BookChapterControl({
localizedBookNames
? getLocalizedBookId(topMatch.book, localizedBookNames)
: undefined,
chapterVerseSeparator,
bookChapterSeparator,
)}
</CommandItem>
</CommandGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ describe('book-chapter-control.navigation', () => {
expect(result.current[3].title).toBe('Next chapter');
});

test('Omits verse navigation buttons when hideVerse is true', () => {
const { result } = renderHook(() =>
useQuickNavButtons(
{ book: 'GEN', chapterNum: 1, verseNum: 1 },
availableBooks,
'ltr',
mockHandleSubmit,
true,
),
);

expect(result.current).toHaveLength(2);
expect(result.current[0].title).toBe('Previous chapter');
expect(result.current[1].title).toBe('Next chapter');
});

describe('Previous chapter navigation', () => {
test('Navigates to previous chapter within same book', () => {
const { result } = renderHook(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function useQuickNavButtons(
availableBooks: string[],
direction: Direction,
handleSubmit: (scrRef: SerializedVerseRef) => void,
hideVerse: boolean = false,
): QuickNavButton[] {
const handlePreviousChapter = useCallback(() => {
if (scrRef.chapterNum > 1) {
Expand Down Expand Up @@ -78,42 +79,43 @@ export function useQuickNavButtons(
}, [scrRef, handleSubmit]);

return useMemo(() => {
return [
{
onClick: handlePreviousChapter,
disabled:
availableBooks.length === 0 ||
(scrRef.chapterNum === 1 && availableBooks.indexOf(scrRef.book) === 0),
title: 'Previous chapter',
icon: direction === 'ltr' ? ChevronsLeft : ChevronsRight,
},
{
onClick: handlePreviousVerse,
disabled: availableBooks.length === 0 || scrRef.verseNum === 0,
title: 'Previous verse',
icon: direction === 'ltr' ? ChevronLeft : ChevronRight,
},
{
onClick: handleNextVerse,
disabled: availableBooks.length === 0,
title: 'Next verse',
icon: direction === 'ltr' ? ChevronRight : ChevronLeft,
},
{
onClick: handleNextChapter,
disabled:
availableBooks.length === 0 ||
((scrRef.chapterNum === fetchEndChapter(scrRef.book) ||
fetchEndChapter(scrRef.book) <= 0) &&
availableBooks.indexOf(scrRef.book) === availableBooks.length - 1),
title: 'Next chapter',
icon: direction === 'ltr' ? ChevronsRight : ChevronsLeft,
},
];
const previousChapter: QuickNavButton = {
onClick: handlePreviousChapter,
disabled:
availableBooks.length === 0 ||
(scrRef.chapterNum === 1 && availableBooks.indexOf(scrRef.book) === 0),
title: 'Previous chapter',
icon: direction === 'ltr' ? ChevronsLeft : ChevronsRight,
};
const previousVerse: QuickNavButton = {
onClick: handlePreviousVerse,
disabled: availableBooks.length === 0 || scrRef.verseNum === 0,
title: 'Previous verse',
icon: direction === 'ltr' ? ChevronLeft : ChevronRight,
};
const nextVerse: QuickNavButton = {
onClick: handleNextVerse,
disabled: availableBooks.length === 0,
title: 'Next verse',
icon: direction === 'ltr' ? ChevronRight : ChevronLeft,
};
const nextChapter: QuickNavButton = {
onClick: handleNextChapter,
disabled:
availableBooks.length === 0 ||
((scrRef.chapterNum === fetchEndChapter(scrRef.book) ||
fetchEndChapter(scrRef.book) <= 0) &&
availableBooks.indexOf(scrRef.book) === availableBooks.length - 1),
title: 'Next chapter',
icon: direction === 'ltr' ? ChevronsRight : ChevronsLeft,
};
if (hideVerse) return [previousChapter, nextChapter];
return [previousChapter, previousVerse, nextVerse, nextChapter];
}, [
scrRef,
availableBooks,
direction,
hideVerse,
handlePreviousChapter,
handlePreviousVerse,
handleNextVerse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const BOOK_CHAPTER_CONTROL_STRING_KEYS = Object.freeze([
'%scripture_section_nt_long%',
'%scripture_section_dc_long%',
'%scripture_section_extra_long%',
// Locale-specific scripture-reference separators. Defaults (used when the
// active locale does not define a value) are " " between book and chapter
// and ":" between chapter and verse. Locales such as French set the
// chapter/verse separator to ".".
'%scripture_book_chapter_separator%',
'%scripture_chapter_verse_separator%',
'%history_recent%',
'%history_recentSearches_ariaLabel%',
] as const);
Expand Down Expand Up @@ -48,4 +54,15 @@ export type BookChapterControlProps = {
onAddRecentSearch?: (scrRef: SerializedVerseRef) => void;
/** Optional ID for the popover content for accessibility */
id?: string;
/**
* If true, the trigger button's label will display the book and chapter only (e.g. "1 John 3"),
* omitting the verse part (":1"). The popover-driven selection still operates on
* `SerializedVerseRef` values internally; only the displayed label is affected.
*
* Useful for chapter-level workflows (e.g. exports or scopes that operate on whole chapters)
* where surfacing a verse number is misleading.
*
* @default false
*/
hideVerse?: boolean;
};
Loading