From 448bb2eb95b69c97d0c31308e4b2f52477a91284 Mon Sep 17 00:00:00 2001 From: kaamil2 Date: Fri, 5 Dec 2025 16:54:43 -0500 Subject: [PATCH 1/2] translation function --- backend/src/controllers/translate.ts | 99 ++++++++++++++++ backend/src/routes/index.ts | 5 +- backend/src/services/translationService.ts | 99 ++++++++++++++++ frontend/assets/translate.png | Bin 0 -> 437 bytes frontend/components/PicturePost.tsx | 92 ++++++++++++++- frontend/components/TextPost.tsx | 92 ++++++++++++++- frontend/components/UserBar.tsx | 128 ++++++++++++--------- frontend/services/translationService.ts | 42 +++++++ 8 files changed, 496 insertions(+), 61 deletions(-) create mode 100644 backend/src/controllers/translate.ts create mode 100644 backend/src/services/translationService.ts create mode 100644 frontend/assets/translate.png create mode 100644 frontend/services/translationService.ts diff --git a/backend/src/controllers/translate.ts b/backend/src/controllers/translate.ts new file mode 100644 index 00000000..4715c6b7 --- /dev/null +++ b/backend/src/controllers/translate.ts @@ -0,0 +1,99 @@ +import { Request, Response } from 'express'; + +const BASE_URL = 'https://ftapi.pythonanywhere.com'; + +interface TranslationResponse { + 'source-language': string; + 'source-text': string; + 'destination-language': string; + 'destination-text': string; + pronunciation: { + 'source-text-phonetic': string | null; + 'source-text-audio': string; + 'destination-text-audio': string; + }; + translations: { + 'all-translations': Array<[string, string[]]> | null; + 'possible-translations': string[]; + 'possible-mistakes': string[] | null; + }; + definitions: any[] | null; + 'see-also': string[] | null; +} + +/** + * translate text with optional source language (auto detect if not provided) + * @query text - Text to translate (required) + * @query dl - Destination language code (required) + * @query sl - Source language code (optional, auto detects if not provided) + */ +export const translateText = async (req: Request, res: Response) => { + try { + const { text, dl, sl } = req.query; + + if (!text || !dl) { + return res.status(400).json({ + error: 'Missing parameters', + message: 'Both text and destination language are required', + }); + } + + const params = new URLSearchParams({ + dl: dl as string, + text: text as string, + }); + + if (sl) { + params.append('sl', sl as string); + } + + const response = await fetch(`${BASE_URL}/translate?${params.toString()}`); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data: TranslationResponse = await response.json(); + + return res.status(200).json({ + success: true, + sourceLanguage: data['source-language'], + sourceText: data['source-text'], + destinationLanguage: data['destination-language'], + destinationText: data['destination-text'], + pronunciation: data.pronunciation, + translations: data.translations, + definitions: data.definitions, + }); + } catch (error) { + console.error('Translation error:', error); + return res.status(500).json({ + error: 'Translation failed', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } +}; + +// get all supported languages +export const getSupportedLanguages = async (req: Request, res: Response) => { + try { + const response = await fetch(`${BASE_URL}/languages`); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const languages = await response.json(); + + return res.status(200).json({ + success: true, + languages, + }); + } catch (error) { + console.error('Error fetching languages:', error); + return res.status(500).json({ + error: 'Failed to fetch supported languages', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } +}; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 4b39c1c2..0100f783 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -19,6 +19,7 @@ import { createPost, getPostById, getPosts, updatePost, deletePost, getPostRepos import { searchMovies, searchUsers, searchReviews, searchPosts } from "../controllers/search.js"; import { getHomeFeed } from "../controllers/feed"; import { getMovieSummaryHandler } from "../controllers/movies.js"; +import { translateText, getSupportedLanguages } from "../controllers/translate"; // backend/src/routes/index.ts const router = Router(); @@ -26,8 +27,8 @@ const router = Router(); router.get("/api/ping", ping); router.get("/api/db-test", dbTest); router.get('/movies/:movieId/summary', getMovieSummaryHandler); - - +router.get("/api/translate", translateText); +router.get("/api/languages", getSupportedLanguages); // Legacy endpoint router.get("/swagger-output.json", serveSwagger); diff --git a/backend/src/services/translationService.ts b/backend/src/services/translationService.ts new file mode 100644 index 00000000..4715c6b7 --- /dev/null +++ b/backend/src/services/translationService.ts @@ -0,0 +1,99 @@ +import { Request, Response } from 'express'; + +const BASE_URL = 'https://ftapi.pythonanywhere.com'; + +interface TranslationResponse { + 'source-language': string; + 'source-text': string; + 'destination-language': string; + 'destination-text': string; + pronunciation: { + 'source-text-phonetic': string | null; + 'source-text-audio': string; + 'destination-text-audio': string; + }; + translations: { + 'all-translations': Array<[string, string[]]> | null; + 'possible-translations': string[]; + 'possible-mistakes': string[] | null; + }; + definitions: any[] | null; + 'see-also': string[] | null; +} + +/** + * translate text with optional source language (auto detect if not provided) + * @query text - Text to translate (required) + * @query dl - Destination language code (required) + * @query sl - Source language code (optional, auto detects if not provided) + */ +export const translateText = async (req: Request, res: Response) => { + try { + const { text, dl, sl } = req.query; + + if (!text || !dl) { + return res.status(400).json({ + error: 'Missing parameters', + message: 'Both text and destination language are required', + }); + } + + const params = new URLSearchParams({ + dl: dl as string, + text: text as string, + }); + + if (sl) { + params.append('sl', sl as string); + } + + const response = await fetch(`${BASE_URL}/translate?${params.toString()}`); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data: TranslationResponse = await response.json(); + + return res.status(200).json({ + success: true, + sourceLanguage: data['source-language'], + sourceText: data['source-text'], + destinationLanguage: data['destination-language'], + destinationText: data['destination-text'], + pronunciation: data.pronunciation, + translations: data.translations, + definitions: data.definitions, + }); + } catch (error) { + console.error('Translation error:', error); + return res.status(500).json({ + error: 'Translation failed', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } +}; + +// get all supported languages +export const getSupportedLanguages = async (req: Request, res: Response) => { + try { + const response = await fetch(`${BASE_URL}/languages`); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const languages = await response.json(); + + return res.status(200).json({ + success: true, + languages, + }); + } catch (error) { + console.error('Error fetching languages:', error); + return res.status(500).json({ + error: 'Failed to fetch supported languages', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } +}; \ No newline at end of file diff --git a/frontend/assets/translate.png b/frontend/assets/translate.png new file mode 100644 index 0000000000000000000000000000000000000000..60125e7fa1630846d05d3ceaa5fdf399b031d04f GIT binary patch literal 437 zcmV;m0ZRUfP)(t^bJgz61N~AZxc2G zq9h}s8Q#vkH?y;Z3{2BBMOD>^{9{zt^=J4g2b!jl*gvK8)Jvpv>p0GV|6k#ILh^LT z2#mQ91v|v2s;c@#JTOgjR@b%Ux^Bc9Bz?P+gD%d(`RDBh5iBVi9{gIF1jMn~XhHmfL#AZ;|1wkOVQH2Z)^PS1pp7R!xkSkO2hqWu&piP-$DDt8D`2c>kwyk}4 fkA`8e2jPDJCAXsyKeGH?00000NkvXXu0mjf@Ke7_ literal 0 HcmV?d00001 diff --git a/frontend/components/PicturePost.tsx b/frontend/components/PicturePost.tsx index 60198e82..44f52617 100644 --- a/frontend/components/PicturePost.tsx +++ b/frontend/components/PicturePost.tsx @@ -1,10 +1,38 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, StyleSheet, Dimensions, Image } from 'react-native'; import UserBar from './UserBar'; import MyCarousel from './Carousel'; +import { useAuth } from '../context/AuthContext'; +import { translateTextApi } from '../services/translationService'; const { width } = Dimensions.get('window'); +function mapPrimaryLanguage(primaryLanguage?: string | null): { + code: string; + label: string; +} { + if (!primaryLanguage) return { code: 'en', label: 'English' }; + + const lower = primaryLanguage.toLowerCase().trim(); + + switch (lower) { + case 'hindi': + return { code: 'hi', label: 'Hindi' }; + case 'tamil': + return { code: 'ta', label: 'Tamil' }; + case 'telugu': + return { code: 'te', label: 'Telugu' }; + case 'bengali': + case 'bangla': + return { code: 'bn', label: 'Bengali' }; + case 'english': + case 'en': + return { code: 'en', label: 'English' }; + default: + return { code: 'en', label: primaryLanguage }; + } +} + type PicturePostProps = { userName: string; username: string; @@ -27,6 +55,60 @@ export default function PicturePost({ userId, spoiler = false, }: PicturePostProps) { + const { profile } = useAuth(); + + const mapped = mapPrimaryLanguage((profile as any)?.primaryLanguage); + const userLangCode = mapped.code; + + const [isTranslated, setIsTranslated] = useState(false); + const [translated, setTranslated] = useState(null); + const [loadingTranslation, setLoadingTranslation] = useState(false); + + const shouldShowTranslate = userLangCode !== 'en'; + + console.log( + '[PicturePost] RENDER: langRaw =', + (profile as any)?.primaryLanguage + ); + console.log('[PicturePost] mapped lang =', mapped); + console.log('[PicturePost] shouldShowTranslate =', shouldShowTranslate); + + const captionToShow = isTranslated && translated ? translated : content; + + const toggleTranslation = async () => { + console.log('=== [PicturePost] toggleTranslation pressed ===', { + isTranslated, + hasTranslated: !!translated, + }); + + const turningOn = !isTranslated; + + if (turningOn && shouldShowTranslate && !translated) { + try { + setLoadingTranslation(true); + + console.log('[PicturePost] Calling translateTextApi with:', { + textSnippet: content.slice(0, 80), + dest: userLangCode, + }); + + // assume original caption is English; force 'en' as source + const response = await translateTextApi(content, userLangCode, 'en'); + console.log('[PicturePost] translateTextApi response =', response); + + const translatedText = response.destinationText || content; + setTranslated(translatedText); + } catch (err) { + console.error('[PicturePost] Translation ERROR:', err); + setTranslated(null); + } finally { + setLoadingTranslation(false); + } + } + + setIsTranslated(prev => !prev); + }; + const imageComponents = imageUrls.map((url, index) => ( - {content} + + {captionToShow} {imageUrls.length > 0 ? ( @@ -72,7 +158,7 @@ const styles = StyleSheet.create({ padding: width * 0.04, marginBottom: width * 0.04, width: '100%', - position: 'relative', // 👈 needed for pill positioning + position: 'relative', // for spoiler pill }, content: { fontSize: width * 0.0375, diff --git a/frontend/components/TextPost.tsx b/frontend/components/TextPost.tsx index 035eaf62..ef670d0b 100644 --- a/frontend/components/TextPost.tsx +++ b/frontend/components/TextPost.tsx @@ -1,9 +1,38 @@ -import React from 'react'; +// frontend/app/components/TextPost.tsx +import React, { useState } from 'react'; import { View, Text, StyleSheet, Dimensions } from 'react-native'; import UserBar from './UserBar'; +import { translateTextApi } from '../services/translationService'; +import { useAuth } from '../context/AuthContext'; const { width } = Dimensions.get('window'); +function mapPrimaryLanguage(primaryLanguage?: string | null): { + code: string; + label: string; +} { + if (!primaryLanguage) return { code: 'en', label: 'English' }; + + const lower = primaryLanguage.toLowerCase().trim(); + + switch (lower) { + case 'hindi': + return { code: 'hi', label: 'Hindi' }; + case 'tamil': + return { code: 'ta', label: 'Tamil' }; + case 'telugu': + return { code: 'te', label: 'Telugu' }; + case 'bengali': + case 'bangla': + return { code: 'bn', label: 'Bengali' }; + case 'english': + case 'en': + return { code: 'en', label: 'English' }; + default: + return { code: 'en', label: primaryLanguage }; + } +} + type TextPostProps = { userName: string; username: string; @@ -11,7 +40,6 @@ type TextPostProps = { avatarUri?: string; content: string; userId?: string; - /** If true, show a 'Spoiler' badge on the card */ spoiler?: boolean; }; @@ -24,9 +52,61 @@ export default function TextPost({ userId, spoiler = false, }: TextPostProps) { + const { profile } = useAuth(); + + const mapped = mapPrimaryLanguage((profile as any)?.primaryLanguage); + const userLangCode = mapped.code; + + const [isTranslated, setIsTranslated] = useState(false); + const [translated, setTranslated] = useState(null); + const [loadingTranslation, setLoadingTranslation] = useState(false); + + const shouldShowTranslate = userLangCode !== 'en'; + + console.log( + '[TextPost] RENDER: langRaw =', + (profile as any)?.primaryLanguage + ); + console.log('[TextPost] mapped lang =', mapped); + console.log('[TextPost] shouldShowTranslate =', shouldShowTranslate); + + const textToShow = isTranslated && translated ? translated : content; + + const toggleTranslation = async () => { + console.log('=== [TextPost] toggleTranslation pressed ===', { + isTranslated, + hasTranslated: !!translated, + }); + + const turningOn = !isTranslated; + + if (turningOn && shouldShowTranslate && !translated) { + try { + setLoadingTranslation(true); + + console.log('[TextPost] Calling translateTextApi with:', { + textSnippet: content.slice(0, 80), + dest: userLangCode, + }); + + const response = await translateTextApi(content, userLangCode, 'en'); + console.log('[TextPost] translateTextApi response =', response); + + const translatedText = response.destinationText || content; + setTranslated(translatedText); + } catch (err) { + console.error('[TextPost] Translation ERROR:', err); + setTranslated(null); + } finally { + setLoadingTranslation(false); + } + } + + setIsTranslated(prev => !prev); + }; + return ( - {/* Top-right spoiler pill */} {spoiler && ( Spoiler @@ -39,8 +119,12 @@ export default function TextPost({ date={date} avatarUri={avatarUri} userId={userId} + showTranslate={shouldShowTranslate} + onPressTranslate={toggleTranslation} + loadingTranslate={loadingTranslation} /> - {content} + + {textToShow} ); } diff --git a/frontend/components/UserBar.tsx b/frontend/components/UserBar.tsx index 9425cdc1..eee1b0ba 100644 --- a/frontend/components/UserBar.tsx +++ b/frontend/components/UserBar.tsx @@ -1,27 +1,32 @@ +// frontend/app/components/UserBar.tsx import React from 'react'; import { View, Text, - Image, StyleSheet, + Image, TouchableOpacity, + ActivityIndicator, Dimensions, } from 'react-native'; -import { useRouter } from 'expo-router'; -import Avatar from './Avatar'; + +// 👇 adjust this path to wherever you put the icon in your project +// e.g. ../assets/translate.png +const translateIcon = require('../assets/translate.png'); const { width } = Dimensions.get('window'); type UserBarProps = { name: string; - username?: string; - date?: string; + username: string; + date: string; avatarUri?: string; - avatarSize?: number; userId?: string; - nameColor?: string; - usernameColor?: string; - dateColor?: string; + + // NEW: translation-related props + showTranslate?: boolean; + onPressTranslate?: () => void; + loadingTranslate?: boolean; }; export default function UserBar({ @@ -29,46 +34,46 @@ export default function UserBar({ username, date, avatarUri, - avatarSize = width * 0.1, + // userId not used visually here but you might use it for nav userId, - nameColor = '#000', - usernameColor = '#666', - dateColor = '#999', + showTranslate = false, + onPressTranslate, + loadingTranslate = false, }: UserBarProps) { - const router = useRouter(); - - const handlePress = () => { - if (userId) { - router.push(`/profilePage?userId=${userId}`); - } - }; - return ( - - + {/* Left: avatar */} + + {avatarUri ? ( + + ) : ( + + )} + + + {name} + @{username} + - - - {name} - - {username && ( - - @{username} - + + {/* Right: translate icon + date */} + + {showTranslate && ( + + {loadingTranslate ? ( + + ) : ( + + )} + )} + + {date} - {date && {date}} ); } @@ -76,27 +81,46 @@ export default function UserBar({ const styles = StyleSheet.create({ container: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', - width: '100%', }, - avatar: { - marginRight: width * 0.03, + leftRow: { + flexDirection: 'row', + alignItems: 'center', }, - textContainer: { - flex: 1, + rightRow: { flexDirection: 'row', alignItems: 'center', - gap: width * 0.02, + gap: 6, + }, + avatar: { + width: width * 0.1, + height: width * 0.1, + borderRadius: width * 0.05, + marginRight: width * 0.03, + }, + avatarPlaceholder: { + backgroundColor: '#DDD', }, name: { - fontSize: 12, + fontSize: width * 0.04, fontWeight: '600', + color: '#000', }, username: { - fontSize: 12, - marginTop: 2, + fontSize: width * 0.032, + color: '#666', }, date: { - fontSize: width * 0.035, + fontSize: width * 0.032, + color: '#888', + }, + translateButton: { + marginRight: 4, + }, + translateIcon: { + width: 16, + height: 16, + resizeMode: 'contain', }, }); diff --git a/frontend/services/translationService.ts b/frontend/services/translationService.ts new file mode 100644 index 00000000..59dba444 --- /dev/null +++ b/frontend/services/translationService.ts @@ -0,0 +1,42 @@ +import { api } from "./apiClient"; + +export type TranslateApiResponse = { + success: boolean; + sourceLanguage: string; + sourceText: string; + destinationLanguage: string; + destinationText: string; + pronunciation: { + "source-text-phonetic": string | null; + "source-text-audio": string; + "destination-text-audio": string; + }; + translations: { + "all-translations": Array<[string, string[]]> | null; + "possible-translations": string[]; + "possible-mistakes": string[] | null; + }; + definitions: any[] | null; +}; + +export async function translateTextApi( + text: string, + destLang: string, + sourceLang?: string +): Promise { + const params: Record = { + text, + dl: destLang, + }; + + if (sourceLang) { + params.sl = sourceLang; + } + + // IMPORTANT: pass params directly (this matches your old working version) + const response = await api.get("/api/translate", params as any); + + // Handle both: api.get returns either raw data OR AxiosResponse + const data = (response as any).data ?? response; + return data as TranslateApiResponse; +} From bc6c3535faa17fefe53ac8f54b190e6cf093d836 Mon Sep 17 00:00:00 2001 From: kaamil2 Date: Fri, 5 Dec 2025 17:05:09 -0500 Subject: [PATCH 2/2] translation function --- frontend/components/UserBar.tsx | 60 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/frontend/components/UserBar.tsx b/frontend/components/UserBar.tsx index eee1b0ba..2f1abfe1 100644 --- a/frontend/components/UserBar.tsx +++ b/frontend/components/UserBar.tsx @@ -1,4 +1,4 @@ -// frontend/app/components/UserBar.tsx +// components/UserBar.tsx import React from 'react'; import { View, @@ -10,20 +10,23 @@ import { Dimensions, } from 'react-native'; -// 👇 adjust this path to wherever you put the icon in your project -// e.g. ../assets/translate.png -const translateIcon = require('../assets/translate.png'); - const { width } = Dimensions.get('window'); -type UserBarProps = { +// ⬇️ adjust if your icon lives somewhere else +const translateIcon = require('../assets/translate.png'); + +export type UserBarProps = { name: string; - username: string; - date: string; + username?: string; + date?: string; avatarUri?: string; userId?: string; - // NEW: translation-related props + // old props used by ReviewPost + avatarSize?: number; // in px + nameColor?: string; + + // new translation props showTranslate?: boolean; onPressTranslate?: () => void; loadingTranslate?: boolean; @@ -34,29 +37,47 @@ export default function UserBar({ username, date, avatarUri, - // userId not used visually here but you might use it for nav - userId, + userId, // not used visually yet, but kept for navigation if you want + avatarSize, + nameColor, showTranslate = false, onPressTranslate, loadingTranslate = false, }: UserBarProps) { + // default avatar size if not provided + const size = avatarSize ?? width * 0.1; + return ( - {/* Left: avatar */} + {/* Left: avatar + name/username */} {avatarUri ? ( - + ) : ( - + )} - {name} - @{username} + + {name} + + {username ? @{username} : null} - {/* Right: translate icon + date */} + {/* Right: translate icon (optional) + date (optional) */} {showTranslate && ( )} - {date} + {date ? {date} : null} ); @@ -94,9 +115,6 @@ const styles = StyleSheet.create({ gap: 6, }, avatar: { - width: width * 0.1, - height: width * 0.1, - borderRadius: width * 0.05, marginRight: width * 0.03, }, avatarPlaceholder: {