From c2818788751f9aa720a303ab8b213555779a7b2c Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Wed, 25 Feb 2026 18:41:54 -0500 Subject: [PATCH] feat: integrate post AI translation with UI toggle and backend queue --- API.js | 7 +++-- components/Post.js | 55 +++++++++++++++++++++++++++++++++++++++- i18nMessages.js | 12 +++++++++ utils/bibleReferences.js | 2 +- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/API.js b/API.js index 262bf4d..0a63c59 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 = {}; @@ -270,6 +270,9 @@ const API = { updatePost(post){ return postCall("/post/" + post._id, {content: post.content}); }, + translatePost(postid, targetLang) { + return postCall("/post/translate", { postid, targetLang }); + }, newPost(content, toProfile, isNews) { //Content is expected to be a string. let params = { content }; diff --git a/components/Post.js b/components/Post.js index 59bae02..6a379a4 100644 --- a/components/Post.js +++ b/components/Post.js @@ -26,6 +26,7 @@ let Post = (props) => { const [deleted, setDeleted] = useState(false); let [likes, changeLikes] = useState(Object.keys(post.reactions).length); let [bookmarked, changeBookmarked] = useState(post.bookmarks && post.bookmarks.includes(viewer._id)); + const [translating, setTranslating] = useState(false); const isOwner = String(post.profileid || '') === String(viewer?._id || ''); const swipeX = useRef(new Animated.Value(0)).current; const mediaGestureActiveRef = useRef(false); @@ -34,6 +35,30 @@ let Post = (props) => { : undefined; let cleanContent = stripInlineTags(stripBibleTokens(post.content)); const navigation = useNavigation(); + + const textWithoutUrls = cleanContent.replace(/(https?:\/\/[^\s]+)/g, '').trim(); + const hasTextContent = textWithoutUrls && textWithoutUrls.length > 0 && cleanContent.length < 1000; + const currentLang = i18n.locale.substring(0, 2).toLowerCase(); + const hasTranslation = post.translations && post.translations[currentLang]; + const [showTranslation, setShowTranslation] = useState(!!hasTranslation); + + const requestTranslation = async () => { + setTranslating(true); + try { + await API.translatePost(post._id, i18n.locale); + setTimeout(async () => { + const updatedPost = await API.getPost(post._id); + if (updatedPost && updatedPost.translations && updatedPost.translations[i18n.locale.substring(0, 2).toLowerCase()]) { + changePost(updatedPost); + setShowTranslation(true); + } + setTranslating(false); + }, 3000); + } catch (e) { + setTranslating(false); + } + }; + //cleanContent = convertLinks(cleanContent); const newComentAdded = (commentData) => { let newPostObj = { ...post }; @@ -168,9 +193,37 @@ let Post = (props) => { { pattern: /(https?:\/\/[^\s]+)/, style: styles.link, onPress: handleLinkPress, onLongPress: handleLinkLongPress }, ]} > - {cleanContent} + {showTranslation && hasTranslation ? stripInlineTags(stripBibleTokens(post.translations[currentLang])) : cleanContent} + {hasTextContent && ( + + {hasTranslation && showTranslation ? ( + + + {i18n.t("message.aiTranslated") || "AI Translated"} + + setShowTranslation(false)}> + + {i18n.t("message.seeOriginal") || "See Original"} + + + + ) : ( + { + if (hasTranslation) { + setShowTranslation(true); + } else { + requestTranslation(); + } + }}> + + {translating ? (i18n.t("message.translating") || "Translating...") : (i18n.t("message.seeTranslation") || "See Translation")} + + + )} + + )} diff --git a/i18nMessages.js b/i18nMessages.js index 0cd9126..74a3d36 100644 --- a/i18nMessages.js +++ b/i18nMessages.js @@ -148,6 +148,9 @@ const messages = { updateProfile: "Update Profile", previous: "Previous", next: "Next", + translating: "Translating...", + seeTranslation: "See Translation", + seeOriginal: "See Original", }, }, es: { @@ -291,6 +294,9 @@ const messages = { updateProfile: "Actualizar perfil", previous: "Anterior", next: "Siguiente", + translating: "Traduciendo...", + seeTranslation: "Ver traducción", + seeOriginal: "Ver original", } }, fr: { @@ -434,6 +440,9 @@ const messages = { updateProfile: "Mettre à jour le profil", previous: "Précédent", next: "Suivant", + translating: "Traduction en cours...", + seeTranslation: "Voir la traduction", + seeOriginal: "Voir l'original", } }, da: { @@ -577,6 +586,9 @@ const messages = { updateProfile: "Opdater profil", previous: "Forrige", next: "Næste", + translating: "Oversætter...", + seeTranslation: "Se oversættelse", + seeOriginal: "Se original", } } } diff --git a/utils/bibleReferences.js b/utils/bibleReferences.js index 8984a23..5894e93 100644 --- a/utils/bibleReferences.js +++ b/utils/bibleReferences.js @@ -155,7 +155,7 @@ export const fetchBibleChapter = async (chapterReference = "", locale = "", cust queryParamsString += p + "=" + queryParams[p] + "&"; }); - const localBaseUrl = global.baseUrl ?? "http://localhost:3000"; + const localBaseUrl = global.baseUrl ?? "https://emiapi.reynafamily.com"; // 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, {