diff --git a/aegis/__main__.py b/aegis/__main__.py index d31cf9fb..264c8f72 100644 --- a/aegis/__main__.py +++ b/aegis/__main__.py @@ -65,7 +65,7 @@ def main( lang: str | None = typer.Option( None, "--lang", - help="Output language (de, en, fr, ja, ko, ru, zh, zh_Hant). Default: auto-detect from AEGIS_LANG or system locale", + help="Output language (de, en, es, fr, ja, ko, ru, zh, zh_Hant). Default: auto-detect from AEGIS_LANG or system locale", envvar="AEGIS_LANG", ), ) -> None: diff --git a/aegis/i18n/locales/__init__.py b/aegis/i18n/locales/__init__.py index 1c6c25c6..6d490c80 100644 --- a/aegis/i18n/locales/__init__.py +++ b/aegis/i18n/locales/__init__.py @@ -1,3 +1,13 @@ """Available locales for Aegis Stack CLI.""" -AVAILABLE_LOCALES: set[str] = {"de", "en", "fr", "ja", "ko", "ru", "zh", "zh_Hant"} +AVAILABLE_LOCALES: set[str] = { + "de", + "en", + "es", + "fr", + "ja", + "ko", + "ru", + "zh", + "zh_Hant", +} diff --git a/aegis/i18n/locales/es.py b/aegis/i18n/locales/es.py new file mode 100644 index 00000000..a7b155ce --- /dev/null +++ b/aegis/i18n/locales/es.py @@ -0,0 +1,757 @@ +"""Spanish locale — Definiciones de mensajes en español.""" + +MESSAGES: dict[str, str] = { + # ── Validation ───────────────────────────────────────────────────── + "validation.invalid_name": ( + "Nombre de proyecto inválido. Solo se permiten letras, números, " + "guiones y guiones bajos." + ), + "validation.reserved_name": "'{name}' es un nombre reservado.", + "validation.name_too_long": ( + "Nombre de proyecto demasiado largo. Máximo 50 caracteres." + ), + "validation.invalid_python": ( + "Versión de Python inválida '{version}'. Debe ser una de: {supported}" + ), + "validation.unknown_service": "Servicio desconocido: {name}", + "validation.unknown_services": "Servicios desconocidos: {names}", + "validation.unknown_component": "Componente desconocido: {name}", + # ── Init command ─────────────────────────────────────────────────── + "init.title": "Inicialización de proyecto Aegis Stack", + "init.location": "Ubicación:", + "init.template_version": "Versión de plantilla:", + "init.dir_exists": "El directorio '{path}' ya existe", + "init.dir_exists_hint": "Usa --force para sobrescribir o elige otro nombre", + "init.overwriting": "Sobrescribiendo directorio existente: {path}", + "init.services_require": "Servicios requieren componentes: {components}", + "init.compat_errors": "Errores de compatibilidad servicio-componente:", + "init.suggestion_add": ( + "Sugerencia: Agrega componentes faltantes --components {components}" + ), + "init.suggestion_remove": ( + "O quita --components para que los servicios agreguen dependencias automáticamente." + ), + "init.suggestion_interactive": ( + "También puedes usar modo interactivo para agregar dependencias de servicios automáticamente." + ), + "init.auto_detected_scheduler": ( + "Auto-detectado: Scheduler con persistencia {backend}" + ), + "init.auto_added_deps": "Dependencias agregadas automáticamente: {deps}", + "init.auto_added_by_services": "Agregado automáticamente por servicios:", + "init.required_by": "requerido por {services}", + "init.config_title": "Configuración del proyecto", + "init.config_name": "Nombre:", + "init.config_core": "Core:", + "init.config_infra": "Infraestructura:", + "init.config_services": "Servicios:", + "init.component_files": "Archivos de componentes:", + "init.entrypoints": "Puntos de entrada:", + "init.worker_queues": "Colas de Worker:", + "init.dependencies": "Dependencias a instalar:", + "init.confirm_create": "¿Crear este proyecto?", + "init.cancelled": "Creación de proyecto cancelada", + "init.removing_dir": "Eliminando directorio existente: {path}", + "init.creating": "Creando proyecto: {name}", + "init.error": "Error al crear proyecto: {error}", + # ── Interactive: section headers ─────────────────────────────────── + "interactive.component_selection": "Selección de componentes", + "interactive.service_selection": "Selección de servicios", + "interactive.core_included": ( + "Componentes core ({components}) incluidos automáticamente" + ), + "interactive.infra_header": "Componentes de infraestructura:", + "interactive.services_intro": ( + "Los servicios proporcionan lógica de negocio para tu aplicación." + ), + # ── Component descriptions ────────────────────────────────────────── + "component.backend": "Servidor backend FastAPI", + "component.frontend": "Interfaz frontend Flet", + "component.redis": "Redis cache y broker de mensajes", + "component.worker": "Procesamiento de tareas en segundo plano (arq, Dramatiq o TaskIQ)", + "component.scheduler": "Infraestructura de ejecución de tareas programadas", + "component.database": "Base de datos con SQLModel ORM (SQLite o PostgreSQL)", + "component.ingress": "Traefik reverse proxy y balanceador de carga", + "component.observability": "Logfire observabilidad, trazas y métricas", + # ── Service descriptions ──────────────────────────────────────────── + "service.auth": "Autenticación y autorización de usuarios con tokens JWT", + "service.ai": "Servicio de chatbot IA con soporte multi-framework", + "service.comms": "Servicio de comunicaciones con email, SMS y voz", + # ── Interactive: component prompts ───────────────────────────────── + "interactive.add_prompt": "¿Agregar {description}?", + "interactive.add_with_redis": "¿Agregar {description}? (agregará Redis automáticamente)", + "interactive.worker_configured": "Worker con backend {backend} configurado", + # ── Interactive: scheduler ───────────────────────────────────────── + "interactive.scheduler_persistence": "Persistencia de Scheduler:", + "interactive.persist_prompt": ( + "¿Persistir tareas programadas? " + "(Habilita historial de tareas, recuperación tras reinicios)" + ), + "interactive.scheduler_db_configured": "Scheduler + base de datos {engine} configurado", + "interactive.bonus_backup": "Extra: Agregando tarea de respaldo de base de datos", + "interactive.backup_desc": ( + "Tarea de respaldo diario de base de datos incluida (se ejecuta a las 2 AM)" + ), + # ── Interactive: database engine ─────────────────────────────────── + "interactive.db_engine_label": "Motor de base de datos {context}:", + "interactive.db_select": "Selecciona motor de base de datos:", + "interactive.db_sqlite": "SQLite - Simple, basado en archivo (ideal para desarrollo)", + "interactive.db_postgres": ( + "PostgreSQL - Listo para producción, soporte multi-contenedor" + ), + "interactive.db_reuse": "Usando base de datos previamente seleccionada: {engine}", + # ── Interactive: worker backend ──────────────────────────────────── + "interactive.worker_label": "Backend de Worker:", + "interactive.worker_select": "Selecciona backend de worker:", + "interactive.worker_arq": "arq - Async, ligero (predeterminado)", + "interactive.worker_dramatiq": ( + "Dramatiq - Basado en procesos, ideal para trabajo CPU-bound" + ), + "interactive.worker_taskiq": ( + "TaskIQ - Async, estilo framework con brokers por cola" + ), + # ── Interactive: auth ────────────────────────────────────────────── + "interactive.auth_header": "Servicios de autenticación:", + "interactive.auth_level_label": "Nivel de autenticación:", + "interactive.auth_select": "¿Qué tipo de autenticación?", + "interactive.auth_basic": "Básico - Login con email/contraseña", + "interactive.auth_rbac": "Con Roles - + control de acceso basado en roles (experimental)", + "interactive.auth_org": "Con Organizaciones - + soporte multi-tenant (experimental)", + "interactive.auth_selected": "Nivel de auth seleccionado: {level}", + "interactive.auth_db_required": "Base de datos requerida:", + "interactive.auth_db_reason": ( + "Autenticación requiere base de datos para almacenar usuarios" + ), + "interactive.auth_db_details": "(cuentas de usuario, sesiones, tokens JWT)", + "interactive.auth_db_already": "Componente de base de datos ya seleccionado", + "interactive.auth_db_confirm": "¿Continuar y agregar componente de base de datos?", + "interactive.auth_cancelled": "Servicio de autenticación cancelado", + "interactive.auth_db_configured": "Autenticación + Base de datos configurado", + # ── Interactive: AI service ──────────────────────────────────────── + "interactive.ai_header": "Servicios de IA y Machine Learning:", + "interactive.ai_framework_label": "Selección de framework IA:", + "interactive.ai_framework_intro": "Elige tu framework de IA:", + "interactive.ai_pydanticai": ( + "PydanticAI - Framework IA type-safe y Pythónico (recomendado)" + ), + "interactive.ai_langchain": ( + "LangChain - Framework popular con integraciones extensivas" + ), + "interactive.ai_use_pydanticai": "¿Usar PydanticAI? (recomendado)", + "interactive.ai_selected_framework": "Framework seleccionado: {framework}", + "interactive.ai_tracking_context": "Seguimiento de uso de IA", + "interactive.ai_tracking_label": "Seguimiento de uso de LLM:", + "interactive.ai_tracking_prompt": ( + "¿Habilitar seguimiento de uso? (conteo de tokens, costos, historial de conversaciones)" + ), + "interactive.ai_sync_label": "Sincronización de catálogo LLM:", + "interactive.ai_sync_desc": ( + "Sincronizar obtiene datos actualizados de modelos desde APIs de OpenRouter/LiteLLM" + ), + "interactive.ai_sync_time": ("Requiere acceso a red y toma ~30-60 segundos"), + "interactive.ai_sync_prompt": "¿Sincronizar catálogo LLM durante la generación del proyecto?", + "interactive.ai_sync_will": "Catálogo LLM se sincronizará tras la generación del proyecto", + "interactive.ai_sync_skipped": ( + "Sincronización LLM omitida - datos de fixture estáticos disponibles" + ), + "interactive.ai_provider_label": "Selección de proveedor IA:", + "interactive.ai_provider_intro": ( + "Elige proveedores de IA a incluir (selección múltiple permitida)" + ), + "interactive.ai_provider_options": "Opciones de proveedor:", + "interactive.ai_provider_recommended": "(Recomendado)", + "interactive.ai_provider.openai": "OpenAI - Modelos GPT (Pago)", + "interactive.ai_provider.anthropic": "Anthropic - Modelos Claude (Pago)", + "interactive.ai_provider.google": "Google - Modelos Gemini (Nivel gratuito)", + "interactive.ai_provider.groq": "Groq - Inferencia rápida (Nivel gratuito)", + "interactive.ai_provider.mistral": "Mistral - Modelos abiertos (Mayormente pago)", + "interactive.ai_provider.cohere": "Cohere - Enfoque empresarial (Gratuito limitado)", + "interactive.ai_provider.ollama": "Ollama - Inferencia local (Gratuito)", + "interactive.ai_no_providers": ( + "Sin proveedores seleccionados, agregando valores predeterminados..." + ), + "interactive.ai_selected_providers": "Proveedores seleccionados: {providers}", + "interactive.ai_deps_optimized": ("Dependencias optimizadas según tu selección"), + "interactive.ai_ollama_label": "Modo de despliegue de Ollama:", + "interactive.ai_ollama_intro": "¿Cómo quieres ejecutar Ollama?", + "interactive.ai_ollama_host": ( + "Host - Conectar a Ollama en tu máquina (Mac/Windows)" + ), + "interactive.ai_ollama_docker": ( + "Docker - Ejecutar Ollama en contenedor Docker (Linux/Deploy)" + ), + "interactive.ai_ollama_host_prompt": ( + "¿Conectar a Ollama del host? (recomendado para Mac/Windows)" + ), + "interactive.ai_ollama_host_ok": ( + "Ollama se conectará a host.docker.internal:11434" + ), + "interactive.ai_ollama_host_hint": "Asegúrate de que Ollama esté corriendo: ollama serve", + "interactive.ai_ollama_docker_ok": ( + "Servicio Ollama se agregará a docker-compose.yml" + ), + "interactive.ai_ollama_docker_hint": ( + "Nota: El primer inicio puede tardar en descargar modelos" + ), + "interactive.ai_rag_label": "RAG (Retrieval-Augmented Generation):", + "interactive.ai_rag_warning": ( + "Advertencia: RAG requiere Python <3.14 (limitación de chromadb/onnxruntime)" + ), + "interactive.ai_rag_compat_note": ( + "Habilitar RAG generará un proyecto que requiere Python 3.11-3.13" + ), + "interactive.ai_rag_compat_prompt": ( + "¿Habilitar RAG a pesar de incompatibilidad con Python 3.14?" + ), + "interactive.ai_rag_prompt": ( + "¿Habilitar RAG para indexación de documentos y búsqueda semántica?" + ), + "interactive.ai_rag_enabled": "RAG habilitado con almacén vectorial ChromaDB", + "interactive.ai_voice_label": "Voz (Text-to-Speech y Speech-to-Text):", + "interactive.ai_voice_prompt": ( + "¿Habilitar capacidades de voz? (TTS y STT para interacciones por voz)" + ), + "interactive.ai_voice_enabled": "Voz habilitada con soporte TTS y STT", + "interactive.ai_db_already": "Base de datos ya seleccionada - seguimiento de uso habilitado", + "interactive.ai_db_added": "Base de datos ({backend}) agregada para seguimiento de uso", + "interactive.ai_configured": "Servicio de IA configurado", + # ── Shared: validation ────────────────────────────────────────────── + "shared.not_copier_project": "Proyecto en {path} no fue generado con Copier.", + "shared.copier_only": ( + "El comando 'aegis {command}' solo funciona con proyectos generados por Copier." + ), + "shared.regenerate_hint": ( + "Para agregar componentes, regenera el proyecto con los nuevos componentes incluidos." + ), + "shared.git_not_initialized": "Proyecto no está en un repositorio git", + "shared.git_required": "Actualizaciones de Copier requieren git para seguimiento de cambios", + "shared.git_init_hint": ( + "Proyectos creados con 'aegis init' deberían tener git inicializado automáticamente" + ), + "shared.git_manual_init": ( + "Si creaste este proyecto manualmente, ejecuta: " + "git init && git add . && git commit -m 'Initial commit'" + ), + "shared.empty_component": "Nombre de componente vacío no permitido", + "shared.empty_service": "Nombre de servicio vacío no permitido", + # ── Shared: next steps / review ────────────────────────────────── + "shared.next_steps": "Próximos pasos:", + "shared.next_make_check": " 1. Ejecuta 'make check' para verificar la actualización", + "shared.next_test": " 2. Prueba tu aplicación", + "shared.next_commit": " 3. Confirma los cambios con: git add . && git commit", + "shared.review_header": "Revisa los cambios:", + "shared.review_docker": " git diff docker-compose.yml", + "shared.review_pyproject": " git diff pyproject.toml", + "shared.operation_cancelled": "Operación cancelada", + "shared.interactive_ignores_args": ( + "Advertencia: la opción --interactive ignora argumentos de componentes" + ), + "shared.no_components_selected": "Sin componentes seleccionados", + "shared.no_services_selected": "Sin servicios seleccionados", + # ── Add command ────────────────────────────────────────────────── + "add.title": "Aegis Stack - Agregar componentes", + "add.project": "Proyecto: {path}", + "add.error_no_args": ( + "Error: argumento de componentes requerido (o usa --interactive)" + ), + "add.usage_hint": "Uso: aegis add scheduler,worker", + "add.interactive_hint": "O: aegis add --interactive", + "add.auto_added_deps": "Dependencias agregadas automáticamente: {deps}", + "add.validation_failed": "Validación de componentes fallida: {error}", + "add.load_config_failed": "No se pudo cargar configuración del proyecto: {error}", + "add.already_enabled": "Ya habilitado: {components}", + "add.all_enabled": "¡Todos los componentes solicitados ya están habilitados!", + "add.components_to_add": "Componentes a agregar:", + "add.scheduler_backend": "Backend de scheduler: {backend}", + "add.confirm": "¿Agregar estos componentes?", + "add.updating": "Actualizando proyecto...", + "add.adding": "Agregando {component}...", + "add.added_files": "{count} archivos agregados", + "add.skipped_files": "{count} archivos existentes omitidos", + "add.success": "¡Componentes agregados!", + "add.failed_component": "Error al agregar {component}: {error}", + "add.failed": "Error al agregar componentes: {error}", + "add.invalid_format": "Formato de componente inválido: {error}", + "add.bracket_override": ( + "Sintaxis de corchetes 'scheduler[{engine}]' sobrescribe --backend {backend}" + ), + "add.invalid_scheduler_backend": "Backend de scheduler inválido: '{backend}'", + "add.valid_backends": "Opciones válidas: {options}", + "add.postgres_coming": "Nota: Soporte para PostgreSQL disponible en versión futura", + "add.auto_added_db": "Componente de base de datos agregado automáticamente para persistencia de scheduler", + # ── Remove command ──────────────────────────────────────────────── + "remove.title": "Aegis Stack - Eliminar componentes", + "remove.project": "Proyecto: {path}", + "remove.error_no_args": ( + "Error: argumento de componentes requerido (o usa --interactive)" + ), + "remove.usage_hint": "Uso: aegis remove scheduler,worker", + "remove.interactive_hint": "O: aegis remove --interactive", + "remove.no_selected": "Sin componentes seleccionados para eliminar", + "remove.validation_failed": "Validación de componentes fallida: {error}", + "remove.load_config_failed": "No se pudo cargar configuración del proyecto: {error}", + "remove.cannot_remove_core": "No se puede eliminar componente core: {component}", + "remove.not_enabled": "No habilitado: {components}", + "remove.nothing_to_remove": "¡No hay componentes que eliminar!", + "remove.auto_remove_redis": ( + "Eliminando redis automáticamente (sin funcionalidad independiente, solo usado por worker)" + ), + "remove.scheduler_persistence_warn": "IMPORTANTE: Advertencia de persistencia de Scheduler", + "remove.scheduler_persistence_detail": ( + "Tu scheduler usa SQLite para persistencia de tareas." + ), + "remove.scheduler_db_remains": ( + "El archivo de base de datos en data/scheduler.db permanecerá." + ), + "remove.scheduler_keep_hint": ( + "Para conservar historial de tareas: Deja el componente de base de datos" + ), + "remove.scheduler_remove_hint": ( + "Para eliminar todos los datos: Elimina también el componente de base de datos" + ), + "remove.components_to_remove": "Componentes a eliminar:", + "remove.warning_delete": ( + "ADVERTENCIA: ¡Esto ELIMINARÁ archivos de componentes de tu proyecto!" + ), + "remove.commit_hint": "Asegúrate de haber confirmado tus cambios en git.", + "remove.confirm": "¿Eliminar estos componentes?", + "remove.removing_all": "Eliminando componentes...", + "remove.removing": "Eliminando {component}...", + "remove.removed_files": "{count} archivos eliminados", + "remove.failed_component": "Error al eliminar {component}: {error}", + "remove.success": "¡Componentes eliminados!", + "remove.failed": "Error al eliminar componentes: {error}", + # ── Manual updater ───────────────────────────────────────────────── + "updater.processing_files": "Procesando {count} archivos de componentes...", + "updater.updating_shared": "Actualizando archivos de plantilla compartidos...", + "updater.running_postgen": "Ejecutando tareas post-generación...", + "updater.deps_synced": "Dependencias sincronizadas (uv sync)", + "updater.code_formatted": "Código formateado (make fix)", + # ── Project map ────────────────────────────────────────────────── + "projectmap.new": "NUEVO", + # ── Post-generation: setup tasks ────────────────────────────────── + "postgen.setup_start": "Configurando entorno de proyecto...", + "postgen.deps_installing": "Instalando dependencias con uv...", + "postgen.deps_success": "Dependencias instaladas", + "postgen.deps_failed": "Generación de proyecto fallida: instalación de dependencias falló", + "postgen.deps_failed_detail": ( + "Los archivos del proyecto generado permanecen, pero el proyecto no es utilizable." + ), + "postgen.deps_failed_hint": ( + "Corrige el problema de dependencias (verifica compatibilidad de versión de Python) e intenta de nuevo." + ), + "postgen.deps_warn_failed": "Advertencia: Instalación de dependencias falló", + "postgen.deps_manual": "Ejecuta 'uv sync' manualmente tras crear el proyecto", + "postgen.deps_timeout": ( + "Advertencia: Tiempo de espera en instalación de dependencias - ejecuta 'uv sync' manualmente" + ), + "postgen.deps_uv_missing": "Advertencia: uv no encontrado en PATH", + "postgen.deps_uv_install": "Instala uv primero: https://github.com/astral-sh/uv", + "postgen.deps_warn_error": "Advertencia: Instalación de dependencias falló: {error}", + "postgen.env_setup": "Configurando entorno...", + "postgen.env_created": "Archivo de entorno creado desde .env.example", + "postgen.env_exists": "Archivo de entorno ya existe", + "postgen.env_missing": "Advertencia: Archivo .env.example no encontrado", + "postgen.env_error": "Advertencia: Configuración de entorno falló: {error}", + "postgen.env_manual": "Copia .env.example a .env manualmente", + # ── Post-generation: database/migrations ──────────────────────────── + "postgen.db_setup": "Configurando esquema de base de datos...", + "postgen.db_success": "Tablas de base de datos creadas", + "postgen.db_alembic_missing": "Advertencia: Archivo de configuración de Alembic no encontrado en {path}", + "postgen.db_alembic_hint": ( + "Omitiendo migración de base de datos. Asegúrate de que el archivo de configuración exista " + "y ejecuta 'alembic upgrade head' manualmente." + ), + "postgen.db_failed": "Advertencia: Configuración de migración de base de datos falló", + "postgen.db_manual": "Ejecuta 'alembic upgrade head' manualmente tras crear el proyecto", + "postgen.db_timeout": ( + "Advertencia: Tiempo de espera en migración - ejecuta 'alembic upgrade head' manualmente" + ), + "postgen.db_error": "Advertencia: Configuración de migración falló: {error}", + # ── Post-generation: LLM fixtures/sync ──────────────────────────── + "postgen.llm_seeding": "Cargando fixtures de LLM...", + "postgen.llm_seed_success": "Fixtures de LLM cargados", + "postgen.llm_seed_failed": "Advertencia: Carga de fixtures de LLM falló", + "postgen.llm_seed_manual": ( + "Puedes cargar fixtures manualmente ejecutando el cargador de fixtures" + ), + "postgen.llm_seed_timeout": "Advertencia: Tiempo de espera en carga de fixtures de LLM", + "postgen.llm_seed_error": "Advertencia: Carga de fixtures de LLM falló: {error}", + "postgen.llm_syncing": "Sincronizando catálogo LLM desde APIs externas...", + "postgen.llm_sync_success": "Catálogo LLM sincronizado", + "postgen.llm_sync_failed": "Advertencia: Sincronización de catálogo LLM falló", + "postgen.llm_sync_manual": ( + "Ejecuta '{slug} llm sync' manualmente para poblar el catálogo" + ), + "postgen.llm_sync_timeout": "Advertencia: Tiempo de espera en sincronización de catálogo LLM", + "postgen.llm_sync_error": "Advertencia: Sincronización de catálogo LLM falló: {error}", + # ── Post-generation: formatting ─────────────────────────────────── + "postgen.format_timeout": ( + "Advertencia: Tiempo de espera en formateo - ejecuta 'make fix' manualmente" + ), + "postgen.format_error": "Advertencia: Auto-formateo omitido: {error}", + "postgen.format_error_manual": "Ejecuta 'make fix' manualmente para formatear código", + "postgen.format_start": "Auto-formateando código generado...", + "postgen.format_success": "Formateo de código completado", + "postgen.format_partial": ( + "Algunos problemas de formateo detectados, pero proyecto creado" + ), + "postgen.format_manual": "Ejecuta 'make fix' manualmente para resolver problemas restantes", + "postgen.format_hint": "Ejecuta 'make fix' para formatear código", + "postgen.llm_sync_skipped": "Sincronización de catálogo LLM omitida", + "postgen.llm_fixtures_outdated": "Datos de fixture estáticos cargados (pueden estar desactualizados)", + "postgen.llm_sync_hint": "Ejecuta '{slug} llm sync' después para obtener datos actualizados", + "postgen.llm_fixtures_fallback": ( + "Datos de fixture estáticos disponibles pero pueden estar desactualizados" + ), + "postgen.ready": "¡Proyecto listo para ejecutar!", + "postgen.next_steps": "Próximos pasos:", + "postgen.next_cd": " cd {path}", + "postgen.next_serve": " make serve", + "postgen.next_dashboard": " Abrir Overseer: http://localhost:8000/dashboard/", + # ── Post-generation: project map ────────────────────────────────── + "projectmap.title": "Estructura del proyecto:", + "projectmap.components": "Componentes", + "projectmap.services": "Lógica de negocio", + "projectmap.models": "Modelos de base de datos", + "projectmap.cli": "Comandos CLI", + "projectmap.entrypoints": "Puntos de ejecución", + "projectmap.tests": "Suite de pruebas", + "projectmap.migrations": "Migraciones", + "projectmap.auth": "Autenticación", + "projectmap.ai": "Conversaciones IA", + "projectmap.comms": "Comunicaciones", + "projectmap.docs": "Documentación", + # ── Post-generation: footer ─────────────────────────────────────── + "postgen.docs_link": "Docs: https://lbedner.github.io/aegis-stack", + "postgen.star_prompt": ( + "Si Aegis Stack te facilitó la vida, considera dejar una estrella:" + ), + # ── Add-service command ──────────────────────────────────────────── + "add_service.title": "Aegis Stack - Agregar servicios", + "add_service.project": "Proyecto: {path}", + "add_service.error_no_args": ( + "Error: argumento de servicios requerido (o usa --interactive)" + ), + "add_service.usage_hint": "Uso: aegis add-service auth,ai", + "add_service.interactive_hint": "O: aegis add-service --interactive", + "add_service.interactive_ignores_args": ( + "Advertencia: la opción --interactive ignora argumentos de servicios" + ), + "add_service.no_selected": "Sin servicios seleccionados", + "add_service.already_enabled": "Ya habilitado: {services}", + "add_service.all_enabled": "¡Todos los servicios solicitados ya están habilitados!", + "add_service.validation_failed": "Validación de servicios fallida: {error}", + "add_service.load_config_failed": "No se pudo cargar configuración del proyecto: {error}", + "add_service.services_to_add": "Servicios a agregar:", + "add_service.required_components": "Componentes requeridos (se agregarán automáticamente):", + "add_service.already_have_components": ( + "Ya tiene componentes requeridos: {components}" + ), + "add_service.confirm": "¿Agregar estos servicios?", + "add_service.adding_component": "Agregando componente requerido: {component}...", + "add_service.failed_component": "Error al agregar componente {component}: {error}", + "add_service.added_files": "{count} archivos agregados", + "add_service.skipped_files": "{count} archivos existentes omitidos", + "add_service.adding_service": "Agregando servicio: {service}...", + "add_service.failed_service": "Error al agregar servicio {service}: {error}", + "add_service.resolve_failed": "Error al resolver dependencias de servicio: {error}", + "add_service.bootstrap_alembic": "Inicializando infraestructura alembic...", + "add_service.created_file": "Creado: {file}", + "add_service.generated_migration": "Migración generada: {name}", + "add_service.applying_migrations": "Aplicando migraciones de base de datos...", + "add_service.migration_failed": ( + "Advertencia: Migración automática falló. Ejecuta 'make migrate' manualmente." + ), + "add_service.success": "¡Servicios agregados!", + "add_service.failed": "Error al agregar servicios: {error}", + "add_service.auth_setup": "Configuración de Auth:", + "add_service.auth_create_users": " 1. Crear usuarios de prueba: {cmd}", + "add_service.auth_view_routes": " 2. Ver rutas de auth: {url}", + "add_service.ai_setup": "Configuración de IA:", + "add_service.ai_set_provider": ( + " 1. Configura {env_var} en .env (openai, anthropic, google, groq)" + ), + "add_service.ai_set_api_key": " 2. Configura la API key del proveedor ({env_var}, etc.)", + "add_service.ai_test_cli": " 3. Prueba con CLI: {cmd}", + # ── Remove-service command ───────────────────────────────────────── + "remove_service.title": "Aegis Stack - Eliminar servicios", + "remove_service.project": "Proyecto: {path}", + "remove_service.error_no_args": ( + "Error: argumento de servicios requerido (o usa --interactive)" + ), + "remove_service.usage_hint": "Uso: aegis remove-service auth,ai", + "remove_service.interactive_hint": "O: aegis remove-service --interactive", + "remove_service.interactive_ignores_args": ( + "Advertencia: la opción --interactive ignora argumentos de servicios" + ), + "remove_service.no_selected": "Sin servicios seleccionados para eliminar", + "remove_service.not_enabled": "No habilitado: {services}", + "remove_service.nothing_to_remove": "¡No hay servicios que eliminar!", + "remove_service.validation_failed": "Validación de servicios fallida: {error}", + "remove_service.load_config_failed": ( + "No se pudo cargar configuración del proyecto: {error}" + ), + "remove_service.services_to_remove": "Servicios a eliminar:", + "remove_service.auth_warning": "IMPORTANTE: Advertencia de servicio Auth", + "remove_service.auth_delete_intro": "Eliminar servicio auth borrará:", + "remove_service.auth_delete_endpoints": "Endpoints API de autenticación de usuarios", + "remove_service.auth_delete_models": "Modelo de usuario y servicios de autenticación", + "remove_service.auth_delete_jwt": "Código de manejo de tokens JWT", + "remove_service.auth_db_note": ( + "Nota: Tablas de base de datos y migraciones alembic NO se eliminan." + ), + "remove_service.warning_delete": ( + "ADVERTENCIA: ¡Esto ELIMINARÁ archivos de servicios de tu proyecto!" + ), + "remove_service.confirm": "¿Eliminar estos servicios?", + "remove_service.removing": "Eliminando servicio: {service}...", + "remove_service.failed_service": "Error al eliminar servicio {service}: {error}", + "remove_service.removed_files": "{count} archivos eliminados", + "remove_service.success": "¡Servicios eliminados!", + "remove_service.failed": "Error al eliminar servicios: {error}", + "remove_service.deps_not_removed": ( + "Nota: Dependencias de servicios (base de datos, etc.) NO fueron eliminadas." + ), + "remove_service.deps_remove_hint": ( + "Usa 'aegis remove ' para eliminar componentes por separado." + ), + # ── Version command ──────────────────────────────────────────────── + "version.info": "Aegis Stack CLI v{version}", + # ── Components command ───────────────────────────────────────────── + "components.core_title": "COMPONENTES CORE", + "components.backend_desc": ( + " backend - Servidor backend FastAPI (siempre incluido)" + ), + "components.frontend_desc": ( + " frontend - Interfaz frontend Flet (siempre incluido)" + ), + "components.infra_title": "COMPONENTES DE INFRAESTRUCTURA", + "components.requires": "Requiere: {deps}", + "components.recommends": "Recomienda: {deps}", + "components.usage_hint": ( + "Usa 'aegis init NOMBRE_PROYECTO --components redis,worker' para seleccionar componentes" + ), + # ── Services command ─────────────────────────────────────────────── + "services.title": "SERVICIOS DISPONIBLES", + "services.type_auth": "Servicios de autenticación", + "services.type_payment": "Servicios de pago", + "services.type_ai": "Servicios de IA y Machine Learning", + "services.type_notification": "Servicios de notificaciones", + "services.type_analytics": "Servicios de analítica", + "services.type_storage": "Servicios de almacenamiento", + "services.requires_components": "Requiere componentes: {deps}", + "services.recommends_components": "Recomienda componentes: {deps}", + "services.requires_services": "Requiere servicios: {deps}", + "services.none_available": " Sin servicios disponibles aún.", + "services.usage_hint": ( + "Usa 'aegis init NOMBRE_PROYECTO --services auth' para agregar servicios" + ), + # ── Update command ───────────────────────────────────────────────── + "update.title": "Aegis Stack - Actualizar plantilla", + "update.not_copier": "Proyecto en {path} no fue generado con Copier.", + "update.copier_only": ( + "El comando 'aegis update' solo funciona con proyectos generados por Copier." + ), + "update.need_regen": "Proyectos generados antes de v0.2.0 necesitan ser regenerados.", + "update.project": "Proyecto: {path}", + "update.commit_or_stash": ( + "Confirma o guarda tus cambios antes de ejecutar 'aegis update'." + ), + "update.clean_required": ( + "Copier requiere un árbol git limpio para fusionar cambios de forma segura." + ), + "update.git_clean": "Árbol git limpio", + "update.dirty_tree": "Árbol git tiene cambios sin confirmar", + "update.changelog_breaking": "Cambios incompatibles:", + "update.changelog_features": "Nuevas funcionalidades:", + "update.changelog_fixes": "Correcciones:", + "update.changelog_other": "Otros cambios:", + "update.current_commit": " Actual: {commit}...", + "update.target_commit": " Destino: {commit}...", + "update.unknown_version": "Advertencia: No se puede determinar versión actual de plantilla", + "update.untagged_commit": ( + "Proyecto puede haber sido generado desde un commit sin etiqueta" + ), + "update.custom_template": "Usando plantilla personalizada ({source}): {path}", + "update.version_info": "Información de versión:", + "update.current_cli": " CLI actual: {version}", + "update.current_template": " Plantilla actual: {version}", + "update.current_template_commit": " Plantilla actual: {commit}... (commit)", + "update.current_template_unknown": " Plantilla actual: desconocida", + "update.target_template": " Plantilla destino: {version}", + "update.already_at_version": "Proyecto ya está en la versión solicitada", + "update.already_at_commit": "Proyecto ya está en el commit destino", + "update.downgrade_blocked": "Degradación no soportada", + "update.downgrade_reason": ( + "Copier no soporta degradación a versiones anteriores de plantilla." + ), + "update.changelog": "Registro de cambios:", + "update.dry_run": "MODO DE PRUEBA - No se aplicarán cambios", + "update.dry_run_hint": "Para aplicar esta actualización, ejecuta:", + "update.confirm": "¿Aplicar esta actualización?", + "update.cancelled": "Actualización cancelada", + "update.creating_backup": "Creando punto de respaldo...", + "update.backup_created": " Respaldo creado: {tag}", + "update.backup_failed": "No se pudo crear punto de respaldo", + "update.updating": "Actualizando proyecto...", + "update.updating_to": "Actualizando a versión de plantilla {version}", + "update.moved_files": " {count} archivos nuevos movidos desde directorio anidado", + "update.synced_files": " {count} cambios de plantilla sincronizados", + "update.merge_conflicts": ( + " {count} archivo(s) tienen conflictos de fusión (busca <<<<<<< para resolver):" + ), + "update.running_postgen": "Ejecutando tareas post-generación...", + "update.version_updated": " __aegis_version__ actualizado a {version}", + "update.success": "¡Actualización completada!", + "update.partial_success": ( + "Actualización completada con algunos fallos en tareas post-generación" + ), + "update.partial_detail": " Algunas tareas de configuración fallaron. Ver detalles arriba.", + "update.next_steps": "Próximos pasos:", + "update.next_review": " 1. Revisa cambios: git diff", + "update.next_conflicts": " 2. Verifica conflictos (archivos *.rej)", + "update.next_test": " 3. Ejecuta pruebas: make check", + "update.next_commit": " 4. Confirma cambios: git add . && git commit", + "update.failed": "Actualización falló: {error}", + "update.rollback_prompt": "¿Revertir al estado anterior?", + "update.manual_rollback": "Reversión manual: git reset --hard {tag}", + "update.troubleshooting": "Solución de problemas:", + "update.troubleshoot_clean": " - Asegúrate de tener un árbol git limpio", + "update.troubleshoot_version": " - Verifica que la versión/commit exista", + "update.troubleshoot_docs": " - Revisa documentación de Copier para problemas de actualización", + # ── Ingress command ──────────────────────────────────────────────── + "ingress.title": "Aegis Stack - Habilitar Ingress TLS", + "ingress.project": "Proyecto: {path}", + "ingress.not_found": "Componente ingress no encontrado. Agregándolo primero...", + "ingress.add_confirm": "¿Agregar componente ingress?", + "ingress.add_failed": "Error al agregar componente ingress: {error}", + "ingress.added": "Componente ingress agregado.", + "ingress.tls_already": "TLS ya está habilitado en este proyecto.", + "ingress.domain_label": " Dominio: {domain}", + "ingress.acme_email": " Email ACME: {email}", + "ingress.domain_prompt": ( + "Nombre de dominio (ej: example.com, o vacío para enrutamiento por IP)" + ), + "ingress.email_reuse": "Usando email existente para ACME: {email}", + "ingress.email_prompt": "Email para notificaciones de Let's Encrypt", + "ingress.email_required": ( + "Error: --email requerido para TLS (necesario para Let's Encrypt)" + ), + "ingress.tls_config": "Configuración TLS:", + "ingress.domain_none": " Dominio: (ninguno - enrutamiento por IP/PathPrefix)", + "ingress.tls_confirm": "¿Habilitar TLS con esta configuración?", + "ingress.enabling": "Habilitando TLS...", + "ingress.updated_file": " Actualizado: {file}", + "ingress.created_file": " Creado: {file}", + "ingress.success": "¡TLS habilitado!", + "ingress.available_at": " Tu app estará disponible en: https://{domain}", + "ingress.https_configured": " HTTPS configurado con Let's Encrypt", + "ingress.next_steps": "Próximos pasos:", + "ingress.next_deploy": " 1. Despliega con: aegis deploy", + "ingress.next_ports": " 2. Asegúrate de que los puertos 80 y 443 estén abiertos en tu servidor", + "ingress.next_dns": ( + " 3. Apunta el registro DNS A de {domain} a la IP de tu servidor" + ), + "ingress.next_certs": " Certificados se aprovisionarán automáticamente en la primera solicitud", + # ── Deploy commands ──────────────────────────────────────────────── + "deploy.no_config": ( + "Sin configuración de despliegue. Ejecuta 'aegis deploy-init' primero." + ), + "deploy.init_saved": "Configuración de despliegue guardada en {file}", + "deploy.init_host": " Host: {host}", + "deploy.init_user": " Usuario: {user}", + "deploy.init_path": " Ruta: {path}", + "deploy.init_docker_context": " Docker Context: {context}", + "deploy.prompt_host": "IP o hostname del servidor", + "deploy.init_gitignore": ( + "Nota: Considera agregar .aegis/ a .gitignore para evitar confirmar config de despliegue" + ), + "deploy.setup_title": "Configurando servidor en {target}...", + "deploy.checking_ssh": "Verificando conectividad SSH...", + "deploy.adding_host_key": "Agregando servidor a known_hosts...", + "deploy.ssh_keyscan_failed": "Error al escanear clave SSH del host: {error}", + "deploy.ssh_failed": "Conexión SSH falló: {error}", + "deploy.copying_script": "Copiando script de configuración al servidor...", + "deploy.copy_failed": "Error al copiar script de configuración", + "deploy.running_setup": "Ejecutando configuración del servidor (puede tardar unos minutos)...", + "deploy.setup_failed": "Configuración del servidor falló", + "deploy.setup_script_missing": "Script de configuración del servidor no encontrado: {path}", + "deploy.setup_script_hint": ( + "Asegúrate de que tu proyecto fue creado con el componente ingress." + ), + "deploy.setup_complete": "¡Configuración del servidor completada!", + "deploy.setup_verify": "Verificando instalación:", + "deploy.setup_verify_docker": " Docker: {version}", + "deploy.setup_verify_compose": " Docker Compose: {version}", + "deploy.setup_verify_uv": " uv: {version}", + "deploy.setup_verify_app_dir": " Directorio de app: {path}", + "deploy.setup_next": "Siguiente: Ejecuta 'aegis deploy' para desplegar tu aplicación", + "deploy.deploying": "Desplegando en {host}...", + "deploy.creating_backup": "Creando respaldo {timestamp}...", + "deploy.backup_failed": "Error al crear respaldo: {error}", + "deploy.backup_db": "Respaldando base de datos PostgreSQL...", + "deploy.backup_db_failed": ( + "Advertencia: Respaldo de base de datos falló, continuando sin él" + ), + "deploy.backup_created": "Respaldo creado: {timestamp}", + "deploy.backup_pruned": "Respaldo antiguo eliminado: {name}", + "deploy.no_existing": "Sin despliegue existente, omitiendo respaldo", + "deploy.syncing": "Sincronizando archivos al servidor...", + "deploy.mkdir_failed": "Error al crear directorio remoto '{path}'", + "deploy.sync_failed": "Error al sincronizar archivos", + "deploy.copying_env": "Copiando {file} al servidor como .env...", + "deploy.env_copy_failed": "Error al copiar archivo .env", + "deploy.stopping": "Deteniendo servicios existentes...", + "deploy.building": "Construyendo e iniciando servicios en servidor...", + "deploy.start_failed": "Error al iniciar servicios", + "deploy.auto_rollback": "Revirtiendo automáticamente a versión anterior...", + "deploy.health_waiting": "Esperando estabilización de contenedores...", + "deploy.health_attempt": "Verificación de salud intento {n}/{total}...", + "deploy.health_passed": "Verificación de salud aprobada", + "deploy.health_retry": "Verificación de salud falló, reintentando en {interval}s...", + "deploy.health_all_failed": "Todas las verificaciones de salud fallaron", + "deploy.rolled_back": "Revertido a respaldo {timestamp}", + "deploy.rollback_failed": "¡Reversión falló! Intervención manual requerida.", + "deploy.health_failed_hint": ( + "Despliegue completado pero verificación de salud falló. Revisa logs con: aegis deploy-logs" + ), + "deploy.complete": "¡Despliegue completado!", + "deploy.app_running": " Aplicación corriendo en: http://{host}", + "deploy.overseer": " Dashboard Overseer: http://{host}/dashboard/", + "deploy.view_logs": " Ver logs: aegis deploy-logs", + "deploy.check_status": " Verificar estado: aegis deploy-status", + "deploy.backup_complete": "¡Respaldo completado!", + "deploy.creating_backup_on": "Creando respaldo en {host}...", + "deploy.no_backups": "Sin respaldos encontrados.", + "deploy.backups_header": "Respaldos en {host} ({count} total):", + "deploy.col_timestamp": "Fecha/Hora", + "deploy.col_size": "Tamaño", + "deploy.col_database": "Base de datos", + "deploy.rollback_hint": ( + "Revertir con: aegis deploy-rollback --backup " + ), + "deploy.no_backups_available": "Sin respaldos disponibles.", + "deploy.rolling_back": "Revirtiendo a respaldo {backup} en {host}...", + "deploy.rollback_not_found": "Respaldo no encontrado: {timestamp}", + "deploy.rollback_stopping": "Deteniendo servicios...", + "deploy.rollback_restoring": "Restaurando archivos desde respaldo {timestamp}...", + "deploy.rollback_restore_failed": "Error al restaurar archivos: {error}", + "deploy.rollback_db": "Restaurando base de datos...", + "deploy.rollback_pg_wait": "Esperando que PostgreSQL esté listo...", + "deploy.rollback_pg_timeout": ( + "PostgreSQL no quedó listo, intentando restauración de todas formas" + ), + "deploy.rollback_db_failed": "Advertencia: Restauración de base de datos falló", + "deploy.rollback_starting": "Iniciando servicios...", + "deploy.rollback_start_failed": "Error al iniciar servicios tras reversión", + "deploy.rollback_complete": "¡Reversión completada!", + "deploy.rollback_failed_final": "¡Reversión falló!", + "deploy.status_header": "Estado de servicios en {host}:", + "deploy.stop_stopping": "Deteniendo servicios...", + "deploy.stop_success": "Servicios detenidos", + "deploy.stop_failed": "Error al detener servicios", + "deploy.restart_restarting": "Reiniciando servicios...", + "deploy.restart_success": "Servicios reiniciados", + "deploy.restart_failed": "Error al reiniciar servicios", +} diff --git a/aegis/i18n/registry.py b/aegis/i18n/registry.py index c883d909..6f1c207e 100644 --- a/aegis/i18n/registry.py +++ b/aegis/i18n/registry.py @@ -75,6 +75,10 @@ def _load_locale(locale: str) -> None: from .locales.de import MESSAGES _messages["de"] = MESSAGES + elif locale == "es": + from .locales.es import MESSAGES + + _messages["es"] = MESSAGES elif locale == "fr": from .locales.fr import MESSAGES diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/__init__.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/__init__.py index 90e031e7..ec4ade08 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/__init__.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/__init__.py @@ -1,3 +1,13 @@ """Available locales for the project CLI.""" -AVAILABLE_LOCALES: set[str] = {"de", "en", "fr", "ja", "ko", "ru", "zh", "zh_Hant"} +AVAILABLE_LOCALES: set[str] = { + "de", + "en", + "es", + "fr", + "ja", + "ko", + "ru", + "zh", + "zh_Hant", +} diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/de.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/de.py index 51b7ee47..b1a592b9 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/de.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/de.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "Sprache '{lang}' nicht unterstützt. Verfügbar: {available}", # Help text "main.help": "Projekt-Management-CLI", - "main.opt_lang": "Ausgabesprache (de, en, fr, ja, ko, ru, zh, zh_Hant). Standard: automatisch", + "main.opt_lang": "Ausgabesprache (de, en, es, fr, ja, ko, ru, zh, zh_Hant). Standard: automatisch", # ── Health ──────────────────────────────────────────────────────── "health.count_healthy": "{count} gesund", "health.count_warning": "{count} Warnung", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/en.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/en.py index 4f6705f9..4bf3a649 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/en.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/en.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "Unsupported language '{lang}'. Available: {available}", # Help text "main.help": "Project management CLI", - "main.opt_lang": "Output language (de, en, fr, ja, ko, ru, zh, zh_Hant). Default: auto-detect", + "main.opt_lang": "Output language (de, en, es, fr, ja, ko, ru, zh, zh_Hant). Default: auto-detect", # ── Health ──────────────────────────────────────────────────────── "health.count_healthy": "{count} healthy", "health.count_warning": "{count} warning", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/es.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/es.py new file mode 100644 index 00000000..7b1c9b98 --- /dev/null +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/es.py @@ -0,0 +1,935 @@ +"""Spanish locale — Definiciones de mensajes en español.""" + +MESSAGES: dict[str, str] = { + # ── Compartido ─────────────────────────────────────────────────── + "shared.error": "Error:", + "shared.cancelled": "Cancelado.", + "shared.yes": "Sí", + "shared.no": "No", + "shared.none": "ninguno", + "shared.unknown": "Desconocido", + "shared.show_help_exit": "Mostrar este mensaje y salir.", + # ── CLI principal ──────────────────────────────────────────────── + "main.unsupported_lang": "Idioma no soportado '{lang}'. Disponibles: {available}", + # Texto de ayuda + "main.help": "CLI de gestión del proyecto", + "main.opt_lang": "Idioma de salida (de, en, es, fr, ja, ko, ru, zh, zh_Hant). Por defecto: auto-detectar", + # ── Health ──────────────────────────────────────────────────────── + "health.count_healthy": "{count} saludable(s)", + "health.count_warning": "{count} advertencia", + "health.count_warnings": "{count} advertencias", + "health.count_info": "{count} info", + "health.count_unhealthy": "{count} no saludable(s)", + "health.zero_components": "0 componentes", + "health.task_statistics": "Estadísticas de tareas:", + "health.task_stats_line": "Total: {total}, Activas: {active}, Pausadas: {paused}", + "health.upcoming_tasks": "Próximas tareas:", + "health.task_next_run": "{name} - Próxima: {time}", + "health.more_tasks": "... y {count} {word} más", + "health.task_word": "tarea", + "health.tasks_word": "tareas", + "health.no_upcoming_tasks": "Sin tareas programadas próximamente", + "health.connect_error": ( + "No se puede conectar al servidor API en {url}. " + "Asegúrate de que la aplicación esté corriendo con " + "'make serve' o 'make run-dev'." + ), + "health.timeout_error": "Solicitud API a {url} expiró tras {timeout} segundos.", + "health.system_unhealthy": "Sistema no saludable", + "health.unhealthy_label": "No saludable: {components}", + "health.health_pct_label": "Salud: {pct}", + "health.api_error": "API devolvió error {status}: {text}", + "health.status_failed": "Verificación de salud falló: {error}", + "health.system_healthy": "Sistema saludable", + "health.system_unhealthy_msg": "Sistema no saludable", + "health.probe_failed": "Sonda de salud falló: {error}", + "health.title": "Salud del sistema", + "health.healthy": "Saludable", + "health.unhealthy": "No saludable", + "health.overall_status": "Estado general:", + "health.health_percentage": "Porcentaje de salud:", + "health.components_label": "Componentes:", + "health.services_label": "Servicios:", + "health.timestamp_label": "Marca de tiempo:", + "health.component_tree": "Árbol de componentes", + "health.component_service_tree": "Árbol de componentes y servicios", + "health.system_info": "Información del sistema", + "health.sysinfo.python_version": "Versión de Python", + "health.sysinfo.platform": "Plataforma", + "health.sysinfo.containerized": "Contenedorizado", + # Texto de ayuda + "health.help": "Comandos de monitoreo de salud del sistema", + "health.help_status": "Mostrar estado de salud del sistema (siempre sale con 0 para inspección). Usa --detailed para metadatos de componentes y tiempos de respuesta.", + "health.help_probe": "Sonda de salud para monitoreo — sale con 1 si no es saludable (como sondas k8s).", + "health.opt_detailed": "Mostrar información detallada de componentes", + "health.opt_json": "Salida en formato JSON", + # Mensajes del servicio de salud (traducidos al mostrar desde respuestas API) + "health.msg.aegis_ok": "Aplicación Aegis Stack", + "health.msg.aegis_issues": "Aegis Stack tiene problemas", + "health.msg.components_ok": "{count} componentes disponibles", + "health.msg.components_issues": "Algunos componentes tienen problemas", + "health.msg.services_ok": "{count} servicios disponibles", + "health.msg.services_issues": "Algunos servicios tienen problemas", + "health.msg.system_ok": "Métricas de contenedor del sistema", + "health.msg.system_issues": "Contenedor del sistema tiene problemas", + "health.msg.db_ok": "Conexión a base de datos establecida", + "health.msg.db_module_missing": "Módulo de base de datos no disponible", + "health.msg.db_not_reachable": "Servidor PostgreSQL no alcanzable", + "health.msg.db_auth_failed": "Autenticación de PostgreSQL falló", + "health.msg.db_not_exist": "Base de datos PostgreSQL no existe", + "health.msg.db_failed": "Conexión a base de datos falló", + "health.msg.db_not_init": "Base de datos no inicializada - archivo no existe", + "health.msg.db_not_accessible": "Archivo de base de datos no accesible", + "health.msg.cache_ok": "Conexión y operaciones de caché Redis correctas", + "health.msg.cache_not_installed": "Librería de caché no instalada", + "health.msg.memory_usage": "Uso de memoria: {pct}%", + "health.msg.disk_usage": "Uso de disco: {pct}%", + "health.msg.cpu_usage": "Uso de CPU: {pct}%", + "health.msg.scheduler_running": "Scheduler corriendo con {count} tareas", + "health.msg.backend_active": "Backend FastAPI activo", + "health.msg.worker_infra": "Infraestructura de worker {backend}", + "health.msg.workers_active": "{active}/{total} workers activos", + "health.msg.queues_configured": "{count} colas funcionales configuradas", + "health.msg.queues_active": "({count} activas)", + "health.msg.no_active_workers": "Sin workers activos", + "health.msg.idle": "inactivo", + "health.msg.processing": "{count} procesando", + "health.msg.queued": "{count} en cola", + "health.msg.completed": "{count} completados", + "health.msg.no_functions": "configurado - sin funciones definidas", + "health.msg.auth_ok": "Servicio auth configurado y listo", + "health.msg.ai_ready": "Servicio AI listo", + "health.msg.ai_disabled": "Servicio AI deshabilitado", + "health.msg.comms_none": "Sin proveedores de comunicación configurados", + "health.msg.comms_partial": "Servicio de comunicaciones parcialmente configurado", + "health.msg.comms_ok": "Servicio de comunicaciones completamente configurado", + "health.msg.ollama_not_reachable": "Servidor Ollama no alcanzable", + "health.msg.ollama_no_models": "Ollama corriendo pero sin modelos instalados", + "health.msg.worker_offline": "worker fuera de línea - sin datos de health check", + "health.msg.failed": "{count} fallidos ({pct}%)", + # Descripciones de colas + "health.msg.queue.load_test": "Pruebas de carga y rendimiento", + "health.msg.queue.media": "Procesamiento de imágenes y archivos", + "health.msg.queue.system": "Tareas de mantenimiento y monitoreo del sistema", + # ── RAG ─────────────────────────────────────────────────────────── + # Gestión de modelos + "rag.openai_key_missing": "Clave API de OpenAI no configurada.", + "rag.openai_key_hint": "Establece la variable de entorno OPENAI_API_KEY para usar embeddings de OpenAI.", + "rag.openai_no_download": "Los embeddings de OpenAI no requieren descarga de modelo local.", + "rag.model_not_found": "Modelo de embedding no encontrado localmente.", + "rag.model_info": "Modelo: {model} (~400MB de descarga)", + "rag.downloading_model": "Descargando modelo de embedding (configuración inicial)...", + "rag.downloading_named_model": "Descargando {model}...", + "rag.model_downloaded": "Modelo descargado", + "rag.model_download_failed": "Fallo al descargar modelo: {error}", + "rag.model_download_hint": "Intenta ejecutar: rag install-model", + "rag.model_installed": "Instalado", + "rag.model_not_installed": "No instalado", + "rag.run_install_model": "ejecutar: rag install-model", + "rag.loading_from_cache": "Cargando modelo desde caché...", + "rag.model_found_in_cache": "Modelo encontrado en caché", + "rag.model_ready_title": "Modelo listo", + "rag.model_install_complete_title": "Instalación de modelo completada", + "rag.system_hf_cache": "caché HuggingFace del sistema", + "rag.cache_dir_hint": "Para usar este caché, establece RAG_MODEL_CACHE_DIR={dir}", + # Etiquetas + "rag.indexing_label": "Indexando:", + "rag.collection_label": "Colección:", + "rag.extensions_label": "Extensiones:", + "rag.adding_label": "Agregando:", + "rag.searching_label": "Buscando:", + "rag.model_label": "Modelo:", + "rag.cache_dir_label": "Directorio de caché:", + "rag.location_label": "Ubicación:", + "rag.duration_label": "Duración:", + "rag.throughput_label": "Rendimiento:", + "rag.collection_size_label": "Tamaño de colección:", + "rag.chunks_label": "Chunks:", + "rag.hash_label": "Hash:", + "rag.ids_label": "IDs:", + "rag.total_label": "Total:", + "rag.score_label": "Puntaje:", + "rag.enabled_label": "Habilitado:", + "rag.persist_dir_label": "Directorio de persistencia:", + "rag.embedding_model_label": "Modelo de embedding:", + "rag.model_status_label": "Estado del modelo:", + "rag.chunk_size_label": "Tamaño de chunk:", + "rag.chunk_overlap_label": "Solapamiento de chunk:", + "rag.default_top_k_label": "Top K por defecto:", + "rag.collections_count_label": "Colecciones:", + "rag.last_activity_label": "Última actividad:", + # Progreso + "rag.indexing_progress": "Indexando...", + "rag.loading_documents": "Cargando documentos...", + "rag.batch_progress": "Lote {batch}/{total} ({chunks} chunks)", + # Fases + "rag.loading_phase": "Cargando:", + "rag.chunking_phase": "Fragmentando:", + "rag.indexing_phase": "Indexando:", + # Unidades + "rag.chunks_per_sec": "chunks/seg", + "rag.chunks_unit": "chunks", + # Resultados de índice + "rag.index_success": "Indexados {chunks} chunks de {files} archivos", + "rag.collection_title": "Colección", + # Resultados de agregar + "rag.added_updated": "Agregado/actualizado:", + # Eliminar + "rag.confirm_remove": "¿Eliminar todos los chunks de '{source}' en '{collection}'?", + "rag.removed_chunks": "Eliminados {count} chunks para:", + "rag.no_chunks_found": "Sin chunks encontrados para:", + "rag.files_hint": "Tip: Usa 'rag files --collection ' para ver rutas indexadas.", + # Archivos + "rag.no_files_in_collection": "Sin archivos indexados en colección:", + "rag.index_hint": ( + "Tip: Usa 'rag index --collection ' o " + "'rag add --collection ' para indexar contenido." + ), + "rag.indexed_files_title": "Archivos indexados: {collection}", + "rag.file_column": "Archivo", + "rag.chunks_column": "Chunks", + "rag.files_and_chunks": "{files} archivos, {chunks} chunks", + # Búsqueda + "rag.no_results": "Sin resultados.", + "rag.search_hint": "Tip: Asegúrate de que la colección exista y contenga documentos.", + "rag.found_results": "Encontrados {count} resultados:", + # Colecciones + "rag.no_collections": "Sin colecciones.", + "rag.create_collection_hint": "Tip: Usa 'rag index --collection ' para crear una colección.", + "rag.collections_title": "Colecciones RAG", + "rag.collection_column": "Colección", + "rag.documents_column": "Documentos", + # Eliminar colección + "rag.confirm_delete": "¿Seguro que deseas eliminar la colección '{collection}'?", + "rag.deleted_collection": "Colección eliminada:", + "rag.collection_not_found": "Colección no encontrada:", + # Estado + "rag.status_title": "Estado del servicio RAG", + # Errores + "rag.path_not_found": "Ruta no encontrada: {path}", + "rag.file_not_found": "Archivo no encontrado: {path}", + # Texto de ayuda + "rag.help": "Comandos del servicio RAG para indexación y búsqueda de documentos", + "rag.help_index": "Indexar documentos desde una ruta en una colección para búsqueda semántica.", + "rag.help_add": "Agregar o actualizar un archivo en la colección (semántica upsert).", + "rag.help_remove": "Eliminar los chunks de un archivo de la colección.", + "rag.help_files": "Listar archivos indexados en una colección con conteo de chunks.", + "rag.help_search": "Buscar documentos en una colección usando búsqueda semántica.", + "rag.help_list": "Listar colecciones en el vector store con conteo de documentos.", + "rag.help_delete": "Eliminar una colección y todos sus documentos del vector store.", + "rag.help_status": "Mostrar estado y configuración del servicio RAG.", + "rag.help_install_model": "Pre-descargar modelo de embedding para operación offline/air-gapped.", + "rag.arg_path": "Ruta de archivo o directorio a indexar", + "rag.arg_file_path": "Ruta de archivo a agregar/actualizar", + "rag.arg_source_path": "Ruta origen del archivo a eliminar (como se almacenó en metadatos)", + "rag.arg_query": "Consulta de búsqueda", + "rag.arg_collection": "Nombre de colección a eliminar", + "rag.opt_collection": "Nombre de colección", + "rag.opt_collection_search": "Colección donde buscar", + "rag.opt_extensions": "Extensiones de archivo separadas por coma (ej., .py,.md)", + "rag.opt_show_ids": "Mostrar IDs de chunks en la salida", + "rag.opt_force": "Omitir confirmación", + "rag.opt_top_k": "Número de resultados", + "rag.opt_content": "Mostrar contenido completo de resultados", + "rag.opt_cache_dir": "Directorio para caché del modelo (por defecto: caché HuggingFace del sistema)", + "rag.opt_model": "Nombre del modelo a descargar (por defecto: desde configuración)", + # ── LLM ─────────────────────────────────────────────────────────── + "llm.vendor_column": "Proveedor", + "llm.vendor_label": "Proveedor:", + "llm.models_column": "Modelos", + "llm.model_label": "Modelo:", + "llm.model_id_label": "ID de modelo:", + "llm.model_id_column": "ID de modelo", + "llm.modalities_label": "Modalidades:", + "llm.metric_column": "Métrica", + "llm.count_column": "Cantidad", + "llm.tokens_unit": "tokens", + "llm.per_million_tokens": "/ 1M tokens", + # Sincronización + "llm.dry_run_mode": "Modo simulación - no se guardarán cambios", + "llm.refresh_dry_run": "Refresh solicitado - truncaría todas las tablas LLM", + "llm.syncing_ollama": "Sincronizando modelos Ollama", + "llm.syncing_catalog_all": "Sincronizando catálogo LLM (modo={mode}, fuente=todas)", + "llm.syncing_catalog": "Sincronizando catálogo LLM (modo={mode})", + "llm.sync_results": "Resultados de sincronización", + "llm.sync_results_dry_run": "Resultados de sincronización (simulación)", + "llm.vendors_added": "Proveedores agregados", + "llm.vendors_updated": "Proveedores actualizados", + "llm.models_added": "Modelos agregados", + "llm.models_updated": "Modelos actualizados", + "llm.deployments_synced": "Deployments sincronizados", + "llm.prices_synced": "Precios sincronizados", + "llm.modalities_synced": "Modalidades sincronizadas", + "llm.duration_row": "Duración", + "llm.errors_row": "Errores", + "llm.errors_header": "Errores:", + "llm.and_more_errors": "... y {count} más", + # Proveedores + "llm.no_vendors": "Sin proveedores. Ejecuta 'llm sync' primero.", + "llm.vendors_title": "Proveedores LLM ({count} en total)", + # Modalidades + "llm.no_modalities": "Sin modalidades. Ejecuta 'llm sync' primero.", + "llm.modalities_title": "Modalidades LLM ({count} en total)", + "llm.modality_column": "Modalidad", + # Listar + "llm.provide_filter": "Proporciona un patrón de búsqueda o filtro.", + "llm.no_models_found": "Sin modelos que coincidan con tu criterio.", + "llm.models_title": "Modelos LLM ({count} resultados)", + "llm.context_column": "Contexto", + "llm.input_price_column": "Entrada $/1M", + "llm.output_price_column": "Salida $/1M", + "llm.released_column": "Lanzado", + # Configuración actual + "llm.current_config": "Configuración LLM actual", + "llm.provider_label": "Proveedor:", + "llm.temperature_label": "Temperatura:", + "llm.max_tokens_label": "Tokens máximos:", + "llm.model_details": "Detalles del modelo (desde catálogo)", + "llm.context_window_label": "Ventana de contexto:", + "llm.input_price_label": "Precio entrada:", + "llm.output_price_label": "Precio salida:", + "llm.model_not_in_catalog": "Modelo no encontrado en catálogo. Ejecuta 'llm sync' para poblar.", + # Usar + "llm.switching_model": "Cambiando a {model_id}", + # Info + "llm.model_not_found": "Modelo '{model_id}' no encontrado en catálogo.", + "llm.run_sync_hint": "Ejecuta 'llm sync' para poblar el catálogo.", + "llm.description_label": "Descripción:", + "llm.streamable_label": "Streaming:", + "llm.enabled_label": "Habilitado:", + "llm.released_label": "Lanzado:", + "llm.pricing_header": "Precios (por 1M tokens)", + "llm.input_label": "Entrada:", + "llm.output_label": "Salida:", + # Estadísticas de catálogo + "llm.catalog_summary": "Resumen del catálogo LLM", + "llm.vendors_row": "Proveedores", + "llm.models_row": "Modelos", + "llm.deployments_row": "Deployments", + "llm.prices_row": "Precios", + "llm.top_vendors": "Proveedores principales por cantidad de modelos", + # Texto de ayuda + "llm.help": "Comandos de catálogo y gestión de modelos LLM", + "llm.help_sync": "Sincronizar catálogo LLM desde APIs en la nube u Ollama local a la base de datos local.", + "llm.help_status": "Mostrar estadísticas del catálogo LLM (proveedores, modelos, deployments, precios).", + "llm.help_vendors": "Listar proveedores LLM en el catálogo con conteo de modelos.", + "llm.help_modalities": "Listar modalidades en el catálogo con conteo de modelos.", + "llm.help_list": "Listar modelos LLM del catálogo. Requiere patrón de búsqueda o filtro.", + "llm.help_current": "Mostrar configuración LLM actual desde .env, enriquecida con datos del catálogo.", + "llm.help_use": "Cambiar a otro modelo LLM. Actualiza AI_MODEL en .env.", + "llm.help_info": "Mostrar información detallada de un modelo LLM específico.", + "llm.opt_mode": "Filtro de modo: 'chat', 'embedding', o 'all'", + "llm.opt_source": "Fuente de datos: 'cloud' (OpenRouter/LiteLLM), 'ollama', o 'all'", + "llm.opt_dry_run": "Previsualizar cambios sin modificar la base de datos", + "llm.opt_refresh": "Truncar todas las tablas LLM antes de sincronizar (refresh completo)", + "llm.arg_pattern": "Patrón de búsqueda para ID o título de modelo (no distingue mayúsculas)", + "llm.opt_limit": "Número máximo de resultados", + "llm.opt_all": "Incluir modelos deshabilitados", + "llm.arg_model_id_use": "ID de modelo a establecer como activo (ej., gpt-4o, claude-sonnet-4-20250514)", + "llm.opt_force": "Omitir validación de catálogo y permitir cualquier cadena de modelo", + "llm.arg_model_id_info": "ID de modelo para obtener información", + # ── Comms ───────────────────────────────────────────────────────── + # Comando de estado + "comms.service_status_title": "Estado del servicio de comunicaciones", + "comms.email_resend_header": "Email (Resend)", + "comms.sms_twilio_header": "SMS (Twilio)", + "comms.voice_twilio_header": "Voz (Twilio)", + "comms.status_label": "Estado:", + "comms.configured": "Configurado", + "comms.not_configured": "No configurado", + "comms.set": "Establecido", + "comms.not_set": "No establecido", + "comms.api_key_label": "Clave API:", + "comms.from_email_label": "Email remitente:", + "comms.account_sid_label": "Account SID:", + "comms.auth_token_label": "Auth Token:", + "comms.messaging_service_label": "Servicio de mensajería:", + "comms.phone_number_label": "Número de teléfono:", + "comms.warning": "Advertencia: {message}", + "comms.services_configured_summary": "{count}/3 servicios configurados", + "comms.quick_start": "Inicio rápido:", + "comms.email_signup_hint": "Email: Regístrate en https://resend.com (plan gratuito disponible)", + "comms.twilio_signup_hint": "SMS/Voz: Regístrate en https://twilio.com/try-twilio (prueba gratuita)", + # Envío de email + "comms.text_or_html_required": "Error: Se requiere --text o --html", + "comms.email_sent": "Email enviado.", + "comms.message_id_label": "ID de mensaje:", + "comms.to_label": "Para:", + "comms.subject_label": "Asunto:", + "comms.configuration_error": "Error de configuración: {error}", + "comms.email_send_failed": "Fallo al enviar email: {error}", + # Envío de SMS + "comms.sms_sent": "SMS enviado.", + "comms.message_sid_label": "SID de mensaje:", + "comms.segments_label": "Segmentos:", + "comms.sms_send_failed": "Fallo al enviar SMS: {error}", + # Llamada de voz + "comms.call_initiated": "Llamada iniciada.", + "comms.call_sid_label": "SID de llamada:", + "comms.call_status_label": "Estado:", + "comms.call_failed": "Fallo al hacer llamada: {error}", + # Proveedores + "comms.providers_title": "Proveedores de comunicaciones", + "comms.channel_column": "Canal", + "comms.provider_column": "Proveedor", + "comms.free_tier_column": "Plan gratuito", + "comms.notes_column": "Notas", + "comms.email": "Email", + "comms.sms": "SMS", + "comms.voice": "Voz", + "comms.free_tier_100_day": "100/día", + "comms.free_tier_15_trial": "$15 de prueba", + "comms.resend_notes": "API de email moderna, gran DX", + "comms.twilio_sms_notes": "Pago por mensaje tras la prueba", + "comms.twilio_voice_notes": "Pago por minuto tras la prueba", + "comms.signup_links": "Enlaces de registro:", + # Advertencias de capa de servicio (traducidas al mostrar) + "comms.warn.resend_api_key": "RESEND_API_KEY no establecida. Regístrate en https://resend.com para obtener tu clave API.", + "comms.warn.resend_from_email": "RESEND_FROM_EMAIL no establecida. Debe ser un email de remitente verificado en tu cuenta Resend.", + "comms.warn.twilio_account_sid": "TWILIO_ACCOUNT_SID no establecida. Encuéntrala en tu panel de Twilio Console.", + "comms.warn.twilio_auth_token": "TWILIO_AUTH_TOKEN no establecida. Encuéntrala en tu panel de Twilio Console.", + "comms.warn.twilio_messaging_sid": "TWILIO_MESSAGING_SERVICE_SID o TWILIO_PHONE_NUMBER deben estar establecidos. Messaging Service SID se requiere para números toll-free.", + "comms.warn.twilio_phone": "TWILIO_PHONE_NUMBER no establecida. Debe ser un número Twilio capaz de hacer llamadas.", + "comms.warn.twilio_creds": "Credenciales Twilio no establecidas. Establece las variables de entorno TWILIO_ACCOUNT_SID y TWILIO_AUTH_TOKEN.", + "comms.warn.resend_api_key_send": "RESEND_API_KEY no establecida. Regístrate en https://resend.com y establece tu clave API.", + # Texto de ayuda + "comms.help": "Comandos del servicio de comunicaciones (email, SMS, voz)", + "comms.help_status": "Mostrar estado de configuración del servicio de comunicaciones.", + "comms.help_email_send": "Enviar un email vía Resend.", + "comms.help_sms_send": "Enviar un SMS vía Twilio.", + "comms.help_call_make": "Hacer una llamada de voz vía Twilio.", + "comms.help_providers": "Mostrar proveedores de comunicaciones disponibles.", + "comms.help_email": "Comandos de email usando Resend", + "comms.help_sms": "Comandos de SMS usando Twilio", + "comms.help_call": "Comandos de llamadas de voz usando Twilio", + "comms.arg_to_email": "Dirección de email del destinatario", + "comms.opt_subject": "Asunto del email", + "comms.opt_text": "Cuerpo en texto plano", + "comms.opt_html": "Cuerpo en HTML", + "comms.arg_to_phone": "Número de teléfono del destinatario (formato E.164)", + "comms.arg_body": "Cuerpo del mensaje SMS", + "comms.arg_twiml_url": "URL que devuelve instrucciones TwiML", + "comms.opt_timeout": "Segundos de espera para respuesta", + # ── Auth ───────────────────────────────────────────────────────── + "auth.auto_generated_email": "Email auto-generado: {email} (siguiente en secuencia)", + "auth.user_already_exists": "Error: Usuario con email '{email}' ya existe", + "auth.user_created": "Usuario de prueba creado.", + "auth.user_details_title": "Detalles del usuario", + "auth.email_label": "Email", + "auth.password_label": "Contraseña", + "auth.name_label": "Nombre", + "auth.user_id_label": "ID de usuario", + "auth.password_auto_generated": "Contraseña auto-generada. ¡Guárdala para pruebas!", + "auth.ready_to_test": "Listo para probar endpoints de auth en http://localhost:8000/docs", + "auth.create_user_failed": "Error: Fallo al crear usuario de prueba: {error}", + "auth.count_must_be_positive": "Error: Cantidad debe ser mayor a 0", + "auth.creating_users": "Creando {count} usuarios de prueba con prefijo '{prefix}'...", + "auth.created_label": "Creado:", + "auth.users_created": "Creados {count} usuarios de prueba.", + "auth.shared_password": "Contraseña compartida:", + "auth.create_users_failed": "Error: Fallo al crear usuarios de prueba: {error}", + "auth.no_users_found": "Sin usuarios.", + "auth.found_users": "Encontrados {count} usuario(s):", + "auth.users_title": "Usuarios", + "auth.created_column": "Creado", + "auth.list_users_failed": "Error: Fallo al listar usuarios: {error}", + # Texto de ayuda + "auth.help": "Comandos de gestión de autenticación", + "auth.help_create_test_user": "Crear un usuario de prueba para desarrollo y testing.", + "auth.help_create_test_users": "Crear múltiples usuarios de prueba para desarrollo y testing.", + "auth.help_list_users": "Listar todos los usuarios del sistema.", + "auth.opt_email": "Email del usuario (auto-incremento: test@example.com, test1@example.com, etc.)", + "auth.opt_password": "Contraseña del usuario (generada si no se proporciona)", + "auth.opt_full_name": "Nombre completo del usuario", + "auth.opt_prefix": "Prefijo de email para emails auto-generados", + "auth.opt_domain": "Dominio de email para emails auto-generados", + "auth.opt_count": "Número de usuarios de prueba a crear", + "auth.opt_shared_password": "Contraseña compartida (generada si no se proporciona)", + # ── Tasks ──────────────────────────────────────────────────────── + "tasks.listing_jobs": "Listando trabajos programados", + "tasks.no_jobs_found": "Sin trabajos programados", + "tasks.scheduled_jobs_title": "Trabajos programados", + "tasks.job_id_column": "ID de trabajo", + "tasks.name_column": "Nombre", + "tasks.status_column": "Estado", + "tasks.next_run_column": "Próxima ejecución", + "tasks.trigger_column": "Disparador", + "tasks.not_scheduled": "No programado", + "tasks.active": "Activo", + "tasks.paused": "Pausado", + "tasks.total_jobs": "Total de trabajos:", + "tasks.list_failed": "Fallo al listar trabajos:", + # Texto de ayuda + "tasks.help": "Comandos de gestión de tareas programadas", + "tasks.help_list": "Listar todos los trabajos programados con su estado y detalles.", + # ── Migrate ────────────────────────────────────────────────────── + "migrate.table_missing_warning": ( + " ADVERTENCIA: Tabla '{table}' no existe. " + "Crea una migración manual con: " + "alembic revision --autogenerate -m 'add {table}'" + ), + "migrate.no_existing_migrations": "Sin migraciones existentes. Ejecuta 'make migrate' primero.", + "migrate.checking_schema": "Verificando schema contra modelos...", + "migrate.applying_pending": "Aplicando migraciones pendientes primero...", + "migrate.schema_up_to_date": "Schema actualizado. Sin correcciones necesarias.", + "migrate.found_differences": "Encontradas {count} diferencias de schema:", + "migrate.add_column": "AGREGAR COLUMNA", + "migrate.add_table": "AGREGAR TABLA", + "migrate.generated_migration": "Migración de corrección generada: {name}", + "migrate.apply_now": "¿Aplicar migración ahora?", + "migrate.applied_migration": "Migración aplicada.", + "migrate.apply_later": "Migración guardada. Ejecuta 'make migrate' para aplicar después.", + "migrate.no_fixable_differences": "Sin diferencias corregibles.", + # ── AI ─────────────────────────────────────────────────────────── + "ai.header_inline": "Illiana: ", + "ai.header": "Illiana:", + "ai.no_response_content": "(Sin contenido de respuesta)", + "ai.conversation_label": "Conversación:", + "ai.messages_label": "Mensajes:", + "ai.response_time_label": "Tiempo de respuesta:", + "ai.tip_label": "Tip:", + "ai.thinking": "Pensando...", + # Texto de ayuda + "ai.help": "Comandos de gestión y chat del servicio AI", + "ai.help_status": "Mostrar estado, configuración y validación del servicio AI.", + "ai.help_providers": "Listar proveedores AI disponibles con estado de instalación.", + "ai.help_add_provider": "Agregar proveedor AI — instala dependencias y configura clave API.", + "ai.help_use_provider": "Cambiar a otro proveedor AI.", + "ai.help_chat": "Enviar mensaje de chat o iniciar sesión interactiva. Reanuda la conversación más reciente por defecto.", + "ai.help_conversations": "Listar conversaciones de un usuario.", + "ai.help_history": "Ver historial de conversación.", + "ai.help_voice": "Enviar entrada de voz al agente AI. Transcribe audio y lo envía al chat.", + "ai.help_record": "Grabar audio del micrófono, transcribir, y opcionalmente chatear con AI.", + "ai.help_transcribe": "Transcribir audio a texto sin chat.", + "ai.help_stt_status": "Mostrar estado del servicio STT (Speech-to-Text).", + "ai.help_speak": "Sintetizar voz desde texto usando el proveedor TTS configurado.", + "ai.help_tts_status": "Mostrar estado del servicio TTS (Text-to-Speech).", + "ai.help_usage": "Mostrar estadísticas de uso de AI incluyendo conteo de tokens, costos y actividad reciente.", + "ai.arg_provider": "Nombre del proveedor (openai, anthropic, google, groq, mistral, cohere)", + "ai.arg_provider_switch": "Nombre del proveedor al que cambiar", + "ai.arg_message": "Mensaje a enviar al AI", + "ai.arg_conversation_id": "ID de conversación", + "ai.arg_audio_file": "Ruta al archivo de audio", + "ai.arg_text": "Texto a sintetizar en voz", + "ai.opt_set_default": "Establecer como proveedor por defecto tras agregar", + "ai.opt_skip_api_key": "Omitir configuración de clave API", + "ai.opt_yes": "Omitir confirmaciones", + "ai.opt_stream": "Habilitar salida en streaming", + "ai.opt_conversation_id": "Continuar conversación existente", + "ai.opt_new": "Iniciar conversación nueva (no reanudar)", + "ai.opt_user_id": "Identificador de usuario", + "ai.opt_verbose": "Mostrar metadatos de conversación", + "ai.opt_rag": "Habilitar RAG (auto-habilita si existen colecciones)", + "ai.opt_collection": "Colección RAG donde buscar", + "ai.opt_top_k": "Número de resultados RAG a usar", + "ai.opt_sources": "Mostrar referencias de fuentes en la salida", + "ai.opt_limit": "Número de conversaciones a mostrar", + "ai.opt_voice_mode": "Resumir respuesta para salida hablada", + "ai.opt_rag_inject": "Habilitar inyección de contexto RAG", + "ai.opt_send": "Enviar transcripción al agente AI", + "ai.opt_voice_response": "Reproducir respuesta TTS (requiere --send)", + "ai.opt_output_recording": "Guardar grabación en archivo (por defecto: archivo temporal)", + "ai.opt_rag_send": "Habilitar inyección de contexto RAG (requiere --send)", + "ai.opt_language": "Código de idioma (ej., 'en', 'es')", + "ai.opt_json": "Salida en JSON crudo", + "ai.opt_output_file": "Ruta de archivo de salida", + "ai.opt_voice": "Voz a usar (alloy, echo, fable, onyx, nova, shimmer)", + "ai.opt_speed": "Velocidad del habla (0.25-4.0)", + "ai.opt_url": "URL base de API (por defecto: localhost:8000)", + "ai.opt_filter_user_id": "Filtrar por ID de usuario", + "ai.opt_recent": "Número de actividades recientes a mostrar", + # Tabla de proveedores + "ai.providers_title": "Proveedores AI", + "ai.col_provider": "Proveedor", + "ai.col_installed": "Instalado", + "ai.col_api_key": "Clave API", + "ai.col_status": "Estado", + "ai.col_free": "Gratuito", + "ai.col_stream": "Stream", + "ai.col_functions": "Funciones", + "ai.col_vision": "Visión", + "ai.prov_yes": "Sí", + "ai.prov_no": "No", + "ai.prov_na": "N/A", + "ai.prov_ready": "Listo", + "ai.prov_local": "Local", + "ai.prov_current": "Actual", + "ai.prov_current_not_installed": "Actual (No instalado)", + "ai.prov_current_need_key": "Actual (Falta clave API)", + "ai.prov_current_local": "Actual (Local)", + "ai.prov_need_key": "Falta clave API", + "ai.prov_not_installed": "No instalado", + "ai.prov_error": "Error", + "ai.providers_tip": "Tip: Ejecuta '{app} ai add-provider ' para instalar proveedores faltantes.", + # Panel de uso + "ai.usage_title": "Resumen de uso AI", + "ai.usage_total_tokens": "Tokens totales:", + "ai.usage_total_cost": "Costo total:", + "ai.usage_total_requests": "Solicitudes totales:", + "ai.usage_success_rate": "Tasa de éxito:", + "ai.svc_initialized": "Inicializado", + "ai.svc_not_initialized": "No inicializado (carga diferida)", + "ai.enabled": "Habilitado", + "ai.disabled": "Deshabilitado", + # Comando de estado + "ai.status_title": "Estado del servicio AI", + "ai.config_valid": "Configuración válida", + "ai.config_issues": "Problemas de configuración:", + "ai.engine_label": "Motor:", + "ai.status_label": "Estado:", + "ai.provider_label": "Proveedor:", + "ai.model_label": "Modelo:", + "ai.temperature_label": "Temperatura:", + "ai.max_tokens_label": "Tokens máximos:", + "ai.api_key_label": "Clave API:", + "ai.free_tier": "Plan gratuito", + "ai.streaming_supported": "Streaming soportado", + "ai.available_providers": "Proveedores disponibles:", + # Gestión de proveedores + "ai.invalid_provider": "Proveedor inválido: {provider}", + "ai.valid_providers": "Proveedores válidos: {names}", + "ai.adding_provider": "Agregando proveedor {provider}...", + "ai.checking_deps": "Verificando dependencias...", + "ai.install_dep": "¿Instalar {dep}?", + "ai.installing": "Instalando...", + "ai.install_cancelled": "Instalación cancelada", + "ai.install_failed_hint": "Intenta instalar manualmente:", + "ai.deps_installed": "Dependencias ya instaladas", + "ai.checking_api_key": "Verificando clave API...", + "ai.no_api_key": "No se encontró {var}", + "ai.get_api_key_at": "Obtén tu clave API en:", + "ai.enter_api_key": "Ingresa tu clave API de {provider} (o presiona Enter para omitir)", + "ai.api_key_skipped": "Configuración de clave API omitida", + "ai.provider_set": "AI_PROVIDER establecido a {provider}", + "ai.provider_added": "Proveedor agregado.", + "ai.test_with": 'Prueba con: {app} ai chat "Hola"', + "ai.switching_to": "Cambiando a proveedor {provider}...", + "ai.deps_not_installed": "Dependencias no instaladas para {provider}", + "ai.run_first": "Ejecuta este comando primero:", + "ai.deps_ok": "Dependencias instaladas", + "ai.no_api_key_configured": "Sin {var} configurada", + "ai.consider_running": "Considera ejecutar:", + "ai.to_configure_key": "para configurar tu clave API", + "ai.switch_anyway": "¿Cambiar de todos modos?", + "ai.switched_to": "Cambiado a {provider}.", + "ai.current_provider": "Proveedor actual: {provider}", + # Conversaciones + "ai.no_conversations": "Sin conversaciones para usuario: {user_id}", + "ai.conversations_for": "Conversaciones de {user_id}:", + "ai.messages_count": "{count} mensajes", + "ai.conversation_not_found": "Conversación no encontrada: {id}", + "ai.access_denied": "Acceso denegado: No eres dueño de esta conversación", + "ai.conversation_id_label": "Conversación: {id}", + "ai.title_label": "Título: {title}", + "ai.provider_info": "Proveedor: {provider}", + "ai.messages_info": "Mensajes: {count}", + "ai.resuming_conversation": "Reanudando conversación {id}... (usa --new para iniciar nueva)", + # Sesión de chat + "ai.initializing": "Inicializando...", + "ai.ok": "OK", + "ai.connecting_to": "Conectando a {provider}...", + "ai.health_label": "Salud:", + "ai.health_ok": "OK ({pct:.0f}%)", + "ai.health_degraded": "DEGRADADO ({pct:.0f}%, {count} problemas)", + "ai.health_na": "N/A", + "ai.online": "En línea", + "ai.chat_hints": "Escribe /help para comandos \u2022 Esc luego Enter para nueva línea \u2022 'exit' para salir", + "ai.chat_ended": "Sesión de chat terminada", + "ai.goodbye": "¡Hasta luego!", + "ai.enter_message_hint": "Ingresa un mensaje o escribe /help.", + "ai.sources_label": "Fuentes:", + "ai.provider_not_installed": "Proveedor no instalado: {provider}", + "ai.run_to_install": "Ejecuta '{command}' para instalarlo.", + "ai.timeout_error": "Solicitud expiró. Prueba un modelo más rápido o aumenta AI_TIMEOUT_SECONDS.", + # Voz/STT/TTS + "ai.stt_status_title": "Estado del servicio STT", + "ai.tts_status_title": "Estado del servicio TTS", + "ai.voice_label": "Voz:", + "ai.speed_label": "Velocidad:", + "ai.openai_voices": "Voces OpenAI:", + "ai.file_not_found": "Archivo no encontrado: {path}", + "ai.unsupported_format": "Formato no soportado: {ext}", + "ai.supported_formats": "Formatos soportados: {formats}", + "ai.transcription_label": "Transcripción:", + "ai.language_label": "Idioma: {lang}", + "ai.response_label": "Respuesta:", + "ai.voice_hint": "Respuesta completa disponible con la opción --no-voice", + "ai.recording_requires": "Grabación de audio requiere los paquetes sounddevice y soundfile.", + "ai.install_with": "Instalar con:", + "ai.macos_portaudio": "En macOS, también puedes necesitar PortAudio:", + "ai.linux_deps": "En Linux:", + "ai.using_mic": "Usando micrófono: {name} @ {rate}Hz", + "ai.recording": "Grabando...", + "ai.recording_hint": "Presiona ENTER para detener la grabación, Ctrl+C para cancelar", + "ai.audio_status": "Estado de audio: {status}", + "ai.recording_cancelled": "Grabación cancelada", + "ai.recording_failed": "Grabación fallida - sin audio capturado", + "ai.check_mic": "Verifica los permisos del micrófono e intenta de nuevo", + "ai.audio_saved": "Audio guardado: {path} ({size} bytes)", + "ai.transcribing": "Transcribiendo...", + "ai.transcribing_audio": "Transcribiendo audio...", + "ai.sending_to_agent": "Enviando al agente...", + "ai.preparing_voice": "Preparando respuesta de voz...", + "ai.generating_speech": "Generando voz...", + "ai.synthesizing": "Sintetizando voz...", + "ai.speech_saved": "Voz guardada en: {path}", + "ai.speech_format": "Formato: {format}", + "ai.speech_size": "Tamaño: {size} bytes", + "ai.speech_duration": "Duración: {duration}s", + "ai.cancelled": "Cancelado", + # Uso/estadísticas + "ai.no_token_usage": "Sin uso de tokens registrado.", + "ai.token_breakdown": "Desglose de tokens", + "ai.input_tokens": "Entrada:", + "ai.output_tokens": "Salida:", + "ai.no_model_usage": "Sin datos de uso por modelo.", + "ai.model_usage": "Uso por modelo", + "ai.no_recent_activity": "Sin actividad reciente.", + "ai.recent_activity": "Actividad reciente", + "ai.connection_error": "Error de conexión: {error}", + "ai.timeout_label": "Error de timeout: {error}", + # Hints RAG (en chat) + "ai.rag_requires_content": "RAG requiere contenido indexado.", + "ai.rag_get_started": "Para comenzar:", + "ai.rag_step_index": "1. Indexa tu código:", + "ai.rag_step_query": "2. Consulta con la colección:", + "ai.rag_list_hint": "Tip: Ejecuta '{app} rag list' para ver colecciones existentes.", + # Comandos slash + "slash.help_desc": "Mostrar comandos disponibles", + "slash.clear_desc": "Limpiar la pantalla", + "slash.new_desc": "Iniciar nueva conversación", + "slash.model_desc": "Cambiar modelo AI (auto-detecta proveedor)", + "slash.status_desc": "Mostrar configuración actual", + "slash.rag_desc": "Gestionar modo RAG", + "slash.sources_desc": "Alternar referencias de fuentes en la salida", + "slash.exit_desc": "Salir de la sesión de chat", + "slash.commands_panel": "Comandos", + "slash.current_panel": "Actual", + "slash.status_panel": "Estado", + "slash.rag_panel": "RAG", + "slash.use_model_hint": "Usa /model para cambiar", + "slash.unknown_command": "Comando desconocido: /{name}. Escribe /help para ver comandos.", + "slash.new_conversation": "Nueva conversación iniciada", + "slash.switched_to": "Cambiado a {provider}/{model}", + "slash.switched_model": "Cambiado a modelo: {model}", + "slash.rag_disabled": "RAG deshabilitado", + "slash.collection_not_found": "Colección '{name}' no encontrada.", + "slash.available_collections": "Disponibles: {list}", + "slash.no_collections": "Sin colecciones. Indexa documentos primero.", + "slash.rag_enabled": "RAG habilitado con colección: {name}", + "slash.sources_require_rag": "Fuentes requieren RAG habilitado.", + "slash.enable_rag_first": "Habilita RAG primero con /rag ", + "slash.sources_status": "Referencias de fuentes {status}", + "slash.sources_enabled": "Referencias de fuentes habilitadas", + "slash.sources_disabled": "Referencias de fuentes deshabilitadas", + "slash.unknown_option": "Opción desconocida: {arg}", + "slash.sources_usage": "Uso: /sources [enable|disable]", + "slash.rag_use_hint": "Usa /rag off o /rag ", + "slash.no_collections_indexed": "Sin colecciones indexadas aún", + "slash.index_documents_hint": "Indexar documentos: {app} rag index ", + # ── Status ─────────────────────────────────────────────────────── + "status.not_connected": "sin conexión", + "status.rag_on": "RAG: ON", + "status.rag_off": "RAG: OFF", + "status.src_on": "SRC: ON", + "status.src_off": "SRC: OFF", + "status.tokens": "tokens", + "status.na": "n/a", + # ── Chat ───────────────────────────────────────────────────────── + "chat.disable_rag": "Deshabilitar RAG", + "chat.show_source_references": "Mostrar referencias de fuentes", + "chat.hide_source_references": "Ocultar referencias de fuentes", + # ── Docs ────────────────────────────────────────────────────────── + "docs.components": "Componentes", + "docs.services": "Servicios", + "docs.guide_label": "Guía:", + "docs.docs_label": "Docs:", + "docs.no_detected": "Sin componentes ni servicios detectados.", + "docs.documentation": "Documentación", + # Texto de ayuda + "docs.help": "Mostrar enlaces de documentación", + "docs.help_show": "Mostrar enlaces de documentación para componentes y servicios instalados.", + # ── Load Test ──────────────────────────────────────────────────── + # Texto de ayuda + "loadtest.help": "Comandos de pruebas de carga para análisis de rendimiento de workers", + "loadtest.help_run": "Ejecutar prueba de carga personalizable con control total de configuración.", + "loadtest.help_cpu": "Prueba rápida de carga intensiva en CPU con valores por defecto optimizados.", + "loadtest.help_io": "Prueba rápida de simulación I/O con valores por defecto optimizados.", + "loadtest.help_memory": "Prueba rápida de asignación de memoria con valores por defecto optimizados.", + "loadtest.help_results": "Mostrar resultados y análisis de una prueba de carga completada.", + "loadtest.help_info": "Mostrar información sobre tipos de prueba de carga disponibles y sus características.", + "loadtest.opt_num_tasks": "Número de tareas a generar", + "loadtest.opt_task_type": "Tipo de prueba de carga a ejecutar", + "loadtest.opt_batch_size": "Tareas por lote", + "loadtest.opt_delay": "Retraso entre lotes (ms)", + "loadtest.opt_queue": "Cola destino para pruebas", + "loadtest.opt_wait": "Esperar a que termine la prueba y mostrar resultados", + "loadtest.opt_timeout": "Timeout de espera (segundos)", + "loadtest.opt_cpu_tasks": "Número de tareas CPU", + "loadtest.opt_io_tasks": "Número de tareas I/O", + "loadtest.opt_memory_tasks": "Número de tareas de memoria", + "loadtest.opt_wait_short": "Esperar finalización", + "loadtest.arg_task_id": "ID de tarea de prueba de carga", + "loadtest.opt_queue_results": "Cola donde se ejecutó la prueba", + "loadtest.opt_detailed": "Mostrar análisis y métricas detalladas", + "loadtest.opt_json": "Salida de resultados en formato JSON", + "loadtest.arg_test_type": "Tipo de prueba específico para mostrar información", + # Salida en ejecución + "loadtest.starting": "Iniciando prueba de carga...", + "loadtest.enqueued": "Prueba de carga encolada.", + "loadtest.task_id_label": "ID de tarea:", + "loadtest.timeout_reached": "Timeout alcanzado tras {timeout}s", + "loadtest.check_results_manual": "Verifica resultados manualmente:", + "loadtest.completed": "Prueba de carga completada.", + "loadtest.check_results_later": "Usa este comando para ver resultados después:", + "loadtest.start_failed": "Fallo al iniciar prueba de carga:", + "loadtest.waiting": "Esperando finalización de prueba de carga (timeout: {timeout}s)...", + "loadtest.waiting_progress": "Esperando finalización de prueba de carga...", + "loadtest.waiting_elapsed": "Esperando finalización... ({elapsed}s)", + "loadtest.timeout_progress": "Timeout alcanzado", + "loadtest.completed_progress": "Prueba de carga completada.", + # Salida de prueba rápida + "loadtest.quick_cpu_title": "Prueba rápida de carga CPU", + "loadtest.quick_cpu_tasks": "{count} tareas intensivas en CPU", + "loadtest.quick_cpu_work": "Fibonacci + operaciones matemáticas + verificación de primos", + "loadtest.cpu_started": "Prueba CPU iniciada.", + "loadtest.cpu_failed": "Prueba CPU falló:", + "loadtest.quick_io_title": "Prueba rápida de carga I/O", + "loadtest.quick_io_tasks": "{count} tareas de simulación I/O", + "loadtest.quick_io_work": "Retrasos de red + async concurrente + operaciones de archivo", + "loadtest.io_started": "Prueba I/O iniciada.", + "loadtest.io_failed": "Prueba I/O falló:", + "loadtest.quick_memory_title": "Prueba rápida de carga de memoria", + "loadtest.quick_memory_tasks": "{count} tareas de asignación de memoria", + "loadtest.quick_memory_work": "Estructuras de datos + patrones de asignación + pruebas de GC", + "loadtest.memory_started": "Prueba de memoria iniciada.", + "loadtest.memory_failed": "Prueba de memoria falló:", + "loadtest.tasks_label": "Tareas:", + "loadtest.work_type_label": "Tipo de trabajo:", + "loadtest.check_results": "Ver resultados:", + # Comando info + "loadtest.available_types": "Tipos de prueba de carga disponibles", + "loadtest.unknown_type": "Tipo de prueba desconocido:", + "loadtest.available_types_list": "Tipos disponibles:", + "loadtest.typical_duration": "Duración típica:", + "loadtest.use_help_hint": "Usa --help con comandos específicos para más detalles", + "loadtest.examples_label": "Ejemplos:", + # Visualización de configuración de prueba + "loadtest.config_title": "Configuración de prueba", + "loadtest.config_tasks": "Tareas:", + "loadtest.config_batching": "Por lotes:", + "loadtest.config_delay": "Retraso:", + "loadtest.config_queue": "Cola:", + "loadtest.config_tasks_detail": "{count} tareas {type}", + "loadtest.config_batch_detail": "{size} tareas por lote", + "loadtest.config_delay_detail": "{ms}ms entre lotes", + "loadtest.type_details_title": "Detalles del tipo de prueba", + "loadtest.type_name": "Nombre:", + "loadtest.type_description": "Descripción:", + "loadtest.type_performance": "Rendimiento:", + "loadtest.type_concurrency": "Concurrencia:", + "loadtest.panel_setup": "Configuración de prueba de carga", + # Visualización de resultados + "loadtest.results_title": "Resumen de resultados de prueba de carga", + "loadtest.metric_column": "Métrica", + "loadtest.value_column": "Valor", + "loadtest.details_column": "Detalles", + "loadtest.metric_status": "Estado", + "loadtest.metric_tasks_sent": "Tareas enviadas", + "loadtest.metric_tasks_completed": "Tareas completadas", + "loadtest.metric_tasks_failed": "Tareas fallidas", + "loadtest.metric_duration": "Duración", + "loadtest.metric_throughput": "Rendimiento", + "loadtest.metric_failure_rate": "Tasa de fallo", + "loadtest.detail_status": "Estado de ejecución de la prueba", + "loadtest.detail_tasks_sent": "Total de tareas encoladas", + "loadtest.detail_tasks_completed": "Completadas", + "loadtest.detail_tasks_failed": "Fallidas durante ejecución", + "loadtest.detail_duration": "Duración total de la prueba", + "loadtest.detail_throughput": "Tasa promedio de finalización de tareas", + "loadtest.detail_failure_rate": "Porcentaje de tareas fallidas", + "loadtest.tasks_per_sec": "tareas/seg", + "loadtest.perf_summary": "Resumen de rendimiento", + "loadtest.no_summary": "Sin resumen disponible", + "loadtest.recommendations": "Recomendaciones", + "loadtest.no_results": "Sin resultados para ID de tarea:", + "loadtest.check_task_hint": "Verifica que el ID de tarea sea correcto y la prueba haya terminado", + "loadtest.results_failed": "Fallo al obtener resultados:", + # Visualización de error/timeout + "loadtest.timed_out_title": "Prueba de carga expiró", + "loadtest.failed_title": "Prueba de carga falló", + "loadtest.error_label": "Error:", + "loadtest.test_id_label": "ID de prueba:", + "loadtest.what_this_means": "Qué significa esto:", + "loadtest.timeout_explanation": "La tarea orquestadora expiró, pero las tareas individuales de los workers pueden haberse completado. Esto ocurre frecuentemente con pruebas de carga grandes que exceden el timeout de la cola.", + "loadtest.to_investigate": "Para investigar:", + "loadtest.tip_check_logs": "Revisa logs de workers para verificar tareas individuales completadas", + "loadtest.tip_smaller_batch": "Considera usar lotes más pequeños para pruebas grandes", + "loadtest.tip_check_metrics": "Revisa métricas de cola para conteos reales de tareas completadas", + "loadtest.analysis_panel": "Análisis de prueba de carga", + "loadtest.next_steps": "Próximos pasos:", + "loadtest.tip_check_worker_logs": "Revisa logs de workers para información detallada de errores", + "loadtest.tip_verify_queue": "Verifica conectividad de cola y estado de workers", + "loadtest.tip_try_smaller": "Prueba con una prueba más pequeña para aislar el problema", + "loadtest.troubleshooting": "Tips de resolución de problemas", + "loadtest.troubleshoot_1": "Revisa logs del contenedor worker: docker compose logs worker", + "loadtest.troubleshoot_2": "Verifica salud del sistema: {app} health check", + "loadtest.troubleshoot_3": "Prueba primero con una carga menor: {app} load-test cpu --tasks 10", + # Análisis de rendimiento + "loadtest.perf_analysis_title": "Análisis de rendimiento", + "loadtest.aspect_column": "Aspecto", + "loadtest.rating_column": "Calificación", + "loadtest.description_column": "Descripción", + "loadtest.aspect_throughput": "Rendimiento", + "loadtest.aspect_efficiency": "Eficiencia", + "loadtest.aspect_queue_pressure": "Presión de cola", + "loadtest.desc_throughput": "Velocidad de procesamiento de tareas", + "loadtest.desc_efficiency": "Tasa de completado de tareas", + "loadtest.desc_queue_pressure": "Nivel de saturación de cola", + "loadtest.test_validation": "Validación de prueba", + "loadtest.validation_type": "Tipo de prueba verificado:", + "loadtest.validation_metrics": "Métricas esperadas presentes:", + "loadtest.validation_signature": "Firma de rendimiento coincide:", + # Información de tipo de prueba (desde capa de servicio) + "loadtest.type.cpu_intensive.name": "CPU intensivo", + "loadtest.type.cpu_intensive.description": "Prueba procesamiento CPU del worker con cálculos fibonacci", + "loadtest.type.cpu_intensive.signature": "Limitado por CPU - debe mostrar escalamiento de tiempo de cómputo según tamaño del problema", + "loadtest.type.cpu_intensive.duration": "1-10ms por tarea", + "loadtest.type.cpu_intensive.concurrency": "Limitado por núcleos de CPU, se beneficia del procesamiento paralelo", + "loadtest.type.io_simulation.name": "Simulación I/O", + "loadtest.type.io_simulation.description": "Prueba manejo de I/O asíncrono con retrasos simulados", + "loadtest.type.io_simulation.signature": "Limitado por I/O - debe mostrar beneficios de concurrencia asíncrona", + "loadtest.type.io_simulation.duration": "5-30ms por tarea (incluye retrasos simulados)", + "loadtest.type.io_simulation.concurrency": "Excelente con async - muchas tareas pueden ejecutarse concurrentemente", + "loadtest.type.memory_operations.name": "Operaciones de memoria", + "loadtest.type.memory_operations.description": "Prueba asignación de memoria y operaciones de estructuras de datos", + "loadtest.type.memory_operations.signature": "Limitado por memoria - debe mostrar patrones de asignación/liberación", + "loadtest.type.memory_operations.duration": "1-5ms por tarea", + "loadtest.type.memory_operations.concurrency": "Moderado - limitado por ancho de banda de memoria y presión de GC", + "loadtest.type.failure_testing.name": "Prueba de fallos", + "loadtest.type.failure_testing.description": "Prueba manejo de errores con ~20% de fallos aleatorios", + "loadtest.type.failure_testing.signature": "Mixto - prueba resiliencia y rutas de manejo de errores", + "loadtest.type.failure_testing.duration": "1-10ms por tarea (cuando tiene éxito)", + "loadtest.type.failure_testing.concurrency": "Prueba recuperación del worker y aislamiento de errores", + # Traducciones de estado/calificación (al mostrar) + "loadtest.status.completed": "completado", + "loadtest.status.timed_out": "expirado", + "loadtest.status.failed": "fallido", + "loadtest.status.unknown": "desconocido", + "loadtest.rating.excellent": "excelente", + "loadtest.rating.good": "bueno", + "loadtest.rating.fair": "regular", + "loadtest.rating.poor": "deficiente", + "loadtest.rating.unknown": "desconocido", + "loadtest.pressure.high": "alta", + "loadtest.pressure.medium": "media", + "loadtest.pressure.low": "baja", + "loadtest.validation.verified": "verificado", + "loadtest.validation.unknown": "desconocido", + "loadtest.validation.yes": "Sí", + "loadtest.validation.no": "No", + # Recomendaciones (desde análisis de capa de servicio) + "loadtest.rec.low_throughput": "Rendimiento bajo detectado. Considera reducir complejidad de tareas o aumentar concurrencia de workers.", + "loadtest.rec.high_failure": "Tasa de fallo alta ({rate}%). Revisa logs de workers para patrones de error.", + "loadtest.rec.long_execution": "Tiempo de ejecución prolongado para pocas tareas sugiere saturación de cola. Considera probar con lotes más pequeños o colas diferentes.", +} diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/fr.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/fr.py index 795b8db6..6e504108 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/fr.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/fr.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "Langue non prise en charge « {lang} ». Disponibles : {available}", # Texte d'aide "main.help": "CLI de gestion de projet", - "main.opt_lang": "Langue de sortie (de, en, fr, ja, ko, ru, zh, zh_Hant). Par défaut : détection auto", + "main.opt_lang": "Langue de sortie (de, en, es, fr, ja, ko, ru, zh, zh_Hant). Par défaut : détection auto", # ── Santé ──────────────────────────────────────────────────────── "health.count_healthy": "{count} sain(s)", "health.count_warning": "{count} avertissement", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ja.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ja.py index 3893abef..7dcc1245 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ja.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ja.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "未対応の言語 '{lang}'。利用可能:{available}", # ヘルプ "main.help": "プロジェクト管理CLI", - "main.opt_lang": "出力言語(de、en、fr、ja、ko、ru、zh、zh_Hant)。デフォルト:自動検出", + "main.opt_lang": "出力言語(de、en、es、fr、ja、ko、ru、zh、zh_Hant)。デフォルト:自動検出", # ── ヘルスチェック ───────────────────────────────────────────────── "health.count_healthy": "{count} 件正常", "health.count_warning": "{count} 件警告", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ko.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ko.py index 36284f92..d78eafc8 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ko.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ko.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "지원하지 않는 언어입니다: '{lang}'. 사용 가능: {available}", # Help text "main.help": "프로젝트 관리 CLI", - "main.opt_lang": "출력 언어 (de, en, fr, ja, ko, ru, zh, zh_Hant). 기본값: 자동 감지", + "main.opt_lang": "출력 언어 (de, en, es, fr, ja, ko, ru, zh, zh_Hant). 기본값: 자동 감지", # ── Health ──────────────────────────────────────────────────────── "health.count_healthy": "{count}개 정상", "health.count_warning": "{count}개 경고", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ru.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ru.py index cef909cc..40a99dec 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ru.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/ru.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "Язык '{lang}' не поддерживается. Доступные: {available}", # Справка "main.help": "CLI управления проектом", - "main.opt_lang": "Язык вывода (de, en, fr, ja, ko, ru, zh, zh_Hant). По умолчанию: автоопределение", + "main.opt_lang": "Язык вывода (de, en, es, fr, ja, ko, ru, zh, zh_Hant). По умолчанию: автоопределение", # ── Здоровье ───────────────────────────────────────────────────── "health.count_healthy": "{count} в норме", "health.count_warning": "{count} предупреждение", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh.py index d20042bf..0a488b87 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "不支持的语言 '{lang}'。可用语言:{available}", # 帮助文本 "main.help": "项目命令行工具", - "main.opt_lang": "输出语言(de、en、fr、ja、ko、ru、zh、zh_Hant),默认自动检测系统语言", + "main.opt_lang": "输出语言(de、en、es、fr、ja、ko、ru、zh、zh_Hant),默认自动检测系统语言", # ── 健康检查 ────────────────────────────────────────────────────── "health.count_healthy": "{count} 个正常", "health.count_warning": "{count} 个警告", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh_hant.py b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh_hant.py index 15c2bbd1..caf84b5f 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh_hant.py +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh_hant.py @@ -13,7 +13,7 @@ "main.unsupported_lang": "不支援的语言 '{lang}'。可用語言:{available}", # 幫助文本 "main.help": "項目命令行工具", - "main.opt_lang": "輸出語言(de、en、fr、ja、ko、ru、zh、zh_Hant),默認自動檢測系統語言", + "main.opt_lang": "輸出語言(de、en、es、fr、ja、ko、ru、zh、zh_Hant),默認自動檢測系統語言", # ── 健康檢查 ────────────────────────────────────────────────────── "health.count_healthy": "{count} 個正常", "health.count_warning": "{count} 個警告", diff --git a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/registry.py.jinja b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/registry.py.jinja index 4a5d0a07..38baf1a7 100644 --- a/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/registry.py.jinja +++ b/aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/registry.py.jinja @@ -77,6 +77,10 @@ def _ensure_loaded(locale: str) -> None: from .locales.de import MESSAGES _messages["de"] = MESSAGES + elif locale == "es": + from .locales.es import MESSAGES + + _messages["es"] = MESSAGES elif locale == "fr": from .locales.fr import MESSAGES diff --git a/tests/core/test_i18n.py b/tests/core/test_i18n.py index 98cbb380..c06446cd 100644 --- a/tests/core/test_i18n.py +++ b/tests/core/test_i18n.py @@ -51,6 +51,14 @@ def test_korean_variants(self) -> None: assert _normalize_locale("ko_KR.UTF-8") == "ko" assert _normalize_locale("ko-KR") == "ko" + def test_spanish_variants(self) -> None: + """Spanish locale variants normalize to 'es'.""" + assert _normalize_locale("es") == "es" + assert _normalize_locale("es_ES") == "es" + assert _normalize_locale("es-MX") == "es" + assert _normalize_locale("es_AR") == "es" + assert _normalize_locale("es_ES.UTF-8") == "es" + def test_german_variants(self) -> None: """German locale variants normalize to 'de'.""" assert _normalize_locale("de") == "de" @@ -70,8 +78,8 @@ def test_russian_variants(self) -> None: def test_unsupported_falls_back_to_english(self) -> None: """Unsupported locales fall back to 'en'.""" assert _normalize_locale("xx") == "en" - assert _normalize_locale("es_MX.UTF-8") == "en" assert _normalize_locale("pt_BR") == "en" + assert _normalize_locale("sv_SE") == "en" def test_encoding_and_modifier_stripped(self) -> None: """Encoding suffixes and modifiers are stripped.""" @@ -97,6 +105,7 @@ def test_all_locales_registered(self) -> None: """All expected locales are in AVAILABLE_LOCALES.""" assert "de" in AVAILABLE_LOCALES assert "en" in AVAILABLE_LOCALES + assert "es" in AVAILABLE_LOCALES assert "fr" in AVAILABLE_LOCALES assert "ja" in AVAILABLE_LOCALES assert "ko" in AVAILABLE_LOCALES @@ -104,6 +113,15 @@ def test_all_locales_registered(self) -> None: assert "zh" in AVAILABLE_LOCALES assert "zh_Hant" in AVAILABLE_LOCALES + def test_es_has_all_keys(self) -> None: + """Spanish has all English keys.""" + from aegis.i18n.locales.es import MESSAGES as ES + + missing = set(EN_MESSAGES) - set(ES) + extra = set(ES) - set(EN_MESSAGES) + assert not missing, f"Keys in en but not es: {missing}" + assert not extra, f"Keys in es but not en: {extra}" + def test_de_has_all_keys(self) -> None: """German has all English keys.""" from aegis.i18n.locales.de import MESSAGES as DE @@ -170,6 +188,7 @@ def test_ko_has_all_keys(self) -> None: def test_no_empty_values(self) -> None: """No locale has empty string values.""" from aegis.i18n.locales.de import MESSAGES as DE + from aegis.i18n.locales.es import MESSAGES as ES from aegis.i18n.locales.fr import MESSAGES as FR from aegis.i18n.locales.ja import MESSAGES as JA from aegis.i18n.locales.ko import MESSAGES as KO @@ -179,6 +198,7 @@ def test_no_empty_values(self) -> None: for name, messages in [ ("de", DE), + ("es", ES), ("fr", FR), ("ja", JA), ("ko", KO),