feat: Add continuous Bible reading mode, backend-driven translation API integration, localized book names, and preference caching
This commit is contained in:
12
API.js
12
API.js
@@ -1,6 +1,6 @@
|
|||||||
import i18n from "./i18nMessages.js";
|
import i18n from "./i18nMessages.js";
|
||||||
const baseUrl = "https://emiapi.reynafamily.com";
|
//const baseUrl = "https://emiapi.reynafamily.com";
|
||||||
//const baseUrl = "http://localhost:3000";
|
const baseUrl = "http://localhost:3000";
|
||||||
const requestErrorCooldownMs = 30000;
|
const requestErrorCooldownMs = 30000;
|
||||||
const profileFailureCooldownMs = 60000;
|
const profileFailureCooldownMs = 60000;
|
||||||
const recentRequestErrors = {};
|
const recentRequestErrors = {};
|
||||||
@@ -367,6 +367,14 @@ const API = {
|
|||||||
return getCall("/user/" + profileid + "/unfollow");
|
return getCall("/user/" + profileid + "/unfollow");
|
||||||
},
|
},
|
||||||
setDataValue(key, value){
|
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});
|
return postCall("/user/setData", {key, value});
|
||||||
},
|
},
|
||||||
//Groups
|
//Groups
|
||||||
|
|||||||
5
App.js
5
App.js
@@ -32,6 +32,7 @@ import SongPlayer from './Views/SongPlayer.js';
|
|||||||
import GlobalChat from './Views/GlobalChat.js';
|
import GlobalChat from './Views/GlobalChat.js';
|
||||||
import BiblePicker from './Views/BiblePicker.js';
|
import BiblePicker from './Views/BiblePicker.js';
|
||||||
import BibleChapterView from './Views/BibleChapterView.js';
|
import BibleChapterView from './Views/BibleChapterView.js';
|
||||||
|
import Bible from './Views/Bible.js';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { PostHogProvider } from 'posthog-react-native'
|
import { PostHogProvider } from 'posthog-react-native'
|
||||||
import * as Updates from 'expo-updates';
|
import * as Updates from 'expo-updates';
|
||||||
@@ -437,6 +438,10 @@ export default function App() {
|
|||||||
component={BibleChapterView}
|
component={BibleChapterView}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Bible"
|
||||||
|
component={Bible}
|
||||||
|
/>
|
||||||
<Stack.Screen name="SinglePost" component={SinglePost} />
|
<Stack.Screen name="SinglePost" component={SinglePost} />
|
||||||
<Stack.Screen name="Login" component={Login} options={{ headerShown: false }} />
|
<Stack.Screen name="Login" component={Login} options={{ headerShown: false }} />
|
||||||
<Tab.Screen name="Logout" component={Login} />
|
<Tab.Screen name="Logout" component={Login} />
|
||||||
|
|||||||
88
Views/Bible.js
Normal file
88
Views/Bible.js
Normal file
@@ -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 (
|
||||||
|
<View style={{ flex: 1, paddingHorizontal: 12, paddingTop: 12 }}>
|
||||||
|
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 6 }}>{i18n.t("message.bible") || "Read the Bible"}</Text>
|
||||||
|
<Text style={{ color: "#6b7280", marginBottom: 10 }}>
|
||||||
|
{i18n.t("message.biblePickerSubtitlePost") || "Select a book and chapter to start reading."}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={{ flexDirection: "row", marginBottom: 10 }}>
|
||||||
|
<Button mode={activeStep === "book" ? "contained-tonal" : "text"} onPress={() => setActiveStep("book")}>
|
||||||
|
{i18n.t("message.book") || "Book"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
mode={activeStep === "chapter" ? "contained-tonal" : "text"}
|
||||||
|
disabled={!selectedBook}
|
||||||
|
onPress={() => setActiveStep("chapter")}
|
||||||
|
>
|
||||||
|
{i18n.t("message.chapter") || "Chapter"}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{activeStep === "book" ? (
|
||||||
|
<>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: "600", marginBottom: 8 }}>{i18n.t("message.books") || "Books"}</Text>
|
||||||
|
<FlatList
|
||||||
|
data={BIBLE_BOOKS}
|
||||||
|
numColumns={2}
|
||||||
|
keyExtractor={(item) => item}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Chip
|
||||||
|
selected={selectedBook === item}
|
||||||
|
style={{ marginRight: 6, marginBottom: 6, width: "48%" }}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedBook(item);
|
||||||
|
setActiveStep("chapter");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeStep === "chapter" ? (
|
||||||
|
<>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: "600", marginBottom: 8 }}>
|
||||||
|
{i18n.t("message.chapters") || "Chapters"} {selectedBook ? `(${selectedBook})` : ""}
|
||||||
|
</Text>
|
||||||
|
<FlatList
|
||||||
|
data={chapterOptions}
|
||||||
|
numColumns={6}
|
||||||
|
keyExtractor={(item) => item}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Chip
|
||||||
|
style={{ marginRight: 6, marginBottom: 6, minWidth: 44 }}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.navigate("BibleChapter", { reference: `${selectedBook} ${item}`, selectable: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bible;
|
||||||
@@ -1,35 +1,61 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FlatList, Pressable, View } from "react-native";
|
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 { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
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 GlobalState from "../contexts/GlobalState.js";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import API from "../API.js";
|
||||||
import i18n from "../i18nMessages.js";
|
import i18n from "../i18nMessages.js";
|
||||||
|
|
||||||
const BibleChapterView = ({ route }) => {
|
const BibleChapterView = ({ route }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const gState = useSnapshot(GlobalState);
|
||||||
const reference = route?.params?.reference || "";
|
const reference = route?.params?.reference || "";
|
||||||
const selectable = route?.params?.selectable === true;
|
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 [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState("");
|
const [error, setError] = React.useState("");
|
||||||
const [chapterData, setChapterData] = React.useState(null);
|
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 listRef = React.useRef(null);
|
||||||
const autoScrolledRef = React.useRef(false);
|
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(() => {
|
React.useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const loadChapter = async () => {
|
const loadChapter = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const payload = await fetchBibleChapter(chapterReference, i18n.locale);
|
const payload = await fetchBibleChapter(chapterReference, i18n.locale, selectedTranslation);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setChapterData(payload);
|
setChapterData(payload);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setError(i18n.t("message.unableLoadChapter"));
|
setError(i18n.t("message.unableLoadChapter") || "Unable to load chapter");
|
||||||
setChapterData(null);
|
setChapterData(null);
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setLoading(false);
|
if (mounted) setLoading(false);
|
||||||
@@ -39,7 +65,7 @@ const BibleChapterView = ({ route }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
};
|
};
|
||||||
}, [chapterReference]);
|
}, [chapterReference, selectedTranslation]);
|
||||||
|
|
||||||
const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : [];
|
const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : [];
|
||||||
const selectedVerseNumber = Number(selectedVerse || 1);
|
const selectedVerseNumber = Number(selectedVerse || 1);
|
||||||
@@ -47,7 +73,7 @@ const BibleChapterView = ({ route }) => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
autoScrolledRef.current = false;
|
autoScrolledRef.current = false;
|
||||||
}, [chapterReference, selectedVerseNumber]);
|
}, [chapterReference, selectedTranslation, selectedVerseNumber]);
|
||||||
|
|
||||||
const scrollToSelectedVerse = React.useCallback((animated = false) => {
|
const scrollToSelectedVerse = React.useCallback((animated = false) => {
|
||||||
if (autoScrolledRef.current) return;
|
if (autoScrolledRef.current) return;
|
||||||
@@ -92,12 +118,48 @@ const BibleChapterView = ({ route }) => {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<View style={{ flex: 1, padding: 12 }}>
|
<View style={{ flex: 1, padding: 12 }}>
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start" }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 4 }}>
|
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 4 }}>
|
||||||
{chapterData?.reference || chapterReference}
|
{chapterData?.reference || chapterReference}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ color: "#6b7280", marginBottom: 10 }}>
|
<Menu
|
||||||
|
visible={translationMenuVisible}
|
||||||
|
onDismiss={() => setTranslationMenuVisible(false)}
|
||||||
|
anchor={
|
||||||
|
<Pressable onPress={() => setTranslationMenuVisible(true)}>
|
||||||
|
<Text style={{ color: "#2563eb", marginBottom: 10, fontWeight: "600", textDecorationLine: "underline" }}>
|
||||||
{chapterData?.translation_name || chapterData?.translation_id || "KJV"}
|
{chapterData?.translation_name || chapterData?.translation_id || "KJV"}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{AVAILABLE_TRANSLATIONS.map((trans) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={trans.id}
|
||||||
|
onPress={() => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</View>
|
||||||
|
{!selectable ? (
|
||||||
|
<IconButton
|
||||||
|
icon="menu-book"
|
||||||
|
size={28}
|
||||||
|
onPress={() => navigation.navigate("Bible")}
|
||||||
|
style={{ margin: 0, marginTop: -4 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
{selectable ? (
|
{selectable ? (
|
||||||
<Text style={{ color: "#6b7280", marginBottom: 8 }}>{i18n.t("message.tapVerseToSelect")}</Text>
|
<Text style={{ color: "#6b7280", marginBottom: 8 }}>{i18n.t("message.tapVerseToSelect")}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -122,6 +184,32 @@ const BibleChapterView = ({ route }) => {
|
|||||||
scrollToSelectedVerse(false);
|
scrollToSelectedVerse(false);
|
||||||
}, 120);
|
}, 120);
|
||||||
}}
|
}}
|
||||||
|
ListFooterComponent={() => (
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 16, marginBottom: 24, paddingHorizontal: 4 }}>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
disabled={!prevReference}
|
||||||
|
onPress={() => {
|
||||||
|
if (prevReference) {
|
||||||
|
navigation.replace("BibleChapter", { reference: prevReference, selectable });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.t("message.previous") || "Previous"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
disabled={!nextReference}
|
||||||
|
onPress={() => {
|
||||||
|
if (nextReference) {
|
||||||
|
navigation.replace("BibleChapter", { reference: nextReference, selectable });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.t("message.next") || "Next"}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
renderItem={({ item }) => {
|
renderItem={({ item }) => {
|
||||||
const verseNumber = Number(item?.verse || 0);
|
const verseNumber = Number(item?.verse || 0);
|
||||||
const isSelected = verseNumber === selectedVerseNumber;
|
const isSelected = verseNumber === selectedVerseNumber;
|
||||||
@@ -129,19 +217,20 @@ const BibleChapterView = ({ route }) => {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => handleVersePress(verseNumber)}
|
onPress={() => handleVersePress(verseNumber)}
|
||||||
style={{
|
style={{
|
||||||
paddingVertical: 8,
|
paddingVertical: 4,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 8,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 6,
|
marginBottom: 2,
|
||||||
backgroundColor: isSelected ? "#fff3cd" : "transparent",
|
backgroundColor: isSelected ? "#fff3cd" : "transparent",
|
||||||
borderWidth: isSelected ? 1 : 0,
|
borderWidth: isSelected ? 1 : 0,
|
||||||
borderColor: isSelected ? "#f59e0b" : "transparent",
|
borderColor: isSelected ? "#f59e0b" : "transparent",
|
||||||
|
flexDirection: "row",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontWeight: "700", color: isSelected ? "#92400e" : "#374151" }}>
|
<Text style={{ fontWeight: "700", color: isSelected ? "#92400e" : "#374151", marginRight: 8, marginTop: 2, fontSize: 12 }}>
|
||||||
{verseNumber}
|
{verseNumber}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ color: "#111827", lineHeight: 21 }}>{item?.text || ""}</Text>
|
<Text style={{ color: "#111827", lineHeight: 22, flex: 1, fontSize: 16 }}>{(item?.text || "").replace(/\n/g, " ").trim()}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ let MenuView = ({ navigation }) => {
|
|||||||
<List.Item key="Logout" title={i18n.t('message.logout')} onPress={() => { navigation.navigate("Logout") }} left={props => <List.Icon {...props} icon="logout" />} />
|
<List.Item key="Logout" title={i18n.t('message.logout')} onPress={() => { navigation.navigate("Logout") }} left={props => <List.Icon {...props} icon="logout" />} />
|
||||||
</List.Section>
|
</List.Section>
|
||||||
<List.Section title={i18n.t("message.fellowshipApp")}>
|
<List.Section title={i18n.t("message.fellowshipApp")}>
|
||||||
|
<List.Item key='Bible' title={i18n.t('message.bible') || 'Bible'} onPress={() => { navigation.navigate("Bible") }} left={props => <List.Icon {...props} icon="menu-book" />} />
|
||||||
<List.Item key='Invite' title={i18n.t('message.invite')} onPress={() => { navigation.navigate("Invite") }} left={props => <List.Icon {...props} icon="person-add" />} />
|
<List.Item key='Invite' title={i18n.t('message.invite')} onPress={() => { navigation.navigate("Invite") }} left={props => <List.Icon {...props} icon="person-add" />} />
|
||||||
<List.Item key='About' title={i18n.t('message.about')} left={props => <List.Icon {...props} icon="more" />} />
|
<List.Item key='About' title={i18n.t('message.about')} left={props => <List.Icon {...props} icon="more" />} />
|
||||||
</List.Section>
|
</List.Section>
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import React, { useMemo, useState } from "react";
|
|||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import { ActivityIndicator, Chip } from "react-native-paper";
|
import { ActivityIndicator, Chip } from "react-native-paper";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
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";
|
import i18n from "../i18nMessages.js";
|
||||||
|
|
||||||
const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = false }) => {
|
const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = false }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const gState = useSnapshot(GlobalState);
|
||||||
const references = useMemo(() => extractBibleReferences(content), [content]);
|
const references = useMemo(() => extractBibleReferences(content), [content]);
|
||||||
const [selectedRef, setSelectedRef] = useState("");
|
const [selectedRef, setSelectedRef] = useState("");
|
||||||
const [byReference, setByReference] = useState({});
|
const [byReference, setByReference] = useState({});
|
||||||
@@ -24,7 +27,8 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress =
|
|||||||
|
|
||||||
setByReference((prev) => ({ ...prev, [reference]: { loading: true } }));
|
setByReference((prev) => ({ ...prev, [reference]: { loading: true } }));
|
||||||
try {
|
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 } }));
|
setByReference((prev) => ({ ...prev, [reference]: { loading: false, ...data } }));
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
setByReference((prev) => ({ ...prev, [reference]: { loading: false, error: true } }));
|
setByReference((prev) => ({ ...prev, [reference]: { loading: false, error: true } }));
|
||||||
@@ -46,7 +50,7 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress =
|
|||||||
style={styles.chip}
|
style={styles.chip}
|
||||||
onPress={() => handleSelectReference(reference)}
|
onPress={() => handleSelectReference(reference)}
|
||||||
>
|
>
|
||||||
{reference}
|
{translateBibleReference(reference, i18n.locale)}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -55,7 +59,7 @@ const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress =
|
|||||||
<View style={styles.previewBox}>
|
<View style={styles.previewBox}>
|
||||||
<Text style={styles.previewText}>{selectedData.text.slice(0, compact ? 160 : 280)}</Text>
|
<Text style={styles.previewText}>{selectedData.text.slice(0, compact ? 160 : 280)}</Text>
|
||||||
<Text style={styles.previewMeta}>
|
<Text style={styles.previewMeta}>
|
||||||
{selectedData.reference} ({selectedData.translation})
|
{translateBibleReference(selectedData.reference, i18n.locale)} ({selectedData.translation})
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ const messages = {
|
|||||||
chapters: "Chapters",
|
chapters: "Chapters",
|
||||||
verses: "Verses",
|
verses: "Verses",
|
||||||
updateProfile: "Update Profile",
|
updateProfile: "Update Profile",
|
||||||
|
previous: "Previous",
|
||||||
|
next: "Next",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
@@ -287,6 +289,8 @@ const messages = {
|
|||||||
chapters: "Capítulos",
|
chapters: "Capítulos",
|
||||||
verses: "Versículos",
|
verses: "Versículos",
|
||||||
updateProfile: "Actualizar perfil",
|
updateProfile: "Actualizar perfil",
|
||||||
|
previous: "Anterior",
|
||||||
|
next: "Siguiente",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
@@ -428,6 +432,8 @@ const messages = {
|
|||||||
chapters: "Chapitres",
|
chapters: "Chapitres",
|
||||||
verses: "Versets",
|
verses: "Versets",
|
||||||
updateProfile: "Mettre à jour le profil",
|
updateProfile: "Mettre à jour le profil",
|
||||||
|
previous: "Précédent",
|
||||||
|
next: "Suivant",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
da: {
|
da: {
|
||||||
@@ -569,6 +575,8 @@ const messages = {
|
|||||||
chapters: "Kapitler",
|
chapters: "Kapitler",
|
||||||
verses: "Vers",
|
verses: "Vers",
|
||||||
updateProfile: "Opdater profil",
|
updateProfile: "Opdater profil",
|
||||||
|
previous: "Forrige",
|
||||||
|
next: "Næste",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
|
import API from '../API.js';
|
||||||
|
|
||||||
const BIBLE_TOKEN_REGEX = /@bible:([^\s]+)/gi;
|
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 = "") => {
|
const getNormalizedLocale = (locale = "") => {
|
||||||
return String(locale || "").toLowerCase().replace("_", "-").trim();
|
return String(locale || "").toLowerCase().replace("_", "-").trim();
|
||||||
@@ -7,13 +26,10 @@ const getNormalizedLocale = (locale = "") => {
|
|||||||
|
|
||||||
const getTranslationForLocale = (locale = "") => {
|
const getTranslationForLocale = (locale = "") => {
|
||||||
const normalized = getNormalizedLocale(locale);
|
const normalized = getNormalizedLocale(locale);
|
||||||
if (!normalized) return "kjv";
|
if (!normalized) return "de4e12af7f28f599-01"; // KJV
|
||||||
if (normalized.startsWith("en-gb")) return "webbe";
|
if (normalized.startsWith("en")) return "de4e12af7f28f599-01"; // KJV
|
||||||
if (normalized.startsWith("en")) return "kjv";
|
if (normalized.startsWith("es")) return "592420522e16049f-01"; // RVR1909
|
||||||
if (normalized.startsWith("zh")) return "cuv";
|
// Default to KJV
|
||||||
if (normalized.startsWith("cs")) return "bkr";
|
|
||||||
if (normalized.startsWith("pt")) return "almeida";
|
|
||||||
if (normalized.startsWith("ro")) return "rccv";
|
|
||||||
return DEFAULT_TRANSLATION;
|
return DEFAULT_TRANSLATION;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,39 +78,144 @@ export const stripBibleTokens = (content = "") => {
|
|||||||
.trim();
|
.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchBibleReference = async (reference = "", locale = "") => {
|
export const AVAILABLE_TRANSLATIONS = [
|
||||||
const safeReference = normalizeReference(reference);
|
{ id: "de4e12af7f28f599-01", name: "King James Version (English)" },
|
||||||
const preferredTranslation = getTranslationForLocale(locale);
|
{ id: "9879dbb7cfe39e4d-01", name: "World English Bible (English)" },
|
||||||
const preferredUrl = `https://bible-api.com/${encodeURIComponent(safeReference)}?translation=${preferredTranslation}`;
|
{ id: "592420522e16049f-01", name: "Reina Valera 1909 (Spanish)" },
|
||||||
const fallbackUrl = `https://bible-api.com/${encodeURIComponent(safeReference)}?translation=${DEFAULT_TRANSLATION}`;
|
{ id: "48acedcf8595c754-01", name: "Palabla de Dios para ti (Spanish)" }
|
||||||
|
];
|
||||||
|
|
||||||
let response = await fetch(preferredUrl);
|
const parseChapterHtml = (htmlStr) => {
|
||||||
if (!response.ok && preferredTranslation !== DEFAULT_TRANSLATION) {
|
const parts = String(htmlStr || "").split(/<span[^>]*data-number=\"/);
|
||||||
response = await fetch(fallbackUrl);
|
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");
|
return versesMap;
|
||||||
|
|
||||||
const payload = await response.json();
|
|
||||||
if (payload?.error) throw new Error(payload.error);
|
|
||||||
return payload;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchBiblePassage = async (reference = "", locale = "") => {
|
const parseChapterJson = (items) => {
|
||||||
const safeReference = normalizeReference(reference);
|
const versesMap = {};
|
||||||
if (!safeReference) {
|
let currentVerse = null;
|
||||||
throw new Error("Missing Bible reference");
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
const payload = await fetchBibleReference(safeReference, locale);
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
reference: payload?.reference || safeReference,
|
reference: payload.reference || chapterReference,
|
||||||
text: (payload?.text || "").trim(),
|
verses: verses,
|
||||||
translation: payload?.translation_name || payload?.translation_id || "KJV",
|
translation_id: preferredTranslation,
|
||||||
|
translation_name: AVAILABLE_TRANSLATIONS.find(t => t.id === preferredTranslation)?.name || "Translation",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchBibleChapter = async (chapterReference = "", locale = "") => {
|
export const fetchBiblePassage = async (reference = "", locale = "", customTranslation = "") => {
|
||||||
const safeReference = normalizeReference(chapterReference);
|
const { verse } = parseBibleReference(reference);
|
||||||
return fetchBibleReference(safeReference, locale);
|
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 = "") => {
|
export const parseBibleReference = (reference = "") => {
|
||||||
@@ -262,3 +383,83 @@ export const BIBLE_BOOK_CHAPTERS = {
|
|||||||
export const getBookChapterCount = (book = "") => {
|
export const getBookChapterCount = (book = "") => {
|
||||||
return BIBLE_BOOK_CHAPTERS[book] || 1;
|
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);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user