diff --git a/messages/en.json b/messages/en.json index 6817727575..afc79d36f5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -38,7 +38,8 @@ "defaultAvatar": "Avatar", "menuProfile": "Profile", "menuSettings": "Settings", - "menuSignOut": "Sign out" + "menuSignOut": "Sign out", + "primaryNav": "Primary" }, "Header": { "dashboard": "Dashboard", @@ -166,7 +167,12 @@ "selectConversation": "Select a conversation", "selectConversationHint": "Choose a conversation from the left to start messaging", "today": "Today", - "yesterday": "Yesterday" + "yesterday": "Yesterday", + "toastSendFailed": "Failed to send: {reason}", + "toastSendTemplateFailed": "Failed to send template: {reason}", + "toastWaitForSend": "Wait for the message to finish sending", + "toastReactionFailed": "Reaction failed: {reason}", + "toastAssignmentFailed": "Failed to update assignment" }, "sessionTimer": { "expired": "Expired", @@ -202,7 +208,14 @@ "send": "Send", "quickReplyNamePrompt": "Name this quick reply:", "quickReplySaved": "Saved as a quick reply.", - "quickReplySaveError": "Couldn't save the quick reply." + "quickReplySaveError": "Couldn't save the quick reply.", + "aiNotConfigured": "AI isn't set up yet — enable it in Settings → AI Assistant.", + "draftFailed": "Couldn't draft a reply.", + "assistantNoReply": "The assistant didn't return a reply.", + "aiUnreachable": "Couldn't reach the AI assistant.", + "recordingTooLong": "Recording is too long (over 16 MB).", + "voiceUnsupported": "Voice recording isn't supported in this browser.", + "micDenied": "Microphone access denied or unavailable." }, "bubble": { "photo": "Photo", @@ -316,7 +329,8 @@ "toastFailedDelete": "Failed to delete contact", "toastDeleted": "Contact deleted", "toastBulkFailedDelete": "Failed to delete contacts", - "toastBulkDeleted": "{count} contacts deleted" + "toastBulkDeleted": "{count} contacts deleted", + "selectAllAria": "Select all contacts on this page" }, "form": { "addTitle": "Add Contact", @@ -358,9 +372,10 @@ "notes": "Notes", "custom": "Custom Fields", "deals": "Deals", - "noTags": "No tags", - "noDeals": "No deals", - "addNotePlaceholder": "Add a note..." + "tags": "Tags", + "noTags": "No tags", + "noDeals": "No deals", + "addNotePlaceholder": "Add a note..." }, "actions": { "edit": "Edit", @@ -544,7 +559,8 @@ "toastMarkedLost": "Marked as lost", "toastReopened": "Deal reopened", "toastFailedDelete": "Failed to delete deal", - "toastDeleted": "Deal deleted" + "toastDeleted": "Deal deleted", + "valuePlaceholder": "0" }, "settings": { "managePipeline": "Manage Pipeline", @@ -603,7 +619,8 @@ "date": "Date" }, "errorLoad": "Failed to load broadcasts", - "retry": "Retry" + "retry": "Retry", + "inProgressAria": "Broadcast in progress" }, "status": { "draft": "Draft", @@ -715,7 +732,11 @@ "errorLoadTags": "Failed to load tags", "errorLoadFields": "Failed to load custom fields", "errorCsvParse": "Could not parse CSV file.", - "errorCsvMissingPhone": "CSV must have a 'phone' column." + "errorCsvMissingPhone": "CSV must have a 'phone' column.", + "summaryTitle": "Audience Summary", + "calculating": "Calculating…", + "estimatedRecipients": "estimated recipients", + "selectTypeHint": "Select an audience type to see the estimate." }, "personalize": { "title": "Personalize Variables", @@ -742,7 +763,8 @@ "name": "Name", "phone": "Phone", "email": "Email" - } + }, + "enterValuePlaceholder": "Enter value..." }, "scheduleSend": { "title": "Review & Send", @@ -763,7 +785,10 @@ "sending": "Sending...", "preparing": "Preparing...", "progressDesc": "Processing {current} of {total} recipients...", - "toastFailedPreview": "Failed to load preview data" + "toastFailedPreview": "Failed to load preview data", + "estimatedReach": "Estimated Reach", + "language": "Language", + "confirmTitle": "Confirm Broadcast" } } }, @@ -796,7 +821,9 @@ "updateError": "Failed to update", "duplicateError": "Failed to duplicate", "deleteError": "Failed to delete" - } + }, + "activeAria": "active", + "openMenu": "Open menu" }, "edit": { "loading": "Loading...", @@ -972,7 +999,11 @@ "matchContains": "Contains", "matchExact": "Exact", "closeConversationHint": "Sets the conversation status to \"closed\". No configuration needed." - } + }, + "schedulePlaceholder": "Cron expression or HH:mm", + "moveUp": "Move up", + "moveDown": "Move down", + "delete": "Delete" } }, "Flows": { @@ -1164,7 +1195,8 @@ "label": "End", "blurb": "Ends the flow" } - } + }, + "editorView": "Editor view" }, "validation": { "noIssues": "No issues. Ready to activate.", @@ -1246,7 +1278,14 @@ "removeDialogDesc": "Remove {name} from the account? They'll be signed out of this account and given a fresh personal account on their next sign-in. Their login isn't deleted.", "cancel": "Cancel", "removing": "Removing...", - "removeBtn": "Remove member" + "removeBtn": "Remove member", + "loadMembersFailed": "Failed to load members", + "loadInvitationsFailed": "Failed to load invitations", + "serverUnreachable": "Could not reach the server", + "updateRoleFailed": "Failed to update role", + "removeMemberFailed": "Failed to remove member", + "revokeInvitationFailed": "Failed to revoke invitation", + "memberAlt": "Member" }, "invite": { "inviteCreated": "Invite created", @@ -1274,7 +1313,9 @@ "labelTooLong": "Label must be {max} characters or fewer", "copied": "Invite link copied", "clipboardBlocked": "Clipboard blocked — copy the link manually", - "whatsappMessage": "Join {accountName} on wacrm using this link (valid for {expiresInDays} days): {url}" + "whatsappMessage": "Join {accountName} on wacrm using this link (valid for {expiresInDays} days): {url}", + "createFailed": "Failed to create invitation", + "serverUnreachable": "Could not reach the server. Try again?" }, "tagsAndFields": { "title": "Fields & tags", @@ -1330,13 +1371,11 @@ "deleteLocallyAria": "Delete template locally", "deleteMetaLocallyTitle": "Delete from Meta and locally", "deleteLocallyTitle": "Delete locally", - "dialogEditTitle": "Edit Message Template", "dialogNewTitle": "New Message Template", "dialogEditDesc": "Save your changes to re-submit to Meta. Status will flip back to PENDING during review.", "dialogNewDesc": "Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.", "authWarning": "AUTHENTICATION templates have a fixed body + OTP button shape that needs a different builder. Create them in Meta WhatsApp Manager for now and use Sync from Meta to bring them in.", - "templateName": "Template Name", "namePlaceholder": "e.g. order_confirmation", "nameFixed": "Name is fixed once a template exists on Meta — create a new template to change it.", @@ -1345,7 +1384,6 @@ "language": "Language", "langFixed": "Language is fixed once a template exists on Meta.", "langHint": "Must match the exact code on Meta — en_US and en are distinct.", - "header": "Header", "headerNone": "None", "headerText": "Text", @@ -1355,7 +1393,6 @@ "headerTextPlaceholder": "Header text (max 60 chars, optional {{1}})", "headerSampleAria": "Sample value for header variable", "headerSamplePlaceholder": "Sample value for {{1}} (required for Meta review)", - "uploadImage": "Upload image", "uploadHint": "JPEG or PNG, ≤5 MB", "mediaUrlPlaceholder": "https://… (or paste a public {format} link)", @@ -1363,17 +1400,14 @@ "mediaHint": "Must be a publicly accessible HTTPS link. Meta fetches it once during review, so it needs to stay live for ~24 hrs.", "videoHint": " Recommended: MP4 / 3GPP, ≤16 MB, ≤60 seconds.", "documentHint": " Recommended: PDF, ≤100 MB.", - "bodyText": "Body Text", "bodyPlaceholder": "Hello {{1}}, your order {{2}} is confirmed.", "bodyHint": "Use {{1}}, {{2}} for variables (must be contiguous starting at {{1}}).", "sampleValues": "Sample values (Meta uses these to review your template)", "sampleAria": "Sample value for body variable {var}", "samplePlaceholder": "Sample for {var}", - "footer": "Footer (optional)", "footerPlaceholder": "Optional footer text (max 60 chars)", - "buttons": "Buttons (optional)", "addButton": "Add Button", "buttonsLimit": "Up to {max} buttons. QUICK_REPLY buttons must come before URL / phone / copy-code buttons.", @@ -1386,19 +1420,16 @@ "urlSamplePlaceholder": "Example value for {{1}} (required when URL has a variable)", "phonePlaceholder": "+15551234567", "codePlaceholder": "Example code (e.g. SUMMER20)", - "cancel": "Cancel", "saving": "Saving…", "submitting": "Submitting…", "saveResubmit": "Save & Resubmit", "submitApproval": "Submit for Approval", - "deleteDialogTitle": "Delete template?", "deleteMetaDesc": "\"{name}\" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.", "deleteLocalDesc": "\"{name}\" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.", "deleting": "Deleting…", "delete": "Delete", - "toastLoadFailed": "Failed to load templates", "toastSubmitFailed": "Failed to submit", "toastSubmitEditSuccess": "Edit submitted — Meta typically reviews within 24 hours.", @@ -1491,7 +1522,29 @@ "step4_3": "Paste the Webhook Callback URL from above", "step4_4": "Enter the same Verify Token you set here", "step4_5": "Subscribe to \"messages\" webhook field", - "metaDocs": "Meta WhatsApp API Documentation" + "metaDocs": "Meta WhatsApp API Documentation", + "phoneNumberIdRequired": "Phone Number ID is required", + "accessTokenRequired": "Access Token is required for initial setup", + "loadFailed": "Failed to load WhatsApp configuration", + "saveFailed": "Failed to save configuration", + "reenterToken": "Please re-enter the Access Token to save changes", + "savedRegisterFailed": "Saved, but Meta couldn't register the number: {error}", + "savedRegisterSkipped": "Credentials saved and verified. Inbound registration was skipped (no PIN) — see Registration status below.", + "savedLive": "Live — {name} can now receive events.", + "savedConnected": "WhatsApp connected. Events will start flowing within a minute.", + "connectedTo": "Connected to {name}", + "connectionSuccess": "API connection successful", + "connectionFailed": "API connection failed", + "connectionTestFailed": "Connection test failed. Check network and try again.", + "numberLive": "Number is fully wired — Meta is delivering events.", + "numberNotRegistered": "Number is not fully registered. See the checks below for which step failed.", + "verifyEndpointFailed": "Could not reach the verification endpoint.", + "resetConfirm": "This will delete the current WhatsApp config so you can re-enter it. Continue?", + "resetFailed": "Failed to reset configuration", + "resetSuccess": "Configuration cleared. You can now re-enter your credentials.", + "webhookCopied": "Webhook URL copied to clipboard", + "phoneNumberIdPlaceholder": "e.g. 100234567890123", + "wabaIdPlaceholder": "e.g. 100234567890456" }, "sections": { "overview": "Overview", @@ -1503,7 +1556,8 @@ "fields": "Fields & tags", "deals": "Deals & currency", "members": "Team members", - "api": "API keys" + "api": "API keys", + "quick-replies": "Quick replies" }, "groups": { "account": "Account", @@ -1537,7 +1591,6 @@ "profileSaved": "Profile saved", "emailChangeFailed": "Email change failed: {message}", "profileSavedEmailCheck": "Profile saved — check your email to confirm the address change", - "passwordTitle": "Password", "passwordDesc": "Use at least {min} characters. You will stay signed in on this device after changing it.", "currentPassword": "Current password", @@ -1551,7 +1604,6 @@ "currentPasswordIncorrect": "Current password is incorrect", "passwordUpdateFailed": "Password update failed: {message}", "passwordUpdated": "Password updated", - "sessionsTitle": "Active sessions", "sessionsDesc": "Sign out of every device where you're logged in — including this one. Useful if you lost a laptop or shared your password.", "signOutAll": "Sign out of all devices", @@ -1560,7 +1612,8 @@ "cancel": "Cancel", "signingOut": "Signing out…", "signOutEverywhere": "Sign out everywhere", - "signOutFailed": "Sign-out failed: {message}" + "signOutFailed": "Sign-out failed: {message}", + "displayNamePlaceholder": "Ada Lovelace" }, "appearance": { "title": "Appearance", @@ -1598,7 +1651,7 @@ "nameLabel": "Name", "namePlaceholder": "e.g. Zapier automation", "scopesLabel": "Scopes", - "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.", + "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.", "cancel": "Cancel", "creating": "Creating…", "createKey": "Create key", @@ -1611,7 +1664,16 @@ "newKeyTitle": "New API key", "newKeyDesc": "Name it after the integration that will use it, and grant only the scopes it needs.", "done": "Done", - "networkError": "Could not reach the server" + "networkError": "Could not reach the server", + "scopeDesc": { + "messages:send": "Send WhatsApp messages", + "messages:read": "Read messages and their delivery status", + "contacts:read": "List and read contacts", + "contacts:write": "Create and update contacts", + "conversations:read": "List and read conversations", + "broadcasts:send": "Launch broadcast campaigns", + "webhooks:manage": "Register and manage outbound event webhooks" + } }, "deals": { "title": "Deals & currency", @@ -1691,6 +1753,259 @@ "reindex": "Reindex", "reindexTooltip": "Re-embed all documents (e.g. after adding an embeddings key)", "loading": "Loading…" + }, + "sectionsNav": "Settings sections", + "quickReplies": { + "nameRequired": "Give the quick reply a name.", + "saveError": "Couldn't save the quick reply.", + "updated": "Quick reply updated.", + "created": "Quick reply created.", + "deleteConfirm": "Delete this quick reply?", + "deleteError": "Couldn't delete the quick reply.", + "title": "Quick replies", + "description": "Reusable snippets — plain text or a saved interactive message — that agents can insert from the inbox composer.", + "newQuickReply": "New quick reply", + "empty": "No quick replies yet. Create one to reuse it across conversations.", + "editQuickReply": "Edit quick reply", + "nameLabel": "Name", + "namePlaceholder": "e.g. Business hours", + "kindText": "Text", + "kindInteractive": "Interactive", + "textPlaceholder": "The message text to insert", + "cancel": "Cancel", + "save": "Save" + } + }, + "Notifications": { + "title": "Notifications", + "subtitle": "Conversations other teammates assign to you show up here.", + "markAllAsRead": "Mark all as read", + "retry": "Retry", + "emptyTitle": "No notifications yet", + "emptyBody": "You'll see an alert here when someone assigns you a conversation.", + "unread": "Unread", + "markReadError": "Failed to mark notification as read", + "markAllError": "Failed to mark all as read" + }, + "Signup": { + "title": "Create account", + "titleInvite": "Create account & join", + "description": "Get started with CRM Template for WhatsApp", + "descriptionInvite": "Verify your email, then accept the invitation to join your team.", + "checkEmailTitle": "Check your email", + "checkEmailBodyBefore": "We've sent a confirmation link to", + "checkEmailBodyAfter": ". Please check your inbox and click the link to verify your account.", + "backToSignIn": "Back to sign in", + "fullName": "Full name", + "fullNamePlaceholder": "John Doe", + "email": "Email", + "password": "Password", + "passwordPlaceholder": "At least 6 characters", + "confirmPassword": "Confirm password", + "confirmPasswordPlaceholder": "Repeat your password", + "createAccount": "Create account", + "creatingAccount": "Creating account...", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", + "passwordsDoNotMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters" + }, + "ForgotPassword": { + "title": "Reset password", + "description": "Enter your email and we'll send you a reset link", + "checkEmailTitle": "Check your email", + "checkEmailBodyBefore": "We've sent a password reset link to", + "checkEmailBodyAfter": ". Please check your inbox.", + "backToSignIn": "Back to sign in", + "email": "Email", + "sending": "Sending...", + "sendResetLink": "Send reset link" + }, + "Join": { + "verifying": "Verifying invitation…", + "tryAgain": "Try again", + "createAccountInstead": "Create a new account instead", + "signIn": "Sign in", + "invitedTo": "You're invited to", + "joinAs": "You'll join as", + "role": { + "admin": "Admin", + "agent": "Agent", + "viewer": "Viewer" + }, + "linkValidUntil": ". Link valid until {date}.", + "accepting": "Accepting…", + "acceptInvitation": "Accept invitation", + "acceptHelpBefore": "Accepting moves your login into", + "acceptHelpAfter": ". Your empty personal account from signup will be cleaned up.", + "conflictTitle": "Can't join {name} with this account", + "conflictHelpBefore": "To join", + "conflictHelpAfter": ", sign out and sign up again with a different email address. The invite link stays valid as long as it hasn't expired.", + "staySignedIn": "Stay signed in", + "signingOut": "Signing out…", + "signOutDifferentEmail": "Sign out & use a different email", + "createAccountAndJoin": "Create account & join", + "alreadyHaveAccount": "I already have an account", + "fail": { + "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." + }, + "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." + }, + "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." + }, + "server_error": { + "title": "Something went wrong", + "body": "We couldn't verify this invitation right now. Try refreshing the page in a moment." + } + }, + "conflictFallback": "You are already in another account. Sign in with a different email to join this one.", + "acceptError": "Failed to accept invitation", + "welcomeToast": "Welcome to the team", + "serverUnreachable": "Could not reach the server", + "signOutError": "Could not sign out. Try refreshing the page." + }, + "Agents": { + "title": "AI Agents", + "subtitle": "Your bring-your-own-key AI agent — set it up, then test it in the playground before it replies to customers in the inbox.", + "tabPlayground": "Playground", + "tabSetup": "Setup", + "tabUsage": "Usage" + }, + "AiPlayground": { + "notConfiguredToast": "No agent configured yet — finish Setup first.", + "noReplyToast": "Couldn't get a reply.", + "unreachableToast": "Couldn't reach the agent.", + "headerTitle": "Playground", + "headerSubtitle": "— test replies as if you were a customer", + "reset": "Reset", + "emptyTitle": "Send a message to see how your agent would reply.", + "emptyDescription": "It uses your knowledge base and behaves exactly like the auto-reply bot — including handoff.", + "goToSetup": "Not set up yet? Go to Setup", + "handoffLabel": "Would hand off to a human here", + "thinking": "Thinking…", + "composerPlaceholder": "Type a customer message…" + }, + "AiUsage": { + "loadError": "Failed to load usage", + "tokenUsage": "Token usage", + "description": "Tokens spent on your provider key by drafts and the auto-reply bot. Counts only — no message content is stored here.", + "lastDays": "Last {w} days", + "noUsage": "No AI usage in the last {n} days yet.", + "noUsageHint": "This fills in as the assistant drafts and auto-replies.", + "totalTokens": "Total tokens", + "llmCalls": "LLM calls", + "autoReply": "Auto-reply", + "drafts": "Drafts", + "tokensPerDay": "Tokens per day", + "byModel": "By model", + "modelUsage": "{tokens} tok · {calls, plural, one {# call} other {# calls}}", + "truncatedNote": "Showing a partial window — usage is high enough that only the most recent records are summarized here." + }, + "FlowsHeader": { + "backToFlows": "Back to Flows", + "flowNamePlaceholder": "Flow name", + "flowNameLabel": "Flow name", + "unsavedChanges": "Unsaved changes — hit Save to persist", + "edited": "Edited", + "runs": "Runs", + "delete": "Delete", + "pause": "Pause", + "activate": "Activate", + "fixIssuesBeforeActivating": "Fix the issues below before activating", + "save": "Save", + "descriptionPlaceholder": "Add a short description (internal — customers don't see this)", + "descriptionLabel": "Flow description", + "statusDraft": "Draft", + "statusActive": "Active", + "statusArchived": "Archived" + }, + "InteractiveBuilder": { + "replyButtons": "Reply buttons", + "list": "List", + "body": "Body", + "bodyPlaceholder": "What the customer reads above the options", + "headerOptional": "Header (optional)", + "footerOptional": "Footer (optional)", + "showReplyIds": "Show reply IDs (advanced)", + "preview": "Preview", + "buttonsCount": "Buttons ({count}/{max})", + "buttonLabel": "Button label", + "idPlaceholder": "id", + "addButton": "Add button", + "listButtonLabel": "List button label", + "rowsCount": "Rows ({count}/{max})", + "sectionTitleOptional": "Section title (optional)", + "rowTitle": "Row title", + "descriptionOptional": "Description (optional)", + "addRow": "Add row", + "addSection": "Add section" + }, + "InteractivePreview": { + "messageBody": "Message body…", + "button": "Button", + "menu": "Menu" + }, + "FlowTemplates": { + "welcome_menu": { + "name": "Welcome menu", + "description": "Greet customers who type a keyword and route them to the right agent based on whether they're new or existing." + }, + "faq_bot": { + "name": "FAQ bot", + "description": "Answer common questions automatically. Customer picks a topic from a list; the bot replies with the answer and ends." + }, + "lead_capture": { + "name": "Lead capture", + "description": "Greet first-time inbounds, capture name + email + company, then hand off to sales with the answers in the note." } + }, + "AutomationTemplates": { + "welcome_message": { + "name": "Welcome Message", + "description": "Auto-reply to first-time contacts with a greeting." + }, + "out_of_office": { + "name": "Out of Office", + "description": "Auto-reply during off-hours so nobody is left waiting." + }, + "lead_qualifier": { + "name": "Lead Qualifier", + "description": "Ask qualification questions to filter inbound leads." + }, + "follow_up_reminder": { + "name": "Follow-up Reminder", + "description": "Send a nudge if a contact has not replied within 24 hours." + } + }, + "Themes": { + "violet": { + "name": "Violet", + "description": "The default — confident, slightly playful." + }, + "emerald": { + "name": "Emerald", + "description": "Growth-coded, nods at messaging without copying WhatsApp green." + }, + "cobalt": { + "name": "Cobalt", + "description": "Clean B2B-SaaS blue — calm and product-y." + }, + "amber": { + "name": "Amber", + "description": "Warm and friendly — feels good for SMB teams." + }, + "rose": { + "name": "Rose", + "description": "Bold and modern — D2C, creator-economy, lifestyle." + } + }, + "Common": { + "loading": "Loading..." } } diff --git a/messages/es.json b/messages/es.json new file mode 100644 index 0000000000..3b4e088685 --- /dev/null +++ b/messages/es.json @@ -0,0 +1,2011 @@ +{ + "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 de IA", + "settings": "Configuración", + "beta": "Beta", + "unreadConversations": "{count} {count, plural, =1 {conversación} other {conversaciones}} sin leer", + "unreadNotifications": "{count} {count, plural, =1 {notificación} other {notificaciones}} sin leer", + "roleOwner": "Propietario", + "roleAdmin": "Administrador", + "roleAgent": "Agente", + "roleViewer": "Espectador", + "closeMenu": "Cerrar menú", + "defaultUser": "Usuario", + "defaultAvatar": "Avatar", + "menuProfile": "Perfil", + "menuSettings": "Configuración", + "menuSignOut": "Cerrar sesión", + "primaryNav": "Principal" + }, + "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 al modo {mode}" + }, + "Dashboard": { + "page": { + "title": "Panel", + "description": "Analíticas en vivo de conversaciones, contactos, negocios, difusiones y automatizaciones.", + "activeConversations": "Conversaciones activas", + "newContactsToday": "Nuevos contactos 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": "La actividad de mensajes, negocios, difusiones y automatizaciones aparecerá aquí.", + "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 poblar 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 promedio de primera respuesta", + "description": "Minutos para responder el primer mensaje sin contestar de un cliente, por día de la semana", + "target": "objetivo {minutes}m", + "thisWeek": "Esta semana:", + "lastWeek": "Semana pasada:", + "noReplies": "Aún no se han registrado respuestas", + "noRepliesHint": "Este gráfico se completa a medida que respondes los mensajes de los clientes." + }, + "emptyState": { + "title": "Aún no hay datos suficientes" + } + }, + "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 las 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": "Asignada", + "unassign": "Quitar asignación", + "noTeammates": "No hay compañeros 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 chatear", + "today": "Hoy", + "yesterday": "Ayer", + "toastSendFailed": "No se pudo enviar: {reason}", + "toastSendTemplateFailed": "No se pudo enviar la plantilla: {reason}", + "toastWaitForSend": "Espera a que el mensaje termine de enviarse", + "toastReactionFailed": "La reacción falló: {reason}", + "toastAssignmentFailed": "No se pudo actualizar la asignación" + }, + "sessionTimer": { + "expired": "Expirada", + "xhRemaining": "{hours}h restantes", + "xmRemaining": "{minutes}m restantes", + "noCustomerMessages": "No hay mensajes del cliente" + }, + "composer": { + "draftHint": "Toca el ✨ para redactar una respuesta con IA — puedes editarla antes de enviar", + "sessionExpiredHint": "La sesión de 24 horas expiró. Usa una plantilla para retomar el contacto.", + "templates": "Plantillas", + "readOnlyPlaceholder": "Solo lectura — los espectadores pueden navegar 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 una respuesta con IA", + "addCaption": "Agrega una descripción…", + "removeAttachment": "Quitar 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.", + "aiNotConfigured": "La IA aún no está configurada: actívala en Configuración → Asistente de IA.", + "draftFailed": "No se pudo redactar una respuesta.", + "assistantNoReply": "El asistente no devolvió una respuesta.", + "aiUnreachable": "No se pudo conectar con el asistente de IA.", + "recordingTooLong": "La grabación es demasiado larga (más de 16 MB).", + "voiceUnsupported": "La grabación de voz no es compatible con este navegador.", + "micDenied": "Acceso al micrófono denegado o no disponible." + }, + "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 para copiar", + "copied": "Copiado", + "copyFailed": "Error al copiar" + }, + "replyQuote": { + "photo": "[Foto]", + "video": "[Video]", + "document": "[Documento]", + "audio": "[Audio]", + "voiceMessage": "[Mensaje de voz]", + "location": "[Ubicación]", + "template": "[Plantilla]", + "message": "[Mensaje]", + "cancelReply": "Cancelar respuesta" + }, + "templatePicker": { + "sendTemplate": "Enviar plantilla", + "fillPlaceholders": "Completa los campos para generar esta plantilla. Meta requiere que todas las variables estén definidas.", + "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 electrónico", + "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", + "selectAllAria": "Seleccionar todos los contactos de esta página" + }, + "form": { + "addTitle": "Agregar contacto", + "addDesc": "Crea un nuevo contacto. El nombre es obligatorio.", + "editTitle": "Editar contacto", + "editDesc": "Actualiza los datos y las etiquetas del contacto.", + "nameLabel": "Nombre", + "namePlaceholder": "Juan 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": "juan@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 correctamente", + "toastSuccessEdit": "Contacto actualizado correctamente", + "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. Crea etiquetas 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", + "tags": "Etiquetas", + "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": "Detalles del contacto", + "phone": "Teléfono", + "email": "Correo electrónico", + "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. Crea etiquetas 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 phone obligatoria. Opcionales: 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 no mostrada", + "moreRows_plural": "+ {count} filas más no mostradas", + "importComplete": "Importación completada", + "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 electrónico", + "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": "Etiquetas desconocidas omitidas (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": "Error en la importación" + }, + "customFields": { + "title": "Campos personalizados", + "desc": "Define campos de contacto adicionales (p. ej. código postal, origen del cliente potencial). 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 almacenado en cada contacto. No se puede deshacer.", + "toastNoAccount": "Tu perfil no está vinculado a una cuenta.", + "toastDuplicate": "Ya existe un campo llamado \"{name}\".", + "toastCreateFailed": "No se pudo crear el campo. Puede 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 hacer seguimiento de los negocios", + "createPipeline": "Crear embudo", + "newPipeline": "Nuevo embudo", + "pipelineName": "Nombre del embudo", + "pipelineNamePlaceholder": "p. ej., Ventas empresariales", + "defaultStagesDesc": "Las etapas predeterminadas (Nuevo cliente potencial → 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 una 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 de cierre estimada", + "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 una 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", + "valuePlaceholder": "0" + }, + "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 en 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 sí se incluyen.", + "pipelineValue": "Valor del embudo", + "pipelineValueTooltip": "Suma de los valores monetarios 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 solo negocio no perdido.", + "weightedValue": "Valor ponderado", + "weightedValueTooltip": "Ingresos esperados: el valor de cada negocio abierto × la probabilidad de su etapa. La primera etapa ≈ 10%, las etapas avanzan hasta el 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ídos", + "status": "Estado", + "date": "Fecha" + }, + "errorLoad": "No se pudieron cargar las difusiones", + "retry": "Reintentar", + "inProgressAria": "Difusión en curso" + }, + "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 {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 activamente", + "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": "Dale un nombre a la difusión antes de guardar un borrador.", + "toastNotSignedIn": "No has iniciado sesión.", + "toastNotLinked": "Tu perfil no está vinculado a una 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": "Primero crea 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 los contactos que coincidan con etiquetas específicas.", + "customFieldDesc": "Enviar a los contactos que coincidan con una regla de campo personalizado.", + "csvDesc": "Sube un CSV con números de teléfono (y nombres opcionales).", + "selectTags": "Selecciona etiquetas", + "searchTags": "Buscar etiquetas...", + "noTagsFound": "No se encontraron etiquetas.", + "excludeTags": "Excluir etiquetas (opcional)", + "searchExcludeTags": "Buscar etiquetas para 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) encontrado(s) en el CSV.", + "errorLoadTags": "No se pudieron cargar las etiquetas", + "errorLoadFields": "No se pudieron cargar los campos personalizados", + "errorCsvParse": "No se pudo analizar el archivo CSV.", + "errorCsvMissingPhone": "El CSV debe tener una columna 'phone'.", + "summaryTitle": "Resumen de la audiencia", + "calculating": "Calculando…", + "estimatedRecipients": "destinatarios estimados", + "selectTypeHint": "Selecciona un tipo de audiencia para ver la estimación." + }, + "personalize": { + "title": "Personalizar variables", + "subtitle": "Asigna las variables de tu plantilla a campos de contacto o proporciona valores estáticos.", + "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": "Define valores para {{name}}", + "type": "Tipo", + "typeStatic": "Valor estático", + "typeContact": "Campo de contacto", + "typeCustom": "Campo personalizado", + "staticValue": "Valor estático", + "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 ejemplo.", + "noPreview": "Vista previa no disponible.", + "fieldMap": { + "name": "Nombre", + "phone": "Teléfono", + "email": "Correo electrónico" + }, + "enterValuePlaceholder": "Ingresa un valor..." + }, + "scheduleSend": { + "title": "Revisar y enviar", + "subtitle": "Dale 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": "Filtrada por etiquetas", + "audienceField": "Filtrada 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", + "estimatedReach": "Alcance estimado", + "language": "Idioma", + "confirmTitle": "Confirmar difusión" + } + } + }, + "Automations": { + "list": { + "title": "Automatizaciones", + "subtitle": "Crea flujos de trabajo que reaccionen automáticamente a los 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" + }, + "activeAria": "activa", + "openMenu": "Abrir menú" + }, + "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": "exitoso", + "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/Si no)", + "send_webhook": "Enviar webhook", + "close_conversation": "Cerrar conversación" + }, + "triggers": { + "new_message_received": { + "label": "Nuevo mensaje 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 para contactos agregados manualmente)" + }, + "keyword_match": { + "label": "Coincidencia de palabra clave", + "hint": "El mensaje contiene palabra(s) clave específica(s)" + }, + "interactive_reply": { + "label": "Respuesta de botón / lista", + "hint": "El cliente toca un botón o una fila de lista cuyo id coincide — encadena menús entre automatizaciones" + }, + "new_contact_created": { + "label": "Nuevo contacto creado", + "hint": "Cuando un contacto se crea automáticamente 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": "sí, no, más_info", + "replyIdsHelp": "IDs de botón/fila de lista separados por comas para coincidir de forma exacta. Usa los mismos ids que definiste en un paso Enviar botones / Enviar lista.", + "schedule": "Horario", + "scheduleHint": "Expresión cron (p. ej. 0 9 * * 1-5)", + "active": "Activa", + "activeAria": "Activa", + "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 electrónico", + "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 un embudo primero…", + "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 de aprobadas", + "unknownLang": "desconocido" + }, + "branches": { + "yes": "Sí", + "no": "No" + }, + "addStep": "Agregar paso", + "config": { + "messageText": "Texto del mensaje", + "placeholderMessageText": "¡Hola! Gracias por contactarnos…", + "tagLabel": "Etiqueta", + "modeLabel": "Modo", + "modes": { + "round_robin": "Rotación (round-robin)", + "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": "Sujeto", + "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": "Exacta", + "closeConversationHint": "Establece el estado de la conversación en \"cerrada\". No se necesita configuración." + }, + "schedulePlaceholder": "Expresión cron u HH:mm", + "moveUp": "Mover hacia arriba", + "moveDown": "Mover hacia abajo", + "delete": "Eliminar" + } + }, + "Flows": { + "list": { + "title": "Flujos", + "beta": "Beta", + "description": "Crea conversaciones de WhatsApp ramificadas y guiadas por botones. Útiles para menús, preguntas frecuentes y clasificación antes de que intervenga una persona.", + "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 clonar", + "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 búsqueda 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": "Comienza desde una plantilla o crea desde cero.", + "startTemplate": "Comenzar desde una plantilla", + "nodeCount": "{count} {count, plural, =1 {nodo} other {nodos}}", + "startBlank": "O comienza 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 los flujos", + "loadError": "No se pudo cargar el flujo." + }, + "logs": { + "title": "Ejecuciones", + "description": "Las 50 veces más recientes que se ejecutó este flujo. Expande una fila para ver el registro del motor paso a paso.", + "emptyState": "Aún no hay ejecuciones. Activa el flujo desde un número personal de WhatsApp para verlo aparecer aquí.", + "loadError": "No se pudieron cargar las ejecuciones.", + "unknownContact": "Contacto desconocido", + "atNode": "en {node}", + "started": "Iniciado {time}", + "reprompts": "{count} re-preguntas", + "ranFor": "duró {duration}", + "capturedVars": "Variables capturadas ({count})", + "noEvents": "No se registraron eventos para esta ejecución.", + "statusActive": "Activa", + "statusCompleted": "Completada", + "statusHandedOff": "Transferida", + "statusTimedOut": "Agotó el tiempo", + "statusPaused": "Pausada por un agente", + "statusFailed": "Fallida" + }, + "builder": { + "triggerTitle": "Disparador", + "whenLabel": "Cuando…", + "triggerKeywordTitle": "Un mensaje contiene una palabra clave", + "triggerFirstInboundTitle": "El 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 luego un Transferencia — esa es la forma de menú de bienvenida del resumen.", + "addNode": "Agregar nodo", + "setAsEntry": "Establecer como entrada", + "removeNode": "Quitar nodo", + "showAdvanced": "Mostrar avanzado", + "hideAdvanced": "Ocultar avanzado", + "nodeKeyLabel": "Clave del nodo (identificador interno — manténla estable para las analíticas)", + "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 modificarlos.", + "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 (almacenada en flow_runs.vars; alfanumérico + guion bajo)", + "varKeyPlaceholder": "p. ej. nombre, correo, empresa", + "varKeyHelp": "Interpólala en las preguntas posteriores y en las notas de transferencia con", + "advanceAfterCapture": "Tras capturar, avanzar a", + "internalNote": "Nota interna (para el agente que la reciba)", + "endNodeHelp": "Nodo terminal. Cuando el ejecutor llega a este nodo, la ejecución se marca como completada. No se necesita 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": "Nodo siguiente…", + "addButton": "Agregar botón", + "buttonLabel": "Etiqueta del botón para desplegar (≤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 la 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": "Descripción (opcional, se muestra debajo del archivo)", + "filenameLabel": "Nombre de archivo mostrado al cliente (solo documentos)", + "filenamePlaceholder": "factura.pdf", + "advanceAfterSending": "Tras enviar, avanzar a", + "pickNextNode": "Elige un nodo siguiente…", + "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 que se pueden tocar" + }, + "send_media": { + "label": "Enviar archivo", + "blurb": "Envía una imagen, video o documento" + }, + "collect_input": { + "label": "Recopilar respuesta", + "blurb": "Hace una pregunta y guarda la respuesta" + }, + "condition": { + "label": "Si / si no", + "blurb": "Se ramifica según una regla" + }, + "set_tag": { + "label": "Etiquetar contacto", + "blurb": "Agrega o quita una etiqueta del contacto" + }, + "handoff": { + "label": "Transferir a un agente", + "blurb": "Transfiere la conversación a una persona" + }, + "end": { + "label": "Fin", + "blurb": "Termina el flujo" + } + }, + "editorView": "Vista del editor" + }, + "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} (ningún 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 no configurado", + "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} pendientes de 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 cada compañero puede hacer.", + "inviteMember": "Invitar miembro", + "online": "en línea", + "away": "ausente", + "offline": "desconectado", + "memberCount": "{count} {count, plural, =1 {miembro} other {miembros}}", + "you": "Tú", + "unnamed": "Sin nombre", + "joined": "Se unió {date}", + "remove": "Quitar", + "removedToast": "Se quitó a {name}", + "updatedToast": "Se actualizó {name} a {role}", + "pendingInvitations": "Invitaciones pendientes", + "inviteHint": "La URL de invitación en texto plano solo se muestra una vez al crearla por seguridad — 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 {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 de esta cuenta y se le dará una nueva cuenta personal en su próximo inicio de sesión. Su acceso no se elimina.", + "cancel": "Cancelar", + "removing": "Quitando...", + "removeBtn": "Quitar miembro", + "loadMembersFailed": "No se pudieron cargar los miembros", + "loadInvitationsFailed": "No se pudieron cargar las invitaciones", + "serverUnreachable": "No se pudo acceder al servidor", + "updateRoleFailed": "No se pudo actualizar el rol", + "removeMemberFailed": "No se pudo eliminar al miembro", + "revokeInvitationFailed": "No se pudo revocar la invitación", + "memberAlt": "Miembro" + }, + "invite": { + "inviteCreated": "Invitación creada", + "inviteCreatedDesc": "Comparte este enlace con tu nuevo compañero. 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 cierras este diálogo, la URL desaparece. Para volver a compartir, revoca esta invitación y crea una nueva.", + "sendViaWhatsApp": "Enviar por WhatsApp", + "done": "Listo", + "dialogTitle": "Invitar a un compañero", + "dialogDesc": "Genera un enlace de invitación de un solo uso. Compártelo por WhatsApp, Slack o el canal que prefieras — no se requiere 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}", + "createFailed": "No se pudo crear la invitación", + "serverUnreachable": "No se pudo acceder al servidor. ¿Intentar de nuevo?" + }, + "tagsAndFields": { + "title": "Campos y etiquetas", + "description": "Dos formas de organizar los 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 de contacto adicionales (p. ej. código postal, origen del cliente potencial). 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 tu 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 de 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 su 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 reenviar 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 + una estructura de botón OTP que necesita un editor diferente. Por ahora, créalas 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 es 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 es fijo una vez que la plantilla existe en Meta.", + "langHint": "Debe coincidir con el código exacto en Meta — en_US y en son distintos.", + "header": "Encabezado", + "headerNone": "Ninguno", + "headerText": "Texto", + "headerImage": "Imagen", + "headerVideo": "Video", + "headerDocument": "Documento", + "headerTextPlaceholder": "Texto del encabezado (máx. 60 caracteres, opcional {{1}})", + "headerSampleAria": "Valor de ejemplo para la variable del encabezado", + "headerSamplePlaceholder": "Valor de ejemplo 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 de {format})", + "imageHint": "Sube un JPEG/PNG (≤5 MB, ≥800×418 px recomendado) o pega un enlace HTTPS público — lo subimos a Meta para su revisión automáticamente.", + "mediaHint": "Debe ser un enlace HTTPS de acceso público. Meta lo obtiene una vez durante la revisión, así que debe permanecer activo durante ~24 h.", + "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 las variables (deben ser contiguas empezando en {{1}}).", + "sampleValues": "Valores de ejemplo (Meta los usa para revisar tu plantilla)", + "sampleAria": "Valor de ejemplo para la variable del cuerpo {var}", + "samplePlaceholder": "Ejemplo 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 de RESPUESTA RÁPIDA deben ir antes de los botones de URL / teléfono / copiar código.", + "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. Vuelve a sincronizar para continuar, o contacta a soporte si esto persiste.", + "toastSyncError": "No se pudieron sincronizar las plantillas", + "toastDeleteSuccess": "Plantilla eliminada", + "toastDeleteError": "No se pudo eliminar la plantilla", + "toastInvalidImage": "La imagen del 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": "Administrar miembros + todo", + "agent": "Agente", + "agentHint": "Usar funciones; sin configuración", + "viewer": "Espectador", + "viewerHint": "Solo lectura en toda la aplicación", + "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 almacenado no se puede descifrar", + "resetting": "Restableciendo...", + "resetConfig": "Restablecer configuración", + "credentialsValid": "Credenciales válidas", + "notConnected": "No conectado", + "connectedDesc": "Tu token de acceso te 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 entregará eventos a wacrm", + "notRegistered": "No registrado — Meta no entregará 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 2 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 se omitió el registro. Ingresa el PIN de 2 pasos abajo y haz clic en Guardar configuración para suscribirlo.", + "diagnosticLastRun": "Diagnóstico — última ejecución: ", + "live": "activo", + "notLive": "no activo", + "apiCredentialsTitle": "Credenciales de la 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 del 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 los 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, luego pégalo aquí para que wacrm pueda suscribir el número — de lo contrario, Meta enruta los eventos entrantes a la última aplicación que lo reclamó (el síntoma que afecta a los segundos números bajo una WABA compartida). Los números de prueba de Meta no tienen PIN y están preregistrados — déjalo en blanco para ellos. Dejarlo en blanco también mantiene sin cambios un registro existente.", + "webhookTitle": "Configuración del webhook", + "webhookDesc": "Usa esta URL como tu callback de webhook en el Panel de aplicaciones de Meta.", + "webhookUrl": "URL de callback del webhook", + "saving": "Guardando...", + "saveConfig": "Guardar configuración", + "testing": "Probando...", + "testConnection": "Probar conexión de la API", + "setupInstructions": "Instrucciones de configuración", + "setupInstructionsDesc": "Sigue estos pasos para conectar tu API de WhatsApp Business.", + "step1": "Crea una aplicación de Meta", + "step1_1": "Ve a developers.facebook.com", + "step1_2": "Haz clic en \"Mis aplicaciones\" y luego en \"Crear aplicación\"", + "step1_3": "Selecciona \"Negocios\" como tipo de aplicación", + "step1_4": "Completa los detalles de la aplicación y créala", + "step2": "Agrega el producto WhatsApp", + "step2_1": "En el panel de tu aplicación, 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 negocio", + "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 del negocio > 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 callback del webhook de arriba", + "step4_4": "Ingresa el mismo Token de verificación que definiste aquí", + "step4_5": "Suscríbete al campo de webhook \"messages\"", + "metaDocs": "Documentación de la API de WhatsApp de Meta", + "phoneNumberIdRequired": "El ID del número de teléfono es obligatorio", + "accessTokenRequired": "El token de acceso es obligatorio para la configuración inicial", + "loadFailed": "No se pudo cargar la configuración de WhatsApp", + "saveFailed": "No se pudo guardar la configuración", + "reenterToken": "Vuelve a ingresar el token de acceso para guardar los cambios", + "savedRegisterFailed": "Se guardó, pero Meta no pudo registrar el número: {error}", + "savedRegisterSkipped": "Credenciales guardadas y verificadas. Se omitió el registro de entrada (sin PIN) — consulta el estado de registro abajo.", + "savedLive": "Activo — {name} ya puede recibir eventos.", + "savedConnected": "WhatsApp conectado. Los eventos comenzarán a llegar en un minuto.", + "connectedTo": "Conectado a {name}", + "connectionSuccess": "Conexión con la API exitosa", + "connectionFailed": "Error de conexión con la API", + "connectionTestFailed": "La prueba de conexión falló. Revisa la red e inténtalo de nuevo.", + "numberLive": "El número está totalmente configurado — Meta está entregando eventos.", + "numberNotRegistered": "El número no está totalmente registrado. Consulta las verificaciones a continuación para ver qué paso falló.", + "verifyEndpointFailed": "No se pudo acceder al endpoint de verificación.", + "resetConfirm": "Esto eliminará la configuración actual de WhatsApp para que puedas ingresarla de nuevo. ¿Continuar?", + "resetFailed": "No se pudo restablecer la configuración", + "resetSuccess": "Configuración eliminada. Ahora puedes volver a ingresar tus credenciales.", + "webhookCopied": "URL del webhook copiada al portapapeles", + "phoneNumberIdPlaceholder": "ej.: 100234567890123", + "wabaIdPlaceholder": "ej.: 100234567890456" + }, + "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", + "quick-replies": "Respuestas rápidas" + }, + "groups": { + "account": "Cuenta", + "workspace": "Espacio de trabajo" + }, + "profile": { + "title": "Tu perfil", + "description": "Cómo apareces en toda la aplicación. Tu avatar y tu nombre aparecen en el encabezado, la barra lateral y en cualquier lugar donde te vean tus compañeros.", + "changePhoto": "Cambiar foto", + "uploadPhoto": "Subir foto", + "remove": "Quitar", + "photoHint": "PNG, JPG, WebP o GIF. Hasta 2 MB.", + "displayName": "Nombre visible", + "email": "Correo electrónico", + "emailChangeHint": "Revisa la bandeja de entrada de {oldEmail} y {newEmail} — ambas deben confirmar antes de que el cambio surta efecto.", + "accountDetails": "Detalles 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 visible 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. Permanecerás con la sesión iniciada en este dispositivo después de cambiarla.", + "currentPassword": "Contraseña actual", + "newPassword": "Nueva contraseña", + "confirmPassword": "Confirmar nueva contraseña", + "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 nueva contraseña 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 la sesión en todos los dispositivos donde has 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": "Se cerrará la sesión en todos los dispositivos con acceso a esta cuenta y tendrán que iniciar sesión de nuevo. 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}", + "displayNamePlaceholder": "Ada Lovelace" + }, + "appearance": { + "title": "Apariencia", + "description": "Define el modo y el color de acento usados en toda la aplicación. 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 la sesión en tus dispositivos. Estas opciones mantienen 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": "Pídele a un administrador que cree una.", + "revoked": "Revocada", + "expired": "Expirada", + "noScopes": "Sin permisos", + "created": "Creada {date}", + "lastUsed": "usada por última vez {date}", + "neverUsed": "nunca usada", + "expires": "expira {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": "Dale un nombre a la clave", + "nameLabel": "Nombre", + "namePlaceholder": "p. ej. Automatización de Zapier", + "scopesLabel": "Permisos", + "scopesHint": "Una clave sin permisos 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 permisos que necesita.", + "done": "Listo", + "networkError": "No se pudo conectar con el servidor", + "scopeDesc": { + "messages:send": "Enviar mensajes de WhatsApp", + "messages:read": "Leer mensajes y su estado de entrega", + "contacts:read": "Listar y leer contactos", + "contacts:write": "Crear y actualizar contactos", + "conversations:read": "Listar y leer conversaciones", + "broadcasts:send": "Lanzar campañas de difusión", + "webhooks:manage": "Registrar y gestionar webhooks de eventos salientes" + } + }, + "deals": { + "title": "Negocios y moneda", + "description": "La moneda usada para los nuevos negocios y para los totales del embudo y del panel.", + "defaultCurrency": "Moneda predeterminada", + "defaultCurrencyDesc": "Los nuevos negocios 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 con 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 la IA.", + "loadFailed": "No se pudo cargar la configuración de la 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 generar embeddings de tu base de conocimiento (text-embedding-3-small){sameKeyText}. Déjala en blanco para usar la búsqueda por palabras clave. 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, lo que 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 de café. Sé cálido y conciso. Nunca cotices precios ni fechas de entrega — transfiere a una persona para eso.", + "enableAssistant": "Habilitar asistente de IA", + "enableAssistantDesc": "Interruptor principal. Activa el botón “Redactar con IA” en la bandeja de entrada.", + "autoReply": "Respuesta automática a mensajes entrantes", + "autoReplyDesc": "El bot responde los nuevos mensajes entrantes automáticamente (solo cuando ningún flujo los gestiona y no hay ningún agente asignado). Transfiere a una persona 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 se queda en silencio.", + "handoffTo": "Transferir a", + "handoffToDesc": "Cuando el bot no puede ayudar — o alcanza el límite de respuestas — se detiene y enruta el chat aquí, con una nota breve sobre lo que ocurrió.", + "handoffQueue": "Cola sin asignar (cualquier agente puede tomarlo)", + "remove": "Quitar", + "save": "Guardar", + "missingModel": "Ingresa un nombre de 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": "Error al reindexar.", + "title": "Base de conocimiento", + "description": "Agrega preguntas frecuentes, políticas o detalles de productos. El asistente recupera las partes relevantes al redactar y responder automáticamente, para que pueda responder en lugar de transferir.{searchType}", + "semanticSearchOn": " La búsqueda semántica está activada (clave de embeddings definida).", + "keywordSearchOn": " Usando búsqueda por palabras clave — agrega una clave de embeddings arriba para la 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 de 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": "Volver a generar embeddings de todos los documentos (p. ej. después de agregar una clave de embeddings)", + "loading": "Cargando…" + }, + "sectionsNav": "Secciones de configuración", + "quickReplies": { + "nameRequired": "Ponle un nombre a la respuesta rápida.", + "saveError": "No se pudo guardar la respuesta rápida.", + "updated": "Respuesta rápida actualizada.", + "created": "Respuesta rápida creada.", + "deleteConfirm": "¿Eliminar esta respuesta rápida?", + "deleteError": "No se pudo eliminar la respuesta rápida.", + "title": "Respuestas rápidas", + "description": "Fragmentos reutilizables — texto simple o un mensaje interactivo guardado — que los agentes pueden insertar desde el compositor de la bandeja de entrada.", + "newQuickReply": "Nueva respuesta rápida", + "empty": "Aún no hay respuestas rápidas. Crea una para reutilizarla en las conversaciones.", + "editQuickReply": "Editar respuesta rápida", + "nameLabel": "Nombre", + "namePlaceholder": "ej.: Horario de atención", + "kindText": "Texto", + "kindInteractive": "Interactivo", + "textPlaceholder": "El texto del mensaje que se insertará", + "cancel": "Cancelar", + "save": "Guardar" + } + }, + "Notifications": { + "title": "Notificaciones", + "subtitle": "Las conversaciones que otros compañeros te asignan aparecen aquí.", + "markAllAsRead": "Marcar todas como leídas", + "retry": "Reintentar", + "emptyTitle": "Aún no hay notificaciones", + "emptyBody": "Verás una alerta aquí cuando alguien te asigne una conversación.", + "unread": "Sin leer", + "markReadError": "No se pudo marcar la notificación como leída", + "markAllError": "No se pudieron marcar todas como leídas" + }, + "Signup": { + "title": "Crear cuenta", + "titleInvite": "Crear cuenta y unirse", + "description": "Comienza con CRM Template for WhatsApp", + "descriptionInvite": "Verifica tu correo y luego acepta la invitación para unirte a tu equipo.", + "checkEmailTitle": "Revisa tu correo", + "checkEmailBodyBefore": "Hemos enviado un enlace de confirmación a", + "checkEmailBodyAfter": ". Revisa tu bandeja de entrada y haz clic en el enlace para verificar tu cuenta.", + "backToSignIn": "Volver al inicio de sesión", + "fullName": "Nombre completo", + "fullNamePlaceholder": "Juan Pérez", + "email": "Correo electrónico", + "password": "Contraseña", + "passwordPlaceholder": "Al menos 6 caracteres", + "confirmPassword": "Confirmar contraseña", + "confirmPasswordPlaceholder": "Repite tu contraseña", + "createAccount": "Crear cuenta", + "creatingAccount": "Creando cuenta...", + "alreadyHaveAccount": "¿Ya tienes una cuenta?", + "signIn": "Iniciar sesión", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "passwordTooShort": "La contraseña debe tener al menos 6 caracteres" + }, + "ForgotPassword": { + "title": "Restablecer contraseña", + "description": "Ingresa tu correo y te enviaremos un enlace de restablecimiento", + "checkEmailTitle": "Revisa tu correo", + "checkEmailBodyBefore": "Hemos enviado un enlace para restablecer la contraseña a", + "checkEmailBodyAfter": ". Revisa tu bandeja de entrada.", + "backToSignIn": "Volver al inicio de sesión", + "email": "Correo electrónico", + "sending": "Enviando...", + "sendResetLink": "Enviar enlace de restablecimiento" + }, + "Join": { + "verifying": "Verificando invitación…", + "tryAgain": "Reintentar", + "createAccountInstead": "Crear una cuenta nueva", + "signIn": "Iniciar sesión", + "invitedTo": "Te han invitado a", + "joinAs": "Te unirás como", + "role": { + "admin": "Administrador", + "agent": "Agente", + "viewer": "Visor" + }, + "linkValidUntil": ". Enlace válido hasta {date}.", + "accepting": "Aceptando…", + "acceptInvitation": "Aceptar invitación", + "acceptHelpBefore": "Al aceptar, tu sesión se traslada a", + "acceptHelpAfter": ". Tu cuenta personal vacía creada al registrarte se eliminará.", + "conflictTitle": "No puedes unirte a {name} con esta cuenta", + "conflictHelpBefore": "Para unirte a", + "conflictHelpAfter": ", cierra sesión y regístrate de nuevo con una dirección de correo diferente. El enlace de invitación sigue siendo válido mientras no haya expirado.", + "staySignedIn": "Permanecer conectado", + "signingOut": "Cerrando sesión…", + "signOutDifferentEmail": "Cerrar sesión y usar otro correo", + "createAccountAndJoin": "Crear cuenta y unirse", + "alreadyHaveAccount": "Ya tengo una cuenta", + "fail": { + "not_found": { + "title": "Invitación no encontrada", + "body": "Este enlace no coincide con una invitación válida. Verifica la URL o pide a quien te invitó que te envíe una nueva." + }, + "used": { + "title": "Invitación ya utilizada", + "body": "Esta invitación ya fue aceptada. Si no fuiste tú, pide al administrador de la cuenta que envíe un enlace nuevo." + }, + "expired": { + "title": "Invitación expirada", + "body": "Esta invitación ha expirado. Pide al administrador de la cuenta que envíe una nueva — tardan unos segundos en generarse." + }, + "server_error": { + "title": "Algo salió mal", + "body": "No pudimos verificar esta invitación en este momento. Intenta actualizar la página en un instante." + } + }, + "conflictFallback": "Ya estás en otra cuenta. Inicia sesión con un correo diferente para unirte a esta.", + "acceptError": "No se pudo aceptar la invitación", + "welcomeToast": "Bienvenido al equipo", + "serverUnreachable": "No se pudo conectar con el servidor", + "signOutError": "No se pudo cerrar sesión. Intenta actualizar la página." + }, + "Agents": { + "title": "Agentes de IA", + "subtitle": "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.", + "tabPlayground": "Playground", + "tabSetup": "Configuración", + "tabUsage": "Uso" + }, + "AiPlayground": { + "notConfiguredToast": "Aún no hay ningún agente configurado — completa la Configuración primero.", + "noReplyToast": "No se pudo obtener una respuesta.", + "unreachableToast": "No se pudo conectar con el agente.", + "headerTitle": "Playground", + "headerSubtitle": "— prueba las respuestas como si fueras un cliente", + "reset": "Restablecer", + "emptyTitle": "Envía un mensaje para ver cómo respondería tu agente.", + "emptyDescription": "Usa tu base de conocimiento y se comporta exactamente como el bot de respuesta automática — incluida la transferencia.", + "goToSetup": "¿Aún no lo configuraste? Ir a la Configuración", + "handoffLabel": "Transferiría a una persona aquí", + "thinking": "Pensando…", + "composerPlaceholder": "Escribe un mensaje de cliente…" + }, + "AiUsage": { + "loadError": "Error al cargar el uso", + "tokenUsage": "Uso de tokens", + "description": "Tokens gastados en tu clave de proveedor por los borradores y el bot de respuesta automática. Solo recuentos — aquí no se almacena ningún contenido de los mensajes.", + "lastDays": "Últimos {w} días", + "noUsage": "Aún no hay uso de IA en los últimos {n} días.", + "noUsageHint": "Esto se completa a medida que el asistente redacta borradores y responde automáticamente.", + "totalTokens": "Tokens totales", + "llmCalls": "Llamadas de LLM", + "autoReply": "Respuesta automática", + "drafts": "Borradores", + "tokensPerDay": "Tokens por día", + "byModel": "Por modelo", + "modelUsage": "{tokens} tok · {calls, plural, one {# llamada} other {# llamadas}}", + "truncatedNote": "Mostrando una ventana parcial — el uso es lo suficientemente alto como para que aquí solo se resuman los registros más recientes." + }, + "FlowsHeader": { + "backToFlows": "Volver a Flujos", + "flowNamePlaceholder": "Nombre del flujo", + "flowNameLabel": "Nombre del flujo", + "unsavedChanges": "Cambios sin guardar — pulsa Guardar para conservarlos", + "edited": "Editado", + "runs": "Ejecuciones", + "delete": "Eliminar", + "pause": "Pausar", + "activate": "Activar", + "fixIssuesBeforeActivating": "Corrige los problemas de abajo antes de activar", + "save": "Guardar", + "descriptionPlaceholder": "Agrega una breve descripción (interna — los clientes no la ven)", + "descriptionLabel": "Descripción del flujo", + "statusDraft": "Borrador", + "statusActive": "Activo", + "statusArchived": "Archivado" + }, + "InteractiveBuilder": { + "replyButtons": "Botones de respuesta", + "list": "Lista", + "body": "Cuerpo", + "bodyPlaceholder": "Lo que el cliente lee encima de las opciones", + "headerOptional": "Encabezado (opcional)", + "footerOptional": "Pie (opcional)", + "showReplyIds": "Mostrar IDs de respuesta (avanzado)", + "preview": "Vista previa", + "buttonsCount": "Botones ({count}/{max})", + "buttonLabel": "Etiqueta del botón", + "idPlaceholder": "id", + "addButton": "Agregar botón", + "listButtonLabel": "Etiqueta del botón de la lista", + "rowsCount": "Filas ({count}/{max})", + "sectionTitleOptional": "Título de la sección (opcional)", + "rowTitle": "Título de la fila", + "descriptionOptional": "Descripción (opcional)", + "addRow": "Agregar fila", + "addSection": "Agregar sección" + }, + "InteractivePreview": { + "messageBody": "Cuerpo del mensaje…", + "button": "Botón", + "menu": "Menú" + }, + "FlowTemplates": { + "welcome_menu": { + "name": "Menú de bienvenida", + "description": "Saluda a los clientes que escriben una palabra clave y dirígelos al agente correcto según si son nuevos o ya existentes." + }, + "faq_bot": { + "name": "Bot de preguntas frecuentes", + "description": "Responde preguntas comunes automáticamente. El cliente elige un tema de una lista; el bot responde y finaliza." + }, + "lead_capture": { + "name": "Captura de prospectos", + "description": "Saluda a los contactos nuevos, captura nombre + correo + empresa y luego deriva a ventas con las respuestas en la nota." + } + }, + "AutomationTemplates": { + "welcome_message": { + "name": "Mensaje de bienvenida", + "description": "Responder automáticamente con un saludo a los contactos por primera vez." + }, + "out_of_office": { + "name": "Fuera de oficina", + "description": "Responder automáticamente fuera del horario para que nadie quede sin atención." + }, + "lead_qualifier": { + "name": "Calificador de prospectos", + "description": "Hacer preguntas de calificación para filtrar los prospectos entrantes." + }, + "follow_up_reminder": { + "name": "Recordatorio de seguimiento", + "description": "Enviar un recordatorio si un contacto no responde en 24 horas." + } + }, + "Themes": { + "violet": { + "name": "Violeta", + "description": "La opción predeterminada — segura y un poco divertida." + }, + "emerald": { + "name": "Esmeralda", + "description": "Con aire de crecimiento, evoca la mensajería sin copiar el verde de WhatsApp." + }, + "cobalt": { + "name": "Cobalto", + "description": "Azul limpio de SaaS B2B — sereno y con estilo de producto." + }, + "amber": { + "name": "Ámbar", + "description": "Cálido y amigable — ideal para equipos de pymes." + }, + "rose": { + "name": "Rosa", + "description": "Audaz y moderno — D2C, economía de creadores, lifestyle." + } + }, + "Common": { + "loading": "Cargando..." + } +} diff --git a/messages/pt.json b/messages/pt.json new file mode 100644 index 0000000000..c1ec7bf014 --- /dev/null +++ b/messages/pt.json @@ -0,0 +1,2011 @@ +{ + "LoginPage": { + "titleAccept": "Entre para aceitar", + "titleWelcome": "Bem-vindo de volta", + "descAccept": "Entre e levaremos você até o convite.", + "descWelcome": "Entre na sua conta", + "emailLabel": "E-mail", + "emailPlaceholder": "voce@exemplo.com", + "passwordLabel": "Senha", + "forgotPassword": "Esqueceu a senha?", + "passwordPlaceholder": "Digite sua senha", + "signingIn": "Entrando...", + "signIn": "Entrar", + "noAccount": "Não tem uma conta?", + "createAccount": "Criar conta" + }, + "Sidebar": { + "title": "Modelo de CRM para WhatsApp", + "dashboard": "Painel", + "inbox": "Caixa de entrada", + "notifications": "Notificações", + "contacts": "Contatos", + "pipelines": "Funis", + "broadcasts": "Transmissões", + "automations": "Automações", + "flows": "Fluxos", + "aiAgents": "Agentes de IA", + "settings": "Configurações", + "beta": "Beta", + "unreadConversations": "{count} {count, plural, =1 {conversa não lida} other {conversas não lidas}}", + "unreadNotifications": "{count} {count, plural, =1 {notificação não lida} other {notificações não lidas}}", + "roleOwner": "Proprietário", + "roleAdmin": "Administrador", + "roleAgent": "Agente", + "roleViewer": "Visualizador", + "closeMenu": "Fechar menu", + "defaultUser": "Usuário", + "defaultAvatar": "Avatar", + "menuProfile": "Perfil", + "menuSettings": "Configurações", + "menuSignOut": "Sair", + "primaryNav": "Principal" + }, + "Header": { + "dashboard": "Painel", + "inbox": "Caixa de entrada", + "notifications": "Notificações", + "contacts": "Contatos", + "pipelines": "Funis", + "broadcasts": "Transmissões", + "automations": "Automações", + "settings": "Configurações", + "openMenu": "Abrir menu", + "openAccountMenu": "Abrir menu da conta", + "defaultUser": "Usuário", + "defaultAvatar": "Avatar", + "menuProfile": "Perfil", + "menuSettings": "Configurações", + "menuSignOut": "Sair" + }, + "ModeToggle": { + "switchMode": "Mudar para o modo {mode}" + }, + "Dashboard": { + "page": { + "title": "Painel", + "description": "Análises em tempo real de conversas, contatos, negócios, transmissões e automações.", + "activeConversations": "Conversas Ativas", + "newContactsToday": "Novos Contatos Hoje", + "openDealsValue": "Valor de Negócios Abertos", + "messagesSentToday": "Mensagens Enviadas Hoje", + "newTodayVsYesterday": "novos hoje vs. ontem", + "vsYesterday": "vs. ontem", + "openDeals": "{count} {count, plural, =1 {negócio aberto} other {negócios abertos}}", + "noChange": "Sem alteração {suffix}" + }, + "quickActions": { + "newContact": "Novo Contato", + "newDeal": "Novo Negócio", + "newBroadcast": "Nova Transmissão", + "newAutomation": "Nova Automação" + }, + "activityFeed": { + "title": "Atividade Recente", + "viewAll": "Ver tudo →", + "noActivity": "Nenhuma atividade ainda", + "noActivityHint": "Atividades de mensagens, negócios, transmissões e automações aparecerão aqui.", + "showingOf": "Exibindo {visible} de {totalLoaded}{plus}", + "show": "Exibir", + "timeS": "há {sec}s", + "timeM": "há {min}min", + "timeH": "há {hr}h", + "timeD": "há {day}d" + }, + "conversationsChart": { + "title": "Conversas ao Longo do Tempo", + "description": "Volume diário de mensagens por direção", + "days": "{count} dias", + "noActivity": "Nenhuma atividade de mensagens neste período", + "noActivityHint": "Envie ou receba mensagens para começar a preencher este gráfico.", + "incoming": "Recebidas", + "outgoing": "Enviadas", + "tooltipIncoming": "{count} recebidas", + "tooltipOutgoing": "{count} enviadas", + "ariaLabel": "Conversas por dia" + }, + "pipelineDonut": { + "title": "Valor do Funil", + "description": "Negócios abertos por etapa", + "noOpenDeals": "Nenhum negócio aberto ainda", + "noOpenDealsHint": "Crie negócios em Funis para ver a distribuição por etapa aqui.", + "dealCount": "{count} {count, plural, =1 {negócio} other {negócios}}", + "total": "Total", + "ariaLabel": "Valor do funil por etapa" + }, + "responseTimeChart": { + "title": "Tempo Médio da Primeira Resposta", + "description": "Minutos para responder à primeira mensagem sem resposta do cliente, por dia da semana", + "target": "meta {minutes}min", + "thisWeek": "Esta semana:", + "lastWeek": "Semana passada:", + "noReplies": "Nenhuma resposta registrada ainda", + "noRepliesHint": "Este gráfico é preenchido conforme você responde às mensagens dos clientes." + }, + "emptyState": { + "title": "Dados insuficientes ainda" + } + }, + "Inbox": { + "page": { + "whatsappNotConnected": "O WhatsApp® não está conectado. Vá até Configurações para conectar sua conta." + }, + "conversationList": { + "searchPlaceholder": "Buscar conversas...", + "filterAll": "Todas", + "filterUnread": "Não lidas", + "filterOpen": "Abertas", + "filterPending": "Pendentes", + "filterClosed": "Fechadas", + "tags": "Etiquetas", + "company": "Empresa", + "allCompanies": "Todas as empresas", + "clearAll": "Limpar tudo", + "noConversations": "Nenhuma conversa encontrada", + "noMessagesYet": "Nenhuma mensagem ainda", + "unknown": "Desconhecido" + }, + "messageThread": { + "backToConversations": "Voltar às conversas", + "hideContactPanel": "Ocultar painel do contato", + "showContactPanel": "Mostrar painel do contato", + "hideContact": "Ocultar contato", + "showContact": "Mostrar contato", + "refresh": "Atualizar", + "refreshConversation": "Atualizar conversa", + "status": "Status", + "statusOpen": "Aberta", + "statusPending": "Pendente", + "statusClosed": "Fechada", + "assign": "Atribuir", + "assigned": "Atribuída", + "unassign": "Remover atribuição", + "noTeammates": "Nenhum colega disponível", + "me": " (eu)", + "noMessagesYet": "Nenhuma mensagem ainda", + "sendTemplateHint": "Envie um modelo para iniciar a conversa", + "selectConversation": "Selecione uma conversa", + "selectConversationHint": "Escolha uma conversa à esquerda para começar a enviar mensagens", + "today": "Hoje", + "yesterday": "Ontem", + "toastSendFailed": "Falha ao enviar: {reason}", + "toastSendTemplateFailed": "Falha ao enviar modelo: {reason}", + "toastWaitForSend": "Aguarde a mensagem terminar de enviar", + "toastReactionFailed": "Falha na reação: {reason}", + "toastAssignmentFailed": "Falha ao atualizar a atribuição" + }, + "sessionTimer": { + "expired": "Expirada", + "xhRemaining": "{hours}h restantes", + "xmRemaining": "{minutes}min restantes", + "noCustomerMessages": "Nenhuma mensagem do cliente" + }, + "composer": { + "draftHint": "Toque no ✨ para redigir uma resposta com IA — você pode editá-la antes de enviar", + "sessionExpiredHint": "Sessão de 24 horas expirada. Use um modelo para reengajar.", + "templates": "Modelos", + "readOnlyPlaceholder": "Somente leitura — visualizadores podem navegar, mas não responder", + "sessionExpiredPlaceholder": "Sessão expirada - use um modelo", + "typeMessagePlaceholder": "Digite uma mensagem... (Shift+Enter para nova linha)", + "readOnlyTitle": "Somente leitura — seu perfil não pode enviar mensagens", + "recording": "Gravando… {current} / {max}", + "cancel": "Cancelar", + "stopAndAttach": "Parar e anexar", + "attachMedia": "Anexar mídia", + "photo": "Foto", + "video": "Vídeo", + "document": "Documento", + "voiceNote": "Mensagem de voz", + "sendTemplate": "Enviar modelo", + "draftWithAI": "Redigir uma resposta com IA", + "addCaption": "Adicionar uma legenda…", + "removeAttachment": "Remover anexo", + "moreActions": "Mais", + "interactiveMessage": "Mensagem interativa", + "quickReplies": "Respostas rápidas", + "quickRepliesEmpty": "Nenhuma resposta rápida ainda. Adicione algumas em Configurações → Respostas rápidas.", + "saveAsQuickReply": "Salvar como resposta rápida", + "send": "Enviar", + "quickReplyNamePrompt": "Dê um nome a esta resposta rápida:", + "quickReplySaved": "Salva como resposta rápida.", + "quickReplySaveError": "Não foi possível salvar a resposta rápida.", + "aiNotConfigured": "A IA ainda não está configurada — ative-a em Configurações → Assistente de IA.", + "draftFailed": "Não foi possível rascunhar uma resposta.", + "assistantNoReply": "O assistente não retornou uma resposta.", + "aiUnreachable": "Não foi possível conectar ao assistente de IA.", + "recordingTooLong": "A gravação é muito longa (mais de 16 MB).", + "voiceUnsupported": "A gravação de voz não é compatível com este navegador.", + "micDenied": "Acesso ao microfone negado ou indisponível." + }, + "bubble": { + "photo": "Foto", + "video": "Vídeo", + "audio": "Áudio", + "unavailable": "{label} indisponível", + "document": "Documento", + "locationShared": "Localização compartilhada", + "template": "Modelo", + "buttonReply": "Resposta de botão", + "interactiveReply": "[Resposta interativa]", + "unsupported": "[Tipo de mensagem não suportado]", + "aiBadge": "IA", + "aiBadgeTitle": "Enviada automaticamente pelo assistente de IA" + }, + "actions": { + "reply": "Responder", + "copyText": "Copiar texto", + "delete": "Excluir", + "react": "Reagir", + "reactWith": "Reagir com {emoji}", + "nothingToCopy": "Nada para copiar", + "copied": "Copiado", + "copyFailed": "Falha ao copiar" + }, + "replyQuote": { + "photo": "[Foto]", + "video": "[Vídeo]", + "document": "[Documento]", + "audio": "[Áudio]", + "voiceMessage": "[Mensagem de voz]", + "location": "[Localização]", + "template": "[Modelo]", + "message": "[Mensagem]", + "cancelReply": "Cancelar resposta" + }, + "templatePicker": { + "sendTemplate": "Enviar modelo", + "fillPlaceholders": "Preencha os campos para renderizar este modelo. A Meta exige que todas as variáveis sejam definidas.", + "pickTemplate": "Escolha um modelo do WhatsApp aprovado para enviar a este contato.", + "noApprovedTemplates": "Nenhum modelo aprovado", + "noApprovedTemplatesHint": "Aprove um modelo no Meta WhatsApp Manager e sincronize-o em Configurações → Modelos.", + "preview": "Prévia", + "headerValuePlaceholder": "Valor para a variável do cabeçalho", + "bodyValuePlaceholder": "Valor para {val}", + "urlSuffixValuePlaceholder": "Valor do sufixo da URL", + "finalUrl": "URL final: {url}", + "back": "Voltar", + "cancel": "Cancelar", + "send": "Enviar modelo" + }, + "sidebar": { + "contactInfo": "Informações do contato", + "tags": "Etiquetas", + "notes": "Notas", + "deals": "Negócios", + "noTags": "Nenhuma etiqueta", + "noDeals": "Nenhum negócio", + "addNotePlaceholder": "Adicionar uma nota..." + }, + "aiBanner": { + "pausedTitle": "O assistente de IA está pausado aqui", + "activeText": "O assistente de IA está respondendo automaticamente", + "takeOver": "Assumir", + "resume": "Retomar IA", + "tookOver": "Você assumiu esta conversa.", + "resumed": "IA retomada.", + "updateError": "Não foi possível atualizar o assistente de IA.", + "networkError": "Não foi possível conectar ao servidor." + } + }, + "Contacts": { + "page": { + "title": "Contatos", + "subtitle": "Gerencie sua lista de contatos. {count} contatos no total.", + "subtitleZero": "Gerencie sua lista de contatos.", + "customFieldsBtn": "Campos personalizados", + "importBtn": "Importar", + "addContactBtn": "Adicionar Contato", + "searchPlaceholder": "Buscar por nome, telefone ou e-mail...", + "filterByTags": "Filtrar por etiquetas", + "clearAll": "Limpar tudo", + "noTagsYet": "Nenhuma etiqueta ainda.", + "selectedCount": "{count} selecionados", + "clearSelection": "Limpar", + "deleteSelected": "Excluir selecionados", + "tableColumns": { + "name": "Nome", + "phone": "Telefone", + "email": "E-mail", + "company": "Empresa", + "tags": "Etiquetas", + "createdAt": "Criado em" + }, + "loading": "Carregando contatos...", + "noContactsMatch": "Nenhum contato corresponde aos seus filtros.", + "noContactsYet": "Nenhum contato ainda.", + "addFirstContact": "Adicione seu primeiro contato", + "unnamed": "Sem nome", + "editAction": "Editar", + "deleteAction": "Excluir", + "showingPagination": "Exibindo {start}-{end} de {total}", + "pageCount": "Página {page} de {total}", + "deleteContactTitle": "Excluir Contato", + "deleteContactDesc": "Tem certeza de que deseja excluir {name}? Esta ação não pode ser desfeita.", + "cancel": "Cancelar", + "deleteBtn": "Excluir", + "deleteBulkTitle": "Excluir Contatos", + "deleteBulkDesc": "Tem certeza de que deseja excluir {count} contatos selecionados? Esta ação não pode ser desfeita.", + "toastFailedLoad": "Falha ao carregar contatos", + "toastFailedDelete": "Falha ao excluir contato", + "toastDeleted": "Contato excluído", + "toastBulkFailedDelete": "Falha ao excluir contatos", + "toastBulkDeleted": "{count} contatos excluídos", + "selectAllAria": "Selecionar todos os contatos desta página" + }, + "form": { + "addTitle": "Adicionar Contato", + "addDesc": "Crie um novo contato. O nome é obrigatório.", + "editTitle": "Editar Contato", + "editDesc": "Atualize os detalhes e etiquetas do contato.", + "nameLabel": "Nome", + "namePlaceholder": "Maria Silva", + "phoneLabel": "Número de Telefone", + "phonePlaceholder": "+5511999999999", + "phoneHint": "Inclua o código do país, ex.: +55 para o Brasil", + "emailLabel": "E-mail", + "emailPlaceholder": "maria@exemplo.com", + "companyLabel": "Empresa", + "companyPlaceholder": "Acme Ltda.", + "tagsLabel": "Etiquetas", + "tagsPlaceholder": "Selecionar etiquetas...", + "noTagsFound": "Nenhuma etiqueta encontrada.", + "createTagAction": "Criar etiqueta \"{value}\"", + "cancel": "Cancelar", + "save": "Salvar Contato", + "update": "Atualizar", + "create": "Criar", + "toastSuccessAdd": "Contato criado com sucesso", + "toastSuccessEdit": "Contato atualizado com sucesso", + "toastError": "Falha ao salvar contato", + "toastConflict": "Já existe um contato com este número de telefone", + "viewExisting": "Ver {name}", + "dupExact": "Já existe um contato com este número de telefone.", + "dupSimilar": "Já existe um contato com um número muito parecido.", + "loadingTags": "Carregando etiquetas...", + "noTagsAvailable": "Nenhuma etiqueta disponível. Crie etiquetas em Configurações.", + "phoneRequired": "O número de telefone é obrigatório" + }, + "detailView": { + "tabs": { + "details": "Detalhes", + "activity": "Atividade", + "notes": "Notas", + "custom": "Campos Personalizados", + "deals": "Negócios", + "tags": "Etiquetas", + "noTags": "Nenhuma etiqueta", + "noDeals": "Nenhum negócio", + "addNotePlaceholder": "Adicionar uma nota..." + }, + "actions": { + "edit": "Editar", + "message": "Mensagem", + "delete": "Excluir" + }, + "contactInfo": "Informações de Contato", + "unnamed": "Desconhecido", + "contactDetailsDesc": "Detalhes do contato", + "phone": "Telefone", + "email": "E-mail", + "company": "Empresa", + "createdAt": "Criado Em", + "customFields": "Campos Personalizados", + "noCustomFields": "Nenhum campo personalizado definido. Crie-os em Configurações.", + "manageFields": "Gerenciar campos", + "enterCustomField": "Insira {name}...", + "activityTab": { + "title": "Histórico de Atividades", + "noActivity": "Nenhuma atividade ainda." + }, + "notesTab": { + "title": "Notas Internas", + "placeholder": "Escreva uma nota...", + "save": "Adicionar Nota", + "noNotes": "Nenhuma nota ainda." + }, + "dealsTab": { + "noDeals": "Nenhum negócio ainda" + }, + "tagsTab": { + "clickTagDesc": "Clique em uma etiqueta para adicioná-la ou removê-la deste contato.", + "noTagsAvailable": "Nenhuma etiqueta disponível. Crie etiquetas em Configurações." + }, + "sendTemplateBtn": "Enviar modelo", + "saveChangesBtn": "Salvar Alterações", + "saveCustomFieldsBtn": "Salvar Campos Personalizados", + "toastPhoneRequired": "O número de telefone é obrigatório", + "toastUpdateFailed": "Falha ao atualizar contato", + "toastUpdated": "Contato atualizado", + "toastNotAuthenticated": "Não autenticado", + "toastNoteAddFailed": "Falha ao adicionar nota", + "toastNoteAdded": "Nota adicionada", + "toastNoteDeleteFailed": "Falha ao excluir nota", + "toastNoteDeleted": "Nota excluída", + "toastCustomFieldsSaved": "Campos personalizados salvos", + "toastCustomFieldsFailed": "Falha ao salvar campos personalizados", + "toastTemplateSent": "Modelo \"{name}\" enviado", + "toastTemplateFailed": "Falha ao enviar modelo: {reason}" + }, + "importModal": { + "title": "Importar Contatos", + "desc": "Envie um CSV com uma coluna phone obrigatória. Opcional: name, email, company, tags (separados por vírgula; use aspas em células com várias etiquetas).", + "uploadDropzone": "Clique para escolher um arquivo CSV", + "uploadHint": ".csv até o limite do seu navegador", + "rowsReady": "{count} linha pronta", + "rowsReady_plural": "{count} linhas prontas", + "preview": "Prévia · primeiras {count}", + "previewTags": "{tags} etiqueta · {contacts} contato", + "previewTags_plural": "{tags} etiquetas · {contacts} contatos", + "moreRows": "+ {count} linha a mais não exibida", + "moreRows_plural": "+ {count} linhas a mais não exibidas", + "importComplete": "Importação concluída", + "resultImported": "{count} importados", + "resultTags": "{count} etiqueta atribuída", + "resultTags_plural": "{count} etiquetas atribuídas", + "resultSkipped": "{count} ignorados", + "resultFailed": "{count} com falha", + "willBeCreated": "{name} (será criado na importação)", + "columns": { + "phone": "Telefone", + "name": "Nome", + "email": "E-mail", + "company": "Empresa", + "tags": "Etiquetas" + }, + "cancel": "Cancelar", + "close": "Fechar", + "importBtn": "Importar {count} contato", + "importBtn_plural": "Importar {count} contatos", + "toastNoValidRows": "Nenhuma linha válida encontrada. Verifique se o CSV tem uma coluna com cabeçalho \"phone\".", + "toastTagsWarning": "Contatos importados, mas algumas atribuições de etiqueta falharam.", + "toastImported": "{count} contato importado", + "toastImported_plural": "{count} contatos importados", + "toastTagsAssigned": "{count} atribuição de etiqueta aplicada", + "toastTagsAssigned_plural": "{count} atribuições de etiqueta aplicadas", + "toastTagsSkipped": "Etiquetas desconhecidas ignoradas (crie-as em Configurações primeiro): {sample}{more}", + "toastSkipped": "{count} duplicado ignorado", + "toastSkipped_plural": "{count} duplicados ignorados", + "toastFailed": "{count} contato falhou ao importar", + "toastFailed_plural": "{count} contatos falharam ao importar", + "toastError": "Falha na importação" + }, + "customFields": { + "title": "Campos personalizados", + "desc": "Defina campos adicionais de contato (ex.: CEP, origem do lead). Eles aparecem em todos os contatos e na ação de automação “Atualizar Campo do Contato”.", + "addField": "Adicionar", + "fieldName": "Nome do novo campo…", + "loading": "Carregando…", + "empty": "Nenhum campo personalizado ainda.", + "renameAria": "Renomear {name}", + "deleteTitle": "Excluir campo", + "deleteConfirm": "Excluir \"{name}\"? Isso também remove o valor armazenado em todos os contatos. Isso não pode ser desfeito.", + "toastNoAccount": "Seu perfil não está vinculado a uma conta.", + "toastDuplicate": "Já existe um campo chamado \"{name}\".", + "toastCreateFailed": "Não foi possível criar o campo. Você pode não ter permissão.", + "toastCreated": "\"{name}\" criado.", + "toastRenameFailed": "Não foi possível renomear o campo.", + "toastDeleteFailed": "Não foi possível excluir o campo.", + "toastDeleted": "\"{name}\" excluído." + } + }, + "Pipelines": { + "page": { + "selectPipeline": "Selecionar Funil", + "noPipelinesYet": "Nenhum funil ainda", + "managePipelines": "Gerenciar Funis", + "addPipeline": "Adicionar Funil", + "addDeal": "Adicionar Negócio", + "createToStartTracking": "Crie um funil para começar a acompanhar negócios", + "createPipeline": "Criar Funil", + "newPipeline": "Novo Funil", + "pipelineName": "Nome do Funil", + "pipelineNamePlaceholder": "ex.: Vendas Corporativas", + "defaultStagesDesc": "As etapas padrão (Novo Lead → Ganho) serão criadas automaticamente.", + "cancel": "Cancelar", + "creating": "Criando...", + "createPipelineBtn": "Criar Funil", + "toastFailedLoadPipelines": "Falha ao carregar funis", + "toastFailedMoveDeal": "Falha ao mover negócio", + "toastFailedCreatePipeline": "Falha ao criar funil", + "toastPipelineCreated": "Funil criado", + "toastNotLinkedToAccount": "Seu perfil não está vinculado a uma conta." + }, + "board": { + "dropDealHere": "Solte um negócio aqui", + "addDeal": "Adicionar Negócio" + }, + "card": { + "noContact": "Sem contato", + "won": "Ganho", + "lost": "Perdido" + }, + "form": { + "editDeal": "Editar Negócio", + "newDeal": "Novo Negócio", + "title": "Título", + "titlePlaceholder": "Título do negócio", + "contact": "Contato", + "selectContact": "Selecione um contato", + "linkToConversation": "Vincular à Conversa", + "value": "Valor", + "currency": "Moeda", + "expectedCloseDate": "Data Prevista de Fechamento", + "stage": "Etapa", + "assignedTo": "Atribuído a", + "unassigned": "Não atribuído", + "notes": "Notas", + "notesPlaceholder": "Adicionar notas...", + "status": "Status", + "markAsWon": "Marcar como Ganho", + "markAsLost": "Marcar como Perdido", + "reopenDeal": "Reabrir negócio", + "cancel": "Cancelar", + "saving": "Salvando...", + "saveChanges": "Salvar Alterações", + "createDeal": "Criar Negócio", + "deletePrompt": "Excluir este negócio?", + "confirm": "Confirmar", + "deleting": "Excluindo...", + "deleteDeal": "Excluir Negócio", + "toastRequired": "Título, contato e etapa são obrigatórios", + "toastFailedSave": "Falha ao salvar negócio", + "toastNotSignedIn": "Não conectado", + "toastNotLinked": "Seu perfil não está vinculado a uma conta.", + "toastFailedCreate": "Falha ao criar negócio", + "toastUpdated": "Negócio atualizado", + "toastCreated": "Negócio criado", + "toastFailedStatus": "Falha ao atualizar o status do negócio", + "toastMarkedWon": "Marcado como ganho", + "toastMarkedLost": "Marcado como perdido", + "toastReopened": "Negócio reaberto", + "toastFailedDelete": "Falha ao excluir negócio", + "toastDeleted": "Negócio excluído", + "valuePlaceholder": "0" + }, + "settings": { + "managePipeline": "Gerenciar Funil", + "deletePipeline": "Excluir Funil", + "deletePipelineDesc": "Isso arquivará todos os negócios deste funil. Isso não pode ser desfeito.", + "cancel": "Cancelar", + "deleting": "Excluindo...", + "deletePipelineBtn": "Excluir Funil", + "pipelineName": "Nome do Funil", + "stages": "Etapas", + "newStageNamePlaceholder": "Nome da nova etapa", + "add": "Adicionar", + "createNewPipeline": "Criar um novo funil", + "saving": "Salvando...", + "saveChanges": "Salvar Alterações", + "dragToReorder": "Arraste para reordenar", + "changeColor": "Alterar cor", + "toastFailedSave": "Falha ao salvar funil", + "toastSaved": "Funil salvo", + "toastFailedAddStage": "Falha ao adicionar etapa", + "toastMoveOrDeleteDeals": "Mova ou exclua os negócios desta etapa primeiro", + "toastFailedDeleteStage": "Falha ao excluir etapa", + "toastFailedDeletePipeline": "Falha ao excluir funil", + "toastDeleted": "Funil excluído" + }, + "analytics": { + "totalDeals": "Total de Negócios", + "totalDealsTooltip": "Contagem de todos os negócios deste funil que não estão marcados como Perdido. Negócios Ganhos também são incluídos.", + "pipelineValue": "Valor do Funil", + "pipelineValueTooltip": "Soma dos valores de todos os negócios deste funil, excluindo os marcados como Perdido.", + "avgDealSize": "Valor Médio do Negócio", + "avgDealSizeTooltip": "Valor do Funil dividido pelo Total de Negócios — o valor médio de um único negócio não perdido.", + "weightedValue": "Valor Ponderado", + "weightedValueTooltip": "Receita esperada: o valor de cada negócio aberto × a probabilidade da sua etapa. A primeira etapa ≈ 10%, as etapas avançam até 90%, Ganho = 100%. Negócios perdidos são excluídos.", + "wonThisMonth": "Ganhos Este Mês", + "wonThisMonthTooltip": "Negócios marcados como Ganho desde o primeiro dia do mês atual.", + "lostThisMonth": "Perdidos Este Mês", + "lostThisMonthTooltip": "Negócios marcados como Perdido desde o primeiro dia do mês atual.", + "howCalculated": "Como {label} é calculado" + } + }, + "Broadcasts": { + "page": { + "title": "Transmissões", + "subtitle": "Envie mensagens em massa aos seus contatos usando modelos aprovados.", + "newBroadcast": "Nova Transmissão", + "noBroadcastsYet": "Nenhuma transmissão ainda", + "createFirst": "Crie sua primeira transmissão para alcançar seus contatos em escala.", + "table": { + "name": "Nome", + "template": "Modelo", + "recipients": "Destinatários", + "delivery": "Entrega", + "read": "Lidas", + "status": "Status", + "date": "Data" + }, + "errorLoad": "Falha ao carregar transmissões", + "retry": "Tentar novamente", + "inProgressAria": "Transmissão em andamento" + }, + "status": { + "draft": "Rascunho", + "scheduled": "Agendada", + "sending": "Enviando", + "sent": "Enviada", + "failed": "Falhou", + "pending": "Pendente", + "delivered": "Entregue", + "read": "Lida", + "replied": "Respondida", + "unknown": "Desconhecido" + }, + "detail": { + "backToBroadcasts": "Voltar às Transmissões", + "notFound": "Transmissão não encontrada", + "template": "Modelo: {name}", + "createdAt": "Criada em {date}", + "deletePrompt": "Excluir esta transmissão?", + "cancel": "Cancelar", + "confirm": "Confirmar", + "deleting": "Excluindo…", + "delete": "Excluir", + "cannotDeleteSending": "Não é possível excluir enquanto uma transmissão está em envio ativo", + "deleteHover": "Excluir esta transmissão", + "funnel": "Funil", + "recipientsHeader": "Destinatários ({filtered} de {total})", + "recipientsHeaderAll": "Destinatários ({total})", + "allStatuses": "Todos os status", + "exportCsv": "Exportar CSV", + "noRecipients": "Nenhum destinatário encontrado.", + "noRecipientsFilter": "Nenhum destinatário corresponde a este filtro.", + "table": { + "contact": "Contato", + "phone": "Telefone", + "status": "Status", + "sent": "Enviada", + "delivered": "Entregue", + "read": "Lida", + "error": "Erro" + }, + "stats": { + "totalRecipients": "Total de Destinatários", + "sent": "Enviadas", + "delivered": "Entregues", + "read": "Lidas", + "replied": "Respondidas", + "failed": "Com falha" + }, + "toastFailedDelete": "Falha ao excluir: {error}", + "toastDeleted": "Transmissão excluída" + }, + "new": { + "title": "Nova Transmissão", + "subtitle": "Crie e envie uma mensagem de transmissão aos seus contatos.", + "steps": { + "template": "Modelo", + "audience": "Público", + "personalize": "Personalizar", + "send": "Enviar" + }, + "toastGiveName": "Dê um nome à transmissão antes de salvar um rascunho.", + "toastNotSignedIn": "Não conectado.", + "toastNotLinked": "Seu perfil não está vinculado a uma conta.", + "toastFailedDraft": "Falha ao salvar rascunho: {error}", + "toastDraftSaved": "Rascunho salvo" + }, + "wizard": { + "next": "Avançar", + "back": "Voltar", + "cancel": "Cancelar", + "chooseTemplate": { + "title": "Escolha um Modelo", + "subtitle": "Selecione um modelo de mensagem aprovado para sua transmissão.", + "noTemplates": "Nenhum modelo disponível.", + "createFirst": "Crie um modelo em Configurações primeiro.", + "errorLoad": "Falha ao carregar modelos" + }, + "selectAudience": { + "title": "Selecionar Público", + "subtitle": "Escolha quem receberá esta transmissão.", + "method": { + "all": "Todos os Contatos", + "tags": "Filtrar por Etiquetas", + "customField": "Campo Personalizado", + "csv": "Enviar CSV" + }, + "allDesc": "Enviar para todos os {count} contatos do seu banco de dados.", + "allDescLoading": "Enviar para todos os contatos do seu banco de dados.", + "tagDesc": "Enviar para contatos que correspondam a etiquetas específicas.", + "customFieldDesc": "Enviar para contatos que correspondam a uma regra de campo personalizado.", + "csvDesc": "Envie um CSV com números de telefone (e nomes opcionais).", + "selectTags": "Selecionar Etiquetas", + "searchTags": "Buscar etiquetas...", + "noTagsFound": "Nenhuma etiqueta encontrada.", + "excludeTags": "Excluir Etiquetas (Opcional)", + "searchExcludeTags": "Buscar etiquetas para excluir...", + "field": "Campo", + "selectField": "Selecione um campo...", + "operator": "Operador", + "operatorIs": "É", + "operatorIsNot": "Não é", + "operatorContains": "Contém", + "value": "Valor", + "valuePlaceholder": "Valor a corresponder", + "uploadCsv": "Enviar Arquivo CSV", + "csvFormatDesc": "Deve conter uma coluna 'phone' (formato E.164). A coluna 'name' é opcional.", + "csvContactsFound": "{count} contato(s) válido(s) encontrado(s) no CSV.", + "errorLoadTags": "Falha ao carregar etiquetas", + "errorLoadFields": "Falha ao carregar campos personalizados", + "errorCsvParse": "Não foi possível processar o arquivo CSV.", + "errorCsvMissingPhone": "O CSV deve ter uma coluna 'phone'.", + "summaryTitle": "Resumo do público", + "calculating": "Calculando…", + "estimatedRecipients": "destinatários estimados", + "selectTypeHint": "Selecione um tipo de público para ver a estimativa." + }, + "personalize": { + "title": "Personalizar Variáveis", + "subtitle": "Mapeie as variáveis do seu modelo para campos de contato ou forneça valores estáticos.", + "headerImage": "Imagem do Cabeçalho", + "headerImageDesc": "Este modelo requer uma imagem de cabeçalho.", + "imageUrl": "URL da Imagem", + "imageUrlPlaceholder": "https://exemplo.com/imagem.jpg", + "variables": "Variáveis", + "varDesc": "Defina valores para {{name}}", + "type": "Tipo", + "typeStatic": "Valor Estático", + "typeContact": "Campo de Contato", + "typeCustom": "Campo Personalizado", + "staticValue": "Valor Estático", + "contactField": "Campo de Contato", + "selectContactField": "Selecione o campo...", + "customField": "Campo Personalizado", + "selectCustomField": "Selecione o campo personalizado...", + "preview": "Prévia", + "previewSample": "Exibindo prévia com dados de exemplo.", + "noPreview": "Prévia não disponível.", + "fieldMap": { + "name": "Nome", + "phone": "Telefone", + "email": "E-mail" + }, + "enterValuePlaceholder": "Insira o valor..." + }, + "scheduleSend": { + "title": "Revisar e Enviar", + "subtitle": "Dê um nome à sua transmissão e confirme o envio.", + "broadcastName": "Nome da Transmissão", + "broadcastNamePlaceholder": "ex.: Promoção de Verão", + "summary": "Resumo", + "template": "Modelo:", + "audience": "Público:", + "audienceAll": "Todos os contatos", + "audienceTags": "Filtrado por etiquetas", + "audienceField": "Filtrado por campo personalizado", + "audienceCsv": "Envio de CSV", + "variables": "Variáveis:", + "variablesMapped": "{count} mapeadas", + "saveDraft": "Salvar como Rascunho", + "sendNow": "Enviar Transmissão", + "sending": "Enviando...", + "preparing": "Preparando...", + "progressDesc": "Processando {current} de {total} destinatários...", + "toastFailedPreview": "Falha ao carregar dados da prévia", + "estimatedReach": "Alcance estimado", + "language": "Idioma", + "confirmTitle": "Confirmar transmissão" + } + } + }, + "Automations": { + "list": { + "title": "Automações", + "subtitle": "Crie fluxos de trabalho que reagem automaticamente a eventos do WhatsApp®.", + "create": "Criar Automação", + "retry": "Tentar novamente", + "templatesTitle": "Modelos de início rápido", + "emptyTitle": "Nenhuma automação ainda", + "emptyDesc": "Escolha um modelo acima ou crie uma do zero.", + "runs": "{count} execução", + "runsPlural": "{count} execuções", + "lastRun": "última {time}", + "activate": "Ativar", + "deactivate": "Desativar", + "edit": "Editar", + "duplicate": "Duplicar", + "viewLogs": "Ver Registros", + "delete": "Excluir", + "cancel": "Cancelar", + "deleteTitle": "Excluir automação", + "deleteDesc": "Isso remove permanentemente {name} e seu histórico de execução. Isso não pode ser desfeito.", + "toasts": { + "activated": "Automação ativada", + "paused": "Automação pausada", + "duplicated": "Automação duplicada", + "deleted": "Automação excluída", + "updateError": "Falha ao atualizar", + "duplicateError": "Falha ao duplicar", + "deleteError": "Falha ao excluir" + }, + "activeAria": "ativa", + "openMenu": "Abrir menu" + }, + "edit": { + "loading": "Carregando...", + "loadError": "Falha ao carregar ({status})", + "back": "Voltar às Automações" + }, + "logs": { + "title": "Registros de execução", + "loadError": "Falha ao carregar registros", + "emptyTitle": "Nenhuma execução ainda", + "emptyDesc": "Acione esta automação para ver as execuções aqui.", + "unknownContact": "Contato desconhecido", + "step": "{count} etapa", + "stepPlural": "{count} etapas", + "noSteps": "Nenhuma etapa registrada.", + "status": { + "success": "sucesso", + "partial": "parcial", + "failed": "falhou" + }, + "back": "Voltar", + "backAria": "Voltar" + }, + "builder": { + "steps": { + "send_message": "Enviar Mensagem", + "send_buttons": "Enviar Botões", + "send_list": "Enviar Lista", + "send_template": "Enviar Modelo", + "add_tag": "Adicionar Etiqueta", + "remove_tag": "Remover Etiqueta", + "assign_conversation": "Atribuir Conversa", + "update_contact_field": "Atualizar Campo do Contato", + "create_deal": "Criar Negócio", + "wait": "Aguardar", + "condition": "Condição (Se/Senão)", + "send_webhook": "Enviar Webhook", + "close_conversation": "Fechar Conversa" + }, + "triggers": { + "new_message_received": { + "label": "Nova Mensagem Recebida", + "hint": "Qualquer mensagem recebida" + }, + "first_inbound_message": { + "label": "Primeira Mensagem do Contato", + "hint": "A primeira vez que este contato lhe envia uma mensagem (também funciona para contatos adicionados manualmente)" + }, + "keyword_match": { + "label": "Correspondência de Palavra-chave", + "hint": "A mensagem contém palavra(s)-chave específica(s)" + }, + "interactive_reply": { + "label": "Resposta de Botão / Lista", + "hint": "O cliente toca em um botão ou linha de lista cujo id corresponde — encadeie menus entre automações" + }, + "new_contact_created": { + "label": "Novo Contato Criado", + "hint": "Quando um contato é criado automaticamente a partir de uma mensagem recebida" + }, + "conversation_assigned": { + "label": "Conversa Atribuída", + "hint": "Quando é atribuída a um agente" + }, + "tag_added": { + "label": "Etiqueta Adicionada", + "hint": "Quando uma etiqueta é adicionada a um contato" + }, + "time_based": { + "label": "Baseado em Tempo", + "hint": "Em uma agenda recorrente" + } + }, + "trigger": "Gatilho", + "triggerType": "Tipo de gatilho", + "keywords": "Palavras-chave", + "keywordsHint": "Lista separada por vírgulas (ex.: olá, ajuda, comprar)", + "replyIds": "IDs de Resposta", + "replyIdsHint": "sim, não, mais_info", + "replyIdsHelp": "IDs de botão/linha de lista separados por vírgula para corresponder exatamente. Use os mesmos ids definidos em uma etapa Enviar Botões / Enviar Lista.", + "schedule": "Agenda", + "scheduleHint": "Expressão cron (ex.: 0 9 * * 1-5)", + "active": "Ativa", + "activeAria": "Ativa", + "save": "Salvar", + "saveDraft": "Salvar Rascunho", + "untitled": "Automação sem título", + "backToAutomations": "Voltar às automações", + "toasts": { + "saved": "Automação salva", + "created": "Automação criada", + "saveFailed": "Falha ao salvar" + }, + "tags": { + "select": "Selecione uma etiqueta…", + "placeholder": "id da etiqueta", + "unknown": "{id} (etiqueta desconhecida)" + }, + "fields": { + "name": "Nome", + "email": "E-mail", + "company": "Empresa", + "customFields": "Campos personalizados", + "unknown": "{id} (campo desconhecido)" + }, + "agents": { + "select": "Selecione um agente…", + "placeholder": "id do agente", + "unknown": "{id} (agente desconhecido)" + }, + "pipelines": { + "pipelineLabel": "Funil", + "pipelineIdLabel": "id do funil", + "stageLabel": "Etapa", + "stageIdLabel": "id da etapa", + "selectPipeline": "Selecione um funil…", + "selectStage": "Selecione uma etapa…", + "selectPipelineFirst": "Selecione um funil primeiro…", + "unknownPipeline": "{id} (funil desconhecido)", + "unknownStage": "{id} (etapa desconhecida)" + }, + "templates": { + "templateLabel": "Modelo", + "templateNameLabel": "Nome do modelo", + "languageLabel": "Idioma", + "select": "Selecione um modelo…", + "unknown": "{name} ({lang}) — não está na lista de aprovados", + "unknownLang": "desconhecido" + }, + "branches": { + "yes": "Sim", + "no": "Não" + }, + "addStep": "Adicionar etapa", + "config": { + "messageText": "Texto da mensagem", + "placeholderMessageText": "Olá! Obrigado por entrar em contato…", + "tagLabel": "Etiqueta", + "modeLabel": "Modo", + "modes": { + "round_robin": "Rodízio", + "specific": "Agente específico" + }, + "agentLabel": "Agente", + "fieldLabel": "Campo", + "valueLabel": "Valor", + "placeholderValue": "Texto ou {{ vars.x }} / {{ message.text }}", + "titleLabel": "Título", + "amountLabel": "Quantidade", + "unitLabel": "Unidade", + "units": { + "minutes": "Minutos", + "hours": "Horas", + "days": "Dias" + }, + "subjectLabel": "Assunto", + "subjects": { + "tag_presence": "Presença de etiqueta", + "contact_field": "Campo do contato", + "message_content": "Conteúdo da mensagem", + "time_of_day": "Hora do dia" + }, + "operandLabel": "Operando", + "placeholderTime": "HH:mm-HH:mm", + "placeholderContact": "nome / e-mail / empresa", + "placeholderTag": "id da etiqueta", + "urlLabel": "URL", + "headersLabel": "Cabeçalhos", + "placeholderHeaders": "{\"Authorization\": \"Bearer ...\"}", + "bodyTemplateLabel": "Modelo do corpo", + "placeholderBody": "{\"id\": \"{{ contact.id }}\"}", + "matchType": "Tipo de correspondência", + "matchContains": "Contém", + "matchExact": "Exata", + "closeConversationHint": "Define o status da conversa como \"fechada\". Nenhuma configuração necessária." + }, + "schedulePlaceholder": "Expressão cron ou HH:mm", + "moveUp": "Mover para cima", + "moveDown": "Mover para baixo", + "delete": "Excluir" + } + }, + "Flows": { + "list": { + "title": "Fluxos", + "beta": "Beta", + "description": "Crie conversas ramificadas do WhatsApp guiadas por botões. Úteis para menus, perguntas frequentes e triagem antes de um humano entrar.", + "newFlow": "Novo fluxo", + "statusDraft": "Rascunho", + "statusActive": "Ativo", + "statusArchived": "Arquivado", + "loadError": "Não foi possível carregar os fluxos.", + "createError": "Não foi possível criar o fluxo.", + "cloneError": "Falha ao clonar", + "deleteConfirm": "Excluir \"{name}\"? Todas as execuções ativas terminarão imediatamente.", + "deleteSuccess": "Fluxo excluído.", + "deleteError": "Não foi possível excluir o fluxo.", + "emptyTitle": "Nenhum fluxo ainda", + "emptyDesc": "Crie sua primeira conversa — um menu de boas-vindas, uma consulta de pedido, um bot de perguntas frequentes. Os clientes tocam em botões; o bot os direciona para a resposta certa (ou o agente certo).", + "createFirst": "Crie seu primeiro fluxo", + "createTitle": "Criar um novo fluxo", + "createDesc": "Comece a partir de um modelo ou crie do zero.", + "startTemplate": "Começar a partir de um modelo", + "nodeCount": "{count} {count, plural, =1 {nó} other {nós}}", + "startBlank": "Ou comece em branco", + "placeholderName": "ex.: Menu de boas-vindas", + "cancel": "Cancelar", + "createBlank": "Criar fluxo em branco", + "runCount": "{count} {count, plural, =1 {execução} other {execuções}}", + "edit": "Editar", + "delete": "Excluir", + "triggerKeywordNone": "Aciona por palavra-chave (nenhuma definida)", + "triggerKeyword": "Aciona por: {keywords}", + "triggerFirstInbound": "Aciona na primeira mensagem recebida de um contato", + "triggerManual": "Gatilho manual" + }, + "edit": { + "notFound": "Fluxo não encontrado.", + "backToFlows": "← Voltar aos fluxos", + "loadError": "Não foi possível carregar o fluxo." + }, + "logs": { + "title": "Execuções", + "description": "As 50 execuções mais recentes deste fluxo. Expanda uma linha para ver o registro do mecanismo por etapa.", + "emptyState": "Nenhuma execução ainda. Acione o fluxo a partir de um número pessoal do WhatsApp para vê-lo aparecer aqui.", + "loadError": "Não foi possível carregar as execuções.", + "unknownContact": "Contato desconhecido", + "atNode": "no nó {node}", + "started": "Iniciada {time}", + "reprompts": "{count} novas solicitações", + "ranFor": "durou {duration}", + "capturedVars": "Variáveis capturadas ({count})", + "noEvents": "Nenhum evento registrado para esta execução.", + "statusActive": "Ativa", + "statusCompleted": "Concluída", + "statusHandedOff": "Transferida", + "statusTimedOut": "Expirada", + "statusPaused": "Pausada pelo agente", + "statusFailed": "Falhou" + }, + "builder": { + "triggerTitle": "Gatilho", + "whenLabel": "Quando…", + "triggerKeywordTitle": "Uma mensagem contém uma palavra-chave", + "triggerFirstInboundTitle": "Primeira mensagem recebida do cliente", + "triggerManualTitle": "Somente manual (sem acionamento automático)", + "keywordsLabel": "Palavras-chave (separadas por vírgula)", + "keywordsPlaceholder": "suporte, ajuda, oi", + "entryNodeTitle": "Nó de entrada:", + "entryNodePlaceholder": "Escolha o primeiro nó…", + "nodesTitle": "Nós ({count})", + "nodesEmpty": "Adicione um nó Início, depois um nó Enviar botões, depois uma Transferência — esse é o formato de menu de boas-vindas do briefing.", + "addNode": "Adicionar nó", + "setAsEntry": "Definir como entrada", + "removeNode": "Remover nó", + "showAdvanced": "Mostrar avançado", + "hideAdvanced": "Ocultar avançado", + "nodeKeyLabel": "Chave do nó (identificador interno — mantenha estável para análises)", + "replyIdsHint": "Os IDs de resposta de cada opção são exibidos acima. Eles são retornados pelo WhatsApp quando um cliente toca; geralmente você não precisa mexer neles.", + "badgeEntry": "Entrada", + "noNodesYet": "Nenhum nó ainda.", + "deleteNode": "Excluir nó", + "canvasView": "Tela", + "listView": "Lista", + "noNodeSelected": "Nenhum nó selecionado", + "noNodeDesc": "Selecione um nó na tela para editar sua configuração.", + "form": { + "advancesTo": "Avança para", + "textToCustomer": "Texto enviado ao cliente", + "promptToCustomer": "Pergunta enviada ao cliente", + "varKeyLabel": "Chave da variável (armazenada em flow_runs.vars; alfanumérico + sublinhado)", + "varKeyPlaceholder": "ex.: nome, email, empresa", + "varKeyHelp": "Interpole em perguntas seguintes e notas de transferência com", + "advanceAfterCapture": "Após capturar, avançar para", + "internalNote": "Nota interna (para o agente que assumir)", + "endNodeHelp": "Nó terminal. Quando o executor alcança este nó, a execução é marcada como concluída. Nenhuma configuração necessária.", + "bodyText": "Texto do corpo", + "footerText": "Rodapé (opcional, 60 caracteres)", + "buttonsHelp": "Botões (1–3) — cada um direciona para um próximo nó diferente", + "optionTitlePlaceholder": "Título visível (≤20 caracteres)", + "nextNodePlaceholder": "Próximo nó…", + "addButton": "Adicionar botão", + "buttonLabel": "Rótulo do botão de expandir (≤20 caracteres)", + "rowsHelp": "Linhas (1–10 no total em todas as seções)", + "sectionTitlePlaceholder": "Título da seção {count} (opcional)", + "rowTitlePlaceholder": "Título da linha (≤24)", + "addRow": "Adicionar linha", + "addSection": "Adicionar seção", + "ifLabel": "Se", + "capturedVariable": "Variável capturada", + "contactHasTag": "Contato tem etiqueta", + "contactField": "Campo do contato", + "varName": "nome da variável", + "tagLabel": "Etiqueta", + "fieldLabel": "Campo", + "pickTag": "Escolha uma etiqueta…", + "pickField": "Escolha um campo…", + "operatorLabel": "Operador", + "isPresent": "está presente", + "isAbsent": "está ausente", + "equals": "é igual a", + "contains": "contém", + "valueLabel": "Valor", + "ifTrueAdvance": "Se verdadeiro → avançar para", + "ifFalseAdvance": "Se falso → avançar para", + "actionLabel": "Ação", + "addTag": "Adicionar etiqueta", + "removeTag": "Remover etiqueta", + "tagUuidPlaceholder": "UUID da etiqueta", + "thenAdvanceTo": "Então avançar para", + "mediaTypeLabel": "Tipo de mídia", + "imageLabel": "Imagem (PNG, JPEG, WebP)", + "videoLabel": "Vídeo (MP4, 3GP)", + "documentLabel": "Documento (PDF, Word, Excel, PowerPoint, TXT)", + "fileLabel": "Arquivo", + "removeFile": "Remover arquivo", + "uploading": "Enviando…", + "clickToUpload": "Clique para enviar (máx. 16 MB)", + "captionLabel": "Legenda (opcional, exibida sob a mídia)", + "filenameLabel": "Nome do arquivo exibido ao cliente (somente documentos)", + "filenamePlaceholder": "fatura.pdf", + "advanceAfterSending": "Após enviar, avançar para", + "pickNextNode": "Escolha um próximo nó…", + "none": "— Nenhum —" + }, + "categories": { + "messaging": "Mensagens", + "logic": "Lógica e dados", + "flow": "Controle de fluxo" + }, + "nodes": { + "start": { + "label": "Início", + "blurb": "Ponto de entrada do fluxo" + }, + "send_message": { + "label": "Enviar mensagem", + "blurb": "Envia uma mensagem de texto do WhatsApp" + }, + "send_buttons": { + "label": "Enviar botões", + "blurb": "Envia botões de resposta rápida" + }, + "send_list": { + "label": "Enviar lista", + "blurb": "Envia uma lista tocável de opções" + }, + "send_media": { + "label": "Enviar mídia", + "blurb": "Envia uma imagem, vídeo ou documento" + }, + "collect_input": { + "label": "Coletar entrada", + "blurb": "Faz uma pergunta e salva a resposta" + }, + "condition": { + "label": "Se / senão", + "blurb": "Ramifica com base em uma regra" + }, + "set_tag": { + "label": "Etiquetar contato", + "blurb": "Adiciona ou remove uma etiqueta do contato" + }, + "handoff": { + "label": "Transferir para agente", + "blurb": "Entrega a conversa a um humano" + }, + "end": { + "label": "Fim", + "blurb": "Encerra o fluxo" + } + }, + "editorView": "Visualização do editor" + }, + "validation": { + "noIssues": "Nenhum problema. Pronto para ativar.", + "summary": "{errorCount} {errorCount, plural, =1 {erro} other {erros}}, {warningCount} {warningCount, plural, =1 {aviso} other {avisos}}", + "jumpToNode": "Ir para o nó {key}" + }, + "editorState": { + "saved": "Salvo.", + "fixIssues": "Corrija os problemas abaixo antes de ativar.", + "statusActivated": "Fluxo ativado.", + "statusArchived": "Arquivado.", + "statusDraft": "Salvo como rascunho." + }, + "summary": { + "options": "{count} {count, plural, =1 {opção} other {opções}}", + "optionsAcrossSections": "{rowCount} {rowCount, plural, =1 {opção} other {opções}} em {sectionCount} {sectionCount, plural, =1 {seção} other {seções}}", + "noFile": "{label} (nenhum arquivo enviado)", + "media": "Mídia", + "image": "Imagem", + "video": "Vídeo", + "document": "Documento", + "audio": "Áudio", + "hasTag": "tem a etiqueta {tag}", + "opContains": "contém", + "opExists": "existe", + "opMissing": "ausente", + "modeAdd": "Adicionar", + "modeRemove": "Remover", + "tagPicked": "{mode} etiqueta {tag}…", + "tagNone": "{mode} etiqueta (nenhuma escolhida)" + } + }, + "Settings": { + "pageTitle": "Configurações", + "pageDesc": "Tudo em um só lugar — sua conta e seu espaço de trabalho. Escolha uma seção para gerenciá-la.", + "overview": { + "notSetup": "Ainda não configurado", + "connected": "Conectado", + "needsReconnecting": "Precisa reconectar", + "viewTeamMembers": "Ver membros da equipe", + "membersCount": "{count} {count, plural, =1 {membro} other {membros}}", + "pendingInvites": "{count} {count, plural, =1 {convite pendente} other {convites pendentes}}", + "manageTemplates": "Gerenciar modelos de mensagem", + "templatesCount": "{count} {count, plural, =1 {modelo} other {modelos}}", + "pendingReview": "{count} em análise", + "tagsAndFields": "Etiquetas e campos personalizados", + "tagsCount": "{count} {count, plural, =1 {etiqueta} other {etiquetas}}", + "fieldsCount": "{count} {count, plural, =1 {campo personalizado} other {campos personalizados}}", + "appearance": "modo {mode} · destaque {theme}", + "yourAccount": "Sua conta", + "loading": "Carregando…" + }, + "members": { + "title": "Membros da equipe", + "description": "Pessoas com acesso a esta conta. As funções controlam o que cada colega pode fazer.", + "inviteMember": "Convidar membro", + "online": "online", + "away": "ausente", + "offline": "offline", + "memberCount": "{count} {count, plural, =1 {membro} other {membros}}", + "you": "Você", + "unnamed": "Sem nome", + "joined": "Entrou em {date}", + "remove": "Remover", + "removedToast": "{name} removido", + "updatedToast": "{name} atualizado para {role}", + "pendingInvitations": "Convites pendentes", + "inviteHint": "A URL de convite em texto simples só é exibida uma vez na criação por segurança — para compartilhar novamente, revogue o convite abaixo e crie um novo.", + "noPendingTitle": "Nenhum convite pendente.", + "noPendingDesc": "Clique em Convidar membro acima para gerar um link compartilhável.", + "untitledInvite": "Convite sem título", + "created": "Criado em {date}", + "expiresInDays": "expira em {days} {days, plural, =1 {dia} other {dias}}", + "expiresInHours": "expira em {hours} {hours, plural, =1 {hora} other {horas}}", + "expired": "expirado", + "revoke": "Revogar", + "revokedToast": "Convite revogado", + "removeDialogTitle": "Remover membro", + "removeDialogDesc": "Remover {name} da conta? Ele será desconectado desta conta e receberá uma nova conta pessoal no próximo login. O login não é excluído.", + "cancel": "Cancelar", + "removing": "Removendo...", + "removeBtn": "Remover membro", + "loadMembersFailed": "Falha ao carregar os membros", + "loadInvitationsFailed": "Falha ao carregar os convites", + "serverUnreachable": "Não foi possível acessar o servidor", + "updateRoleFailed": "Falha ao atualizar a função", + "removeMemberFailed": "Falha ao remover o membro", + "revokeInvitationFailed": "Falha ao revogar o convite", + "memberAlt": "Membro" + }, + "invite": { + "inviteCreated": "Convite criado", + "inviteCreatedDesc": "Compartilhe este link com seu novo colega. Ele poderá se cadastrar (ou entrar) e ingressar na conta como {role}. O link é válido por {days} {days, plural, =1 {dia} other {dias}}.", + "inviteLink": "Link de convite", + "copy": "Copiar", + "saveLinkNow": "Salve este link agora.", + "saveLinkHint": "Nunca armazenamos o texto simples — assim que você fechar esta caixa de diálogo, a URL desaparece. Para compartilhar de novo, revogue este convite e crie um novo.", + "sendViaWhatsApp": "Enviar via WhatsApp", + "done": "Concluído", + "dialogTitle": "Convidar um colega", + "dialogDesc": "Gere um link de convite de uso único. Compartilhe via WhatsApp, Slack ou qualquer canal que preferir — nenhum serviço de e-mail necessário.", + "roleLabel": "Função", + "validForLabel": "Link válido por", + "days1": "1 dia", + "days7": "7 dias", + "days30": "30 dias", + "labelTitle": "Rótulo", + "optional": "(opcional)", + "labelPlaceholder": "ex.: Sara — equipe de suporte", + "labelHint": "Ajuda você a lembrar para quem enviou o link na lista de pendentes abaixo.", + "cancel": "Cancelar", + "creating": "Criando...", + "generateLink": "Gerar link", + "labelTooLong": "O rótulo deve ter no máximo {max} caracteres", + "copied": "Link de convite copiado", + "clipboardBlocked": "Área de transferência bloqueada — copie o link manualmente", + "whatsappMessage": "Entre na {accountName} no wacrm usando este link (válido por {expiresInDays} dias): {url}", + "createFailed": "Falha ao criar o convite", + "serverUnreachable": "Não foi possível acessar o servidor. Tentar novamente?" + }, + "tagsAndFields": { + "title": "Campos e etiquetas", + "description": "Duas formas de organizar contatos: etiquetas com cores para agrupamento rápido e campos personalizados para dados estruturados.", + "tagsTitle": "Etiquetas", + "tagsDesc": "Rótulos coloridos para agrupar e filtrar contatos.", + "fieldsTitle": "Campos personalizados", + "fieldsDesc": "Campos adicionais de contato (ex.: CEP, origem do lead). Eles aparecem em todos os contatos e na ação de automação “Atualizar Campo do Contato”.", + "adminRole": "Administrador", + "failedToLoadTags": "Falha ao carregar etiquetas", + "nameRequired": "O nome da etiqueta é obrigatório", + "notAuthenticated": "Não autenticado", + "tagCreated": "Etiqueta criada", + "failedToCreateTag": "Falha ao criar etiqueta", + "tagDeleted": "Etiqueta excluída", + "failedToDeleteTag": "Falha ao excluir etiqueta", + "deleteAria": "Excluir {name}", + "noTags": "Nenhuma etiqueta ainda — crie a primeira abaixo.", + "placeholder": "ex.: Newsletter", + "useColor": "Usar {color}", + "addTag": "Adicionar etiqueta", + "deleteTag": "Excluir etiqueta", + "deleteConfirm": "Excluir a etiqueta \"{name}\"? Isso a remove de todos os contatos e não pode ser desfeito.", + "cancel": "Cancelar", + "deleting": "Excluindo...", + "colors": { + "red": "Vermelho", + "orange": "Laranja", + "amber": "Âmbar", + "emerald": "Esmeralda", + "cyan": "Ciano", + "blue": "Azul", + "violet": "Violeta", + "pink": "Rosa" + } + }, + "templates": { + "title": "Modelos de mensagem", + "description": "Crie modelos e envie-os à Meta para aprovação. Use \"Sincronizar da Meta\" para trazer modelos aprovados em outro lugar.", + "syncFromMeta": "Sincronizar da Meta", + "syncing": "Sincronizando…", + "syncTitle": "Traga modelos aprovados da sua Conta do WhatsApp Business da Meta", + "newTemplate": "Novo Modelo", + "noTemplates": "Nenhum modelo ainda.", + "createFirst": "Crie seu primeiro modelo de mensagem para começar.", + "editTitle": "Editar aciona uma nova análise da Meta — o status muda para PENDENTE.", + "editLabel": "Editar modelo", + "edit": "Editar", + "resubmitTitle": "Edite o modelo e reenvie à Meta para análise.", + "resubmitLabel": "Editar e reenviar modelo", + "resubmit": "Reenviar", + "deleteMetaLocallyAria": "Excluir modelo da Meta e localmente", + "deleteLocallyAria": "Excluir modelo localmente", + "deleteMetaLocallyTitle": "Excluir da Meta e localmente", + "deleteLocallyTitle": "Excluir localmente", + "dialogEditTitle": "Editar Modelo de Mensagem", + "dialogNewTitle": "Novo Modelo de Mensagem", + "dialogEditDesc": "Salve suas alterações para reenviar à Meta. O status voltará para PENDENTE durante a análise.", + "dialogNewDesc": "Crie um modelo e envie-o à Meta para aprovação. Uma vez aprovado, você pode usá-lo em transmissões e na caixa de entrada.", + "authWarning": "Modelos de AUTENTICAÇÃO têm um formato fixo de corpo + botão de OTP que exige um construtor diferente. Crie-os no Meta WhatsApp Manager por enquanto e use Sincronizar da Meta para trazê-los.", + "templateName": "Nome do Modelo", + "namePlaceholder": "ex.: confirmacao_pedido", + "nameFixed": "O nome é fixo depois que um modelo existe na Meta — crie um novo modelo para alterá-lo.", + "nameHint": "Somente letras minúsculas, dígitos e sublinhados.", + "category": "Categoria", + "language": "Idioma", + "langFixed": "O idioma é fixo depois que um modelo existe na Meta.", + "langHint": "Deve corresponder ao código exato na Meta — en_US e en são distintos.", + "header": "Cabeçalho", + "headerNone": "Nenhum", + "headerText": "Texto", + "headerImage": "Imagem", + "headerVideo": "Vídeo", + "headerDocument": "Documento", + "headerTextPlaceholder": "Texto do cabeçalho (máx. 60 caracteres, {{1}} opcional)", + "headerSampleAria": "Valor de exemplo para a variável do cabeçalho", + "headerSamplePlaceholder": "Valor de exemplo para {{1}} (obrigatório para a análise da Meta)", + "uploadImage": "Enviar imagem", + "uploadHint": "JPEG ou PNG, ≤5 MB", + "mediaUrlPlaceholder": "https://… (ou cole um link público de {format})", + "imageHint": "Envie um JPEG/PNG (≤5 MB, ≥800×418 px recomendado) ou cole um link HTTPS público — nós o enviamos à Meta para análise automaticamente.", + "mediaHint": "Deve ser um link HTTPS publicamente acessível. A Meta o busca uma vez durante a análise, então ele precisa permanecer ativo por ~24 h.", + "videoHint": " Recomendado: MP4 / 3GPP, ≤16 MB, ≤60 segundos.", + "documentHint": " Recomendado: PDF, ≤100 MB.", + "bodyText": "Texto do Corpo", + "bodyPlaceholder": "Olá {{1}}, seu pedido {{2}} está confirmado.", + "bodyHint": "Use {{1}}, {{2}} para variáveis (devem ser contíguas, começando em {{1}}).", + "sampleValues": "Valores de exemplo (a Meta os usa para analisar seu modelo)", + "sampleAria": "Valor de exemplo para a variável do corpo {var}", + "samplePlaceholder": "Exemplo para {var}", + "footer": "Rodapé (opcional)", + "footerPlaceholder": "Texto de rodapé opcional (máx. 60 caracteres)", + "buttons": "Botões (opcional)", + "addButton": "Adicionar Botão", + "buttonsLimit": "Até {max} botões. Botões de RESPOSTA RÁPIDA devem vir antes dos botões de URL / telefone / copiar código.", + "btnQuickReply": "Resposta Rápida", + "btnUrl": "URL", + "btnPhone": "Telefone", + "btnCopyCode": "Copiar Código", + "btnLabelPlaceholder": "Rótulo do botão", + "urlPlaceholder": "https://exemplo.com/caminho ou com sufixo {{1}}", + "urlSamplePlaceholder": "Valor de exemplo para {{1}} (obrigatório quando a URL tem uma variável)", + "phonePlaceholder": "+5511999999999", + "codePlaceholder": "Código de exemplo (ex.: VERAO20)", + "cancel": "Cancelar", + "saving": "Salvando…", + "submitting": "Enviando…", + "saveResubmit": "Salvar e Reenviar", + "submitApproval": "Enviar para Aprovação", + "deleteDialogTitle": "Excluir modelo?", + "deleteMetaDesc": "\"{name}\" será excluído da Meta e do wacrm. Transmissões ativas que usam este modelo começarão a falhar no próximo envio. Isso não pode ser desfeito.", + "deleteLocalDesc": "\"{name}\" será excluído do wacrm. Ele nunca foi enviado à Meta, então nenhuma limpeza remota é necessária.", + "deleting": "Excluindo…", + "delete": "Excluir", + "toastLoadFailed": "Falha ao carregar modelos", + "toastSubmitFailed": "Falha ao enviar", + "toastSubmitEditSuccess": "Edição enviada — a Meta normalmente analisa em até 24 horas.", + "toastSubmitNewSuccess": "Enviado à Meta — o tempo de análise típico é de 24 horas. O status é atualizado automaticamente.", + "toastSaveEditDry": "Modelo atualizado (simulação — nenhuma chamada à Meta)", + "toastSaveNewDry": "Modelo salvo (simulação — nenhuma chamada à Meta)", + "toastSyncCount": "Sincronizados {total} {total, plural, =1 {modelo} other {modelos}} da Meta", + "toastSyncDetails": " ({inserted} novos, {updated} atualizados)", + "toastSyncFailed": "Falha ao sincronizar: {preview}", + "toastSyncTruncated": "Sincronizados apenas os primeiros 2000 modelos — sua conta tem mais. Sincronize novamente para continuar, ou contate o suporte se isso persistir.", + "toastSyncError": "Falha ao sincronizar modelos", + "toastDeleteSuccess": "Modelo excluído", + "toastDeleteError": "Falha ao excluir modelo", + "toastInvalidImage": "A imagem do cabeçalho deve ser JPEG ou PNG.", + "toastImageTooLarge": "A imagem tem {size} MB — o limite da Meta é 5 MB.", + "toastUploadSuccess": "Imagem enviada.", + "toastUploadFailed": "Falha no envio." + }, + "roles": { + "admin": "Administrador", + "adminHint": "Gerencia membros + tudo", + "agent": "Agente", + "agentHint": "Usa recursos; sem configurações", + "viewer": "Visualizador", + "viewerHint": "Somente leitura em todo o app", + "owner": "Proprietário", + "ownerHint": "Controle total sobre conta e faturamento" + }, + "whatsapp": { + "title": "Conexão do WhatsApp", + "description": "Conecte sua API do WhatsApp Business da Meta. Credenciais, webhook e etapas de configuração ficam todos aqui.", + "tokenCorrupted": "O token armazenado não pode ser descriptografado", + "resetting": "Redefinindo...", + "resetConfig": "Redefinir Configuração", + "credentialsValid": "Credenciais válidas", + "notConnected": "Não Conectado", + "connectedDesc": "Seu token de acesso autentica com a Meta. Veja o status de Registro abaixo para saber se os webhooks estão realmente configurados.", + "notConnectedDesc": "Configure suas credenciais da API da Meta abaixo para conectar sua conta do WhatsApp Business.", + "registered": "Registrado — a Meta entregará eventos ao wacrm", + "notRegistered": "Não registrado — a Meta não entregará eventos", + "verifyWithMeta": "Verificar com a Meta", + "subscribedSince": "Inscrito desde {date}. Clique em Verificar com a Meta se os eventos pararem de chegar.", + "unknownDate": "desconhecida", + "lastAttemptFailed": "A última tentativa falhou com: ", + "retryHint": "Insira (ou corrija) o PIN de verificação em 2 etapas abaixo e clique em Salvar Configuração para tentar novamente.", + "noRegistrationHint": "Este número foi salvo antes de existir o rastreamento de registro, ou o registro foi ignorado. Insira o PIN de verificação em 2 etapas abaixo e clique em Salvar Configuração para inscrevê-lo.", + "diagnosticLastRun": "Diagnóstico — última execução: ", + "live": "ativo", + "notLive": "inativo", + "apiCredentialsTitle": "Credenciais da API", + "apiCredentialsDesc": "Insira suas credenciais da API do WhatsApp Business da Meta.", + "phoneNumberId": "ID do Número de Telefone", + "wabaId": "ID da Conta do WhatsApp Business", + "accessToken": "Token de Acesso Permanente", + "accessTokenPlaceholder": "Insira seu token de acesso", + "tokenHidden": "O token está oculto por segurança. Insira-o novamente para atualizar a configuração.", + "webhookVerifyToken": "Token de Verificação do Webhook", + "webhookVerifyTokenPlaceholder": "Crie um token de verificação personalizado", + "webhookVerifyTokenHint": "Uma string personalizada que você cria. Deve corresponder ao token definido nas configurações de webhook da Meta.", + "twoStepPin": "PIN de verificação em duas etapas", + "optional": "(opcional)", + "pinPlaceholder": "PIN de 6 dígitos do Meta WhatsApp Manager", + "pinHint": "Necessário apenas para configurar mensagens recebidas de um número de produção. Defina-o em Meta Business Manager → Contas do WhatsApp → Números de Telefone → Verificação em duas etapas, depois cole-o aqui para que o wacrm possa inscrever o número — caso contrário, a Meta encaminha os eventos recebidos para qualquer app que o reivindicou por último (o sintoma que afeta números secundários sob uma WABA compartilhada). Números de teste da Meta não têm PIN e são pré-registrados — deixe em branco para eles. Deixar em branco também mantém um registro existente intacto.", + "webhookTitle": "Configuração do Webhook", + "webhookDesc": "Use esta URL como seu callback de webhook no Painel de Apps da Meta.", + "webhookUrl": "URL de Callback do Webhook", + "saving": "Salvando...", + "saveConfig": "Salvar Configuração", + "testing": "Testando...", + "testConnection": "Testar Conexão da API", + "setupInstructions": "Instruções de Configuração", + "setupInstructionsDesc": "Siga estas etapas para conectar sua API do WhatsApp Business.", + "step1": "Crie um App da Meta", + "step1_1": "Acesse developers.facebook.com", + "step1_2": "Clique em \"Meus Apps\" e depois em \"Criar App\"", + "step1_3": "Selecione \"Empresa\" como o tipo de app", + "step1_4": "Preencha os detalhes do app e crie", + "step2": "Adicione o Produto WhatsApp", + "step2_1": "No painel do seu app, clique em \"Adicionar Produto\"", + "step2_2": "Encontre \"WhatsApp\" e clique em \"Configurar\"", + "step2_3": "Siga o assistente de configuração para vincular sua empresa", + "step3": "Obtenha as Credenciais da API", + "step3_1": "Acesse WhatsApp > Configuração da API", + "step3_2": "Copie seu ID do Número de Telefone", + "step3_3": "Copie seu ID da Conta do WhatsApp Business", + "step3_4": "Gere um Token de Acesso Permanente em Configurações da Empresa > Usuários do Sistema", + "step4": "Configure os Webhooks", + "step4_1": "Acesse WhatsApp > Configuração", + "step4_2": "Clique em \"Editar\" na seção Webhook", + "step4_3": "Cole a URL de Callback do Webhook acima", + "step4_4": "Insira o mesmo Token de Verificação que você definiu aqui", + "step4_5": "Inscreva-se no campo de webhook \"messages\"", + "metaDocs": "Documentação da API do WhatsApp da Meta", + "phoneNumberIdRequired": "O ID do número de telefone é obrigatório", + "accessTokenRequired": "O token de acesso é obrigatório para a configuração inicial", + "loadFailed": "Falha ao carregar a configuração do WhatsApp", + "saveFailed": "Falha ao salvar a configuração", + "reenterToken": "Insira novamente o token de acesso para salvar as alterações", + "savedRegisterFailed": "Salvo, mas a Meta não conseguiu registrar o número: {error}", + "savedRegisterSkipped": "Credenciais salvas e verificadas. O registro de entrada foi ignorado (sem PIN) — veja o status de registro abaixo.", + "savedLive": "Ativo — {name} já pode receber eventos.", + "savedConnected": "WhatsApp conectado. Os eventos começarão a fluir em até um minuto.", + "connectedTo": "Conectado a {name}", + "connectionSuccess": "Conexão com a API bem-sucedida", + "connectionFailed": "Falha na conexão com a API", + "connectionTestFailed": "Falha no teste de conexão. Verifique a rede e tente novamente.", + "numberLive": "Número totalmente configurado — a Meta está entregando eventos.", + "numberNotRegistered": "O número não está totalmente registrado. Veja as verificações abaixo para saber qual etapa falhou.", + "verifyEndpointFailed": "Não foi possível acessar o endpoint de verificação.", + "resetConfirm": "Isso excluirá a configuração atual do WhatsApp para que você possa inseri-la novamente. Continuar?", + "resetFailed": "Falha ao redefinir a configuração", + "resetSuccess": "Configuração limpa. Agora você pode inserir suas credenciais novamente.", + "webhookCopied": "URL do webhook copiada para a área de transferência", + "phoneNumberIdPlaceholder": "ex.: 100234567890123", + "wabaIdPlaceholder": "ex.: 100234567890456" + }, + "sections": { + "overview": "Visão geral", + "profile": "Seu perfil", + "security": "Login e segurança", + "appearance": "Aparência", + "whatsapp": "WhatsApp", + "templates": "Modelos", + "fields": "Campos e etiquetas", + "deals": "Negócios e moeda", + "members": "Membros da equipe", + "api": "Chaves de API", + "quick-replies": "Respostas rápidas" + }, + "groups": { + "account": "Conta", + "workspace": "Espaço de trabalho" + }, + "profile": { + "title": "Seu perfil", + "description": "Como você aparece em todo o app. Seu avatar e nome aparecem no cabeçalho, na barra lateral e em qualquer lugar onde seus colegas o veem.", + "changePhoto": "Alterar foto", + "uploadPhoto": "Enviar foto", + "remove": "Remover", + "photoHint": "PNG, JPG, WebP ou GIF. Até 2 MB.", + "displayName": "Nome de exibição", + "email": "E-mail", + "emailChangeHint": "Verifique as caixas de entrada de {oldEmail} e {newEmail} — ambas precisam confirmar antes que a alteração tenha efeito.", + "accountDetails": "Detalhes da conta", + "role": "Função", + "joined": "Entrou em", + "userId": "ID do Usuário", + "loading": "Carregando seu perfil…", + "saving": "Salvando…", + "saveChanges": "Salvar alterações", + "unsupportedImage": "Tipo de imagem não suportado", + "unsupportedImageDesc": "Use PNG, JPG, WebP ou GIF.", + "imageTooLarge": "A imagem é muito grande", + "imageTooLargeDesc": "Máximo de 2 MB.", + "nameRequired": "O nome de exibição é obrigatório", + "invalidEmail": "Insira um endereço de e-mail válido", + "uploadFailed": "Falha no envio: {message}", + "saveFailed": "Falha ao salvar: {message}", + "profileSaved": "Perfil salvo", + "emailChangeFailed": "Falha na alteração de e-mail: {message}", + "profileSavedEmailCheck": "Perfil salvo — verifique seu e-mail para confirmar a alteração do endereço", + "passwordTitle": "Senha", + "passwordDesc": "Use pelo menos {min} caracteres. Você permanecerá conectado neste dispositivo após alterá-la.", + "currentPassword": "Senha atual", + "newPassword": "Nova senha", + "confirmPassword": "Confirmar nova senha", + "updating": "Atualizando…", + "updatePassword": "Atualizar senha", + "cannotChangeNoEmail": "Não é possível alterar a senha sem um e-mail atual", + "passwordTooShort": "A senha deve ter pelo menos {min} caracteres", + "passwordMismatch": "A nova senha e a confirmação não coincidem", + "currentPasswordIncorrect": "A senha atual está incorreta", + "passwordUpdateFailed": "Falha ao atualizar a senha: {message}", + "passwordUpdated": "Senha atualizada", + "sessionsTitle": "Sessões ativas", + "sessionsDesc": "Saia de todos os dispositivos em que você está conectado — incluindo este. Útil se você perdeu um laptop ou compartilhou sua senha.", + "signOutAll": "Sair de todos os dispositivos", + "signOutConfirmTitle": "Sair de todos os lugares?", + "signOutConfirmDesc": "Todos os dispositivos conectados a esta conta serão desconectados e precisarão fazer login novamente. Você será redirecionado para a página de login.", + "cancel": "Cancelar", + "signingOut": "Saindo…", + "signOutEverywhere": "Sair de todos os lugares", + "signOutFailed": "Falha ao sair: {message}", + "displayNamePlaceholder": "Ada Lovelace" + }, + "appearance": { + "title": "Aparência", + "description": "Defina o modo e a cor de destaque usados em todo o app. Salvo neste dispositivo — experimente, muda ao vivo.", + "mode": "Modo", + "useMode": "Usar o modo {mode}", + "active": "Ativo", + "accentColor": "Cor de destaque", + "useTheme": "Usar o tema {name}" + }, + "security": { + "title": "Login e segurança", + "description": "Altere sua senha e saia dos seus dispositivos. Isso mantém sua conta segura." + }, + "apiKeys": { + "title": "Chaves de API", + "description": "As chaves autenticam a API REST pública (/api/v1) para que você possa criar suas próprias automações. Envie-as como Authorization: Bearer <key>.", + "newApiKey": "Nova chave de API", + "noApiKeys": "Nenhuma chave de API ainda.", + "createOneHint": "Clique em Nova chave de API para criar uma.", + "askAdminHint": "Peça a um administrador para criar uma.", + "revoked": "Revogada", + "expired": "Expirada", + "noScopes": "Nenhum escopo", + "created": "Criada em {date}", + "lastUsed": "usada pela última vez em {date}", + "neverUsed": "nunca usada", + "expires": "expira em {date}", + "revoke": "Revogar", + "revokeSuccess": "\"{name}\" revogada", + "revokeFailed": "Falha ao revogar a chave", + "loadFailed": "Falha ao carregar as chaves de API", + "createError": "Falha ao criar a chave", + "nameRequired": "Dê um nome à chave", + "nameLabel": "Nome", + "namePlaceholder": "ex.: automação do Zapier", + "scopesLabel": "Escopos", + "scopesHint": "Uma chave sem escopos ainda pode chamar GET /api/v1/me para verificar se funciona.", + "cancel": "Cancelar", + "creating": "Criando…", + "createKey": "Criar chave", + "copyTitle": "Copie sua chave de API", + "copyDesc": "Esta é a única vez que a chave completa é exibida. Guarde-a em local seguro — se perdê-la, revogue-a e crie uma nova.", + "apiKeyLabel": "Chave de API", + "copy": "Copiar", + "copySuccess": "Chave de API copiada", + "copyFailed": "Falha ao copiar — selecione e copie manualmente", + "newKeyTitle": "Nova chave de API", + "newKeyDesc": "Dê um nome de acordo com a integração que a usará e conceda apenas os escopos necessários.", + "done": "Concluído", + "networkError": "Não foi possível conectar ao servidor", + "scopeDesc": { + "messages:send": "Enviar mensagens do WhatsApp", + "messages:read": "Ler mensagens e o status de entrega", + "contacts:read": "Listar e ler contatos", + "contacts:write": "Criar e atualizar contatos", + "conversations:read": "Listar e ler conversas", + "broadcasts:send": "Disparar campanhas de transmissão", + "webhooks:manage": "Registrar e gerenciar webhooks de eventos de saída" + } + }, + "deals": { + "title": "Negócios e moeda", + "description": "A moeda usada para novos negócios e para os totais do funil e do painel.", + "defaultCurrency": "Moeda padrão", + "defaultCurrencyDesc": "Novos negócios usam esta moeda por padrão, e os totais do funil e do painel são exibidos nela. Negócios existentes mantêm a moeda com que foram salvos.", + "currencyLabel": "Moeda", + "adminOnlyHint": "Apenas administradores da conta podem alterar a moeda padrão.", + "save": "Salvar", + "saving": "Salvando...", + "saveFailed": "Falha ao salvar a moeda padrão", + "saveSuccess": "Moeda padrão atualizada" + }, + "aiConfig": { + "title": "Configuração do agente", + "description": "Traga sua própria chave da OpenAI ou Anthropic. O wacrm chama o provedor diretamente com sua chave — sem taxas de IA por assento, e seus dados continuam seus. Isso alimenta as respostas redigidas por IA na caixa de entrada, o bot de resposta automática e o Playground.", + "adminOnlyConfig": "Apenas administradores e proprietários podem alterar a configuração de IA.", + "loadFailed": "Falha ao carregar a configuração de IA", + "providerAndKey": "Provedor e chave", + "encryptionNotice": "Sua chave é criptografada em repouso (AES-256-GCM) e nunca é exibida novamente após ser salva.", + "provider": "Provedor", + "model": "Modelo", + "apiKey": "Chave de API", + "testKey": "Testar chave", + "testSuccess": "A chave funciona — o provedor respondeu.", + "testRejected": "O provedor rejeitou a solicitação.", + "testNetworkError": "Não foi possível conectar ao provedor.", + "embeddingsKey": "Chave de embeddings", + "optionalSemanticSearch": "(opcional — habilita a busca semântica na base de conhecimento)", + "embeddingsHint": "Uma chave da OpenAI usada apenas para gerar embeddings da sua base de conhecimento (text-embedding-3-small){sameKeyText}. Deixe em branco para usar a busca por palavra-chave. Limpe-a para desativar a busca semântica.", + "sameKeyText": " — pode ser a mesma chave acima", + "behaviour": "Comportamento", + "behaviourDesc": "Conte ao assistente sobre seu negócio — produtos, tom, o que ele pode e não pode prometer. Este contexto alimenta tanto os rascunhos quanto as respostas automáticas.", + "businessContext": "Contexto e instruções do negócio", + "promptPlaceholder": "ex.: Somos a Acme, uma loja de equipamentos de café. Seja acolhedor e conciso. Nunca informe preços ou prazos de entrega — transfira para um humano nesses casos.", + "enableAssistant": "Habilitar assistente de IA", + "enableAssistantDesc": "Interruptor principal. Ativa o botão “Redigir com IA” na caixa de entrada.", + "autoReply": "Resposta automática a mensagens recebidas", + "autoReplyDesc": "O bot responde automaticamente a novas mensagens recebidas (apenas quando nenhum fluxo as trata e nenhum agente está atribuído). Transfere para um humano quando não consegue ajudar.", + "maxAutoReplies": "Máximo de respostas automáticas por conversa", + "maxAutoRepliesDesc": "Após este número de respostas do bot em uma conversa, o bot fica em silêncio.", + "handoffTo": "Transferir para", + "handoffToDesc": "Quando o bot não consegue ajudar — ou atinge o limite de respostas — ele pausa e direciona a conversa para cá, com uma breve nota sobre o que aconteceu.", + "handoffQueue": "Fila não atribuída (qualquer agente pode assumir)", + "remove": "Remover", + "save": "Salvar", + "missingModel": "Insira um nome de modelo.", + "missingApiKey": "Insira sua chave de API.", + "saveSuccess": "Assistente de IA salvo.", + "saveFailed": "Falha ao salvar.", + "removeSuccess": "Configuração de IA removida.", + "removeFailed": "Falha ao remover." + }, + "aiKnowledge": { + "loadFailed": "Falha ao carregar a base de conhecimento", + "openFailed": "Falha ao abrir o documento", + "titleContentRequired": "O título e o conteúdo são obrigatórios.", + "saveSuccessNew": "Documento adicionado.", + "saveSuccessUpdate": "Documento atualizado.", + "saveFailed": "Falha ao salvar.", + "removeSuccess": "Documento removido.", + "removeFailed": "Falha ao remover.", + "reindexSuccess": "Reindexados {count} documento(s).", + "reindexFailed": "Falha na reindexação.", + "title": "Base de conhecimento", + "description": "Adicione perguntas frequentes, políticas ou detalhes de produtos. O assistente recupera os trechos relevantes ao redigir e responder automaticamente, para que possa responder em vez de transferir.{searchType}", + "semanticSearchOn": " A busca semântica está ativada (chave de embeddings definida).", + "keywordSearchOn": " Usando busca por palavra-chave — adicione uma chave de embeddings acima para busca semântica.", + "noDocs": "Nenhum documento ainda.", + "editDocTitle": "Título", + "editDocTitlePlaceholder": "ex.: Política de trocas e reembolsos", + "editDocContent": "Conteúdo", + "editDocContentPlaceholder": "Cole a resposta da FAQ, o texto da política ou os detalhes do produto…", + "cancel": "Cancelar", + "saveDoc": "Salvar documento", + "addDoc": "Adicionar documento", + "reindex": "Reindexar", + "reindexTooltip": "Regerar embeddings de todos os documentos (ex.: após adicionar uma chave de embeddings)", + "loading": "Carregando…" + }, + "sectionsNav": "Seções de configurações", + "quickReplies": { + "nameRequired": "Dê um nome à resposta rápida.", + "saveError": "Não foi possível salvar a resposta rápida.", + "updated": "Resposta rápida atualizada.", + "created": "Resposta rápida criada.", + "deleteConfirm": "Excluir esta resposta rápida?", + "deleteError": "Não foi possível excluir a resposta rápida.", + "title": "Respostas rápidas", + "description": "Trechos reutilizáveis — texto simples ou uma mensagem interativa salva — que os agentes podem inserir a partir do compositor da caixa de entrada.", + "newQuickReply": "Nova resposta rápida", + "empty": "Ainda não há respostas rápidas. Crie uma para reutilizá-la nas conversas.", + "editQuickReply": "Editar resposta rápida", + "nameLabel": "Nome", + "namePlaceholder": "ex.: Horário de atendimento", + "kindText": "Texto", + "kindInteractive": "Interativa", + "textPlaceholder": "O texto da mensagem a inserir", + "cancel": "Cancelar", + "save": "Salvar" + } + }, + "Notifications": { + "title": "Notificações", + "subtitle": "As conversas que outros colegas atribuem a você aparecem aqui.", + "markAllAsRead": "Marcar todas como lidas", + "retry": "Tentar novamente", + "emptyTitle": "Nenhuma notificação ainda", + "emptyBody": "Você verá um alerta aqui quando alguém atribuir uma conversa a você.", + "unread": "Não lida", + "markReadError": "Falha ao marcar a notificação como lida", + "markAllError": "Falha ao marcar todas como lidas" + }, + "Signup": { + "title": "Criar conta", + "titleInvite": "Criar conta e entrar", + "description": "Comece a usar o CRM Template for WhatsApp", + "descriptionInvite": "Verifique seu e-mail e depois aceite o convite para entrar na sua equipe.", + "checkEmailTitle": "Verifique seu e-mail", + "checkEmailBodyBefore": "Enviamos um link de confirmação para", + "checkEmailBodyAfter": ". Verifique sua caixa de entrada e clique no link para confirmar sua conta.", + "backToSignIn": "Voltar para o login", + "fullName": "Nome completo", + "fullNamePlaceholder": "João da Silva", + "email": "E-mail", + "password": "Senha", + "passwordPlaceholder": "Pelo menos 6 caracteres", + "confirmPassword": "Confirmar senha", + "confirmPasswordPlaceholder": "Repita sua senha", + "createAccount": "Criar conta", + "creatingAccount": "Criando conta...", + "alreadyHaveAccount": "Já tem uma conta?", + "signIn": "Entrar", + "passwordsDoNotMatch": "As senhas não coincidem", + "passwordTooShort": "A senha deve ter pelo menos 6 caracteres" + }, + "ForgotPassword": { + "title": "Redefinir senha", + "description": "Informe seu e-mail e enviaremos um link de redefinição", + "checkEmailTitle": "Verifique seu e-mail", + "checkEmailBodyBefore": "Enviamos um link para redefinir a senha para", + "checkEmailBodyAfter": ". Verifique sua caixa de entrada.", + "backToSignIn": "Voltar para o login", + "email": "E-mail", + "sending": "Enviando...", + "sendResetLink": "Enviar link de redefinição" + }, + "Join": { + "verifying": "Verificando convite…", + "tryAgain": "Tentar novamente", + "createAccountInstead": "Criar uma nova conta", + "signIn": "Entrar", + "invitedTo": "Você foi convidado para", + "joinAs": "Você entrará como", + "role": { + "admin": "Administrador", + "agent": "Agente", + "viewer": "Visualizador" + }, + "linkValidUntil": ". Link válido até {date}.", + "accepting": "Aceitando…", + "acceptInvitation": "Aceitar convite", + "acceptHelpBefore": "Ao aceitar, seu login será movido para", + "acceptHelpAfter": ". Sua conta pessoal vazia criada no cadastro será removida.", + "conflictTitle": "Não é possível entrar em {name} com esta conta", + "conflictHelpBefore": "Para entrar em", + "conflictHelpAfter": ", saia e cadastre-se novamente com um endereço de e-mail diferente. O link do convite permanece válido enquanto não expirar.", + "staySignedIn": "Continuar conectado", + "signingOut": "Saindo…", + "signOutDifferentEmail": "Sair e usar outro e-mail", + "createAccountAndJoin": "Criar conta e entrar", + "alreadyHaveAccount": "Já tenho uma conta", + "fail": { + "not_found": { + "title": "Convite não encontrado", + "body": "Este link não corresponde a um convite válido. Verifique a URL ou peça à pessoa que convidou para enviar um novo." + }, + "used": { + "title": "Convite já utilizado", + "body": "Este convite já foi aceito. Se não foi você, peça ao administrador da conta para enviar um novo link." + }, + "expired": { + "title": "Convite expirado", + "body": "Este convite expirou. Peça ao administrador da conta para enviar um novo — leva alguns segundos para gerar." + }, + "server_error": { + "title": "Algo deu errado", + "body": "Não conseguimos verificar este convite agora. Tente atualizar a página em instantes." + } + }, + "conflictFallback": "Você já está em outra conta. Entre com um e-mail diferente para participar desta.", + "acceptError": "Falha ao aceitar o convite", + "welcomeToast": "Bem-vindo à equipe", + "serverUnreachable": "Não foi possível conectar ao servidor", + "signOutError": "Não foi possível sair. Tente atualizar a página." + }, + "Agents": { + "title": "Agentes de IA", + "subtitle": "Seu agente de IA com chave própria — configure-o e depois teste-o no playground antes que ele responda aos clientes na caixa de entrada.", + "tabPlayground": "Playground", + "tabSetup": "Configuração", + "tabUsage": "Uso" + }, + "AiPlayground": { + "notConfiguredToast": "Nenhum agente configurado ainda — conclua a Configuração primeiro.", + "noReplyToast": "Não foi possível obter uma resposta.", + "unreachableToast": "Não foi possível conectar ao agente.", + "headerTitle": "Playground", + "headerSubtitle": "— teste as respostas como se você fosse um cliente", + "reset": "Redefinir", + "emptyTitle": "Envie uma mensagem para ver como seu agente responderia.", + "emptyDescription": "Ele usa sua base de conhecimento e se comporta exatamente como o bot de resposta automática — incluindo a transferência.", + "goToSetup": "Ainda não configurou? Ir para a Configuração", + "handoffLabel": "Transferiria para um humano aqui", + "thinking": "Pensando…", + "composerPlaceholder": "Digite uma mensagem de cliente…" + }, + "AiUsage": { + "loadError": "Falha ao carregar o uso", + "tokenUsage": "Uso de tokens", + "description": "Tokens gastos na sua chave de provedor por rascunhos e pelo bot de resposta automática. Apenas contagens — nenhum conteúdo de mensagem é armazenado aqui.", + "lastDays": "Últimos {w} dias", + "noUsage": "Nenhum uso de IA nos últimos {n} dias ainda.", + "noUsageHint": "Isso é preenchido conforme o assistente cria rascunhos e responde automaticamente.", + "totalTokens": "Total de tokens", + "llmCalls": "Chamadas de LLM", + "autoReply": "Resposta automática", + "drafts": "Rascunhos", + "tokensPerDay": "Tokens por dia", + "byModel": "Por modelo", + "modelUsage": "{tokens} tok · {calls, plural, one {# chamada} other {# chamadas}}", + "truncatedNote": "Mostrando uma janela parcial — o uso é alto o suficiente para que apenas os registros mais recentes sejam resumidos aqui." + }, + "FlowsHeader": { + "backToFlows": "Voltar para Fluxos", + "flowNamePlaceholder": "Nome do fluxo", + "flowNameLabel": "Nome do fluxo", + "unsavedChanges": "Alterações não salvas — clique em Salvar para persistir", + "edited": "Editado", + "runs": "Execuções", + "delete": "Excluir", + "pause": "Pausar", + "activate": "Ativar", + "fixIssuesBeforeActivating": "Corrija os problemas abaixo antes de ativar", + "save": "Salvar", + "descriptionPlaceholder": "Adicione uma breve descrição (interna — os clientes não veem isso)", + "descriptionLabel": "Descrição do fluxo", + "statusDraft": "Rascunho", + "statusActive": "Ativo", + "statusArchived": "Arquivado" + }, + "InteractiveBuilder": { + "replyButtons": "Botões de resposta", + "list": "Lista", + "body": "Corpo", + "bodyPlaceholder": "O que o cliente lê acima das opções", + "headerOptional": "Cabeçalho (opcional)", + "footerOptional": "Rodapé (opcional)", + "showReplyIds": "Mostrar IDs de resposta (avançado)", + "preview": "Pré-visualização", + "buttonsCount": "Botões ({count}/{max})", + "buttonLabel": "Rótulo do botão", + "idPlaceholder": "id", + "addButton": "Adicionar botão", + "listButtonLabel": "Rótulo do botão da lista", + "rowsCount": "Linhas ({count}/{max})", + "sectionTitleOptional": "Título da seção (opcional)", + "rowTitle": "Título da linha", + "descriptionOptional": "Descrição (opcional)", + "addRow": "Adicionar linha", + "addSection": "Adicionar seção" + }, + "InteractivePreview": { + "messageBody": "Corpo da mensagem…", + "button": "Botão", + "menu": "Menu" + }, + "FlowTemplates": { + "welcome_menu": { + "name": "Menu de boas-vindas", + "description": "Cumprimente clientes que digitam uma palavra-chave e direcione-os ao atendente certo com base em serem novos ou já existentes." + }, + "faq_bot": { + "name": "Bot de perguntas frequentes", + "description": "Responda perguntas comuns automaticamente. O cliente escolhe um tópico em uma lista; o bot responde e encerra." + }, + "lead_capture": { + "name": "Captura de leads", + "description": "Cumprimente novos contatos, capture nome + e-mail + empresa e depois transfira para vendas com as respostas na nota." + } + }, + "AutomationTemplates": { + "welcome_message": { + "name": "Mensagem de boas-vindas", + "description": "Responder automaticamente com uma saudação a contatos de primeira vez." + }, + "out_of_office": { + "name": "Fora do expediente", + "description": "Responder automaticamente fora do horário para que ninguém fique sem resposta." + }, + "lead_qualifier": { + "name": "Qualificador de leads", + "description": "Fazer perguntas de qualificação para filtrar leads recebidos." + }, + "follow_up_reminder": { + "name": "Lembrete de acompanhamento", + "description": "Enviar um lembrete se um contato não responder em 24 horas." + } + }, + "Themes": { + "violet": { + "name": "Violeta", + "description": "O padrão — confiante e um pouco descontraído." + }, + "emerald": { + "name": "Esmeralda", + "description": "Com clima de crescimento, remete a mensagens sem copiar o verde do WhatsApp." + }, + "cobalt": { + "name": "Cobalto", + "description": "Azul limpo de SaaS B2B — calmo e com cara de produto." + }, + "amber": { + "name": "Âmbar", + "description": "Quente e amigável — combina com equipes de pequenas e médias empresas." + }, + "rose": { + "name": "Rosa", + "description": "Ousado e moderno — D2C, economia de criadores, lifestyle." + } + }, + "Common": { + "loading": "Carregando..." + } +} diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index d2a1784fb7..8a123c1905 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -14,8 +14,10 @@ import { CardTitle, } from "@/components/ui/card"; import { MessageSquare, CheckCircle, ArrowLeft } from "lucide-react"; +import { useTranslations } from "next-intl"; export default function ForgotPasswordPage() { + const t = useTranslations("ForgotPassword"); const [email, setEmail] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); @@ -50,12 +52,12 @@ export default function ForgotPasswordPage() { - Check your email + {t("checkEmailTitle")} - We've sent a password reset link to{" "} - {email}. Please check your - inbox. + {t("checkEmailBodyBefore")}{" "} + {email} + {t("checkEmailBodyAfter")} @@ -64,7 +66,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 + {t("backToSignIn")} @@ -80,9 +82,9 @@ export default function ForgotPasswordPage() {
- Reset password + {t("title")} - Enter your email and we'll send you a reset link + {t("description")} @@ -95,7 +97,7 @@ export default function ForgotPasswordPage() {
- {loading ? "Sending..." : "Send reset link"} + {loading ? t("sending") : t("sendResetLink")} @@ -122,7 +124,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 + {t("backToSignIn")} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 56985cdb2f..ca5f8abd18 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -15,6 +15,7 @@ import { CardTitle, } from "@/components/ui/card"; import { MessageSquare, CheckCircle, UsersRound } from "lucide-react"; +import { useTranslations } from "next-intl"; // `useSearchParams` opts the component out of static prerendering // unless wrapped in Suspense — same pattern as /login. @@ -27,6 +28,7 @@ export default function SignupPage() { } function SignupPageInner() { + const t = useTranslations("Signup"); const searchParams = useSearchParams(); // When the user lands here from `/join/` we carry the // invite token in the query so it survives the signup → email @@ -49,12 +51,12 @@ function SignupPageInner() { setError(null); if (password !== confirmPassword) { - setError("Passwords do not match"); + setError(t("passwordsDoNotMatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("passwordTooShort")); return; } @@ -98,12 +100,12 @@ function SignupPageInner() {
- Check your email + {t("checkEmailTitle")} - We've sent a confirmation link to{" "} - {email}. Please check your - inbox and click the link to verify your account. + {t("checkEmailBodyBefore")}{" "} + {email} + {t("checkEmailBodyAfter")} @@ -118,7 +120,7 @@ function SignupPageInner() { variant="outline" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground" > - Back to sign in + {t("backToSignIn")} @@ -139,12 +141,10 @@ function SignupPageInner() { )} - {inviteToken ? "Create account & join" : "Create account"} + {inviteToken ? t("titleInvite") : t("title")} - {inviteToken - ? "Verify your email, then accept the invitation to join your team." - : "Get started with CRM Template for WhatsApp"} + {inviteToken ? t("descriptionInvite") : t("description")} @@ -157,12 +157,12 @@ function SignupPageInner() {
setFullName(e.target.value)} required @@ -172,7 +172,7 @@ function SignupPageInner() {
setPassword(e.target.value)} required @@ -202,12 +202,12 @@ function SignupPageInner() {
setConfirmPassword(e.target.value)} required @@ -220,12 +220,12 @@ function SignupPageInner() { disabled={loading} className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50" > - {loading ? "Creating account..." : "Create account"} + {loading ? t("creatingAccount") : t("createAccount")}

- Already have an account?{" "} + {t("alreadyHaveAccount")}{" "} - Sign in + {t("signIn")}

diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx index c3774849a4..35cf1f9bd3 100644 --- a/src/app/(dashboard)/agents/page.tsx +++ b/src/app/(dashboard)/agents/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { Bot, Sparkles, Settings2, BarChart3 } from 'lucide-react'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { AiPlayground } from '@/components/agents/ai-playground'; @@ -12,6 +13,7 @@ import { canEditSettings } from '@/lib/auth/roles'; type Tab = 'playground' | 'setup' | 'usage'; export default function AgentsPage() { + const t = useTranslations('Agents'); const { accountRole } = useAuth(); const canViewUsage = accountRole ? canEditSettings(accountRole) : false; const [tab, setTab] = useState('playground'); @@ -41,12 +43,11 @@ export default function AgentsPage() {

- AI Agents + {t('title')}

- Your bring-your-own-key AI agent — set it up, then test it in the - playground before it replies to customers in the inbox. + {t('subtitle')}

{decided && ( @@ -57,14 +58,14 @@ export default function AgentsPage() { > - Playground + {t('tabPlayground')} - Setup + {t('tabSetup')} {canViewUsage && ( - Usage + {t('tabUsage')} )} diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx index 4a87a8801c..8fabe96ebc 100644 --- a/src/app/(dashboard)/automations/page.tsx +++ b/src/app/(dashboard)/automations/page.tsx @@ -40,7 +40,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { AUTOMATION_TEMPLATES, type TemplateSlug } from "@/lib/automations/templates" +import { type TemplateSlug } from "@/lib/automations/templates" import { triggerMeta, formatRelative } from "@/lib/automations/trigger-meta" import { cn } from "@/lib/utils" @@ -62,6 +62,7 @@ export default function AutomationsPage() { const router = useRouter() const canCreate = useCan("send-messages") const t = useTranslations("Automations.list") + const tmplT = useTranslations("AutomationTemplates") const [automations, setAutomations] = useState(null) const [error, setError] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) @@ -183,7 +184,6 @@ export default function AutomationsPage() {

{t("templatesTitle")}

{TEMPLATE_ORDER.map((slug) => { - const t = AUTOMATION_TEMPLATES[slug] const Icon = TEMPLATE_ICON[slug] return (
-
{t.name}
-

{t.description}

+
{tmplT(`${slug}.name`)}
+

{tmplT(`${slug}.description`)}

) })} @@ -299,7 +299,7 @@ function AutomationCard({ {automation.name} {automation.is_active && ( - + @@ -336,7 +336,7 @@ function AutomationCard({ diff --git a/src/app/(dashboard)/broadcasts/page.tsx b/src/app/(dashboard)/broadcasts/page.tsx index ca89c8106b..dc837c8a1a 100644 --- a/src/app/(dashboard)/broadcasts/page.tsx +++ b/src/app/(dashboard)/broadcasts/page.tsx @@ -157,7 +157,7 @@ export default function BroadcastsPage() { {anySending && (
diff --git a/src/app/(dashboard)/contacts/page.tsx b/src/app/(dashboard)/contacts/page.tsx index 0facba6932..d8f1dc1f63 100644 --- a/src/app/(dashboard)/contacts/page.tsx +++ b/src/app/(dashboard)/contacts/page.tsx @@ -538,7 +538,7 @@ export default function ContactsPage() { indeterminate={!allOnPageSelected && someOnPageSelected} onCheckedChange={toggleSelectAll} disabled={contacts.length === 0} - aria-label="Select all contacts on this page" + aria-label={t('selectAllAria')} /> {t('tableColumns.name')} diff --git a/src/app/(dashboard)/dashboard-shell.tsx b/src/app/(dashboard)/dashboard-shell.tsx index 06acfde102..61c823a00b 100644 --- a/src/app/(dashboard)/dashboard-shell.tsx +++ b/src/app/(dashboard)/dashboard-shell.tsx @@ -6,6 +6,7 @@ import { AuthProvider, useAuth } from "@/hooks/use-auth"; import { Sidebar } from "@/components/layout/sidebar"; import { Header } from "@/components/layout/header"; import { PresenceHeartbeat } from "@/components/presence/presence-heartbeat"; +import { useTranslations } from "next-intl"; // Auth-gated dashboard shell. Extracted from the layout so the layout // itself can stay a server component and export metadata (noindex) — @@ -14,6 +15,7 @@ import { PresenceHeartbeat } from "@/components/presence/presence-heartbeat"; function DashboardShellInner({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const router = useRouter(); + const t = useTranslations("Common"); // Sidebar drawer state — only used on mobile. On lg+ the sidebar is // always visible and this stays at `false` (ignored by the component). @@ -31,7 +33,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
-

Loading...

+

{t("loading")}

); diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx index 0753f5a707..97bafd71f5 100644 --- a/src/app/(dashboard)/flows/page.tsx +++ b/src/app/(dashboard)/flows/page.tsx @@ -86,6 +86,7 @@ export default function FlowsPage() { const router = useRouter(); const canCreate = useCan("send-messages"); const t = useTranslations("Flows.list"); + const tmplT = useTranslations("FlowTemplates"); const [flows, setFlows] = useState([]); const [loading, setLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); @@ -274,10 +275,10 @@ export default function FlowsPage() { > - {template.name} + {tmplT(`${template.slug}.name`)} - {template.description} + {tmplT(`${template.slug}.description`)} {t("nodeCount", { count: template.node_count })} diff --git a/src/app/(dashboard)/notifications/page.tsx b/src/app/(dashboard)/notifications/page.tsx index c658fa9b92..10ef85a62c 100644 --- a/src/app/(dashboard)/notifications/page.tsx +++ b/src/app/(dashboard)/notifications/page.tsx @@ -10,6 +10,7 @@ import { formatDistanceToNow } from "date-fns"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; // Icon per notification type. Only one type exists today // (conversation_assigned) but this keeps future types a one-line add. @@ -18,6 +19,7 @@ const TYPE_ICON: Record = { }; export default function NotificationsPage() { + const t = useTranslations("Notifications"); const router = useRouter(); const { accountId } = useAuth(); const [notifications, setNotifications] = useState( @@ -104,11 +106,11 @@ export default function NotificationsPage() { .eq("id", id) .is("read_at", null); if (updateErr) { - toast.error("Failed to mark notification as read"); + toast.error(t("markReadError")); load(); } }, - [load], + [load, t], ); const handleClick = useCallback( @@ -137,17 +139,17 @@ export default function NotificationsPage() { .is("read_at", null); setMarkingAll(false); if (updateErr) { - toast.error("Failed to mark all as read"); + toast.error(t("markAllError")); load(); } - }, [unreadIds.length, load]); + }, [unreadIds.length, load, t]); if (error) { return (

{error}

); @@ -165,9 +167,9 @@ export default function NotificationsPage() {
-

Notifications

+

{t("title")}

- Conversations other teammates assign to you show up here. + {t("subtitle")}

@@ -191,11 +193,10 @@ export default function NotificationsPage() {

- No notifications yet + {t("emptyTitle")}

- You'll see an alert here when someone assigns you a - conversation. + {t("emptyBody")}

) : ( @@ -241,7 +242,7 @@ export default function NotificationsPage() { {isUnread && ( )} diff --git a/src/app/join/[token]/page.tsx b/src/app/join/[token]/page.tsx index 32e09bbc5a..be6e6e9787 100644 --- a/src/app/join/[token]/page.tsx +++ b/src/app/join/[token]/page.tsx @@ -26,6 +26,7 @@ import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { toast } from 'sonner'; +import { useTranslations } from 'next-intl'; import { AlertTriangle, CheckCircle, @@ -65,32 +66,8 @@ interface PeekFail { } type PeekResult = PeekOk | PeekFail; -const ROLE_LABEL: Record = { - admin: 'Admin', - agent: 'Agent', - viewer: 'Viewer', -}; - -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.', - }, - 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.', - }, - 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.', - }, - server_error: { - title: 'Something went wrong', - body: 'We couldn’t verify this invitation right now. Try refreshing the page in a moment.', - }, -}; - export default function JoinPage() { + const t = useTranslations('Join'); const params = useParams<{ token: string }>(); const token = params?.token; @@ -161,7 +138,7 @@ export default function JoinPage() { return () => { cancelled = true; }; - }, [token]); + }, [token, t]); const handleAccept = useCallback(async () => { if (!token) return; @@ -181,26 +158,23 @@ export default function JoinPage() { // a clear next-action (sign out → use different email) // rather than a 3-second toast. if (res.status === 409) { - setConflictMessage( - payload.error || - 'You are already in another account. Sign in with a different email to join this one.', - ); + setConflictMessage(payload.error || t('conflictFallback')); } else { - toast.error(payload.error || 'Failed to accept invitation'); + toast.error(payload.error || t('acceptError')); } setAccepting(false); return; } - toast.success('Welcome to the team'); + toast.success(t('welcomeToast')); // 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(t('serverUnreachable')); setAccepting(false); } - }, [token]); + }, [token, t]); const handleSignOutAndRetry = useCallback(async () => { setSigningOut(true); @@ -212,10 +186,10 @@ 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(t('signOutError')); setSigningOut(false); } - }, []); + }, [t]); // ----- Loading state (peek pending OR auth not yet resolved) ----- if (peek === null || authedUserId === undefined) { @@ -223,7 +197,7 @@ export default function JoinPage() { -

Verifying invitation…

+

{t('verifying')}

); @@ -231,7 +205,10 @@ export default function JoinPage() { // ----- Peek failed ----- if (!peek.ok) { - const copy = FAIL_COPY[peek.reason]; + const copy = { + title: t(`fail.${peek.reason}.title`), + body: t(`fail.${peek.reason}.body`), + }; return ( @@ -257,14 +234,14 @@ export default function JoinPage() { onClick={loadPeekAndAuth} className="w-full bg-primary text-primary-foreground hover:bg-primary/90" > - Try again + {t('tryAgain')} @@ -272,7 +249,7 @@ export default function JoinPage() { <> @@ -280,7 +257,7 @@ export default function JoinPage() { variant="outline" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground" > - Sign in + {t('signIn')} @@ -297,22 +274,22 @@ export default function JoinPage() {
- You're invited to{' '} + {t('invitedTo')}{' '} {peek.account_name} - You'll join as{' '} + {t('joinAs')}{' '} - {ROLE_LABEL[peek.role]} + {t(`role.${peek.role}`)} - . Link valid until{' '} - {new Date(peek.expires_at).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', + {t('linkValidUntil', { + date: new Date(peek.expires_at).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }), })} - . ); @@ -332,19 +309,19 @@ export default function JoinPage() { {accepting ? ( <> - Accepting… + {t('accepting')} ) : ( <> - Accept invitation + {t('acceptInvitation')} )}

- Accepting moves your login into{' '} - {peek.account_name}. Your - empty personal account from signup will be cleaned up. + {t('acceptHelpBefore')}{' '} + {peek.account_name} + {t('acceptHelpAfter')}

@@ -363,7 +340,7 @@ export default function JoinPage() { - Can't join {peek.account_name} with this account + {t('conflictTitle', { name: peek.account_name })} {conflictMessage} @@ -371,11 +348,9 @@ export default function JoinPage() {

- To join{' '} - {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. + {t('conflictHelpBefore')}{' '} + {peek.account_name} + {t('conflictHelpAfter')}

@@ -384,7 +359,7 @@ export default function JoinPage() { onClick={() => setConflictMessage(null)} className="border-border text-popover-foreground hover:bg-muted" > - Stay signed in + {t('staySignedIn')} @@ -414,7 +389,7 @@ export default function JoinPage() { @@ -422,7 +397,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 + {t('alreadyHaveAccount')} diff --git a/src/components/agents/ai-playground.tsx b/src/components/agents/ai-playground.tsx index 6033cc26c5..9a9c2373f8 100644 --- a/src/components/agents/ai-playground.tsx +++ b/src/components/agents/ai-playground.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { Bot, RotateCcw, Send, Loader2, UserCircle2, ArrowRight } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -14,6 +15,7 @@ interface Turn { } export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { + const tr = useTranslations('AiPlayground'); const [turns, setTurns] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); @@ -43,9 +45,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(tr('notConfiguredToast')); } else { - toast.error(data.error ?? "Couldn't get a reply."); + toast.error(data.error ?? tr('noReplyToast')); } // Roll the unsent user turn back so the transcript stays clean. setTurns(turns); @@ -64,7 +66,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { }, ]); } catch { - toast.error("Couldn't reach the agent."); + toast.error(tr('unreachableToast')); setTurns(turns); setInput(text); } finally { @@ -85,9 +87,9 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) {
- Playground + {tr('headerTitle')} - — test replies as if you were a customer + {tr('headerSubtitle')}
@@ -106,10 +108,9 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { {turns.length === 0 && (
-

Send a message to see how your agent would reply.

+

{tr('emptyTitle')}

- It uses your knowledge base and behaves exactly like the - auto-reply bot — including handoff. + {tr('emptyDescription')}

{onGoToSetup && ( )}
@@ -152,7 +153,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { )} > - Would hand off to a human here + {tr('handoffLabel')}

)}
@@ -165,7 +166,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { {sending && (
- Thinking… + {tr('thinking')}
)}
@@ -176,7 +177,7 @@ export function AiPlayground({ onGoToSetup }: { onGoToSetup?: () => void }) { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder="Type a customer message…" + placeholder={tr('composerPlaceholder')} 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..249fee2f49 100644 --- a/src/components/agents/ai-usage.tsx +++ b/src/components/agents/ai-usage.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { BarChart3, Bot, PencilLine } from 'lucide-react'; import { useAuth } from '@/hooks/use-auth'; @@ -54,6 +55,7 @@ const WINDOWS = [7, 30, 90] as const; * `GET /api/ai/usage` route. Renders nothing for non-admins. */ export function AiUsageCard() { + const t = useTranslations('AiUsage'); const { accountId, accountRole, profileLoading } = useAuth(); const canView = accountRole ? canEditSettings(accountRole) : false; @@ -70,18 +72,18 @@ 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 ?? t('loadError')); setData(null); return; } setData(json as UsageResponse); } catch { - toast.error('Failed to load usage'); + toast.error(t('loadError')); setData(null); } finally { setLoading(false); } - }, []); + }, [t]); useEffect(() => { if (!canView || !accountId) return; @@ -105,11 +107,10 @@ export function AiUsageCard() {
- Token usage + {t('tokenUsage')} - Tokens spent on your provider key by drafts and the auto-reply - bot. Counts only — no message content is stored here. + {t('description')}
onConfigChange({ ...config, schedule: e.target.value }) @@ -1133,7 +1133,7 @@ function StepRenderer({ variant="ghost" size="icon" disabled={index === 0} - aria-label="Move up" + aria-label={t("moveUp")} onClick={() => props.moveStepAt(path, -1)} > @@ -1142,7 +1142,7 @@ function StepRenderer({ variant="ghost" size="icon" disabled={index === total - 1} - aria-label="Move down" + aria-label={t("moveDown")} onClick={() => props.moveStepAt(path, 1)} > @@ -1154,7 +1154,7 @@ function StepRenderer({ onClick={() => props.deleteStepAt(path)} > - {t("delete", { defaultValue: "Delete" })} + {t("delete")}
diff --git a/src/components/broadcasts/step2-select-audience.tsx b/src/components/broadcasts/step2-select-audience.tsx index b832903a63..07420449d7 100644 --- a/src/components/broadcasts/step2-select-audience.tsx +++ b/src/components/broadcasts/step2-select-audience.tsx @@ -429,11 +429,11 @@ export function Step2SelectAudience({ {/* Audience Summary */}
-

Audience Summary

+

{t('selectAudience.summaryTitle')}

{loadingCount ? (
- Calculating… + {t('selectAudience.calculating')}
) : estimatedCount !== null ? (
@@ -441,11 +441,11 @@ export function Step2SelectAudience({ {estimatedCount.toLocaleString()} - estimated recipients + {t('selectAudience.estimatedRecipients')}
) : (

- Select an audience type to see the estimate. + {t('selectAudience.selectTypeHint')}

)}
diff --git a/src/components/broadcasts/step3-personalize.tsx b/src/components/broadcasts/step3-personalize.tsx index 990aeacdf3..22e01e6f2c 100644 --- a/src/components/broadcasts/step3-personalize.tsx +++ b/src/components/broadcasts/step3-personalize.tsx @@ -322,7 +322,17 @@ export function Step3Personalize({ } > - + + {(value) => + value === 'static' + ? t('personalize.typeStatic') + : value === 'field' + ? t('personalize.typeContact') + : value === 'custom_field' + ? t('personalize.typeCustom') + : null + } + {t('personalize.typeStatic')} @@ -344,7 +354,7 @@ export function Step3Personalize({ onChange={(e) => updateVariable(key, { value: e.target.value }) } - placeholder="Enter value..." + placeholder={t('personalize.enterValuePlaceholder')} className="border-border bg-muted text-foreground placeholder:text-muted-foreground" /> ) : mapping.type === 'field' ? ( diff --git a/src/components/broadcasts/step4-schedule-send.tsx b/src/components/broadcasts/step4-schedule-send.tsx index c4f8e68e39..26e962c620 100644 --- a/src/components/broadcasts/step4-schedule-send.tsx +++ b/src/components/broadcasts/step4-schedule-send.tsx @@ -125,7 +125,7 @@ export function Step4ScheduleSend({

{audienceLabel}

-

Estimated Reach

+

{t('scheduleSend.estimatedReach')}

{loadingReach ? ( @@ -138,7 +138,7 @@ export function Step4ScheduleSend({
-

Language

+

{t('scheduleSend.language')}

{template.language ?? 'en_US'}

@@ -201,7 +201,7 @@ export function Step4ScheduleSend({ - Confirm Broadcast + {t('scheduleSend.confirmTitle')} You are about to send this broadcast to{' '} {estimatedReach.toLocaleString()}{' '} diff --git a/src/components/contacts/contact-detail-view.tsx b/src/components/contacts/contact-detail-view.tsx index 5d50424de4..fd6eb82c42 100644 --- a/src/components/contacts/contact-detail-view.tsx +++ b/src/components/contacts/contact-detail-view.tsx @@ -366,7 +366,7 @@ export function ContactDetailView({ toast.success(t('toastTemplateSent', { name: template.name })); } catch (err) { const reason = err instanceof Error ? err.message : 'network error'; - toast.error(`Failed to send template: ${reason}`); + toast.error(t('toastTemplateFailed', { reason })); } finally { setSendingTemplate(false); } @@ -468,7 +468,7 @@ export function ContactDetailView({ value="tags" className="data-active:bg-muted data-active:text-primary text-muted-foreground" > - {t('tabs.tags', { fallback: 'Tags' })} + {t('tabs.tags')} - + + {(value) => + value === 'keyword' + ? t('triggerKeywordTitle') + : value === 'first_inbound_message' + ? t('triggerFirstInboundTitle') + : value === 'manual' + ? t('triggerManualTitle') + : null + } + diff --git a/src/components/flows/flow-editor-shell.tsx b/src/components/flows/flow-editor-shell.tsx index 384417dce1..ec82ba5102 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) {
- + + {(value) => + value === "var" + ? t("capturedVariable") + : value === "tag" + ? t("contactHasTag") + : value === "contact_field" + ? t("contactField") + : null + } + {t("capturedVariable")} @@ -710,7 +720,19 @@ function ConditionForm({ } > - + + {(value) => + value === "present" + ? t("isPresent") + : value === "absent" + ? t("isAbsent") + : value === "equals" + ? t("equals") + : value === "contains" + ? t("contains") + : null + } + {t("isPresent")} @@ -789,7 +811,11 @@ function SetTagForm({ } > - + + {(value) => + value === "remove" ? t("removeTag") : t("addTag") + } + {t("addTag")} @@ -961,7 +987,15 @@ function SendMediaForm({ }} > - + + {(value) => + value === "video" + ? t("videoLabel") + : value === "document" + ? t("documentLabel") + : t("imageLabel") + } + {t("imageLabel")} diff --git a/src/components/flows/header.tsx b/src/components/flows/header.tsx index 8d7df821c9..7d00b54770 100644 --- a/src/components/flows/header.tsx +++ b/src/components/flows/header.tsx @@ -23,6 +23,7 @@ */ import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { ArrowLeft, CircleDot, @@ -44,6 +45,7 @@ import { export function EditorHeader() { const router = useRouter(); + const t = useTranslations("FlowsHeader"); const { flow, state, @@ -64,8 +66,8 @@ export function EditorHeader() { {state.status === "active" ? ( ) : ( )}
@@ -166,8 +164,8 @@ export function EditorHeader() { onChange={(e) => setState((s) => ({ ...s, description: e.target.value })) } - placeholder="Add a short description (internal — customers don't see this)" - aria-label="Flow description" + placeholder={t("descriptionPlaceholder")} + aria-label={t("descriptionLabel")} className="w-full max-w-[78ch] rounded-md border border-transparent bg-transparent px-2 py-1 text-[13px] text-muted-foreground outline-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/50 focus:border-primary focus:bg-transparent focus:text-foreground" /> @@ -175,20 +173,21 @@ export function EditorHeader() { } function StatusChip({ status }: { status: BuilderState["status"] }) { + const t = useTranslations("FlowsHeader"); const cfg = { draft: { // Neutral, not amber — amber is reserved for the adjacent // "Edited" dirty signal, so the two don't read as the same alert. cls: "border-border bg-muted text-muted-foreground", - label: "Draft", + label: t("statusDraft"), }, active: { cls: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300", - label: "Active", + label: t("statusActive"), }, archived: { cls: "border-border bg-muted/50 text-muted-foreground", - label: "Archived", + label: t("statusArchived"), }, }[status]; return ( diff --git a/src/components/inbox/message-composer.tsx b/src/components/inbox/message-composer.tsx index 7c13b2102d..df9a129686 100644 --- a/src/components/inbox/message-composer.tsx +++ b/src/components/inbox/message-composer.tsx @@ -269,15 +269,15 @@ export function MessageComposer({ const data = await res.json().catch(() => ({})); if (!res.ok) { if (data.code === "ai_not_configured") { - toast.error("AI isn't set up yet — enable it in Settings → AI Assistant."); + toast.error(t("aiNotConfigured")); } else { - toast.error(data.error ?? "Couldn't draft a reply."); + toast.error(data.error ?? t("draftFailed")); } return; } const draftText = typeof data.draft === "string" ? data.draft.trim() : ""; if (!draftText) { - toast.error("The assistant didn't return a reply."); + toast.error(t("assistantNoReply")); return; } setText(draftText); @@ -292,11 +292,11 @@ export function MessageComposer({ } }); } catch { - toast.error("Couldn't reach the AI assistant."); + toast.error(t("aiUnreachable")); } finally { setDrafting(false); } - }, [drafting, conversationId, adjustHeight]); + }, [drafting, conversationId, adjustHeight, t]); // ---- Interactive message + quick replies -------------------------- @@ -431,7 +431,7 @@ export function MessageComposer({ }); if (file.size === 0) return; // cancelled / empty take if (file.size > MEDIA_MAX_BYTES_BY_KIND.audio) { - toast.error("Recording is too long (over 16 MB)."); + toast.error(t("recordingTooLong")); return; } setBusy(true); @@ -445,13 +445,13 @@ export function MessageComposer({ setBusy(false); } }, - [removeStaged], + [removeStaged, t], ); const startRecording = useCallback(async () => { if (inputsDisabled || busy || recording) return; if (!navigator.mediaDevices?.getUserMedia || typeof AudioContext === "undefined") { - toast.error("Voice recording isn't supported in this browser."); + toast.error(t("voiceUnsupported")); return; } try { @@ -478,9 +478,9 @@ export function MessageComposer({ } catch { void recorderRef.current?.stop().catch(() => {}); recorderRef.current = null; - toast.error("Microphone access denied or unavailable."); + toast.error(t("micDenied")); } - }, [inputsDisabled, busy, recording, finalizeRecording]); + }, [inputsDisabled, busy, recording, finalizeRecording, t]); const stopRecording = useCallback(() => { clearTimer(); diff --git a/src/components/inbox/message-thread.tsx b/src/components/inbox/message-thread.tsx index 6ff3c6ad27..25b6b6e603 100644 --- a/src/components/inbox/message-thread.tsx +++ b/src/components/inbox/message-thread.tsx @@ -482,7 +482,7 @@ export function MessageThread({ if (!res.ok) { const reason = payload?.error || `HTTP ${res.status}`; console.error("Failed to send message:", reason); - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); // Mark the optimistic bubble as failed so the user sees what happened onUpdateMessage(tempId, { status: "failed" }); return; @@ -495,11 +495,11 @@ export function MessageThread({ } catch (err) { console.error("Failed to send message:", err); const reason = err instanceof Error ? err.message : "network error"; - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); } }, - [conversation, onNewMessage, onUpdateMessage] + [conversation, onNewMessage, onUpdateMessage, t] ); const handleSendMedia = useCallback( @@ -548,7 +548,7 @@ export function MessageThread({ if (!res.ok) { const reason = data?.error || `HTTP ${res.status}`; console.error("Failed to send media:", reason); - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); // The upload never reached the recipient — GC the orphaned // object rather than leaving it in the public bucket forever. @@ -560,12 +560,12 @@ export function MessageThread({ } catch (err) { console.error("Failed to send media:", err); const reason = err instanceof Error ? err.message : "network error"; - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); void deleteAccountMedia(CHAT_MEDIA_BUCKET, payload.path).catch(() => {}); } }, - [conversation, onNewMessage, onUpdateMessage], + [conversation, onNewMessage, onUpdateMessage, t], ); const handleSendInteractive = useCallback( @@ -605,7 +605,7 @@ export function MessageThread({ if (!res.ok) { const reason = data?.error || `HTTP ${res.status}`; console.error("Failed to send interactive message:", reason); - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); return; } @@ -614,11 +614,11 @@ export function MessageThread({ } catch (err) { console.error("Failed to send interactive message:", err); const reason = err instanceof Error ? err.message : "network error"; - toast.error(`Failed to send: ${reason}`); + toast.error(t("toastSendFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); } }, - [conversation, onNewMessage, onUpdateMessage], + [conversation, onNewMessage, onUpdateMessage, t], ); const handleStatusChange = useCallback( @@ -694,7 +694,7 @@ export function MessageThread({ if (!res.ok) { const reason = payload?.error || `HTTP ${res.status}`; console.error("Failed to send template:", reason); - toast.error(`Failed to send template: ${reason}`); + toast.error(t("toastSendTemplateFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); return; } @@ -703,11 +703,11 @@ export function MessageThread({ } catch (err) { console.error("Failed to send template:", err); const reason = err instanceof Error ? err.message : "network error"; - toast.error(`Failed to send template: ${reason}`); + toast.error(t("toastSendTemplateFailed", { reason })); onUpdateMessage(tempId, { status: "failed" }); } }, - [conversation, onNewMessage, onUpdateMessage], + [conversation, onNewMessage, onUpdateMessage, t], ); // Build a quick id → Message map so reply quotes can be rendered without @@ -764,7 +764,7 @@ export function MessageThread({ return; } if (messageId.startsWith("temp-")) { - toast.error("Wait for the message to finish sending"); + toast.error(t("toastWaitForSend")); return; } @@ -810,11 +810,11 @@ export function MessageThread({ } } catch (err) { const reason = err instanceof Error ? err.message : "network error"; - toast.error(`Reaction failed: ${reason}`); + toast.error(t("toastReactionFailed", { reason })); setReactions(snapshot); } }, - [conversation, user?.id], + [conversation, user?.id, t], ); const handleAssignChange = useCallback( @@ -829,13 +829,13 @@ export function MessageThread({ if (error) { console.error("Failed to update assignment:", error); - toast.error("Failed to update assignment"); + toast.error(t("toastAssignmentFailed")); return; } onAssignChange(conversation.id, agentId); }, - [conversation, onAssignChange], + [conversation, onAssignChange, t], ); // Empty state — same WhatsApp-style doodle background as the active diff --git a/src/components/interactive/interactive-builder.tsx b/src/components/interactive/interactive-builder.tsx index 4282bdf31a..be2baf911e 100644 --- a/src/components/interactive/interactive-builder.tsx +++ b/src/components/interactive/interactive-builder.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -71,6 +72,7 @@ export function InteractiveBuilder({ onChange, showPreview = true, }: InteractiveBuilderProps) { + const t = useTranslations("InteractiveBuilder"); const [advanced, setAdvanced] = useState(false); const validation = validateInteractivePayload(value); @@ -94,29 +96,29 @@ export function InteractiveBuilder({
switchKind("buttons")} /> switchKind("list")} />
- +