Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions backend/src/controllers/translate.ts
Original file line number Diff line number Diff line change
@@ -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',
});
}
};
5 changes: 3 additions & 2 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ 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();

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);

Expand Down
99 changes: 99 additions & 0 deletions backend/src/services/translationService.ts
Original file line number Diff line number Diff line change
@@ -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',
});
}
};
Binary file added frontend/assets/translate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 89 additions & 3 deletions frontend/components/PicturePost.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string | null>(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) => (
<Image
source={{ uri: url }}
Expand All @@ -51,8 +133,12 @@ export default function PicturePost({
date={date}
avatarUri={avatarUri}
userId={userId}
showTranslate={shouldShowTranslate}
onPressTranslate={toggleTranslation}
loadingTranslate={loadingTranslation}
/>
<Text style={styles.content}>{content}</Text>

<Text style={styles.content}>{captionToShow}</Text>

{imageUrls.length > 0 ? (
<MyCarousel components={imageComponents} width={100} height={40} />
Expand All @@ -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,
Expand Down
Loading