diff --git a/assets/localization/en.json b/assets/localization/en.json index 4c899abedfb..ca48c191ab1 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -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", diff --git a/assets/localization/fr.json b/assets/localization/fr.json index fd9fe157d1f..209fbfceca8 100644 --- a/assets/localization/fr.json +++ b/assets/localization/fr.json @@ -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", @@ -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)", @@ -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", @@ -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", @@ -103,10 +113,10 @@ "%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 ", @@ -114,14 +124,14 @@ "%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" } diff --git a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.component.tsx b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.component.tsx index cf71052b511..2ce7e99b109 100644 --- a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.component.tsx +++ b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.component.tsx @@ -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, @@ -51,6 +51,7 @@ export function BookChapterControl({ recentSearches, onAddRecentSearch, id, + hideVerse = false, }: BookChapterControlProps) { const direction: Direction = readDirection(); @@ -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'); @@ -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); @@ -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 " " + // 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) => { @@ -539,7 +568,9 @@ export function BookChapterControl({ formatScrRef(verseRef, 'English')} + renderItem={(verseRef) => + formatScrRef(verseRef, 'English', chapterVerseSeparator, bookChapterSeparator) + } getItemKey={(verseRef) => `${verseRef.book}-${verseRef.chapterNum}-${verseRef.verseNum}` } @@ -644,6 +675,8 @@ export function BookChapterControl({ localizedBookNames ? getLocalizedBookId(topMatch.book, localizedBookNames) : undefined, + chapterVerseSeparator, + bookChapterSeparator, )} diff --git a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.test.ts b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.test.ts index d5ea24e04ae..749b628bf62 100644 --- a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.test.ts +++ b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.test.ts @@ -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(() => diff --git a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.ts b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.ts index ff908128591..0f38030c596 100644 --- a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.ts +++ b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.navigation.ts @@ -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) { @@ -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, diff --git a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.types.ts b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.types.ts index caa69ec7eb0..e59a3a73984 100644 --- a/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.types.ts +++ b/lib/platform-bible-react/src/components/advanced/book-chapter-control/book-chapter-control.types.ts @@ -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); @@ -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; }; diff --git a/lib/platform-bible-react/src/stories/advanced/book-chapter-control.stories.tsx b/lib/platform-bible-react/src/stories/advanced/book-chapter-control.stories.tsx index 9ed86252f21..6dc0c730b79 100644 --- a/lib/platform-bible-react/src/stories/advanced/book-chapter-control.stories.tsx +++ b/lib/platform-bible-react/src/stories/advanced/book-chapter-control.stories.tsx @@ -128,6 +128,29 @@ export const WithCustomTrigger: Story = { }, }; +export const HideVerse: Story = { + args: { + scrRef: { + book: 'PSA', + chapterNum: 23, + verseNum: 1, + }, + hideVerse: true, + }, + parameters: { + docs: { + description: { + story: + 'When `hideVerse` is true, the trigger button label shows the book and chapter only ' + + '(e.g. "Psalms 23"), omitting ":1". Useful for chapter-level workflows such as ' + + 'whole-chapter exports where surfacing a verse number is misleading. The popover and ' + + 'submitted reference still operate on `SerializedVerseRef` values internally; only the ' + + 'displayed label is affected.', + }, + }, + }, +}; + export const EmptyBookList: Story = { args: { scrRef: defaultScrRef,