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:
Claude
2026-02-06 17:46:01 +00:00
parent 9457d3d02f
commit f719d181c0
15 changed files with 768 additions and 58 deletions

View File

@@ -7,11 +7,12 @@ import math
from uuid import uuid4
from typing import Optional, Any, Literal
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends, Header
from fastapi.responses import JSONResponse
from beyond_api.security import get_current_user
from beyond_api.services.analysis_service import run_analysis_collect_json
from beyond_api.i18n import t, get_language_from_header
# Cache paths - same as in cache.py
CACHE_DIR = Path(os.getenv("CACHE_DIR", "/data/cache"))
@@ -52,6 +53,7 @@ async def analysis_endpoint(
economy_json: Optional[str] = Form(default=None),
analysis: Literal["basic", "premium"] = Form(default="premium"),
current_user: str = Depends(get_current_user),
accept_language: Optional[str] = Header(None),
):
"""
Ejecuta el pipeline sobre un CSV subido (multipart/form-data) y devuelve
@@ -62,12 +64,13 @@ async def analysis_endpoint(
- "premium": usa la configuración completa por defecto
(p.ej. beyond_metrics_config.json), sin romper lo existente.
"""
lang = get_language_from_header(accept_language)
# Validar `analysis` (por si llega algo raro)
if analysis not in {"basic", "premium"}:
raise HTTPException(
status_code=400,
detail="analysis debe ser 'basic' o 'premium'.",
detail=t("analysis.invalid_type", lang),
)
# 1) Parseo de economía (si viene)
@@ -78,7 +81,7 @@ async def analysis_endpoint(
except json.JSONDecodeError:
raise HTTPException(
status_code=400,
detail="economy_json no es un JSON válido.",
detail=t("analysis.invalid_economy_json", lang),
)
# 2) Guardar el CSV subido en una carpeta de trabajo
@@ -159,23 +162,26 @@ async def analysis_cached_endpoint(
economy_json: Optional[str] = Form(default=None),
analysis: Literal["basic", "premium"] = Form(default="premium"),
current_user: str = Depends(get_current_user),
accept_language: Optional[str] = Header(None),
):
"""
Ejecuta el pipeline sobre el archivo CSV cacheado en el servidor.
Útil para re-analizar sin tener que subir el archivo de nuevo.
"""
lang = get_language_from_header(accept_language)
# Validar que existe el archivo cacheado
if not CACHED_FILE.exists():
raise HTTPException(
status_code=404,
detail="No hay archivo cacheado en el servidor. Sube un archivo primero.",
detail=t("analysis.no_cached_file", lang),
)
# Validar `analysis`
if analysis not in {"basic", "premium"}:
raise HTTPException(
status_code=400,
detail="analysis debe ser 'basic' o 'premium'.",
detail=t("analysis.invalid_type", lang),
)
# Parseo de economía (si viene)
@@ -186,7 +192,7 @@ async def analysis_cached_endpoint(
except json.JSONDecodeError:
raise HTTPException(
status_code=400,
detail="economy_json no es un JSON válido.",
detail=t("analysis.invalid_economy_json", lang),
)
# Extraer metadatos del CSV
@@ -204,7 +210,7 @@ async def analysis_cached_endpoint(
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error ejecutando análisis: {str(e)}",
detail=t("analysis.execution_error", lang, error=str(e)),
)
# Limpiar NaN/inf para que el JSON sea válido