diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 3786abe3..2275b3e2 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,623 +1,623 @@ -{ - "twoFactorAuth": { - "ariaLabel": "Two-factor authentication setup", - "stepsLabel": "Setup steps", - "title": "Enable Two-Factor Authentication", - "description": "Protect your account with an authenticator app. You will need to scan a QR code to get started.", - "enableButton": "Enable 2FA", - "settingUp": "Setting up…", - "scanTitle": "Scan with your authenticator app", - "scanDescription": "Use Google Authenticator, Authy, or any TOTP-compatible app to scan the code below.", - "qrCodeAlt": "TOTP QR code — scan with your authenticator app", - "qrCodeSkeletonLabel": "Generating QR code…", - "manualKeyLabel": "Or enter key manually", - "manualKeyAria": "Manual setup key: {key}", - "codeInputLabel": "Enter 6-digit code", - "codeInputPlaceholder": "000000", - "verifyButton": "Verify & Enable", - "verifying": "Verifying…", - "successTitle": "Two-Factor Authentication Enabled", - "successDescription": "Your account is now protected. You will be prompted for a code on each login.", - "error": { - "setupFailed": "Failed to start 2FA setup. Please try again.", - "invalidCode": "Invalid code. Please try again.", - "codeLength": "Please enter the 6-digit code from your authenticator app." - } - }, - "localeSwitcher": { - "label": "Lang", - "ariaLabel": "Select language", - "options": { - "en": "English", - "es": "Espanol", - "pt": "Portugues" - } - }, - "nav": { - "home": "Home", - "docs": "Docs", - "login": "Login", - "register": "Register", - "toggleMenu": "Toggle menu" - }, - "sidebar": { - "overview": "Overview", - "payments": "Payments", - "webhookLogs": "Webhook Logs", - "apiKeys": "API Keys", - "settings": "Settings", - "workspace": "Authenticated workspace", - "shortcuts": "Shortcuts", - "createPayment": "Create Payment", - "network": "Mainnet-testnet", - "collapse": "Collapse sidebar", - "expand": "Expand sidebar", - "close": "Close navigation" - }, - "breadcrumbs": { - "home": "Home", - "segments": { - "dashboard": "Dashboard", - "create": "Create", - "payments": "Payments", - "settings": "Settings", - "webhook-logs": "Webhook Logs", - "api-keys": "API Keys" - } - }, - "copyButton": { - "ariaLabel": "Copy to clipboard", - "copied": "Copied!", - "copiedState": "Copied" - }, - "dashboardPage": { - "title": "Merchant Overview", - "description": "Monitor your transaction volume, track payments in real-time, and manage your PLUTO integration.", - "paymentMetrics": "Payment Metrics", - "recentActivity": "Recent Activity", - "viewAllPayments": "View all payments", - "quickActions": "Quick Actions", - "createPaymentLink": "Create Payment Link", - "withdrawFunds": "Withdraw Funds (SEP-24)", - "development": "Development", - "apiKeysTip": "Use the API Keys page to rotate your secret token periodically.", - "webhookLogsTip": "Check Webhook Logs status if payments are not triggering your backend." - }, - "realTimeBalanceSync": { - "title": "Real-time Balances", - "refreshButton": "Refresh", - "syncing": "Syncing…", - "sectionAriaLabel": "Real-time balance information", - "balancesListAriaLabel": "Account balances", - "emptyState": "No balances available.", - "updatedLabel": "Updated", - "balanceItemAriaLabel": "{asset} balance: {balance}", - "liveRegion": { - "syncing": "Syncing balances…", - "error": "Balance sync error: {error}", - "updatedAt": "Balances updated at {time}." - } - }, - "createPaymentPage": { - "eyebrow": "Dashboard", - "title": "Create Payment Link", - "description": "Set an amount, choose an asset, and enter a recipient address to generate a shareable PLUTO payment link.", - "newHere": "New here?", - "registerMerchant": "Register a merchant account" - }, - "createPaymentForm": { - "invalidAmount": "Amount must be a positive number.", - "invalidRecipient": "Recipient must be a valid Stellar public key (56 characters, starts with G).", - "invalidHexColor": "{field} must be a valid hex color.", - "failedCreate": "Failed to create payment link", - "createdToast": "Payment link created!", - "noApiKeyTitle": "No API key found", - "noApiKeyDescription": "Register a merchant account to get your API key and start creating payment links.", - "registerAsMerchant": "Register as Merchant", - "readyEyebrow": "Payment Link Ready", - "readyTitle": "Link Created Successfully", - "readyDescription": "Share this link with your customer to collect payment.", - "shareLink": "Share Link", - "shareTitle": "Payment link ready", - "shareText": "Open this PLUTO payment link.", - "shareFailed": "Native sharing is unavailable right now.", - "paymentLink": "Payment Link", - "paymentId": "Payment ID", - "status": "Status", - "createAnother": "Create another payment link", - "amount": "Amount", - "amountPlaceholder": "Enter {exampleAmount} {asset}", - "asset": "Asset", - "selectAsset": "Select asset", - "issuer": "Issuer", - "trustedAddresses": "Select from Trusted Addresses", - "selectSavedAddress": "-- Select a saved address --", - "recipientAddress": "Recipient Address", - "recipientPlaceholder": "Paste the {asset} destination wallet", - "recipientPlaceholderSelected": "{label} selected for {asset}", - "descriptionLabel": "Description", - "optional": "optional", - "descriptionPlaceholder": "What is this {asset} request for?", - "brandingTitle": "Session Branding Overrides", - "brandingDescription": "Apply custom colors to this payment link only.", - "enabled": "Enabled", - "disabled": "Disabled", - "primary": "Primary", - "secondary": "Secondary", - "background": "Background", - "checkoutPreview": "Checkout preview", - "payNow": "Pay now", - "generating": "Generating...", - "generate": "Generate Payment Link", - "rateLimitError": "You're creating links too quickly. Try again in {seconds} seconds.", - "retryWait": "Wait {seconds}s…", - "integrationCode": "Integration Code", - "snippetsHelper": "Code snippets update automatically with your form values." - }, - "paymentMetrics": { - "downloadImage": "Download Image", - "downloadPng": "Download PNG", - "downloadSvg": "Download SVG", - "exporting": "Exporting...", - "exportSuccess": "Chart downloaded as {format}", - "exportFailed": "Failed to export chart.", - "fetchMetricsFailed": "Failed to fetch metrics", - "fetchVolumeFailed": "Failed to fetch volume data", - "retry": "Retry", - "sevenDayVolume": "7-Day Volume", - "totalPayments": "Total Payments", - "paymentsCount": "{count, plural, one {payment} other {payments}}", - "chartTitle": "Multi-Asset Volume Comparison", - "chartSubtitle": "Daily transaction volume broken down by asset", - "heatmapTitle": "Payment Density Heatmap", - "heatmapSubtitle": "GitHub-style activity intensity across the last 365 days", - "heatmapTotal": "{count, plural, one {# payment in the last year} other {# payments in the last year}}", - "heatmapTooltip": "{count, plural, one {# payment} other {# payments}} on {date}", - "heatmapAriaLabel": "365 day payment density heatmap", - "heatmapEmptyDay": "No payments recorded", - "heatmapLegendLess": "Less", - "heatmapLegendMore": "More", - "heatmapWeekdays": { - "sun": "S", - "mon": "M", - "tue": "T", - "wed": "W", - "thu": "T", - "fri": "F", - "sat": "S" - }, - "showRange": "Show {range}", - "toggleAssetVisibility": "Toggle asset visibility", - "showAsset": "Show {asset}", - "hideAsset": "Hide {asset}", - "noPayments": "No completed payments in this period.", - "weeklyAvgLabel": "7D Avg", - "ranges": { - "7D": "7 Days", - "30D": "30 Days", - "1Y": "1 Year" - } - }, - "paymentsPage": { - "title": "Payment History", - "description": "Monitor all incoming transactions and their verification status." - }, - "recentPayments": { - "missingApiKey": "API key not found. Please register or log in.", - "fetchFailed": "Failed to fetch payments", - "loadFailed": "Failed to load payments", - "connectionError": "Connection Error", - "backendHint": "Make sure the backend is running and the payments endpoint is available.", - "retryConnection": "Retry Connection", - "testWebhook": "Test Webhook Anyway", - "troubleshootingTip": "Troubleshooting Tip", - "troubleshootingDescription": "You can still test webhook functionality while backend services are being restored.", - "emptyTitle": "No payments yet", - "emptyDescription": "Start accepting payments by creating your first payment link or testing webhooks to see transaction data flow.", - "createPaymentLink": "Create Payment Link", - "sendTestWebhook": "Send Test Webhook", - "gettingStartedTitle": "Getting Started Guide", - "gettingStartedDescription": "Use webhook tools to test payment notifications and see real-time data appear in this dashboard.", - "search": "Search", - "searchPlaceholder": "Search by payment ID or description...", - "status": "Status", - "allStatuses": "All Statuses", - "asset": "Asset", - "allAssets": "All Assets", - "fromDate": "From Date", - "toDate": "To Date", - "activeFilters": "Active filters:", - "searchChip": "Search: \"{value}\"", - "statusChip": "Status: {value}", - "assetChip": "Asset: {value}", - "fromChip": "From: {value}", - "toChip": "To: {value}", - "clearSearchFilter": "Clear search filter", - "clearStatusFilter": "Clear status filter", - "clearAssetFilter": "Clear asset filter", - "clearFromDateFilter": "Clear from date filter", - "clearToDateFilter": "Clear to date filter", - "clearAll": "Clear all", - "linkCopied": "Payment link copied to clipboard!", - "showingResults": "Showing {shown} of {total} payments", - "filteredSuffix": "(filters applied)", - "tableStatus": "Status", - "tableAmount": "Amount", - "tableRecipient": "Recipient", - "tableDate": "Date", - "tableLink": "Link", - "view": "View", - "emptyDescriptionValue": "-", - "statuses": { - "all": "All", - "pending": "Pending", - "confirmed": "Confirmed", - "failed": "Failed", - "refunded": "Refunded" - }, - "pagination": { - "ariaLabel": "Payment history pagination", - "previous": "Previous page", - "next": "Next page", - "goToPage": "Go to page {page}", - "currentPage": "Current page, page {page}", - "pageLabel": "Page {page} of {totalPages}", - "range": "Showing {start}-{end} of {total}" - } - }, - "walletSelector": { - "chooseWallet": "Choose a wallet", - "description": "Connect your Stellar wallet to complete this payment.", - "connecting": "Connecting...", - "walletConnectWaiting": "Waiting for wallet...", - "noProjectId": "(no project ID)", - "notInstalled": "(not installed)", - "tapToConnect": "Click to connect", - "scanTitle": "Scan with your compatible wallet app", - "scanDescription": "Scan with Freighter mobile or any WalletConnect-compatible wallet", - "pairingFailed": "WalletConnect pairing failed", - "userRejected": "Connection request was canceled. Please approve it in your wallet to continue.", - "walletConnectUnavailable": "WalletConnect is unavailable right now. Try Freighter or enable a valid WalletConnect project ID.", - "extensionNotFound": "We couldn't find that wallet on this device. Install the extension and try again.", - "noAccountFound": "Your wallet connected, but no Stellar account was returned. Open a Stellar account in the wallet and retry.", - "walletConnectFailed": "We couldn't finish the WalletConnect handshake. Please try again.", - "connectionFailed": "We couldn't connect to that wallet right now. Please try again." - }, - "support": { - "panelAriaLabel": "Help and support panel", - "heading": "Help & Support", - "title": "Need help with payments?", - "description": "Need help? Send us a message or tip to get priority support.", - "contactEmail": "Contact via Email", - "openChat": "Open support chat", - "closeChat": "Close support chat", - "yourWallet": "Your Wallet", - "notConnected": "Not Connected", - "connectingWallet": "Connecting…", - "amount": "Amount", - "loadingBalance": "Loading balance...", - "unfunded": "Unfunded (Fund on Testnet)", - "available": "Available: {balance} {asset}", - "availableAriaLabel": "Available balance: {balance} {asset}", - "balanceSyncing": "Syncing wallet balance…", - "walletUnfunded": "Wallet is unfunded.", - "balanceSynced": "Balance synced: {balance} {asset}.", - "insufficientBalance": "Insufficient balance", - "messageLabel": "Leave a message (optional)", - "messagePlaceholder": "Thanks for the great work!", - "sendSupport": "Send Support", - "sending": "Sending…", - "submissionFailed": "Submission failed", - "failedToRecord": "Failed to record support transaction" - }, - "checkout": { - "paymentRequest": "Payment Request", - "completePayment": "Complete Payment", - "paymentNotFound": "Payment not found", - "loadFailed": "Could not load payment details.", - "paymentMissing": "Payment not found.", - "loadPaymentFailed": "Failed to load payment.", - "errorTitle": "Error", - "errorDescription": "Check the payment link and try again, or contact the sender.", - "processingFallback": "Processing transaction...", - "doNotClose": "Do not close this tab", - "recipient": "Recipient", - "scanToPay": "Scan to Pay", - "scanDescription": "Scan with Freighter or any compatible wallet", - "openQrModal": "Pay with QR code", - "qrModalTitle": "Pay with QR code", - "qrModalDescription": "Scan this code on another device or share it with your wallet app.", - "downloadQr": "Download PNG", - "downloadQrSuccess": "QR code downloaded.", - "downloadQrError": "Could not download QR code.", - "viewRawIntent": "View raw intent link", - "hideRawIntent": "Hide raw intent link", - "created": "Created", - "activeViewers": "{count, plural, one {# person is viewing this payment link} other {# people are viewing this payment link}}", - "transaction": "Transaction", - "paymentConfirmed": "Payment confirmed!", - "paymentSent": "Payment sent!", - "paymentFailed": "Payment failed. Please try again.", - "connectedVia": "Connected via {provider}", - "payInXlm": "Pay in XLM", - "standardAssetOption": "Pay the invoice in {asset}", - "pathPaymentOption": "Pay in XLM", - "approximateCost": "Approximate cost in XLM", - "merchantReceives": "Merchant receives {amount} {asset}", - "quoteLoading": "Checking XLM path payment routes...", - "quoteUnavailable": "XLM payment routing is unavailable for this invoice right now.", - "quoteBuffer": "You may send up to {amount} XLM to cover price movement.", - "pathPaymentHint": "Uses Stellar path payments under the hood through your connected wallet.", - "approximateCostLabel": "Approximate cost in XLM", - "approximateCostHelp": "Estimated amount to deliver {amount} {asset} to the merchant.", - "slippageBuffer": "{percent}% safety buffer included. Max send: {sendMax} {asset}", - "pathPaymentTogglePrefix": "Pay with", - "pathPaymentToggleSuffix": "instead", - "processing": "Processing...", - "payWith": "Pay with {provider}", - "payWithFallback": "Pay with Wallet", - "reviewPaymentTitle": "Review Payment", - "loadingNetworkFee": "Fetching current network fee...", - "networkFeeLabel": "Network Fee: ~{amount} XLM", - "networkFeeUnavailable": "Network fee unavailable right now.", - "cancel": "Cancel", - "confirmPayment": "Confirm Payment", - "receivedTitle": "This payment has been received.", - "receivedDescription": "The transaction was confirmed on-chain via PLUTO.", - "downloadReceipt": "Download Receipt", - "downloadReceiptLoading": "Preparing Receipt...", - "receiptDownloaded": "Receipt download started.", - "receiptDownloadFailed": "Could not generate the receipt PDF.", - "receiptHashUnavailable": "Unavailable", - "failedTitle": "This payment has failed.", - "failedDescription": "Contact the merchant if you believe this is an error.", - "status": { - "pending": "Awaiting Payment", - "confirmed": "Confirmed", - "completed": "Completed", - "failed": "Failed" - } - }, - "profileCard": { - "account": "Account", - "openProfileMenu": "Open profile menu", - "dashboard": "Dashboard", - "settings": "Settings", - "createPayment": "Create Payment", - "logoutAccount": "Logout Account", - "loadingProfile": "Loading profile…" - }, - "settingsPage": { - "eyebrow": "Settings", - "eyebrowAccount": "Account", - "title": "Settings", - "description": "Manage your credentials, branding, and integrations.", - "loadingSettings": "Loading settings…", - "noApiKeyTitle": "No API key found", - "noApiKeyDescription": "Register a merchant account first to manage your credentials here.", - "registerAsMerchant": "Register as Merchant", - "navAriaLabel": "Settings navigation", - "navApiKeys": "API Keys", - "navBranding": "Branding", - "navDisplay": "Display", - "navWebhooks": "Webhooks", - "navPermissions": "Permissions", - "navDanger": "Danger Zone", - "apiAuthTitle": "API Authentication", - "apiAuthDescription": "Your secret key for server-side API requests.", - "liveApiKey": "Live API Key", - "hide": "Hide", - "reveal": "Reveal", - "apiKeyHeaderHintPrefix": "Pass as", - "apiKeyHeaderHintSuffix": "header on every request.", - "apiKeyVisible": "Live API key is currently visible.", - "apiKeyHidden": "Live API key is currently hidden.", - "rotateApiKeyTitle": "Rotate API Key", - "rotateApiKeyDescription": "Generates a new key and immediately invalidates the current one.", - "rotateKeyEllipsis": "Rotate Key…", - "confirmAction": "Confirm Action", - "rotateKeyWarning": "The old key will stop working immediately.", - "confirm": "Confirm", - "rotating": "Rotating…", - "cancel": "Cancel", - "checkoutBrandingTitle": "Checkout Branding", - "checkoutBrandingDescription": "Customize the look of your payment checkout page.", - "storeLogo": "Store Logo", - "clickOrDragToReplace": "Click or drag to replace", - "dropHere": "Drop here", - "uploadLogo": "Upload logo", - "logoFormats": "PNG, JPG or SVG · max 2MB", - "removeLogo": "Remove logo", - "colors": "Colors", - "primary": "Primary", - "secondary": "Secondary", - "background": "Background", - "preview": "Preview", - "sampleCheckout": "Sample checkout", - "payNow": "Pay Now", - "lowContrastWarning": "Colors may not meet WCAG contrast targets. Consider adjusting.", - "saving": "Saving…", - "saveBranding": "Save Branding", - "previewReceipt": "Preview Receipt", - "displayPreferencesTitle": "Display Preferences", - "displayPreferencesDescription": "Adjust how values appear across the dashboard.", - "hideTrailingCents": "Hide trailing cents", - "hideTrailingCentsDescription": "Whole amounts like 50 will display without the .00 suffix.", - "webhookEndpointTitle": "Webhook Endpoint", - "webhookEndpointDescription": "Receive real-time payment events at your server.", - "verified": "Verified", - "unverified": "Unverified", - "endpointUrl": "Endpoint URL", - "saveUrl": "Save URL", - "testing": "Testing…", - "sendTest": "Send Test", - "domainVerification": "Domain Verification", - "hostTokenAt": "Host this token at", - "verifying": "Verifying…", - "verifyDomain": "Verify Domain", - "signingSecretTitle": "Signing Secret", - "signingSecretDescriptionPrefix": "Validate the", - "signingSecretDescriptionSuffix": "header on incoming webhooks.", - "hideWebhookSecret": "Hide webhook secret", - "showWebhookSecret": "Show webhook secret", - "webhookSecretVisible": "Webhook secret is visible. Copy now — shown once.", - "webhookSecretHidden": "Webhook secret is hidden. Toggle reveal to inspect it.", - "regenerateSecretEllipsis": "Regenerate Secret…", - "regenerateSecretWarning": "The current secret will stop working immediately.", - "regenerating": "Regenerating…", - "dangerZoneDescription": "Irreversible actions. Proceed with caution.", - "failedToLoadBranding": "Failed to load branding", - "failedToLoadWebhookSettings": "Failed to load webhook settings", - "failedToRotateKey": "Failed to rotate key", - "apiKeyRotatedSuccess": "API key rotated — update any integrations using the old key.", - "imageSizeLimit": "Image must be under 2MB", - "logoUploaded": "Logo uploaded!", - "hexColorRequired": "{field} must be a valid hex color", - "failedToSaveBranding": "Failed to save branding", - "brandingSaved": "Branding saved", - "webhookUrlMustBeHttps": "Webhook URL must use HTTPS", - "webhookUrlInvalid": "Invalid URL (e.g. https://example.com/webhook)", - "failedToSaveWebhookUrl": "Failed to save webhook URL", - "webhookUrlSaved": "Webhook URL saved", - "webhookUrlCleared": "Webhook URL cleared", - "failedToVerifyDomain": "Failed to verify webhook domain", - "domainVerified": "Domain verified", - "domainStillUnverified": "Domain still unverified", - "failedToRegenerateSecret": "Failed to regenerate secret", - "webhookSecretRegenerated": "Webhook secret regenerated — update your integrations.", - "testWebhookRequestFailed": "Test webhook request failed", - "failedToTestWebhook": "Failed to test webhook", - "testWebhookSentStatus": "Test webhook sent — status {status}", - "colorPickerLabel": "{label} color picker" - }, - "notificationCenter": { - "buttonLabel": "Notifications", - "buttonLabelWithCount": "Notifications ({count} unread)", - "panelLabel": "Notification Center", - "heading": "Notifications", - "unreadCount": "{count} unread", - "noAlerts": "No new alerts", - "alertLabel": "Alert", - "dismissLabel": "Dismiss notification: {message}", - "notificationLabel": "Notification: {message}", - "timestampLabel": "Timestamp: {timestamp}" - }, - "fiatOnramp": { - "title": "Buy / Deposit Funds", - "triggerLabel": "Buy / Deposit", - "description": "Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly in your connected wallet.", - "selectAsset": "Select Asset", - "amountLabel": "Amount", - "amountOptional": "(optional)", - "amountPlaceholder": "e.g. {amount} {asset}", - "anchorDomainLabel": "Anchor Domain", - "anchorDomainPlaceholder": "e.g. testanchor.stellar.org", - "continueButton": "Continue to Anchor", - "footerNote": "Secured by Stellar Network - SEP-0024 Standard", - "stepConnecting": "Connecting to anchor...", - "stepAuth": "Waiting for wallet signature...", - "stepGenerating": "Preparing your deposit form...", - "authHint": "Please sign the challenge transaction in your wallet to securely connect to {domain}.", - "genericHint": "This should only take a moment.", - "iframeTitle": "Anchor deposit form", - "genericError": "Deposit failed" - }, - "kycPage": { - "title": "KYC Verification", - "description": "Complete the verification process to comply with regulatory requirements and unlock full access to platform features.", - "whyKyc": "Why KYC?", - "reasonComply": "Comply with regulatory requirements", - "reasonLimits": "Increase transaction limits", - "reasonFeatures": "Access premium features", - "reasonSecurity": "Enhanced account security" - }, - "kycForm": { - "formTitle": "KYC Submission Form", - "step": "Step", - "of": "of", - "steps": "Steps", - "completed": "completed", - "current": "current", - "upcoming": "upcoming", - "personalInfo": "Personal Information", - "addressInfo": "Address Information", - "documents": "Documents", - "review": "Review", - "firstName": "First Name", - "lastName": "Last Name", - "email": "Email", - "nationality": "Nationality", - "dateOfBirth": "Date of Birth", - "street": "Street", - "city": "City", - "state": "State", - "postalCode": "Postal Code", - "country": "Country", - "idType": "ID Type", - "selectIdType": "Select ID type", - "passport": "Passport", - "driversLicense": "Driver's License", - "nationalId": "National ID", - "idNumber": "ID Number", - "idFront": "ID Front", - "idBack": "ID Back", - "selfie": "Selfie", - "required": "Required", - "validationError": "Please fill in all required fields", - "back": "Back", - "next": "Next", - "submit": "Submit KYC", - "submitting": "Submitting...", - "submitError": "Submission failed", - "submitAnother": "Submit Another", - "successTitle": "KYC Submitted Successfully!", - "successDescription": "Your KYC verification has been submitted and is under review.", - "dash": "—" - }, - "darkModeTheme": { - "loading": "Loading theme settings", - "errorCleared": "Error cleared. Attempting to toggle theme.", - "switchingTo": "Switching to {theme} theme.", - "themeChanged": "Theme changed to {theme}.", - "themeError": "Failed to toggle theme. Please try again.", - "currentTheme": "Current theme: {theme}", - "systemTheme": "System ({theme})", - "ariaError": "Theme toggle with error — press to retry", - "ariaLabel": "Theme toggle, current theme: {theme}. Press to switch to next theme.", - "ariaLoading": "Theme is loading", - "errorTitle": "Theme error: {error}. Press to retry.", - "titleSystem": "Theme: System ({theme})", - "titleLight": "Theme: Light", - "titleDark": "Theme: Dark", - "description": "Cycle through light, dark, and system themes. System follows your device preference. Press Space or Enter to activate.", - "preferenceLabel": "Theme preference", - "preferenceSaved": "Theme preference saved", - "preferenceRestored": "Theme preference restored", - "themes": { - "light": "light", - "dark": "dark", - "system": "system" - }, - "themeNames": { - "light": "Light", - "dark": "Dark", - "system": "System" - }, - "sr": { - "sunIcon": "Sun icon — light mode active", - "moonIcon": "Moon icon — dark mode active", - "systemIcon": "Monitor icon — system theme active", - "errorIcon": "Error icon — theme failed to apply" - } - }, - "portfolioChartWidget": { - "title": "Portfolio Value", - "allocation": "Allocation", - "trend": "Trend", - "loading": "Loading portfolio chart...", - "historyLoading": "Loading performance history...", - "emptyAllocation": "No allocation data available yet.", - "emptyAssets": "No portfolio assets available yet.", - "emptyHistory": "No performance history available yet." - } +{ + "twoFactorAuth": { + "ariaLabel": "Two-factor authentication setup", + "stepsLabel": "Setup steps", + "title": "Enable Two-Factor Authentication", + "description": "Protect your account with an authenticator app. You will need to scan a QR code to get started.", + "enableButton": "Enable 2FA", + "settingUp": "Setting up…", + "scanTitle": "Scan with your authenticator app", + "scanDescription": "Use Google Authenticator, Authy, or any TOTP-compatible app to scan the code below.", + "qrCodeAlt": "TOTP QR code — scan with your authenticator app", + "qrCodeSkeletonLabel": "Generating QR code…", + "manualKeyLabel": "Or enter key manually", + "manualKeyAria": "Manual setup key: {key}", + "codeInputLabel": "Enter 6-digit code", + "codeInputPlaceholder": "000000", + "verifyButton": "Verify & Enable", + "verifying": "Verifying…", + "successTitle": "Two-Factor Authentication Enabled", + "successDescription": "Your account is now protected. You will be prompted for a code on each login.", + "error": { + "setupFailed": "Failed to start 2FA setup. Please try again.", + "invalidCode": "Invalid code. Please try again.", + "codeLength": "Please enter the 6-digit code from your authenticator app." + } + }, + "localeSwitcher": { + "label": "Lang", + "ariaLabel": "Select language", + "options": { + "en": "English", + "es": "Espanol", + "pt": "Portugues" + } + }, + "nav": { + "home": "Home", + "docs": "Docs", + "login": "Login", + "register": "Register", + "toggleMenu": "Toggle menu" + }, + "sidebar": { + "overview": "Overview", + "payments": "Payments", + "webhookLogs": "Webhook Logs", + "apiKeys": "API Keys", + "settings": "Settings", + "workspace": "Authenticated workspace", + "shortcuts": "Shortcuts", + "createPayment": "Create Payment", + "network": "Mainnet-testnet", + "collapse": "Collapse sidebar", + "expand": "Expand sidebar", + "close": "Close navigation" + }, + "breadcrumbs": { + "home": "Home", + "segments": { + "dashboard": "Dashboard", + "create": "Create", + "payments": "Payments", + "settings": "Settings", + "webhook-logs": "Webhook Logs", + "api-keys": "API Keys" + } + }, + "copyButton": { + "ariaLabel": "Copy to clipboard", + "copied": "Copied!", + "copiedState": "Copied" + }, + "dashboardPage": { + "title": "Merchant Overview", + "description": "Monitor your transaction volume, track payments in real-time, and manage your PLUTO integration.", + "paymentMetrics": "Payment Metrics", + "recentActivity": "Recent Activity", + "viewAllPayments": "View all payments", + "quickActions": "Quick Actions", + "createPaymentLink": "Create Payment Link", + "withdrawFunds": "Withdraw Funds (SEP-24)", + "development": "Development", + "apiKeysTip": "Use the API Keys page to rotate your secret token periodically.", + "webhookLogsTip": "Check Webhook Logs status if payments are not triggering your backend." + }, + "realTimeBalanceSync": { + "title": "Real-time Balances", + "refreshButton": "Refresh", + "syncing": "Syncing…", + "sectionAriaLabel": "Real-time balance information", + "balancesListAriaLabel": "Account balances", + "emptyState": "No balances available.", + "updatedLabel": "Updated", + "balanceItemAriaLabel": "{asset} balance: {balance}", + "liveRegion": { + "syncing": "Syncing balances…", + "error": "Balance sync error: {error}", + "updatedAt": "Balances updated at {time}." + } + }, + "createPaymentPage": { + "eyebrow": "Dashboard", + "title": "Create Payment Link", + "description": "Set an amount, choose an asset, and enter a recipient address to generate a shareable PLUTO payment link.", + "newHere": "New here?", + "registerMerchant": "Register a merchant account" + }, + "createPaymentForm": { + "invalidAmount": "Amount must be a positive number.", + "invalidRecipient": "Recipient must be a valid Stellar public key (56 characters, starts with G).", + "invalidHexColor": "{field} must be a valid hex color.", + "failedCreate": "Failed to create payment link", + "createdToast": "Payment link created!", + "noApiKeyTitle": "No API key found", + "noApiKeyDescription": "Register a merchant account to get your API key and start creating payment links.", + "registerAsMerchant": "Register as Merchant", + "readyEyebrow": "Payment Link Ready", + "readyTitle": "Link Created Successfully", + "readyDescription": "Share this link with your customer to collect payment.", + "shareLink": "Share Link", + "shareTitle": "Payment link ready", + "shareText": "Open this PLUTO payment link.", + "shareFailed": "Native sharing is unavailable right now.", + "paymentLink": "Payment Link", + "paymentId": "Payment ID", + "status": "Status", + "createAnother": "Create another payment link", + "amount": "Amount", + "amountPlaceholder": "Enter {exampleAmount} {asset}", + "asset": "Asset", + "selectAsset": "Select asset", + "issuer": "Issuer", + "trustedAddresses": "Select from Trusted Addresses", + "selectSavedAddress": "-- Select a saved address --", + "recipientAddress": "Recipient Address", + "recipientPlaceholder": "Paste the {asset} destination wallet", + "recipientPlaceholderSelected": "{label} selected for {asset}", + "descriptionLabel": "Description", + "optional": "optional", + "descriptionPlaceholder": "What is this {asset} request for?", + "brandingTitle": "Session Branding Overrides", + "brandingDescription": "Apply custom colors to this payment link only.", + "enabled": "Enabled", + "disabled": "Disabled", + "primary": "Primary", + "secondary": "Secondary", + "background": "Background", + "checkoutPreview": "Checkout preview", + "payNow": "Pay now", + "generating": "Generating...", + "generate": "Generate Payment Link", + "rateLimitError": "You're creating links too quickly. Try again in {seconds} seconds.", + "retryWait": "Wait {seconds}s…", + "integrationCode": "Integration Code", + "snippetsHelper": "Code snippets update automatically with your form values." + }, + "paymentMetrics": { + "downloadImage": "Download Image", + "downloadPng": "Download PNG", + "downloadSvg": "Download SVG", + "exporting": "Exporting...", + "exportSuccess": "Chart downloaded as {format}", + "exportFailed": "Failed to export chart.", + "fetchMetricsFailed": "Failed to fetch metrics", + "fetchVolumeFailed": "Failed to fetch volume data", + "retry": "Retry", + "sevenDayVolume": "7-Day Volume", + "totalPayments": "Total Payments", + "paymentsCount": "{count, plural, one {payment} other {payments}}", + "chartTitle": "Multi-Asset Volume Comparison", + "chartSubtitle": "Daily transaction volume broken down by asset", + "heatmapTitle": "Payment Density Heatmap", + "heatmapSubtitle": "GitHub-style activity intensity across the last 365 days", + "heatmapTotal": "{count, plural, one {# payment in the last year} other {# payments in the last year}}", + "heatmapTooltip": "{count, plural, one {# payment} other {# payments}} on {date}", + "heatmapAriaLabel": "365 day payment density heatmap", + "heatmapEmptyDay": "No payments recorded", + "heatmapLegendLess": "Less", + "heatmapLegendMore": "More", + "heatmapWeekdays": { + "sun": "S", + "mon": "M", + "tue": "T", + "wed": "W", + "thu": "T", + "fri": "F", + "sat": "S" + }, + "showRange": "Show {range}", + "toggleAssetVisibility": "Toggle asset visibility", + "showAsset": "Show {asset}", + "hideAsset": "Hide {asset}", + "noPayments": "No completed payments in this period.", + "weeklyAvgLabel": "7D Avg", + "ranges": { + "7D": "7 Days", + "30D": "30 Days", + "1Y": "1 Year" + } + }, + "paymentsPage": { + "title": "Payment History", + "description": "Monitor all incoming transactions and their verification status." + }, + "recentPayments": { + "missingApiKey": "API key not found. Please register or log in.", + "fetchFailed": "Failed to fetch payments", + "loadFailed": "Failed to load payments", + "connectionError": "Connection Error", + "backendHint": "Make sure the backend is running and the payments endpoint is available.", + "retryConnection": "Retry Connection", + "testWebhook": "Test Webhook Anyway", + "troubleshootingTip": "Troubleshooting Tip", + "troubleshootingDescription": "You can still test webhook functionality while backend services are being restored.", + "emptyTitle": "No payments yet", + "emptyDescription": "Start accepting payments by creating your first payment link or testing webhooks to see transaction data flow.", + "createPaymentLink": "Create Payment Link", + "sendTestWebhook": "Send Test Webhook", + "gettingStartedTitle": "Getting Started Guide", + "gettingStartedDescription": "Use webhook tools to test payment notifications and see real-time data appear in this dashboard.", + "search": "Search", + "searchPlaceholder": "Search by payment ID or description...", + "status": "Status", + "allStatuses": "All Statuses", + "asset": "Asset", + "allAssets": "All Assets", + "fromDate": "From Date", + "toDate": "To Date", + "activeFilters": "Active filters:", + "searchChip": "Search: \"{value}\"", + "statusChip": "Status: {value}", + "assetChip": "Asset: {value}", + "fromChip": "From: {value}", + "toChip": "To: {value}", + "clearSearchFilter": "Clear search filter", + "clearStatusFilter": "Clear status filter", + "clearAssetFilter": "Clear asset filter", + "clearFromDateFilter": "Clear from date filter", + "clearToDateFilter": "Clear to date filter", + "clearAll": "Clear all", + "linkCopied": "Payment link copied to clipboard!", + "showingResults": "Showing {shown} of {total} payments", + "filteredSuffix": "(filters applied)", + "tableStatus": "Status", + "tableAmount": "Amount", + "tableRecipient": "Recipient", + "tableDate": "Date", + "tableLink": "Link", + "view": "View", + "emptyDescriptionValue": "-", + "statuses": { + "all": "All", + "pending": "Pending", + "confirmed": "Confirmed", + "failed": "Failed", + "refunded": "Refunded" + }, + "pagination": { + "ariaLabel": "Payment history pagination", + "previous": "Previous page", + "next": "Next page", + "goToPage": "Go to page {page}", + "currentPage": "Current page, page {page}", + "pageLabel": "Page {page} of {totalPages}", + "range": "Showing {start}-{end} of {total}" + } + }, + "walletSelector": { + "chooseWallet": "Choose a wallet", + "description": "Connect your Stellar wallet to complete this payment.", + "connecting": "Connecting...", + "walletConnectWaiting": "Waiting for wallet...", + "noProjectId": "(no project ID)", + "notInstalled": "(not installed)", + "tapToConnect": "Click to connect", + "scanTitle": "Scan with your compatible wallet app", + "scanDescription": "Scan with Freighter mobile or any WalletConnect-compatible wallet", + "pairingFailed": "WalletConnect pairing failed", + "userRejected": "Connection request was canceled. Please approve it in your wallet to continue.", + "walletConnectUnavailable": "WalletConnect is unavailable right now. Try Freighter or enable a valid WalletConnect project ID.", + "extensionNotFound": "We couldn't find that wallet on this device. Install the extension and try again.", + "noAccountFound": "Your wallet connected, but no Stellar account was returned. Open a Stellar account in the wallet and retry.", + "walletConnectFailed": "We couldn't finish the WalletConnect handshake. Please try again.", + "connectionFailed": "We couldn't connect to that wallet right now. Please try again." + }, + "support": { + "panelAriaLabel": "Help and support panel", + "heading": "Help & Support", + "title": "Need help with payments?", + "description": "Need help? Send us a message or tip to get priority support.", + "contactEmail": "Contact via Email", + "openChat": "Open support chat", + "closeChat": "Close support chat", + "yourWallet": "Your Wallet", + "notConnected": "Not Connected", + "connectingWallet": "Connecting…", + "amount": "Amount", + "loadingBalance": "Loading balance...", + "unfunded": "Unfunded (Fund on Testnet)", + "available": "Available: {balance} {asset}", + "availableAriaLabel": "Available balance: {balance} {asset}", + "balanceSyncing": "Syncing wallet balance…", + "walletUnfunded": "Wallet is unfunded.", + "balanceSynced": "Balance synced: {balance} {asset}.", + "insufficientBalance": "Insufficient balance", + "messageLabel": "Leave a message (optional)", + "messagePlaceholder": "Thanks for the great work!", + "sendSupport": "Send Support", + "sending": "Sending…", + "submissionFailed": "Submission failed", + "failedToRecord": "Failed to record support transaction" + }, + "checkout": { + "paymentRequest": "Payment Request", + "completePayment": "Complete Payment", + "paymentNotFound": "Payment not found", + "loadFailed": "Could not load payment details.", + "paymentMissing": "Payment not found.", + "loadPaymentFailed": "Failed to load payment.", + "errorTitle": "Error", + "errorDescription": "Check the payment link and try again, or contact the sender.", + "processingFallback": "Processing transaction...", + "doNotClose": "Do not close this tab", + "recipient": "Recipient", + "scanToPay": "Scan to Pay", + "scanDescription": "Scan with Freighter or any compatible wallet", + "openQrModal": "Pay with QR code", + "qrModalTitle": "Pay with QR code", + "qrModalDescription": "Scan this code on another device or share it with your wallet app.", + "downloadQr": "Download PNG", + "downloadQrSuccess": "QR code downloaded.", + "downloadQrError": "Could not download QR code.", + "viewRawIntent": "View raw intent link", + "hideRawIntent": "Hide raw intent link", + "created": "Created", + "activeViewers": "{count, plural, one {# person is viewing this payment link} other {# people are viewing this payment link}}", + "transaction": "Transaction", + "paymentConfirmed": "Payment confirmed!", + "paymentSent": "Payment sent!", + "paymentFailed": "Payment failed. Please try again.", + "connectedVia": "Connected via {provider}", + "payInXlm": "Pay in XLM", + "standardAssetOption": "Pay the invoice in {asset}", + "pathPaymentOption": "Pay in XLM", + "approximateCost": "Approximate cost in XLM", + "merchantReceives": "Merchant receives {amount} {asset}", + "quoteLoading": "Checking XLM path payment routes...", + "quoteUnavailable": "XLM payment routing is unavailable for this invoice right now.", + "quoteBuffer": "You may send up to {amount} XLM to cover price movement.", + "pathPaymentHint": "Uses Stellar path payments under the hood through your connected wallet.", + "approximateCostLabel": "Approximate cost in XLM", + "approximateCostHelp": "Estimated amount to deliver {amount} {asset} to the merchant.", + "slippageBuffer": "{percent}% safety buffer included. Max send: {sendMax} {asset}", + "pathPaymentTogglePrefix": "Pay with", + "pathPaymentToggleSuffix": "instead", + "processing": "Processing...", + "payWith": "Pay with {provider}", + "payWithFallback": "Pay with Wallet", + "reviewPaymentTitle": "Review Payment", + "loadingNetworkFee": "Fetching current network fee...", + "networkFeeLabel": "Network Fee: ~{amount} XLM", + "networkFeeUnavailable": "Network fee unavailable right now.", + "cancel": "Cancel", + "confirmPayment": "Confirm Payment", + "receivedTitle": "This payment has been received.", + "receivedDescription": "The transaction was confirmed on-chain via PLUTO.", + "downloadReceipt": "Download Receipt", + "downloadReceiptLoading": "Preparing Receipt...", + "receiptDownloaded": "Receipt download started.", + "receiptDownloadFailed": "Could not generate the receipt PDF.", + "receiptHashUnavailable": "Unavailable", + "failedTitle": "This payment has failed.", + "failedDescription": "Contact the merchant if you believe this is an error.", + "status": { + "pending": "Awaiting Payment", + "confirmed": "Confirmed", + "completed": "Completed", + "failed": "Failed" + } + }, + "profileCard": { + "account": "Account", + "openProfileMenu": "Open profile menu", + "dashboard": "Dashboard", + "settings": "Settings", + "createPayment": "Create Payment", + "logoutAccount": "Logout Account", + "loadingProfile": "Loading profile…" + }, + "settingsPage": { + "eyebrow": "Settings", + "eyebrowAccount": "Account", + "title": "Settings", + "description": "Manage your credentials, branding, and integrations.", + "loadingSettings": "Loading settings…", + "noApiKeyTitle": "No API key found", + "noApiKeyDescription": "Register a merchant account first to manage your credentials here.", + "registerAsMerchant": "Register as Merchant", + "navAriaLabel": "Settings navigation", + "navApiKeys": "API Keys", + "navBranding": "Branding", + "navDisplay": "Display", + "navWebhooks": "Webhooks", + "navPermissions": "Permissions", + "navDanger": "Danger Zone", + "apiAuthTitle": "API Authentication", + "apiAuthDescription": "Your secret key for server-side API requests.", + "liveApiKey": "Live API Key", + "hide": "Hide", + "reveal": "Reveal", + "apiKeyHeaderHintPrefix": "Pass as", + "apiKeyHeaderHintSuffix": "header on every request.", + "apiKeyVisible": "Live API key is currently visible.", + "apiKeyHidden": "Live API key is currently hidden.", + "rotateApiKeyTitle": "Rotate API Key", + "rotateApiKeyDescription": "Generates a new key and immediately invalidates the current one.", + "rotateKeyEllipsis": "Rotate Key…", + "confirmAction": "Confirm Action", + "rotateKeyWarning": "The old key will stop working immediately.", + "confirm": "Confirm", + "rotating": "Rotating…", + "cancel": "Cancel", + "checkoutBrandingTitle": "Checkout Branding", + "checkoutBrandingDescription": "Customize the look of your payment checkout page.", + "storeLogo": "Store Logo", + "clickOrDragToReplace": "Click or drag to replace", + "dropHere": "Drop here", + "uploadLogo": "Upload logo", + "logoFormats": "PNG, JPG or SVG · max 2MB", + "removeLogo": "Remove logo", + "colors": "Colors", + "primary": "Primary", + "secondary": "Secondary", + "background": "Background", + "preview": "Preview", + "sampleCheckout": "Sample checkout", + "payNow": "Pay Now", + "lowContrastWarning": "Colors may not meet WCAG contrast targets. Consider adjusting.", + "saving": "Saving…", + "saveBranding": "Save Branding", + "previewReceipt": "Preview Receipt", + "displayPreferencesTitle": "Display Preferences", + "displayPreferencesDescription": "Adjust how values appear across the dashboard.", + "hideTrailingCents": "Hide trailing cents", + "hideTrailingCentsDescription": "Whole amounts like 50 will display without the .00 suffix.", + "webhookEndpointTitle": "Webhook Endpoint", + "webhookEndpointDescription": "Receive real-time payment events at your server.", + "verified": "Verified", + "unverified": "Unverified", + "endpointUrl": "Endpoint URL", + "saveUrl": "Save URL", + "testing": "Testing…", + "sendTest": "Send Test", + "domainVerification": "Domain Verification", + "hostTokenAt": "Host this token at", + "verifying": "Verifying…", + "verifyDomain": "Verify Domain", + "signingSecretTitle": "Signing Secret", + "signingSecretDescriptionPrefix": "Validate the", + "signingSecretDescriptionSuffix": "header on incoming webhooks.", + "hideWebhookSecret": "Hide webhook secret", + "showWebhookSecret": "Show webhook secret", + "webhookSecretVisible": "Webhook secret is visible. Copy now — shown once.", + "webhookSecretHidden": "Webhook secret is hidden. Toggle reveal to inspect it.", + "regenerateSecretEllipsis": "Regenerate Secret…", + "regenerateSecretWarning": "The current secret will stop working immediately.", + "regenerating": "Regenerating…", + "dangerZoneDescription": "Irreversible actions. Proceed with caution.", + "failedToLoadBranding": "Failed to load branding", + "failedToLoadWebhookSettings": "Failed to load webhook settings", + "failedToRotateKey": "Failed to rotate key", + "apiKeyRotatedSuccess": "API key rotated — update any integrations using the old key.", + "imageSizeLimit": "Image must be under 2MB", + "logoUploaded": "Logo uploaded!", + "hexColorRequired": "{field} must be a valid hex color", + "failedToSaveBranding": "Failed to save branding", + "brandingSaved": "Branding saved", + "webhookUrlMustBeHttps": "Webhook URL must use HTTPS", + "webhookUrlInvalid": "Invalid URL (e.g. https://example.com/webhook)", + "failedToSaveWebhookUrl": "Failed to save webhook URL", + "webhookUrlSaved": "Webhook URL saved", + "webhookUrlCleared": "Webhook URL cleared", + "failedToVerifyDomain": "Failed to verify webhook domain", + "domainVerified": "Domain verified", + "domainStillUnverified": "Domain still unverified", + "failedToRegenerateSecret": "Failed to regenerate secret", + "webhookSecretRegenerated": "Webhook secret regenerated — update your integrations.", + "testWebhookRequestFailed": "Test webhook request failed", + "failedToTestWebhook": "Failed to test webhook", + "testWebhookSentStatus": "Test webhook sent — status {status}", + "colorPickerLabel": "{label} color picker" + }, + "notificationCenter": { + "buttonLabel": "Notifications", + "buttonLabelWithCount": "Notifications ({count} unread)", + "panelLabel": "Notification Center", + "heading": "Notifications", + "unreadCount": "{count} unread", + "noAlerts": "No new alerts", + "alertLabel": "Alert", + "dismissLabel": "Dismiss notification: {message}", + "notificationLabel": "Notification: {message}", + "timestampLabel": "Timestamp: {timestamp}" + }, + "fiatOnramp": { + "title": "Buy / Deposit Funds", + "triggerLabel": "Buy / Deposit", + "description": "Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly in your connected wallet.", + "selectAsset": "Select Asset", + "amountLabel": "Amount", + "amountOptional": "(optional)", + "amountPlaceholder": "e.g. {amount} {asset}", + "anchorDomainLabel": "Anchor Domain", + "anchorDomainPlaceholder": "e.g. testanchor.stellar.org", + "continueButton": "Continue to Anchor", + "footerNote": "Secured by Stellar Network - SEP-0024 Standard", + "stepConnecting": "Connecting to anchor...", + "stepAuth": "Waiting for wallet signature...", + "stepGenerating": "Preparing your deposit form...", + "authHint": "Please sign the challenge transaction in your wallet to securely connect to {domain}.", + "genericHint": "This should only take a moment.", + "iframeTitle": "Anchor deposit form", + "genericError": "Deposit failed" + }, + "kycPage": { + "title": "KYC Verification", + "description": "Complete the verification process to comply with regulatory requirements and unlock full access to platform features.", + "whyKyc": "Why KYC?", + "reasonComply": "Comply with regulatory requirements", + "reasonLimits": "Increase transaction limits", + "reasonFeatures": "Access premium features", + "reasonSecurity": "Enhanced account security" + }, + "kycForm": { + "formTitle": "KYC Submission Form", + "step": "Step", + "of": "of", + "steps": "Steps", + "completed": "completed", + "current": "current", + "upcoming": "upcoming", + "personalInfo": "Personal Information", + "addressInfo": "Address Information", + "documents": "Documents", + "review": "Review", + "firstName": "First Name", + "lastName": "Last Name", + "email": "Email", + "nationality": "Nationality", + "dateOfBirth": "Date of Birth", + "street": "Street", + "city": "City", + "state": "State", + "postalCode": "Postal Code", + "country": "Country", + "idType": "ID Type", + "selectIdType": "Select ID type", + "passport": "Passport", + "driversLicense": "Driver's License", + "nationalId": "National ID", + "idNumber": "ID Number", + "idFront": "ID Front", + "idBack": "ID Back", + "selfie": "Selfie", + "required": "Required", + "validationError": "Please fill in all required fields", + "back": "Back", + "next": "Next", + "submit": "Submit KYC", + "submitting": "Submitting...", + "submitError": "Submission failed", + "submitAnother": "Submit Another", + "successTitle": "KYC Submitted Successfully!", + "successDescription": "Your KYC verification has been submitted and is under review.", + "dash": "—" + }, + "darkModeTheme": { + "loading": "Loading theme settings", + "errorCleared": "Error cleared. Attempting to toggle theme.", + "switchingTo": "Switching to {theme} theme.", + "themeChanged": "Theme changed to {theme}.", + "themeError": "Failed to toggle theme. Please try again.", + "currentTheme": "Current theme: {theme}", + "systemTheme": "System ({theme})", + "ariaError": "Theme toggle with error — press to retry", + "ariaLabel": "Theme toggle, current theme: {theme}. Press to switch to next theme.", + "ariaLoading": "Theme is loading", + "errorTitle": "Theme error: {error}. Press to retry.", + "titleSystem": "Theme: System ({theme})", + "titleLight": "Theme: Light", + "titleDark": "Theme: Dark", + "description": "Cycle through light, dark, and system themes. System follows your device preference. Press Space or Enter to activate.", + "preferenceLabel": "Theme preference", + "preferenceSaved": "Theme preference saved", + "preferenceRestored": "Theme preference restored", + "themes": { + "light": "light", + "dark": "dark", + "system": "system" + }, + "themeNames": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, + "sr": { + "sunIcon": "Sun icon — light mode active", + "moonIcon": "Moon icon — dark mode active", + "systemIcon": "Monitor icon — system theme active", + "errorIcon": "Error icon — theme failed to apply" + } + }, + "portfolioChartWidget": { + "title": "Portfolio Value", + "allocation": "Allocation", + "trend": "Trend", + "loading": "Loading portfolio chart...", + "historyLoading": "Loading performance history...", + "emptyAllocation": "No allocation data available yet.", + "emptyAssets": "No portfolio assets available yet.", + "emptyHistory": "No performance history available yet." + } } \ No newline at end of file diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 815f06c8..55f15553 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,623 +1,623 @@ -{ - "twoFactorAuth": { - "ariaLabel": "Configuracion de autenticacion en dos pasos", - "stepsLabel": "Pasos de configuracion", - "title": "Activar autenticacion en dos pasos", - "description": "Protege tu cuenta con una aplicacion de autenticacion. Necesitaras escanear un codigo QR para comenzar.", - "enableButton": "Activar 2FA", - "settingUp": "Configurando…", - "scanTitle": "Escanea con tu aplicacion de autenticacion", - "scanDescription": "Usa Google Authenticator, Authy o cualquier aplicacion compatible con TOTP para escanear el codigo.", - "qrCodeAlt": "Codigo QR TOTP — escanea con tu aplicacion de autenticacion", - "qrCodeSkeletonLabel": "Generando codigo QR…", - "manualKeyLabel": "O ingresa la clave manualmente", - "manualKeyAria": "Clave de configuracion manual: {key}", - "codeInputLabel": "Ingresa el codigo de 6 digitos", - "codeInputPlaceholder": "000000", - "verifyButton": "Verificar y activar", - "verifying": "Verificando…", - "successTitle": "Autenticacion en dos pasos activada", - "successDescription": "Tu cuenta esta ahora protegida. Se te pedira un codigo en cada inicio de sesion.", - "error": { - "setupFailed": "No se pudo iniciar la configuracion de 2FA. Intentalo de nuevo.", - "invalidCode": "Codigo invalido. Intentalo de nuevo.", - "codeLength": "Ingresa el codigo de 6 digitos de tu aplicacion de autenticacion." - } - }, - "localeSwitcher": { - "label": "Idioma", - "ariaLabel": "Seleccionar idioma", - "options": { - "en": "English", - "es": "Espanol", - "pt": "Portugues" - } - }, - "nav": { - "home": "Inicio", - "docs": "Docs", - "login": "Iniciar sesion", - "register": "Registrarse", - "toggleMenu": "Abrir menu" - }, - "sidebar": { - "overview": "Resumen", - "payments": "Pagos", - "webhookLogs": "Registros Webhook", - "apiKeys": "Claves API", - "settings": "Configuracion", - "workspace": "Area autenticada", - "shortcuts": "Accesos", - "createPayment": "Crear pago", - "network": "Mainnet-testnet", - "collapse": "Contraer barra lateral", - "expand": "Expandir barra lateral", - "close": "Cerrar navegacion" - }, - "breadcrumbs": { - "home": "Inicio", - "segments": { - "dashboard": "Panel", - "create": "Crear", - "payments": "Pagos", - "settings": "Configuracion", - "webhook-logs": "Registros Webhook", - "api-keys": "Claves API" - } - }, - "copyButton": { - "ariaLabel": "Copiar al portapapeles", - "copied": "Copiado", - "copiedState": "Copiado" - }, - "dashboardPage": { - "title": "Resumen del comercio", - "description": "Supervisa tu volumen de transacciones, sigue pagos en tiempo real y administra tu integracion con Stellar.", - "paymentMetrics": "Metricas de pago", - "recentActivity": "Actividad reciente", - "viewAllPayments": "Ver todos los pagos", - "quickActions": "Acciones rapidas", - "createPaymentLink": "Crear enlace de pago", - "withdrawFunds": "Retirar fondos (SEP-24)", - "development": "Desarrollo", - "apiKeysTip": "Usa la pagina de Claves API para rotar tu token secreto periodicamente.", - "webhookLogsTip": "Revisa Registros Webhook si los pagos no estan activando tu backend." - }, - "realTimeBalanceSync": { - "title": "Saldos en tiempo real", - "refreshButton": "Actualizar", - "syncing": "Sincronizando…", - "sectionAriaLabel": "Informacion de saldo en tiempo real", - "balancesListAriaLabel": "Saldos de la cuenta", - "emptyState": "No hay saldos disponibles.", - "updatedLabel": "Actualizado", - "balanceItemAriaLabel": "{asset} saldo: {balance}", - "liveRegion": { - "syncing": "Sincronizando saldos…", - "error": "Error de sincronizacion de saldos: {error}", - "updatedAt": "Saldos actualizados a las {time}." - } - }, - "createPaymentPage": { - "eyebrow": "Panel", - "title": "Crear enlace de pago", - "description": "Define un monto, elige un activo e ingresa una direccion Stellar para generar un enlace compartible.", - "newHere": "Eres nuevo?", - "registerMerchant": "Registrar una cuenta de comercio" - }, - "createPaymentForm": { - "invalidAmount": "El monto debe ser un numero positivo.", - "invalidRecipient": "El destinatario debe ser una clave publica valida de Stellar (56 caracteres, empieza con G).", - "invalidHexColor": "{field} debe ser un color hexadecimal valido.", - "failedCreate": "No se pudo crear el enlace de pago", - "createdToast": "Enlace de pago creado", - "noApiKeyTitle": "No se encontro una clave API", - "noApiKeyDescription": "Registra una cuenta de comercio para obtener tu clave API y empezar a crear enlaces de pago.", - "registerAsMerchant": "Registrarse como comercio", - "readyEyebrow": "Enlace listo", - "readyTitle": "Enlace creado correctamente", - "readyDescription": "Comparte este enlace con tu cliente para cobrar el pago.", - "shareLink": "Compartir enlace", - "shareTitle": "Enlace de pago listo", - "shareText": "Abre este enlace de pago de Stellar.", - "shareFailed": "El uso compartido nativo no esta disponible ahora mismo.", - "paymentLink": "Enlace de pago", - "paymentId": "ID del pago", - "status": "Estado", - "createAnother": "Crear otro enlace de pago", - "amount": "Monto", - "amountPlaceholder": "Ingresa {exampleAmount} {asset}", - "asset": "Activo", - "selectAsset": "Seleccionar activo", - "issuer": "Emisor", - "trustedAddresses": "Seleccionar de direcciones confiables", - "selectSavedAddress": "-- Selecciona una direccion guardada --", - "recipientAddress": "Direccion del destinatario", - "recipientPlaceholder": "Pega la billetera de destino para {asset}", - "recipientPlaceholderSelected": "{label} seleccionado para {asset}", - "descriptionLabel": "Descripcion", - "optional": "opcional", - "descriptionPlaceholder": "Para que es este cobro en {asset}?", - "brandingTitle": "Personalizacion temporal", - "brandingDescription": "Aplica colores personalizados solo a este enlace de pago.", - "enabled": "Activado", - "disabled": "Desactivado", - "primary": "Primario", - "secondary": "Secundario", - "background": "Fondo", - "checkoutPreview": "Vista previa del checkout", - "payNow": "Pagar ahora", - "generating": "Generando...", - "generate": "Generar enlace de pago", - "rateLimitError": "Estás creando enlaces demasiado rápido. Inténtalo de nuevo en {seconds} segundos.", - "retryWait": "Espera {seconds}s…", - "integrationCode": "Código de Integración", - "snippetsHelper": "Los fragmentos de código se actualizan automáticamente con los valores del formulario." - }, - "paymentMetrics": { - "downloadImage": "Descargar imagen", - "downloadPng": "Descargar PNG", - "downloadSvg": "Descargar SVG", - "exporting": "Exportando...", - "exportSuccess": "Grafico descargado como {format}", - "exportFailed": "No se pudo exportar el grafico.", - "fetchMetricsFailed": "No se pudieron cargar las metricas", - "fetchVolumeFailed": "No se pudieron cargar los datos de volumen", - "retry": "Reintentar", - "sevenDayVolume": "Volumen de 7 dias", - "totalPayments": "Pagos totales", - "paymentsCount": "{count, plural, one {pago} other {pagos}}", - "chartTitle": "Comparacion de volumen por activo", - "chartSubtitle": "Volumen diario de transacciones desglosado por activo", - "heatmapTitle": "Mapa de calor de pagos", - "heatmapSubtitle": "Intensidad de actividad estilo GitHub durante los ultimos 365 dias", - "heatmapTotal": "{count, plural, one {# pago en el ultimo ano} other {# pagos en el ultimo ano}}", - "heatmapTooltip": "{count, plural, one {# pago} other {# pagos}} el {date}", - "heatmapAriaLabel": "Mapa de calor de pagos de 365 dias", - "heatmapEmptyDay": "No se registraron pagos", - "heatmapLegendLess": "Menos", - "heatmapLegendMore": "Mas", - "heatmapWeekdays": { - "sun": "D", - "mon": "L", - "tue": "M", - "wed": "X", - "thu": "J", - "fri": "V", - "sat": "S" - }, - "showRange": "Mostrar {range}", - "toggleAssetVisibility": "Cambiar visibilidad del activo", - "showAsset": "Mostrar {asset}", - "hideAsset": "Ocultar {asset}", - "noPayments": "No hay pagos completados en este periodo.", - "weeklyAvgLabel": "Prom. 7D", - "ranges": { - "7D": "7 dias", - "30D": "30 dias", - "1Y": "1 ano" - } - }, - "paymentsPage": { - "title": "Historial de pagos", - "description": "Supervisa todas las transacciones entrantes y su estado de verificacion." - }, - "recentPayments": { - "missingApiKey": "No se encontro la clave API. Registrate o inicia sesion.", - "fetchFailed": "No se pudieron obtener los pagos", - "loadFailed": "No se pudieron cargar los pagos", - "connectionError": "Error de conexion", - "backendHint": "Asegurate de que el backend este activo y que el endpoint de pagos este disponible.", - "retryConnection": "Reintentar conexion", - "testWebhook": "Probar webhook de todos modos", - "troubleshootingTip": "Consejo de solucion", - "troubleshootingDescription": "Todavia puedes probar la funcionalidad webhook mientras se restauran los servicios del backend.", - "emptyTitle": "Todavia no hay pagos", - "emptyDescription": "Empieza a aceptar pagos creando tu primer enlace o probando webhooks para ver el flujo de datos.", - "createPaymentLink": "Crear enlace de pago", - "sendTestWebhook": "Enviar webhook de prueba", - "gettingStartedTitle": "Guia de inicio", - "gettingStartedDescription": "Usa herramientas webhook para probar notificaciones de pago y ver datos en tiempo real en este panel.", - "search": "Buscar", - "searchPlaceholder": "Buscar por ID de pago o descripcion...", - "status": "Estado", - "allStatuses": "Todos los estados", - "asset": "Activo", - "allAssets": "Todos los activos", - "fromDate": "Desde", - "toDate": "Hasta", - "activeFilters": "Filtros activos:", - "searchChip": "Busqueda: \"{value}\"", - "statusChip": "Estado: {value}", - "assetChip": "Activo: {value}", - "fromChip": "Desde: {value}", - "toChip": "Hasta: {value}", - "clearSearchFilter": "Borrar filtro de busqueda", - "clearStatusFilter": "Borrar filtro de estado", - "clearAssetFilter": "Borrar filtro de activo", - "clearFromDateFilter": "Borrar filtro de fecha inicial", - "clearToDateFilter": "Borrar filtro de fecha final", - "clearAll": "Borrar todo", - "linkCopied": "¡Enlace de pago copiado al portapapeles!", - "showingResults": "Mostrando {shown} de {total} pagos", - "filteredSuffix": "(con filtros)", - "tableStatus": "Estado", - "tableAmount": "Monto", - "tableRecipient": "Destinatario", - "tableDate": "Fecha", - "tableLink": "Enlace", - "view": "Ver", - "emptyDescriptionValue": "-", - "statuses": { - "all": "Todos", - "pending": "Pendiente", - "confirmed": "Confirmado", - "failed": "Fallido", - "refunded": "Reembolsado" - }, - "pagination": { - "ariaLabel": "Paginación del historial de pagos", - "previous": "Página anterior", - "next": "Página siguiente", - "goToPage": "Ir a la página {page}", - "currentPage": "Página actual, página {page}", - "pageLabel": "Página {page} de {totalPages}", - "range": "Mostrando {start}-{end} de {total}" - } - }, - "walletSelector": { - "chooseWallet": "Elige una billetera", - "description": "Conecta tu billetera Stellar para completar este pago.", - "connecting": "Conectando...", - "walletConnectWaiting": "Esperando la billetera...", - "noProjectId": "(sin project ID)", - "notInstalled": "(no instalada)", - "tapToConnect": "Haz clic para conectar", - "scanTitle": "Escanea con tu app de billetera Stellar", - "scanDescription": "Escanea con Freighter móvil o cualquier billetera compatible con WalletConnect", - "pairingFailed": "La vinculacion de WalletConnect fallo", - "userRejected": "La solicitud de conexión fue cancelada. Apruébala en tu billetera para continuar.", - "walletConnectUnavailable": "WalletConnect no está disponible en este momento. Prueba con Freighter o habilita un WalletConnect project ID válido.", - "extensionNotFound": "No pudimos encontrar esa billetera en este dispositivo. Instala la extensión e inténtalo de nuevo.", - "noAccountFound": "La billetera se conectó, pero no devolvió ninguna cuenta Stellar. Abre una cuenta Stellar en la billetera y vuelve a intentarlo.", - "walletConnectFailed": "No pudimos completar la conexión con WalletConnect. Inténtalo de nuevo.", - "connectionFailed": "No pudimos conectar esa billetera en este momento. Inténtalo de nuevo." - }, - "support": { - "panelAriaLabel": "Panel de ayuda y soporte", - "heading": "Ayuda y Soporte", - "title": "¿Necesitas ayuda con los pagos?", - "description": "¿Necesitas ayuda? Envíanos un mensaje o propina para obtener soporte prioritario.", - "contactEmail": "Contactar por correo", - "openChat": "Abrir chat de soporte", - "closeChat": "Cerrar chat de soporte", - "yourWallet": "Tu Billetera", - "notConnected": "No Conectada", - "connectingWallet": "Conectando…", - "amount": "Monto", - "loadingBalance": "Cargando saldo...", - "unfunded": "Sin fondos (Fondear en Testnet)", - "available": "Disponible: {balance} {asset}", - "availableAriaLabel": "Saldo disponible: {balance} {asset}", - "balanceSyncing": "Sincronizando saldo de billetera…", - "walletUnfunded": "La billetera no tiene fondos.", - "balanceSynced": "Saldo sincronizado: {balance} {asset}.", - "insufficientBalance": "Saldo insuficiente", - "messageLabel": "Deja un mensaje (opcional)", - "messagePlaceholder": "¡Gracias por el gran trabajo!", - "sendSupport": "Enviar Soporte", - "sending": "Enviando…", - "submissionFailed": "Error al enviar", - "failedToRecord": "Error al registrar la transacción de soporte" - }, - "checkout": { - "paymentRequest": "Solicitud de pago", - "completePayment": "Completar pago", - "paymentNotFound": "Pago no encontrado", - "loadFailed": "No se pudieron cargar los detalles del pago.", - "paymentMissing": "Pago no encontrado.", - "loadPaymentFailed": "No se pudo cargar el pago.", - "errorTitle": "Error", - "errorDescription": "Revisa el enlace de pago e intentalo de nuevo o contacta al remitente.", - "processingFallback": "Procesando transaccion...", - "doNotClose": "No cierres esta pestana", - "recipient": "Destinatario", - "scanToPay": "Escanear para pagar", - "scanDescription": "Escanea con Freighter o cualquier billetera Stellar", - "openQrModal": "Pagar con codigo QR", - "qrModalTitle": "Pagar con codigo QR", - "qrModalDescription": "Escanea este codigo en otro dispositivo o compartelo con tu app de billetera.", - "downloadQr": "Descargar PNG", - "downloadQrSuccess": "Codigo QR descargado.", - "downloadQrError": "No se pudo descargar el codigo QR.", - "viewRawIntent": "Ver enlace intent sin procesar", - "hideRawIntent": "Ocultar enlace intent sin procesar", - "created": "Creado", - "activeViewers": "{count, plural, one {# persona esta viendo este enlace de pago} other {# personas estan viendo este enlace de pago}}", - "transaction": "Transaccion", - "paymentConfirmed": "Pago confirmado!", - "paymentSent": "Pago enviado", - "paymentFailed": "El pago fallo. Intentalo de nuevo.", - "connectedVia": "Conectado mediante {provider}", - "payInXlm": "Pagar en XLM", - "standardAssetOption": "Pagar la factura en {asset}", - "pathPaymentOption": "Pagar en XLM", - "approximateCost": "Costo aproximado en XLM", - "merchantReceives": "El comercio recibe {amount} {asset}", - "quoteLoading": "Buscando rutas de pago en XLM...", - "quoteUnavailable": "La ruta de pago en XLM no esta disponible para esta factura ahora mismo.", - "quoteBuffer": "Puedes enviar hasta {amount} XLM para cubrir movimientos de precio.", - "pathPaymentHint": "Usa Stellar path payments desde tu billetera conectada.", - "approximateCostLabel": "Costo aproximado en XLM", - "approximateCostHelp": "Monto estimado para entregar {amount} {asset} al comercio.", - "slippageBuffer": "Incluye un margen de seguridad del {percent}%. Envio maximo: {sendMax} {asset}", - "pathPaymentTogglePrefix": "Pagar con", - "pathPaymentToggleSuffix": "en su lugar", - "processing": "Procesando...", - "payWith": "Pagar con {provider}", - "payWithFallback": "Pagar con billetera", - "reviewPaymentTitle": "Revisar pago", - "loadingNetworkFee": "Obteniendo la comision actual de red...", - "networkFeeLabel": "Comision de red: ~{amount} XLM", - "networkFeeUnavailable": "La comision de red no esta disponible en este momento.", - "cancel": "Cancelar", - "confirmPayment": "Confirmar pago", - "receivedTitle": "Este pago ya fue recibido.", - "receivedDescription": "La transaccion fue confirmada en la red Stellar.", - "downloadReceipt": "Descargar recibo", - "downloadReceiptLoading": "Preparando recibo...", - "receiptDownloaded": "La descarga del recibo ha comenzado.", - "receiptDownloadFailed": "No se pudo generar el PDF del recibo.", - "receiptHashUnavailable": "No disponible", - "failedTitle": "Este pago ha fallado.", - "failedDescription": "Contacta al comercio si crees que esto es un error.", - "status": { - "pending": "Esperando pago", - "confirmed": "Confirmado", - "completed": "Completado", - "failed": "Fallido" - } - }, - "profileCard": { - "account": "Cuenta", - "openProfileMenu": "Abrir menú de perfil", - "dashboard": "Panel", - "settings": "Configuración", - "createPayment": "Crear pago", - "logoutAccount": "Cerrar sesión", - "loadingProfile": "Cargando perfil…" - }, - "settingsPage": { - "eyebrow": "Configuración", - "eyebrowAccount": "Cuenta", - "title": "Configuración", - "description": "Administra tus credenciales, marca e integraciones.", - "loadingSettings": "Cargando configuración…", - "noApiKeyTitle": "No se encontró clave API", - "noApiKeyDescription": "Registra una cuenta de comercio primero para administrar tus credenciales aquí.", - "registerAsMerchant": "Registrarse como Comercio", - "navAriaLabel": "Navegación de configuración", - "navApiKeys": "Claves API", - "navBranding": "Marca", - "navDisplay": "Pantalla", - "navWebhooks": "Webhooks", - "navPermissions": "Permisos", - "navDanger": "Zona de Peligro", - "apiAuthTitle": "Autenticación API", - "apiAuthDescription": "Tu clave secreta para solicitudes API del lado del servidor.", - "liveApiKey": "Clave API en Vivo", - "hide": "Ocultar", - "reveal": "Revelar", - "apiKeyHeaderHintPrefix": "Envíala como el encabezado", - "apiKeyHeaderHintSuffix": "en cada solicitud.", - "apiKeyVisible": "La clave API en vivo está visible actualmente.", - "apiKeyHidden": "La clave API en vivo está oculta actualmente.", - "rotateApiKeyTitle": "Rotar Clave API", - "rotateApiKeyDescription": "Genera una nueva clave e invalida inmediatamente la actual.", - "rotateKeyEllipsis": "Rotar Clave…", - "confirmAction": "Confirmar Acción", - "rotateKeyWarning": "La clave anterior dejará de funcionar inmediatamente.", - "confirm": "Confirmar", - "rotating": "Rotando…", - "cancel": "Cancelar", - "checkoutBrandingTitle": "Marca del Checkout", - "checkoutBrandingDescription": "Personaliza la apariencia de tu página de pago.", - "storeLogo": "Logo de la Tienda", - "clickOrDragToReplace": "Haz clic o arrastra para reemplazar", - "dropHere": "Suelta aquí", - "uploadLogo": "Subir logo", - "logoFormats": "PNG, JPG o SVG · máx 2MB", - "removeLogo": "Eliminar logo", - "colors": "Colores", - "primary": "Primario", - "secondary": "Secundario", - "background": "Fondo", - "preview": "Vista previa", - "sampleCheckout": "Checkout de ejemplo", - "payNow": "Pagar Ahora", - "lowContrastWarning": "Los colores podrían no cumplir los objetivos de contraste WCAG. Considera ajustarlos.", - "saving": "Guardando…", - "saveBranding": "Guardar Marca", - "previewReceipt": "Vista Previa del Recibo", - "displayPreferencesTitle": "Preferencias de Pantalla", - "displayPreferencesDescription": "Ajusta cómo aparecen los valores en el panel.", - "hideTrailingCents": "Ocultar centavos finales", - "hideTrailingCentsDescription": "Los montos enteros como 50 se mostrarán sin el sufijo .00.", - "webhookEndpointTitle": "Punto de Enlace Webhook", - "webhookEndpointDescription": "Recibe eventos de pago en tiempo real en tu servidor.", - "verified": "Verificado", - "unverified": "No verificado", - "endpointUrl": "URL del Punto de Enlace", - "saveUrl": "Guardar URL", - "testing": "Probando…", - "sendTest": "Enviar Prueba", - "domainVerification": "Verificación de Dominio", - "hostTokenAt": "Aloja este token en", - "verifying": "Verificando…", - "verifyDomain": "Verificar Dominio", - "signingSecretTitle": "Secreto de Firma", - "signingSecretDescriptionPrefix": "Valida el encabezado", - "signingSecretDescriptionSuffix": "en los webhooks entrantes.", - "hideWebhookSecret": "Ocultar secreto del webhook", - "showWebhookSecret": "Mostrar secreto del webhook", - "webhookSecretVisible": "El secreto del webhook está visible. Cópialo ahora — se muestra una sola vez.", - "webhookSecretHidden": "El secreto del webhook está oculto. Alterna para inspeccionarlo.", - "regenerateSecretEllipsis": "Regenerar Secreto…", - "regenerateSecretWarning": "El secreto actual dejará de funcionar inmediatamente.", - "regenerating": "Regenerando…", - "dangerZoneDescription": "Acciones irreversibles. Procede con precaución.", - "failedToLoadBranding": "Error al cargar la marca", - "failedToLoadWebhookSettings": "Error al cargar la configuración del webhook", - "failedToRotateKey": "Error al rotar la clave", - "apiKeyRotatedSuccess": "Clave API rotada — actualiza cualquier integración que use la clave anterior.", - "imageSizeLimit": "La imagen debe ser menor a 2MB", - "logoUploaded": "¡Logo subido!", - "hexColorRequired": "{field} debe ser un color hexadecimal válido", - "failedToSaveBranding": "Error al guardar la marca", - "brandingSaved": "Marca guardada", - "webhookUrlMustBeHttps": "La URL del webhook debe usar HTTPS", - "webhookUrlInvalid": "URL inválida (ej. https://example.com/webhook)", - "failedToSaveWebhookUrl": "Error al guardar la URL del webhook", - "webhookUrlSaved": "URL del webhook guardada", - "webhookUrlCleared": "URL del webhook eliminada", - "failedToVerifyDomain": "Error al verificar el dominio del webhook", - "domainVerified": "Dominio verificado", - "domainStillUnverified": "El dominio sigue sin verificarse", - "failedToRegenerateSecret": "Error al regenerar el secreto", - "webhookSecretRegenerated": "Secreto del webhook regenerado — actualiza tus integraciones.", - "testWebhookRequestFailed": "Falló la solicitud de prueba del webhook", - "failedToTestWebhook": "Error al probar el webhook", - "testWebhookSentStatus": "Webhook de prueba enviado — estado {status}", - "colorPickerLabel": "Selector de color {label}" - }, - "notificationCenter": { - "buttonLabel": "Notificaciones", - "buttonLabelWithCount": "Notificaciones ({count} sin leer)", - "panelLabel": "Centro de Notificaciones", - "heading": "Notificaciones", - "unreadCount": "{count} sin leer", - "noAlerts": "Sin nuevas alertas", - "alertLabel": "Alerta", - "dismissLabel": "Descartar notificación: {message}", - "notificationLabel": "Notificación: {message}", - "timestampLabel": "Marca de tiempo: {timestamp}" - }, - "fiatOnramp": { - "title": "Comprar / Depositar Fondos", - "triggerLabel": "Comprar / Depositar", - "description": "Deposita fiat a traves de un anchor de Stellar (SEP-0024). Los fondos llegan como tokens directamente a tu billetera conectada.", - "selectAsset": "Seleccionar Activo", - "amountLabel": "Monto", - "amountOptional": "(opcional)", - "amountPlaceholder": "ej. {amount} {asset}", - "anchorDomainLabel": "Dominio del Anchor", - "anchorDomainPlaceholder": "ej. testanchor.stellar.org", - "continueButton": "Continuar al Anchor", - "footerNote": "Protegido por la red Stellar - Estandar SEP-0024", - "stepConnecting": "Conectando con el anchor...", - "stepAuth": "Esperando firma de la billetera...", - "stepGenerating": "Preparando tu formulario de deposito...", - "authHint": "Firma la transaccion de desafio en tu billetera para conectarte de forma segura a {domain}.", - "genericHint": "Esto solo deberia tomar un momento.", - "iframeTitle": "Formulario de deposito del anchor", - "genericError": "El deposito fallo" - }, - "kycPage": { - "title": "Verificacion KYC", - "description": "Completa el proceso de verificacion para cumplir con los requisitos regulatorios y desbloquear el acceso completo a las funciones de la plataforma.", - "whyKyc": "Por que KYC?", - "reasonComply": "Cumplir con los requisitos regulatorios", - "reasonLimits": "Aumentar los limites de transaccion", - "reasonFeatures": "Acceder a funciones premium", - "reasonSecurity": "Seguridad mejorada de la cuenta" - }, - "kycForm": { - "formTitle": "Formulario de envio KYC", - "step": "Paso", - "of": "de", - "steps": "Pasos", - "completed": "completado", - "current": "actual", - "upcoming": "proximo", - "personalInfo": "Informacion Personal", - "addressInfo": "Informacion de Direccion", - "documents": "Documentos", - "review": "Revisar", - "firstName": "Nombre", - "lastName": "Apellido", - "email": "Correo electronico", - "nationality": "Nacionalidad", - "dateOfBirth": "Fecha de nacimiento", - "street": "Calle", - "city": "Ciudad", - "state": "Estado", - "postalCode": "Codigo postal", - "country": "Pais", - "idType": "Tipo de ID", - "selectIdType": "Seleccionar tipo de ID", - "passport": "Pasaporte", - "driversLicense": "Licencia de conducir", - "nationalId": "ID Nacional", - "idNumber": "Numero de ID", - "idFront": "Frente del ID", - "idBack": "Reverso del ID", - "selfie": "Foto selfie", - "required": "Requerido", - "validationError": "Por favor completa todos los campos requeridos", - "back": "Atras", - "next": "Siguiente", - "submit": "Enviar KYC", - "submitting": "Enviando...", - "submitError": "Error al enviar", - "submitAnother": "Enviar otro", - "successTitle": "KYC enviado exitosamente!", - "successDescription": "Tu verificacion KYC ha sido enviada y esta en revision.", - "dash": "—" - }, - "darkModeTheme": { - "loading": "Cargando configuracion del tema", - "errorCleared": "Error eliminado. Intentando cambiar el tema.", - "switchingTo": "Cambiando al tema {theme}.", - "themeChanged": "Tema cambiado a {theme}.", - "themeError": "Error al cambiar el tema. Intentalo de nuevo.", - "currentTheme": "Tema actual: {theme}", - "systemTheme": "Sistema ({theme})", - "ariaError": "Cambio de tema con error — presiona para reintentar", - "ariaLabel": "Cambio de tema, tema actual: {theme}. Presiona para pasar al siguiente tema.", - "ariaLoading": "El tema esta cargando", - "errorTitle": "Error de tema: {error}. Presiona para reintentar.", - "titleSystem": "Tema: Sistema ({theme})", - "titleLight": "Tema: Claro", - "titleDark": "Tema: Oscuro", - "description": "Cambia entre los temas claro, oscuro y del sistema. El sistema sigue la preferencia del dispositivo. Presiona Espacio o Intro para activar.", - "preferenceLabel": "Preferencia de tema", - "preferenceSaved": "Preferencia de tema guardada", - "preferenceRestored": "Preferencia de tema restaurada", - "themes": { - "light": "claro", - "dark": "oscuro", - "system": "sistema" - }, - "themeNames": { - "light": "Claro", - "dark": "Oscuro", - "system": "Sistema" - }, - "sr": { - "sunIcon": "Icono de sol — modo claro activo", - "moonIcon": "Icono de luna — modo oscuro activo", - "systemIcon": "Icono de monitor — tema del sistema activo", - "errorIcon": "Icono de error — no se pudo aplicar el tema" - } - }, - "portfolioChartWidget": { - "title": "Valor del portafolio", - "allocation": "Distribucion", - "trend": "Tendencia", - "loading": "Cargando grafico del portafolio...", - "historyLoading": "Cargando historial de rendimiento...", - "emptyAllocation": "Todavia no hay datos de asignacion.", - "emptyAssets": "Todavia no hay activos en el portafolio.", - "emptyHistory": "Todavia no hay historial de rendimiento." - } +{ + "twoFactorAuth": { + "ariaLabel": "Configuracion de autenticacion en dos pasos", + "stepsLabel": "Pasos de configuracion", + "title": "Activar autenticacion en dos pasos", + "description": "Protege tu cuenta con una aplicacion de autenticacion. Necesitaras escanear un codigo QR para comenzar.", + "enableButton": "Activar 2FA", + "settingUp": "Configurando…", + "scanTitle": "Escanea con tu aplicacion de autenticacion", + "scanDescription": "Usa Google Authenticator, Authy o cualquier aplicacion compatible con TOTP para escanear el codigo.", + "qrCodeAlt": "Codigo QR TOTP — escanea con tu aplicacion de autenticacion", + "qrCodeSkeletonLabel": "Generando codigo QR…", + "manualKeyLabel": "O ingresa la clave manualmente", + "manualKeyAria": "Clave de configuracion manual: {key}", + "codeInputLabel": "Ingresa el codigo de 6 digitos", + "codeInputPlaceholder": "000000", + "verifyButton": "Verificar y activar", + "verifying": "Verificando…", + "successTitle": "Autenticacion en dos pasos activada", + "successDescription": "Tu cuenta esta ahora protegida. Se te pedira un codigo en cada inicio de sesion.", + "error": { + "setupFailed": "No se pudo iniciar la configuracion de 2FA. Intentalo de nuevo.", + "invalidCode": "Codigo invalido. Intentalo de nuevo.", + "codeLength": "Ingresa el codigo de 6 digitos de tu aplicacion de autenticacion." + } + }, + "localeSwitcher": { + "label": "Idioma", + "ariaLabel": "Seleccionar idioma", + "options": { + "en": "English", + "es": "Espanol", + "pt": "Portugues" + } + }, + "nav": { + "home": "Inicio", + "docs": "Docs", + "login": "Iniciar sesion", + "register": "Registrarse", + "toggleMenu": "Abrir menu" + }, + "sidebar": { + "overview": "Resumen", + "payments": "Pagos", + "webhookLogs": "Registros Webhook", + "apiKeys": "Claves API", + "settings": "Configuracion", + "workspace": "Area autenticada", + "shortcuts": "Accesos", + "createPayment": "Crear pago", + "network": "Mainnet-testnet", + "collapse": "Contraer barra lateral", + "expand": "Expandir barra lateral", + "close": "Cerrar navegacion" + }, + "breadcrumbs": { + "home": "Inicio", + "segments": { + "dashboard": "Panel", + "create": "Crear", + "payments": "Pagos", + "settings": "Configuracion", + "webhook-logs": "Registros Webhook", + "api-keys": "Claves API" + } + }, + "copyButton": { + "ariaLabel": "Copiar al portapapeles", + "copied": "Copiado", + "copiedState": "Copiado" + }, + "dashboardPage": { + "title": "Resumen del comercio", + "description": "Supervisa tu volumen de transacciones, sigue pagos en tiempo real y administra tu integracion con Stellar.", + "paymentMetrics": "Metricas de pago", + "recentActivity": "Actividad reciente", + "viewAllPayments": "Ver todos los pagos", + "quickActions": "Acciones rapidas", + "createPaymentLink": "Crear enlace de pago", + "withdrawFunds": "Retirar fondos (SEP-24)", + "development": "Desarrollo", + "apiKeysTip": "Usa la pagina de Claves API para rotar tu token secreto periodicamente.", + "webhookLogsTip": "Revisa Registros Webhook si los pagos no estan activando tu backend." + }, + "realTimeBalanceSync": { + "title": "Saldos en tiempo real", + "refreshButton": "Actualizar", + "syncing": "Sincronizando…", + "sectionAriaLabel": "Informacion de saldo en tiempo real", + "balancesListAriaLabel": "Saldos de la cuenta", + "emptyState": "No hay saldos disponibles.", + "updatedLabel": "Actualizado", + "balanceItemAriaLabel": "{asset} saldo: {balance}", + "liveRegion": { + "syncing": "Sincronizando saldos…", + "error": "Error de sincronizacion de saldos: {error}", + "updatedAt": "Saldos actualizados a las {time}." + } + }, + "createPaymentPage": { + "eyebrow": "Panel", + "title": "Crear enlace de pago", + "description": "Define un monto, elige un activo e ingresa una direccion Stellar para generar un enlace compartible.", + "newHere": "Eres nuevo?", + "registerMerchant": "Registrar una cuenta de comercio" + }, + "createPaymentForm": { + "invalidAmount": "El monto debe ser un numero positivo.", + "invalidRecipient": "El destinatario debe ser una clave publica valida de Stellar (56 caracteres, empieza con G).", + "invalidHexColor": "{field} debe ser un color hexadecimal valido.", + "failedCreate": "No se pudo crear el enlace de pago", + "createdToast": "Enlace de pago creado", + "noApiKeyTitle": "No se encontro una clave API", + "noApiKeyDescription": "Registra una cuenta de comercio para obtener tu clave API y empezar a crear enlaces de pago.", + "registerAsMerchant": "Registrarse como comercio", + "readyEyebrow": "Enlace listo", + "readyTitle": "Enlace creado correctamente", + "readyDescription": "Comparte este enlace con tu cliente para cobrar el pago.", + "shareLink": "Compartir enlace", + "shareTitle": "Enlace de pago listo", + "shareText": "Abre este enlace de pago de Stellar.", + "shareFailed": "El uso compartido nativo no esta disponible ahora mismo.", + "paymentLink": "Enlace de pago", + "paymentId": "ID del pago", + "status": "Estado", + "createAnother": "Crear otro enlace de pago", + "amount": "Monto", + "amountPlaceholder": "Ingresa {exampleAmount} {asset}", + "asset": "Activo", + "selectAsset": "Seleccionar activo", + "issuer": "Emisor", + "trustedAddresses": "Seleccionar de direcciones confiables", + "selectSavedAddress": "-- Selecciona una direccion guardada --", + "recipientAddress": "Direccion del destinatario", + "recipientPlaceholder": "Pega la billetera de destino para {asset}", + "recipientPlaceholderSelected": "{label} seleccionado para {asset}", + "descriptionLabel": "Descripcion", + "optional": "opcional", + "descriptionPlaceholder": "Para que es este cobro en {asset}?", + "brandingTitle": "Personalizacion temporal", + "brandingDescription": "Aplica colores personalizados solo a este enlace de pago.", + "enabled": "Activado", + "disabled": "Desactivado", + "primary": "Primario", + "secondary": "Secundario", + "background": "Fondo", + "checkoutPreview": "Vista previa del checkout", + "payNow": "Pagar ahora", + "generating": "Generando...", + "generate": "Generar enlace de pago", + "rateLimitError": "Estás creando enlaces demasiado rápido. Inténtalo de nuevo en {seconds} segundos.", + "retryWait": "Espera {seconds}s…", + "integrationCode": "Código de Integración", + "snippetsHelper": "Los fragmentos de código se actualizan automáticamente con los valores del formulario." + }, + "paymentMetrics": { + "downloadImage": "Descargar imagen", + "downloadPng": "Descargar PNG", + "downloadSvg": "Descargar SVG", + "exporting": "Exportando...", + "exportSuccess": "Grafico descargado como {format}", + "exportFailed": "No se pudo exportar el grafico.", + "fetchMetricsFailed": "No se pudieron cargar las metricas", + "fetchVolumeFailed": "No se pudieron cargar los datos de volumen", + "retry": "Reintentar", + "sevenDayVolume": "Volumen de 7 dias", + "totalPayments": "Pagos totales", + "paymentsCount": "{count, plural, one {pago} other {pagos}}", + "chartTitle": "Comparacion de volumen por activo", + "chartSubtitle": "Volumen diario de transacciones desglosado por activo", + "heatmapTitle": "Mapa de calor de pagos", + "heatmapSubtitle": "Intensidad de actividad estilo GitHub durante los ultimos 365 dias", + "heatmapTotal": "{count, plural, one {# pago en el ultimo ano} other {# pagos en el ultimo ano}}", + "heatmapTooltip": "{count, plural, one {# pago} other {# pagos}} el {date}", + "heatmapAriaLabel": "Mapa de calor de pagos de 365 dias", + "heatmapEmptyDay": "No se registraron pagos", + "heatmapLegendLess": "Menos", + "heatmapLegendMore": "Mas", + "heatmapWeekdays": { + "sun": "D", + "mon": "L", + "tue": "M", + "wed": "X", + "thu": "J", + "fri": "V", + "sat": "S" + }, + "showRange": "Mostrar {range}", + "toggleAssetVisibility": "Cambiar visibilidad del activo", + "showAsset": "Mostrar {asset}", + "hideAsset": "Ocultar {asset}", + "noPayments": "No hay pagos completados en este periodo.", + "weeklyAvgLabel": "Prom. 7D", + "ranges": { + "7D": "7 dias", + "30D": "30 dias", + "1Y": "1 ano" + } + }, + "paymentsPage": { + "title": "Historial de pagos", + "description": "Supervisa todas las transacciones entrantes y su estado de verificacion." + }, + "recentPayments": { + "missingApiKey": "No se encontro la clave API. Registrate o inicia sesion.", + "fetchFailed": "No se pudieron obtener los pagos", + "loadFailed": "No se pudieron cargar los pagos", + "connectionError": "Error de conexion", + "backendHint": "Asegurate de que el backend este activo y que el endpoint de pagos este disponible.", + "retryConnection": "Reintentar conexion", + "testWebhook": "Probar webhook de todos modos", + "troubleshootingTip": "Consejo de solucion", + "troubleshootingDescription": "Todavia puedes probar la funcionalidad webhook mientras se restauran los servicios del backend.", + "emptyTitle": "Todavia no hay pagos", + "emptyDescription": "Empieza a aceptar pagos creando tu primer enlace o probando webhooks para ver el flujo de datos.", + "createPaymentLink": "Crear enlace de pago", + "sendTestWebhook": "Enviar webhook de prueba", + "gettingStartedTitle": "Guia de inicio", + "gettingStartedDescription": "Usa herramientas webhook para probar notificaciones de pago y ver datos en tiempo real en este panel.", + "search": "Buscar", + "searchPlaceholder": "Buscar por ID de pago o descripcion...", + "status": "Estado", + "allStatuses": "Todos los estados", + "asset": "Activo", + "allAssets": "Todos los activos", + "fromDate": "Desde", + "toDate": "Hasta", + "activeFilters": "Filtros activos:", + "searchChip": "Busqueda: \"{value}\"", + "statusChip": "Estado: {value}", + "assetChip": "Activo: {value}", + "fromChip": "Desde: {value}", + "toChip": "Hasta: {value}", + "clearSearchFilter": "Borrar filtro de busqueda", + "clearStatusFilter": "Borrar filtro de estado", + "clearAssetFilter": "Borrar filtro de activo", + "clearFromDateFilter": "Borrar filtro de fecha inicial", + "clearToDateFilter": "Borrar filtro de fecha final", + "clearAll": "Borrar todo", + "linkCopied": "¡Enlace de pago copiado al portapapeles!", + "showingResults": "Mostrando {shown} de {total} pagos", + "filteredSuffix": "(con filtros)", + "tableStatus": "Estado", + "tableAmount": "Monto", + "tableRecipient": "Destinatario", + "tableDate": "Fecha", + "tableLink": "Enlace", + "view": "Ver", + "emptyDescriptionValue": "-", + "statuses": { + "all": "Todos", + "pending": "Pendiente", + "confirmed": "Confirmado", + "failed": "Fallido", + "refunded": "Reembolsado" + }, + "pagination": { + "ariaLabel": "Paginación del historial de pagos", + "previous": "Página anterior", + "next": "Página siguiente", + "goToPage": "Ir a la página {page}", + "currentPage": "Página actual, página {page}", + "pageLabel": "Página {page} de {totalPages}", + "range": "Mostrando {start}-{end} de {total}" + } + }, + "walletSelector": { + "chooseWallet": "Elige una billetera", + "description": "Conecta tu billetera Stellar para completar este pago.", + "connecting": "Conectando...", + "walletConnectWaiting": "Esperando la billetera...", + "noProjectId": "(sin project ID)", + "notInstalled": "(no instalada)", + "tapToConnect": "Haz clic para conectar", + "scanTitle": "Escanea con tu app de billetera Stellar", + "scanDescription": "Escanea con Freighter móvil o cualquier billetera compatible con WalletConnect", + "pairingFailed": "La vinculacion de WalletConnect fallo", + "userRejected": "La solicitud de conexión fue cancelada. Apruébala en tu billetera para continuar.", + "walletConnectUnavailable": "WalletConnect no está disponible en este momento. Prueba con Freighter o habilita un WalletConnect project ID válido.", + "extensionNotFound": "No pudimos encontrar esa billetera en este dispositivo. Instala la extensión e inténtalo de nuevo.", + "noAccountFound": "La billetera se conectó, pero no devolvió ninguna cuenta Stellar. Abre una cuenta Stellar en la billetera y vuelve a intentarlo.", + "walletConnectFailed": "No pudimos completar la conexión con WalletConnect. Inténtalo de nuevo.", + "connectionFailed": "No pudimos conectar esa billetera en este momento. Inténtalo de nuevo." + }, + "support": { + "panelAriaLabel": "Panel de ayuda y soporte", + "heading": "Ayuda y Soporte", + "title": "¿Necesitas ayuda con los pagos?", + "description": "¿Necesitas ayuda? Envíanos un mensaje o propina para obtener soporte prioritario.", + "contactEmail": "Contactar por correo", + "openChat": "Abrir chat de soporte", + "closeChat": "Cerrar chat de soporte", + "yourWallet": "Tu Billetera", + "notConnected": "No Conectada", + "connectingWallet": "Conectando…", + "amount": "Monto", + "loadingBalance": "Cargando saldo...", + "unfunded": "Sin fondos (Fondear en Testnet)", + "available": "Disponible: {balance} {asset}", + "availableAriaLabel": "Saldo disponible: {balance} {asset}", + "balanceSyncing": "Sincronizando saldo de billetera…", + "walletUnfunded": "La billetera no tiene fondos.", + "balanceSynced": "Saldo sincronizado: {balance} {asset}.", + "insufficientBalance": "Saldo insuficiente", + "messageLabel": "Deja un mensaje (opcional)", + "messagePlaceholder": "¡Gracias por el gran trabajo!", + "sendSupport": "Enviar Soporte", + "sending": "Enviando…", + "submissionFailed": "Error al enviar", + "failedToRecord": "Error al registrar la transacción de soporte" + }, + "checkout": { + "paymentRequest": "Solicitud de pago", + "completePayment": "Completar pago", + "paymentNotFound": "Pago no encontrado", + "loadFailed": "No se pudieron cargar los detalles del pago.", + "paymentMissing": "Pago no encontrado.", + "loadPaymentFailed": "No se pudo cargar el pago.", + "errorTitle": "Error", + "errorDescription": "Revisa el enlace de pago e intentalo de nuevo o contacta al remitente.", + "processingFallback": "Procesando transaccion...", + "doNotClose": "No cierres esta pestana", + "recipient": "Destinatario", + "scanToPay": "Escanear para pagar", + "scanDescription": "Escanea con Freighter o cualquier billetera Stellar", + "openQrModal": "Pagar con codigo QR", + "qrModalTitle": "Pagar con codigo QR", + "qrModalDescription": "Escanea este codigo en otro dispositivo o compartelo con tu app de billetera.", + "downloadQr": "Descargar PNG", + "downloadQrSuccess": "Codigo QR descargado.", + "downloadQrError": "No se pudo descargar el codigo QR.", + "viewRawIntent": "Ver enlace intent sin procesar", + "hideRawIntent": "Ocultar enlace intent sin procesar", + "created": "Creado", + "activeViewers": "{count, plural, one {# persona esta viendo este enlace de pago} other {# personas estan viendo este enlace de pago}}", + "transaction": "Transaccion", + "paymentConfirmed": "Pago confirmado!", + "paymentSent": "Pago enviado", + "paymentFailed": "El pago fallo. Intentalo de nuevo.", + "connectedVia": "Conectado mediante {provider}", + "payInXlm": "Pagar en XLM", + "standardAssetOption": "Pagar la factura en {asset}", + "pathPaymentOption": "Pagar en XLM", + "approximateCost": "Costo aproximado en XLM", + "merchantReceives": "El comercio recibe {amount} {asset}", + "quoteLoading": "Buscando rutas de pago en XLM...", + "quoteUnavailable": "La ruta de pago en XLM no esta disponible para esta factura ahora mismo.", + "quoteBuffer": "Puedes enviar hasta {amount} XLM para cubrir movimientos de precio.", + "pathPaymentHint": "Usa Stellar path payments desde tu billetera conectada.", + "approximateCostLabel": "Costo aproximado en XLM", + "approximateCostHelp": "Monto estimado para entregar {amount} {asset} al comercio.", + "slippageBuffer": "Incluye un margen de seguridad del {percent}%. Envio maximo: {sendMax} {asset}", + "pathPaymentTogglePrefix": "Pagar con", + "pathPaymentToggleSuffix": "en su lugar", + "processing": "Procesando...", + "payWith": "Pagar con {provider}", + "payWithFallback": "Pagar con billetera", + "reviewPaymentTitle": "Revisar pago", + "loadingNetworkFee": "Obteniendo la comision actual de red...", + "networkFeeLabel": "Comision de red: ~{amount} XLM", + "networkFeeUnavailable": "La comision de red no esta disponible en este momento.", + "cancel": "Cancelar", + "confirmPayment": "Confirmar pago", + "receivedTitle": "Este pago ya fue recibido.", + "receivedDescription": "La transaccion fue confirmada en la red Stellar.", + "downloadReceipt": "Descargar recibo", + "downloadReceiptLoading": "Preparando recibo...", + "receiptDownloaded": "La descarga del recibo ha comenzado.", + "receiptDownloadFailed": "No se pudo generar el PDF del recibo.", + "receiptHashUnavailable": "No disponible", + "failedTitle": "Este pago ha fallado.", + "failedDescription": "Contacta al comercio si crees que esto es un error.", + "status": { + "pending": "Esperando pago", + "confirmed": "Confirmado", + "completed": "Completado", + "failed": "Fallido" + } + }, + "profileCard": { + "account": "Cuenta", + "openProfileMenu": "Abrir menú de perfil", + "dashboard": "Panel", + "settings": "Configuración", + "createPayment": "Crear pago", + "logoutAccount": "Cerrar sesión", + "loadingProfile": "Cargando perfil…" + }, + "settingsPage": { + "eyebrow": "Configuración", + "eyebrowAccount": "Cuenta", + "title": "Configuración", + "description": "Administra tus credenciales, marca e integraciones.", + "loadingSettings": "Cargando configuración…", + "noApiKeyTitle": "No se encontró clave API", + "noApiKeyDescription": "Registra una cuenta de comercio primero para administrar tus credenciales aquí.", + "registerAsMerchant": "Registrarse como Comercio", + "navAriaLabel": "Navegación de configuración", + "navApiKeys": "Claves API", + "navBranding": "Marca", + "navDisplay": "Pantalla", + "navWebhooks": "Webhooks", + "navPermissions": "Permisos", + "navDanger": "Zona de Peligro", + "apiAuthTitle": "Autenticación API", + "apiAuthDescription": "Tu clave secreta para solicitudes API del lado del servidor.", + "liveApiKey": "Clave API en Vivo", + "hide": "Ocultar", + "reveal": "Revelar", + "apiKeyHeaderHintPrefix": "Envíala como el encabezado", + "apiKeyHeaderHintSuffix": "en cada solicitud.", + "apiKeyVisible": "La clave API en vivo está visible actualmente.", + "apiKeyHidden": "La clave API en vivo está oculta actualmente.", + "rotateApiKeyTitle": "Rotar Clave API", + "rotateApiKeyDescription": "Genera una nueva clave e invalida inmediatamente la actual.", + "rotateKeyEllipsis": "Rotar Clave…", + "confirmAction": "Confirmar Acción", + "rotateKeyWarning": "La clave anterior dejará de funcionar inmediatamente.", + "confirm": "Confirmar", + "rotating": "Rotando…", + "cancel": "Cancelar", + "checkoutBrandingTitle": "Marca del Checkout", + "checkoutBrandingDescription": "Personaliza la apariencia de tu página de pago.", + "storeLogo": "Logo de la Tienda", + "clickOrDragToReplace": "Haz clic o arrastra para reemplazar", + "dropHere": "Suelta aquí", + "uploadLogo": "Subir logo", + "logoFormats": "PNG, JPG o SVG · máx 2MB", + "removeLogo": "Eliminar logo", + "colors": "Colores", + "primary": "Primario", + "secondary": "Secundario", + "background": "Fondo", + "preview": "Vista previa", + "sampleCheckout": "Checkout de ejemplo", + "payNow": "Pagar Ahora", + "lowContrastWarning": "Los colores podrían no cumplir los objetivos de contraste WCAG. Considera ajustarlos.", + "saving": "Guardando…", + "saveBranding": "Guardar Marca", + "previewReceipt": "Vista Previa del Recibo", + "displayPreferencesTitle": "Preferencias de Pantalla", + "displayPreferencesDescription": "Ajusta cómo aparecen los valores en el panel.", + "hideTrailingCents": "Ocultar centavos finales", + "hideTrailingCentsDescription": "Los montos enteros como 50 se mostrarán sin el sufijo .00.", + "webhookEndpointTitle": "Punto de Enlace Webhook", + "webhookEndpointDescription": "Recibe eventos de pago en tiempo real en tu servidor.", + "verified": "Verificado", + "unverified": "No verificado", + "endpointUrl": "URL del Punto de Enlace", + "saveUrl": "Guardar URL", + "testing": "Probando…", + "sendTest": "Enviar Prueba", + "domainVerification": "Verificación de Dominio", + "hostTokenAt": "Aloja este token en", + "verifying": "Verificando…", + "verifyDomain": "Verificar Dominio", + "signingSecretTitle": "Secreto de Firma", + "signingSecretDescriptionPrefix": "Valida el encabezado", + "signingSecretDescriptionSuffix": "en los webhooks entrantes.", + "hideWebhookSecret": "Ocultar secreto del webhook", + "showWebhookSecret": "Mostrar secreto del webhook", + "webhookSecretVisible": "El secreto del webhook está visible. Cópialo ahora — se muestra una sola vez.", + "webhookSecretHidden": "El secreto del webhook está oculto. Alterna para inspeccionarlo.", + "regenerateSecretEllipsis": "Regenerar Secreto…", + "regenerateSecretWarning": "El secreto actual dejará de funcionar inmediatamente.", + "regenerating": "Regenerando…", + "dangerZoneDescription": "Acciones irreversibles. Procede con precaución.", + "failedToLoadBranding": "Error al cargar la marca", + "failedToLoadWebhookSettings": "Error al cargar la configuración del webhook", + "failedToRotateKey": "Error al rotar la clave", + "apiKeyRotatedSuccess": "Clave API rotada — actualiza cualquier integración que use la clave anterior.", + "imageSizeLimit": "La imagen debe ser menor a 2MB", + "logoUploaded": "¡Logo subido!", + "hexColorRequired": "{field} debe ser un color hexadecimal válido", + "failedToSaveBranding": "Error al guardar la marca", + "brandingSaved": "Marca guardada", + "webhookUrlMustBeHttps": "La URL del webhook debe usar HTTPS", + "webhookUrlInvalid": "URL inválida (ej. https://example.com/webhook)", + "failedToSaveWebhookUrl": "Error al guardar la URL del webhook", + "webhookUrlSaved": "URL del webhook guardada", + "webhookUrlCleared": "URL del webhook eliminada", + "failedToVerifyDomain": "Error al verificar el dominio del webhook", + "domainVerified": "Dominio verificado", + "domainStillUnverified": "El dominio sigue sin verificarse", + "failedToRegenerateSecret": "Error al regenerar el secreto", + "webhookSecretRegenerated": "Secreto del webhook regenerado — actualiza tus integraciones.", + "testWebhookRequestFailed": "Falló la solicitud de prueba del webhook", + "failedToTestWebhook": "Error al probar el webhook", + "testWebhookSentStatus": "Webhook de prueba enviado — estado {status}", + "colorPickerLabel": "Selector de color {label}" + }, + "notificationCenter": { + "buttonLabel": "Notificaciones", + "buttonLabelWithCount": "Notificaciones ({count} sin leer)", + "panelLabel": "Centro de Notificaciones", + "heading": "Notificaciones", + "unreadCount": "{count} sin leer", + "noAlerts": "Sin nuevas alertas", + "alertLabel": "Alerta", + "dismissLabel": "Descartar notificación: {message}", + "notificationLabel": "Notificación: {message}", + "timestampLabel": "Marca de tiempo: {timestamp}" + }, + "fiatOnramp": { + "title": "Comprar / Depositar Fondos", + "triggerLabel": "Comprar / Depositar", + "description": "Deposita fiat a traves de un anchor de Stellar (SEP-0024). Los fondos llegan como tokens directamente a tu billetera conectada.", + "selectAsset": "Seleccionar Activo", + "amountLabel": "Monto", + "amountOptional": "(opcional)", + "amountPlaceholder": "ej. {amount} {asset}", + "anchorDomainLabel": "Dominio del Anchor", + "anchorDomainPlaceholder": "ej. testanchor.stellar.org", + "continueButton": "Continuar al Anchor", + "footerNote": "Protegido por la red Stellar - Estandar SEP-0024", + "stepConnecting": "Conectando con el anchor...", + "stepAuth": "Esperando firma de la billetera...", + "stepGenerating": "Preparando tu formulario de deposito...", + "authHint": "Firma la transaccion de desafio en tu billetera para conectarte de forma segura a {domain}.", + "genericHint": "Esto solo deberia tomar un momento.", + "iframeTitle": "Formulario de deposito del anchor", + "genericError": "El deposito fallo" + }, + "kycPage": { + "title": "Verificacion KYC", + "description": "Completa el proceso de verificacion para cumplir con los requisitos regulatorios y desbloquear el acceso completo a las funciones de la plataforma.", + "whyKyc": "Por que KYC?", + "reasonComply": "Cumplir con los requisitos regulatorios", + "reasonLimits": "Aumentar los limites de transaccion", + "reasonFeatures": "Acceder a funciones premium", + "reasonSecurity": "Seguridad mejorada de la cuenta" + }, + "kycForm": { + "formTitle": "Formulario de envio KYC", + "step": "Paso", + "of": "de", + "steps": "Pasos", + "completed": "completado", + "current": "actual", + "upcoming": "proximo", + "personalInfo": "Informacion Personal", + "addressInfo": "Informacion de Direccion", + "documents": "Documentos", + "review": "Revisar", + "firstName": "Nombre", + "lastName": "Apellido", + "email": "Correo electronico", + "nationality": "Nacionalidad", + "dateOfBirth": "Fecha de nacimiento", + "street": "Calle", + "city": "Ciudad", + "state": "Estado", + "postalCode": "Codigo postal", + "country": "Pais", + "idType": "Tipo de ID", + "selectIdType": "Seleccionar tipo de ID", + "passport": "Pasaporte", + "driversLicense": "Licencia de conducir", + "nationalId": "ID Nacional", + "idNumber": "Numero de ID", + "idFront": "Frente del ID", + "idBack": "Reverso del ID", + "selfie": "Foto selfie", + "required": "Requerido", + "validationError": "Por favor completa todos los campos requeridos", + "back": "Atras", + "next": "Siguiente", + "submit": "Enviar KYC", + "submitting": "Enviando...", + "submitError": "Error al enviar", + "submitAnother": "Enviar otro", + "successTitle": "KYC enviado exitosamente!", + "successDescription": "Tu verificacion KYC ha sido enviada y esta en revision.", + "dash": "—" + }, + "darkModeTheme": { + "loading": "Cargando configuracion del tema", + "errorCleared": "Error eliminado. Intentando cambiar el tema.", + "switchingTo": "Cambiando al tema {theme}.", + "themeChanged": "Tema cambiado a {theme}.", + "themeError": "Error al cambiar el tema. Intentalo de nuevo.", + "currentTheme": "Tema actual: {theme}", + "systemTheme": "Sistema ({theme})", + "ariaError": "Cambio de tema con error — presiona para reintentar", + "ariaLabel": "Cambio de tema, tema actual: {theme}. Presiona para pasar al siguiente tema.", + "ariaLoading": "El tema esta cargando", + "errorTitle": "Error de tema: {error}. Presiona para reintentar.", + "titleSystem": "Tema: Sistema ({theme})", + "titleLight": "Tema: Claro", + "titleDark": "Tema: Oscuro", + "description": "Cambia entre los temas claro, oscuro y del sistema. El sistema sigue la preferencia del dispositivo. Presiona Espacio o Intro para activar.", + "preferenceLabel": "Preferencia de tema", + "preferenceSaved": "Preferencia de tema guardada", + "preferenceRestored": "Preferencia de tema restaurada", + "themes": { + "light": "claro", + "dark": "oscuro", + "system": "sistema" + }, + "themeNames": { + "light": "Claro", + "dark": "Oscuro", + "system": "Sistema" + }, + "sr": { + "sunIcon": "Icono de sol — modo claro activo", + "moonIcon": "Icono de luna — modo oscuro activo", + "systemIcon": "Icono de monitor — tema del sistema activo", + "errorIcon": "Icono de error — no se pudo aplicar el tema" + } + }, + "portfolioChartWidget": { + "title": "Valor del portafolio", + "allocation": "Distribucion", + "trend": "Tendencia", + "loading": "Cargando grafico del portafolio...", + "historyLoading": "Cargando historial de rendimiento...", + "emptyAllocation": "Todavia no hay datos de asignacion.", + "emptyAssets": "Todavia no hay activos en el portafolio.", + "emptyHistory": "Todavia no hay historial de rendimiento." + } } \ No newline at end of file diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index 9eda02e2..875b31e6 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -1,623 +1,623 @@ -{ - "twoFactorAuth": { - "ariaLabel": "Configuracao de autenticacao em duas etapas", - "stepsLabel": "Etapas de configuracao", - "title": "Ativar autenticacao em duas etapas", - "description": "Proteja sua conta com um aplicativo de autenticacao. Voce precisara escanear um codigo QR para comecar.", - "enableButton": "Ativar 2FA", - "settingUp": "Configurando…", - "scanTitle": "Escaneie com seu aplicativo de autenticacao", - "scanDescription": "Use Google Authenticator, Authy ou qualquer aplicativo compativel com TOTP para escanear o codigo.", - "qrCodeAlt": "Codigo QR TOTP — escaneie com seu aplicativo de autenticacao", - "qrCodeSkeletonLabel": "Gerando codigo QR…", - "manualKeyLabel": "Ou insira a chave manualmente", - "manualKeyAria": "Chave de configuracao manual: {key}", - "codeInputLabel": "Insira o codigo de 6 digitos", - "codeInputPlaceholder": "000000", - "verifyButton": "Verificar e ativar", - "verifying": "Verificando…", - "successTitle": "Autenticacao em duas etapas ativada", - "successDescription": "Sua conta esta agora protegida. Voce sera solicitado um codigo em cada login.", - "error": { - "setupFailed": "Falha ao iniciar a configuracao de 2FA. Tente novamente.", - "invalidCode": "Codigo invalido. Tente novamente.", - "codeLength": "Insira o codigo de 6 digitos do seu aplicativo de autenticacao." - } - }, - "localeSwitcher": { - "label": "Idioma", - "ariaLabel": "Selecionar idioma", - "options": { - "en": "English", - "es": "Espanol", - "pt": "Portugues" - } - }, - "nav": { - "home": "Inicio", - "docs": "Docs", - "login": "Entrar", - "register": "Cadastrar", - "toggleMenu": "Abrir menu" - }, - "sidebar": { - "overview": "Visao geral", - "payments": "Pagamentos", - "webhookLogs": "Logs de Webhook", - "apiKeys": "Chaves API", - "settings": "Configuracoes", - "workspace": "Area autenticada", - "shortcuts": "Atalhos", - "createPayment": "Criar pagamento", - "network": "Mainnet-testnet", - "collapse": "Recolher barra lateral", - "expand": "Expandir barra lateral", - "close": "Fechar navegacao" - }, - "breadcrumbs": { - "home": "Inicio", - "segments": { - "dashboard": "Painel", - "create": "Criar", - "payments": "Pagamentos", - "settings": "Configuracoes", - "webhook-logs": "Logs de Webhook", - "api-keys": "Chaves API" - } - }, - "copyButton": { - "ariaLabel": "Copiar para a area de transferencia", - "copied": "Copiado", - "copiedState": "Copiado" - }, - "dashboardPage": { - "title": "Visao geral do lojista", - "description": "Acompanhe seu volume de transacoes, monitore pagamentos em tempo real e gerencie sua integracao Stellar.", - "paymentMetrics": "Metricas de pagamento", - "recentActivity": "Atividade recente", - "viewAllPayments": "Ver todos os pagamentos", - "quickActions": "Acoes rapidas", - "createPaymentLink": "Criar link de pagamento", - "withdrawFunds": "Sacar fundos (SEP-24)", - "development": "Desenvolvimento", - "apiKeysTip": "Use a pagina de Chaves API para rotacionar seu token secreto periodicamente.", - "webhookLogsTip": "Confira os Logs de Webhook se os pagamentos nao estiverem acionando seu backend." - }, - "realTimeBalanceSync": { - "title": "Saldos em tempo real", - "refreshButton": "Atualizar", - "syncing": "Sincronizando…", - "sectionAriaLabel": "Informacoes de saldo em tempo real", - "balancesListAriaLabel": "Saldos da conta", - "emptyState": "Nenhum saldo disponivel.", - "updatedLabel": "Atualizado", - "balanceItemAriaLabel": "{asset} saldo: {balance}", - "liveRegion": { - "syncing": "Sincronizando saldos…", - "error": "Erro de sincronizacao de saldos: {error}", - "updatedAt": "Saldos atualizados as {time}." - } - }, - "createPaymentPage": { - "eyebrow": "Painel", - "title": "Criar link de pagamento", - "description": "Defina um valor, escolha um ativo e informe um endereco Stellar para gerar um link compartilhavel.", - "newHere": "Novo aqui?", - "registerMerchant": "Cadastrar uma conta de lojista" - }, - "createPaymentForm": { - "invalidAmount": "O valor deve ser um numero positivo.", - "invalidRecipient": "O destinatario deve ser uma chave publica Stellar valida (56 caracteres, comeca com G).", - "invalidHexColor": "{field} deve ser uma cor hexadecimal valida.", - "failedCreate": "Falha ao criar o link de pagamento", - "createdToast": "Link de pagamento criado", - "noApiKeyTitle": "Nenhuma chave API encontrada", - "noApiKeyDescription": "Cadastre uma conta de lojista para receber sua chave API e comecar a criar links de pagamento.", - "registerAsMerchant": "Cadastrar como lojista", - "readyEyebrow": "Link pronto", - "readyTitle": "Link criado com sucesso", - "readyDescription": "Compartilhe este link com seu cliente para receber o pagamento.", - "shareLink": "Compartilhar link", - "shareTitle": "Link de pagamento pronto", - "shareText": "Abra este link de pagamento Stellar.", - "shareFailed": "O compartilhamento nativo nao esta disponivel agora.", - "paymentLink": "Link de pagamento", - "paymentId": "ID do pagamento", - "status": "Status", - "createAnother": "Criar outro link de pagamento", - "amount": "Valor", - "amountPlaceholder": "Digite {exampleAmount} {asset}", - "asset": "Ativo", - "selectAsset": "Selecionar ativo", - "issuer": "Emissor", - "trustedAddresses": "Selecionar entre enderecos confiaveis", - "selectSavedAddress": "-- Selecione um endereco salvo --", - "recipientAddress": "Endereco do destinatario", - "recipientPlaceholder": "Cole a carteira de destino para {asset}", - "recipientPlaceholderSelected": "{label} selecionado para {asset}", - "descriptionLabel": "Descricao", - "optional": "opcional", - "descriptionPlaceholder": "Qual e o contexto desta cobranca em {asset}?", - "brandingTitle": "Personalizacao desta sessao", - "brandingDescription": "Aplique cores personalizadas somente neste link de pagamento.", - "enabled": "Ativado", - "disabled": "Desativado", - "primary": "Primaria", - "secondary": "Secundaria", - "background": "Fundo", - "checkoutPreview": "Previa do checkout", - "payNow": "Pagar agora", - "generating": "Gerando...", - "generate": "Gerar link de pagamento", - "rateLimitError": "Você está criando links rápido demais. Tente novamente em {seconds} segundos.", - "retryWait": "Aguarde {seconds}s…", - "integrationCode": "Código de Integração", - "snippetsHelper": "Os trechos de código são atualizados automaticamente com os valores do formulário." - }, - "paymentMetrics": { - "downloadImage": "Baixar imagem", - "downloadPng": "Baixar PNG", - "downloadSvg": "Baixar SVG", - "exporting": "Exportando...", - "exportSuccess": "Grafico baixado como {format}", - "exportFailed": "Falha ao exportar o grafico.", - "fetchMetricsFailed": "Falha ao buscar metricas", - "fetchVolumeFailed": "Falha ao buscar dados de volume", - "retry": "Tentar novamente", - "sevenDayVolume": "Volume de 7 dias", - "totalPayments": "Total de pagamentos", - "paymentsCount": "{count, plural, one {pagamento} other {pagamentos}}", - "chartTitle": "Comparacao de volume por ativo", - "chartSubtitle": "Volume diario de transacoes por ativo", - "heatmapTitle": "Mapa de calor de pagamentos", - "heatmapSubtitle": "Intensidade de atividade estilo GitHub nos ultimos 365 dias", - "heatmapTotal": "{count, plural, one {# pagamento no ultimo ano} other {# pagamentos no ultimo ano}}", - "heatmapTooltip": "{count, plural, one {# pagamento} other {# pagamentos}} em {date}", - "heatmapAriaLabel": "Mapa de calor de pagamentos de 365 dias", - "heatmapEmptyDay": "Nenhum pagamento registrado", - "heatmapLegendLess": "Menos", - "heatmapLegendMore": "Mais", - "heatmapWeekdays": { - "sun": "D", - "mon": "S", - "tue": "T", - "wed": "Q", - "thu": "Q", - "fri": "S", - "sat": "S" - }, - "showRange": "Mostrar {range}", - "toggleAssetVisibility": "Alternar visibilidade do ativo", - "showAsset": "Mostrar {asset}", - "hideAsset": "Ocultar {asset}", - "noPayments": "Nao ha pagamentos concluidos neste periodo.", - "weeklyAvgLabel": "Med. 7D", - "ranges": { - "7D": "7 dias", - "30D": "30 dias", - "1Y": "1 ano" - } - }, - "paymentsPage": { - "title": "Historico de pagamentos", - "description": "Monitore todas as transacoes recebidas e seu status de verificacao." - }, - "recentPayments": { - "missingApiKey": "Chave API nao encontrada. Cadastre-se ou faca login.", - "fetchFailed": "Falha ao buscar pagamentos", - "loadFailed": "Falha ao carregar pagamentos", - "connectionError": "Erro de conexao", - "backendHint": "Verifique se o backend esta ativo e se o endpoint de pagamentos esta disponivel.", - "retryConnection": "Tentar novamente", - "testWebhook": "Testar webhook mesmo assim", - "troubleshootingTip": "Dica de diagnostico", - "troubleshootingDescription": "Voce ainda pode testar a funcionalidade de webhook enquanto os servicos do backend sao restaurados.", - "emptyTitle": "Ainda nao ha pagamentos", - "emptyDescription": "Comece a receber pagamentos criando seu primeiro link ou testando webhooks para ver os dados fluindo.", - "createPaymentLink": "Criar link de pagamento", - "sendTestWebhook": "Enviar webhook de teste", - "gettingStartedTitle": "Guia inicial", - "gettingStartedDescription": "Use ferramentas de webhook para testar notificacoes de pagamento e ver dados em tempo real neste painel.", - "search": "Buscar", - "searchPlaceholder": "Buscar por ID do pagamento ou descricao...", - "status": "Status", - "allStatuses": "Todos os status", - "asset": "Ativo", - "allAssets": "Todos os ativos", - "fromDate": "Data inicial", - "toDate": "Data final", - "activeFilters": "Filtros ativos:", - "searchChip": "Busca: \"{value}\"", - "statusChip": "Status: {value}", - "assetChip": "Ativo: {value}", - "fromChip": "De: {value}", - "toChip": "Ate: {value}", - "clearSearchFilter": "Limpar filtro de busca", - "clearStatusFilter": "Limpar filtro de status", - "clearAssetFilter": "Limpar filtro de ativo", - "clearFromDateFilter": "Limpar filtro de data inicial", - "clearToDateFilter": "Limpar filtro de data final", - "clearAll": "Limpar tudo", - "linkCopied": "Link de pagamento copiado para a área de transferência!", - "showingResults": "Mostrando {shown} de {total} pagamentos", - "filteredSuffix": "(com filtros)", - "tableStatus": "Status", - "tableAmount": "Valor", - "tableRecipient": "Destinatario", - "tableDate": "Data", - "tableLink": "Link", - "view": "Ver", - "emptyDescriptionValue": "-", - "statuses": { - "all": "Todos", - "pending": "Pendente", - "confirmed": "Confirmado", - "failed": "Falhou", - "refunded": "Reembolsado" - }, - "pagination": { - "ariaLabel": "Paginação do histórico de pagamentos", - "previous": "Página anterior", - "next": "Próxima página", - "goToPage": "Ir para a página {page}", - "currentPage": "Página atual, página {page}", - "pageLabel": "Página {page} de {totalPages}", - "range": "Mostrando {start}-{end} de {total}" - } - }, - "walletSelector": { - "chooseWallet": "Escolha uma carteira", - "description": "Conecte sua carteira Stellar para concluir este pagamento.", - "connecting": "Conectando...", - "walletConnectWaiting": "Aguardando a carteira...", - "noProjectId": "(sem project ID)", - "notInstalled": "(nao instalada)", - "tapToConnect": "Clique para conectar", - "scanTitle": "Escaneie com seu app de carteira Stellar", - "scanDescription": "Escaneie com o Freighter mobile ou qualquer carteira compatível com WalletConnect", - "pairingFailed": "Falha no pareamento do WalletConnect", - "userRejected": "A solicitação de conexão foi cancelada. Aprove-a na carteira para continuar.", - "walletConnectUnavailable": "O WalletConnect não está disponível agora. Tente usar o Freighter ou habilite um WalletConnect project ID válido.", - "extensionNotFound": "Não encontramos essa carteira neste dispositivo. Instale a extensão e tente novamente.", - "noAccountFound": "A carteira conectou, mas não retornou nenhuma conta Stellar. Abra uma conta Stellar na carteira e tente novamente.", - "walletConnectFailed": "Não conseguimos concluir a conexão com o WalletConnect. Tente novamente.", - "connectionFailed": "Não conseguimos conectar essa carteira agora. Tente novamente." - }, - "support": { - "panelAriaLabel": "Painel de ajuda e suporte", - "heading": "Ajuda e Suporte", - "title": "Precisa de ajuda com pagamentos?", - "description": "Precisa de ajuda? Envie-nos uma mensagem ou gorjeta para obter suporte prioritário.", - "contactEmail": "Contato por e-mail", - "openChat": "Abrir chat de suporte", - "closeChat": "Fechar chat de suporte", - "yourWallet": "Sua Carteira", - "notConnected": "Não Conectada", - "connectingWallet": "Conectando…", - "amount": "Valor", - "loadingBalance": "Carregando saldo...", - "unfunded": "Sem fundos (Adicionar fundos no Testnet)", - "available": "Disponível: {balance} {asset}", - "availableAriaLabel": "Saldo disponível: {balance} {asset}", - "balanceSyncing": "Sincronizando saldo da carteira…", - "walletUnfunded": "A carteira não tem fundos.", - "balanceSynced": "Saldo sincronizado: {balance} {asset}.", - "insufficientBalance": "Saldo insuficiente", - "messageLabel": "Deixe uma mensagem (opcional)", - "messagePlaceholder": "Obrigado pelo ótimo trabalho!", - "sendSupport": "Enviar Suporte", - "sending": "Enviando…", - "submissionFailed": "Falha no envio", - "failedToRecord": "Falha ao registrar a transação de suporte" - }, - "checkout": { - "paymentRequest": "Solicitacao de pagamento", - "completePayment": "Concluir pagamento", - "paymentNotFound": "Pagamento nao encontrado", - "loadFailed": "Nao foi possivel carregar os detalhes do pagamento.", - "paymentMissing": "Pagamento nao encontrado.", - "loadPaymentFailed": "Falha ao carregar pagamento.", - "errorTitle": "Erro", - "errorDescription": "Confira o link de pagamento e tente novamente ou entre em contato com o remetente.", - "processingFallback": "Processando transacao...", - "doNotClose": "Nao feche esta aba", - "recipient": "Destinatario", - "scanToPay": "Escanear para pagar", - "scanDescription": "Escaneie com o Freighter ou qualquer carteira Stellar", - "openQrModal": "Pagar com codigo QR", - "qrModalTitle": "Pagar com codigo QR", - "qrModalDescription": "Escaneie este codigo em outro dispositivo ou compartilhe com seu app de carteira.", - "downloadQr": "Baixar PNG", - "downloadQrSuccess": "Codigo QR baixado.", - "downloadQrError": "Nao foi possivel baixar o codigo QR.", - "viewRawIntent": "Ver link bruto de intent", - "hideRawIntent": "Ocultar link bruto de intent", - "created": "Criado", - "activeViewers": "{count, plural, one {# pessoa esta vendo este link de pagamento} other {# pessoas estao vendo este link de pagamento}}", - "transaction": "Transacao", - "paymentConfirmed": "Pagamento confirmado!", - "paymentSent": "Pagamento enviado", - "paymentFailed": "O pagamento falhou. Tente novamente.", - "connectedVia": "Conectado via {provider}", - "payInXlm": "Pagar em XLM", - "standardAssetOption": "Pagar a fatura em {asset}", - "pathPaymentOption": "Pagar em XLM", - "approximateCost": "Custo aproximado em XLM", - "merchantReceives": "O lojista recebe {amount} {asset}", - "quoteLoading": "Buscando rotas de pagamento em XLM...", - "quoteUnavailable": "O roteamento em XLM nao esta disponivel para esta fatura agora.", - "quoteBuffer": "Voce pode enviar ate {amount} XLM para cobrir variacoes de preco.", - "pathPaymentHint": "Usa Stellar path payments pela carteira conectada.", - "approximateCostLabel": "Custo aproximado em XLM", - "approximateCostHelp": "Valor estimado para entregar {amount} {asset} ao lojista.", - "slippageBuffer": "Inclui margem de seguranca de {percent}%. Envio maximo: {sendMax} {asset}", - "pathPaymentTogglePrefix": "Pagar com", - "pathPaymentToggleSuffix": "em vez disso", - "processing": "Processando...", - "payWith": "Pagar com {provider}", - "payWithFallback": "Pagar com carteira", - "reviewPaymentTitle": "Revisar pagamento", - "loadingNetworkFee": "Buscando a taxa atual da rede...", - "networkFeeLabel": "Taxa da rede: ~{amount} XLM", - "networkFeeUnavailable": "A taxa da rede nao esta disponivel agora.", - "cancel": "Cancelar", - "confirmPayment": "Confirmar pagamento", - "receivedTitle": "Este pagamento foi recebido.", - "receivedDescription": "A transacao foi confirmada na rede Stellar.", - "downloadReceipt": "Baixar recibo", - "downloadReceiptLoading": "Preparando recibo...", - "receiptDownloaded": "O download do recibo foi iniciado.", - "receiptDownloadFailed": "Nao foi possivel gerar o PDF do recibo.", - "receiptHashUnavailable": "Indisponivel", - "failedTitle": "Este pagamento falhou.", - "failedDescription": "Entre em contato com o lojista se voce acredita que isto e um erro.", - "status": { - "pending": "Aguardando pagamento", - "confirmed": "Confirmado", - "completed": "Concluido", - "failed": "Falhou" - } - }, - "profileCard": { - "account": "Conta", - "openProfileMenu": "Abrir menu de perfil", - "dashboard": "Painel", - "settings": "Configuracoes", - "createPayment": "Criar Pagamento", - "logoutAccount": "Sair da Conta", - "loadingProfile": "Carregando perfil…" - }, - "settingsPage": { - "eyebrow": "Configuracoes", - "eyebrowAccount": "Conta", - "title": "Configuracoes", - "description": "Gerencie suas credenciais, marca e integracoes.", - "loadingSettings": "Carregando configuracoes…", - "noApiKeyTitle": "Nenhuma chave de API encontrada", - "noApiKeyDescription": "Registre uma conta de lojista primeiro para gerenciar suas credenciais aqui.", - "registerAsMerchant": "Registrar como Lojista", - "navAriaLabel": "Navegacao de configuracoes", - "navApiKeys": "Chaves de API", - "navBranding": "Marca", - "navDisplay": "Exibicao", - "navWebhooks": "Webhooks", - "navPermissions": "Permissoes", - "navDanger": "Zona de Perigo", - "apiAuthTitle": "Autenticacao de API", - "apiAuthDescription": "Sua chave secreta para solicitacoes de API do lado do servidor.", - "liveApiKey": "Chave de API Ativa", - "hide": "Ocultar", - "reveal": "Revelar", - "apiKeyHeaderHintPrefix": "Envie como o cabecalho", - "apiKeyHeaderHintSuffix": "em cada solicitacao.", - "apiKeyVisible": "A chave de API ativa esta visivel no momento.", - "apiKeyHidden": "A chave de API ativa esta oculta no momento.", - "rotateApiKeyTitle": "Rotacionar Chave de API", - "rotateApiKeyDescription": "Gera uma nova chave e invalida imediatamente a atual.", - "rotateKeyEllipsis": "Rotacionar Chave…", - "confirmAction": "Confirmar Acao", - "rotateKeyWarning": "A chave antiga deixara de funcionar imediatamente.", - "confirm": "Confirmar", - "rotating": "Rotacionando…", - "cancel": "Cancelar", - "checkoutBrandingTitle": "Marca do Checkout", - "checkoutBrandingDescription": "Personalize a aparencia da sua pagina de checkout.", - "storeLogo": "Logo da Loja", - "clickOrDragToReplace": "Clique ou arraste para substituir", - "dropHere": "Solte aqui", - "uploadLogo": "Enviar logo", - "logoFormats": "PNG, JPG ou SVG · max 2MB", - "removeLogo": "Remover logo", - "colors": "Cores", - "primary": "Primaria", - "secondary": "Secundaria", - "background": "Fundo", - "preview": "Pre-visualizacao", - "sampleCheckout": "Checkout de exemplo", - "payNow": "Pagar Agora", - "lowContrastWarning": "As cores podem nao atender as metas de contraste WCAG. Considere ajusta-las.", - "saving": "Salvando…", - "saveBranding": "Salvar Marca", - "previewReceipt": "Pre-visualizar Recibo", - "displayPreferencesTitle": "Preferencias de Exibicao", - "displayPreferencesDescription": "Ajuste como os valores aparecem no painel.", - "hideTrailingCents": "Ocultar centavos finais", - "hideTrailingCentsDescription": "Valores inteiros como 50 serao exibidos sem o sufixo .00.", - "webhookEndpointTitle": "Endpoint do Webhook", - "webhookEndpointDescription": "Receba eventos de pagamento em tempo real no seu servidor.", - "verified": "Verificado", - "unverified": "Nao verificado", - "endpointUrl": "URL do Endpoint", - "saveUrl": "Salvar URL", - "testing": "Testando…", - "sendTest": "Enviar Teste", - "domainVerification": "Verificacao de Dominio", - "hostTokenAt": "Hospede este token em", - "verifying": "Verificando…", - "verifyDomain": "Verificar Dominio", - "signingSecretTitle": "Segredo de Assinatura", - "signingSecretDescriptionPrefix": "Valide o cabecalho", - "signingSecretDescriptionSuffix": "nos webhooks recebidos.", - "hideWebhookSecret": "Ocultar segredo do webhook", - "showWebhookSecret": "Mostrar segredo do webhook", - "webhookSecretVisible": "O segredo do webhook esta visivel. Copie agora — exibido apenas uma vez.", - "webhookSecretHidden": "O segredo do webhook esta oculto. Alterne para inspeciona-lo.", - "regenerateSecretEllipsis": "Regenerar Segredo…", - "regenerateSecretWarning": "O segredo atual deixara de funcionar imediatamente.", - "regenerating": "Regenerando…", - "dangerZoneDescription": "Acoes irreversiveis. Prossiga com cautela.", - "failedToLoadBranding": "Falha ao carregar a marca", - "failedToLoadWebhookSettings": "Falha ao carregar as configuracoes do webhook", - "failedToRotateKey": "Falha ao rotacionar a chave", - "apiKeyRotatedSuccess": "Chave de API rotacionada — atualize as integracoes que usam a chave antiga.", - "imageSizeLimit": "A imagem deve ter menos de 2MB", - "logoUploaded": "Logo enviado!", - "hexColorRequired": "{field} deve ser uma cor hexadecimal valida", - "failedToSaveBranding": "Falha ao salvar a marca", - "brandingSaved": "Marca salva", - "webhookUrlMustBeHttps": "A URL do webhook deve usar HTTPS", - "webhookUrlInvalid": "URL invalida (ex. https://example.com/webhook)", - "failedToSaveWebhookUrl": "Falha ao salvar a URL do webhook", - "webhookUrlSaved": "URL do webhook salva", - "webhookUrlCleared": "URL do webhook removida", - "failedToVerifyDomain": "Falha ao verificar o dominio do webhook", - "domainVerified": "Dominio verificado", - "domainStillUnverified": "Dominio ainda nao verificado", - "failedToRegenerateSecret": "Falha ao regenerar o segredo", - "webhookSecretRegenerated": "Segredo do webhook regenerado — atualize suas integracoes.", - "testWebhookRequestFailed": "Falha na solicitacao de teste do webhook", - "failedToTestWebhook": "Falha ao testar o webhook", - "testWebhookSentStatus": "Webhook de teste enviado — status {status}", - "colorPickerLabel": "Seletor de cor {label}" - }, - "notificationCenter": { - "buttonLabel": "Notificacoes", - "buttonLabelWithCount": "Notificacoes ({count} nao lidas)", - "panelLabel": "Central de Notificacoes", - "heading": "Notificacoes", - "unreadCount": "{count} nao lidas", - "noAlerts": "Sem novos alertas", - "alertLabel": "Alerta", - "dismissLabel": "Dispensar notificacao: {message}", - "notificationLabel": "Notificacao: {message}", - "timestampLabel": "Carimbo de data/hora: {timestamp}" - }, - "fiatOnramp": { - "title": "Comprar / Depositar Fundos", - "triggerLabel": "Comprar / Depositar", - "description": "Deposite fiat atraves de um anchor Stellar (SEP-0024). Os fundos chegam como tokens diretamente na sua carteira conectada.", - "selectAsset": "Selecionar Ativo", - "amountLabel": "Valor", - "amountOptional": "(opcional)", - "amountPlaceholder": "ex. {amount} {asset}", - "anchorDomainLabel": "Dominio do Anchor", - "anchorDomainPlaceholder": "ex. testanchor.stellar.org", - "continueButton": "Continuar para o Anchor", - "footerNote": "Protegido pela rede Stellar - Padrao SEP-0024", - "stepConnecting": "Conectando ao anchor...", - "stepAuth": "Aguardando assinatura da carteira...", - "stepGenerating": "Preparando seu formulario de deposito...", - "authHint": "Assine a transacao de desafio na sua carteira para se conectar com seguranca a {domain}.", - "genericHint": "Isso deve levar apenas um momento.", - "iframeTitle": "Formulario de deposito do anchor", - "genericError": "Falha no deposito" - }, - "kycPage": { - "title": "Verificacao KYC", - "description": "Complete o processo de verificacao para atender aos requisitos regulatorios e desbloquear o acesso completo aos recursos da plataforma.", - "whyKyc": "Por que KYC?", - "reasonComply": "Atender aos requisitos regulatorios", - "reasonLimits": "Aumentar os limites de transacao", - "reasonFeatures": "Acessar recursos premium", - "reasonSecurity": "Seguranca aprimorada da conta" - }, - "kycForm": { - "formTitle": "Formulario de envio KYC", - "step": "Passo", - "of": "de", - "steps": "Passos", - "completed": "concluido", - "current": "atual", - "upcoming": "proximo", - "personalInfo": "Informacoes Pessoais", - "addressInfo": "Informacoes de Endereco", - "documents": "Documentos", - "review": "Revisar", - "firstName": "Nome", - "lastName": "Sobrenome", - "email": "E-mail", - "nationality": "Nacionalidade", - "dateOfBirth": "Data de nascimento", - "street": "Rua", - "city": "Cidade", - "state": "Estado", - "postalCode": "Codigo postal", - "country": "Pais", - "idType": "Tipo de ID", - "selectIdType": "Selecionar tipo de ID", - "passport": "Passaporte", - "driversLicense": "Carteira de motorista", - "nationalId": "ID Nacional", - "idNumber": "Numero do ID", - "idFront": "Frente do ID", - "idBack": "Verso do ID", - "selfie": "Foto selfie", - "required": "Obrigatorio", - "validationError": "Por favor preencha todos os campos obrigatorios", - "back": "Voltar", - "next": "Proximo", - "submit": "Enviar KYC", - "submitting": "Enviando...", - "submitError": "Falha no envio", - "submitAnother": "Enviar outro", - "successTitle": "KYC enviado com sucesso!", - "successDescription": "Sua verificacao KYC foi enviada e esta em revisao.", - "dash": "—" - }, - "darkModeTheme": { - "loading": "Carregando configuracoes de tema", - "errorCleared": "Erro eliminado. Tentando alternar o tema.", - "switchingTo": "Alternando para o tema {theme}.", - "themeChanged": "Tema alterado para {theme}.", - "themeError": "Falha ao alternar o tema. Tente novamente.", - "currentTheme": "Tema atual: {theme}", - "systemTheme": "Sistema ({theme})", - "ariaError": "Alternador de tema com erro — pressione para tentar novamente", - "ariaLabel": "Alternador de tema, tema atual: {theme}. Pressione para ir ao proximo tema.", - "ariaLoading": "O tema esta carregando", - "errorTitle": "Erro de tema: {error}. Pressione para tentar novamente.", - "titleSystem": "Tema: Sistema ({theme})", - "titleLight": "Tema: Claro", - "titleDark": "Tema: Escuro", - "description": "Alterne entre os temas claro, escuro e do sistema. O sistema segue a preferencia do dispositivo. Pressione Espaco ou Enter para ativar.", - "preferenceLabel": "Preferencia de tema", - "preferenceSaved": "Preferencia de tema salva", - "preferenceRestored": "Preferencia de tema restaurada", - "themes": { - "light": "claro", - "dark": "escuro", - "system": "sistema" - }, - "themeNames": { - "light": "Claro", - "dark": "Escuro", - "system": "Sistema" - }, - "sr": { - "sunIcon": "Icone de sol — modo claro ativo", - "moonIcon": "Icone de lua — modo escuro ativo", - "systemIcon": "Icone de monitor — tema do sistema ativo", - "errorIcon": "Icone de erro — falha ao aplicar o tema" - } - }, - "portfolioChartWidget": { - "title": "Valor do portafolio", - "allocation": "Distribuicao", - "trend": "Tendencia", - "loading": "Carregando grafico do portafolio...", - "historyLoading": "Carregando historico de desempenho...", - "emptyAllocation": "Ainda nao ha dados de alocacao.", - "emptyAssets": "Ainda nao ha ativos no portafolio.", - "emptyHistory": "Ainda nao ha historico de desempenho." - } +{ + "twoFactorAuth": { + "ariaLabel": "Configuracao de autenticacao em duas etapas", + "stepsLabel": "Etapas de configuracao", + "title": "Ativar autenticacao em duas etapas", + "description": "Proteja sua conta com um aplicativo de autenticacao. Voce precisara escanear um codigo QR para comecar.", + "enableButton": "Ativar 2FA", + "settingUp": "Configurando…", + "scanTitle": "Escaneie com seu aplicativo de autenticacao", + "scanDescription": "Use Google Authenticator, Authy ou qualquer aplicativo compativel com TOTP para escanear o codigo.", + "qrCodeAlt": "Codigo QR TOTP — escaneie com seu aplicativo de autenticacao", + "qrCodeSkeletonLabel": "Gerando codigo QR…", + "manualKeyLabel": "Ou insira a chave manualmente", + "manualKeyAria": "Chave de configuracao manual: {key}", + "codeInputLabel": "Insira o codigo de 6 digitos", + "codeInputPlaceholder": "000000", + "verifyButton": "Verificar e ativar", + "verifying": "Verificando…", + "successTitle": "Autenticacao em duas etapas ativada", + "successDescription": "Sua conta esta agora protegida. Voce sera solicitado um codigo em cada login.", + "error": { + "setupFailed": "Falha ao iniciar a configuracao de 2FA. Tente novamente.", + "invalidCode": "Codigo invalido. Tente novamente.", + "codeLength": "Insira o codigo de 6 digitos do seu aplicativo de autenticacao." + } + }, + "localeSwitcher": { + "label": "Idioma", + "ariaLabel": "Selecionar idioma", + "options": { + "en": "English", + "es": "Espanol", + "pt": "Portugues" + } + }, + "nav": { + "home": "Inicio", + "docs": "Docs", + "login": "Entrar", + "register": "Cadastrar", + "toggleMenu": "Abrir menu" + }, + "sidebar": { + "overview": "Visao geral", + "payments": "Pagamentos", + "webhookLogs": "Logs de Webhook", + "apiKeys": "Chaves API", + "settings": "Configuracoes", + "workspace": "Area autenticada", + "shortcuts": "Atalhos", + "createPayment": "Criar pagamento", + "network": "Mainnet-testnet", + "collapse": "Recolher barra lateral", + "expand": "Expandir barra lateral", + "close": "Fechar navegacao" + }, + "breadcrumbs": { + "home": "Inicio", + "segments": { + "dashboard": "Painel", + "create": "Criar", + "payments": "Pagamentos", + "settings": "Configuracoes", + "webhook-logs": "Logs de Webhook", + "api-keys": "Chaves API" + } + }, + "copyButton": { + "ariaLabel": "Copiar para a area de transferencia", + "copied": "Copiado", + "copiedState": "Copiado" + }, + "dashboardPage": { + "title": "Visao geral do lojista", + "description": "Acompanhe seu volume de transacoes, monitore pagamentos em tempo real e gerencie sua integracao Stellar.", + "paymentMetrics": "Metricas de pagamento", + "recentActivity": "Atividade recente", + "viewAllPayments": "Ver todos os pagamentos", + "quickActions": "Acoes rapidas", + "createPaymentLink": "Criar link de pagamento", + "withdrawFunds": "Sacar fundos (SEP-24)", + "development": "Desenvolvimento", + "apiKeysTip": "Use a pagina de Chaves API para rotacionar seu token secreto periodicamente.", + "webhookLogsTip": "Confira os Logs de Webhook se os pagamentos nao estiverem acionando seu backend." + }, + "realTimeBalanceSync": { + "title": "Saldos em tempo real", + "refreshButton": "Atualizar", + "syncing": "Sincronizando…", + "sectionAriaLabel": "Informacoes de saldo em tempo real", + "balancesListAriaLabel": "Saldos da conta", + "emptyState": "Nenhum saldo disponivel.", + "updatedLabel": "Atualizado", + "balanceItemAriaLabel": "{asset} saldo: {balance}", + "liveRegion": { + "syncing": "Sincronizando saldos…", + "error": "Erro de sincronizacao de saldos: {error}", + "updatedAt": "Saldos atualizados as {time}." + } + }, + "createPaymentPage": { + "eyebrow": "Painel", + "title": "Criar link de pagamento", + "description": "Defina um valor, escolha um ativo e informe um endereco Stellar para gerar um link compartilhavel.", + "newHere": "Novo aqui?", + "registerMerchant": "Cadastrar uma conta de lojista" + }, + "createPaymentForm": { + "invalidAmount": "O valor deve ser um numero positivo.", + "invalidRecipient": "O destinatario deve ser uma chave publica Stellar valida (56 caracteres, comeca com G).", + "invalidHexColor": "{field} deve ser uma cor hexadecimal valida.", + "failedCreate": "Falha ao criar o link de pagamento", + "createdToast": "Link de pagamento criado", + "noApiKeyTitle": "Nenhuma chave API encontrada", + "noApiKeyDescription": "Cadastre uma conta de lojista para receber sua chave API e comecar a criar links de pagamento.", + "registerAsMerchant": "Cadastrar como lojista", + "readyEyebrow": "Link pronto", + "readyTitle": "Link criado com sucesso", + "readyDescription": "Compartilhe este link com seu cliente para receber o pagamento.", + "shareLink": "Compartilhar link", + "shareTitle": "Link de pagamento pronto", + "shareText": "Abra este link de pagamento Stellar.", + "shareFailed": "O compartilhamento nativo nao esta disponivel agora.", + "paymentLink": "Link de pagamento", + "paymentId": "ID do pagamento", + "status": "Status", + "createAnother": "Criar outro link de pagamento", + "amount": "Valor", + "amountPlaceholder": "Digite {exampleAmount} {asset}", + "asset": "Ativo", + "selectAsset": "Selecionar ativo", + "issuer": "Emissor", + "trustedAddresses": "Selecionar entre enderecos confiaveis", + "selectSavedAddress": "-- Selecione um endereco salvo --", + "recipientAddress": "Endereco do destinatario", + "recipientPlaceholder": "Cole a carteira de destino para {asset}", + "recipientPlaceholderSelected": "{label} selecionado para {asset}", + "descriptionLabel": "Descricao", + "optional": "opcional", + "descriptionPlaceholder": "Qual e o contexto desta cobranca em {asset}?", + "brandingTitle": "Personalizacao desta sessao", + "brandingDescription": "Aplique cores personalizadas somente neste link de pagamento.", + "enabled": "Ativado", + "disabled": "Desativado", + "primary": "Primaria", + "secondary": "Secundaria", + "background": "Fundo", + "checkoutPreview": "Previa do checkout", + "payNow": "Pagar agora", + "generating": "Gerando...", + "generate": "Gerar link de pagamento", + "rateLimitError": "Você está criando links rápido demais. Tente novamente em {seconds} segundos.", + "retryWait": "Aguarde {seconds}s…", + "integrationCode": "Código de Integração", + "snippetsHelper": "Os trechos de código são atualizados automaticamente com os valores do formulário." + }, + "paymentMetrics": { + "downloadImage": "Baixar imagem", + "downloadPng": "Baixar PNG", + "downloadSvg": "Baixar SVG", + "exporting": "Exportando...", + "exportSuccess": "Grafico baixado como {format}", + "exportFailed": "Falha ao exportar o grafico.", + "fetchMetricsFailed": "Falha ao buscar metricas", + "fetchVolumeFailed": "Falha ao buscar dados de volume", + "retry": "Tentar novamente", + "sevenDayVolume": "Volume de 7 dias", + "totalPayments": "Total de pagamentos", + "paymentsCount": "{count, plural, one {pagamento} other {pagamentos}}", + "chartTitle": "Comparacao de volume por ativo", + "chartSubtitle": "Volume diario de transacoes por ativo", + "heatmapTitle": "Mapa de calor de pagamentos", + "heatmapSubtitle": "Intensidade de atividade estilo GitHub nos ultimos 365 dias", + "heatmapTotal": "{count, plural, one {# pagamento no ultimo ano} other {# pagamentos no ultimo ano}}", + "heatmapTooltip": "{count, plural, one {# pagamento} other {# pagamentos}} em {date}", + "heatmapAriaLabel": "Mapa de calor de pagamentos de 365 dias", + "heatmapEmptyDay": "Nenhum pagamento registrado", + "heatmapLegendLess": "Menos", + "heatmapLegendMore": "Mais", + "heatmapWeekdays": { + "sun": "D", + "mon": "S", + "tue": "T", + "wed": "Q", + "thu": "Q", + "fri": "S", + "sat": "S" + }, + "showRange": "Mostrar {range}", + "toggleAssetVisibility": "Alternar visibilidade do ativo", + "showAsset": "Mostrar {asset}", + "hideAsset": "Ocultar {asset}", + "noPayments": "Nao ha pagamentos concluidos neste periodo.", + "weeklyAvgLabel": "Med. 7D", + "ranges": { + "7D": "7 dias", + "30D": "30 dias", + "1Y": "1 ano" + } + }, + "paymentsPage": { + "title": "Historico de pagamentos", + "description": "Monitore todas as transacoes recebidas e seu status de verificacao." + }, + "recentPayments": { + "missingApiKey": "Chave API nao encontrada. Cadastre-se ou faca login.", + "fetchFailed": "Falha ao buscar pagamentos", + "loadFailed": "Falha ao carregar pagamentos", + "connectionError": "Erro de conexao", + "backendHint": "Verifique se o backend esta ativo e se o endpoint de pagamentos esta disponivel.", + "retryConnection": "Tentar novamente", + "testWebhook": "Testar webhook mesmo assim", + "troubleshootingTip": "Dica de diagnostico", + "troubleshootingDescription": "Voce ainda pode testar a funcionalidade de webhook enquanto os servicos do backend sao restaurados.", + "emptyTitle": "Ainda nao ha pagamentos", + "emptyDescription": "Comece a receber pagamentos criando seu primeiro link ou testando webhooks para ver os dados fluindo.", + "createPaymentLink": "Criar link de pagamento", + "sendTestWebhook": "Enviar webhook de teste", + "gettingStartedTitle": "Guia inicial", + "gettingStartedDescription": "Use ferramentas de webhook para testar notificacoes de pagamento e ver dados em tempo real neste painel.", + "search": "Buscar", + "searchPlaceholder": "Buscar por ID do pagamento ou descricao...", + "status": "Status", + "allStatuses": "Todos os status", + "asset": "Ativo", + "allAssets": "Todos os ativos", + "fromDate": "Data inicial", + "toDate": "Data final", + "activeFilters": "Filtros ativos:", + "searchChip": "Busca: \"{value}\"", + "statusChip": "Status: {value}", + "assetChip": "Ativo: {value}", + "fromChip": "De: {value}", + "toChip": "Ate: {value}", + "clearSearchFilter": "Limpar filtro de busca", + "clearStatusFilter": "Limpar filtro de status", + "clearAssetFilter": "Limpar filtro de ativo", + "clearFromDateFilter": "Limpar filtro de data inicial", + "clearToDateFilter": "Limpar filtro de data final", + "clearAll": "Limpar tudo", + "linkCopied": "Link de pagamento copiado para a área de transferência!", + "showingResults": "Mostrando {shown} de {total} pagamentos", + "filteredSuffix": "(com filtros)", + "tableStatus": "Status", + "tableAmount": "Valor", + "tableRecipient": "Destinatario", + "tableDate": "Data", + "tableLink": "Link", + "view": "Ver", + "emptyDescriptionValue": "-", + "statuses": { + "all": "Todos", + "pending": "Pendente", + "confirmed": "Confirmado", + "failed": "Falhou", + "refunded": "Reembolsado" + }, + "pagination": { + "ariaLabel": "Paginação do histórico de pagamentos", + "previous": "Página anterior", + "next": "Próxima página", + "goToPage": "Ir para a página {page}", + "currentPage": "Página atual, página {page}", + "pageLabel": "Página {page} de {totalPages}", + "range": "Mostrando {start}-{end} de {total}" + } + }, + "walletSelector": { + "chooseWallet": "Escolha uma carteira", + "description": "Conecte sua carteira Stellar para concluir este pagamento.", + "connecting": "Conectando...", + "walletConnectWaiting": "Aguardando a carteira...", + "noProjectId": "(sem project ID)", + "notInstalled": "(nao instalada)", + "tapToConnect": "Clique para conectar", + "scanTitle": "Escaneie com seu app de carteira Stellar", + "scanDescription": "Escaneie com o Freighter mobile ou qualquer carteira compatível com WalletConnect", + "pairingFailed": "Falha no pareamento do WalletConnect", + "userRejected": "A solicitação de conexão foi cancelada. Aprove-a na carteira para continuar.", + "walletConnectUnavailable": "O WalletConnect não está disponível agora. Tente usar o Freighter ou habilite um WalletConnect project ID válido.", + "extensionNotFound": "Não encontramos essa carteira neste dispositivo. Instale a extensão e tente novamente.", + "noAccountFound": "A carteira conectou, mas não retornou nenhuma conta Stellar. Abra uma conta Stellar na carteira e tente novamente.", + "walletConnectFailed": "Não conseguimos concluir a conexão com o WalletConnect. Tente novamente.", + "connectionFailed": "Não conseguimos conectar essa carteira agora. Tente novamente." + }, + "support": { + "panelAriaLabel": "Painel de ajuda e suporte", + "heading": "Ajuda e Suporte", + "title": "Precisa de ajuda com pagamentos?", + "description": "Precisa de ajuda? Envie-nos uma mensagem ou gorjeta para obter suporte prioritário.", + "contactEmail": "Contato por e-mail", + "openChat": "Abrir chat de suporte", + "closeChat": "Fechar chat de suporte", + "yourWallet": "Sua Carteira", + "notConnected": "Não Conectada", + "connectingWallet": "Conectando…", + "amount": "Valor", + "loadingBalance": "Carregando saldo...", + "unfunded": "Sem fundos (Adicionar fundos no Testnet)", + "available": "Disponível: {balance} {asset}", + "availableAriaLabel": "Saldo disponível: {balance} {asset}", + "balanceSyncing": "Sincronizando saldo da carteira…", + "walletUnfunded": "A carteira não tem fundos.", + "balanceSynced": "Saldo sincronizado: {balance} {asset}.", + "insufficientBalance": "Saldo insuficiente", + "messageLabel": "Deixe uma mensagem (opcional)", + "messagePlaceholder": "Obrigado pelo ótimo trabalho!", + "sendSupport": "Enviar Suporte", + "sending": "Enviando…", + "submissionFailed": "Falha no envio", + "failedToRecord": "Falha ao registrar a transação de suporte" + }, + "checkout": { + "paymentRequest": "Solicitacao de pagamento", + "completePayment": "Concluir pagamento", + "paymentNotFound": "Pagamento nao encontrado", + "loadFailed": "Nao foi possivel carregar os detalhes do pagamento.", + "paymentMissing": "Pagamento nao encontrado.", + "loadPaymentFailed": "Falha ao carregar pagamento.", + "errorTitle": "Erro", + "errorDescription": "Confira o link de pagamento e tente novamente ou entre em contato com o remetente.", + "processingFallback": "Processando transacao...", + "doNotClose": "Nao feche esta aba", + "recipient": "Destinatario", + "scanToPay": "Escanear para pagar", + "scanDescription": "Escaneie com o Freighter ou qualquer carteira Stellar", + "openQrModal": "Pagar com codigo QR", + "qrModalTitle": "Pagar com codigo QR", + "qrModalDescription": "Escaneie este codigo em outro dispositivo ou compartilhe com seu app de carteira.", + "downloadQr": "Baixar PNG", + "downloadQrSuccess": "Codigo QR baixado.", + "downloadQrError": "Nao foi possivel baixar o codigo QR.", + "viewRawIntent": "Ver link bruto de intent", + "hideRawIntent": "Ocultar link bruto de intent", + "created": "Criado", + "activeViewers": "{count, plural, one {# pessoa esta vendo este link de pagamento} other {# pessoas estao vendo este link de pagamento}}", + "transaction": "Transacao", + "paymentConfirmed": "Pagamento confirmado!", + "paymentSent": "Pagamento enviado", + "paymentFailed": "O pagamento falhou. Tente novamente.", + "connectedVia": "Conectado via {provider}", + "payInXlm": "Pagar em XLM", + "standardAssetOption": "Pagar a fatura em {asset}", + "pathPaymentOption": "Pagar em XLM", + "approximateCost": "Custo aproximado em XLM", + "merchantReceives": "O lojista recebe {amount} {asset}", + "quoteLoading": "Buscando rotas de pagamento em XLM...", + "quoteUnavailable": "O roteamento em XLM nao esta disponivel para esta fatura agora.", + "quoteBuffer": "Voce pode enviar ate {amount} XLM para cobrir variacoes de preco.", + "pathPaymentHint": "Usa Stellar path payments pela carteira conectada.", + "approximateCostLabel": "Custo aproximado em XLM", + "approximateCostHelp": "Valor estimado para entregar {amount} {asset} ao lojista.", + "slippageBuffer": "Inclui margem de seguranca de {percent}%. Envio maximo: {sendMax} {asset}", + "pathPaymentTogglePrefix": "Pagar com", + "pathPaymentToggleSuffix": "em vez disso", + "processing": "Processando...", + "payWith": "Pagar com {provider}", + "payWithFallback": "Pagar com carteira", + "reviewPaymentTitle": "Revisar pagamento", + "loadingNetworkFee": "Buscando a taxa atual da rede...", + "networkFeeLabel": "Taxa da rede: ~{amount} XLM", + "networkFeeUnavailable": "A taxa da rede nao esta disponivel agora.", + "cancel": "Cancelar", + "confirmPayment": "Confirmar pagamento", + "receivedTitle": "Este pagamento foi recebido.", + "receivedDescription": "A transacao foi confirmada na rede Stellar.", + "downloadReceipt": "Baixar recibo", + "downloadReceiptLoading": "Preparando recibo...", + "receiptDownloaded": "O download do recibo foi iniciado.", + "receiptDownloadFailed": "Nao foi possivel gerar o PDF do recibo.", + "receiptHashUnavailable": "Indisponivel", + "failedTitle": "Este pagamento falhou.", + "failedDescription": "Entre em contato com o lojista se voce acredita que isto e um erro.", + "status": { + "pending": "Aguardando pagamento", + "confirmed": "Confirmado", + "completed": "Concluido", + "failed": "Falhou" + } + }, + "profileCard": { + "account": "Conta", + "openProfileMenu": "Abrir menu de perfil", + "dashboard": "Painel", + "settings": "Configuracoes", + "createPayment": "Criar Pagamento", + "logoutAccount": "Sair da Conta", + "loadingProfile": "Carregando perfil…" + }, + "settingsPage": { + "eyebrow": "Configuracoes", + "eyebrowAccount": "Conta", + "title": "Configuracoes", + "description": "Gerencie suas credenciais, marca e integracoes.", + "loadingSettings": "Carregando configuracoes…", + "noApiKeyTitle": "Nenhuma chave de API encontrada", + "noApiKeyDescription": "Registre uma conta de lojista primeiro para gerenciar suas credenciais aqui.", + "registerAsMerchant": "Registrar como Lojista", + "navAriaLabel": "Navegacao de configuracoes", + "navApiKeys": "Chaves de API", + "navBranding": "Marca", + "navDisplay": "Exibicao", + "navWebhooks": "Webhooks", + "navPermissions": "Permissoes", + "navDanger": "Zona de Perigo", + "apiAuthTitle": "Autenticacao de API", + "apiAuthDescription": "Sua chave secreta para solicitacoes de API do lado do servidor.", + "liveApiKey": "Chave de API Ativa", + "hide": "Ocultar", + "reveal": "Revelar", + "apiKeyHeaderHintPrefix": "Envie como o cabecalho", + "apiKeyHeaderHintSuffix": "em cada solicitacao.", + "apiKeyVisible": "A chave de API ativa esta visivel no momento.", + "apiKeyHidden": "A chave de API ativa esta oculta no momento.", + "rotateApiKeyTitle": "Rotacionar Chave de API", + "rotateApiKeyDescription": "Gera uma nova chave e invalida imediatamente a atual.", + "rotateKeyEllipsis": "Rotacionar Chave…", + "confirmAction": "Confirmar Acao", + "rotateKeyWarning": "A chave antiga deixara de funcionar imediatamente.", + "confirm": "Confirmar", + "rotating": "Rotacionando…", + "cancel": "Cancelar", + "checkoutBrandingTitle": "Marca do Checkout", + "checkoutBrandingDescription": "Personalize a aparencia da sua pagina de checkout.", + "storeLogo": "Logo da Loja", + "clickOrDragToReplace": "Clique ou arraste para substituir", + "dropHere": "Solte aqui", + "uploadLogo": "Enviar logo", + "logoFormats": "PNG, JPG ou SVG · max 2MB", + "removeLogo": "Remover logo", + "colors": "Cores", + "primary": "Primaria", + "secondary": "Secundaria", + "background": "Fundo", + "preview": "Pre-visualizacao", + "sampleCheckout": "Checkout de exemplo", + "payNow": "Pagar Agora", + "lowContrastWarning": "As cores podem nao atender as metas de contraste WCAG. Considere ajusta-las.", + "saving": "Salvando…", + "saveBranding": "Salvar Marca", + "previewReceipt": "Pre-visualizar Recibo", + "displayPreferencesTitle": "Preferencias de Exibicao", + "displayPreferencesDescription": "Ajuste como os valores aparecem no painel.", + "hideTrailingCents": "Ocultar centavos finais", + "hideTrailingCentsDescription": "Valores inteiros como 50 serao exibidos sem o sufixo .00.", + "webhookEndpointTitle": "Endpoint do Webhook", + "webhookEndpointDescription": "Receba eventos de pagamento em tempo real no seu servidor.", + "verified": "Verificado", + "unverified": "Nao verificado", + "endpointUrl": "URL do Endpoint", + "saveUrl": "Salvar URL", + "testing": "Testando…", + "sendTest": "Enviar Teste", + "domainVerification": "Verificacao de Dominio", + "hostTokenAt": "Hospede este token em", + "verifying": "Verificando…", + "verifyDomain": "Verificar Dominio", + "signingSecretTitle": "Segredo de Assinatura", + "signingSecretDescriptionPrefix": "Valide o cabecalho", + "signingSecretDescriptionSuffix": "nos webhooks recebidos.", + "hideWebhookSecret": "Ocultar segredo do webhook", + "showWebhookSecret": "Mostrar segredo do webhook", + "webhookSecretVisible": "O segredo do webhook esta visivel. Copie agora — exibido apenas uma vez.", + "webhookSecretHidden": "O segredo do webhook esta oculto. Alterne para inspeciona-lo.", + "regenerateSecretEllipsis": "Regenerar Segredo…", + "regenerateSecretWarning": "O segredo atual deixara de funcionar imediatamente.", + "regenerating": "Regenerando…", + "dangerZoneDescription": "Acoes irreversiveis. Prossiga com cautela.", + "failedToLoadBranding": "Falha ao carregar a marca", + "failedToLoadWebhookSettings": "Falha ao carregar as configuracoes do webhook", + "failedToRotateKey": "Falha ao rotacionar a chave", + "apiKeyRotatedSuccess": "Chave de API rotacionada — atualize as integracoes que usam a chave antiga.", + "imageSizeLimit": "A imagem deve ter menos de 2MB", + "logoUploaded": "Logo enviado!", + "hexColorRequired": "{field} deve ser uma cor hexadecimal valida", + "failedToSaveBranding": "Falha ao salvar a marca", + "brandingSaved": "Marca salva", + "webhookUrlMustBeHttps": "A URL do webhook deve usar HTTPS", + "webhookUrlInvalid": "URL invalida (ex. https://example.com/webhook)", + "failedToSaveWebhookUrl": "Falha ao salvar a URL do webhook", + "webhookUrlSaved": "URL do webhook salva", + "webhookUrlCleared": "URL do webhook removida", + "failedToVerifyDomain": "Falha ao verificar o dominio do webhook", + "domainVerified": "Dominio verificado", + "domainStillUnverified": "Dominio ainda nao verificado", + "failedToRegenerateSecret": "Falha ao regenerar o segredo", + "webhookSecretRegenerated": "Segredo do webhook regenerado — atualize suas integracoes.", + "testWebhookRequestFailed": "Falha na solicitacao de teste do webhook", + "failedToTestWebhook": "Falha ao testar o webhook", + "testWebhookSentStatus": "Webhook de teste enviado — status {status}", + "colorPickerLabel": "Seletor de cor {label}" + }, + "notificationCenter": { + "buttonLabel": "Notificacoes", + "buttonLabelWithCount": "Notificacoes ({count} nao lidas)", + "panelLabel": "Central de Notificacoes", + "heading": "Notificacoes", + "unreadCount": "{count} nao lidas", + "noAlerts": "Sem novos alertas", + "alertLabel": "Alerta", + "dismissLabel": "Dispensar notificacao: {message}", + "notificationLabel": "Notificacao: {message}", + "timestampLabel": "Carimbo de data/hora: {timestamp}" + }, + "fiatOnramp": { + "title": "Comprar / Depositar Fundos", + "triggerLabel": "Comprar / Depositar", + "description": "Deposite fiat atraves de um anchor Stellar (SEP-0024). Os fundos chegam como tokens diretamente na sua carteira conectada.", + "selectAsset": "Selecionar Ativo", + "amountLabel": "Valor", + "amountOptional": "(opcional)", + "amountPlaceholder": "ex. {amount} {asset}", + "anchorDomainLabel": "Dominio do Anchor", + "anchorDomainPlaceholder": "ex. testanchor.stellar.org", + "continueButton": "Continuar para o Anchor", + "footerNote": "Protegido pela rede Stellar - Padrao SEP-0024", + "stepConnecting": "Conectando ao anchor...", + "stepAuth": "Aguardando assinatura da carteira...", + "stepGenerating": "Preparando seu formulario de deposito...", + "authHint": "Assine a transacao de desafio na sua carteira para se conectar com seguranca a {domain}.", + "genericHint": "Isso deve levar apenas um momento.", + "iframeTitle": "Formulario de deposito do anchor", + "genericError": "Falha no deposito" + }, + "kycPage": { + "title": "Verificacao KYC", + "description": "Complete o processo de verificacao para atender aos requisitos regulatorios e desbloquear o acesso completo aos recursos da plataforma.", + "whyKyc": "Por que KYC?", + "reasonComply": "Atender aos requisitos regulatorios", + "reasonLimits": "Aumentar os limites de transacao", + "reasonFeatures": "Acessar recursos premium", + "reasonSecurity": "Seguranca aprimorada da conta" + }, + "kycForm": { + "formTitle": "Formulario de envio KYC", + "step": "Passo", + "of": "de", + "steps": "Passos", + "completed": "concluido", + "current": "atual", + "upcoming": "proximo", + "personalInfo": "Informacoes Pessoais", + "addressInfo": "Informacoes de Endereco", + "documents": "Documentos", + "review": "Revisar", + "firstName": "Nome", + "lastName": "Sobrenome", + "email": "E-mail", + "nationality": "Nacionalidade", + "dateOfBirth": "Data de nascimento", + "street": "Rua", + "city": "Cidade", + "state": "Estado", + "postalCode": "Codigo postal", + "country": "Pais", + "idType": "Tipo de ID", + "selectIdType": "Selecionar tipo de ID", + "passport": "Passaporte", + "driversLicense": "Carteira de motorista", + "nationalId": "ID Nacional", + "idNumber": "Numero do ID", + "idFront": "Frente do ID", + "idBack": "Verso do ID", + "selfie": "Foto selfie", + "required": "Obrigatorio", + "validationError": "Por favor preencha todos os campos obrigatorios", + "back": "Voltar", + "next": "Proximo", + "submit": "Enviar KYC", + "submitting": "Enviando...", + "submitError": "Falha no envio", + "submitAnother": "Enviar outro", + "successTitle": "KYC enviado com sucesso!", + "successDescription": "Sua verificacao KYC foi enviada e esta em revisao.", + "dash": "—" + }, + "darkModeTheme": { + "loading": "Carregando configuracoes de tema", + "errorCleared": "Erro eliminado. Tentando alternar o tema.", + "switchingTo": "Alternando para o tema {theme}.", + "themeChanged": "Tema alterado para {theme}.", + "themeError": "Falha ao alternar o tema. Tente novamente.", + "currentTheme": "Tema atual: {theme}", + "systemTheme": "Sistema ({theme})", + "ariaError": "Alternador de tema com erro — pressione para tentar novamente", + "ariaLabel": "Alternador de tema, tema atual: {theme}. Pressione para ir ao proximo tema.", + "ariaLoading": "O tema esta carregando", + "errorTitle": "Erro de tema: {error}. Pressione para tentar novamente.", + "titleSystem": "Tema: Sistema ({theme})", + "titleLight": "Tema: Claro", + "titleDark": "Tema: Escuro", + "description": "Alterne entre os temas claro, escuro e do sistema. O sistema segue a preferencia do dispositivo. Pressione Espaco ou Enter para ativar.", + "preferenceLabel": "Preferencia de tema", + "preferenceSaved": "Preferencia de tema salva", + "preferenceRestored": "Preferencia de tema restaurada", + "themes": { + "light": "claro", + "dark": "escuro", + "system": "sistema" + }, + "themeNames": { + "light": "Claro", + "dark": "Escuro", + "system": "Sistema" + }, + "sr": { + "sunIcon": "Icone de sol — modo claro ativo", + "moonIcon": "Icone de lua — modo escuro ativo", + "systemIcon": "Icone de monitor — tema do sistema ativo", + "errorIcon": "Icone de erro — falha ao aplicar o tema" + } + }, + "portfolioChartWidget": { + "title": "Valor do portafolio", + "allocation": "Distribuicao", + "trend": "Tendencia", + "loading": "Carregando grafico do portafolio...", + "historyLoading": "Carregando historico de desempenho...", + "emptyAllocation": "Ainda nao ha dados de alocacao.", + "emptyAssets": "Ainda nao ha ativos no portafolio.", + "emptyHistory": "Ainda nao ha historico de desempenho." + } } \ No newline at end of file diff --git a/frontend/src/app/(public)/register/page.tsx b/frontend/src/app/(public)/register/page.tsx index 3d9db6e2..ffb4551d 100644 --- a/frontend/src/app/(public)/register/page.tsx +++ b/frontend/src/app/(public)/register/page.tsx @@ -1,39 +1,102 @@ import RegistrationForm from "@/components/RegistrationForm"; import Link from "next/link"; import GuestGuard from "@/components/GuestGuard"; +import { OnboardingProgressTracker, type OnboardingStep } from "@/components/OnboardingProgressTracker"; + +/** + * Demo steps shown on the registration page so users understand the full + * onboarding journey before they submit the form. + * Defined as a server-side constant — zero runtime cost. + */ +const ONBOARDING_STEPS: OnboardingStep[] = [ + { + id: "create-account", + title: "Create your account", + description: "Register your merchant profile with a secure password.", + completed: false, + required: true, + order: 1, + }, + { + id: "verify-email", + title: "Verify your email", + description: "Confirm your email address to activate your account.", + completed: false, + required: true, + order: 2, + }, + { + id: "api-keys", + title: "Generate API keys", + description: "Create your live API key to start accepting payments.", + completed: false, + required: true, + order: 3, + }, + { + id: "webhook", + title: "Configure a webhook", + description: "Point PLUTO at your server to receive real-time payment events.", + completed: false, + required: false, + order: 4, + }, + { + id: "branding", + title: "Customise checkout branding", + description: "Upload your logo and set brand colours for the checkout page.", + completed: false, + required: false, + order: 5, + }, +]; export default function RegisterPage() { return ( -
+
+ + {/* ── Page header ─────────────────────────────────────────────────── */}
-

+

Onboarding

-

+

Join PLUTO

-

+

Create your merchant profile to start accepting modern payments and managing assets on the PLUTO infrastructure.

+ {/* ── Onboarding Progress Tracker ──────────────────────────────────── */} + + + {/* ── Registration form ────────────────────────────────────────────── */}
+ {/* ── Footer ──────────────────────────────────────────────────────── */}
-

+

Already have an account?{" "} Log in here

+
); diff --git a/frontend/src/components/OnboardingProgressTracker.test.tsx b/frontend/src/components/OnboardingProgressTracker.test.tsx index 558a4f78..e277147b 100644 --- a/frontend/src/components/OnboardingProgressTracker.test.tsx +++ b/frontend/src/components/OnboardingProgressTracker.test.tsx @@ -1,13 +1,15 @@ /** * @vitest-environment jsdom * - * Unit tests for OnboardingProgressTracker + * Unit tests for OnboardingProgressTracker (i18n refactor) * * Covers: - * - #809 framer-motion animation variants and reduced-motion support - * - #810 comprehensive unit test coverage - * - #811 screen reader / accessibility attributes - * - #812 optimistic updates and rollback behaviour + * - #809 framer-motion animation variants and reduced-motion support + * - #810 rendering, props, interactions, completion + * - #811 screen-reader / accessibility attributes + * - #812 optimistic updates and rollback + * - i18n useOnboardingI18n hook integration (translated strings) + * - hook useOnboardingProgress (SYNC_STEPS, external currentStep sync) */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; @@ -19,33 +21,48 @@ import React from "react"; // Mocks // --------------------------------------------------------------------------- +/** + * next-intl: return the key (with inline {token} substitution) so assertions + * are locale-independent and don't depend on the real message catalogue. + */ vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, + useTranslations: () => (key: string, params?: Record) => { + if (!params) return key; + return Object.entries(params).reduce( + (acc, [k, v]) => acc.replace(`{${k}}`, String(v)), + key, + ); + }, })); -// Mock framer-motion — renders plain HTML elements so tests stay fast — #809 +/** + * framer-motion: strip animation props and render plain HTML so tests run fast + * and don't depend on JSDOM animation support. (#809) + */ vi.mock("framer-motion", () => ({ - motion: { - div: React.forwardRef(({ children, ...props }: any, ref: any) => ( -
{children}
- )), - ol: React.forwardRef(({ children, ...props }: any, ref: any) => ( -
    {children}
- )), - li: React.forwardRef(({ children, ...props }: any, ref: any) => ( -
  • {children}
  • - )), - button: React.forwardRef(({ children, ...props }: any, ref: any) => ( - - )), - span: React.forwardRef(({ children, ...props }: any, ref: any) => ( - {children} - )), - svg: React.forwardRef(({ children, ...props }: any, ref: any) => ( - {children} - )), - }, - AnimatePresence: ({ children }: any) => <>{children}, + motion: new Proxy( + {}, + { + get: (_t, tag: string) => + React.forwardRef( + ( + { + children, + animate, + variants, + initial, + exit, + transition, + whileHover, + whileTap, + ...rest + }: any, + ref: any, + ) => React.createElement(tag, { ...rest, ref }, children), + ), + }, + ), + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, useReducedMotion: () => false, })); @@ -53,32 +70,20 @@ vi.mock("framer-motion", () => ({ // Fixtures // --------------------------------------------------------------------------- -const mockSteps = [ - { - id: "1", - title: "Step 1", - description: "Description 1", - completed: true, - required: true, - order: 1, - }, - { - id: "2", - title: "Step 2", - description: "Description 2", - completed: false, - required: true, - order: 2, - }, - { - id: "3", - title: "Step 3", - description: "Description 3", - completed: false, - required: false, - order: 3, - }, -]; +const step1 = { + id: "1", title: "Step 1", description: "Desc 1", + completed: true, required: true, order: 1, +}; +const step2 = { + id: "2", title: "Step 2", description: "Desc 2", + completed: false, required: true, order: 2, +}; +const step3 = { + id: "3", title: "Step 3", description: "Desc 3", + completed: false, required: false, order: 3, +}; + +const mockSteps = [step1, step2, step3]; const defaultProps = { steps: mockSteps, @@ -86,46 +91,56 @@ const defaultProps = { onComplete: vi.fn(), }; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Find a step button using a fragment of its translated aria-label. + * The mock translates e.g. "stepLabelCompletedRequired" → + * "stepLabelCompletedRequired" (the key), but our assertions match on title. + */ +const getStepBtn = (fragment: string) => + screen.getByRole("button", { name: new RegExp(fragment, "i") }); + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("OnboardingProgressTracker", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => vi.clearAllMocks()); - // ------------------------------------------------------------------------- - // #810 — Rendering - // ------------------------------------------------------------------------- + // ── #810 Rendering ──────────────────────────────────────────────────────── describe("Rendering", () => { - it("renders the component with the correct title", () => { + it("renders the translated title key", () => { render(); - expect(screen.getByText("onboarding.title")).toBeInTheDocument(); + expect(screen.getByText("title")).toBeInTheDocument(); }); - it("renders all steps with correct titles and descriptions", () => { + it("renders the translated subtitle key", () => { render(); - mockSteps.forEach((step) => { - expect(screen.getByText(step.title)).toBeInTheDocument(); - expect(screen.getByText(step.description)).toBeInTheDocument(); - }); + expect(screen.getByText("subtitle")).toBeInTheDocument(); }); - it("shows the correct progress percentage", () => { + it("renders all step titles and descriptions", () => { render(); - // 1 of 3 completed = 33 % - expect(screen.getByText("33%")).toBeInTheDocument(); + mockSteps.forEach((s) => { + expect(screen.getByText(s.title)).toBeInTheDocument(); + expect(screen.getByText(s.description)).toBeInTheDocument(); + }); }); it("renders the progress bar fill element", () => { render(); - const fill = screen.getByTestId("progress-bar-fill"); - expect(fill).toBeInTheDocument(); - expect(fill).toHaveStyle("width: 33%"); + expect(screen.getByTestId("progress-bar-fill")).toBeInTheDocument(); }); - it("renders the progress bar with correct ARIA attributes", () => { + it("sets width on progress bar fill to 33%", () => { + render(); + expect(screen.getByTestId("progress-bar-fill")).toHaveStyle("width: 33%"); + }); + + it("sets correct ARIA attributes on the progressbar element", () => { render(); const bar = screen.getByRole("progressbar"); expect(bar).toHaveAttribute("aria-valuenow", "33"); @@ -140,82 +155,88 @@ describe("OnboardingProgressTracker", () => { it("shows the completion banner when all required steps are done", async () => { const allDone = [ - { ...mockSteps[0], completed: true }, - { ...mockSteps[1], completed: true }, - { ...mockSteps[2], completed: false }, // optional — not required + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, // optional — does not block completion ]; render(); - await waitFor(() => { - expect(screen.getByTestId("completion-banner")).toBeInTheDocument(); - }); + await waitFor(() => + expect(screen.getByTestId("completion-banner")).toBeInTheDocument(), + ); }); - }); - // ------------------------------------------------------------------------- - // #810 — Props and variants - // ------------------------------------------------------------------------- - describe("Props and variants", () => { - it("hides step numbers when showStepNumbers is false", () => { - render(); - // Numbers 1, 2, 3 should not appear as standalone text nodes - expect(screen.queryByText("1")).not.toBeInTheDocument(); - expect(screen.queryByText("2")).not.toBeInTheDocument(); + it("renders the sr-only announcement region", () => { + render(); + expect(screen.getByTestId("sr-announcement")).toBeInTheDocument(); }); + }); - it("applies compact padding class when compact prop is true", () => { + // ── #810 Props / variants ───────────────────────────────────────────────── + describe("Props and variants", () => { + it("applies compact padding class when compact=true", () => { const { container } = render( - + , ); expect(container.querySelector(".p-4")).toBeInTheDocument(); }); - it("applies horizontal layout classes when orientation is horizontal", () => { - render(); - const list = screen.getByRole("list"); - expect(list).toHaveClass("flex"); - expect(list).toHaveClass("gap-4"); + it("renders the correct number of list items", () => { + render(); + expect(screen.getAllByRole("listitem")).toHaveLength(3); }); - it("renders a vertical list by default", () => { - render(); - const list = screen.getByRole("list"); - expect(list).not.toHaveClass("flex"); + it("applies an extra className to the root wrapper", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("extra-class"); }); }); - // ------------------------------------------------------------------------- - // #810 — Interactions - // ------------------------------------------------------------------------- + // ── #810 Interactions ───────────────────────────────────────────────────── describe("Interactions", () => { - it("calls onStepChange when a step button is clicked", () => { + it("calls onStepChange when a step button is clicked", async () => { render(); - const btn = screen.getByLabelText(/Step 2: Step 2. Required/i); - fireEvent.click(btn); - expect(defaultProps.onStepChange).toHaveBeenCalledWith("2"); + // Step 2 aria-label uses the "stepLabelRequired" key → "stepLabelRequired" + // Our mock substitutes {number} and {title}: "Step 2: Step 2. Required" + const btn2 = screen.getAllByRole("button")[1]; // second step + fireEvent.click(btn2); + await waitFor(() => + expect(defaultProps.onStepChange).toHaveBeenCalledWith("2"), + ); }); - it("calls onStepChange with the correct step id", () => { - render(); - const btn = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - fireEvent.click(btn); - expect(defaultProps.onStepChange).toHaveBeenCalledWith("1"); + it("does not call onStepChange while another step change is pending", async () => { + let resolveCallback!: () => void; + const slowCb = vi.fn( + () => new Promise((res) => { resolveCallback = res; }), + ); + render( + , + ); + + const buttons = screen.getAllByRole("button"); + fireEvent.click(buttons[1]); // click step 2 + + await waitFor(() => expect(buttons[0]).toBeDisabled()); + + fireEvent.click(buttons[2]); // try clicking step 3 while pending + expect(slowCb).toHaveBeenCalledTimes(1); + + act(() => resolveCallback()); }); }); - // ------------------------------------------------------------------------- - // #810 — Completion logic - // ------------------------------------------------------------------------- + // ── #810 Completion ─────────────────────────────────────────────────────── describe("Completion logic", () => { it("calls onComplete when all required steps are completed", async () => { const allRequired = [ - { ...mockSteps[0], completed: true }, - { ...mockSteps[1], completed: true }, - { ...mockSteps[2], completed: false }, + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, ]; render(); - await waitFor(() => { - expect(defaultProps.onComplete).toHaveBeenCalled(); - }); + await waitFor(() => expect(defaultProps.onComplete).toHaveBeenCalled()); }); it("does not call onComplete when a required step is incomplete", () => { @@ -223,102 +244,103 @@ describe("OnboardingProgressTracker", () => { expect(defaultProps.onComplete).not.toHaveBeenCalled(); }); - it("shows success title in completion banner", async () => { + it("renders the translated successTitle in the completion banner", async () => { const allRequired = [ - { ...mockSteps[0], completed: true }, - { ...mockSteps[1], completed: true }, - { ...mockSteps[2], completed: false }, + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, ]; render(); - await waitFor(() => { - expect(screen.getByText("onboarding.successTitle")).toBeInTheDocument(); - }); + await waitFor(() => + expect(screen.getByText("successTitle")).toBeInTheDocument(), + ); }); - it("does not show success title when onboarding is incomplete", () => { - render(); - expect(screen.queryByText("onboarding.successTitle")).not.toBeInTheDocument(); + it("renders the translated successMessage in the completion banner", async () => { + const allRequired = [ + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, + ]; + render(); + await waitFor(() => + expect(screen.getByText("successMessage")).toBeInTheDocument(), + ); }); }); - // ------------------------------------------------------------------------- - // #811 — Accessibility / screen reader support - // ------------------------------------------------------------------------- - describe("Accessibility — screen reader support (#811)", () => { - it("wraps the tracker in a region landmark with aria-label", () => { + // ── #811 Accessibility ──────────────────────────────────────────────────── + describe("Accessibility (#811)", () => { + it("wraps tracker in a region with translated aria-label", () => { render(); - const region = screen.getByRole("region"); - expect(region).toHaveAttribute("aria-label", "onboarding.progressTracker"); + expect(screen.getByRole("region")).toHaveAttribute( + "aria-label", + "progressTracker", + ); }); it("sets aria-live='polite' on the region", () => { render(); - const region = screen.getByRole("region"); - expect(region).toHaveAttribute("aria-live", "polite"); - }); - - it("renders a live status region for announcements", () => { - render(); - const status = screen.getByRole("status"); - expect(status).toBeInTheDocument(); + expect(screen.getByRole("region")).toHaveAttribute("aria-live", "polite"); }); - it("announces progress percentage on mount", () => { + it("provides an aria-live status region for announcements", () => { render(); - const status = screen.getByRole("status"); - expect(status.textContent).toContain("33"); + expect(screen.getByRole("status")).toBeInTheDocument(); }); - it("announces step details when a step is clicked", async () => { + it("announces progress on mount", () => { render(); - const btn = screen.getByLabelText(/Step 2: Step 2. Required/i); - fireEvent.click(btn); - const status = screen.getByRole("status"); - await waitFor(() => { - expect(status.textContent).toContain("Step 2"); - expect(status.textContent).toContain("Description 2"); - }); + expect(screen.getByRole("status").textContent).toMatch(/33/); }); - it("labels the steps list with aria-label", () => { + it("labels the steps list with the translated key", () => { render(); - const list = screen.getByRole("list"); - expect(list).toHaveAttribute("aria-label", "onboarding.stepsList"); + expect(screen.getByRole("list")).toHaveAttribute("aria-label", "stepsList"); }); - it("marks the current step with aria-current='step'", () => { + it("sets aria-current='step' on the active step", () => { render(); - const btn = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - expect(btn).toHaveAttribute("aria-current", "step"); + expect(screen.getAllByRole("button")[0]).toHaveAttribute( + "aria-current", + "step", + ); }); - it("does not set aria-current on non-current steps", () => { + it("does not set aria-current on inactive steps", () => { render(); - const btn = screen.getByLabelText(/Step 2: Step 2. Required/i); - expect(btn).not.toHaveAttribute("aria-current"); + expect(screen.getAllByRole("button")[1]).not.toHaveAttribute("aria-current"); }); it("sets aria-setsize and aria-posinset on step buttons", () => { render(); - const btn1 = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - expect(btn1).toHaveAttribute("aria-setsize", "3"); - expect(btn1).toHaveAttribute("aria-posinset", "1"); - - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); - expect(btn2).toHaveAttribute("aria-posinset", "2"); + const buttons = screen.getAllByRole("button"); + expect(buttons[0]).toHaveAttribute("aria-setsize", "3"); + expect(buttons[0]).toHaveAttribute("aria-posinset", "1"); + expect(buttons[1]).toHaveAttribute("aria-posinset", "2"); + expect(buttons[2]).toHaveAttribute("aria-posinset", "3"); }); it("sets aria-roledescription on step buttons", () => { render(); - const btn = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - expect(btn).toHaveAttribute("aria-roledescription", "onboarding step"); + screen.getAllByRole("button").forEach((btn) => { + expect(btn).toHaveAttribute("aria-roledescription", "onboarding step"); + }); + }); + + it("marks the progressbar with translated aria-label", () => { + render(); + expect(screen.getByRole("progressbar")).toHaveAttribute( + "aria-label", + "progressBar", + ); }); it("completion banner has role='alert' and aria-live='polite'", async () => { const allDone = [ - { ...mockSteps[0], completed: true }, - { ...mockSteps[1], completed: true }, - { ...mockSteps[2], completed: false }, + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, ]; render(); await waitFor(() => { @@ -328,152 +350,238 @@ describe("OnboardingProgressTracker", () => { }); }); - it("marks required asterisk with aria-label", () => { + it("marks required asterisk with translated aria-label", () => { render(); - const requiredMarkers = screen.getAllByLabelText("onboarding.required"); - expect(requiredMarkers.length).toBeGreaterThan(0); + // Both step1 and step2 are required → two * elements + expect(screen.getAllByLabelText("required").length).toBeGreaterThan(0); + }); + + it("announces step details when a step button is clicked", async () => { + render(); + fireEvent.click(screen.getAllByRole("button")[1]); + await waitFor(() => { + expect(screen.getByRole("status").textContent).toMatch(/Step 2|Desc 2/); + }); }); }); - // ------------------------------------------------------------------------- - // #812 — Optimistic updates - // ------------------------------------------------------------------------- + // ── #812 Optimistic updates ─────────────────────────────────────────────── describe("Optimistic updates (#812)", () => { - it("immediately reflects the clicked step as current before callback resolves", async () => { + it("immediately marks the clicked step as current before callback resolves", async () => { let resolveCallback!: () => void; - const slowCallback = vi.fn( - () => new Promise((res) => { resolveCallback = res; }) + const slowCb = vi.fn( + () => new Promise((res) => { resolveCallback = res; }), ); - render( + />, ); - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); + const btn2 = screen.getAllByRole("button")[1]; fireEvent.click(btn2); - - // Optimistic: step 2 should be current immediately - await waitFor(() => { - expect(btn2).toHaveAttribute("aria-current", "step"); - }); - - // Resolve the callback + await waitFor(() => + expect(btn2).toHaveAttribute("aria-current", "step"), + ); act(() => resolveCallback()); }); it("confirms the optimistic update after callback resolves", async () => { - const asyncCallback = vi.fn(() => Promise.resolve()); + const asyncCb = vi.fn(() => Promise.resolve()); render( + />, ); - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); + const btn2 = screen.getAllByRole("button")[1]; fireEvent.click(btn2); - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith("2"); + expect(asyncCb).toHaveBeenCalledWith("2"); expect(btn2).toHaveAttribute("aria-current", "step"); }); }); - it("rolls back the optimistic update when callback throws", async () => { - const failingCallback = vi.fn(() => Promise.reject(new Error("server error"))); - + it("rolls back to the previous step when the callback throws", async () => { + const failCb = vi.fn(() => Promise.reject(new Error("server error"))); render( + />, ); - const btn1 = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); + const btn1 = screen.getAllByRole("button")[0]; + const btn2 = screen.getAllByRole("button")[1]; fireEvent.click(btn2); - - // After rollback, step 1 should be current again await waitFor(() => { expect(btn1).toHaveAttribute("aria-current", "step"); expect(btn2).not.toHaveAttribute("aria-current"); }); }); - it("announces a failure message to screen readers on rollback", async () => { - const failingCallback = vi.fn(() => Promise.reject(new Error("fail"))); - + it("announces the translated stepChangeFailed key on rollback", async () => { + const failCb = vi.fn(() => Promise.reject(new Error("fail"))); render( + />, ); - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); - fireEvent.click(btn2); - + fireEvent.click(screen.getAllByRole("button")[1]); await waitFor(() => { - const status = screen.getByRole("status"); - expect(status.textContent).toContain("onboarding.stepChangeFailed"); + expect(screen.getByRole("status").textContent).toContain( + "stepChangeFailed", + ); }); }); - it("sets aria-busy on the pending step button during optimistic update", async () => { + it("sets aria-busy on the pending step button", async () => { let resolveCallback!: () => void; - const slowCallback = vi.fn( - () => new Promise((res) => { resolveCallback = res; }) + const slowCb = vi.fn( + () => new Promise((res) => { resolveCallback = res; }), ); - render( + />, ); - const btn2 = screen.getByLabelText(/Step 2: Step 2. Required/i); + const btn2 = screen.getAllByRole("button")[1]; fireEvent.click(btn2); + await waitFor(() => expect(btn2).toHaveAttribute("aria-busy", "true")); + act(() => resolveCallback()); + }); + + it("disables all step buttons while pending", async () => { + let resolveCallback!: () => void; + const slowCb = vi.fn( + () => new Promise((res) => { resolveCallback = res; }), + ); + render( + , + ); + fireEvent.click(screen.getAllByRole("button")[1]); await waitFor(() => { - expect(btn2).toHaveAttribute("aria-busy", "true"); + screen.getAllByRole("button").forEach((btn) => + expect(btn).toBeDisabled(), + ); }); - act(() => resolveCallback()); }); }); - // ------------------------------------------------------------------------- - // #809 — Animation / reduced-motion - // ------------------------------------------------------------------------- + // ── i18n ────────────────────────────────────────────────────────────────── + describe("i18n — useOnboardingI18n", () => { + it("uses 'progressTracker' key for region aria-label", () => { + render(); + expect(screen.getByRole("region")).toHaveAttribute( + "aria-label", + "progressTracker", + ); + }); + + it("uses 'progressBar' key for progressbar aria-label", () => { + render(); + expect(screen.getByRole("progressbar")).toHaveAttribute( + "aria-label", + "progressBar", + ); + }); + + it("uses 'stepsList' key for list aria-label", () => { + render(); + expect(screen.getByRole("list")).toHaveAttribute("aria-label", "stepsList"); + }); + + it("renders allCompleted when onboarding is complete", async () => { + const allDone = [ + { ...step1, completed: true }, + { ...step2, completed: true }, + { ...step3, completed: false }, + ]; + render(); + await waitFor(() => + expect(screen.getByText("allCompleted")).toBeInTheDocument(), + ); + }); + + it("uses parameterised percentComplete key in header badge", () => { + render(); + // 1 of 3 = 33 — mock produces "percentComplete" with {percent} substituted + expect(screen.getByText(/33/)).toBeInTheDocument(); + }); + + it("uses parameterised stepsCompleted key in summary line", () => { + render(); + // mock renders "stepsCompleted" with tokens substituted + expect(screen.getAllByText(/stepsCompleted|1.*3/i).length).toBeGreaterThan(0); + }); + }); + + // ── #809 Animations ─────────────────────────────────────────────────────── describe("Animations (#809)", () => { - it("renders the progress bar fill element used for animation", () => { + it("renders the progress bar fill element", () => { render(); expect(screen.getByTestId("progress-bar-fill")).toBeInTheDocument(); }); - it("renders step list items that carry animation variant props", () => { + it("renders all step list items", () => { render(); - // All 3 steps should be in the DOM - const items = screen.getAllByRole("listitem"); - expect(items).toHaveLength(3); + expect(screen.getAllByRole("listitem")).toHaveLength(3); }); + }); - it("renders completed step with a checkmark icon", () => { - render(); - // Step 1 is completed — its button should not show a number - const btn1 = screen.getByLabelText(/Step 1: Step 1. Completed. Required/i); - // The number span is not rendered for completed steps - expect(btn1.querySelector("span")).not.toBeInTheDocument(); + // ── Hook integration (SYNC_STEPS / currentStep prop sync) ───────────────── + describe("useOnboardingProgress integration", () => { + it("syncs an external currentStep prop change", async () => { + const { rerender } = render( + , + ); + expect(screen.getAllByRole("button")[0]).toHaveAttribute("aria-current", "step"); + + rerender(); + + await waitFor(() => + expect(screen.getAllByRole("button")[1]).toHaveAttribute("aria-current", "step"), + ); + }); + + it("updates progress percentage when a completed step is added via SYNC_STEPS", async () => { + const { rerender } = render( + , + ); + expect(screen.getByTestId("progress-bar-fill")).toHaveStyle("width: 33%"); + + rerender( + , + ); + + await waitFor(() => + expect(screen.getByTestId("progress-bar-fill")).toHaveStyle("width: 67%"), + ); }); }); }); diff --git a/frontend/src/components/OnboardingProgressTracker.tsx b/frontend/src/components/OnboardingProgressTracker.tsx index 4fb85f34..e60b72df 100644 --- a/frontend/src/components/OnboardingProgressTracker.tsx +++ b/frontend/src/components/OnboardingProgressTracker.tsx @@ -1,274 +1,298 @@ "use client"; -import React, { useCallback, useMemo, useEffect, useId, useReducer, useRef } from "react"; -import { motion, AnimatePresence, useReducedMotion, type Variants } from "framer-motion"; -import { useTranslations } from "next-intl"; -import { onboardingReducer, createInitialOnboardingState } from "./onboarding-reducer"; - /** - * Step interface for onboarding progress + * OnboardingProgressTracker + * + * Refactored for full i18n support via the "onboarding" next-intl namespace. + * + * Additional improvements: + * - All user-visible strings sourced from useOnboardingI18n (no hardcoded English) + * - Dark mode: dark: Tailwind variants on every surface, border, text, gradient + * - Mobile-first responsive layout (vertical default, horizontal on sm+) + * - Improved a11y: translated aria-labels, aria-busy spinner, focus-visible rings + * - StepIcon and StatusBadge extracted as memoised sub-components + * - prefers-reduced-motion respected throughout + * - Connector lines between vertical steps */ -interface OnboardingStep { - id: string; - title: string; - description: string; - completed: boolean; - required: boolean; - order: number; -} -/** - * Props for OnboardingProgressTracker component - */ -interface OnboardingProgressTrackerProps { +import React, { memo } from "react"; +import { + motion, + AnimatePresence, + useReducedMotion, + type Variants, +} from "framer-motion"; +import { + useOnboardingProgress, + type OnboardingStep, +} from "@/hooks/useOnboardingProgress"; +import { useOnboardingI18n } from "@/hooks/useOnboardingI18n"; + +// ── Re-export so consumers need only one import ─────────────────────────────── +export type { OnboardingStep }; + +// ── Props ───────────────────────────────────────────────────────────────────── + +export interface OnboardingProgressTrackerProps { steps: OnboardingStep[]; currentStep?: string; - onStepChange?: (stepId: string) => void; + onStepChange?: (stepId: string) => void | Promise; onComplete?: () => void; + /** Show numeric labels inside step circles. Default: true. */ showStepNumbers?: boolean; + /** Stack direction. Default: "vertical". */ orientation?: "vertical" | "horizontal"; + /** Reduced padding variant. Default: false. */ compact?: boolean; + /** Extra className on the root wrapper. */ + className?: string; } -// --------------------------------------------------------------------------- -// Animation variants — #809 framer-motion animations -// --------------------------------------------------------------------------- +// ── Animation variants ──────────────────────────────────────────────────────── -/** - * Animation variants for step container — staggered children entrance - */ const containerVariants: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, - transition: { - staggerChildren: 0.1, - delayChildren: 0.2, - }, + transition: { staggerChildren: 0.08, delayChildren: 0.15 }, }, }; -/** - * Animation variants for individual steps — slide-in from left - */ const stepVariants: Variants = { - hidden: { opacity: 0, x: -20 }, - visible: { - opacity: 1, - x: 0, - transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] }, - }, - exit: { - opacity: 0, - x: 20, - transition: { duration: 0.2 }, - }, + hidden: { opacity: 0, x: -16 }, + visible: { opacity: 1, x: 0, transition: { duration: 0.35, ease: [0.16, 1, 0.3, 1] } }, + exit: { opacity: 0, x: 16, transition: { duration: 0.2 } }, }; -/** Reduced-motion safe step variants — no translate, only fade */ const stepVariantsReduced: Variants = { hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.2 } }, - exit: { opacity: 0, transition: { duration: 0.1 } }, + visible: { opacity: 1, transition: { duration: 0.15 } }, + exit: { opacity: 0, transition: { duration: 0.1 } }, }; -/** - * Animation variants for progress bar — scale from left origin - */ const progressBarVariants: Variants = { hidden: { scaleX: 0, originX: 0 }, - visible: { - scaleX: 1, - originX: 0, - transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] }, - }, + visible: { scaleX: 1, originX: 0, transition: { duration: 0.55, ease: [0.16, 1, 0.3, 1] } }, }; -/** Reduced-motion progress bar — just fade */ const progressBarVariantsReduced: Variants = { hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.3 } }, + visible: { opacity: 1, transition: { duration: 0.25 } }, }; -/** - * Animation variants for check mark — spring pop-in - */ const checkMarkVariants: Variants = { hidden: { scale: 0, opacity: 0 }, visible: { - scale: 1, - opacity: 1, - transition: { - type: "spring", - stiffness: 260, - damping: 20, - delay: 0.2, - }, + scale: 1, opacity: 1, + transition: { type: "spring", stiffness: 280, damping: 22, delay: 0.15 }, }, }; -/** Reduced-motion check mark — simple fade */ const checkMarkVariantsReduced: Variants = { hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.2 } }, + visible: { opacity: 1, transition: { duration: 0.15 } }, }; -/** - * Animation variants for completion banner — slide up - */ const completionVariants: Variants = { - hidden: { opacity: 0, y: 10 }, - visible: { opacity: 1, y: 0, transition: { duration: 0.35, ease: "easeOut" } }, - exit: { opacity: 0, y: -10, transition: { duration: 0.2 } }, + hidden: { opacity: 0, y: 12 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: "easeOut" } }, + exit: { opacity: 0, y: -8, transition: { duration: 0.2 } }, }; -/** Reduced-motion completion banner */ const completionVariantsReduced: Variants = { hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.2 } }, - exit: { opacity: 0, transition: { duration: 0.1 } }, + visible: { opacity: 1, transition: { duration: 0.15 } }, + exit: { opacity: 0, transition: { duration: 0.1 } }, }; -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -/** - * OnboardingProgressTracker - * - * Displays onboarding progress with: - * - framer-motion animations (entrance, progress bar, check marks, completion) — #809 - * - Comprehensive unit-test surface (exported types, deterministic state) — #810 - * - Full screen-reader support: ARIA live regions, aria-roledescription, - * aria-setsize / aria-posinset, aria-valuenow on progress bar — #811 - * - Optimistic updates with rollback on step navigation — #812 - */ -export const OnboardingProgressTracker: React.FC = ({ - steps, - currentStep: currentStepProp, - onStepChange, - onComplete, - showStepNumbers = true, - orientation = "vertical", - compact = false, -}) => { - const t = useTranslations(); - const progressSummaryId = useId(); +// ── StepIcon ────────────────────────────────────────────────────────────────── - // Respect user's OS-level "reduce motion" preference — #809 - const prefersReducedMotion = useReducedMotion(); +interface StepIconProps { + completed: boolean; + isPending: boolean; + isCurrent: boolean; + number: number; + showNumber: boolean; + compact: boolean; + checkVariants: Variants; + prefersReducedMotion: boolean | null; +} - const [state, dispatch] = useReducer( - onboardingReducer, - createInitialOnboardingState(currentStepProp || steps[0]?.id), +const StepIcon = memo(function StepIcon({ + completed, + isPending, + isCurrent, + number, + showNumber, + compact, + checkVariants, + prefersReducedMotion, +}: StepIconProps) { + return ( + + {completed ? ( + + + + ) : isPending ? ( + + + + ) : ( + + )} + ); +}); - // Track previous step for rollback — #812 - const previousStepRef = useRef(state.currentStep); +// ── StatusBadge ─────────────────────────────────────────────────────────────── - /** Sort steps by order */ - const sortedSteps = useMemo( - () => [...steps].sort((a, b) => a.order - b.order), - [steps] - ); +interface StatusBadgeProps { + completed: boolean; + isCurrent: boolean; + compact: boolean; + completedLabel: string; + inProgressLabel: string; + pendingLabel: string; + prefersReducedMotion: boolean | null; +} - /** Number of completed steps for visible and assistive summaries */ - const completedStepsCount = useMemo( - () => sortedSteps.filter((s) => s.completed).length, - [sortedSteps], - ); +const StatusBadge = memo(function StatusBadge({ + completed, + isCurrent, + compact, + completedLabel, + inProgressLabel, + pendingLabel, + prefersReducedMotion, +}: StatusBadgeProps) { + const label = completed + ? completedLabel + : isCurrent + ? inProgressLabel + : pendingLabel; + + const colorClass = completed + ? "bg-pluto-100 text-pluto-800 dark:bg-pluto-900/40 dark:text-pluto-200" + : isCurrent + ? "bg-pluto-200 text-pluto-900 dark:bg-pluto-800/50 dark:text-pluto-100" + : "bg-pluto-50 text-pluto-700 dark:bg-pluto-900/20 dark:text-pluto-300 group-hover:bg-pluto-100 dark:group-hover:bg-pluto-900/40"; - /** Progress percentage based on completed steps */ - const progressPercentage = useMemo(() => { - if (sortedSteps.length === 0) return 0; - return Math.round((completedStepsCount / sortedSteps.length) * 100); - }, [completedStepsCount, sortedSteps.length]); - - /** Stable screen-reader summary for the tracker and progress bar */ - const progressSummary = `${completedStepsCount} of ${sortedSteps.length} steps completed. ${progressPercentage}% complete.`; - - /** True when all required steps are completed */ - const isOnboardingComplete = useMemo(() => { - const requiredSteps = sortedSteps.filter((s) => s.required); - return requiredSteps.length > 0 && requiredSteps.every((s) => s.completed); - }, [sortedSteps]); - - /** - * Effective current step — optimistic value takes precedence while pending — #812 - */ - const effectiveCurrentStep = state.optimisticStep ?? state.currentStep; - - /** - * Handle step click with optimistic update — #812 - * Immediately reflects the new step in UI, then calls the callback. - * If the callback throws, the optimistic update is rolled back. - */ - const handleStepClick = useCallback( - async (stepId: string) => { - if (state.isPending) return; - - const step = sortedSteps.find((s) => s.id === stepId); - if (!step) return; - - // Store previous step for potential rollback - previousStepRef.current = effectiveCurrentStep; - - // Optimistic update — UI responds immediately — #812 - dispatch({ type: "OPTIMISTIC_STEP", payload: stepId }); - - // Announce to screen readers immediately — #811 - const announcement = `${t("onboarding.stepProgress") || "Step"} ${step.order} of ${sortedSteps.length}: ${step.title}. ${step.description}. ${step.completed ? t("onboarding.completed") || "Completed" : t("onboarding.pending") || "Pending"}.`; - dispatch({ type: "SET_ANNOUNCEMENT", payload: announcement }); - - try { - // Call the callback (may be async / server-side) - await onStepChange?.(stepId); - // Confirm the optimistic update - dispatch({ type: "CONFIRM_STEP", payload: stepId }); - } catch { - // Rollback on failure — #812 - dispatch({ type: "ROLLBACK_STEP" }); - const rollbackAnnouncement = t("onboarding.stepChangeFailed") || "Step change failed. Please try again."; - dispatch({ type: "SET_ANNOUNCEMENT", payload: rollbackAnnouncement }); - } - }, - [sortedSteps, effectiveCurrentStep, onStepChange, state.isPending, t] + return ( + + {label} + ); +}); + +// ── Main component ──────────────────────────────────────────────────────────── - /** Announce completion and fire callback — #811 */ - useEffect(() => { - if (isOnboardingComplete && sortedSteps.length > 0) { - const announcement = t("onboarding.completed") || "Onboarding completed. All required steps are done."; - dispatch({ type: "SET_ANNOUNCEMENT", payload: announcement }); - onComplete?.(); - } - }, [isOnboardingComplete, sortedSteps.length, onComplete, t]); - - /** Announce progress percentage changes — #811 */ - useEffect(() => { - const msg = `${t("onboarding.progress") || "Progress"}: ${progressPercentage}% complete`; - dispatch({ type: "SET_ANNOUNCEMENT", payload: msg }); - }, [progressPercentage, t]); - - // Pick motion variants based on reduced-motion preference — #809 - const activeStepVariants = prefersReducedMotion ? stepVariantsReduced : stepVariants; +export const OnboardingProgressTracker = memo(function OnboardingProgressTracker({ + steps, + currentStep, + onStepChange, + onComplete, + showStepNumbers = true, + orientation = "vertical", + compact = false, + className = "", +}: OnboardingProgressTrackerProps) { + const i18n = useOnboardingI18n(); + const prefersReducedMotion = useReducedMotion(); + + const { + sortedSteps, + effectiveCurrentStep, + state, + progressPercent, + completedCount, + isComplete, + progressSummaryId, + handleStepClick, + } = useOnboardingProgress({ steps, currentStep, onStepChange, onComplete }); + + // Pick motion variants based on reduced-motion preference + const activeStepVariants = prefersReducedMotion ? stepVariantsReduced : stepVariants; const activeProgressBarVariants = prefersReducedMotion ? progressBarVariantsReduced : progressBarVariants; - const activeCheckMarkVariants = prefersReducedMotion ? checkMarkVariantsReduced : checkMarkVariants; - const activeCompletionVariants = prefersReducedMotion ? completionVariantsReduced : completionVariants; + const activeCheckMarkVariants = prefersReducedMotion ? checkMarkVariantsReduced : checkMarkVariants; + const activeCompletionVariants = prefersReducedMotion ? completionVariantsReduced : completionVariants; return (
    + {/* sr-only progress summary */}

    - {progressSummary} + {i18n.stepsCompletedLabel(completedCount, sortedSteps.length)}{" "} + {i18n.percentCompleteLabel(progressPercent)}

    - {/* Screen reader live announcement area — #811 */} + {/* Assertive live region for step-change announcements */}
    {state.announcementText}
    - {/* Pending indicator for screen readers — #812 */} + {/* Polite pending indicator */} {state.isPending && (
    - {t("onboarding.updating") || "Updating step…"} + {i18n.updating}
    )} - {/* Container */} + {/* ── Card ──────────────────────────────────────────────────────────── */}
    - {/* Header */} -
    -
    -

    - {t("onboarding.title") || "Onboarding Progress"} + {/* ── Header ──────────────────────────────────────────────────────── */} +
    +
    +

    + {i18n.title}

    -

    - {t("onboarding.subtitle") || "Complete all required steps to finish setup"} + +

    + {i18n.subtitle}

    - {/* Overall progress bar — animated with framer-motion — #809 */} + {/* Progress bar */}
    - {/* Status text */} -
    - {/* Steps list — staggered entrance animation — #809 */} + {/* ── Steps list ───────────────────────────────────────────────────── */} > {sortedSteps.map((step, index) => { - const isCurrentStep = effectiveCurrentStep === step.id; - const isPendingStep = state.isPending && isCurrentStep; - const stepDescriptionId = `${progressSummaryId}-step-${index + 1}-description`; + const isCurrent = effectiveCurrentStep === step.id; + const isPending = state.isPending && isCurrent; + const stepDescId = `${progressSummaryId}-desc-${index}`; + + const indicatorColorClass = step.completed + ? "border-pluto-500 bg-pluto-100 text-pluto-800 shadow-[0_4px_12px_rgba(74,111,165,0.18)] dark:border-pluto-400 dark:bg-pluto-800/60 dark:text-pluto-100" + : isCurrent + ? "border-pluto-600 bg-pluto-50 text-pluto-700 shadow-[0_4px_12px_rgba(74,111,165,0.14)] dark:border-pluto-400 dark:bg-pluto-900/60 dark:text-pluto-200" + : "border-pluto-200 bg-white text-pluto-600 dark:border-pluto-700 dark:bg-pluto-900/40 dark:text-pluto-400 group-hover:border-pluto-400 group-hover:bg-pluto-50 dark:group-hover:border-pluto-500 dark:group-hover:bg-pluto-800/50"; return ( - {/* Step content */} - + {/* Step text content */} +

    {step.title} {step.required && ( * )}

    +

    {step.description}

    - {/* Status badge — animated scale-in — #809 */} -
    - - {step.completed - ? t("onboarding.completed") || "Completed" - : isCurrentStep - ? t("onboarding.inProgress") || "In Progress" - : t("onboarding.pending") || "Pending"} - -
    - - - {/* Connector line (vertical orientation only) */} + +
    + + {/* Vertical connector line */} {orientation === "vertical" && index < sortedSteps.length - 1 && (

    ); -}; +}); export default OnboardingProgressTracker; diff --git a/frontend/src/hooks/useOnboardingI18n.ts b/frontend/src/hooks/useOnboardingI18n.ts new file mode 100644 index 00000000..65b6e658 --- /dev/null +++ b/frontend/src/hooks/useOnboardingI18n.ts @@ -0,0 +1,149 @@ +/** + * useOnboardingI18n + * + * Centralises every translated string used by the Onboarding Progress Tracker. + * Consuming components call this hook once and receive a stable object of + * typed helpers — no duplicated useTranslations("onboarding") calls scattered + * across files. + */ + +"use client"; + +import { useCallback } from "react"; +import { useTranslations } from "next-intl"; + +// ── Step data shape (minimal — component owns the full type) ────────────────── + +interface StepMeta { + order: number; + title: string; + description: string; + completed: boolean; + required: boolean; +} + +// ── Return type ─────────────────────────────────────────────────────────────── + +export interface OnboardingI18n { + // Static strings + title: string; + subtitle: string; + progressBar: string; + stepsList: string; + progressTracker: string; + allCompleted: string; + successTitle: string; + successMessage: string; + completed: string; + inProgress: string; + pending: string; + required: string; + optional: string; + updating: string; + stepChangeFailed: string; + + // Parameterised helpers + /** "X of Y steps completed" */ + stepsCompletedLabel: (completed: number, total: number) => string; + /** "N% complete" */ + percentCompleteLabel: (percent: number) => string; + /** Progress announcement for aria-live: "Progress: N% complete" */ + progressAnnouncement: (percent: number) => string; + /** + * Full step button aria-label. + * Appends ". Completed" and/or ". Required" as appropriate. + */ + stepAriaLabel: ( + number: number, + title: string, + completed: boolean, + required: boolean, + ) => string; + /** Step click announcement for the aria-live region. */ + stepAnnouncement: (step: StepMeta, total: number, statusLabel: string) => string; + /** Status label for a given step state. */ + statusLabel: (completed: boolean, isCurrent: boolean) => string; +} + +// ── Hook ────────────────────────────────────────────────────────────────────── + +export function useOnboardingI18n(): OnboardingI18n { + const t = useTranslations("onboarding"); + + const stepsCompletedLabel = useCallback( + (completed: number, total: number) => + t("stepsCompleted", { completed, total }), + [t], + ); + + const percentCompleteLabel = useCallback( + (percent: number) => t("percentComplete", { percent }), + [t], + ); + + const progressAnnouncement = useCallback( + (percent: number) => t("progressAnnouncement", { percent }), + [t], + ); + + const stepAriaLabel = useCallback( + ( + number: number, + title: string, + completed: boolean, + required: boolean, + ): string => { + if (completed && required) + return t("stepLabelCompletedRequired", { number, title }); + if (completed) return t("stepLabelCompleted", { number, title }); + if (required) return t("stepLabelRequired", { number, title }); + return t("stepLabel", { number, title }); + }, + [t], + ); + + const stepAnnouncement = useCallback( + (step: StepMeta, total: number, statusLabel: string): string => + t("stepAnnouncement", { + number: step.order, + total, + title: step.title, + description: step.description, + status: statusLabel, + }), + [t], + ); + + const statusLabel = useCallback( + (completed: boolean, isCurrent: boolean): string => { + if (completed) return t("completed"); + if (isCurrent) return t("inProgress"); + return t("pending"); + }, + [t], + ); + + return { + title: t("title"), + subtitle: t("subtitle"), + progressBar: t("progressBar"), + stepsList: t("stepsList"), + progressTracker: t("progressTracker"), + allCompleted: t("allCompleted"), + successTitle: t("successTitle"), + successMessage: t("successMessage"), + completed: t("completed"), + inProgress: t("inProgress"), + pending: t("pending"), + required: t("required"), + optional: t("optional"), + updating: t("updating"), + stepChangeFailed: t("stepChangeFailed"), + stepsCompletedLabel, + percentCompleteLabel, + progressAnnouncement, + stepAriaLabel, + stepAnnouncement, + statusLabel, + }; +} diff --git a/frontend/src/hooks/useOnboardingProgress.ts b/frontend/src/hooks/useOnboardingProgress.ts new file mode 100644 index 00000000..7a9b1f46 --- /dev/null +++ b/frontend/src/hooks/useOnboardingProgress.ts @@ -0,0 +1,195 @@ +/** + * useOnboardingProgress + * + * Encapsulates all stateful logic for the Onboarding Progress Tracker so the + * component stays a pure presentation layer. + * + * Responsibilities: + * - Owns the useReducer instance and exposes read-only derived values. + * - Syncs external prop changes (steps array) into the reducer via SYNC_STEPS. + * - Syncs external currentStep prop changes via SET_CURRENT_STEP. + * - Handles optimistic step navigation with async callback + rollback. + * - Produces translated screen-reader announcement strings via useOnboardingI18n. + * - Fires onComplete when all required steps are done. + */ + +"use client"; + +import { + useCallback, + useEffect, + useId, + useMemo, + useReducer, + useRef, +} from "react"; +import { + onboardingReducer, + createInitialOnboardingState, + selectEffectiveStep, + selectProgressPercent, + type OnboardingState, +} from "@/components/onboarding-reducer"; +import { useOnboardingI18n } from "@/hooks/useOnboardingI18n"; + +// ── Shared step type (re-exported for consumers) ────────────────────────────── + +export interface OnboardingStep { + id: string; + title: string; + description: string; + completed: boolean; + required: boolean; + order: number; +} + +// ── Options / return types ──────────────────────────────────────────────────── + +export interface UseOnboardingProgressOptions { + steps: OnboardingStep[]; + currentStep?: string; + onStepChange?: (stepId: string) => void | Promise; + onComplete?: () => void; +} + +export interface UseOnboardingProgressReturn { + sortedSteps: OnboardingStep[]; + effectiveCurrentStep: string | undefined; + state: OnboardingState; + progressPercent: number; + completedCount: number; + isComplete: boolean; + progressSummaryId: string; + handleStepClick: (stepId: string) => Promise; +} + +// ── Hook ────────────────────────────────────────────────────────────────────── + +export function useOnboardingProgress({ + steps, + currentStep: currentStepProp, + onStepChange, + onComplete, +}: UseOnboardingProgressOptions): UseOnboardingProgressReturn { + const i18n = useOnboardingI18n(); + const progressSummaryId = useId(); + + // ── Derived step data ──────────────────────────────────────────────────── + + const sortedSteps = useMemo( + () => [...steps].sort((a, b) => a.order - b.order), + [steps], + ); + + const completedCount = useMemo( + () => sortedSteps.filter((s) => s.completed).length, + [sortedSteps], + ); + + const isComplete = useMemo(() => { + const required = sortedSteps.filter((s) => s.required); + return required.length > 0 && required.every((s) => s.completed); + }, [sortedSteps]); + + // ── Reducer ────────────────────────────────────────────────────────────── + + const [state, dispatch] = useReducer( + onboardingReducer, + createInitialOnboardingState( + currentStepProp ?? sortedSteps[0]?.id, + sortedSteps.length, + completedCount, + ), + ); + + // Sync step counts when the steps prop changes + useEffect(() => { + dispatch({ + type: "SYNC_STEPS", + payload: { total: sortedSteps.length, completed: completedCount }, + }); + }, [sortedSteps.length, completedCount]); + + // Sync external currentStep prop + const prevCurrentStepPropRef = useRef(currentStepProp); + useEffect(() => { + if ( + currentStepProp !== undefined && + currentStepProp !== prevCurrentStepPropRef.current + ) { + dispatch({ type: "SET_CURRENT_STEP", payload: currentStepProp }); + prevCurrentStepPropRef.current = currentStepProp; + } + }, [currentStepProp]); + + // ── Derived values ──────────────────────────────────────────────────────── + + const effectiveCurrentStep = selectEffectiveStep(state); + const progressPercent = selectProgressPercent(state); + + // ── Completion side-effect ──────────────────────────────────────────────── + + const onCompleteRef = useRef(onComplete); + onCompleteRef.current = onComplete; + + useEffect(() => { + if (isComplete && sortedSteps.length > 0) { + dispatch({ type: "SET_ANNOUNCEMENT", payload: i18n.successTitle }); + onCompleteRef.current?.(); + } + }, [isComplete, sortedSteps.length, i18n.successTitle]); + + // ── Progress announcements ──────────────────────────────────────────────── + + useEffect(() => { + dispatch({ + type: "SET_ANNOUNCEMENT", + payload: i18n.progressAnnouncement(progressPercent), + }); + }, [progressPercent, i18n]); + + // ── Step click handler ──────────────────────────────────────────────────── + + const handleStepClick = useCallback( + async (stepId: string) => { + if (state.isPending) return; + + const step = sortedSteps.find((s) => s.id === stepId); + if (!step) return; + + dispatch({ type: "OPTIMISTIC_STEP", payload: stepId }); + + const status = i18n.statusLabel( + step.completed, + effectiveCurrentStep === stepId, + ); + dispatch({ + type: "SET_ANNOUNCEMENT", + payload: i18n.stepAnnouncement(step, sortedSteps.length, status), + }); + + try { + await onStepChange?.(stepId); + dispatch({ type: "CONFIRM_STEP", payload: stepId }); + } catch { + dispatch({ type: "ROLLBACK_STEP" }); + dispatch({ + type: "SET_ANNOUNCEMENT", + payload: i18n.stepChangeFailed, + }); + } + }, + [sortedSteps, effectiveCurrentStep, onStepChange, state.isPending, i18n], + ); + + return { + sortedSteps, + effectiveCurrentStep, + state, + progressPercent, + completedCount, + isComplete, + progressSummaryId, + handleStepClick, + }; +}