feat: integrate post AI translation with UI toggle and backend queue

This commit is contained in:
Adolfo Reyna
2026-02-25 18:41:54 -05:00
parent 9da5874977
commit c281878875
4 changed files with 72 additions and 4 deletions

7
API.js
View File

@@ -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 = {};
@@ -270,6 +270,9 @@ const API = {
updatePost(post){ updatePost(post){
return postCall("/post/" + post._id, {content: post.content}); return postCall("/post/" + post._id, {content: post.content});
}, },
translatePost(postid, targetLang) {
return postCall("/post/translate", { postid, targetLang });
},
newPost(content, toProfile, isNews) { newPost(content, toProfile, isNews) {
//Content is expected to be a string. //Content is expected to be a string.
let params = { content }; let params = { content };

View File

@@ -26,6 +26,7 @@ let Post = (props) => {
const [deleted, setDeleted] = useState(false); const [deleted, setDeleted] = useState(false);
let [likes, changeLikes] = useState(Object.keys(post.reactions).length); let [likes, changeLikes] = useState(Object.keys(post.reactions).length);
let [bookmarked, changeBookmarked] = useState(post.bookmarks && post.bookmarks.includes(viewer._id)); 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 isOwner = String(post.profileid || '') === String(viewer?._id || '');
const swipeX = useRef(new Animated.Value(0)).current; const swipeX = useRef(new Animated.Value(0)).current;
const mediaGestureActiveRef = useRef(false); const mediaGestureActiveRef = useRef(false);
@@ -34,6 +35,30 @@ let Post = (props) => {
<ProfilePhotoCircle profileid={post.toProfile} small={true} /> : undefined; <ProfilePhotoCircle profileid={post.toProfile} small={true} /> : undefined;
let cleanContent = stripInlineTags(stripBibleTokens(post.content)); let cleanContent = stripInlineTags(stripBibleTokens(post.content));
const navigation = useNavigation(); 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); //cleanContent = convertLinks(cleanContent);
const newComentAdded = (commentData) => { const newComentAdded = (commentData) => {
let newPostObj = { ...post }; let newPostObj = { ...post };
@@ -168,9 +193,37 @@ let Post = (props) => {
{ pattern: /(https?:\/\/[^\s]+)/, style: styles.link, onPress: handleLinkPress, onLongPress: handleLinkLongPress }, { pattern: /(https?:\/\/[^\s]+)/, style: styles.link, onPress: handleLinkPress, onLongPress: handleLinkLongPress },
]} ]}
> >
{cleanContent} {showTranslation && hasTranslation ? stripInlineTags(stripBibleTokens(post.translations[currentLang])) : cleanContent}
</ParsedText> </ParsedText>
</Pressable> </Pressable>
{hasTextContent && (
<View style={{ paddingLeft: 40, paddingRight: 8, marginTop: 4 }}>
{hasTranslation && showTranslation ? (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingRight: 4 }}>
<Text style={{ fontSize: 11, color: '#16a34a', fontStyle: 'italic' }}>
{i18n.t("message.aiTranslated") || "AI Translated"}
</Text>
<Pressable onPress={() => setShowTranslation(false)}>
<Text style={{ color: '#6b7280', fontSize: 11, fontWeight: '500' }}>
{i18n.t("message.seeOriginal") || "See Original"}
</Text>
</Pressable>
</View>
) : (
<Pressable onPress={() => {
if (hasTranslation) {
setShowTranslation(true);
} else {
requestTranslation();
}
}}>
<Text style={{ color: '#2563eb', fontSize: 13, fontWeight: '600', marginBottom: 6 }}>
{translating ? (i18n.t("message.translating") || "Translating...") : (i18n.t("message.seeTranslation") || "See Translation")}
</Text>
</Pressable>
)}
</View>
)}
<View style={{ paddingLeft: 40, paddingRight: 8 }}> <View style={{ paddingLeft: 40, paddingRight: 8 }}>
<BibleEmbeddedView content={post.content} openChapterOnPress /> <BibleEmbeddedView content={post.content} openChapterOnPress />
</View> </View>

View File

@@ -148,6 +148,9 @@ const messages = {
updateProfile: "Update Profile", updateProfile: "Update Profile",
previous: "Previous", previous: "Previous",
next: "Next", next: "Next",
translating: "Translating...",
seeTranslation: "See Translation",
seeOriginal: "See Original",
}, },
}, },
es: { es: {
@@ -291,6 +294,9 @@ const messages = {
updateProfile: "Actualizar perfil", updateProfile: "Actualizar perfil",
previous: "Anterior", previous: "Anterior",
next: "Siguiente", next: "Siguiente",
translating: "Traduciendo...",
seeTranslation: "Ver traducción",
seeOriginal: "Ver original",
} }
}, },
fr: { fr: {
@@ -434,6 +440,9 @@ const messages = {
updateProfile: "Mettre à jour le profil", updateProfile: "Mettre à jour le profil",
previous: "Précédent", previous: "Précédent",
next: "Suivant", next: "Suivant",
translating: "Traduction en cours...",
seeTranslation: "Voir la traduction",
seeOriginal: "Voir l'original",
} }
}, },
da: { da: {
@@ -577,6 +586,9 @@ const messages = {
updateProfile: "Opdater profil", updateProfile: "Opdater profil",
previous: "Forrige", previous: "Forrige",
next: "Næste", next: "Næste",
translating: "Oversætter...",
seeTranslation: "Se oversættelse",
seeOriginal: "Se original",
} }
} }
} }

View File

@@ -155,7 +155,7 @@ export const fetchBibleChapter = async (chapterReference = "", locale = "", cust
queryParamsString += p + "=" + queryParams[p] + "&"; 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), // 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. // we can use standard fetch with the same cookie inclusion strategy.
const fetchRes = await fetch(localBaseUrl + url + queryParamsString, { const fetchRes = await fetch(localBaseUrl + url + queryParamsString, {