feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
216
frontend/src/app/dashboard/translate/useTranslationConfig.ts
Normal file
216
frontend/src/app/dashboard/translate/useTranslationConfig.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import type {
|
||||
UseTranslationConfigReturn,
|
||||
Language,
|
||||
TranslationMode,
|
||||
Provider,
|
||||
TranslationConfig,
|
||||
AvailableProvider,
|
||||
} from './types';
|
||||
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
/** Fallback when API fails — Google is always available server-side */
|
||||
const FALLBACK_PROVIDERS: AvailableProvider[] = [
|
||||
{ id: 'google', label: 'Google Traduction', description: 'Traduction rapide, 130+ langues', mode: 'classic' },
|
||||
];
|
||||
|
||||
const FALLBACK_LANGUAGES: Language[] = [
|
||||
// Top 5 — dominant on the internet
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Spanish' },
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'ja', name: 'Japanese' },
|
||||
// Top 6-15
|
||||
{ code: 'pt', name: 'Portuguese' },
|
||||
{ code: 'ru', name: 'Russian' },
|
||||
{ code: 'it', name: 'Italian' },
|
||||
{ code: 'zh-CN', name: 'Chinese (Simplified)' },
|
||||
{ code: 'zh-TW', name: 'Chinese (Traditional)' },
|
||||
{ code: 'pl', name: 'Polish' },
|
||||
{ code: 'nl', name: 'Dutch' },
|
||||
{ code: 'tr', name: 'Turkish' },
|
||||
{ code: 'ko', name: 'Korean' },
|
||||
{ code: 'ar', name: 'Arabic' },
|
||||
// Top 16-25
|
||||
{ code: 'fa', name: 'Persian (Farsi)' },
|
||||
{ code: 'vi', name: 'Vietnamese' },
|
||||
{ code: 'id', name: 'Indonesian' },
|
||||
{ code: 'uk', name: 'Ukrainian' },
|
||||
{ code: 'sv', name: 'Swedish' },
|
||||
{ code: 'cs', name: 'Czech' },
|
||||
{ code: 'el', name: 'Greek' },
|
||||
{ code: 'he', name: 'Hebrew' },
|
||||
{ code: 'hi', name: 'Hindi' },
|
||||
{ code: 'ro', name: 'Romanian' },
|
||||
// Others
|
||||
{ code: 'da', name: 'Danish' },
|
||||
{ code: 'fi', name: 'Finnish' },
|
||||
{ code: 'no', name: 'Norwegian' },
|
||||
{ code: 'hu', name: 'Hungarian' },
|
||||
{ code: 'th', name: 'Thai' },
|
||||
{ code: 'sk', name: 'Slovak' },
|
||||
{ code: 'bg', name: 'Bulgarian' },
|
||||
{ code: 'hr', name: 'Croatian' },
|
||||
{ code: 'ca', name: 'Catalan' },
|
||||
{ code: 'ms', name: 'Malay' },
|
||||
];
|
||||
|
||||
export function useTranslationConfig(hasFile: boolean): UseTranslationConfigReturn {
|
||||
const [sourceLang, setSourceLang] = useState('auto');
|
||||
const [targetLang, setTargetLang] = useState('');
|
||||
const [provider, setProvider] = useState<Provider | null>(null);
|
||||
const [availableProviders, setAvailableProviders] = useState<AvailableProvider[]>([]);
|
||||
const [isLoadingProviders, setIsLoadingProviders] = useState(false);
|
||||
const [languages, setLanguages] = useState<Language[]>([]);
|
||||
const [isPro, setIsPro] = useState(false);
|
||||
const [isLoadingLanguages, setIsLoadingLanguages] = useState(false);
|
||||
const [languagesError, setLanguagesError] = useState<string | null>(null);
|
||||
|
||||
// Fetch available (admin-configured) providers
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const fetchProviders = async () => {
|
||||
setIsLoadingProviders(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/providers/available`, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const list = data.providers || [];
|
||||
setAvailableProviders(list.length > 0 ? list : FALLBACK_PROVIDERS);
|
||||
} else {
|
||||
setAvailableProviders(FALLBACK_PROVIDERS);
|
||||
}
|
||||
} catch {
|
||||
// Backend down or timeout — use fallback so user can still try
|
||||
setAvailableProviders(FALLBACK_PROVIDERS);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
setIsLoadingProviders(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProviders();
|
||||
return () => { controller.abort(); clearTimeout(timeoutId); };
|
||||
}, []);
|
||||
|
||||
// Fetch supported languages
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const fetchLanguages = async () => {
|
||||
setIsLoadingLanguages(true);
|
||||
setLanguagesError(null);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/languages`, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
const langList: Language[] = Object.entries(data.supported_languages || {}).map(
|
||||
([code, name]) => ({ code, name: name as string })
|
||||
);
|
||||
setLanguages(langList.length > 0 ? langList : FALLBACK_LANGUAGES);
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
console.warn('Language fetch timed out, using fallback list');
|
||||
} else {
|
||||
setLanguagesError(error instanceof Error ? error.message : 'Failed to load languages');
|
||||
}
|
||||
setLanguages(FALLBACK_LANGUAGES);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
setIsLoadingLanguages(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLanguages();
|
||||
return () => { controller.abort(); clearTimeout(timeoutId); };
|
||||
}, []);
|
||||
|
||||
// Check user tier
|
||||
useEffect(() => {
|
||||
const checkTier = async () => {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
if (user.tier) { setIsPro(user.tier === 'pro'); return; }
|
||||
} catch { /* continue */ }
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) { setIsPro(false); return; }
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/me`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const user = result.data;
|
||||
setIsPro(user.tier === 'pro');
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
} else {
|
||||
setIsPro(false);
|
||||
}
|
||||
} catch { setIsPro(false); }
|
||||
};
|
||||
checkTier();
|
||||
}, []);
|
||||
|
||||
// Mode is derived from the selected provider, never set manually.
|
||||
const mode = useMemo<TranslationMode>(() => {
|
||||
if (!provider) return 'classic';
|
||||
const p = availableProviders.find((ap) => ap.id === provider);
|
||||
return p?.mode === 'llm' ? 'llm' : 'classic';
|
||||
}, [provider, availableProviders]);
|
||||
|
||||
const isConfigValid = useMemo(() => {
|
||||
if (!hasFile || !targetLang) return false;
|
||||
if (!provider) return false;
|
||||
return true;
|
||||
}, [hasFile, targetLang, provider]);
|
||||
|
||||
const getConfig = useCallback((): TranslationConfig => ({
|
||||
sourceLang,
|
||||
targetLang,
|
||||
mode,
|
||||
provider: provider ?? undefined,
|
||||
}), [sourceLang, targetLang, mode, provider]);
|
||||
|
||||
return {
|
||||
sourceLang,
|
||||
targetLang,
|
||||
mode,
|
||||
provider,
|
||||
availableProviders,
|
||||
isLoadingProviders,
|
||||
languages,
|
||||
isPro,
|
||||
isConfigValid,
|
||||
isLoadingLanguages,
|
||||
languagesError,
|
||||
setSourceLang,
|
||||
setTargetLang,
|
||||
setProvider,
|
||||
getConfig,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user