Add English language support with i18n implementation
Implemented comprehensive internationalization (i18n) for both frontend and backend: Frontend: - Added react-i18next configuration with Spanish (default) and English - Created translation files (locales/es.json, locales/en.json) - Refactored core components to use i18n: LoginPage, DashboardHeader, DataUploader - Created LanguageSelector component with toggle between ES/EN - Updated API client to send Accept-Language header Backend: - Created i18n module with translation dictionary for error messages - Updated security.py to return localized authentication errors - Updated api/analysis.py to return localized validation errors - Implemented language detection from Accept-Language header Spanish remains the default language ensuring backward compatibility. Users can switch between languages using the language selector in the dashboard header. https://claude.ai/code/session_1N9VX
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { UploadCloud, File, Sheet, Loader2, CheckCircle, Sparkles, Wand2, BarChart3 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generateSyntheticCsv } from '../utils/syntheticDataGenerator';
|
||||
import { TierKey } from '../types';
|
||||
|
||||
@@ -21,6 +22,7 @@ const formatFileSize = (bytes: number, decimals = 2) => {
|
||||
};
|
||||
|
||||
const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisReady, isAnalyzing }) => {
|
||||
const { t } = useTranslation();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [sheetUrl, setSheetUrl] = useState('');
|
||||
const [status, setStatus] = useState<UploadStatus>('idle');
|
||||
@@ -57,7 +59,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
setFile(selectedFile);
|
||||
setSheetUrl('');
|
||||
} else {
|
||||
setError('Tipo de archivo no válido. Sube un CSV o Excel.');
|
||||
setError(t('upload.invalidFileType'));
|
||||
setFile(null);
|
||||
}
|
||||
}
|
||||
@@ -89,19 +91,19 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
setStatus('generating');
|
||||
setTimeout(() => {
|
||||
const csvData = generateSyntheticCsv(selectedTier);
|
||||
handleDataReady('Datos Sintéticos Generados!');
|
||||
handleDataReady(t('upload.syntheticDataGenerated'));
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!file && !sheetUrl) {
|
||||
setError('Por favor, sube un archivo o introduce una URL de Google Sheet.');
|
||||
setError(t('upload.pleaseUploadFile'));
|
||||
return;
|
||||
}
|
||||
resetState(false);
|
||||
setStatus('uploading');
|
||||
setTimeout(() => {
|
||||
handleDataReady('Datos Recibidos!');
|
||||
handleDataReady(t('upload.dataReceived'));
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
@@ -114,7 +116,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
className="w-full flex items-center justify-center gap-2 text-white px-6 py-3 rounded-lg transition-colors shadow-sm hover:shadow-md bg-green-600 hover:bg-green-700 disabled:opacity-75 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isAnalyzing ? <Loader2 className="animate-spin" size={20} /> : <BarChart3 size={20} />}
|
||||
{isAnalyzing ? 'Analizando...' : 'Ver Dashboard de Diagnóstico'}
|
||||
{isAnalyzing ? t('upload.analyzingData') : t('dashboard.viewDashboard')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -126,7 +128,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
className="w-full flex items-center justify-center gap-2 text-white px-6 py-3 rounded-lg transition-colors shadow-sm hover:shadow-md bg-blue-600 hover:bg-blue-700 disabled:opacity-75 disabled:cursor-not-allowed"
|
||||
>
|
||||
{status === 'uploading' ? <Loader2 className="animate-spin" size={20} /> : <Wand2 size={20} />}
|
||||
{status === 'uploading' ? 'Procesando...' : 'Generar Análisis'}
|
||||
{status === 'uploading' ? t('upload.processingData') : t('dashboard.generateAnalysis')}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -134,10 +136,10 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="mb-6">
|
||||
<span className="text-blue-600 font-semibold mb-1 block">Paso 2</span>
|
||||
<h2 className="text-2xl font-bold text-slate-900">Sube tus Datos y Ejecuta el Análisis</h2>
|
||||
<span className="text-blue-600 font-semibold mb-1 block">{t('stepper.step2')}</span>
|
||||
<h2 className="text-2xl font-bold text-slate-900">{t('upload.title')}</h2>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Usa una de las siguientes opciones para enviarnos tus datos para el análisis.
|
||||
{t('upload.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -158,33 +160,33 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
/>
|
||||
<label htmlFor="file-upload" className="cursor-pointer flex flex-col items-center justify-center">
|
||||
<UploadCloud className="w-12 h-12 text-slate-400 mb-2" />
|
||||
<span className="font-semibold text-blue-600">Haz clic para subir un fichero</span>
|
||||
<span className="text-slate-500"> o arrástralo aquí</span>
|
||||
<p className="text-xs text-slate-400 mt-2">CSV, XLSX, o XLS</p>
|
||||
<span className="font-semibold text-blue-600">{t('upload.clickToUpload')}</span>
|
||||
<span className="text-slate-500"> {t('upload.dragAndDrop')}</span>
|
||||
<p className="text-xs text-slate-400 mt-2">{t('upload.fileTypes')}</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-slate-500">
|
||||
<hr className="w-full border-slate-300" />
|
||||
<span className="px-4 font-medium text-sm">O</span>
|
||||
<span className="px-4 font-medium text-sm">{t('common.or')}</span>
|
||||
<hr className="w-full border-slate-300" />
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-slate-50 rounded-lg">
|
||||
<p className="text-sm text-slate-600 mb-3">¿No tienes datos a mano? Genera un set de datos de ejemplo.</p>
|
||||
<p className="text-sm text-slate-600 mb-3">{t('upload.noDataPrompt')}</p>
|
||||
<button
|
||||
onClick={handleGenerateSyntheticData}
|
||||
disabled={isActionInProgress}
|
||||
className="flex items-center justify-center gap-2 w-full sm:w-auto mx-auto bg-fuchsia-100 text-fuchsia-700 px-6 py-3 rounded-lg hover:bg-fuchsia-200 hover:text-fuchsia-800 transition-colors shadow-sm hover:shadow-md disabled:opacity-75 disabled:cursor-not-allowed font-semibold"
|
||||
>
|
||||
{status === 'generating' ? <Loader2 className="animate-spin" size={20} /> : <Sparkles size={20} />}
|
||||
{status === 'generating' ? 'Generando...' : 'Generar Datos Sintéticos'}
|
||||
{status === 'generating' ? t('upload.generating') : t('upload.generateSyntheticData')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-slate-500">
|
||||
<hr className="w-full border-slate-300" />
|
||||
<span className="px-4 font-medium text-sm">O</span>
|
||||
<span className="px-4 font-medium text-sm">{t('common.or')}</span>
|
||||
<hr className="w-full border-slate-300" />
|
||||
</div>
|
||||
|
||||
@@ -192,7 +194,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
<Sheet className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="url"
|
||||
placeholder="Pega la URL de tu Google Sheet aquí"
|
||||
placeholder={t('upload.googleSheetPlaceholder')}
|
||||
value={sheetUrl}
|
||||
onChange={(e) => {
|
||||
resetState();
|
||||
@@ -249,7 +251,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ selectedTier, onAnalysisRea
|
||||
{status === 'success' && (
|
||||
<div className="flex items-center justify-center gap-2 p-4 bg-green-50 border border-green-200 text-green-800 rounded-lg">
|
||||
<CheckCircle className="w-6 h-6 flex-shrink-0" />
|
||||
<span className="font-semibold">{successMessage} ¡Listo para analizar!</span>
|
||||
<span className="font-semibold">{successMessage} {t('upload.readyToAnalyze')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user