From 9da5874977ee33a36d22a71a10260331138f6f83 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Wed, 25 Feb 2026 18:13:53 -0500 Subject: [PATCH] feat: Add continuous Bible reading mode, backend-driven translation API integration, localized book names, and preference caching --- API.js | 12 +- App.js | 5 + Views/Bible.js | 88 +++++++++++ Views/BibleChapterView.js | 125 ++++++++++++--- Views/Menu.js | 1 + components/BibleEmbeddedView.js | 12 +- i18nMessages.js | 8 + utils/bibleReferences.js | 265 ++++++++++++++++++++++++++++---- 8 files changed, 460 insertions(+), 56 deletions(-) create mode 100644 Views/Bible.js diff --git a/API.js b/API.js index c8bc13e..262bf4d 100644 --- a/API.js +++ b/API.js @@ -1,6 +1,6 @@ import i18n from "./i18nMessages.js"; -const baseUrl = "https://emiapi.reynafamily.com"; -//const baseUrl = "http://localhost:3000"; +//const baseUrl = "https://emiapi.reynafamily.com"; +const baseUrl = "http://localhost:3000"; const requestErrorCooldownMs = 30000; const profileFailureCooldownMs = 60000; const recentRequestErrors = {}; @@ -367,6 +367,14 @@ const API = { return getCall("/user/" + profileid + "/unfollow"); }, setDataValue(key, value){ + if (CurrentProfile && userNameCache[CurrentProfile]) { + if (!userNameCache[CurrentProfile].data) userNameCache[CurrentProfile].data = {}; + userNameCache[CurrentProfile].data[key] = value; + } + if (CurrentProfileData) { + if (!CurrentProfileData.data) CurrentProfileData.data = {}; + CurrentProfileData.data[key] = value; + } return postCall("/user/setData", {key, value}); }, //Groups diff --git a/App.js b/App.js index f447214..206ad83 100644 --- a/App.js +++ b/App.js @@ -32,6 +32,7 @@ import SongPlayer from './Views/SongPlayer.js'; import GlobalChat from './Views/GlobalChat.js'; import BiblePicker from './Views/BiblePicker.js'; import BibleChapterView from './Views/BibleChapterView.js'; +import Bible from './Views/Bible.js'; import { Platform } from 'react-native'; import { PostHogProvider } from 'posthog-react-native' import * as Updates from 'expo-updates'; @@ -437,6 +438,10 @@ export default function App() { component={BibleChapterView} options={{ headerShown: false }} /> + diff --git a/Views/Bible.js b/Views/Bible.js new file mode 100644 index 0000000..ba5c69c --- /dev/null +++ b/Views/Bible.js @@ -0,0 +1,88 @@ +import React from "react"; +import { FlatList, View } from "react-native"; +import { Button, Chip, Text } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import { BIBLE_BOOKS, getBookChapterCount } from "../utils/bibleReferences.js"; +import i18n from "../i18nMessages.js"; + +const Bible = () => { + const navigation = useNavigation(); + const [selectedBook, setSelectedBook] = React.useState(""); + const [activeStep, setActiveStep] = React.useState("book"); + + const selectedBookChapterCount = React.useMemo(() => getBookChapterCount(selectedBook), [selectedBook]); + const chapterOptions = React.useMemo( + () => Array.from({ length: selectedBookChapterCount }, (_v, i) => String(i + 1)), + [selectedBookChapterCount] + ); + + return ( + + {i18n.t("message.bible") || "Read the Bible"} + + {i18n.t("message.biblePickerSubtitlePost") || "Select a book and chapter to start reading."} + + + + + + + + {activeStep === "book" ? ( + <> + {i18n.t("message.books") || "Books"} + item} + renderItem={({ item }) => ( + { + setSelectedBook(item); + setActiveStep("chapter"); + }} + > + {item} + + )} + /> + + ) : null} + + {activeStep === "chapter" ? ( + <> + + {i18n.t("message.chapters") || "Chapters"} {selectedBook ? `(${selectedBook})` : ""} + + item} + renderItem={({ item }) => ( + { + navigation.navigate("BibleChapter", { reference: `${selectedBook} ${item}`, selectable: false }); + }} + > + {item} + + )} + /> + + ) : null} + + ); +}; + +export default Bible; diff --git a/Views/BibleChapterView.js b/Views/BibleChapterView.js index a3ffd19..aefcdfc 100644 --- a/Views/BibleChapterView.js +++ b/Views/BibleChapterView.js @@ -1,35 +1,61 @@ import React from "react"; import { FlatList, Pressable, View } from "react-native"; -import { ActivityIndicator, Text } from "react-native-paper"; +import { ActivityIndicator, IconButton, Menu, Text, Button } from "react-native-paper"; import { SafeAreaView } from "react-native-safe-area-context"; import { useNavigation } from "@react-navigation/native"; -import { fetchBibleChapter, parseBibleReference } from "../utils/bibleReferences.js"; +import { fetchBibleChapter, parseBibleReference, AVAILABLE_TRANSLATIONS, BIBLE_BOOKS, getBookChapterCount } from "../utils/bibleReferences.js"; import GlobalState from "../contexts/GlobalState.js"; +import { useSnapshot } from "valtio"; +import API from "../API.js"; import i18n from "../i18nMessages.js"; const BibleChapterView = ({ route }) => { const navigation = useNavigation(); + const gState = useSnapshot(GlobalState); const reference = route?.params?.reference || ""; const selectable = route?.params?.selectable === true; - const { chapterReference, verse: selectedVerse } = parseBibleReference(reference); + const { chapterReference, verse: selectedVerse, book, chapter } = parseBibleReference(reference); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(""); const [chapterData, setChapterData] = React.useState(null); + const initialTranslation = gState.me?.data?.bibleTranslation || ""; + const [selectedTranslation, setSelectedTranslation] = React.useState(initialTranslation); + const [translationMenuVisible, setTranslationMenuVisible] = React.useState(false); const listRef = React.useRef(null); const autoScrolledRef = React.useRef(false); + const bookIndex = BIBLE_BOOKS.indexOf(book); + const chapterCount = getBookChapterCount(book); + + let prevReference = null; + if (chapter > 1) { + prevReference = `${book} ${chapter - 1}`; + } else if (bookIndex > 0) { + const prevBook = BIBLE_BOOKS[bookIndex - 1]; + const prevBookChapters = getBookChapterCount(prevBook); + prevReference = `${prevBook} ${prevBookChapters}`; + } + + let nextReference = null; + if (chapter < chapterCount) { + nextReference = `${book} ${chapter + 1}`; + } else if (bookIndex >= 0 && bookIndex < BIBLE_BOOKS.length - 1) { + const nextBook = BIBLE_BOOKS[bookIndex + 1]; + nextReference = `${nextBook} 1`; + } + React.useEffect(() => { let mounted = true; const loadChapter = async () => { setLoading(true); setError(""); try { - const payload = await fetchBibleChapter(chapterReference, i18n.locale); + const payload = await fetchBibleChapter(chapterReference, i18n.locale, selectedTranslation); if (!mounted) return; setChapterData(payload); } catch (_error) { if (!mounted) return; - setError(i18n.t("message.unableLoadChapter")); + setError(i18n.t("message.unableLoadChapter") || "Unable to load chapter"); setChapterData(null); } finally { if (mounted) setLoading(false); @@ -39,7 +65,7 @@ const BibleChapterView = ({ route }) => { return () => { mounted = false; }; - }, [chapterReference]); + }, [chapterReference, selectedTranslation]); const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : []; const selectedVerseNumber = Number(selectedVerse || 1); @@ -47,7 +73,7 @@ const BibleChapterView = ({ route }) => { React.useEffect(() => { autoScrolledRef.current = false; - }, [chapterReference, selectedVerseNumber]); + }, [chapterReference, selectedTranslation, selectedVerseNumber]); const scrollToSelectedVerse = React.useCallback((animated = false) => { if (autoScrolledRef.current) return; @@ -92,12 +118,48 @@ const BibleChapterView = ({ route }) => { return ( - - {chapterData?.reference || chapterReference} - - - {chapterData?.translation_name || chapterData?.translation_id || "KJV"} - + + + + {chapterData?.reference || chapterReference} + + setTranslationMenuVisible(false)} + anchor={ + setTranslationMenuVisible(true)}> + + {chapterData?.translation_name || chapterData?.translation_id || "KJV"} + + + } + > + {AVAILABLE_TRANSLATIONS.map((trans) => ( + { + setSelectedTranslation(trans.id); + setTranslationMenuVisible(false); + if (GlobalState.me) { + if (!GlobalState.me.data) GlobalState.me.data = {}; + GlobalState.me.data.bibleTranslation = trans.id; + } + API.setDataValue("bibleTranslation", trans.id); + }} + title={trans.name} + /> + ))} + + + {!selectable ? ( + navigation.navigate("Bible")} + style={{ margin: 0, marginTop: -4 }} + /> + ) : null} + {selectable ? ( {i18n.t("message.tapVerseToSelect")} ) : null} @@ -122,6 +184,32 @@ const BibleChapterView = ({ route }) => { scrollToSelectedVerse(false); }, 120); }} + ListFooterComponent={() => ( + + + + + )} renderItem={({ item }) => { const verseNumber = Number(item?.verse || 0); const isSelected = verseNumber === selectedVerseNumber; @@ -129,19 +217,20 @@ const BibleChapterView = ({ route }) => { handleVersePress(verseNumber)} style={{ - paddingVertical: 8, - paddingHorizontal: 10, + paddingVertical: 4, + paddingHorizontal: 8, borderRadius: 8, - marginBottom: 6, + marginBottom: 2, backgroundColor: isSelected ? "#fff3cd" : "transparent", borderWidth: isSelected ? 1 : 0, borderColor: isSelected ? "#f59e0b" : "transparent", + flexDirection: "row", }} > - + {verseNumber} - {item?.text || ""} + {(item?.text || "").replace(/\n/g, " ").trim()} ); }} diff --git a/Views/Menu.js b/Views/Menu.js index 15edbd5..88ad30e 100644 --- a/Views/Menu.js +++ b/Views/Menu.js @@ -91,6 +91,7 @@ let MenuView = ({ navigation }) => { { navigation.navigate("Logout") }} left={props => } /> + { navigation.navigate("Bible") }} left={props => } /> { navigation.navigate("Invite") }} left={props => } /> } /> diff --git a/components/BibleEmbeddedView.js b/components/BibleEmbeddedView.js index 9849a96..37b3b56 100644 --- a/components/BibleEmbeddedView.js +++ b/components/BibleEmbeddedView.js @@ -2,11 +2,14 @@ import React, { useMemo, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { ActivityIndicator, Chip } from "react-native-paper"; import { useNavigation } from "@react-navigation/native"; -import { extractBibleReferences, fetchBiblePassage } from "../utils/bibleReferences.js"; +import { extractBibleReferences, fetchBiblePassage, translateBibleReference } from "../utils/bibleReferences.js"; +import GlobalState from "../contexts/GlobalState.js"; +import { useSnapshot } from "valtio"; import i18n from "../i18nMessages.js"; const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = false }) => { const navigation = useNavigation(); + const gState = useSnapshot(GlobalState); const references = useMemo(() => extractBibleReferences(content), [content]); const [selectedRef, setSelectedRef] = useState(""); const [byReference, setByReference] = useState({}); @@ -24,7 +27,8 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = setByReference((prev) => ({ ...prev, [reference]: { loading: true } })); try { - const data = await fetchBiblePassage(reference, i18n.locale); + const preferredTranslation = GlobalState.me?.data?.bibleTranslation || ""; + const data = await fetchBiblePassage(reference, i18n.locale, preferredTranslation); setByReference((prev) => ({ ...prev, [reference]: { loading: false, ...data } })); } catch (_error) { setByReference((prev) => ({ ...prev, [reference]: { loading: false, error: true } })); @@ -46,7 +50,7 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = style={styles.chip} onPress={() => handleSelectReference(reference)} > - {reference} + {translateBibleReference(reference, i18n.locale)} ))} @@ -55,7 +59,7 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = {selectedData.text.slice(0, compact ? 160 : 280)} - {selectedData.reference} ({selectedData.translation}) + {translateBibleReference(selectedData.reference, i18n.locale)} ({selectedData.translation}) ) : null} diff --git a/i18nMessages.js b/i18nMessages.js index ce12a72..0cd9126 100644 --- a/i18nMessages.js +++ b/i18nMessages.js @@ -146,6 +146,8 @@ const messages = { chapters: "Chapters", verses: "Verses", updateProfile: "Update Profile", + previous: "Previous", + next: "Next", }, }, es: { @@ -287,6 +289,8 @@ const messages = { chapters: "Capítulos", verses: "Versículos", updateProfile: "Actualizar perfil", + previous: "Anterior", + next: "Siguiente", } }, fr: { @@ -428,6 +432,8 @@ const messages = { chapters: "Chapitres", verses: "Versets", updateProfile: "Mettre à jour le profil", + previous: "Précédent", + next: "Suivant", } }, da: { @@ -569,6 +575,8 @@ const messages = { chapters: "Kapitler", verses: "Vers", updateProfile: "Opdater profil", + previous: "Forrige", + next: "Næste", } } } diff --git a/utils/bibleReferences.js b/utils/bibleReferences.js index 19993f3..8984a23 100644 --- a/utils/bibleReferences.js +++ b/utils/bibleReferences.js @@ -1,5 +1,24 @@ +import API from '../API.js'; + const BIBLE_TOKEN_REGEX = /@bible:([^\s]+)/gi; -const DEFAULT_TRANSLATION = "web"; +const DEFAULT_TRANSLATION = "de4e12af7f28f599-01"; // KJV + +export const BIBLE_BOOK_IDS = { + "Genesis": "GEN", "Exodus": "EXO", "Leviticus": "LEV", "Numbers": "NUM", "Deuteronomy": "DEU", + "Joshua": "JOS", "Judges": "JDG", "Ruth": "RUT", "1 Samuel": "1SA", "2 Samuel": "2SA", + "1 Kings": "1KI", "2 Kings": "2KI", "1 Chronicles": "1CH", "2 Chronicles": "2CH", "Ezra": "EZR", + "Nehemiah": "NEH", "Esther": "EST", "Job": "JOB", "Psalms": "PSA", "Proverbs": "PRO", + "Ecclesiastes": "ECC", "Song of Solomon": "SNG", "Isaiah": "ISA", "Jeremiah": "JER", + "Lamentations": "LAM", "Ezekiel": "EZK", "Daniel": "DAN", "Hosea": "HOS", "Joel": "JOL", + "Amos": "AMO", "Obadiah": "OBA", "Jonah": "JON", "Micah": "MIC", "Nahum": "NAM", + "Habakkuk": "HAB", "Zephaniah": "ZEP", "Haggai": "HAG", "Zechariah": "ZEC", "Malachi": "MAL", + "Matthew": "MAT", "Mark": "MRK", "Luke": "LUK", "John": "JHN", "Acts": "ACT", + "Romans": "ROM", "1 Corinthians": "1CO", "2 Corinthians": "2CO", "Galatians": "GAL", + "Ephesians": "EPH", "Philippians": "PHP", "Colossians": "COL", "1 Thessalonians": "1TH", + "2 Thessalonians": "2TH", "1 Timothy": "1TI", "2 Timothy": "2TI", "Titus": "TIT", + "Philemon": "PHM", "Hebrews": "HEB", "James": "JAS", "1 Peter": "1PE", "2 Peter": "2PE", + "1 John": "1JN", "2 John": "2JN", "3 John": "3JN", "Jude": "JUD", "Revelation": "REV" +}; const getNormalizedLocale = (locale = "") => { return String(locale || "").toLowerCase().replace("_", "-").trim(); @@ -7,13 +26,10 @@ const getNormalizedLocale = (locale = "") => { const getTranslationForLocale = (locale = "") => { const normalized = getNormalizedLocale(locale); - if (!normalized) return "kjv"; - if (normalized.startsWith("en-gb")) return "webbe"; - if (normalized.startsWith("en")) return "kjv"; - if (normalized.startsWith("zh")) return "cuv"; - if (normalized.startsWith("cs")) return "bkr"; - if (normalized.startsWith("pt")) return "almeida"; - if (normalized.startsWith("ro")) return "rccv"; + if (!normalized) return "de4e12af7f28f599-01"; // KJV + if (normalized.startsWith("en")) return "de4e12af7f28f599-01"; // KJV + if (normalized.startsWith("es")) return "592420522e16049f-01"; // RVR1909 + // Default to KJV return DEFAULT_TRANSLATION; }; @@ -62,39 +78,144 @@ export const stripBibleTokens = (content = "") => { .trim(); }; -export const fetchBibleReference = async (reference = "", locale = "") => { - const safeReference = normalizeReference(reference); - const preferredTranslation = getTranslationForLocale(locale); - const preferredUrl = `https://bible-api.com/${encodeURIComponent(safeReference)}?translation=${preferredTranslation}`; - const fallbackUrl = `https://bible-api.com/${encodeURIComponent(safeReference)}?translation=${DEFAULT_TRANSLATION}`; +export const AVAILABLE_TRANSLATIONS = [ + { id: "de4e12af7f28f599-01", name: "King James Version (English)" }, + { id: "9879dbb7cfe39e4d-01", name: "World English Bible (English)" }, + { id: "592420522e16049f-01", name: "Reina Valera 1909 (Spanish)" }, + { id: "48acedcf8595c754-01", name: "Palabla de Dios para ti (Spanish)" } +]; - let response = await fetch(preferredUrl); - if (!response.ok && preferredTranslation !== DEFAULT_TRANSLATION) { - response = await fetch(fallbackUrl); +const parseChapterHtml = (htmlStr) => { + const parts = String(htmlStr || "").split(/]*data-number=\"/); + const versesMap = {}; + for (let i = 1; i < parts.length; i++) { + const part = parts[i]; + const numMatch = part.match(/^(\d+)\"/); + if (!numMatch) continue; + const vNum = numMatch[1]; + + let text = part.replace(/^[^>]+>/, ''); + text = text.replace(/<[^>]+>/g, ''); + text = text.replace(new RegExp('^' + vNum + '\\s*'), '').trim(); + text = text.replace(/ /g, ' ').replace(/ /g, ' '); + + if (!versesMap[vNum]) versesMap[vNum] = ''; + versesMap[vNum] += text + ' '; } - if (!response.ok) throw new Error("Failed to load Bible passage"); - - const payload = await response.json(); - if (payload?.error) throw new Error(payload.error); - return payload; + return versesMap; }; -export const fetchBiblePassage = async (reference = "", locale = "") => { - const safeReference = normalizeReference(reference); - if (!safeReference) { - throw new Error("Missing Bible reference"); +const parseChapterJson = (items) => { + const versesMap = {}; + let currentVerse = null; + + const extract = (nodes) => { + if (!nodes) return; + for (const item of nodes) { + if (item.name === 'verse') { + currentVerse = item.attrs?.number || currentVerse; + if (currentVerse && !versesMap[currentVerse]) versesMap[currentVerse] = ""; + } else if (item.type === 'text') { + if (item.attrs?.verseId) { + const vNum = item.attrs.verseId.split('.').pop(); + if (!versesMap[vNum]) versesMap[vNum] = ""; + versesMap[vNum] += item.text; + } else if (currentVerse) { + // Skip if the text is exactly the verse number to prevent duplicates + if (item.text.trim() === String(currentVerse)) { + continue; + } + versesMap[currentVerse] += item.text; + } + } + if (item.items) { + extract(item.items); + } + } + }; + extract(items); + return versesMap; +}; + +export const fetchBibleChapter = async (chapterReference = "", locale = "", customTranslation = "") => { + const { book, chapter } = parseBibleReference(chapterReference); + const bookId = BIBLE_BOOK_IDS[book]; + if (!bookId) throw new Error("Missing or unknown Bible reference book"); + + const chapterId = `${bookId}.${chapter}`; + const preferredTranslation = customTranslation || getTranslationForLocale(locale); + + // Call our backend API which has the correct key and handles auth + let response; + try { + const url = `/bible/chapters/${chapterId}`; + const queryParams = { bibleId: preferredTranslation, "content-type": "json" }; + let queryParamsString = "?"; + Object.keys(queryParams).forEach(p => { + queryParamsString += p + "=" + queryParams[p] + "&"; + }); + + const localBaseUrl = global.baseUrl ?? "http://localhost:3000"; + // To bypass getCall's strict JSON structure mapping (since our backend just passes the API response directly), + // we can use standard fetch with the same cookie inclusion strategy. + const fetchRes = await fetch(localBaseUrl + url + queryParamsString, { + method: 'GET', + mode: 'cors', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept-Language': locale, + 'x-app-language': locale, + } + }); + if (!fetchRes.ok) throw new Error("Failed to load Bible passage"); + response = await fetchRes.json(); + } catch (error) { + throw new Error(error.message || "Failed to load Bible passage"); } - const payload = await fetchBibleReference(safeReference, locale); + + if (response?.error) throw new Error(response.error); + const payload = response.data; + if (!payload || !payload.content) throw new Error("Invalid Bible data"); + + let versesMap = {}; + if (typeof payload.content === 'string') { + versesMap = parseChapterHtml(payload.content); + } else if (Array.isArray(payload.content)) { + versesMap = parseChapterJson(payload.content); + } + + const verses = Object.keys(versesMap) + .sort((a, b) => Number(a) - Number(b)) + .map(vNum => ({ + verse: Number(vNum), + text: versesMap[vNum].replace(/\n/g, " ").trim() + })); + return { - reference: payload?.reference || safeReference, - text: (payload?.text || "").trim(), - translation: payload?.translation_name || payload?.translation_id || "KJV", + reference: payload.reference || chapterReference, + verses: verses, + translation_id: preferredTranslation, + translation_name: AVAILABLE_TRANSLATIONS.find(t => t.id === preferredTranslation)?.name || "Translation", }; }; -export const fetchBibleChapter = async (chapterReference = "", locale = "") => { - const safeReference = normalizeReference(chapterReference); - return fetchBibleReference(safeReference, locale); +export const fetchBiblePassage = async (reference = "", locale = "", customTranslation = "") => { + const { verse } = parseBibleReference(reference); + const chapterData = await fetchBibleChapter(reference, locale, customTranslation); + + const verseData = chapterData.verses.find(v => v.verse === Number(verse)); + if (!verseData) throw new Error("Verse not found"); + + return { + reference: `${chapterData.reference}:${verse}`, + text: verseData.text, + translation: chapterData.translation_name, + }; +}; + +export const fetchBibleReference = async (reference = "", locale = "", customTranslation = "") => { + return fetchBibleChapter(reference, locale, customTranslation); }; export const parseBibleReference = (reference = "") => { @@ -262,3 +383,83 @@ export const BIBLE_BOOK_CHAPTERS = { export const getBookChapterCount = (book = "") => { return BIBLE_BOOK_CHAPTERS[book] || 1; }; + +const BIBLE_BOOK_TRANSLATIONS = { + "Genesis": { "es": "Génesis", "fr": "Genèse", "da": "1 Mosebog" }, + "Exodus": { "es": "Éxodo", "fr": "Exode", "da": "2 Mosebog" }, + "Leviticus": { "es": "Levítico", "fr": "Lévitique", "da": "3 Mosebog" }, + "Numbers": { "es": "Números", "fr": "Nombres", "da": "4 Mosebog" }, + "Deuteronomy": { "es": "Deuteronomio", "fr": "Deutéronome", "da": "5 Mosebog" }, + "Joshua": { "es": "Josué", "fr": "Josué", "da": "Josva" }, + "Judges": { "es": "Jueces", "fr": "Juges", "da": "Dommerne" }, + "Ruth": { "es": "Rut", "fr": "Ruth", "da": "Ruth" }, + "1 Samuel": { "es": "1 Samuel", "fr": "1 Samuel", "da": "1 Samuel" }, + "2 Samuel": { "es": "2 Samuel", "fr": "2 Samuel", "da": "2 Samuel" }, + "1 Kings": { "es": "1 Reyes", "fr": "1 Rois", "da": "1 Kongebog" }, + "2 Kings": { "es": "2 Reyes", "fr": "2 Rois", "da": "2 Kongebog" }, + "1 Chronicles": { "es": "1 Crónicas", "fr": "1 Chroniques", "da": "1 Krønikebog" }, + "2 Chronicles": { "es": "2 Crónicas", "fr": "2 Chroniques", "da": "2 Krønikebog" }, + "Ezra": { "es": "Esdras", "fr": "Esdras", "da": "Ezra" }, + "Nehemiah": { "es": "Nehemías", "fr": "Néhémie", "da": "Nehemias" }, + "Esther": { "es": "Ester", "fr": "Esther", "da": "Ester" }, + "Job": { "es": "Job", "fr": "Job", "da": "Job" }, + "Psalms": { "es": "Salmos", "fr": "Psaumes", "da": "Salmerne" }, + "Proverbs": { "es": "Proverbios", "fr": "Proverbes", "da": "Ordsprogene" }, + "Ecclesiastes": { "es": "Eclesiastés", "fr": "Ecclésiaste", "da": "Prædikeren" }, + "Song of Solomon": { "es": "Cantares", "fr": "Cantique des Cantiques", "da": "Højsangen" }, + "Isaiah": { "es": "Isaías", "fr": "Ésaïe", "da": "Esajas" }, + "Jeremiah": { "es": "Jeremías", "fr": "Jérémie", "da": "Jeremias" }, + "Lamentations": { "es": "Lamentaciones", "fr": "Lamentations", "da": "Klagesangene" }, + "Ezekiel": { "es": "Ezequiel", "fr": "Ézéchiel", "da": "Ezekiel" }, + "Daniel": { "es": "Daniel", "fr": "Daniel", "da": "Daniel" }, + "Hosea": { "es": "Oseas", "fr": "Osée", "da": "Hoseas" }, + "Joel": { "es": "Joel", "fr": "Joël", "da": "Joel" }, + "Amos": { "es": "Amós", "fr": "Amos", "da": "Amos" }, + "Obadiah": { "es": "Abdías", "fr": "Abdias", "da": "Obadias" }, + "Jonah": { "es": "Jonás", "fr": "Jonas", "da": "Jonas" }, + "Micah": { "es": "Miqueas", "fr": "Michée", "da": "Mika" }, + "Nahum": { "es": "Nahúm", "fr": "Nahum", "da": "Nahum" }, + "Habakkuk": { "es": "Habacuc", "fr": "Habacuc", "da": "Habakkuk" }, + "Zephaniah": { "es": "Sofonías", "fr": "Sophonie", "da": "Sefanias" }, + "Haggai": { "es": "Hageo", "fr": "Aggée", "da": "Haggaj" }, + "Zechariah": { "es": "Zacarías", "fr": "Zacharie", "da": "Zakarias" }, + "Malachi": { "es": "Malaquías", "fr": "Malachie", "da": "Malakias" }, + "Matthew": { "es": "Mateo", "fr": "Matthieu", "da": "Matthæus" }, + "Mark": { "es": "Marcos", "fr": "Marc", "da": "Markus" }, + "Luke": { "es": "Lucas", "fr": "Luc", "da": "Lukas" }, + "John": { "es": "Juan", "fr": "Jean", "da": "Johannes" }, + "Acts": { "es": "Hechos", "fr": "Actes", "da": "Apostlenes Gerninger" }, + "Romans": { "es": "Romanos", "fr": "Romains", "da": "Romerne" }, + "1 Corinthians": { "es": "1 Corintios", "fr": "1 Corinthiens", "da": "1 Korinther" }, + "2 Corinthians": { "es": "2 Corintios", "fr": "2 Corinthiens", "da": "2 Korinther" }, + "Galatians": { "es": "Gálatas", "fr": "Galates", "da": "Galaterne" }, + "Ephesians": { "es": "Efesios", "fr": "Éphésiens", "da": "Efeserne" }, + "Philippians": { "es": "Filipenses", "fr": "Philippiens", "da": "Filipperne" }, + "Colossians": { "es": "Colosenses", "fr": "Colossiens", "da": "Kolossenserne" }, + "1 Thessalonians": { "es": "1 Tesalonicenses", "fr": "1 Thessaloniciens", "da": "1 Thessaloniker" }, + "2 Thessalonians": { "es": "2 Tesalonicenses", "fr": "2 Thessaloniciens", "da": "2 Thessaloniker" }, + "1 Timothy": { "es": "1 Timoteo", "fr": "1 Timothée", "da": "1 Timotheus" }, + "2 Timothy": { "es": "2 Timoteo", "fr": "2 Timothée", "da": "2 Timotheus" }, + "Titus": { "es": "Tito", "fr": "Tite", "da": "Titus" }, + "Philemon": { "es": "Filemón", "fr": "Philémon", "da": "Filemon" }, + "Hebrews": { "es": "Hebreos", "fr": "Hébreux", "da": "Hebræerne" }, + "James": { "es": "Santiago", "fr": "Jacques", "da": "Jakob" }, + "1 Peter": { "es": "1 Pedro", "fr": "1 Pierre", "da": "1 Peter" }, + "2 Peter": { "es": "2 Pedro", "fr": "2 Pierre", "da": "2 Peter" }, + "1 John": { "es": "1 Juan", "fr": "1 Jean", "da": "1 Johannes" }, + "2 John": { "es": "2 Juan", "fr": "2 Jean", "da": "2 Johannes" }, + "3 John": { "es": "3 Juan", "fr": "3 Jean", "da": "3 Johannes" }, + "Jude": { "es": "Judas", "fr": "Jude", "da": "Judas" }, + "Revelation": { "es": "Apocalipsis", "fr": "Apocalypse", "da": "Åbenbaringen" } +}; + +export const translateBibleReference = (reference = "", locale = "en") => { + const lang = String(locale || "en").substring(0, 2).toLowerCase(); + if (lang === "en") return reference; + + const parsed = parseBibleReference(reference); + if (!parsed || !parsed.book || !BIBLE_BOOK_TRANSLATIONS[parsed.book]) return reference; + + const translatedBook = BIBLE_BOOK_TRANSLATIONS[parsed.book][lang] || parsed.book; + return reference.replace(parsed.book, translatedBook); +};