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
129 lines
5.4 KiB
Python
129 lines
5.4 KiB
Python
"""
|
|
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"
|