112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
|
|
interface TranslationState {
|
|
translated: string;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
isTranslated: boolean;
|
|
}
|
|
|
|
interface TranslationsMap {
|
|
[key: string]: TranslationState;
|
|
}
|
|
|
|
/**
|
|
* Hook for translating text on demand using MyMemory free API
|
|
* - 1000 translations/day free (anonymous)
|
|
* - 10000/day with free email registration
|
|
* - No API key required for basic usage
|
|
*/
|
|
export function useTranslation(targetLang: string = 'en') {
|
|
const [translations, setTranslations] = useState<TranslationsMap>({});
|
|
const translationsRef = useRef<TranslationsMap>({});
|
|
|
|
// Keep ref in sync with state
|
|
translationsRef.current = translations;
|
|
|
|
const translate = useCallback(async (text: string, id: string, sourceLang: string = 'auto') => {
|
|
const current = translationsRef.current[id];
|
|
|
|
// If already translated, toggle back to original
|
|
if (current?.isTranslated) {
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: { ...prev[id], isTranslated: false }
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// If we have a cached translation, just show it
|
|
if (current?.translated) {
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: { ...prev[id], isTranslated: true }
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Start loading
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: { translated: '', isLoading: true, error: null, isTranslated: false }
|
|
}));
|
|
|
|
try {
|
|
// Use MyMemory free translation API
|
|
// Docs: https://mymemory.translated.net/doc/spec.php
|
|
// Note: 500 character limit per query
|
|
const truncatedText = text.length > 500 ? text.substring(0, 497) + '...' : text;
|
|
const langPair = sourceLang === 'auto' ? `autodetect|${targetLang}` : `${sourceLang}|${targetLang}`;
|
|
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(truncatedText)}&langpair=${langPair}`;
|
|
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Translation failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Handle "same language" error - text is already in target language
|
|
if (data.responseStatus !== 200) {
|
|
const errorMsg = data.responseDetails || '';
|
|
if (errorMsg.includes('DISTINCT LANGUAGES') || errorMsg.includes('same language')) {
|
|
// Text is already in target language, just show original
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: { translated: text, isLoading: false, error: null, isTranslated: true }
|
|
}));
|
|
return;
|
|
}
|
|
throw new Error(errorMsg || 'Translation failed');
|
|
}
|
|
|
|
const translatedText = data.responseData.translatedText;
|
|
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: { translated: translatedText, isLoading: false, error: null, isTranslated: true }
|
|
}));
|
|
} catch (error) {
|
|
console.error('[Translation] Error:', error);
|
|
setTranslations(prev => ({
|
|
...prev,
|
|
[id]: {
|
|
translated: '',
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Translation failed',
|
|
isTranslated: false
|
|
}
|
|
}));
|
|
}
|
|
}, [targetLang]);
|
|
|
|
const getState = useCallback((id: string): TranslationState => {
|
|
return translations[id] || { translated: '', isLoading: false, error: null, isTranslated: false };
|
|
}, [translations]);
|
|
|
|
return { translate, getState };
|
|
}
|