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:
128
backend/beyond_api/i18n.py
Normal file
128
backend/beyond_api/i18n.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Módulo de internacionalización para Beyond API.
|
||||
Proporciona traducciones para mensajes de error, validaciones y clasificaciones.
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
Language = Literal["es", "en"]
|
||||
|
||||
# Diccionario de traducciones
|
||||
MESSAGES = {
|
||||
"es": {
|
||||
# Errores de autenticación
|
||||
"auth.credentials_required": "Credenciales requeridas",
|
||||
"auth.incorrect_credentials": "Credenciales incorrectas",
|
||||
|
||||
# Errores de análisis
|
||||
"analysis.invalid_type": "analysis debe ser 'basic' o 'premium'.",
|
||||
"analysis.invalid_economy_json": "economy_json no es un JSON válido.",
|
||||
"analysis.no_cached_file": "No hay archivo cacheado en el servidor. Sube un archivo primero.",
|
||||
"analysis.execution_error": "Error ejecutando análisis: {error}",
|
||||
|
||||
# Errores de validación
|
||||
"validation.field_not_numeric": "El campo '{field}' debe ser numérico (float). Valor recibido: {value}",
|
||||
"validation.segments_not_dict": "customer_segments debe ser un diccionario {segment: level}",
|
||||
"validation.segment_value_not_str": "El valor de customer_segments['{key}'] debe ser str. Valor recibido: {value}",
|
||||
"validation.csv_not_found": "El CSV no existe: {path}",
|
||||
"validation.not_csv_file": "La ruta no apunta a un fichero CSV: {path}",
|
||||
"validation.missing_columns": "Faltan columnas obligatorias para {metric}: {missing}",
|
||||
|
||||
# Clasificaciones Agentic Readiness
|
||||
"agentic.ready_for_copilot": "Listo para Copilot",
|
||||
"agentic.ready_for_copilot_desc": "Procesos con predictibilidad y simplicidad suficientes para asistencia IA (sugerencias en tiempo real, autocompletado).",
|
||||
"agentic.optimize_first": "Optimizar primero",
|
||||
"agentic.optimize_first_desc": "Estandarizar procesos y reducir variabilidad antes de implementar asistencia IA.",
|
||||
"agentic.requires_human": "Requiere gestión humana",
|
||||
"agentic.requires_human_desc": "Procesos complejos o variables que necesitan intervención humana antes de considerar automatización.",
|
||||
},
|
||||
"en": {
|
||||
# Authentication errors
|
||||
"auth.credentials_required": "Credentials required",
|
||||
"auth.incorrect_credentials": "Incorrect credentials",
|
||||
|
||||
# Analysis errors
|
||||
"analysis.invalid_type": "analysis must be 'basic' or 'premium'.",
|
||||
"analysis.invalid_economy_json": "economy_json is not valid JSON.",
|
||||
"analysis.no_cached_file": "No cached file on server. Upload a file first.",
|
||||
"analysis.execution_error": "Error executing analysis: {error}",
|
||||
|
||||
# Validation errors
|
||||
"validation.field_not_numeric": "Field '{field}' must be numeric (float). Received value: {value}",
|
||||
"validation.segments_not_dict": "customer_segments must be a dictionary {segment: level}",
|
||||
"validation.segment_value_not_str": "Value of customer_segments['{key}'] must be str. Received value: {value}",
|
||||
"validation.csv_not_found": "CSV does not exist: {path}",
|
||||
"validation.not_csv_file": "Path does not point to a CSV file: {path}",
|
||||
"validation.missing_columns": "Missing required columns for {metric}: {missing}",
|
||||
|
||||
# Agentic Readiness classifications
|
||||
"agentic.ready_for_copilot": "Ready for Copilot",
|
||||
"agentic.ready_for_copilot_desc": "Processes with sufficient predictability and simplicity for AI assistance (real-time suggestions, autocomplete).",
|
||||
"agentic.optimize_first": "Optimize first",
|
||||
"agentic.optimize_first_desc": "Standardize processes and reduce variability before implementing AI assistance.",
|
||||
"agentic.requires_human": "Requires human management",
|
||||
"agentic.requires_human_desc": "Complex or variable processes that need human intervention before considering automation.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def t(key: str, lang: Language = "es", **kwargs) -> str:
|
||||
"""
|
||||
Traduce un mensaje al idioma especificado.
|
||||
|
||||
Args:
|
||||
key: Clave del mensaje (ej: 'auth.credentials_required')
|
||||
lang: Idioma ('es' o 'en'). Por defecto 'es'
|
||||
**kwargs: Variables para interpolación (ej: field='nombre', value='123')
|
||||
|
||||
Returns:
|
||||
Mensaje traducido con variables interpoladas
|
||||
|
||||
Examples:
|
||||
>>> t('auth.credentials_required', 'en')
|
||||
'Credentials required'
|
||||
|
||||
>>> t('validation.field_not_numeric', 'en', field='age', value='abc')
|
||||
"Field 'age' must be numeric (float). Received value: abc"
|
||||
"""
|
||||
messages = MESSAGES.get(lang, MESSAGES["es"])
|
||||
message = messages.get(key, key)
|
||||
|
||||
# Interpolar variables si se proporcionan
|
||||
if kwargs:
|
||||
try:
|
||||
return message.format(**kwargs)
|
||||
except KeyError:
|
||||
# Si falta alguna variable, devolver el mensaje sin interpolar
|
||||
return message
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def get_language_from_header(accept_language: str | None) -> Language:
|
||||
"""
|
||||
Extrae el idioma preferido del header Accept-Language.
|
||||
|
||||
Args:
|
||||
accept_language: Valor del header Accept-Language (ej: 'en-US,en;q=0.9,es;q=0.8')
|
||||
|
||||
Returns:
|
||||
'en' si el idioma preferido es inglés, 'es' en caso contrario
|
||||
|
||||
Examples:
|
||||
>>> get_language_from_header('en-US,en;q=0.9')
|
||||
'en'
|
||||
|
||||
>>> get_language_from_header('es-ES,es;q=0.9')
|
||||
'es'
|
||||
|
||||
>>> get_language_from_header(None)
|
||||
'es'
|
||||
"""
|
||||
if not accept_language:
|
||||
return "es"
|
||||
|
||||
# Extraer el primer idioma del header
|
||||
primary_lang = accept_language.split(',')[0].split('-')[0].strip().lower()
|
||||
|
||||
return "en" if primary_lang == "en" else "es"
|
||||
Reference in New Issue
Block a user