diff --git a/.env.local.example b/.env.local.example
index f848864f6f..73c783739a 100644
--- a/.env.local.example
+++ b/.env.local.example
@@ -42,8 +42,8 @@ META_APP_SECRET=your-meta-app-secret
# or a background worker that has no incoming request.
NEXT_PUBLIC_SITE_URL=https://crm.example.com
-# Default language locale (e.g. en)
-NEXT_PUBLIC_APP_LOCALE=en
+# Default language locale (e.g. es)
+NEXT_PUBLIC_APP_LOCALE=es
# ============================================================
# OPTIONAL — only needed if you use the feature.
diff --git a/messages/es.json b/messages/es.json
new file mode 100644
index 0000000000..28560476db
--- /dev/null
+++ b/messages/es.json
@@ -0,0 +1,1696 @@
+{
+ "LoginPage": {
+ "titleAccept": "Inicia sesión para aceptar",
+ "titleWelcome": "Bienvenido de nuevo",
+ "descAccept": "Inicia sesión y te llevaremos a la invitación.",
+ "descWelcome": "Inicia sesión en tu cuenta",
+ "emailLabel": "Correo electrónico",
+ "emailPlaceholder": "tu@ejemplo.com",
+ "passwordLabel": "Contraseña",
+ "forgotPassword": "¿Olvidaste tu contraseña?",
+ "passwordPlaceholder": "Ingresa tu contraseña",
+ "signingIn": "Iniciando sesión...",
+ "signIn": "Iniciar sesión",
+ "noAccount": "¿No tienes una cuenta?",
+ "createAccount": "Crear cuenta"
+ },
+ "Sidebar": {
+ "title": "Plantilla de CRM para WhatsApp",
+ "dashboard": "Panel",
+ "inbox": "Bandeja de entrada",
+ "notifications": "Notificaciones",
+ "contacts": "Contactos",
+ "pipelines": "Embudos",
+ "broadcasts": "Difusiones",
+ "automations": "Automatizaciones",
+ "flows": "Flujos",
+ "aiAgents": "Agentes IA",
+ "settings": "Configuración",
+ "beta": "Beta",
+ "unreadConversations": "{count} {count, plural, =1 {conversación sin leer} other {conversaciones sin leer}}",
+ "unreadNotifications": "{count} {count, plural, =1 {notificación sin leer} other {notificaciones sin leer}}",
+ "roleOwner": "Propietario",
+ "roleAdmin": "Administrador",
+ "roleAgent": "Agente",
+ "roleViewer": "Visualizador",
+ "closeMenu": "Cerrar menú",
+ "defaultUser": "Usuario",
+ "defaultAvatar": "Avatar",
+ "menuProfile": "Perfil",
+ "menuSettings": "Configuración",
+ "menuSignOut": "Cerrar sesión"
+ },
+ "Header": {
+ "dashboard": "Panel",
+ "inbox": "Bandeja de entrada",
+ "notifications": "Notificaciones",
+ "contacts": "Contactos",
+ "pipelines": "Embudos",
+ "broadcasts": "Difusiones",
+ "automations": "Automatizaciones",
+ "settings": "Configuración",
+ "openMenu": "Abrir menú",
+ "openAccountMenu": "Abrir menú de cuenta",
+ "defaultUser": "Usuario",
+ "defaultAvatar": "Avatar",
+ "menuProfile": "Perfil",
+ "menuSettings": "Configuración",
+ "menuSignOut": "Cerrar sesión"
+ },
+ "ModeToggle": {
+ "switchMode": "Cambiar a modo {mode}"
+ },
+ "Dashboard": {
+ "page": {
+ "title": "Panel",
+ "description": "Analítica en vivo de conversaciones, contactos, negocios, difusiones y automatizaciones.",
+ "activeConversations": "Conversaciones activas",
+ "newContactsToday": "Contactos nuevos hoy",
+ "openDealsValue": "Valor de negocios abiertos",
+ "messagesSentToday": "Mensajes enviados hoy",
+ "newTodayVsYesterday": "nuevos hoy vs. ayer",
+ "vsYesterday": "vs. ayer",
+ "openDeals": "{count} {count, plural, =1 {negocio abierto} other {negocios abiertos}}",
+ "noChange": "Sin cambios {suffix}"
+ },
+ "quickActions": {
+ "newContact": "Nuevo contacto",
+ "newDeal": "Nuevo negocio",
+ "newBroadcast": "Nueva difusión",
+ "newAutomation": "Nueva automatización"
+ },
+ "activityFeed": {
+ "title": "Actividad reciente",
+ "viewAll": "Ver todo →",
+ "noActivity": "Aún no hay actividad",
+ "noActivityHint": "Aquí aparecerá la actividad de mensajes, negocios, difusiones y automatizaciones.",
+ "showingOf": "Mostrando {visible} de {totalLoaded}{plus}",
+ "show": "Mostrar",
+ "timeS": "hace {sec}s",
+ "timeM": "hace {min}m",
+ "timeH": "hace {hr}h",
+ "timeD": "hace {day}d"
+ },
+ "conversationsChart": {
+ "title": "Conversaciones a lo largo del tiempo",
+ "description": "Volumen diario de mensajes por dirección",
+ "days": "{count} días",
+ "noActivity": "No hay actividad de mensajes en este rango",
+ "noActivityHint": "Envía o recibe mensajes para empezar a llenar este gráfico.",
+ "incoming": "Entrantes",
+ "outgoing": "Salientes",
+ "tooltipIncoming": "{count} entrantes",
+ "tooltipOutgoing": "{count} salientes",
+ "ariaLabel": "Conversaciones por día"
+ },
+ "pipelineDonut": {
+ "title": "Valor del embudo",
+ "description": "Negocios abiertos por etapa",
+ "noOpenDeals": "Aún no hay negocios abiertos",
+ "noOpenDealsHint": "Crea negocios en Embudos para ver el desglose por etapa aquí.",
+ "dealCount": "{count} {count, plural, =1 {negocio} other {negocios}}",
+ "total": "Total",
+ "ariaLabel": "Valor del embudo por etapa"
+ },
+ "responseTimeChart": {
+ "title": "Tiempo de primera respuesta promedio",
+ "description": "Minutos para responder al primer mensaje sin contestar de un cliente, por día de la semana",
+ "target": "meta {minutes}m",
+ "thisWeek": "Esta semana:",
+ "lastWeek": "Semana pasada:",
+ "noReplies": "Aún no se han registrado respuestas",
+ "noRepliesHint": "Este gráfico se llena a medida que respondes mensajes de clientes."
+ },
+ "emptyState": {
+ "title": "Aún no hay suficientes datos"
+ }
+ },
+ "Inbox": {
+ "page": {
+ "whatsappNotConnected": "WhatsApp® no está conectado. Ve a Configuración para conectar tu cuenta."
+ },
+ "conversationList": {
+ "searchPlaceholder": "Buscar conversaciones...",
+ "filterAll": "Todas",
+ "filterUnread": "Sin leer",
+ "filterOpen": "Abiertas",
+ "filterPending": "Pendientes",
+ "filterClosed": "Cerradas",
+ "tags": "Etiquetas",
+ "company": "Empresa",
+ "allCompanies": "Todas las empresas",
+ "clearAll": "Limpiar todo",
+ "noConversations": "No se encontraron conversaciones",
+ "noMessagesYet": "Aún no hay mensajes",
+ "unknown": "Desconocido"
+ },
+ "messageThread": {
+ "backToConversations": "Volver a conversaciones",
+ "hideContactPanel": "Ocultar panel de contacto",
+ "showContactPanel": "Mostrar panel de contacto",
+ "hideContact": "Ocultar contacto",
+ "showContact": "Mostrar contacto",
+ "refresh": "Actualizar",
+ "refreshConversation": "Actualizar conversación",
+ "status": "Estado",
+ "statusOpen": "Abierta",
+ "statusPending": "Pendiente",
+ "statusClosed": "Cerrada",
+ "assign": "Asignar",
+ "assigned": "Asignado",
+ "unassign": "Quitar asignación",
+ "noTeammates": "No hay compañeros de equipo disponibles",
+ "me": " (yo)",
+ "noMessagesYet": "Aún no hay mensajes",
+ "sendTemplateHint": "Envía una plantilla para iniciar la conversación",
+ "selectConversation": "Selecciona una conversación",
+ "selectConversationHint": "Elige una conversación de la izquierda para empezar a escribir",
+ "today": "Hoy",
+ "yesterday": "Ayer"
+ },
+ "sessionTimer": {
+ "expired": "Expirada",
+ "xhRemaining": "quedan {hours}h",
+ "xmRemaining": "quedan {minutes}m",
+ "noCustomerMessages": "Sin mensajes del cliente"
+ },
+ "composer": {
+ "draftHint": "Toca el ✨ para redactar una respuesta con IA — puedes editarla antes de enviarla",
+ "sessionExpiredHint": "La sesión de 24 horas expiró. Usa una plantilla para retomar el contacto.",
+ "templates": "Plantillas",
+ "readOnlyPlaceholder": "Solo lectura — los visualizadores pueden ver pero no responder",
+ "sessionExpiredPlaceholder": "Sesión expirada - usa una plantilla",
+ "typeMessagePlaceholder": "Escribe un mensaje... (Shift+Enter para nueva línea)",
+ "readOnlyTitle": "Solo lectura — tu rol no puede enviar mensajes",
+ "recording": "Grabando… {current} / {max}",
+ "cancel": "Cancelar",
+ "stopAndAttach": "Detener y adjuntar",
+ "attachMedia": "Adjuntar archivo",
+ "photo": "Foto",
+ "video": "Video",
+ "document": "Documento",
+ "voiceNote": "Nota de voz",
+ "sendTemplate": "Enviar plantilla",
+ "draftWithAI": "Redactar respuesta con IA",
+ "addCaption": "Agregar un pie de foto…",
+ "removeAttachment": "Quitar archivo adjunto",
+ "moreActions": "Más",
+ "interactiveMessage": "Mensaje interactivo",
+ "quickReplies": "Respuestas rápidas",
+ "quickRepliesEmpty": "Aún no hay respuestas rápidas. Agrega algunas en Configuración → Respuestas rápidas.",
+ "saveAsQuickReply": "Guardar como respuesta rápida",
+ "send": "Enviar",
+ "quickReplyNamePrompt": "Nombra esta respuesta rápida:",
+ "quickReplySaved": "Guardada como respuesta rápida.",
+ "quickReplySaveError": "No se pudo guardar la respuesta rápida."
+ },
+ "bubble": {
+ "photo": "Foto",
+ "video": "Video",
+ "audio": "Audio",
+ "unavailable": "{label} no disponible",
+ "document": "Documento",
+ "locationShared": "Ubicación compartida",
+ "template": "Plantilla",
+ "buttonReply": "Respuesta de botón",
+ "interactiveReply": "[Respuesta interactiva]",
+ "unsupported": "[Tipo de mensaje no compatible]",
+ "aiBadge": "IA",
+ "aiBadgeTitle": "Enviado automáticamente por el asistente de IA"
+ },
+ "actions": {
+ "reply": "Responder",
+ "copyText": "Copiar texto",
+ "delete": "Eliminar",
+ "react": "Reaccionar",
+ "reactWith": "Reaccionar con {emoji}",
+ "nothingToCopy": "Nada que copiar",
+ "copied": "Copiado",
+ "copyFailed": "Error al copiar"
+ },
+ "replyQuote": {
+ "photo": "[Foto]",
+ "video": "[Video]",
+ "document": "[Documento]",
+ "audio": "[Audio]",
+ "voiceMessage": "[Nota de voz]",
+ "location": "[Ubicación]",
+ "template": "[Plantilla]",
+ "message": "[Mensaje]",
+ "cancelReply": "Cancelar respuesta"
+ },
+ "templatePicker": {
+ "sendTemplate": "Enviar plantilla",
+ "fillPlaceholders": "Completa los marcadores para generar esta plantilla. Meta exige que se establezca cada variable.",
+ "pickTemplate": "Elige una plantilla de WhatsApp aprobada para enviar a este contacto.",
+ "noApprovedTemplates": "No hay plantillas aprobadas",
+ "noApprovedTemplatesHint": "Aprueba una plantilla en el Administrador de WhatsApp de Meta y luego sincronízala desde Configuración → Plantillas.",
+ "preview": "Vista previa",
+ "headerValuePlaceholder": "Valor para la variable del encabezado",
+ "bodyValuePlaceholder": "Valor para {val}",
+ "urlSuffixValuePlaceholder": "Valor del sufijo de la URL",
+ "finalUrl": "URL final: {url}",
+ "back": "Atrás",
+ "cancel": "Cancelar",
+ "send": "Enviar plantilla"
+ },
+ "sidebar": {
+ "contactInfo": "Información de contacto",
+ "tags": "Etiquetas",
+ "notes": "Notas",
+ "deals": "Negocios",
+ "noTags": "Sin etiquetas",
+ "noDeals": "Sin negocios",
+ "addNotePlaceholder": "Agregar una nota..."
+ },
+ "aiBanner": {
+ "pausedTitle": "El asistente de IA está pausado aquí",
+ "activeText": "El asistente de IA está respondiendo automáticamente",
+ "takeOver": "Tomar el control",
+ "resume": "Reanudar IA",
+ "tookOver": "Has tomado el control de este chat.",
+ "resumed": "IA reanudada.",
+ "updateError": "No se pudo actualizar el asistente de IA.",
+ "networkError": "No se pudo conectar con el servidor."
+ }
+ },
+ "Contacts": {
+ "page": {
+ "title": "Contactos",
+ "subtitle": "Administra tu lista de contactos. {count} contactos en total.",
+ "subtitleZero": "Administra tu lista de contactos.",
+ "customFieldsBtn": "Campos personalizados",
+ "importBtn": "Importar",
+ "addContactBtn": "Agregar contacto",
+ "searchPlaceholder": "Buscar por nombre, teléfono o correo...",
+ "filterByTags": "Filtrar por etiquetas",
+ "clearAll": "Limpiar todo",
+ "noTagsYet": "Aún no hay etiquetas.",
+ "selectedCount": "{count} seleccionados",
+ "clearSelection": "Limpiar",
+ "deleteSelected": "Eliminar seleccionados",
+ "tableColumns": {
+ "name": "Nombre",
+ "phone": "Teléfono",
+ "email": "Correo",
+ "company": "Empresa",
+ "tags": "Etiquetas",
+ "createdAt": "Creado"
+ },
+ "loading": "Cargando contactos...",
+ "noContactsMatch": "Ningún contacto coincide con tus filtros.",
+ "noContactsYet": "Aún no hay contactos.",
+ "addFirstContact": "Agrega tu primer contacto",
+ "unnamed": "Sin nombre",
+ "editAction": "Editar",
+ "deleteAction": "Eliminar",
+ "showingPagination": "Mostrando {start}-{end} de {total}",
+ "pageCount": "Página {page} de {total}",
+ "deleteContactTitle": "Eliminar contacto",
+ "deleteContactDesc": "¿Seguro que quieres eliminar a {name}? Esta acción no se puede deshacer.",
+ "cancel": "Cancelar",
+ "deleteBtn": "Eliminar",
+ "deleteBulkTitle": "Eliminar contactos",
+ "deleteBulkDesc": "¿Seguro que quieres eliminar los {count} contactos seleccionados? Esta acción no se puede deshacer.",
+ "toastFailedLoad": "No se pudieron cargar los contactos",
+ "toastFailedDelete": "No se pudo eliminar el contacto",
+ "toastDeleted": "Contacto eliminado",
+ "toastBulkFailedDelete": "No se pudieron eliminar los contactos",
+ "toastBulkDeleted": "{count} contactos eliminados"
+ },
+ "form": {
+ "addTitle": "Agregar contacto",
+ "addDesc": "Crea un nuevo contacto. El nombre es obligatorio.",
+ "editTitle": "Editar contacto",
+ "editDesc": "Actualiza los datos y etiquetas del contacto.",
+ "nameLabel": "Nombre",
+ "namePlaceholder": "Juana Pérez",
+ "phoneLabel": "Número de teléfono",
+ "phonePlaceholder": "+1234567890",
+ "phoneHint": "Incluye el código de país, p. ej. +1 para EE. UU.",
+ "emailLabel": "Correo electrónico",
+ "emailPlaceholder": "juana@ejemplo.com",
+ "companyLabel": "Empresa",
+ "companyPlaceholder": "Acme Corp",
+ "tagsLabel": "Etiquetas",
+ "tagsPlaceholder": "Selecciona etiquetas...",
+ "noTagsFound": "No se encontraron etiquetas.",
+ "createTagAction": "Crear etiqueta \"{value}\"",
+ "cancel": "Cancelar",
+ "save": "Guardar contacto",
+ "update": "Actualizar",
+ "create": "Crear",
+ "toastSuccessAdd": "Contacto creado con éxito",
+ "toastSuccessEdit": "Contacto actualizado con éxito",
+ "toastError": "No se pudo guardar el contacto",
+ "toastConflict": "Ya existe un contacto con este número de teléfono",
+ "viewExisting": "Ver {name}",
+ "dupExact": "Ya existe un contacto con este número de teléfono.",
+ "dupSimilar": "Ya existe un contacto con un número muy similar.",
+ "loadingTags": "Cargando etiquetas...",
+ "noTagsAvailable": "No hay etiquetas disponibles. Créalas en Configuración.",
+ "phoneRequired": "El número de teléfono es obligatorio"
+ },
+ "detailView": {
+ "tabs": {
+ "details": "Detalles",
+ "activity": "Actividad",
+ "notes": "Notas",
+ "custom": "Campos personalizados",
+ "deals": "Negocios",
+ "noTags": "Sin etiquetas",
+ "noDeals": "Sin negocios",
+ "addNotePlaceholder": "Agregar una nota..."
+ },
+ "actions": {
+ "edit": "Editar",
+ "message": "Mensaje",
+ "delete": "Eliminar"
+ },
+ "contactInfo": "Información de contacto",
+ "unnamed": "Desconocido",
+ "contactDetailsDesc": "Datos del contacto",
+ "phone": "Teléfono",
+ "email": "Correo",
+ "company": "Empresa",
+ "createdAt": "Fecha de creación",
+ "customFields": "Campos personalizados",
+ "noCustomFields": "No hay campos personalizados definidos. Créalos en Configuración.",
+ "manageFields": "Administrar campos",
+ "enterCustomField": "Ingresa {name}...",
+ "activityTab": {
+ "title": "Historial de actividad",
+ "noActivity": "Aún no hay actividad."
+ },
+ "notesTab": {
+ "title": "Notas internas",
+ "placeholder": "Escribe una nota...",
+ "save": "Agregar nota",
+ "noNotes": "Aún no hay notas."
+ },
+ "dealsTab": {
+ "noDeals": "Aún no hay negocios"
+ },
+ "tagsTab": {
+ "clickTagDesc": "Haz clic en una etiqueta para agregarla o quitarla de este contacto.",
+ "noTagsAvailable": "No hay etiquetas disponibles. Créalas en Configuración."
+ },
+ "sendTemplateBtn": "Enviar plantilla",
+ "saveChangesBtn": "Guardar cambios",
+ "saveCustomFieldsBtn": "Guardar campos personalizados",
+ "toastPhoneRequired": "El número de teléfono es obligatorio",
+ "toastUpdateFailed": "No se pudo actualizar el contacto",
+ "toastUpdated": "Contacto actualizado",
+ "toastNotAuthenticated": "No autenticado",
+ "toastNoteAddFailed": "No se pudo agregar la nota",
+ "toastNoteAdded": "Nota agregada",
+ "toastNoteDeleteFailed": "No se pudo eliminar la nota",
+ "toastNoteDeleted": "Nota eliminada",
+ "toastCustomFieldsSaved": "Campos personalizados guardados",
+ "toastCustomFieldsFailed": "No se pudieron guardar los campos personalizados",
+ "toastTemplateSent": "Plantilla \"{name}\" enviada",
+ "toastTemplateFailed": "No se pudo enviar la plantilla: {reason}"
+ },
+ "importModal": {
+ "title": "Importar contactos",
+ "desc": "Sube un CSV con una columna obligatoria phone. Opcional: name, email, company, tags (separadas por comas; usa comillas en celdas con varias etiquetas).",
+ "uploadDropzone": "Haz clic para elegir un archivo CSV",
+ "uploadHint": ".csv hasta el límite de tu navegador",
+ "rowsReady": "{count} fila lista",
+ "rowsReady_plural": "{count} filas listas",
+ "preview": "Vista previa · primeras {count}",
+ "previewTags": "{tags} etiqueta · {contacts} contacto",
+ "previewTags_plural": "{tags} etiquetas · {contacts} contactos",
+ "moreRows": "+ {count} fila más sin mostrar",
+ "moreRows_plural": "+ {count} filas más sin mostrar",
+ "importComplete": "Importación completa",
+ "resultImported": "{count} importados",
+ "resultTags": "{count} etiqueta asignada",
+ "resultTags_plural": "{count} etiquetas asignadas",
+ "resultSkipped": "{count} omitidos",
+ "resultFailed": "{count} fallidos",
+ "willBeCreated": "{name} (se creará al importar)",
+ "columns": {
+ "phone": "Teléfono",
+ "name": "Nombre",
+ "email": "Correo",
+ "company": "Empresa",
+ "tags": "Etiquetas"
+ },
+ "cancel": "Cancelar",
+ "close": "Cerrar",
+ "importBtn": "Importar {count} contacto",
+ "importBtn_plural": "Importar {count} contactos",
+ "toastNoValidRows": "No se encontraron filas válidas. Asegúrate de que el CSV tenga un encabezado de columna \"phone\".",
+ "toastTagsWarning": "Contactos importados, pero algunas asignaciones de etiquetas fallaron.",
+ "toastImported": "{count} contacto importado",
+ "toastImported_plural": "{count} contactos importados",
+ "toastTagsAssigned": "{count} asignación de etiqueta aplicada",
+ "toastTagsAssigned_plural": "{count} asignaciones de etiquetas aplicadas",
+ "toastTagsSkipped": "Se omitieron etiquetas desconocidas (créalas primero en Configuración): {sample}{more}",
+ "toastSkipped": "{count} duplicado omitido",
+ "toastSkipped_plural": "{count} duplicados omitidos",
+ "toastFailed": "{count} contacto no se pudo importar",
+ "toastFailed_plural": "{count} contactos no se pudieron importar",
+ "toastError": "Falló la importación"
+ },
+ "customFields": {
+ "title": "Campos personalizados",
+ "desc": "Define campos adicionales de contacto (p. ej. código postal, origen del lead). Aparecen en cada contacto y en la acción de automatización “Actualizar campo de contacto”.",
+ "addField": "Agregar",
+ "fieldName": "Nombre del nuevo campo…",
+ "loading": "Cargando…",
+ "empty": "Aún no hay campos personalizados.",
+ "renameAria": "Renombrar {name}",
+ "deleteTitle": "Eliminar campo",
+ "deleteConfirm": "¿Eliminar \"{name}\"? Esto también elimina su valor guardado en cada contacto. No se puede deshacer.",
+ "toastNoAccount": "Tu perfil no está vinculado a ninguna cuenta.",
+ "toastDuplicate": "Ya existe un campo llamado \"{name}\".",
+ "toastCreateFailed": "No se pudo crear el campo. Es posible que no tengas permiso.",
+ "toastCreated": "Se creó \"{name}\".",
+ "toastRenameFailed": "No se pudo renombrar el campo.",
+ "toastDeleteFailed": "No se pudo eliminar el campo.",
+ "toastDeleted": "Se eliminó \"{name}\"."
+ }
+ },
+ "Pipelines": {
+ "page": {
+ "selectPipeline": "Seleccionar embudo",
+ "noPipelinesYet": "Aún no hay embudos",
+ "managePipelines": "Administrar embudos",
+ "addPipeline": "Agregar embudo",
+ "addDeal": "Agregar negocio",
+ "createToStartTracking": "Crea un embudo para empezar a dar seguimiento a negocios",
+ "createPipeline": "Crear embudo",
+ "newPipeline": "Nuevo embudo",
+ "pipelineName": "Nombre del embudo",
+ "pipelineNamePlaceholder": "p. ej., Ventas empresariales",
+ "defaultStagesDesc": "Las etapas predeterminadas (Nuevo lead → Ganado) se crearán automáticamente.",
+ "cancel": "Cancelar",
+ "creating": "Creando...",
+ "createPipelineBtn": "Crear embudo",
+ "toastFailedLoadPipelines": "No se pudieron cargar los embudos",
+ "toastFailedMoveDeal": "No se pudo mover el negocio",
+ "toastFailedCreatePipeline": "No se pudo crear el embudo",
+ "toastPipelineCreated": "Embudo creado",
+ "toastNotLinkedToAccount": "Tu perfil no está vinculado a ninguna cuenta."
+ },
+ "board": {
+ "dropDealHere": "Suelta un negocio aquí",
+ "addDeal": "Agregar negocio"
+ },
+ "card": {
+ "noContact": "Sin contacto",
+ "won": "Ganado",
+ "lost": "Perdido"
+ },
+ "form": {
+ "editDeal": "Editar negocio",
+ "newDeal": "Nuevo negocio",
+ "title": "Título",
+ "titlePlaceholder": "Título del negocio",
+ "contact": "Contacto",
+ "selectContact": "Selecciona un contacto",
+ "linkToConversation": "Vincular a conversación",
+ "value": "Valor",
+ "currency": "Moneda",
+ "expectedCloseDate": "Fecha estimada de cierre",
+ "stage": "Etapa",
+ "assignedTo": "Asignado a",
+ "unassigned": "Sin asignar",
+ "notes": "Notas",
+ "notesPlaceholder": "Agregar notas...",
+ "status": "Estado",
+ "markAsWon": "Marcar como ganado",
+ "markAsLost": "Marcar como perdido",
+ "reopenDeal": "Reabrir negocio",
+ "cancel": "Cancelar",
+ "saving": "Guardando...",
+ "saveChanges": "Guardar cambios",
+ "createDeal": "Crear negocio",
+ "deletePrompt": "¿Eliminar este negocio?",
+ "confirm": "Confirmar",
+ "deleting": "Eliminando...",
+ "deleteDeal": "Eliminar negocio",
+ "toastRequired": "El título, el contacto y la etapa son obligatorios",
+ "toastFailedSave": "No se pudo guardar el negocio",
+ "toastNotSignedIn": "No has iniciado sesión",
+ "toastNotLinked": "Tu perfil no está vinculado a ninguna cuenta.",
+ "toastFailedCreate": "No se pudo crear el negocio",
+ "toastUpdated": "Negocio actualizado",
+ "toastCreated": "Negocio creado",
+ "toastFailedStatus": "No se pudo actualizar el estado del negocio",
+ "toastMarkedWon": "Marcado como ganado",
+ "toastMarkedLost": "Marcado como perdido",
+ "toastReopened": "Negocio reabierto",
+ "toastFailedDelete": "No se pudo eliminar el negocio",
+ "toastDeleted": "Negocio eliminado"
+ },
+ "settings": {
+ "managePipeline": "Administrar embudo",
+ "deletePipeline": "Eliminar embudo",
+ "deletePipelineDesc": "Esto archivará todos los negocios de este embudo. No se puede deshacer.",
+ "cancel": "Cancelar",
+ "deleting": "Eliminando...",
+ "deletePipelineBtn": "Eliminar embudo",
+ "pipelineName": "Nombre del embudo",
+ "stages": "Etapas",
+ "newStageNamePlaceholder": "Nombre de la nueva etapa",
+ "add": "Agregar",
+ "createNewPipeline": "Crear un nuevo embudo",
+ "saving": "Guardando...",
+ "saveChanges": "Guardar cambios",
+ "dragToReorder": "Arrastra para reordenar",
+ "changeColor": "Cambiar color",
+ "toastFailedSave": "No se pudo guardar el embudo",
+ "toastSaved": "Embudo guardado",
+ "toastFailedAddStage": "No se pudo agregar la etapa",
+ "toastMoveOrDeleteDeals": "Primero mueve o elimina los negocios de esta etapa",
+ "toastFailedDeleteStage": "No se pudo eliminar la etapa",
+ "toastFailedDeletePipeline": "No se pudo eliminar el embudo",
+ "toastDeleted": "Embudo eliminado"
+ },
+ "analytics": {
+ "totalDeals": "Total de negocios",
+ "totalDealsTooltip": "Cantidad de todos los negocios de este embudo que no están marcados como perdidos. Los negocios ganados se siguen incluyendo.",
+ "pipelineValue": "Valor del embudo",
+ "pipelineValueTooltip": "Suma de los valores en dólares de todos los negocios de este embudo, excluyendo los marcados como perdidos.",
+ "avgDealSize": "Tamaño promedio del negocio",
+ "avgDealSizeTooltip": "Valor del embudo dividido entre el total de negocios: el valor promedio de un negocio no perdido.",
+ "weightedValue": "Valor ponderado",
+ "weightedValueTooltip": "Ingreso esperado: el valor de cada negocio abierto × la probabilidad de su etapa. La primera etapa ≈ 10%, las etapas avanzan hasta 90%, Ganado = 100%. Los negocios perdidos se excluyen.",
+ "wonThisMonth": "Ganados este mes",
+ "wonThisMonthTooltip": "Negocios marcados como ganados desde el primer día del mes actual.",
+ "lostThisMonth": "Perdidos este mes",
+ "lostThisMonthTooltip": "Negocios marcados como perdidos desde el primer día del mes actual.",
+ "howCalculated": "Cómo se calcula {label}"
+ }
+ },
+ "Broadcasts": {
+ "page": {
+ "title": "Difusiones",
+ "subtitle": "Envía mensajes masivos a tus contactos usando plantillas aprobadas.",
+ "newBroadcast": "Nueva difusión",
+ "noBroadcastsYet": "Aún no hay difusiones",
+ "createFirst": "Crea tu primera difusión para llegar a tus contactos a gran escala.",
+ "table": {
+ "name": "Nombre",
+ "template": "Plantilla",
+ "recipients": "Destinatarios",
+ "delivery": "Entrega",
+ "read": "Leído",
+ "status": "Estado",
+ "date": "Fecha"
+ },
+ "errorLoad": "No se pudieron cargar las difusiones",
+ "retry": "Reintentar"
+ },
+ "status": {
+ "draft": "Borrador",
+ "scheduled": "Programada",
+ "sending": "Enviando",
+ "sent": "Enviada",
+ "failed": "Fallida",
+ "pending": "Pendiente",
+ "delivered": "Entregada",
+ "read": "Leída",
+ "replied": "Respondida",
+ "unknown": "Desconocido"
+ },
+ "detail": {
+ "backToBroadcasts": "Volver a Difusiones",
+ "notFound": "Difusión no encontrada",
+ "template": "Plantilla: {name}",
+ "createdAt": "Creada el {date}",
+ "deletePrompt": "¿Eliminar esta difusión?",
+ "cancel": "Cancelar",
+ "confirm": "Confirmar",
+ "deleting": "Eliminando…",
+ "delete": "Eliminar",
+ "cannotDeleteSending": "No se puede eliminar mientras una difusión se está enviando",
+ "deleteHover": "Eliminar esta difusión",
+ "funnel": "Embudo",
+ "recipientsHeader": "Destinatarios ({filtered} de {total})",
+ "recipientsHeaderAll": "Destinatarios ({total})",
+ "allStatuses": "Todos los estados",
+ "exportCsv": "Exportar CSV",
+ "noRecipients": "No se encontraron destinatarios.",
+ "noRecipientsFilter": "Ningún destinatario coincide con este filtro.",
+ "table": {
+ "contact": "Contacto",
+ "phone": "Teléfono",
+ "status": "Estado",
+ "sent": "Enviado",
+ "delivered": "Entregado",
+ "read": "Leído",
+ "error": "Error"
+ },
+ "stats": {
+ "totalRecipients": "Total de destinatarios",
+ "sent": "Enviados",
+ "delivered": "Entregados",
+ "read": "Leídos",
+ "replied": "Respondidos",
+ "failed": "Fallidos"
+ },
+ "toastFailedDelete": "No se pudo eliminar: {error}",
+ "toastDeleted": "Difusión eliminada"
+ },
+ "new": {
+ "title": "Nueva difusión",
+ "subtitle": "Crea y envía un mensaje de difusión a tus contactos.",
+ "steps": {
+ "template": "Plantilla",
+ "audience": "Audiencia",
+ "personalize": "Personalizar",
+ "send": "Enviar"
+ },
+ "toastGiveName": "Ponle un nombre a la difusión antes de guardar un borrador.",
+ "toastNotSignedIn": "No has iniciado sesión.",
+ "toastNotLinked": "Tu perfil no está vinculado a ninguna cuenta.",
+ "toastFailedDraft": "No se pudo guardar el borrador: {error}",
+ "toastDraftSaved": "Borrador guardado"
+ },
+ "wizard": {
+ "next": "Siguiente",
+ "back": "Atrás",
+ "cancel": "Cancelar",
+ "chooseTemplate": {
+ "title": "Elige una plantilla",
+ "subtitle": "Selecciona una plantilla de mensaje aprobada para tu difusión.",
+ "noTemplates": "No hay plantillas disponibles.",
+ "createFirst": "Crea primero una plantilla en Configuración.",
+ "errorLoad": "No se pudieron cargar las plantillas"
+ },
+ "selectAudience": {
+ "title": "Selecciona la audiencia",
+ "subtitle": "Elige quién recibirá esta difusión.",
+ "method": {
+ "all": "Todos los contactos",
+ "tags": "Filtrar por etiquetas",
+ "customField": "Campo personalizado",
+ "csv": "Subir CSV"
+ },
+ "allDesc": "Enviar a los {count} contactos de tu base de datos.",
+ "allDescLoading": "Enviar a todos los contactos de tu base de datos.",
+ "tagDesc": "Enviar a contactos que coincidan con etiquetas específicas.",
+ "customFieldDesc": "Enviar a contactos que coincidan con una regla de campo personalizado.",
+ "csvDesc": "Sube un CSV con números de teléfono (y nombres opcionales).",
+ "selectTags": "Seleccionar etiquetas",
+ "searchTags": "Buscar etiquetas...",
+ "noTagsFound": "No se encontraron etiquetas.",
+ "excludeTags": "Excluir etiquetas (opcional)",
+ "searchExcludeTags": "Buscar etiquetas a excluir...",
+ "field": "Campo",
+ "selectField": "Selecciona un campo...",
+ "operator": "Operador",
+ "operatorIs": "Es",
+ "operatorIsNot": "No es",
+ "operatorContains": "Contiene",
+ "value": "Valor",
+ "valuePlaceholder": "Valor a coincidir",
+ "uploadCsv": "Subir archivo CSV",
+ "csvFormatDesc": "Debe contener una columna 'phone' (formato E.164). La columna 'name' es opcional.",
+ "csvContactsFound": "{count} contacto(s) válido(s) encontrados en el CSV.",
+ "errorLoadTags": "No se pudieron cargar las etiquetas",
+ "errorLoadFields": "No se pudieron cargar los campos personalizados",
+ "errorCsvParse": "No se pudo procesar el archivo CSV.",
+ "errorCsvMissingPhone": "El CSV debe tener una columna 'phone'."
+ },
+ "personalize": {
+ "title": "Personalizar variables",
+ "subtitle": "Asocia las variables de tu plantilla con campos de contacto o proporciona valores fijos.",
+ "headerImage": "Imagen de encabezado",
+ "headerImageDesc": "Esta plantilla requiere una imagen de encabezado.",
+ "imageUrl": "URL de la imagen",
+ "imageUrlPlaceholder": "https://ejemplo.com/imagen.jpg",
+ "variables": "Variables",
+ "varDesc": "Establece valores para {{name}}",
+ "type": "Tipo",
+ "typeStatic": "Valor fijo",
+ "typeContact": "Campo de contacto",
+ "typeCustom": "Campo personalizado",
+ "staticValue": "Valor fijo",
+ "contactField": "Campo de contacto",
+ "selectContactField": "Selecciona un campo...",
+ "customField": "Campo personalizado",
+ "selectCustomField": "Selecciona un campo personalizado...",
+ "preview": "Vista previa",
+ "previewSample": "Mostrando vista previa con datos de muestra.",
+ "noPreview": "Vista previa no disponible.",
+ "fieldMap": {
+ "name": "Nombre",
+ "phone": "Teléfono",
+ "email": "Correo"
+ }
+ },
+ "scheduleSend": {
+ "title": "Revisar y enviar",
+ "subtitle": "Ponle un nombre a tu difusión y confirma el envío.",
+ "broadcastName": "Nombre de la difusión",
+ "broadcastNamePlaceholder": "p. ej., Promo de verano",
+ "summary": "Resumen",
+ "template": "Plantilla:",
+ "audience": "Audiencia:",
+ "audienceAll": "Todos los contactos",
+ "audienceTags": "Filtrado por etiquetas",
+ "audienceField": "Filtrado por campo personalizado",
+ "audienceCsv": "Carga de CSV",
+ "variables": "Variables:",
+ "variablesMapped": "{count} asignadas",
+ "saveDraft": "Guardar como borrador",
+ "sendNow": "Enviar difusión",
+ "sending": "Enviando...",
+ "preparing": "Preparando...",
+ "progressDesc": "Procesando {current} de {total} destinatarios...",
+ "toastFailedPreview": "No se pudieron cargar los datos de vista previa"
+ }
+ }
+ },
+ "Automations": {
+ "list": {
+ "title": "Automatizaciones",
+ "subtitle": "Crea flujos de trabajo que reaccionan automáticamente a eventos de WhatsApp®.",
+ "create": "Crear automatización",
+ "retry": "Reintentar",
+ "templatesTitle": "Plantillas de inicio rápido",
+ "emptyTitle": "Aún no hay automatizaciones",
+ "emptyDesc": "Elige una plantilla arriba o crea una desde cero.",
+ "runs": "{count} ejecución",
+ "runsPlural": "{count} ejecuciones",
+ "lastRun": "última {time}",
+ "activate": "Activar",
+ "deactivate": "Desactivar",
+ "edit": "Editar",
+ "duplicate": "Duplicar",
+ "viewLogs": "Ver registros",
+ "delete": "Eliminar",
+ "cancel": "Cancelar",
+ "deleteTitle": "Eliminar automatización",
+ "deleteDesc": "Esto elimina permanentemente {name} y su historial de ejecución. No se puede deshacer.",
+ "toasts": {
+ "activated": "Automatización activada",
+ "paused": "Automatización pausada",
+ "duplicated": "Automatización duplicada",
+ "deleted": "Automatización eliminada",
+ "updateError": "No se pudo actualizar",
+ "duplicateError": "No se pudo duplicar",
+ "deleteError": "No se pudo eliminar"
+ }
+ },
+ "edit": {
+ "loading": "Cargando...",
+ "loadError": "No se pudo cargar ({status})",
+ "back": "Volver a Automatizaciones"
+ },
+ "logs": {
+ "title": "Registros de ejecución",
+ "loadError": "No se pudieron cargar los registros",
+ "emptyTitle": "Aún no hay ejecuciones",
+ "emptyDesc": "Activa esta automatización para ver las ejecuciones aquí.",
+ "unknownContact": "Contacto desconocido",
+ "step": "{count} paso",
+ "stepPlural": "{count} pasos",
+ "noSteps": "No se registraron pasos.",
+ "status": {
+ "success": "éxito",
+ "partial": "parcial",
+ "failed": "fallido"
+ },
+ "back": "Atrás",
+ "backAria": "Atrás"
+ },
+ "builder": {
+ "steps": {
+ "send_message": "Enviar mensaje",
+ "send_buttons": "Enviar botones",
+ "send_list": "Enviar lista",
+ "send_template": "Enviar plantilla",
+ "add_tag": "Agregar etiqueta",
+ "remove_tag": "Quitar etiqueta",
+ "assign_conversation": "Asignar conversación",
+ "update_contact_field": "Actualizar campo de contacto",
+ "create_deal": "Crear negocio",
+ "wait": "Esperar",
+ "condition": "Condición (Si/Sino)",
+ "send_webhook": "Enviar webhook",
+ "close_conversation": "Cerrar conversación"
+ },
+ "triggers": {
+ "new_message_received": {
+ "label": "Mensaje nuevo recibido",
+ "hint": "Cualquier mensaje entrante"
+ },
+ "first_inbound_message": {
+ "label": "Primer mensaje del contacto",
+ "hint": "La primera vez que este contacto te escribe (también funciona con contactos agregados manualmente)"
+ },
+ "keyword_match": {
+ "label": "Coincidencia de palabra clave",
+ "hint": "El mensaje contiene palabra(s) clave específicas"
+ },
+ "interactive_reply": {
+ "label": "Respuesta de botón / lista",
+ "hint": "El cliente toca un botón o fila de lista cuyo id coincide — encadena menús entre automatizaciones"
+ },
+ "new_contact_created": {
+ "label": "Contacto nuevo creado",
+ "hint": "Cuando se crea automáticamente un contacto a partir de un mensaje entrante"
+ },
+ "conversation_assigned": {
+ "label": "Conversación asignada",
+ "hint": "Cuando se asigna a un agente"
+ },
+ "tag_added": {
+ "label": "Etiqueta agregada",
+ "hint": "Cuando se agrega una etiqueta a un contacto"
+ },
+ "time_based": {
+ "label": "Basado en tiempo",
+ "hint": "En un horario recurrente"
+ }
+ },
+ "trigger": "Disparador",
+ "triggerType": "Tipo de disparador",
+ "keywords": "Palabras clave",
+ "keywordsHint": "Lista separada por comas (p. ej. hola, ayuda, comprar)",
+ "replyIds": "IDs de respuesta",
+ "replyIdsHint": "si, no, mas_info",
+ "replyIdsHelp": "IDs de botón/fila de lista a coincidir, exactos, separados por comas. Usa los mismos ids que definiste en un paso Enviar botones / Enviar lista.",
+ "schedule": "Programación",
+ "scheduleHint": "Expresión cron (p. ej. 0 9 * * 1-5)",
+ "active": "Activo",
+ "activeAria": "Activo",
+ "save": "Guardar",
+ "saveDraft": "Guardar borrador",
+ "untitled": "Automatización sin título",
+ "backToAutomations": "Volver a automatizaciones",
+ "toasts": {
+ "saved": "Automatización guardada",
+ "created": "Automatización creada",
+ "saveFailed": "Error al guardar"
+ },
+ "tags": {
+ "select": "Selecciona una etiqueta…",
+ "placeholder": "ID de etiqueta",
+ "unknown": "{id} (etiqueta desconocida)"
+ },
+ "fields": {
+ "name": "Nombre",
+ "email": "Correo",
+ "company": "Empresa",
+ "customFields": "Campos personalizados",
+ "unknown": "{id} (campo desconocido)"
+ },
+ "agents": {
+ "select": "Selecciona un agente…",
+ "placeholder": "ID de agente",
+ "unknown": "{id} (agente desconocido)"
+ },
+ "pipelines": {
+ "pipelineLabel": "Embudo",
+ "pipelineIdLabel": "ID de embudo",
+ "stageLabel": "Etapa",
+ "stageIdLabel": "ID de etapa",
+ "selectPipeline": "Selecciona un embudo…",
+ "selectStage": "Selecciona una etapa…",
+ "selectPipelineFirst": "Selecciona primero un embudo…",
+ "unknownPipeline": "{id} (embudo desconocido)",
+ "unknownStage": "{id} (etapa desconocida)"
+ },
+ "templates": {
+ "templateLabel": "Plantilla",
+ "templateNameLabel": "Nombre de la plantilla",
+ "languageLabel": "Idioma",
+ "select": "Selecciona una plantilla…",
+ "unknown": "{name} ({lang}) — no está en la lista aprobada",
+ "unknownLang": "desconocido"
+ },
+ "branches": {
+ "yes": "Sí",
+ "no": "No"
+ },
+ "addStep": "Agregar paso",
+ "config": {
+ "messageText": "Texto del mensaje",
+ "placeholderMessageText": "¡Hola! Gracias por escribirnos…",
+ "tagLabel": "Etiqueta",
+ "modeLabel": "Modo",
+ "modes": {
+ "round_robin": "Rotación automática",
+ "specific": "Agente específico"
+ },
+ "agentLabel": "Agente",
+ "fieldLabel": "Campo",
+ "valueLabel": "Valor",
+ "placeholderValue": "Texto o {{ vars.x }} / {{ message.text }}",
+ "titleLabel": "Título",
+ "amountLabel": "Cantidad",
+ "unitLabel": "Unidad",
+ "units": {
+ "minutes": "Minutos",
+ "hours": "Horas",
+ "days": "Días"
+ },
+ "subjectLabel": "Asunto",
+ "subjects": {
+ "tag_presence": "Presencia de etiqueta",
+ "contact_field": "Campo de contacto",
+ "message_content": "Contenido del mensaje",
+ "time_of_day": "Hora del día"
+ },
+ "operandLabel": "Operando",
+ "placeholderTime": "HH:mm-HH:mm",
+ "placeholderContact": "nombre / correo / empresa",
+ "placeholderTag": "id de etiqueta",
+ "urlLabel": "URL",
+ "headersLabel": "Encabezados",
+ "placeholderHeaders": "{\"Authorization\": \"Bearer ...\"}",
+ "bodyTemplateLabel": "Plantilla del cuerpo",
+ "placeholderBody": "{\"id\": \"{{ contact.id }}\"}",
+ "matchType": "Tipo de coincidencia",
+ "matchContains": "Contiene",
+ "matchExact": "Exacto",
+ "closeConversationHint": "Establece el estado de la conversación en \"cerrada\". No requiere configuración."
+ }
+ }
+ },
+ "Flows": {
+ "list": {
+ "title": "Flujos",
+ "beta": "Beta",
+ "description": "Crea conversaciones de WhatsApp ramificadas y guiadas por botones. Útil para menús, preguntas frecuentes y clasificación antes de que intervenga un humano.",
+ "newFlow": "Nuevo flujo",
+ "statusDraft": "Borrador",
+ "statusActive": "Activo",
+ "statusArchived": "Archivado",
+ "loadError": "No se pudieron cargar los flujos.",
+ "createError": "No se pudo crear el flujo.",
+ "cloneError": "Error al duplicar",
+ "deleteConfirm": "¿Eliminar \"{name}\"? Cualquier ejecución activa terminará de inmediato.",
+ "deleteSuccess": "Flujo eliminado.",
+ "deleteError": "No se pudo eliminar el flujo.",
+ "emptyTitle": "Aún no hay flujos",
+ "emptyDesc": "Crea tu primera conversación — un menú de bienvenida, una consulta de pedidos, un bot de preguntas frecuentes. Los clientes tocan botones; el bot los dirige a la respuesta correcta (o al agente correcto).",
+ "createFirst": "Crea tu primer flujo",
+ "createTitle": "Crear un nuevo flujo",
+ "createDesc": "Empieza desde una plantilla o crea uno desde cero.",
+ "startTemplate": "Empezar desde una plantilla",
+ "nodeCount": "{count} {count, plural, =1 {nodo} other {nodos}}",
+ "startBlank": "O empezar en blanco",
+ "placeholderName": "p. ej. Menú de bienvenida",
+ "cancel": "Cancelar",
+ "createBlank": "Crear flujo en blanco",
+ "runCount": "{count} {count, plural, =1 {ejecución} other {ejecuciones}}",
+ "edit": "Editar",
+ "delete": "Eliminar",
+ "triggerKeywordNone": "Se activa por palabra clave (ninguna definida)",
+ "triggerKeyword": "Se activa con: {keywords}",
+ "triggerFirstInbound": "Se activa con el primer mensaje entrante de un contacto",
+ "triggerManual": "Activación manual"
+ },
+ "edit": {
+ "notFound": "Flujo no encontrado.",
+ "backToFlows": "← Volver a flujos",
+ "loadError": "No se pudo cargar el flujo."
+ },
+ "logs": {
+ "title": "Ejecuciones",
+ "description": "Las 50 ejecuciones más recientes de este flujo. Expande una fila para ver el registro paso a paso del motor.",
+ "emptyState": "Aún no hay ejecuciones. Activa el flujo desde un número de WhatsApp personal para verlo aparecer aquí.",
+ "loadError": "No se pudieron cargar las ejecuciones.",
+ "unknownContact": "Contacto desconocido",
+ "atNode": "en {node}",
+ "started": "Iniciado {time}",
+ "reprompts": "{count} reintentos de pregunta",
+ "ranFor": "duró {duration}",
+ "capturedVars": "Variables capturadas ({count})",
+ "noEvents": "No se registraron eventos para esta ejecución.",
+ "statusActive": "Activo",
+ "statusCompleted": "Completado",
+ "statusHandedOff": "Transferido",
+ "statusTimedOut": "Tiempo agotado",
+ "statusPaused": "Pausado por agente",
+ "statusFailed": "Fallido"
+ },
+ "builder": {
+ "triggerTitle": "Disparador",
+ "whenLabel": "Cuando…",
+ "triggerKeywordTitle": "Un mensaje contiene una palabra clave",
+ "triggerFirstInboundTitle": "Primer mensaje entrante del cliente",
+ "triggerManualTitle": "Solo manual (sin activación automática)",
+ "keywordsLabel": "Palabras clave (separadas por comas)",
+ "keywordsPlaceholder": "soporte, ayuda, hola",
+ "entryNodeTitle": "Nodo de entrada:",
+ "entryNodePlaceholder": "Elige el primer nodo…",
+ "nodesTitle": "Nodos ({count})",
+ "nodesEmpty": "Agrega un nodo Inicio, luego un nodo Enviar botones, y después un Transferir — esa es la forma de menú de bienvenida del brief.",
+ "addNode": "Agregar nodo",
+ "setAsEntry": "Definir como entrada",
+ "removeNode": "Quitar nodo",
+ "showAdvanced": "Mostrar avanzado",
+ "hideAdvanced": "Ocultar avanzado",
+ "nodeKeyLabel": "Clave del nodo (identificador interno — mantenla estable para la analítica)",
+ "replyIdsHint": "Los IDs de respuesta de cada opción se muestran en línea arriba. WhatsApp los devuelve cuando un cliente toca; normalmente no necesitas tocarlos.",
+ "badgeEntry": "Entrada",
+ "noNodesYet": "Aún no hay nodos.",
+ "deleteNode": "Eliminar nodo",
+ "canvasView": "Lienzo",
+ "listView": "Lista",
+ "noNodeSelected": "Ningún nodo seleccionado",
+ "noNodeDesc": "Selecciona un nodo en el lienzo para editar su configuración.",
+ "form": {
+ "advancesTo": "Avanza a",
+ "textToCustomer": "Texto enviado al cliente",
+ "promptToCustomer": "Pregunta enviada al cliente",
+ "varKeyLabel": "Clave de variable (guardada en flow_runs.vars; alfanumérica + guion bajo)",
+ "varKeyPlaceholder": "p. ej. nombre, correo, empresa",
+ "varKeyHelp": "Interpola en las preguntas posteriores y notas de transferencia con",
+ "advanceAfterCapture": "Después de capturar, avanzar a",
+ "internalNote": "Nota interna (para el agente que la reciba)",
+ "endNodeHelp": "Nodo terminal. Cuando el motor llega a este nodo, la ejecución se marca como completada. No requiere configuración.",
+ "bodyText": "Texto del cuerpo",
+ "footerText": "Pie de página (opcional, 60 caracteres)",
+ "buttonsHelp": "Botones (1–3) — cada uno dirige a un nodo siguiente distinto",
+ "optionTitlePlaceholder": "Título visible (≤20 caracteres)",
+ "nextNodePlaceholder": "Siguiente nodo…",
+ "addButton": "Agregar botón",
+ "buttonLabel": "Etiqueta del botón para expandir (≤20 caracteres)",
+ "rowsHelp": "Filas (1–10 en total entre todas las secciones)",
+ "sectionTitlePlaceholder": "Título de la sección {count} (opcional)",
+ "rowTitlePlaceholder": "Título de fila (≤24)",
+ "addRow": "Agregar fila",
+ "addSection": "Agregar sección",
+ "ifLabel": "Si",
+ "capturedVariable": "Variable capturada",
+ "contactHasTag": "El contacto tiene la etiqueta",
+ "contactField": "Campo de contacto",
+ "varName": "nombre de variable",
+ "tagLabel": "Etiqueta",
+ "fieldLabel": "Campo",
+ "pickTag": "Elige una etiqueta…",
+ "pickField": "Elige un campo…",
+ "operatorLabel": "Operador",
+ "isPresent": "está presente",
+ "isAbsent": "está ausente",
+ "equals": "es igual a",
+ "contains": "contiene",
+ "valueLabel": "Valor",
+ "ifTrueAdvance": "Si es verdadero → avanzar a",
+ "ifFalseAdvance": "Si es falso → avanzar a",
+ "actionLabel": "Acción",
+ "addTag": "Agregar etiqueta",
+ "removeTag": "Quitar etiqueta",
+ "tagUuidPlaceholder": "UUID de etiqueta",
+ "thenAdvanceTo": "Luego avanzar a",
+ "mediaTypeLabel": "Tipo de archivo",
+ "imageLabel": "Imagen (PNG, JPEG, WebP)",
+ "videoLabel": "Video (MP4, 3GP)",
+ "documentLabel": "Documento (PDF, Word, Excel, PowerPoint, TXT)",
+ "fileLabel": "Archivo",
+ "removeFile": "Quitar archivo",
+ "uploading": "Subiendo…",
+ "clickToUpload": "Haz clic para subir (máx. 16 MB)",
+ "captionLabel": "Pie de foto (opcional, se muestra debajo del archivo)",
+ "filenameLabel": "Nombre de archivo mostrado al cliente (solo documentos)",
+ "filenamePlaceholder": "factura.pdf",
+ "advanceAfterSending": "Después de enviar, avanzar a",
+ "pickNextNode": "Elige un siguiente nodo…",
+ "none": "— Ninguno —"
+ },
+ "categories": {
+ "messaging": "Mensajería",
+ "logic": "Lógica y datos",
+ "flow": "Control de flujo"
+ },
+ "nodes": {
+ "start": {
+ "label": "Inicio",
+ "blurb": "Punto de entrada del flujo"
+ },
+ "send_message": {
+ "label": "Enviar mensaje",
+ "blurb": "Envía un mensaje de texto de WhatsApp"
+ },
+ "send_buttons": {
+ "label": "Enviar botones",
+ "blurb": "Envía botones de respuesta rápida"
+ },
+ "send_list": {
+ "label": "Enviar lista",
+ "blurb": "Envía una lista de opciones táctil"
+ },
+ "send_media": {
+ "label": "Enviar archivo",
+ "blurb": "Envía una imagen, video o documento"
+ },
+ "collect_input": {
+ "label": "Capturar respuesta",
+ "blurb": "Hace una pregunta y guarda la respuesta"
+ },
+ "condition": {
+ "label": "Si / sino",
+ "blurb": "Se ramifica según una regla"
+ },
+ "set_tag": {
+ "label": "Etiquetar contacto",
+ "blurb": "Agrega o quita una etiqueta de contacto"
+ },
+ "handoff": {
+ "label": "Transferir a agente",
+ "blurb": "Transfiere la conversación a un humano"
+ },
+ "end": {
+ "label": "Fin",
+ "blurb": "Termina el flujo"
+ }
+ }
+ },
+ "validation": {
+ "noIssues": "Sin problemas. Listo para activar.",
+ "summary": "{errorCount} {errorCount, plural, =1 {error} other {errores}}, {warningCount} {warningCount, plural, =1 {advertencia} other {advertencias}}",
+ "jumpToNode": "Ir al nodo {key}"
+ },
+ "editorState": {
+ "saved": "Guardado.",
+ "fixIssues": "Corrige los problemas de abajo antes de activar.",
+ "statusActivated": "Flujo activado.",
+ "statusArchived": "Archivado.",
+ "statusDraft": "Guardado como borrador."
+ },
+ "summary": {
+ "options": "{count} {count, plural, =1 {opción} other {opciones}}",
+ "optionsAcrossSections": "{rowCount} {rowCount, plural, =1 {opción} other {opciones}} en {sectionCount} {sectionCount, plural, =1 {sección} other {secciones}}",
+ "noFile": "{label} (sin archivo subido)",
+ "media": "Archivo",
+ "image": "Imagen",
+ "video": "Video",
+ "document": "Documento",
+ "audio": "Audio",
+ "hasTag": "tiene la etiqueta {tag}",
+ "opContains": "contiene",
+ "opExists": "existe",
+ "opMissing": "falta",
+ "modeAdd": "Agregar",
+ "modeRemove": "Quitar",
+ "tagPicked": "{mode} etiqueta {tag}…",
+ "tagNone": "{mode} etiqueta (ninguna elegida)"
+ }
+ },
+ "Settings": {
+ "pageTitle": "Configuración",
+ "pageDesc": "Todo en un solo lugar — tu cuenta y tu espacio de trabajo. Elige una sección para administrarla.",
+ "overview": {
+ "notSetup": "Aún sin configurar",
+ "connected": "Conectado",
+ "needsReconnecting": "Necesita reconectarse",
+ "viewTeamMembers": "Ver miembros del equipo",
+ "membersCount": "{count} {count, plural, =1 {miembro} other {miembros}}",
+ "pendingInvites": "{count} {count, plural, =1 {invitación pendiente} other {invitaciones pendientes}}",
+ "manageTemplates": "Administrar plantillas de mensajes",
+ "templatesCount": "{count} {count, plural, =1 {plantilla} other {plantillas}}",
+ "pendingReview": "{count} en revisión",
+ "tagsAndFields": "Etiquetas y campos personalizados",
+ "tagsCount": "{count} {count, plural, =1 {etiqueta} other {etiquetas}}",
+ "fieldsCount": "{count} {count, plural, =1 {campo personalizado} other {campos personalizados}}",
+ "appearance": "modo {mode} · acento {theme}",
+ "yourAccount": "Tu cuenta",
+ "loading": "Cargando…"
+ },
+ "members": {
+ "title": "Miembros del equipo",
+ "description": "Personas con acceso a esta cuenta. Los roles controlan lo que puede hacer cada compañero de equipo.",
+ "inviteMember": "Invitar miembro",
+ "online": "en línea",
+ "away": "ausente",
+ "offline": "sin conexión",
+ "memberCount": "{count} {count, plural, =1 {miembro} other {miembros}}",
+ "you": "Tú",
+ "unnamed": "Sin nombre",
+ "joined": "Se unió el {date}",
+ "remove": "Quitar",
+ "removedToast": "Se quitó a {name}",
+ "updatedToast": "Se actualizó a {name} a {role}",
+ "pendingInvitations": "Invitaciones pendientes",
+ "inviteHint": "Por seguridad, la URL de invitación en texto plano solo se muestra una vez al crearla — para volver a compartirla, revoca la invitación de abajo y crea una nueva.",
+ "noPendingTitle": "No hay invitaciones pendientes.",
+ "noPendingDesc": "Haz clic en Invitar miembro arriba para generar un enlace para compartir.",
+ "untitledInvite": "Invitación sin título",
+ "created": "Creada el {date}",
+ "expiresInDays": "expira en {days} {days, plural, =1 {día} other {días}}",
+ "expiresInHours": "expira en {hours} {hours, plural, =1 {hora} other {horas}}",
+ "expired": "expirada",
+ "revoke": "Revocar",
+ "revokedToast": "Invitación revocada",
+ "removeDialogTitle": "Quitar miembro",
+ "removeDialogDesc": "¿Quitar a {name} de la cuenta? Se cerrará su sesión en esta cuenta y se le dará una cuenta personal nueva en su próximo inicio de sesión. Su acceso no se elimina.",
+ "cancel": "Cancelar",
+ "removing": "Quitando...",
+ "removeBtn": "Quitar miembro"
+ },
+ "invite": {
+ "inviteCreated": "Invitación creada",
+ "inviteCreatedDesc": "Comparte este enlace con tu nuevo compañero de equipo. Podrá registrarse (o iniciar sesión) y unirse a la cuenta como {role}. El enlace es válido por {days} {days, plural, =1 {día} other {días}}.",
+ "inviteLink": "Enlace de invitación",
+ "copy": "Copiar",
+ "saveLinkNow": "Guarda este enlace ahora.",
+ "saveLinkHint": "Nunca almacenamos el texto plano — una vez que cierres este cuadro de diálogo, la URL desaparece. Para volver a compartirlo, revoca esta invitación y crea una nueva.",
+ "sendViaWhatsApp": "Enviar por WhatsApp",
+ "done": "Listo",
+ "dialogTitle": "Invitar a un compañero de equipo",
+ "dialogDesc": "Genera un enlace de invitación de un solo uso. Compártelo por WhatsApp, Slack o cualquier canal que prefieras — no se requiere un servicio de correo.",
+ "roleLabel": "Rol",
+ "validForLabel": "Enlace válido por",
+ "days1": "1 día",
+ "days7": "7 días",
+ "days30": "30 días",
+ "labelTitle": "Etiqueta",
+ "optional": "(opcional)",
+ "labelPlaceholder": "p. ej. Sara — equipo de soporte",
+ "labelHint": "Te ayuda a recordar a quién le enviaste el enlace en la lista de pendientes de abajo.",
+ "cancel": "Cancelar",
+ "creating": "Creando...",
+ "generateLink": "Generar enlace",
+ "labelTooLong": "La etiqueta debe tener {max} caracteres o menos",
+ "copied": "Enlace de invitación copiado",
+ "clipboardBlocked": "Portapapeles bloqueado — copia el enlace manualmente",
+ "whatsappMessage": "Únete a {accountName} en wacrm usando este enlace (válido por {expiresInDays} días): {url}"
+ },
+ "tagsAndFields": {
+ "title": "Campos y etiquetas",
+ "description": "Dos formas de organizar contactos: etiquetas con código de color para agrupar rápido, y campos personalizados para datos estructurados.",
+ "tagsTitle": "Etiquetas",
+ "tagsDesc": "Etiquetas con código de color para agrupar y filtrar contactos.",
+ "fieldsTitle": "Campos personalizados",
+ "fieldsDesc": "Campos adicionales de contacto (p. ej. código postal, origen del lead). Aparecen en cada contacto y en la acción de automatización “Actualizar campo de contacto”.",
+ "adminRole": "Administrador",
+ "failedToLoadTags": "No se pudieron cargar las etiquetas",
+ "nameRequired": "El nombre de la etiqueta es obligatorio",
+ "notAuthenticated": "No autenticado",
+ "tagCreated": "Etiqueta creada",
+ "failedToCreateTag": "No se pudo crear la etiqueta",
+ "tagDeleted": "Etiqueta eliminada",
+ "failedToDeleteTag": "No se pudo eliminar la etiqueta",
+ "deleteAria": "Eliminar {name}",
+ "noTags": "Aún no hay etiquetas — crea la primera abajo.",
+ "placeholder": "p. ej. Boletín",
+ "useColor": "Usar {color}",
+ "addTag": "Agregar etiqueta",
+ "deleteTag": "Eliminar etiqueta",
+ "deleteConfirm": "¿Eliminar la etiqueta \"{name}\"? Esto la quita de todos los contactos y no se puede deshacer.",
+ "cancel": "Cancelar",
+ "deleting": "Eliminando...",
+ "colors": {
+ "red": "Rojo",
+ "orange": "Naranja",
+ "amber": "Ámbar",
+ "emerald": "Esmeralda",
+ "cyan": "Cian",
+ "blue": "Azul",
+ "violet": "Violeta",
+ "pink": "Rosa"
+ }
+ },
+ "templates": {
+ "title": "Plantillas de mensajes",
+ "description": "Crea plantillas y envíalas a Meta para su aprobación. Usa \"Sincronizar desde Meta\" para traer plantillas aprobadas en otro lugar.",
+ "syncFromMeta": "Sincronizar desde Meta",
+ "syncing": "Sincronizando…",
+ "syncTitle": "Trae las plantillas aprobadas de tu cuenta de WhatsApp Business en Meta",
+ "newTemplate": "Nueva plantilla",
+ "noTemplates": "Aún no hay plantillas.",
+ "createFirst": "Crea tu primera plantilla de mensaje para empezar.",
+ "editTitle": "Editar activa una nueva revisión de Meta — el estado cambia a PENDIENTE.",
+ "editLabel": "Editar plantilla",
+ "edit": "Editar",
+ "resubmitTitle": "Edita la plantilla y vuelve a enviarla a Meta para revisión.",
+ "resubmitLabel": "Editar y reenviar plantilla",
+ "resubmit": "Reenviar",
+ "deleteMetaLocallyAria": "Eliminar plantilla de Meta y localmente",
+ "deleteLocallyAria": "Eliminar plantilla localmente",
+ "deleteMetaLocallyTitle": "Eliminar de Meta y localmente",
+ "deleteLocallyTitle": "Eliminar localmente",
+
+ "dialogEditTitle": "Editar plantilla de mensaje",
+ "dialogNewTitle": "Nueva plantilla de mensaje",
+ "dialogEditDesc": "Guarda tus cambios para reenviarlos a Meta. El estado volverá a PENDIENTE durante la revisión.",
+ "dialogNewDesc": "Crea una plantilla y envíala a Meta para su aprobación. Una vez aprobada, podrás usarla en difusiones y en la bandeja de entrada.",
+ "authWarning": "Las plantillas de AUTENTICACIÓN tienen un cuerpo fijo y un botón de OTP que requieren un creador distinto. Créalas por ahora en el Administrador de WhatsApp de Meta y usa Sincronizar desde Meta para traerlas.",
+
+ "templateName": "Nombre de la plantilla",
+ "namePlaceholder": "p. ej. confirmacion_pedido",
+ "nameFixed": "El nombre queda fijo una vez que la plantilla existe en Meta — crea una nueva plantilla para cambiarlo.",
+ "nameHint": "Solo letras minúsculas, dígitos y guiones bajos.",
+ "category": "Categoría",
+ "language": "Idioma",
+ "langFixed": "El idioma queda fijo una vez que la plantilla existe en Meta.",
+ "langHint": "Debe coincidir exactamente con el código en Meta — en_US y en son distintos.",
+
+ "header": "Encabezado",
+ "headerNone": "Ninguno",
+ "headerText": "Texto",
+ "headerImage": "Imagen",
+ "headerVideo": "Video",
+ "headerDocument": "Documento",
+ "headerTextPlaceholder": "Texto de encabezado (máx. 60 caracteres, {{1}} opcional)",
+ "headerSampleAria": "Valor de muestra para la variable del encabezado",
+ "headerSamplePlaceholder": "Valor de muestra para {{1}} (requerido para la revisión de Meta)",
+
+ "uploadImage": "Subir imagen",
+ "uploadHint": "JPEG o PNG, ≤5 MB",
+ "mediaUrlPlaceholder": "https://… (o pega un enlace público {format})",
+ "imageHint": "Sube un JPEG/PNG (≤5 MB, se recomienda ≥800×418 px) o pega un enlace HTTPS público — lo subimos a Meta para revisión automáticamente.",
+ "mediaHint": "Debe ser un enlace HTTPS de acceso público. Meta lo consulta una vez durante la revisión, así que debe permanecer activo unas ~24 horas.",
+ "videoHint": " Recomendado: MP4 / 3GPP, ≤16 MB, ≤60 segundos.",
+ "documentHint": " Recomendado: PDF, ≤100 MB.",
+
+ "bodyText": "Texto del cuerpo",
+ "bodyPlaceholder": "Hola {{1}}, tu pedido {{2}} está confirmado.",
+ "bodyHint": "Usa {{1}}, {{2}} para variables (deben ser contiguas empezando en {{1}}).",
+ "sampleValues": "Valores de muestra (Meta los usa para revisar tu plantilla)",
+ "sampleAria": "Valor de muestra para la variable del cuerpo {var}",
+ "samplePlaceholder": "Muestra para {var}",
+
+ "footer": "Pie de página (opcional)",
+ "footerPlaceholder": "Texto de pie de página opcional (máx. 60 caracteres)",
+
+ "buttons": "Botones (opcional)",
+ "addButton": "Agregar botón",
+ "buttonsLimit": "Hasta {max} botones. Los botones QUICK_REPLY deben ir antes que los de URL / teléfono / código de copia.",
+ "btnQuickReply": "Respuesta rápida",
+ "btnUrl": "URL",
+ "btnPhone": "Teléfono",
+ "btnCopyCode": "Copiar código",
+ "btnLabelPlaceholder": "Etiqueta del botón",
+ "urlPlaceholder": "https://ejemplo.com/ruta o con sufijo {{1}}",
+ "urlSamplePlaceholder": "Valor de ejemplo para {{1}} (requerido cuando la URL tiene una variable)",
+ "phonePlaceholder": "+15551234567",
+ "codePlaceholder": "Código de ejemplo (p. ej. VERANO20)",
+
+ "cancel": "Cancelar",
+ "saving": "Guardando…",
+ "submitting": "Enviando…",
+ "saveResubmit": "Guardar y reenviar",
+ "submitApproval": "Enviar para aprobación",
+
+ "deleteDialogTitle": "¿Eliminar plantilla?",
+ "deleteMetaDesc": "\"{name}\" se eliminará de Meta y de wacrm. Las difusiones activas que usen esta plantilla empezarán a fallar en su próximo envío. Esto no se puede deshacer.",
+ "deleteLocalDesc": "\"{name}\" se eliminará de wacrm. Nunca se envió a Meta, así que no se necesita limpieza remota.",
+ "deleting": "Eliminando…",
+ "delete": "Eliminar",
+
+ "toastLoadFailed": "No se pudieron cargar las plantillas",
+ "toastSubmitFailed": "No se pudo enviar",
+ "toastSubmitEditSuccess": "Edición enviada — Meta normalmente revisa en un plazo de 24 horas.",
+ "toastSubmitNewSuccess": "Enviada a Meta — el tiempo de revisión típico es de 24 horas. El estado se actualiza automáticamente.",
+ "toastSaveEditDry": "Plantilla actualizada (simulación — sin llamada a Meta)",
+ "toastSaveNewDry": "Plantilla guardada (simulación — sin llamada a Meta)",
+ "toastSyncCount": "Se sincronizaron {total} {total, plural, =1 {plantilla} other {plantillas}} desde Meta",
+ "toastSyncDetails": " ({inserted} nuevas, {updated} actualizadas)",
+ "toastSyncFailed": "No se pudo sincronizar: {preview}",
+ "toastSyncTruncated": "Se sincronizaron solo las primeras 2000 plantillas — tu cuenta tiene más. Sincroniza de nuevo para continuar, o contacta a soporte si persiste.",
+ "toastSyncError": "No se pudieron sincronizar las plantillas",
+ "toastDeleteSuccess": "Plantilla eliminada",
+ "toastDeleteError": "No se pudo eliminar la plantilla",
+ "toastInvalidImage": "La imagen de encabezado debe ser JPEG o PNG.",
+ "toastImageTooLarge": "La imagen pesa {size} MB — el límite de Meta es 5 MB.",
+ "toastUploadSuccess": "Imagen subida.",
+ "toastUploadFailed": "Error al subir."
+ },
+ "roles": {
+ "admin": "Administrador",
+ "adminHint": "Administra miembros y todo lo demás",
+ "agent": "Agente",
+ "agentHint": "Usa funciones; sin configuración",
+ "viewer": "Visualizador",
+ "viewerHint": "Solo lectura en toda la app",
+ "owner": "Propietario",
+ "ownerHint": "Control total sobre la cuenta y la facturación"
+ },
+ "whatsapp": {
+ "title": "Conexión de WhatsApp",
+ "description": "Conecta tu API de WhatsApp Business de Meta. Las credenciales, el webhook y los pasos de configuración están todos aquí.",
+ "tokenCorrupted": "El token guardado no se puede descifrar",
+ "resetting": "Restableciendo...",
+ "resetConfig": "Restablecer configuración",
+ "credentialsValid": "Credenciales válidas",
+ "notConnected": "No conectado",
+ "connectedDesc": "Tu token de acceso autentica con Meta. Consulta el estado de registro abajo para saber si los webhooks están realmente conectados.",
+ "notConnectedDesc": "Configura tus credenciales de la API de Meta abajo para conectar tu cuenta de WhatsApp Business.",
+ "registered": "Registrado — Meta enviará eventos a wacrm",
+ "notRegistered": "No registrado — Meta no enviará eventos",
+ "verifyWithMeta": "Verificar con Meta",
+ "subscribedSince": "Suscrito desde {date}. Haz clic en Verificar con Meta si los eventos dejan de llegar.",
+ "unknownDate": "desconocida",
+ "lastAttemptFailed": "El último intento falló con: ",
+ "retryHint": "Ingresa (o corrige) el PIN de verificación en dos pasos abajo y haz clic en Guardar configuración para reintentar.",
+ "noRegistrationHint": "Este número se guardó antes de que existiera el seguimiento de registro, o el registro se omitió. Ingresa el PIN de verificación en dos pasos abajo y haz clic en Guardar configuración para suscribirlo.",
+ "diagnosticLastRun": "Diagnóstico — última ejecución: ",
+ "live": "en vivo",
+ "notLive": "no activo",
+ "apiCredentialsTitle": "Credenciales de API",
+ "apiCredentialsDesc": "Ingresa las credenciales de tu API de WhatsApp Business de Meta.",
+ "phoneNumberId": "ID del número de teléfono",
+ "wabaId": "ID de la cuenta de WhatsApp Business",
+ "accessToken": "Token de acceso permanente",
+ "accessTokenPlaceholder": "Ingresa tu token de acceso",
+ "tokenHidden": "El token está oculto por seguridad. Vuelve a ingresarlo para actualizar la configuración.",
+ "webhookVerifyToken": "Token de verificación de webhook",
+ "webhookVerifyTokenPlaceholder": "Crea un token de verificación personalizado",
+ "webhookVerifyTokenHint": "Una cadena personalizada que tú creas. Debe coincidir con el token que definiste en la configuración del webhook de Meta.",
+ "twoStepPin": "PIN de verificación en dos pasos",
+ "optional": "(opcional)",
+ "pinPlaceholder": "PIN de 6 dígitos del Administrador de WhatsApp de Meta",
+ "pinHint": "Solo se necesita para conectar mensajes entrantes de un número de producción. Configúralo en Meta Business Manager → Cuentas de WhatsApp → Números de teléfono → Verificación en dos pasos, y luego pégalo aquí para que wacrm pueda suscribir el número — de lo contrario Meta enruta los eventos entrantes a la última app que reclamó el número (el síntoma que afecta a números secundarios bajo una WABA compartida). Los números de prueba de Meta no tienen PIN y ya están registrados — déjalo en blanco para esos casos. Dejarlo en blanco también mantiene intacto un registro existente.",
+ "webhookTitle": "Configuración del webhook",
+ "webhookDesc": "Usa esta URL como devolución de llamada del webhook en el Panel de apps de Meta.",
+ "webhookUrl": "URL de devolución de llamada del webhook",
+ "saving": "Guardando...",
+ "saveConfig": "Guardar configuración",
+ "testing": "Probando...",
+ "testConnection": "Probar conexión con la API",
+ "setupInstructions": "Instrucciones de configuración",
+ "setupInstructionsDesc": "Sigue estos pasos para conectar tu API de WhatsApp Business.",
+ "step1": "Crea una app de Meta",
+ "step1_1": "Ve a developers.facebook.com",
+ "step1_2": "Haz clic en \"Mis apps\" y luego en \"Crear app\"",
+ "step1_3": "Selecciona \"Empresa\" como tipo de app",
+ "step1_4": "Completa los datos de la app y créala",
+ "step2": "Agrega el producto WhatsApp",
+ "step2_1": "En el panel de tu app, haz clic en \"Agregar producto\"",
+ "step2_2": "Busca \"WhatsApp\" y haz clic en \"Configurar\"",
+ "step2_3": "Sigue el asistente de configuración para vincular tu empresa",
+ "step3": "Obtén las credenciales de la API",
+ "step3_1": "Ve a WhatsApp > Configuración de la API",
+ "step3_2": "Copia tu ID del número de teléfono",
+ "step3_3": "Copia tu ID de la cuenta de WhatsApp Business",
+ "step3_4": "Genera un token de acceso permanente desde Configuración empresarial > Usuarios del sistema",
+ "step4": "Configura los webhooks",
+ "step4_1": "Ve a WhatsApp > Configuración",
+ "step4_2": "Haz clic en \"Editar\" en la sección de Webhook",
+ "step4_3": "Pega la URL de devolución de llamada del webhook de arriba",
+ "step4_4": "Ingresa el mismo token de verificación que configuraste aquí",
+ "step4_5": "Suscríbete al campo de webhook \"messages\"",
+ "metaDocs": "Documentación de la API de WhatsApp de Meta"
+ },
+ "sections": {
+ "overview": "Resumen",
+ "profile": "Tu perfil",
+ "security": "Inicio de sesión y seguridad",
+ "appearance": "Apariencia",
+ "whatsapp": "WhatsApp",
+ "templates": "Plantillas",
+ "fields": "Campos y etiquetas",
+ "deals": "Negocios y moneda",
+ "members": "Miembros del equipo",
+ "api": "Claves de API"
+ },
+ "groups": {
+ "account": "Cuenta",
+ "workspace": "Espacio de trabajo"
+ },
+ "profile": {
+ "title": "Tu perfil",
+ "description": "Cómo apareces en toda la app. Tu avatar y nombre aparecen en el encabezado, la barra lateral y donde sea que tus compañeros de equipo te vean.",
+ "changePhoto": "Cambiar foto",
+ "uploadPhoto": "Subir foto",
+ "remove": "Quitar",
+ "photoHint": "PNG, JPG, WebP o GIF. Hasta 2 MB.",
+ "displayName": "Nombre para mostrar",
+ "email": "Correo electrónico",
+ "emailChangeHint": "Revisa la bandeja de entrada de {oldEmail} y {newEmail} — ambos deben confirmar antes de que el cambio surta efecto.",
+ "accountDetails": "Datos de la cuenta",
+ "role": "Rol",
+ "joined": "Se unió",
+ "userId": "ID de usuario",
+ "loading": "Cargando tu perfil…",
+ "saving": "Guardando…",
+ "saveChanges": "Guardar cambios",
+ "unsupportedImage": "Tipo de imagen no compatible",
+ "unsupportedImageDesc": "Usa PNG, JPG, WebP o GIF.",
+ "imageTooLarge": "La imagen es demasiado grande",
+ "imageTooLargeDesc": "Máximo 2 MB.",
+ "nameRequired": "El nombre para mostrar es obligatorio",
+ "invalidEmail": "Ingresa una dirección de correo válida",
+ "uploadFailed": "Error al subir: {message}",
+ "saveFailed": "Error al guardar: {message}",
+ "profileSaved": "Perfil guardado",
+ "emailChangeFailed": "Error al cambiar el correo: {message}",
+ "profileSavedEmailCheck": "Perfil guardado — revisa tu correo para confirmar el cambio de dirección",
+
+ "passwordTitle": "Contraseña",
+ "passwordDesc": "Usa al menos {min} caracteres. Tu sesión seguirá activa en este dispositivo después de cambiarla.",
+ "currentPassword": "Contraseña actual",
+ "newPassword": "Contraseña nueva",
+ "confirmPassword": "Confirmar contraseña nueva",
+ "updating": "Actualizando…",
+ "updatePassword": "Actualizar contraseña",
+ "cannotChangeNoEmail": "No se puede cambiar la contraseña sin un correo actual",
+ "passwordTooShort": "La contraseña debe tener al menos {min} caracteres",
+ "passwordMismatch": "La contraseña nueva y la confirmación no coinciden",
+ "currentPasswordIncorrect": "La contraseña actual es incorrecta",
+ "passwordUpdateFailed": "Error al actualizar la contraseña: {message}",
+ "passwordUpdated": "Contraseña actualizada",
+
+ "sessionsTitle": "Sesiones activas",
+ "sessionsDesc": "Cierra sesión en todos los dispositivos donde hayas iniciado sesión — incluido este. Útil si perdiste una laptop o compartiste tu contraseña.",
+ "signOutAll": "Cerrar sesión en todos los dispositivos",
+ "signOutConfirmTitle": "¿Cerrar sesión en todas partes?",
+ "signOutConfirmDesc": "Todos los dispositivos con sesión iniciada en esta cuenta cerrarán sesión y deberán volver a iniciar sesión. Serás redirigido a la página de inicio de sesión.",
+ "cancel": "Cancelar",
+ "signingOut": "Cerrando sesión…",
+ "signOutEverywhere": "Cerrar sesión en todas partes",
+ "signOutFailed": "Error al cerrar sesión: {message}"
+ },
+ "appearance": {
+ "title": "Apariencia",
+ "description": "Define el modo y el color de acento usados en toda la app. Se guarda en este dispositivo — pruébalo, cambia en vivo.",
+ "mode": "Modo",
+ "useMode": "Usar modo {mode}",
+ "active": "Activo",
+ "accentColor": "Color de acento",
+ "useTheme": "Usar tema {name}"
+ },
+ "security": {
+ "title": "Inicio de sesión y seguridad",
+ "description": "Cambia tu contraseña y cierra sesión en tus dispositivos. Esto mantiene tu cuenta segura."
+ },
+ "apiKeys": {
+ "title": "Claves de API",
+ "description": "Las claves autentican la API REST pública (/api/v1) para que puedas crear tus propias automatizaciones. Envíalas como Authorization: Bearer <key>.",
+ "newApiKey": "Nueva clave de API",
+ "noApiKeys": "Aún no hay claves de API.",
+ "createOneHint": "Haz clic en Nueva clave de API para crear una.",
+ "askAdminHint": "Pide a un administrador que cree una.",
+ "revoked": "Revocada",
+ "expired": "Expirada",
+ "noScopes": "Sin alcances",
+ "created": "Creada el {date}",
+ "lastUsed": "último uso {date}",
+ "neverUsed": "nunca usada",
+ "expires": "expira el {date}",
+ "revoke": "Revocar",
+ "revokeSuccess": "Se revocó \"{name}\"",
+ "revokeFailed": "No se pudo revocar la clave",
+ "loadFailed": "No se pudieron cargar las claves de API",
+ "createError": "No se pudo crear la clave",
+ "nameRequired": "Ponle un nombre a la clave",
+ "nameLabel": "Nombre",
+ "namePlaceholder": "p. ej. Automatización de Zapier",
+ "scopesLabel": "Alcances",
+ "scopesHint": "Una clave sin alcances aún puede llamar a GET /api/v1/me para verificar que funciona.",
+ "cancel": "Cancelar",
+ "creating": "Creando…",
+ "createKey": "Crear clave",
+ "copyTitle": "Copia tu clave de API",
+ "copyDesc": "Esta es la única vez que se muestra la clave completa. Guárdala en un lugar seguro — si la pierdes, revócala y crea una nueva.",
+ "apiKeyLabel": "Clave de API",
+ "copy": "Copiar",
+ "copySuccess": "Clave de API copiada",
+ "copyFailed": "Error al copiar — selecciona y copia manualmente",
+ "newKeyTitle": "Nueva clave de API",
+ "newKeyDesc": "Nómbrala según la integración que la usará y otórgale solo los alcances que necesita.",
+ "done": "Listo",
+ "networkError": "No se pudo conectar con el servidor"
+ },
+ "deals": {
+ "title": "Negocios y moneda",
+ "description": "La moneda usada para negocios nuevos y para los totales del embudo y del panel.",
+ "defaultCurrency": "Moneda predeterminada",
+ "defaultCurrencyDesc": "Los negocios nuevos usan esta moneda de forma predeterminada, y los totales del embudo y del panel se muestran en ella. Los negocios existentes conservan la moneda con la que se guardaron.",
+ "currencyLabel": "Moneda",
+ "adminOnlyHint": "Solo los administradores de la cuenta pueden cambiar la moneda predeterminada.",
+ "save": "Guardar",
+ "saving": "Guardando...",
+ "saveFailed": "No se pudo guardar la moneda predeterminada",
+ "saveSuccess": "Moneda predeterminada actualizada"
+ },
+ "aiConfig": {
+ "title": "Configuración del agente",
+ "description": "Usa tu propia clave de OpenAI o Anthropic. wacrm llama al proveedor directamente con tu clave — sin tarifas de IA por usuario, y tus datos siguen siendo tuyos. Esto impulsa las respuestas redactadas por IA en la bandeja de entrada, el bot de respuesta automática y el Playground.",
+ "adminOnlyConfig": "Solo los administradores y propietarios pueden cambiar la configuración de IA.",
+ "loadFailed": "No se pudo cargar la configuración de IA",
+ "providerAndKey": "Proveedor y clave",
+ "encryptionNotice": "Tu clave se cifra en reposo (AES-256-GCM) y nunca se vuelve a mostrar después de guardarla.",
+ "provider": "Proveedor",
+ "model": "Modelo",
+ "apiKey": "Clave de API",
+ "testKey": "Probar clave",
+ "testSuccess": "La clave funciona — el proveedor respondió.",
+ "testRejected": "El proveedor rechazó la solicitud.",
+ "testNetworkError": "No se pudo conectar con el proveedor.",
+ "embeddingsKey": "Clave de embeddings",
+ "optionalSemanticSearch": "(opcional — habilita la búsqueda semántica en la base de conocimiento)",
+ "embeddingsHint": "Una clave de OpenAI usada solo para vectorizar tu base de conocimiento (text-embedding-3-small){sameKeyText}. Déjala en blanco para usar búsqueda por palabras clave en su lugar. Bórrala para desactivar la búsqueda semántica.",
+ "sameKeyText": " — puede ser la misma clave de arriba",
+ "behaviour": "Comportamiento",
+ "behaviourDesc": "Cuéntale al asistente sobre tu negocio — productos, tono, qué puede y no puede prometer. Este contexto alimenta tanto los borradores como las respuestas automáticas.",
+ "businessContext": "Contexto e instrucciones del negocio",
+ "promptPlaceholder": "p. ej. Somos Acme, una tienda de equipos para café. Sé cálido y conciso. Nunca cotices precios ni fechas de entrega — transfiere esos casos a un humano.",
+ "enableAssistant": "Habilitar asistente de IA",
+ "enableAssistantDesc": "Interruptor general. Activa el botón “Redactar con IA” en la bandeja de entrada.",
+ "autoReply": "Respuesta automática a mensajes entrantes",
+ "autoReplyDesc": "El bot responde automáticamente a mensajes entrantes nuevos (solo cuando ningún flujo los maneja y no hay un agente asignado). Transfiere a un humano cuando no puede ayudar.",
+ "maxAutoReplies": "Máximo de respuestas automáticas por conversación",
+ "maxAutoRepliesDesc": "Después de esta cantidad de respuestas del bot en un mismo hilo, el bot deja de responder.",
+ "handoffTo": "Transferir a",
+ "handoffToDesc": "Cuando el bot no puede ayudar — o llega al límite de respuestas — se pausa y dirige el chat aquí, con una breve nota de lo ocurrido.",
+ "handoffQueue": "Cola sin asignar (cualquier agente puede tomarla)",
+ "remove": "Quitar",
+ "save": "Guardar",
+ "missingModel": "Ingresa el nombre de un modelo.",
+ "missingApiKey": "Ingresa tu clave de API.",
+ "saveSuccess": "Asistente de IA guardado.",
+ "saveFailed": "No se pudo guardar.",
+ "removeSuccess": "Configuración de IA eliminada.",
+ "removeFailed": "No se pudo eliminar."
+ },
+ "aiKnowledge": {
+ "loadFailed": "No se pudo cargar la base de conocimiento",
+ "openFailed": "No se pudo abrir el documento",
+ "titleContentRequired": "El título y el contenido son obligatorios.",
+ "saveSuccessNew": "Documento agregado.",
+ "saveSuccessUpdate": "Documento actualizado.",
+ "saveFailed": "No se pudo guardar.",
+ "removeSuccess": "Documento eliminado.",
+ "removeFailed": "No se pudo eliminar.",
+ "reindexSuccess": "Se reindexaron {count} documento(s).",
+ "reindexFailed": "Falló la reindexación.",
+ "title": "Base de conocimiento",
+ "description": "Agrega preguntas frecuentes, políticas o detalles de productos. El asistente recupera las piezas relevantes al redactar y responder automáticamente, para poder responder en lugar de transferir.{searchType}",
+ "semanticSearchOn": " La búsqueda semántica está activada (clave de embeddings configurada).",
+ "keywordSearchOn": " Usando búsqueda por palabras clave — agrega una clave de embeddings arriba para búsqueda semántica.",
+ "noDocs": "Aún no hay documentos.",
+ "editDocTitle": "Título",
+ "editDocTitlePlaceholder": "p. ej. Política de devoluciones y reembolsos",
+ "editDocContent": "Contenido",
+ "editDocContentPlaceholder": "Pega la respuesta a la pregunta frecuente, el texto de la política o los detalles del producto…",
+ "cancel": "Cancelar",
+ "saveDoc": "Guardar documento",
+ "addDoc": "Agregar documento",
+ "reindex": "Reindexar",
+ "reindexTooltip": "Vuelve a vectorizar todos los documentos (p. ej. después de agregar una clave de embeddings)",
+ "loading": "Cargando…"
+ }
+ }
+}
diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx
index d2a1784fb7..c547be730e 100644
--- a/src/app/(auth)/forgot-password/page.tsx
+++ b/src/app/(auth)/forgot-password/page.tsx
@@ -50,12 +50,12 @@ export default function ForgotPasswordPage() {
- Check your email
+ Revisa tu correo
- We've sent a password reset link to{" "}
- {email}. Please check your
- inbox.
+ Enviamos un enlace para restablecer tu contraseña a{" "}
+ {email}. Revisa tu
+ bandeja de entrada.
@@ -64,7 +64,7 @@ export default function ForgotPasswordPage() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
- Back to sign in
+ Volver a iniciar sesión
@@ -80,9 +80,9 @@ export default function ForgotPasswordPage() {
- Reset password
+ Restablecer contraseña
- Enter your email and we'll send you a reset link
+ Ingresa tu correo y te enviaremos un enlace para restablecerla
@@ -95,12 +95,12 @@ export default function ForgotPasswordPage() {
setEmail(e.target.value)}
required
@@ -113,7 +113,7 @@ export default function ForgotPasswordPage() {
disabled={loading}
className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
- {loading ? "Sending..." : "Send reset link"}
+ {loading ? "Enviando..." : "Enviar enlace"}
@@ -122,7 +122,7 @@ export default function ForgotPasswordPage() {
className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground"
>
- Back to sign in
+ Volver a iniciar sesión
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index 56985cdb2f..1c05cbac8d 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -49,12 +49,12 @@ function SignupPageInner() {
setError(null);
if (password !== confirmPassword) {
- setError("Passwords do not match");
+ setError("Las contraseñas no coinciden");
return;
}
if (password.length < 6) {
- setError("Password must be at least 6 characters");
+ setError("La contraseña debe tener al menos 6 caracteres");
return;
}
@@ -98,12 +98,12 @@ function SignupPageInner() {
- Check your email
+ Revisa tu correo
- We've sent a confirmation link to{" "}
- {email}. Please check your
- inbox and click the link to verify your account.
+ Enviamos un enlace de confirmación a{" "}
+ {email}. Revisa tu
+ bandeja de entrada y haz clic en el enlace para verificar tu cuenta.
@@ -118,7 +118,7 @@ function SignupPageInner() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
- Back to sign in
+ Volver a iniciar sesión
@@ -139,12 +139,12 @@ function SignupPageInner() {
)}
- {inviteToken ? "Create account & join" : "Create account"}
+ {inviteToken ? "Crear cuenta y unirte" : "Crear cuenta"}
{inviteToken
- ? "Verify your email, then accept the invitation to join your team."
- : "Get started with CRM Template for WhatsApp"}
+ ? "Verifica tu correo y luego acepta la invitación para unirte a tu equipo."
+ : "Comienza con la plantilla de CRM para WhatsApp"}
@@ -157,12 +157,12 @@ function SignupPageInner() {
setFullName(e.target.value)}
required
@@ -172,12 +172,12 @@ function SignupPageInner() {
setEmail(e.target.value)}
required
@@ -187,12 +187,12 @@ function SignupPageInner() {
setPassword(e.target.value)}
required
@@ -202,12 +202,12 @@ function SignupPageInner() {
- Already have an account?{" "}
+ ¿Ya tienes una cuenta?{" "}
- Sign in
+ Iniciar sesión
diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx
index c3774849a4..03987edac7 100644
--- a/src/app/(dashboard)/agents/page.tsx
+++ b/src/app/(dashboard)/agents/page.tsx
@@ -41,12 +41,12 @@ export default function AgentsPage() {
- AI Agents
+ Agentes IA
- Your bring-your-own-key AI agent — set it up, then test it in the
- playground before it replies to customers in the inbox.
+ Tu agente de IA con clave propia — configúralo y luego pruébalo en el
+ playground antes de que responda a los clientes en la bandeja de entrada.
{decided && (
@@ -60,11 +60,11 @@ export default function AgentsPage() {
Playground
- Setup
+ Configuración
{canViewUsage && (
- Usage
+ Uso
)}
diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx
index 4a87a8801c..d906b1d306 100644
--- a/src/app/(dashboard)/automations/page.tsx
+++ b/src/app/(dashboard)/automations/page.tsx
@@ -77,7 +77,7 @@ export default function AutomationsPage() {
if (fetchErr) throw fetchErr
setAutomations((data ?? []) as Automation[])
} catch (err) {
- setError(err instanceof Error ? err.message : "Failed to load automations")
+ setError(err instanceof Error ? err.message : "No se pudieron cargar las automatizaciones")
}
}
@@ -169,7 +169,7 @@ export default function AutomationsPage() {
setCreateOpen(true)}
>
@@ -345,7 +345,7 @@ function EmptyState({
diff --git a/src/app/(dashboard)/notifications/page.tsx b/src/app/(dashboard)/notifications/page.tsx
index c658fa9b92..77671119c9 100644
--- a/src/app/(dashboard)/notifications/page.tsx
+++ b/src/app/(dashboard)/notifications/page.tsx
@@ -104,7 +104,7 @@ export default function NotificationsPage() {
.eq("id", id)
.is("read_at", null);
if (updateErr) {
- toast.error("Failed to mark notification as read");
+ toast.error("No se pudo marcar la notificación como leída");
load();
}
},
@@ -137,7 +137,7 @@ export default function NotificationsPage() {
.is("read_at", null);
setMarkingAll(false);
if (updateErr) {
- toast.error("Failed to mark all as read");
+ toast.error("No se pudieron marcar todas como leídas");
load();
}
}, [unreadIds.length, load]);
@@ -147,7 +147,7 @@ export default function NotificationsPage() {
{error}
);
@@ -165,9 +165,9 @@ export default function NotificationsPage() {
-
Notifications
+
Notificaciones
- Conversations other teammates assign to you show up here.
+ Aquí aparecen las conversaciones que otros compañeros te asignan.
)}
- Mark all as read
+ Marcar todas como leídas
@@ -191,11 +191,11 @@ export default function NotificationsPage() {
- No notifications yet
+ Aún no hay notificaciones
- You'll see an alert here when someone assigns you a
- conversation.
+ Verás una alerta aquí cuando alguien te asigne una
+ conversación.
) : (
@@ -241,7 +241,7 @@ export default function NotificationsPage() {
{isUnread && (
)}
diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx
index fff6f98d68..043229cf02 100644
--- a/src/app/(dashboard)/pipelines/page.tsx
+++ b/src/app/(dashboard)/pipelines/page.tsx
@@ -38,11 +38,11 @@ import { useTranslations } from "next-intl";
// Spec-defined seed — name and color per the product spec.
const SPEC_DEFAULT_STAGES = [
- { name: "New Lead", color: "#3b82f6", position: 0 }, // blue
- { name: "Qualified", color: "#eab308", position: 1 }, // yellow
- { name: "Proposal Sent", color: "#f97316", position: 2 }, // orange
- { name: "Negotiation", color: "#8b5cf6", position: 3 }, // purple
- { name: "Won", color: "#22c55e", position: 4 }, // green
+ { name: "Nuevo Lead", color: "#3b82f6", position: 0 }, // blue
+ { name: "Calificado", color: "#eab308", position: 1 }, // yellow
+ { name: "Propuesta enviada", color: "#f97316", position: 2 }, // orange
+ { name: "Negociación", color: "#8b5cf6", position: 3 }, // purple
+ { name: "Ganado", color: "#22c55e", position: 4 }, // green
];
export default function PipelinesPage() {
@@ -120,7 +120,7 @@ export default function PipelinesPage() {
const { data: pipeline, error } = await supabase
.from("pipelines")
- .insert({ user_id: user.id, account_id: accountId, name: "Sales Pipeline" })
+ .insert({ user_id: user.id, account_id: accountId, name: "Embudo de ventas" })
.select()
.single();
@@ -370,7 +370,7 @@ export default function PipelinesPage() {
setNewPipelineOpen(true)}
className="border-border bg-card text-foreground hover:bg-muted"
>
@@ -379,7 +379,7 @@ export default function PipelinesPage() {
handleAddDeal()}
className="bg-primary text-primary-foreground hover:bg-primary/90"
@@ -402,7 +402,7 @@ export default function PipelinesPage() {
setNewPipelineOpen(true)}
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
>
diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx
index 97d192b356..4d0cebba98 100644
--- a/src/app/(dashboard)/settings/page.tsx
+++ b/src/app/(dashboard)/settings/page.tsx
@@ -47,7 +47,7 @@ export default function SettingsPage() {
// already in context.
const hints: Partial> = useMemo(
() => ({
- appearance: mode.charAt(0).toUpperCase() + mode.slice(1),
+ appearance: mode === 'dark' ? 'Oscuro' : 'Claro',
deals: defaultCurrency,
}),
[mode, defaultCurrency],
diff --git a/src/app/join/[token]/page.tsx b/src/app/join/[token]/page.tsx
index 32e09bbc5a..14fdbca3bb 100644
--- a/src/app/join/[token]/page.tsx
+++ b/src/app/join/[token]/page.tsx
@@ -66,27 +66,27 @@ interface PeekFail {
type PeekResult = PeekOk | PeekFail;
const ROLE_LABEL: Record = {
- admin: 'Admin',
- agent: 'Agent',
- viewer: 'Viewer',
+ admin: 'Administrador',
+ agent: 'Agente',
+ viewer: 'Visualizador',
};
const FAIL_COPY: Record = {
not_found: {
- title: 'Invite not found',
- body: 'This link doesn’t match a valid invitation. Double-check the URL or ask the person who invited you to send a new one.',
+ title: 'Invitación no encontrada',
+ body: 'Este enlace no corresponde a una invitación válida. Revisa la URL o pídele a la persona que te invitó que te envíe una nueva.',
},
used: {
- title: 'Invite already used',
- body: 'This invitation has already been accepted. If that wasn’t you, ask the account admin to send a fresh link.',
+ title: 'Invitación ya utilizada',
+ body: 'Esta invitación ya fue aceptada. Si no fuiste tú, pídele al administrador de la cuenta que envíe un nuevo enlace.',
},
expired: {
- title: 'Invite expired',
- body: 'This invitation has expired. Ask the account admin to send a new one — they take a few seconds to generate.',
+ title: 'Invitación expirada',
+ body: 'Esta invitación ha expirado. Pídele al administrador de la cuenta que envíe una nueva; solo toma unos segundos generarla.',
},
server_error: {
- title: 'Something went wrong',
- body: 'We couldn’t verify this invitation right now. Try refreshing the page in a moment.',
+ title: 'Algo salió mal',
+ body: 'No pudimos verificar esta invitación en este momento. Intenta actualizar la página en un momento.',
},
};
@@ -183,21 +183,21 @@ export default function JoinPage() {
if (res.status === 409) {
setConflictMessage(
payload.error ||
- 'You are already in another account. Sign in with a different email to join this one.',
+ 'Ya perteneces a otra cuenta. Inicia sesión con un correo diferente para unirte a esta.',
);
} else {
- toast.error(payload.error || 'Failed to accept invitation');
+ toast.error(payload.error || 'No se pudo aceptar la invitación');
}
setAccepting(false);
return;
}
- toast.success('Welcome to the team');
+ toast.success('Bienvenido al equipo');
// Full reload (not router.push) so AuthProvider re-fetches
// the profile with the new account_id and account_role.
window.location.href = '/dashboard';
} catch (err) {
console.error('[join] redeem error:', err);
- toast.error('Could not reach the server');
+ toast.error('No se pudo conectar con el servidor');
setAccepting(false);
}
}, [token]);
@@ -212,7 +212,7 @@ export default function JoinPage() {
window.location.reload();
} catch (err) {
console.error('[join] sign-out error:', err);
- toast.error('Could not sign out. Try refreshing the page.');
+ toast.error('No se pudo cerrar sesión. Intenta actualizar la página.');
setSigningOut(false);
}
}, []);
@@ -223,7 +223,7 @@ export default function JoinPage() {
-
Verifying invitation…
+
Verificando invitación…
);
@@ -257,14 +257,14 @@ export default function JoinPage() {
onClick={loadPeekAndAuth}
className="w-full bg-primary text-primary-foreground hover:bg-primary/90"
>
- Try again
+ Intentar de nuevo
>
@@ -272,7 +272,7 @@ export default function JoinPage() {
<>
@@ -280,7 +280,7 @@ export default function JoinPage() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
- Sign in
+ Iniciar sesión
>
@@ -297,16 +297,16 @@ export default function JoinPage() {
- Accepting moves your login into{' '}
- {peek.account_name}. Your
- empty personal account from signup will be cleaned up.
+ Al aceptar, tu cuenta pasará a formar parte de{' '}
+ {peek.account_name}. Tu
+ cuenta personal vacía creada al registrarte será eliminada.
@@ -363,7 +363,7 @@ export default function JoinPage() {
- Can't join {peek.account_name} with this account
+ No puedes unirte a {peek.account_name} con esta cuenta
{conflictMessage}
@@ -371,11 +371,11 @@ export default function JoinPage() {
- To join{' '}
+ Para unirte a{' '}
{peek.account_name},
- sign out and sign up again with a different email address.
- The invite link stays valid as long as it hasn't
- expired.
+ cierra sesión y regístrate de nuevo con un correo diferente.
+ El enlace de invitación seguirá siendo válido mientras no
+ haya expirado.
@@ -384,7 +384,7 @@ export default function JoinPage() {
onClick={() => setConflictMessage(null)}
className="border-border text-popover-foreground hover:bg-muted"
>
- Stay signed in
+ Mantener sesión iniciada
@@ -414,7 +414,7 @@ export default function JoinPage() {
@@ -422,7 +422,7 @@ export default function JoinPage() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
- I already have an account
+ Ya tengo una cuenta
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 4a8c05a344..af4ad8a6ae 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -25,7 +25,7 @@ export const metadata: Metadata = {
default: "wacrm",
template: "%s — wacrm",
},
- description: "Self-hostable CRM template for WhatsApp.",
+ description: "Plantilla de CRM autoalojable para WhatsApp.",
robots: {
index: false,
follow: false,
diff --git a/src/components/agents/ai-playground.tsx b/src/components/agents/ai-playground.tsx
index 6033cc26c5..69ab44c3ce 100644
--- a/src/components/agents/ai-playground.tsx
+++ b/src/components/agents/ai-playground.tsx
@@ -43,9 +43,9 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
if (data.code === 'ai_not_configured') {
- toast.error('No agent configured yet — finish Setup first.');
+ toast.error('Aún no hay un agente configurado — primero completa la Configuración.');
} else {
- toast.error(data.error ?? "Couldn't get a reply.");
+ toast.error(data.error ?? 'No se pudo obtener una respuesta.');
}
// Roll the unsent user turn back so the transcript stays clean.
setTurns(turns);
@@ -64,7 +64,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
},
]);
} catch {
- toast.error("Couldn't reach the agent.");
+ toast.error('No se pudo contactar al agente.');
setTurns(turns);
setInput(text);
} finally {
@@ -85,9 +85,9 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
- Playground
+ Zona de pruebas
- — test replies as if you were a customer
+ — prueba las respuestas como si fueras un cliente
Envía un mensaje para ver cómo respondería tu agente.
- It uses your knowledge base and behaves exactly like the
- auto-reply bot — including handoff.
+ Usa tu base de conocimientos y se comporta igual que el bot de
+ respuesta automática, incluida la derivación a un humano.
{onGoToSetup && (
)}
@@ -152,7 +152,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
)}
>
- Would hand off to a human here
+ Aquí derivaría a un humano
)}
)}
@@ -176,7 +176,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder="Type a customer message…"
+ placeholder="Escribe un mensaje de cliente…"
rows={1}
className="flex-1 resize-none rounded-xl border border-border bg-muted px-4 py-2.5 text-sm text-foreground placeholder-muted-foreground outline-none focus:border-primary/50"
/>
diff --git a/src/components/agents/ai-usage.tsx b/src/components/agents/ai-usage.tsx
index d24f16fc34..fb0f026001 100644
--- a/src/components/agents/ai-usage.tsx
+++ b/src/components/agents/ai-usage.tsx
@@ -70,13 +70,13 @@ export function AiUsageCard() {
});
const json = await res.json().catch(() => null);
if (!res.ok) {
- toast.error(json?.error ?? 'Failed to load usage');
+ toast.error(json?.error ?? 'No se pudo cargar el uso');
setData(null);
return;
}
setData(json as UsageResponse);
} catch {
- toast.error('Failed to load usage');
+ toast.error('No se pudo cargar el uso');
setData(null);
} finally {
setLoading(false);
@@ -105,11 +105,12 @@ export function AiUsageCard() {
- Token usage
+ Uso de tokens
- Tokens spent on your provider key by drafts and the auto-reply
- bot. Counts only — no message content is stored here.
+ Tokens consumidos con tu clave de proveedor por los borradores y
+ el bot de respuesta automática. Solo se cuentan tokens — aquí no
+ se guarda el contenido de los mensajes.
diff --git a/src/components/contacts/contact-form.tsx b/src/components/contacts/contact-form.tsx
index 8c287f85d4..2c55e1c6bb 100644
--- a/src/components/contacts/contact-form.tsx
+++ b/src/components/contacts/contact-form.tsx
@@ -143,8 +143,8 @@ export function ContactForm({
data: { session },
} = await supabase.auth.getSession();
const user = session?.user;
- if (!user) throw new Error('Not authenticated');
- if (!accountId) throw new Error('Your profile is not linked to an account.');
+ if (!user) throw new Error('No autenticado');
+ if (!accountId) throw new Error('Tu perfil no está vinculado a ninguna cuenta.');
let contactId = contact?.id;
diff --git a/src/components/contacts/import-modal.tsx b/src/components/contacts/import-modal.tsx
index 0cce8b4ee6..31a18f8551 100644
--- a/src/components/contacts/import-modal.tsx
+++ b/src/components/contacts/import-modal.tsx
@@ -214,9 +214,9 @@ export function ImportModal({
data: { session },
} = await supabase.auth.getSession();
const user = session?.user;
- if (!user) throw new Error('Not authenticated');
+ if (!user) throw new Error('No autenticado');
if (!accountId)
- throw new Error('Your profile is not linked to an account.');
+ throw new Error('Tu perfil no está vinculado a ninguna cuenta.');
let imported = 0;
let skipped = 0;
diff --git a/src/components/dashboard/response-time-chart.tsx b/src/components/dashboard/response-time-chart.tsx
index a7d2a4c7db..8aa489f144 100644
--- a/src/components/dashboard/response-time-chart.tsx
+++ b/src/components/dashboard/response-time-chart.tsx
@@ -24,8 +24,8 @@ import { useTranslations } from 'next-intl'
// Single category, single colour — the data is "average minutes
// per weekday". Tremor expects categories as the second tuple in
// the row object, so we shape the buckets into
-// `{ day: 'Mon', 'Avg minutes': 4.2 }` rows below.
-const CATEGORY = 'Avg minutes'
+// `{ day: 'Mon', 'Minutos promedio': 4.2 }` rows below.
+const CATEGORY = 'Minutos promedio'
export function ResponseTimeChart({
data,
diff --git a/src/components/flows/flow-editor-shell.tsx b/src/components/flows/flow-editor-shell.tsx
index 384417dce1..99373b3e74 100644
--- a/src/components/flows/flow-editor-shell.tsx
+++ b/src/components/flows/flow-editor-shell.tsx
@@ -106,7 +106,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
- Theme id: {id}
+ ID de tema: {id}
);
}
diff --git a/src/components/settings/invite-member-dialog.tsx b/src/components/settings/invite-member-dialog.tsx
index b211491bc8..09a7b5d0b6 100644
--- a/src/components/settings/invite-member-dialog.tsx
+++ b/src/components/settings/invite-member-dialog.tsx
@@ -117,7 +117,7 @@ export function InviteMemberDialog({
if (!res.ok) {
const payload = await res.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to create invitation');
+ toast.error(payload.error || 'No se pudo crear la invitación');
return;
}
@@ -135,12 +135,12 @@ export function InviteMemberDialog({
// string if `account` hasn't loaded yet (shouldn't happen
// — the dialog requires admin+ which requires a loaded
// profile — but stay safe).
- accountName: account?.name ?? 'our wacrm account',
+ accountName: account?.name ?? 'nuestra cuenta de wacrm',
});
onCreated();
} catch (err) {
console.error('[InviteMemberDialog] create error:', err);
- toast.error('Could not reach the server. Try again?');
+ toast.error('No se pudo conectar con el servidor. ¿Intentar de nuevo?');
} finally {
setSubmitting(false);
}
@@ -164,7 +164,7 @@ export function InviteMemberDialog({
// they're being invited to before clicking through. This matters
// for users in multi-team contexts where "our wacrm account"
// wouldn't be enough to disambiguate.
- const accountName = result?.accountName ?? 'our wacrm account';
+ const accountName = result?.accountName ?? 'nuestra cuenta de wacrm';
const message = t('whatsappMessage', { accountName, expiresInDays: result?.expiresInDays ?? 0, url });
return `https://wa.me/?text=${encodeURIComponent(message)}`;
}
diff --git a/src/components/settings/members-tab.tsx b/src/components/settings/members-tab.tsx
index 6aa296ffde..29479a8007 100644
--- a/src/components/settings/members-tab.tsx
+++ b/src/components/settings/members-tab.tsx
@@ -151,7 +151,7 @@ export function MembersTab() {
if (!mres.ok) {
const payload = await mres.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to load members');
+ toast.error(payload.error || 'No se pudieron cargar los miembros');
return;
}
const mdata = (await mres.json()) as { members: Member[] };
@@ -160,7 +160,7 @@ export function MembersTab() {
if (ires) {
if (!ires.ok) {
const payload = await ires.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to load invitations');
+ toast.error(payload.error || 'No se pudieron cargar las invitaciones');
return;
}
const idata = (await ires.json()) as { invitations: Invitation[] };
@@ -170,7 +170,7 @@ export function MembersTab() {
}
} catch (err) {
console.error('[MembersTab] load error:', err);
- toast.error('Could not reach the server');
+ toast.error('No se pudo conectar con el servidor');
} finally {
setLoading(false);
}
@@ -210,7 +210,7 @@ export function MembersTab() {
),
);
const payload = await res.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to update role');
+ toast.error(payload.error || 'No se pudo actualizar el rol');
return;
}
toast.success(t('updatedToast', { name: member.full_name || t('unnamed'), role: tRoles(nextRole) }));
@@ -222,7 +222,7 @@ export function MembersTab() {
),
);
console.error('[MembersTab] role change error:', err);
- toast.error('Could not reach the server');
+ toast.error('No se pudo conectar con el servidor');
} finally {
setPendingMemberAction(null);
}
@@ -238,7 +238,7 @@ export function MembersTab() {
);
if (!res.ok) {
const payload = await res.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to remove member');
+ toast.error(payload.error || 'No se pudo eliminar al miembro');
return;
}
toast.success(t('removedToast', { name: removingMember.full_name || t('unnamed') }));
@@ -248,7 +248,7 @@ export function MembersTab() {
setRemovingMember(null);
} catch (err) {
console.error('[MembersTab] remove error:', err);
- toast.error('Could not reach the server');
+ toast.error('No se pudo conectar con el servidor');
} finally {
setPendingMemberAction(null);
}
@@ -261,14 +261,14 @@ export function MembersTab() {
});
if (!res.ok) {
const payload = await res.json().catch(() => ({}));
- toast.error(payload.error || 'Failed to revoke invitation');
+ toast.error(payload.error || 'No se pudo revocar la invitación');
return;
}
toast.success(t('revokedToast'));
setInvitations((prev) => prev.filter((i) => i.id !== invite.id));
} catch (err) {
console.error('[MembersTab] revoke error:', err);
- toast.error('Could not reach the server');
+ toast.error('No se pudo conectar con el servidor');
}
}
@@ -357,7 +357,7 @@ export function MembersTab() {
{member.avatar_url ? (
) : null}
diff --git a/src/components/settings/password-form.tsx b/src/components/settings/password-form.tsx
index 3112555104..e38eb36321 100644
--- a/src/components/settings/password-form.tsx
+++ b/src/components/settings/password-form.tsx
@@ -75,7 +75,7 @@ export function PasswordForm() {
setConfirm('');
toast.success(t('passwordUpdated'));
} catch (err) {
- const msg = err instanceof Error ? err.message : 'Unknown error';
+ const msg = err instanceof Error ? err.message : 'Error desconocido';
toast.error(msg);
} finally {
setSaving(false);
diff --git a/src/components/settings/profile-form.tsx b/src/components/settings/profile-form.tsx
index 4663cd0f37..73f7fbbbcb 100644
--- a/src/components/settings/profile-form.tsx
+++ b/src/components/settings/profile-form.tsx
@@ -184,7 +184,7 @@ export function ProfileForm() {
: t('profileSaved'),
);
} catch (err) {
- const msg = err instanceof Error ? err.message : 'Unknown error';
+ const msg = err instanceof Error ? err.message : 'Error desconocido';
toast.error(msg);
} finally {
setSaving(false);
@@ -219,7 +219,7 @@ export function ProfileForm() {
{currentAvatar ? (
-
+
) : null}
{initial}
diff --git a/src/components/settings/quick-replies-manager.tsx b/src/components/settings/quick-replies-manager.tsx
index 4a34f0857e..d00e926ca0 100644
--- a/src/components/settings/quick-replies-manager.tsx
+++ b/src/components/settings/quick-replies-manager.tsx
@@ -77,7 +77,7 @@ export function QuickRepliesManager() {
const save = useCallback(async () => {
if (!draft) return;
if (!draft.title.trim()) {
- toast.error("Give the quick reply a name.");
+ toast.error("Ponle un nombre a la respuesta rápida.");
return;
}
const payload =
@@ -97,14 +97,14 @@ export function QuickRepliesManager() {
);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
- toast.error(data.error ?? "Couldn't save the quick reply.");
+ toast.error(data.error ?? "No se pudo guardar la respuesta rápida.");
return;
}
- toast.success(draft.id ? "Quick reply updated." : "Quick reply created.");
+ toast.success(draft.id ? "Respuesta rápida actualizada." : "Respuesta rápida creada.");
setDraft(null);
await load();
} catch {
- toast.error("Couldn't save the quick reply.");
+ toast.error("No se pudo guardar la respuesta rápida.");
} finally {
setSaving(false);
}
@@ -112,10 +112,10 @@ export function QuickRepliesManager() {
const remove = useCallback(
async (id: string) => {
- if (!window.confirm("Delete this quick reply?")) return;
+ if (!window.confirm("¿Eliminar esta respuesta rápida?")) return;
const res = await fetch(`/api/quick-replies/${id}`, { method: "DELETE" });
if (!res.ok) {
- toast.error("Couldn't delete the quick reply.");
+ toast.error("No se pudo eliminar la respuesta rápida.");
return;
}
await load();
@@ -126,12 +126,12 @@ export function QuickRepliesManager() {
return (
- New quick reply
+ Nueva respuesta rápida
}
/>
@@ -142,7 +142,7 @@ export function QuickRepliesManager() {
) : items.length === 0 ? (
- No quick replies yet. Create one to reuse it across conversations.
+ Aún no hay respuestas rápidas. Crea una para reutilizarla en varias conversaciones.
) : (
@@ -185,28 +185,28 @@ export function QuickRepliesManager() {