Files
office_translator/frontend/src/app/dashboard/translate/useTranslationConfig.ts
sepehr 52748ee653
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m57s
feat(pricing): remove DeepL from all user-facing surfaces
Pricing: 'deepl' removed from plan provider lists and the comparison
table row. Landing and pricing copy (13 locales) no longer mention
DeepL (65 values cleaned, 39 dead keys deleted: pricing.comparison.deepl,
providerTheme.classic.deepl.*).

Providers: available-provider responses are filtered so a DeepL entry
from the backend can never surface in the engine picker or the services
page; DeepL theme entry and static lib/api.ts list entry removed.

Admin: DeepL config card, type fields, defaults and fallback-chain
mentions removed; stats/chart/status maps and mock data cleaned.

Backend adapter untouched — DeepL is invisible app-wide via the UI
filter; removing the Python adapter itself is a separate step if wanted.

Verified: build exit 0, vitest 9/9, eslint clean on touched files,
zero 'deepl' occurrences outside the two intentional UI filters.
2026-08-30 22:20:53 +02:00

272 lines
9.5 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import type {
UseTranslationConfigReturn,
Language,
TranslationMode,
Provider,
TranslationConfig,
AvailableProvider,
} from './types';
import { API_BASE } from '@/lib/config';
import { useTranslationStore } from '@/lib/store';
/** 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 { settings } = useTranslationStore();
const [sourceLang, setSourceLang] = useState('auto');
const [targetLang, setTargetLang] = useState(settings.defaultTargetLanguage || '');
const [provider, setProvider] = useState<Provider | null>(null);
const [glossaryId, setGlossaryId] = useState<string | null>(null);
const [translateImages, setTranslateImages] = useState(false);
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);
// Sync with store default target language
useEffect(() => {
if (settings.defaultTargetLanguage && !targetLang) {
setTargetLang(settings.defaultTargetLanguage);
}
}, [settings.defaultTargetLanguage]); // eslint-disable-line react-hooks/exhaustive-deps
// Reset glossary selection when source language changes
// (multilingual glossaries are compatible with any target language)
useEffect(() => {
// If there is a query param, do not reset it on first load
const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
if (params.get('glossaryId') === glossaryId) return;
setGlossaryId(null);
}, [sourceLang]); // eslint-disable-line react-hooks/exhaustive-deps
// Pre-select glossary and LLM provider from query parameter
useEffect(() => {
if (typeof window === 'undefined' || isLoadingProviders || availableProviders.length === 0) return;
const params = new URLSearchParams(window.location.search);
const qGlossaryId = params.get('glossaryId');
if (qGlossaryId) {
setGlossaryId(qGlossaryId);
const p = availableProviders.find((ap) => ap.id === provider);
if (!p || p.mode !== 'llm') {
const firstLlm = availableProviders.find((ap) => ap.mode === 'llm');
if (firstLlm) {
setProvider(firstLlm.id);
}
}
}
}, [isLoadingProviders, availableProviders, provider]);
// 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 || []).filter((p: AvailableProvider) => p.id !== 'deepl');
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); };
}, []);
// Auto-select first classic provider for non-Pro users
useEffect(() => {
if (isLoadingProviders) return;
if (provider !== null) return;
if (isPro) return;
if (availableProviders.length === 0) return;
const firstClassic = availableProviders.find((p) => p.mode === 'classic');
if (firstClassic) setProvider(firstClassic.id);
}, [availableProviders, isLoadingProviders, isPro, provider]);
// 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 isProTier = (u: any) => ['pro', 'business', 'enterprise'].includes(u?.plan ?? u?.tier ?? '');
const userStr = localStorage.getItem('user');
if (userStr) {
try {
const user = JSON.parse(userStr);
if (user?.plan || user?.tier) { setIsPro(isProTier(user)); }
} 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(isProTier(user));
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;
// Translating a language into itself is always a mistake
if (sourceLang !== 'auto' && sourceLang === targetLang) return false;
return true;
}, [hasFile, targetLang, provider, sourceLang]);
const getConfig = useCallback((): TranslationConfig => ({
sourceLang,
targetLang,
mode,
provider: provider ?? undefined,
glossaryId,
translateImages,
}), [sourceLang, targetLang, mode, provider, glossaryId, translateImages]);
return {
sourceLang,
targetLang,
mode,
provider,
availableProviders,
isLoadingProviders,
languages,
isPro,
isConfigValid,
isLoadingLanguages,
languagesError,
setSourceLang,
setTargetLang,
setProvider,
glossaryId,
setGlossaryId,
translateImages,
setTranslateImages,
getConfig,
};
}