From 14a2e3e28ad665de1f6128824be226fbd3b0f927 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 19:39:34 -0400 Subject: [PATCH 01/61] Complete i18n coverage for named UI pages --- frontend/src/locales/en/translation.json | 229 +++++++++++++++++++++ frontend/src/locales/es/translation.json | 229 +++++++++++++++++++++ frontend/src/pages/CashFlowForecast.tsx | 58 +++--- frontend/src/pages/CustomReportBuilder.tsx | 56 +++-- frontend/src/pages/EmployeePortal.tsx | 134 ++++++------ frontend/src/pages/Forecasting.tsx | 61 +++--- frontend/src/pages/HelpCenter.tsx | 73 ++----- frontend/src/pages/TaxComplianceWizard.tsx | 84 ++++---- 8 files changed, 680 insertions(+), 244 deletions(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 1188a937..e7bc6219 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -204,5 +204,234 @@ "loginDescription": "Enter the 6-digit code from your authenticator app to finish signing in.", "loginButton": "Verify and continue", "loginRecoveryHint": "Lost your device? Enter one of your recovery codes instead." + }, + "helpCenter": { + "faqs": "FAQs", + "addEmployeeQuestion": "How do I add an employee?", + "addEmployeeAnswer": "Open the Employees page and use Add Employee to enter their details and wallet address.", + "resetPasswordQuestion": "How do I reset my password?", + "resetPasswordAnswer": "Use the account recovery flow from the sign-in screen or contact your organization administrator.", + "stellarConcepts": "Stellar Concepts", + "trustlineQuestion": "What is a trustline?", + "trustlineAnswer": "A trustline lets a Stellar account hold a specific issued asset.", + "anchorQuestion": "What is a Stellar anchor?", + "anchorAnswer": "An anchor connects Stellar assets with traditional financial rails and local currencies.", + "troubleshooting": "Troubleshooting", + "payrollFailedQuestion": "Why did my payroll transaction fail?", + "payrollFailedAnswer": "Check the transaction status, account funding, trustlines, and network details before retrying.", + "employeeMissingQuestion": "Why is an employee missing?", + "employeeMissingAnswer": "Confirm the employee is active and belongs to the organization you are viewing.", + "titlePrefix": "Help", + "titleHighlight": "Center", + "subtitle": "Guides, answers, and troubleshooting", + "searchPlaceholder": "Search help topics…", + "noResults": "No matching help topics found." + }, + "forecasting": { + "errors": { + "loadFailed": "Failed to load forecast data.", + "updateFailed": "Failed to update liquidity settings.", + "refreshFailed": "Failed to refresh forecast." + }, + "alertShortfall": "{{severity}} liquidity alert: {{amount}} {{asset}} shortfall", + "settingsUpdated": "Liquidity settings updated.", + "title": "Liquidity Forecast", + "subtitle": "Project payroll liquidity and configure funding alerts.", + "months": "Months forward", + "refresh": "Refresh", + "liquidityStatus": "Liquidity Status", + "liquidityAvailable": "Available liquidity", + "configureLiquidity": "Configure Liquidity", + "projectedMonthlyPayroll": "Projected Monthly Payroll", + "liquiditySettings": "Liquidity Settings", + "distributionAccount": "Distribution Account", + "assetIssuer": "Asset Issuer", + "assetCode": "Asset Code", + "benefitsRate": "Benefits Rate (%)", + "yellowBuffer": "Warning Buffer (%)", + "alertEmails": "Alert Emails", + "save": "Save Settings", + "saved": "Saved" + }, + "customReportBuilder": { + "exporting": "Exporting {{count}} records with columns: {{columns}}", + "title": "Custom Report Builder", + "subtitle": "Choose a date range and columns for your report.", + "dateRange": "Date Range", + "startDate": "Start Date", + "endDate": "End Date", + "columnsTitle": "Columns", + "exportData": "Export Data", + "livePreview": "Live Preview", + "recordsFound": "{{count}} records found", + "noData": "No data in this date range.", + "selectColumn": "Select at least one column", + "columns": { + "workerId": "Worker ID", + "amount": "Amount", + "asset": "Asset", + "setupDate": "Setup Date", + "payoutDate": "Payout Date", + "status": "Status" + } + }, + "employeePortal": { + "titlePrefix": "My", + "titleHighlight": "Portal", + "subtitle": "View your salary payments, balances, and transaction history", + "common": { + "type": "Type", + "amount": "Amount", + "refresh": "Refresh", + "tryAgain": "Please try again." + }, + "errors": { + "loadPendingClaims": "Failed to load pending claims", + "refreshPendingClaims": "Failed to refresh pending claims" + }, + "deductions": { + "title": "Deductions Breakdown", + "grossPay": "Gross Pay", + "totalDeductions": "Total Deductions", + "netPay": "Net Pay", + "deduction": "Deduction", + "destination": "Destination", + "wallet": "Wallet", + "emptyTitle": "No deductions configured", + "emptyBody": "Your net pay equals your gross pay for now." + }, + "balance": { + "total": "Total Balance", + "updated": "Updated {{time}}", + "localCurrency": "Local Currency:" + }, + "stats": { + "totalReceived": "Total Received", + "cashOut": "Cash Out", + "withdrawLocal": "Withdraw to Local Currency", + "transactions": "Transactions", + "lastPayment": "Last Payment" + }, + "status": { + "completed": "Completed", + "pending": "Pending", + "failed": "Failed" + }, + "type": { + "salary": "Salary", + "bonus": "Bonus", + "reimbursement": "Reimbursement" + }, + "trustline": { + "establishedTitle": "{{assetCode}} Trustline Established", + "establishedBody": "You can now receive payroll in this asset.", + "failedTitle": "Failed to establish trustline", + "setupRequired": "Setup Required", + "explanation": "To receive payments in certain assets, you must first establish a trustline with the issuer. This is a standard Stellar security feature.", + "assetTrustline": "Stellar Asset Trustline", + "establish": "Establish Trustline" + }, + "claim": { + "successTitle": "Claim successful!", + "successBody": "The funds have been transferred to your wallet.", + "failedTitle": "Claim failed", + "failedBody": "A contract error occurred. Please review the details below.", + "pendingTitle": "Pending Claims", + "help": "If you have a pending claim, add the trustline in your wallet and then claim the balance.", + "created": "Created {{date}}", + "balanceId": "Balance ID: {{id}}", + "claimFunds": "Claim Funds" + }, + "history": { + "title": "Payment History", + "searchPlaceholder": "Search tx hash, memo…", + "allStatus": "All Status", + "allTypes": "All Types", + "date": "Date", + "description": "Description", + "status": "Status", + "hash": "Hash", + "verify": "Verify", + "emptyTitle": "No transactions found", + "emptyBody": "Try adjusting your filters or check back later.", + "viewOnExplorer": "View on Stellar Expert", + "explorer": "Explorer" + } + }, + "cashFlowForecast": { + "errors": { + "missingInputs": "Please provide distribution account and asset issuer", + "loadFailed": "Failed to load cash flow forecast" + }, + "updated": "Cash flow forecast updated", + "title": "Cash Flow Forecast", + "subtitle": "Analyze historical payroll data and project future cash flow requirements", + "distributionAccount": "Distribution Account", + "assetIssuer": "Asset Issuer", + "daysPlaceholder": "Days (90)", + "loading": "Loading...", + "refresh": "Refresh", + "currentBalance": "Current Balance", + "projectedOutflow": "Projected Outflow", + "scheduledPayments": "{{count}} scheduled payments", + "projectedBalance": "Projected Balance", + "afterProjections": "After all projections", + "trend": "Trend", + "vsHistorical": "{{percent}}% vs historical", + "budgetAlerts": "Budget Alerts ({{count}})", + "shortfall": "Shortfall: {{amount}} {{asset}}", + "cashFlowProjection": "Cash Flow Projection", + "historicalVsProjected": "Historical vs Projected", + "historical": "Historical", + "projected": "Projected", + "historicalAverages": "Historical Averages", + "weeklyAverage": "Weekly Average", + "biweeklyAverage": "Biweekly Average", + "monthlyAverage": "Monthly Average", + "emptyPrompt": "Enter distribution account and asset issuer to load forecast" + }, + "taxCompliance": { + "title": "Tax Compliance Wizard", + "subtitle": "Configure tax rules and generate compliance reports for your organization", + "steps": { + "selectCountry": "Step 1: Select Country", + "configuration": "Step 2: Tax Configuration", + "preview": "Step 3: Preview Calculator", + "report": "Step 4: Generate Report", + "export": "Step 5: Export Summary" + }, + "searchCountries": "Search countries...", + "noCountries": "No countries found matching \"{{query}}\"", + "configuration": "Tax compliance configuration", + "activeRules": "Active Tax Rules ({{count}})", + "configureForCountry": "Configure tax identifiers and rates for {{country}} compliance", + "defaultGrossPay": "Default Gross Pay (per employee)", + "numberOfEmployees": "Number of Employees", + "previewSummary": "Tax Preview Summary", + "grossPay": "Gross Pay", + "estimatedTax": "Est. Tax", + "netPay": "Net Pay", + "defaultRateNote": "* Based on default 25% tax rate. Actual calculations will use configured rates.", + "reportStartDate": "Report Start Date", + "reportEndDate": "Report End Date", + "reportPreview": "Report Preview", + "employee": "Employee", + "taxAmount": "Tax Amount", + "moreEmployees": "And {{count}} more employees...", + "reportGenerated": "Report Generated Successfully!", + "reportReady": "Your tax compliance report for {{country}} is ready for export", + "totalEmployees": "Total Employees", + "totalGross": "Total Gross", + "taxCollected": "Tax Collected", + "totalNet": "Total Net", + "downloadCsv": "Download CSV Report", + "exportHint": "Export as CSV for local government filing", + "cancel": "Cancel", + "back": "Back", + "validateContinue": "Validate & Continue", + "calculating": "Calculating...", + "calculatePreview": "Calculate Preview", + "generateReport": "Generate Report", + "continue": "Continue" } } diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 0966f7f9..34048c9b 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -204,5 +204,234 @@ "loginDescription": "Introduce el codigo de 6 digitos de tu app de autenticacion para terminar de iniciar sesion.", "loginButton": "Verificar y continuar", "loginRecoveryHint": "Perdiste tu dispositivo? Introduce uno de tus codigos de recuperacion." + }, + "helpCenter": { + "faqs": "Preguntas frecuentes", + "addEmployeeQuestion": "¿Cómo añado un empleado?", + "addEmployeeAnswer": "Abre la página Empleados y usa Añadir empleado para introducir sus datos y su dirección de billetera.", + "resetPasswordQuestion": "¿Cómo restablezco mi contraseña?", + "resetPasswordAnswer": "Usa la recuperación de cuenta desde la pantalla de inicio de sesión o contacta al administrador de tu organización.", + "stellarConcepts": "Conceptos de Stellar", + "trustlineQuestion": "¿Qué es una línea de confianza?", + "trustlineAnswer": "Una línea de confianza permite que una cuenta Stellar mantenga un activo emitido específico.", + "anchorQuestion": "¿Qué es un ancla de Stellar?", + "anchorAnswer": "Un ancla conecta los activos de Stellar con las redes financieras tradicionales y las monedas locales.", + "troubleshooting": "Solución de problemas", + "payrollFailedQuestion": "¿Por qué falló mi transacción de nómina?", + "payrollFailedAnswer": "Comprueba el estado de la transacción, los fondos de la cuenta, las líneas de confianza y los detalles de red antes de reintentar.", + "employeeMissingQuestion": "¿Por qué falta un empleado?", + "employeeMissingAnswer": "Confirma que el empleado esté activo y pertenezca a la organización que estás viendo.", + "titlePrefix": "Centro de", + "titleHighlight": "Ayuda", + "subtitle": "Guías, respuestas y solución de problemas", + "searchPlaceholder": "Buscar temas de ayuda…", + "noResults": "No se encontraron temas de ayuda coincidentes." + }, + "forecasting": { + "errors": { + "loadFailed": "No se pudieron cargar los datos del pronóstico.", + "updateFailed": "No se pudo actualizar la configuración de liquidez.", + "refreshFailed": "No se pudo actualizar el pronóstico." + }, + "alertShortfall": "Alerta de liquidez {{severity}}: déficit de {{amount}} {{asset}}", + "settingsUpdated": "Configuración de liquidez actualizada.", + "title": "Pronóstico de liquidez", + "subtitle": "Proyecta la liquidez de nómina y configura alertas de financiación.", + "months": "Meses a futuro", + "refresh": "Actualizar", + "liquidityStatus": "Estado de liquidez", + "liquidityAvailable": "Liquidez disponible", + "configureLiquidity": "Configurar liquidez", + "projectedMonthlyPayroll": "Nómina mensual proyectada", + "liquiditySettings": "Configuración de liquidez", + "distributionAccount": "Cuenta de distribución", + "assetIssuer": "Emisor del activo", + "assetCode": "Código del activo", + "benefitsRate": "Tasa de beneficios (%)", + "yellowBuffer": "Margen de aviso (%)", + "alertEmails": "Correos de alerta", + "save": "Guardar configuración", + "saved": "Guardado" + }, + "customReportBuilder": { + "exporting": "Exportando {{count}} registros con columnas: {{columns}}", + "title": "Generador de informes personalizados", + "subtitle": "Elige un intervalo de fechas y las columnas del informe.", + "dateRange": "Intervalo de fechas", + "startDate": "Fecha de inicio", + "endDate": "Fecha de fin", + "columnsTitle": "Columnas", + "exportData": "Exportar datos", + "livePreview": "Vista previa", + "recordsFound": "{{count}} registros encontrados", + "noData": "No hay datos en este intervalo de fechas.", + "selectColumn": "Selecciona al menos una columna", + "columns": { + "workerId": "ID del trabajador", + "amount": "Importe", + "asset": "Activo", + "setupDate": "Fecha de configuración", + "payoutDate": "Fecha de pago", + "status": "Estado" + } + }, + "employeePortal": { + "titlePrefix": "Mi", + "titleHighlight": "Portal", + "subtitle": "Consulta tus pagos de salario, saldos e historial de transacciones", + "common": { + "type": "Tipo", + "amount": "Importe", + "refresh": "Actualizar", + "tryAgain": "Inténtalo de nuevo." + }, + "errors": { + "loadPendingClaims": "No se pudieron cargar los cobros pendientes", + "refreshPendingClaims": "No se pudieron actualizar los cobros pendientes" + }, + "deductions": { + "title": "Desglose de deducciones", + "grossPay": "Pago bruto", + "totalDeductions": "Deducciones totales", + "netPay": "Pago neto", + "deduction": "Deducción", + "destination": "Destino", + "wallet": "Billetera", + "emptyTitle": "No hay deducciones configuradas", + "emptyBody": "Por ahora, tu pago neto es igual a tu pago bruto." + }, + "balance": { + "total": "Saldo total", + "updated": "Actualizado {{time}}", + "localCurrency": "Moneda local:" + }, + "stats": { + "totalReceived": "Total recibido", + "cashOut": "Retirar", + "withdrawLocal": "Retirar a moneda local", + "transactions": "Transacciones", + "lastPayment": "Último pago" + }, + "status": { + "completed": "Completado", + "pending": "Pendiente", + "failed": "Fallido" + }, + "type": { + "salary": "Salario", + "bonus": "Bono", + "reimbursement": "Reembolso" + }, + "trustline": { + "establishedTitle": "Línea de confianza {{assetCode}} establecida", + "establishedBody": "Ya puedes recibir la nómina en este activo.", + "failedTitle": "No se pudo establecer la línea de confianza", + "setupRequired": "Configuración necesaria", + "explanation": "Para recibir pagos en determinados activos, primero debes establecer una línea de confianza con el emisor. Es una función de seguridad estándar de Stellar.", + "assetTrustline": "Línea de confianza de activo Stellar", + "establish": "Establecer línea de confianza" + }, + "claim": { + "successTitle": "¡Cobro realizado!", + "successBody": "Los fondos se han transferido a tu billetera.", + "failedTitle": "Error al cobrar", + "failedBody": "Se produjo un error de contrato. Revisa los detalles a continuación.", + "pendingTitle": "Cobros pendientes", + "help": "Si tienes un cobro pendiente, añade la línea de confianza en tu billetera y después reclama el saldo.", + "created": "Creado {{date}}", + "balanceId": "ID de saldo: {{id}}", + "claimFunds": "Reclamar fondos" + }, + "history": { + "title": "Historial de pagos", + "searchPlaceholder": "Buscar hash de transacción, memo…", + "allStatus": "Todos los estados", + "allTypes": "Todos los tipos", + "date": "Fecha", + "description": "Descripción", + "status": "Estado", + "hash": "Hash", + "verify": "Verificar", + "emptyTitle": "No se encontraron transacciones", + "emptyBody": "Ajusta los filtros o vuelve a intentarlo más tarde.", + "viewOnExplorer": "Ver en Stellar Expert", + "explorer": "Explorador" + } + }, + "cashFlowForecast": { + "errors": { + "missingInputs": "Indica la cuenta de distribución y el emisor del activo", + "loadFailed": "No se pudo cargar el pronóstico de flujo de caja" + }, + "updated": "Pronóstico de flujo de caja actualizado", + "title": "Pronóstico de flujo de caja", + "subtitle": "Analiza los datos históricos de nómina y proyecta las necesidades futuras de flujo de caja", + "distributionAccount": "Cuenta de distribución", + "assetIssuer": "Emisor del activo", + "daysPlaceholder": "Días (90)", + "loading": "Cargando...", + "refresh": "Actualizar", + "currentBalance": "Saldo actual", + "projectedOutflow": "Salida proyectada", + "scheduledPayments": "{{count}} pagos programados", + "projectedBalance": "Saldo proyectado", + "afterProjections": "Después de todas las proyecciones", + "trend": "Tendencia", + "vsHistorical": "{{percent}}% frente al histórico", + "budgetAlerts": "Alertas de presupuesto ({{count}})", + "shortfall": "Déficit: {{amount}} {{asset}}", + "cashFlowProjection": "Proyección de flujo de caja", + "historicalVsProjected": "Histórico frente a proyectado", + "historical": "Histórico", + "projected": "Proyectado", + "historicalAverages": "Promedios históricos", + "weeklyAverage": "Promedio semanal", + "biweeklyAverage": "Promedio quincenal", + "monthlyAverage": "Promedio mensual", + "emptyPrompt": "Introduce la cuenta de distribución y el emisor del activo para cargar el pronóstico" + }, + "taxCompliance": { + "title": "Asistente de cumplimiento fiscal", + "subtitle": "Configura reglas fiscales y genera informes de cumplimiento para tu organización", + "steps": { + "selectCountry": "Paso 1: Seleccionar país", + "configuration": "Paso 2: Configuración fiscal", + "preview": "Paso 3: Calculadora de vista previa", + "report": "Paso 4: Generar informe", + "export": "Paso 5: Exportar resumen" + }, + "searchCountries": "Buscar países...", + "noCountries": "No se encontraron países que coincidan con \"{{query}}\"", + "configuration": "Configuración de cumplimiento fiscal", + "activeRules": "Reglas fiscales activas ({{count}})", + "configureForCountry": "Configura identificadores y tasas fiscales para el cumplimiento en {{country}}", + "defaultGrossPay": "Pago bruto predeterminado (por empleado)", + "numberOfEmployees": "Número de empleados", + "previewSummary": "Resumen de vista previa fiscal", + "grossPay": "Pago bruto", + "estimatedTax": "Impuesto estimado", + "netPay": "Pago neto", + "defaultRateNote": "* Basado en una tasa fiscal predeterminada del 25 %. Los cálculos reales usarán las tasas configuradas.", + "reportStartDate": "Fecha inicial del informe", + "reportEndDate": "Fecha final del informe", + "reportPreview": "Vista previa del informe", + "employee": "Empleado", + "taxAmount": "Importe del impuesto", + "moreEmployees": "Y {{count}} empleados más...", + "reportGenerated": "¡Informe generado correctamente!", + "reportReady": "Tu informe de cumplimiento fiscal para {{country}} está listo para exportar", + "totalEmployees": "Total de empleados", + "totalGross": "Bruto total", + "taxCollected": "Impuestos recaudados", + "totalNet": "Neto total", + "downloadCsv": "Descargar informe CSV", + "exportHint": "Exportar como CSV para la presentación ante el gobierno local", + "cancel": "Cancelar", + "back": "Atrás", + "validateContinue": "Validar y continuar", + "calculating": "Calculando...", + "calculatePreview": "Calcular vista previa", + "generateReport": "Generar informe", + "continue": "Continuar" } } diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index b3796322..dcfd863c 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { BarChart, Bar, @@ -38,6 +39,7 @@ interface ForecastParams { } export default function CashFlowForecast() { + const { t } = useTranslation(); const { notifyError, notifySuccess } = useNotification(); const [forecast, setForecast] = useState(null); const [historical, setHistorical] = useState([]); @@ -51,7 +53,7 @@ export default function CashFlowForecast() { const loadForecast = useCallback(async () => { if (!params.distributionAccount || !params.assetIssuer) { - notifyError('Please provide distribution account and asset issuer'); + notifyError(t('cashFlowForecast.errors.missingInputs')); return; } @@ -66,13 +68,13 @@ export default function CashFlowForecast() { setForecast(forecastData); setHistorical(historicalData.historical); setAlerts(alertsData.alerts); - notifySuccess('Cash flow forecast updated'); + notifySuccess(t('cashFlowForecast.updated')); } catch (error) { - notifyError(error instanceof Error ? error.message : 'Failed to load cash flow forecast'); + notifyError(error instanceof Error ? error.message : t('cashFlowForecast.errors.loadFailed')); } finally { setIsLoading(false); } - }, [params, notifyError, notifySuccess]); + }, [params, notifyError, notifySuccess, t]); useEffect(() => { void loadForecast(); @@ -145,29 +147,29 @@ export default function CashFlowForecast() {
-

Cash Flow Forecast

+

{t('cashFlowForecast.title')}

- Analyze historical payroll data and project future cash flow requirements + {t('cashFlowForecast.subtitle')}

setParams({ ...params, distributionAccount: e.target.value })} className="px-4 py-2 bg-(--surface) border border-(--border) rounded-lg text-(--text) text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[44px]" /> setParams({ ...params, assetIssuer: e.target.value })} className="px-4 py-2 bg-(--surface) border border-(--border) rounded-lg text-(--text) text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[44px]" /> setParams({ ...params, forecastDays: parseInt(e.target.value, 10) || 90 }) @@ -182,7 +184,7 @@ export default function CashFlowForecast() { className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-(--text) rounded-lg font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 min-h-[44px] touch-manipulation" > - {isLoading ? 'Loading...' : 'Refresh'} + {isLoading ? t('cashFlowForecast.loading') : t('cashFlowForecast.refresh')}
@@ -202,7 +204,7 @@ export default function CashFlowForecast() {
- Current Balance + {t('cashFlowForecast.currentBalance')}

@@ -216,7 +218,7 @@ export default function CashFlowForecast() {

- Projected Outflow + {t('cashFlowForecast.projectedOutflow')}

@@ -226,13 +228,13 @@ export default function CashFlowForecast() { })}

- {forecast.projections.length} scheduled payments + {t('cashFlowForecast.scheduledPayments', { count: forecast.projections.length })}

- Projected Balance + {t('cashFlowForecast.projectedBalance')}

-

After all projections

+

{t('cashFlowForecast.afterProjections')}

- Trend + {t('cashFlowForecast.trend')} {getTrendIcon()}

@@ -258,7 +260,7 @@ export default function CashFlowForecast() {

{forecast.trendAnalysis.changePercent > 0 ? '+' : ''} - {forecast.trendAnalysis.changePercent.toFixed(1)}% vs historical + {t('cashFlowForecast.vsHistorical', { percent: forecast.trendAnalysis.changePercent.toFixed(1) })}

@@ -268,7 +270,7 @@ export default function CashFlowForecast() {

- Budget Alerts ({alerts.length}) + {t('cashFlowForecast.budgetAlerts', { count: alerts.length })}

{alerts.map((alert) => ( @@ -304,7 +306,7 @@ export default function CashFlowForecast() {

{alert.message}

{alert.shortfall && (

- Shortfall: {alert.shortfall.toFixed(2)} ORGUSD + {t('cashFlowForecast.shortfall', { amount: alert.shortfall.toFixed(2), asset: 'ORGUSD' })}

)}
@@ -319,7 +321,7 @@ export default function CashFlowForecast() {
{/* Cash Flow Projection Chart */}
-

Cash Flow Projection

+

{t('cashFlowForecast.cashFlowProjection')}

@@ -351,7 +353,7 @@ export default function CashFlowForecast() { {/* Historical vs Projected Chart */}
-

Historical vs Projected

+

{t('cashFlowForecast.historicalVsProjected')}

@@ -365,8 +367,8 @@ export default function CashFlowForecast() { }} /> - - + +
@@ -374,10 +376,10 @@ export default function CashFlowForecast() { {/* Historical Averages */}
-

Historical Averages

+

{t('cashFlowForecast.historicalAverages')}

-

Weekly Average

+

{t('cashFlowForecast.weeklyAverage')}

{forecast.historicalAverage.weekly.toLocaleString('en-US', { minimumFractionDigits: 2, @@ -386,7 +388,7 @@ export default function CashFlowForecast() {

-

Biweekly Average

+

{t('cashFlowForecast.biweeklyAverage')}

{forecast.historicalAverage.biweekly.toLocaleString('en-US', { minimumFractionDigits: 2, @@ -395,7 +397,7 @@ export default function CashFlowForecast() {

-

Monthly Average

+

{t('cashFlowForecast.monthlyAverage')}

{forecast.historicalAverage.monthly.toLocaleString('en-US', { minimumFractionDigits: 2, @@ -409,7 +411,7 @@ export default function CashFlowForecast() { ) : (

- Enter distribution account and asset issuer to load forecast + {t('cashFlowForecast.emptyPrompt')}

)} diff --git a/frontend/src/pages/CustomReportBuilder.tsx b/frontend/src/pages/CustomReportBuilder.tsx index 49cfb19b..3e52dc67 100644 --- a/frontend/src/pages/CustomReportBuilder.tsx +++ b/frontend/src/pages/CustomReportBuilder.tsx @@ -1,19 +1,20 @@ import { useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Card, Icon } from '@stellar/design-system'; // Define all possible columns for the report type ReportColumn = { id: string; - label: string; + labelKey: string; }; const ALL_COLUMNS: ReportColumn[] = [ - { id: 'worker_id', label: 'Worker ID' }, - { id: 'amount', label: 'Amount' }, - { id: 'asset', label: 'Asset' }, - { id: 'setup_date', label: 'Stream Setup Date' }, - { id: 'payout_date', label: 'Expected Payout Date' }, - { id: 'status', label: 'Status' }, + { id: 'worker_id', labelKey: 'customReportBuilder.columns.workerId' }, + { id: 'amount', labelKey: 'customReportBuilder.columns.amount' }, + { id: 'asset', labelKey: 'customReportBuilder.columns.asset' }, + { id: 'setup_date', labelKey: 'customReportBuilder.columns.setupDate' }, + { id: 'payout_date', labelKey: 'customReportBuilder.columns.payoutDate' }, + { id: 'status', labelKey: 'customReportBuilder.columns.status' }, ]; // Mock data (matching the columns) @@ -61,6 +62,7 @@ const MOCK_DATA = [ ]; const CustomReportBuilder = () => { + const { t } = useTranslation(); const [selectedColumns, setSelectedColumns] = useState(ALL_COLUMNS.map((c) => c.id)); const [startDate, setStartDate] = useState('2026-02-01'); const [endDate, setEndDate] = useState('2026-02-28'); @@ -86,17 +88,15 @@ const CustomReportBuilder = () => { const handleExport = () => { // Simulate export logic (e.g. converting filteredData to CSV and triggering download) alert( - `Exporting ${filteredData.length} records with columns: ${activeColumns.map((c) => c.label).join(', ')}` + t('customReportBuilder.exporting', { count: filteredData.length, columns: activeColumns.map((c) => t(c.labelKey)).join(', ') }) ); }; return (
-

Custom Report Builder

-

- Select columns and date ranges to preview and export custom payroll data. -

+

{t('customReportBuilder.title')}

+

{t('customReportBuilder.subtitle')}

@@ -104,10 +104,10 @@ const CustomReportBuilder = () => {
-

Date Range

+

{t('customReportBuilder.dateRange')}

- + { />
- + {
-

Columns

+

{t('customReportBuilder.columnsTitle')}

{ALL_COLUMNS.map((col) => ( ))}
@@ -153,9 +153,7 @@ const CustomReportBuilder = () => { size="md" className="w-full flex justify-center mt-auto" > - - Export Data - + {t('customReportBuilder.exportData')}
{/* Live Preview Pane */} @@ -163,9 +161,9 @@ const CustomReportBuilder = () => {
-

Live Preview

+

{t('customReportBuilder.livePreview')}

- {filteredData.length} records found + {t('customReportBuilder.recordsFound', { count: filteredData.length })}
@@ -179,7 +177,7 @@ const CustomReportBuilder = () => { key={col.id} className="px-6 py-3 text-left text-xs font-medium text-(--muted) uppercase tracking-wider" > - {col.label} + {t(col.labelKey)} ))} @@ -192,7 +190,9 @@ const CustomReportBuilder = () => { key={col.id} className="px-6 py-4 whitespace-nowrap text-sm text-(--text)" > - {row[col.id as keyof typeof row]} + {col.id === 'status' + ? t(`customReportBuilder.status.${String(row[col.id as keyof typeof row]).toLowerCase()}`) + : row[col.id as keyof typeof row]} ))} @@ -202,17 +202,13 @@ const CustomReportBuilder = () => { - No data found for the selected date range. - + >{t('customReportBuilder.noData')} )} ) : ( -
- Please select at least one column to preview data. -
+
{t('customReportBuilder.selectColumn')}
)}
diff --git a/frontend/src/pages/EmployeePortal.tsx b/frontend/src/pages/EmployeePortal.tsx index abd4b997..48b68e96 100644 --- a/frontend/src/pages/EmployeePortal.tsx +++ b/frontend/src/pages/EmployeePortal.tsx @@ -1,4 +1,5 @@ import React, { ChangeEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ArrowUpRight, RefreshCw, @@ -36,7 +37,7 @@ import { import WithdrawalFlow from '../components/WithdrawalFlow'; /* ── Helper: status badge ────────── */ -function StatusBadge({ status }: { status: EmployeeTransaction['status'] }) { +function StatusBadge({ status, label }: { status: EmployeeTransaction['status']; label: string }) { const map = { completed: { cls: styles.statusCompleted, dot: styles.statusDotCompleted }, pending: { cls: styles.statusPending, dot: styles.statusDotPending }, @@ -46,20 +47,20 @@ function StatusBadge({ status }: { status: EmployeeTransaction['status'] }) { return ( - {status} + {label} ); } /* ── Helper: type badge ──────────── */ -function TypeBadge({ type }: { type: EmployeeTransaction['type'] }) { +function TypeBadge({ type, label }: { type: EmployeeTransaction['type']; label: string }) { const map = { salary: { cls: styles.txMemoTypeSalary, icon: }, bonus: { cls: styles.txMemoTypeBonus, icon: }, reimbursement: { cls: styles.txMemoTypeReimbursement, icon: }, }; const { cls } = map[type]; - return {type}; + return {label}; } /* ── Loading skeleton ────────────── */ @@ -75,6 +76,7 @@ function LoadingSkeleton() { /* ── Main Page Component ─────────── */ const EmployeePortal: React.FC = () => { + const { t } = useTranslation(); const { address } = useWallet(); const { notifySuccess, notifyError } = useNotification(); const { contractError, handleContractError, clearContractError } = useContractError(); @@ -167,7 +169,7 @@ const EmployeePortal: React.FC = () => { } catch (e: unknown) { if (!cancelled) { setPendingClaims([]); - const errorMessage = e instanceof Error ? e.message : 'Failed to load pending claims'; + const errorMessage = e instanceof Error ? e.message : t('employeePortal.errors.loadPendingClaims'); setPendingClaimsError(errorMessage); } } @@ -177,7 +179,7 @@ const EmployeePortal: React.FC = () => { return () => { cancelled = true; }; - }, [address]); + }, [address, t]); const handleClaim = async (claimId: string) => { setIsClaiming(claimId); @@ -194,7 +196,7 @@ const EmployeePortal: React.FC = () => { throw new Error('Contract invocation failed'); } - notifySuccess('Claim successful!', 'The funds have been transferred to your wallet.'); + notifySuccess(t('employeePortal.claim.successTitle'), t('employeePortal.claim.successBody')); // Remove the claimed item from the list setPendingClaims((prev: PendingClaimRecord[]) => @@ -202,7 +204,7 @@ const EmployeePortal: React.FC = () => { ); } catch (err: unknown) { console.error(err); - notifyError('Claim failed', 'A contract error occurred. Please review the details below.'); + notifyError(t('employeePortal.claim.failedTitle'), t('employeePortal.claim.failedBody')); } finally { setIsClaiming(null); } @@ -221,13 +223,13 @@ const EmployeePortal: React.FC = () => { await new Promise((resolve) => setTimeout(resolve, 2000)); notifySuccess( - `${assetCode} Trustline Established`, - 'You can now receive payroll in this asset.' + t('employeePortal.trustline.establishedTitle', { assetCode }), + t('employeePortal.trustline.establishedBody') ); setMissingTrustlines((prev: string[]) => prev.filter((c: string) => c !== assetCode)); } catch (err: unknown) { console.error(err); - notifyError('Failed to establish trustline', 'Please try again.'); + notifyError(t('employeePortal.trustline.failedTitle'), t('employeePortal.common.tryAgain')); } finally { setIsEstablishing(null); } @@ -243,10 +245,10 @@ const EmployeePortal: React.FC = () => {

- My Portal + {t('employeePortal.titlePrefix')} {t('employeePortal.titleHighlight')}

- View your salary payments, balances, and transaction history + {t('employeePortal.subtitle')}

@@ -254,26 +256,26 @@ const EmployeePortal: React.FC = () => { {deductionsDraft && (
-

Deductions Breakdown

+

{t('employeePortal.deductions.title')}

-
Gross Pay
+
{t('employeePortal.deductions.grossPay')}
{formatCurrency(deductionsDraft.gross_amount, 'USD')}
-
Total Deductions
+
{t('employeePortal.deductions.totalDeductions')}
{formatCurrency(deductionsDraft.total_deductions, 'USD')}
-
Net Pay
+
{t('employeePortal.deductions.netPay')}
{formatCurrency(deductionsDraft.net_amount, 'USD')}
@@ -288,19 +290,19 @@ const EmployeePortal: React.FC = () => { padding: '10px 24px', }} > - Deduction - Type - Amount - - + {t('employeePortal.deductions.deduction')} + {t('employeePortal.common.type')} + {t('employeePortal.common.amount')} + +
{deductionsDraft.lines.length === 0 ? (
-

No deductions configured

-

Your net pay equals your gross pay for now.

+

{t('employeePortal.deductions.emptyTitle')}

+

{t('employeePortal.deductions.emptyBody')}

) : ( deductionsDraft.lines.map((line) => ( @@ -356,7 +358,7 @@ const EmployeePortal: React.FC = () => { {/* ── Balance Card ─────────────── */}
-

Total Balance

+

{t('employeePortal.balance.total')}

{isLoading ? (
@@ -375,12 +377,12 @@ const EmployeePortal: React.FC = () => {

1 ORGUSD ≈ {getCurrencySymbol(selectedCurrency)} {balance?.exchangeRate?.toLocaleString()} {selectedCurrency} - {balance?.lastUpdated && <> · Updated {balance.lastUpdated.toLocaleTimeString()}} + {balance?.lastUpdated && <> · {t('employeePortal.balance.updated', { time: balance.lastUpdated.toLocaleTimeString() })}}

- Local Currency: + {t('employeePortal.balance.localCurrency')} { value={filterStatus} onChange={(e: ChangeEvent) => setFilterStatus(e.target.value)} > - - - - + + + +
{/* Rows */} @@ -681,8 +681,8 @@ const EmployeePortal: React.FC = () => { ) : transactions.length === 0 ? (
-

No transactions found

-

Try adjusting your filters or check back later.

+

{t('employeePortal.history.emptyTitle')}

+

{t('employeePortal.history.emptyBody')}

) : ( transactions.map((tx: EmployeeTransaction) => ( @@ -707,7 +707,7 @@ const EmployeePortal: React.FC = () => { {/* Memo + Type */}
{tx.memo} - +
{/* Amount */} @@ -720,7 +720,7 @@ const EmployeePortal: React.FC = () => { {/* Status */}
- +
{/* Hash */} @@ -737,10 +737,10 @@ const EmployeePortal: React.FC = () => { target="_blank" rel="noopener noreferrer" className={styles.stellarLink} - title="View on Stellar Expert" + title={t('employeePortal.history.viewOnExplorer')} > - Explorer + {t('employeePortal.history.explorer')}
diff --git a/frontend/src/pages/Forecasting.tsx b/frontend/src/pages/Forecasting.tsx index a5ad4d0a..f055cf50 100644 --- a/frontend/src/pages/Forecasting.tsx +++ b/frontend/src/pages/Forecasting.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { CartesianGrid, Line, @@ -22,7 +23,7 @@ import { function getErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; - return 'Unknown error'; + return ''; } type LiquidityAlertPayload = { @@ -51,6 +52,7 @@ function statusClasses(status: 'green' | 'yellow' | 'red'): string { } export default function Forecasting() { + const { t } = useTranslation(); const [forecast, setForecast] = useState(null); const [isLoading, setIsLoading] = useState(false); const [monthsForward, setMonthsForward] = useState(6); @@ -82,7 +84,7 @@ export default function Forecasting() { subscribeToOrganization(f.organizationId); } } catch (e: unknown) { - notifyError(getErrorMessage(e) || 'Failed to load forecast'); + notifyError(getErrorMessage(e) || t('forecasting.errors.loadFailed')); } finally { setIsLoading(false); } @@ -104,7 +106,11 @@ export default function Forecasting() { const handler = (payload: unknown) => { const normalized = normalizeLiquidityAlertPayload(payload); notifyError( - `Liquidity ${String(normalized.severity || '').toUpperCase()}: shortfall ${normalized.shortfallAmount ?? ''} ${normalized.assetCode ?? ''}` + t('forecasting.alertShortfall', { + severity: String(normalized.severity || '').toUpperCase(), + amount: normalized.shortfallAmount ?? '', + asset: normalized.assetCode ?? '', + }) ); }; @@ -112,7 +118,7 @@ export default function Forecasting() { return () => { socket.off('liquidity:alert', handler); }; - }, [socket, notifyError]); + }, [socket, notifyError, t]); const chartData = useMemo(() => forecast?.monthly || [], [forecast]); @@ -122,12 +128,12 @@ export default function Forecasting() { try { const updated = await updateLiquiditySettings(settingsDraft); setSettings(updated); - notifySuccess('Liquidity settings updated'); + notifySuccess(t('forecasting.settingsUpdated')); const f = await getForecast(monthsForward); setForecast(f); } catch (e: unknown) { - notifyError(getErrorMessage(e) || 'Failed to update settings'); + notifyError(getErrorMessage(e) || t('forecasting.errors.updateFailed')); } }; @@ -136,10 +142,10 @@ export default function Forecasting() {
- Payroll Forecasting + {t('forecasting.title')} - Project payroll liabilities and monitor liquidity risk. + {t('forecasting.subtitle')}
@@ -150,7 +156,7 @@ export default function Forecasting() { onChange={(e) => setMonthsForward(Math.min(6, Math.max(3, Number(e.target.value) || 6))) } - label="Months" + label={t('forecasting.months')} type="number" fieldSize="sm" /> @@ -164,12 +170,12 @@ export default function Forecasting() { const f = await getForecast(monthsForward); setForecast(f); } catch (e: unknown) { - notifyError(getErrorMessage(e) || 'Failed to refresh'); + notifyError(getErrorMessage(e) || t('forecasting.errors.refreshFailed')); } })() } > - Refresh + {t('forecasting.refresh')}
@@ -178,16 +184,19 @@ export default function Forecasting() {
- Liquidity Status + {t('forecasting.liquidityStatus')} {liquidity ? ( - Available: {liquidity.availableBalance} {liquidity.assetCode} · Required (next 2 - runs): {liquidity.requiredNext2Runs} {liquidity.assetCode} + {t('forecasting.liquidityAvailable', { + available: liquidity.availableBalance, + required: liquidity.requiredNext2Runs, + asset: liquidity.assetCode, + })} ) : ( - Configure liquidity settings to calculate status. + {t('forecasting.configureLiquidity')} )}
@@ -198,7 +207,7 @@ export default function Forecasting() { liquidity.status )}`} > - {liquidity.status} + {t(`forecasting.status.${liquidity.status}`)}
)}
@@ -206,7 +215,7 @@ export default function Forecasting() { - Projected Monthly Payroll Cost + {t('forecasting.projectedMonthlyPayroll')}
@@ -229,12 +238,12 @@ export default function Forecasting() { - Liquidity Settings + {t('forecasting.liquiditySettings')}
setSettingsDraft((s) => ({ ...s, distributionAccount: e.target.value })) @@ -243,21 +252,21 @@ export default function Forecasting() { /> setSettingsDraft((s) => ({ ...s, assetIssuer: e.target.value }))} fieldSize="sm" /> setSettingsDraft((s) => ({ ...s, assetCode: e.target.value }))} fieldSize="sm" /> @@ -267,7 +276,7 @@ export default function Forecasting() { /> @@ -277,7 +286,7 @@ export default function Forecasting() { /> setSettingsDraft((s) => ({ @@ -294,11 +303,11 @@ export default function Forecasting() {
{settings && ( - Saved. + {t('forecasting.saved')} )}
diff --git a/frontend/src/pages/HelpCenter.tsx b/frontend/src/pages/HelpCenter.tsx index d757ab24..295a97f9 100644 --- a/frontend/src/pages/HelpCenter.tsx +++ b/frontend/src/pages/HelpCenter.tsx @@ -1,56 +1,25 @@ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ChevronDown } from 'lucide-react'; -const docs = [ - { - category: 'FAQs', - items: [ - { - question: 'How do I add an employee?', - answer: - "Go to the Employees page and click 'Add Employee'. Fill in the required details and save.", - }, - { - question: 'How do I reset my password?', - answer: - "Click your profile in the sidebar, then select 'Reset Password' and follow the instructions.", - }, - ], - }, - { - category: 'Stellar Concepts', - items: [ - { - question: 'What is a trustline?', - answer: - 'A trustline is a permission you grant to hold a specific asset on the Stellar network.', - }, - { - question: 'What is an anchor?', - answer: - 'An anchor is an entity that issues assets and connects the Stellar network to traditional banking.', - }, - ], - }, - { - category: 'Troubleshooting', - items: [ - { - question: 'Payroll failed to send.', - answer: - 'Check your account balance and trustlines. Ensure all employees have valid Stellar addresses.', - }, - { - question: 'Employee not receiving payments.', - answer: - 'Verify the employee’s Stellar address and that they have established the necessary trustlines.', - }, - ], - }, -]; - export default function HelpCenter() { + const { t } = useTranslation(); const [search, setSearch] = useState(''); + + const docs = [ + { category: t('helpCenter.faqs'), items: [ + { question: t('helpCenter.addEmployeeQuestion'), answer: t('helpCenter.addEmployeeAnswer') }, + { question: t('helpCenter.resetPasswordQuestion'), answer: t('helpCenter.resetPasswordAnswer') }, + ]}, + { category: t('helpCenter.stellarConcepts'), items: [ + { question: t('helpCenter.trustlineQuestion'), answer: t('helpCenter.trustlineAnswer') }, + { question: t('helpCenter.anchorQuestion'), answer: t('helpCenter.anchorAnswer') }, + ]}, + { category: t('helpCenter.troubleshooting'), items: [ + { question: t('helpCenter.payrollFailedQuestion'), answer: t('helpCenter.payrollFailedAnswer') }, + { question: t('helpCenter.employeeMissingQuestion'), answer: t('helpCenter.employeeMissingAnswer') }, + ]}, + ]; const [openItem, setOpenItem] = useState(null); const toggleItem = (id: string) => { @@ -74,10 +43,10 @@ export default function HelpCenter() { {/* Header */}

- Help Center + {t('helpCenter.titlePrefix')} {t('helpCenter.titleHighlight')}

- Documentation · FAQs · Troubleshooting + {t('helpCenter.subtitle')}

@@ -85,7 +54,7 @@ export default function HelpCenter() {
setSearch(e.target.value)} className="w-full px-5 py-3 rounded-xl bg-(--surface-hi) border border-(--border) focus:outline-none focus:ring-2 focus:ring-(--accent) text-sm transition" @@ -94,7 +63,7 @@ export default function HelpCenter() { {/* Results */} {filteredDocs.length === 0 && ( -

No results found.

+

{t('helpCenter.noResults')}

)} {/* Accordion Sections */} diff --git a/frontend/src/pages/TaxComplianceWizard.tsx b/frontend/src/pages/TaxComplianceWizard.tsx index 4048c9ae..f23a2a2a 100644 --- a/frontend/src/pages/TaxComplianceWizard.tsx +++ b/frontend/src/pages/TaxComplianceWizard.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { AlertTriangle, @@ -46,6 +47,7 @@ interface TaxConfig { } const TaxComplianceWizard: React.FC = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); @@ -168,20 +170,20 @@ const TaxComplianceWizard: React.FC = () => { return (
-

Tax Compliance Wizard

+

{t('taxCompliance.title')}

- Configure tax rules and generate compliance reports for your organization + {t('taxCompliance.subtitle')}

- {step === 1 && 'Step 1: Select Country'} - {step === 2 && 'Step 2: Tax Configuration'} - {step === 3 && 'Step 3: Preview Calculator'} - {step === 4 && 'Step 4: Generate Report'} - {step === 5 && 'Step 5: Export Summary'} + {step === 1 && t('taxCompliance.steps.selectCountry')} + {step === 2 && t('taxCompliance.steps.configuration')} + {step === 3 && t('taxCompliance.steps.preview')} + {step === 4 && t('taxCompliance.steps.report')} + {step === 5 && t('taxCompliance.steps.export')}

{[1, 2, 3, 4, 5].map((i) => ( @@ -201,7 +203,7 @@ const TaxComplianceWizard: React.FC = () => { setSearchQuery(e.target.value)} className="w-full bg-black/20 border border-hi rounded-xl p-4 pl-12 text-text outline-none focus:border-accent/50 focus:bg-accent/5 transition-all" @@ -225,7 +227,7 @@ const TaxComplianceWizard: React.FC = () => { {filteredCountries.length === 0 && (
-

No countries found matching "{searchQuery}"

+

{t('taxCompliance.noCountries', { query: searchQuery })}

)}
@@ -237,7 +239,7 @@ const TaxComplianceWizard: React.FC = () => { {COUNTRY_FLAGS[config.country] || '🌍'}

{config.country}

-

Tax compliance configuration

+

{t('taxCompliance.configuration')}

@@ -276,7 +278,7 @@ const TaxComplianceWizard: React.FC = () => { {taxRules.length > 0 && (

- Active Tax Rules ({taxRules.length}) + {t('taxCompliance.activeRules', { count: taxRules.length })}

{taxRules.slice(0, 5).map((rule) => ( @@ -295,7 +297,7 @@ const TaxComplianceWizard: React.FC = () => {
- Configure tax identifiers and rates for {config.country} compliance + {t('taxCompliance.configureForCountry', { country: config.country })}
@@ -306,7 +308,7 @@ const TaxComplianceWizard: React.FC = () => {
$ @@ -320,7 +322,7 @@ const TaxComplianceWizard: React.FC = () => {
{

- Tax Preview Summary + {t('taxCompliance.previewSummary')}

-

Gross Pay

+

{t('taxCompliance.grossPay')}

${(parseFloat(grossPay) || 0).toLocaleString()}

-

Est. Tax

+

{t('taxCompliance.estimatedTax')}

${((parseFloat(grossPay) || 0) * 0.25).toFixed(2)}

-

Net Pay

+

{t('taxCompliance.netPay')}

${((parseFloat(grossPay) || 0) * 0.75).toFixed(2)}

- * Based on default 25% tax rate. Actual calculations will use configured rates. + {t('taxCompliance.defaultRateNote')}

@@ -369,7 +371,7 @@ const TaxComplianceWizard: React.FC = () => {
{
{

- Report Preview + {t('taxCompliance.reportPreview')}

- - - - + + + + @@ -424,7 +426,7 @@ const TaxComplianceWizard: React.FC = () => { {taxPreviews.length > 5 && (

- And {taxPreviews.length - 5} more employees... + {t('taxCompliance.moreEmployees', { count: taxPreviews.length - 5 })}

)} @@ -436,32 +438,32 @@ const TaxComplianceWizard: React.FC = () => {

- Report Generated Successfully! + {t('taxCompliance.reportGenerated')}

- Your tax compliance report for {config.country} is ready for export + {t('taxCompliance.reportReady', { country: config.country })}

-

Total Employees

+

{t('taxCompliance.totalEmployees')}

{reportData.employeeCount}

-

Total Gross

+

{t('taxCompliance.totalGross')}

${reportData.totalGrossPay.toLocaleString()}

-

Tax Collected

+

{t('taxCompliance.taxCollected')}

${reportData.totalTaxCollected.toLocaleString()}

-

Total Net

+

{t('taxCompliance.totalNet')}

${reportData.totalNetPay.toLocaleString()}

@@ -474,10 +476,10 @@ const TaxComplianceWizard: React.FC = () => { className="flex items-center justify-center gap-2 py-4 px-6 rounded-full bg-accent text-on-accent font-bold hover:bg-accent-hover transition-all" > - Download CSV Report + {t('taxCompliance.downloadCsv')}

- Export as CSV for local government filing + {t('taxCompliance.exportHint')}

@@ -493,10 +495,10 @@ const TaxComplianceWizard: React.FC = () => { onClick={step === 1 ? () => navigate('/') : handleBack} > {step === 1 ? ( - 'Cancel' + t('taxCompliance.cancel') ) : ( - Back + {t('taxCompliance.back')} )} @@ -507,7 +509,7 @@ const TaxComplianceWizard: React.FC = () => { className="py-2 px-6 rounded-full bg-accent text-on-accent font-bold text-sm tracking-wide hover:bg-accent-hover shadow-(--shadow-sm) transition-all" onClick={handleValidateConfig} > - Validate & Continue + {t('taxCompliance.validateContinue')} )} {step === 3 && ( @@ -518,7 +520,7 @@ const TaxComplianceWizard: React.FC = () => { }} disabled={isLoading} > - {isLoading ? 'Calculating...' : 'Calculate Preview'} + {isLoading ? t('taxCompliance.calculating') : t('taxCompliance.calculatePreview')} )} {step === 4 && ( @@ -526,7 +528,7 @@ const TaxComplianceWizard: React.FC = () => { className="py-2 px-6 rounded-full bg-accent text-on-accent font-bold text-sm tracking-wide hover:bg-accent-hover shadow-(--shadow-sm) transition-all" onClick={handleGenerateReport} > - Generate Report + {t('taxCompliance.generateReport')} )} {step < 5 && step > 3 && ( @@ -534,7 +536,7 @@ const TaxComplianceWizard: React.FC = () => { className="py-2 px-6 rounded-full bg-accent text-on-accent font-bold text-sm tracking-wide hover:bg-accent-hover shadow-(--shadow-sm) transition-all" onClick={handleNext} > - Continue + {t('taxCompliance.continue')} )} From 1e0cbca21a1f643c4c81fd5cb005c5f5440dc3cb Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 19:40:17 -0400 Subject: [PATCH 02/61] Localize payment amount header --- frontend/src/pages/EmployeePortal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/EmployeePortal.tsx b/frontend/src/pages/EmployeePortal.tsx index 48b68e96..a646debd 100644 --- a/frontend/src/pages/EmployeePortal.tsx +++ b/frontend/src/pages/EmployeePortal.tsx @@ -669,7 +669,7 @@ const EmployeePortal: React.FC = () => { > {t('employeePortal.history.date')} {t('employeePortal.history.description')} - Amount + {t('employeePortal.common.amount')} {t('employeePortal.history.verify')} From 44a459988dfb325bc1abd052d8176fae0bc50a9c Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:23:01 -0400 Subject: [PATCH 03/61] fix(i18n): resolve dynamic statuses and preserve liquidity figures --- frontend/src/locales/en/translation.json | 11 ++++++++++- frontend/src/locales/es/translation.json | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index e7bc6219..a7db9e02 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -239,8 +239,13 @@ "subtitle": "Project payroll liquidity and configure funding alerts.", "months": "Months forward", "refresh": "Refresh", + "status": { + "green": "Healthy", + "yellow": "Warning", + "red": "Critical" + }, "liquidityStatus": "Liquidity Status", - "liquidityAvailable": "Available liquidity", + "liquidityAvailable": "Available: {{available}} {{asset}} · Required (next 2 runs): {{required}} {{asset}}", "configureLiquidity": "Configure Liquidity", "projectedMonthlyPayroll": "Projected Monthly Payroll", "liquiditySettings": "Liquidity Settings", @@ -260,6 +265,10 @@ "dateRange": "Date Range", "startDate": "Start Date", "endDate": "End Date", + "status": { + "paid": "Paid", + "pending": "Pending" + }, "columnsTitle": "Columns", "exportData": "Export Data", "livePreview": "Live Preview", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 34048c9b..c168c8e0 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -239,8 +239,13 @@ "subtitle": "Proyecta la liquidez de nómina y configura alertas de financiación.", "months": "Meses a futuro", "refresh": "Actualizar", + "status": { + "green": "Normal", + "yellow": "Advertencia", + "red": "Crítico" + }, "liquidityStatus": "Estado de liquidez", - "liquidityAvailable": "Liquidez disponible", + "liquidityAvailable": "Disponible: {{available}} {{asset}} · Necesario (próximas 2 ejecuciones): {{required}} {{asset}}", "configureLiquidity": "Configurar liquidez", "projectedMonthlyPayroll": "Nómina mensual proyectada", "liquiditySettings": "Configuración de liquidez", @@ -260,6 +265,10 @@ "dateRange": "Intervalo de fechas", "startDate": "Fecha de inicio", "endDate": "Fecha de fin", + "status": { + "paid": "Pagado", + "pending": "Pendiente" + }, "columnsTitle": "Columnas", "exportData": "Exportar datos", "livePreview": "Vista previa", From 532b55f0441df6919c83dae2f9fa3237546f25bf Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:29:00 -0400 Subject: [PATCH 04/61] fix(i18n): localize forecast chart tooltip labels --- frontend/src/pages/CashFlowForecast.tsx | 1 + frontend/src/pages/Forecasting.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index dcfd863c..a6434694 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -343,6 +343,7 @@ export default function CashFlowForecast() { Date: Tue, 6 Oct 2026 14:38:34 -0400 Subject: [PATCH 05/61] fix(i18n): localize contract error panel --- frontend/src/components/ContractErrorPanel.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ContractErrorPanel.tsx b/frontend/src/components/ContractErrorPanel.tsx index 83a0108c..09da9191 100644 --- a/frontend/src/components/ContractErrorPanel.tsx +++ b/frontend/src/components/ContractErrorPanel.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { ChevronDown, ChevronUp, Copy, AlertTriangle, Info } from 'lucide-react'; import { ContractErrorDetails } from '../utils/contractErrorParser'; +import { useTranslation } from 'react-i18next'; import styles from './ContractErrorPanel.module.css'; interface Props { @@ -10,6 +11,7 @@ interface Props { export const ContractErrorPanel: React.FC = ({ error, className = '' }) => { const [isExpanded, setIsExpanded] = useState(true); + const { t } = useTranslation(); if (!error) return null; @@ -26,7 +28,7 @@ export const ContractErrorPanel: React.FC = ({ error, className = '' }) =
setIsExpanded(!isExpanded)}>
- Contract Invocation Failed + {t('contractError.title')}
{error.code} @@ -43,7 +45,7 @@ export const ContractErrorPanel: React.FC = ({ error, className = '' }) =
- Suggested Action + {t('contractError.suggestedAction')}

{error.action}

@@ -51,19 +53,19 @@ export const ContractErrorPanel: React.FC = ({ error, className = '' }) = {(isUnknown || error.rawXdr) && (
- Raw Transaction Result (XDR) + {t('contractError.rawTransactionResult')}
- {error.rawXdr || 'N/A'} + {error.rawXdr || t('contractError.notAvailable')}
)} From 049fe482547dd970d7498c65b08a869efe8b2944 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:38:37 -0400 Subject: [PATCH 06/61] fix(i18n): localize account connection controls --- frontend/src/components/ConnectAccount.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/ConnectAccount.tsx b/frontend/src/components/ConnectAccount.tsx index a1c3394d..c92b6685 100644 --- a/frontend/src/components/ConnectAccount.tsx +++ b/frontend/src/components/ConnectAccount.tsx @@ -20,15 +20,15 @@ const ConnectAccount: React.FC = () => { {token && (
- Social Active + {t('connectAccount.socialActive')} - Session Active + {t('connectAccount.sessionActive')}
)} {address && (
- Stellar + {t('connectAccount.stellar')} {address.slice(0, 6)}...{address.slice(-4)} @@ -42,7 +42,7 @@ const ConnectAccount: React.FC = () => { }} className="px-4 py-2 rounded-full border border-border-hi text-xs font-semibold text-text hover:bg-danger/10 hover:border-danger/30 hover:text-danger transition-colors" > - Exit + {t('connectAccount.exit')}
); @@ -56,7 +56,7 @@ const ConnectAccount: React.FC = () => { }} className="px-3 py-2 text-sm font-semibold text-text hover:text-accent transition-colors" > - Sign In + {t('connectAccount.signIn')} From dbf777e97f58121e27e5f748a0cd10f0684845f1 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:38:40 -0400 Subject: [PATCH 07/61] fix(i18n): localize scheduling wizard --- frontend/src/components/SchedulingWizard.tsx | 61 ++++++++++---------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/SchedulingWizard.tsx b/frontend/src/components/SchedulingWizard.tsx index e6852f0a..7a43474c 100644 --- a/frontend/src/components/SchedulingWizard.tsx +++ b/frontend/src/components/SchedulingWizard.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { fetchEmployees, Employee } from '../services/auditApi'; +import { useTranslation } from 'react-i18next'; interface EmployeePreference { id: string; @@ -24,6 +25,7 @@ export const SchedulingWizard = ({ onComplete: (config: SchedulingConfig) => void; onCancel: () => void; }) => { + const { t, i18n } = useTranslation(); const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(true); const [config, setConfig] = useState({ @@ -56,6 +58,8 @@ export const SchedulingWizard = ({ void loadEmployees(); }, []); + const weekdays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const; + const handleNext = () => setStep((s: number) => Math.min(s + 1, 3)); const handleBack = () => setStep((s: number) => Math.max(s - 1, 1)); @@ -88,9 +92,9 @@ export const SchedulingWizard = ({ {/* Wizard Header */}

- {step === 1 && 'Step 1: Set Schedule'} - {step === 2 && 'Step 2: Currency Preferences'} - {step === 3 && 'Step 3: Preview & Confirm'} + {step === 1 && t('schedulingWizard.steps.schedule')} + {step === 2 && t('schedulingWizard.steps.currencyPreferences')} + {step === 3 && t('schedulingWizard.steps.previewConfirm')}

{[1, 2, 3].map((i) => ( @@ -119,28 +123,28 @@ export const SchedulingWizard = ({

- Loading Organizational Data... + {t('schedulingWizard.loading')}

) : step === 2 ? (

- Set default currency payout outputs for each employee. + {t('schedulingWizard.currencyIntro')}

EmployeeGross PayTax AmountNet Pay{t('taxCompliance.employee')}{t('taxCompliance.grossPay')}{t('taxCompliance.taxAmount')}{t('taxCompliance.netPay')}
- - - + + + {config.preferences.length === 0 ? ( ) : ( @@ -172,9 +176,9 @@ export const SchedulingWizard = ({ }} className="bg-transparent border border-hi rounded p-1 text-text focus:border-accent outline-none" > - - - + + + @@ -191,7 +195,7 @@ export const SchedulingWizard = ({
{['weekly', 'biweekly', 'monthly'].map((freq) => ( @@ -210,7 +214,7 @@ export const SchedulingWizard = ({ : 'border-hi text-muted hover:border-accent/40' }`} > - {freq} + {t(`schedulingWizard.frequency.${freq}`)} ))}
@@ -219,7 +223,7 @@ export const SchedulingWizard = ({ {(config.frequency === 'weekly' || config.frequency === 'biweekly') && (
- Schedule Overview + {t('schedulingWizard.scheduleOverview')}
- Frequency - {config.frequency} + {t('schedulingWizard.frequencyLabel')} + {t(`schedulingWizard.frequency.${config.frequency}`)}
- Time + {t('schedulingWizard.time')} {config.timeOfDay}
@@ -313,7 +316,7 @@ export const SchedulingWizard = ({

- Upcoming Runs + {t('schedulingWizard.upcomingRuns')}

    {generatePreviewDates().map((date, i) => ( @@ -324,7 +327,7 @@ export const SchedulingWizard = ({ {i + 1} - {date.toLocaleString()} + {date.toLocaleString(i18n.language)} ))}
@@ -338,7 +341,7 @@ export const SchedulingWizard = ({ className={`py-2 px-6 rounded-lg font-bold text-sm tracking-wide transition-colors ${step === 1 ? 'text-muted hover:text-text' : 'bg-surface hover:bg-hi/50 text-text'}`} onClick={step === 1 ? onCancel : handleBack} > - {step === 1 ? 'Cancel' : 'Back'} + {step === 1 ? t('schedulingWizard.cancel') : t('schedulingWizard.back')} {step < 3 ? ( @@ -346,14 +349,14 @@ export const SchedulingWizard = ({ className="py-2 px-6 rounded-full bg-accent text-on-accent font-bold text-sm tracking-wide hover:bg-accent-hover shadow-(--shadow-sm) transition-all" onClick={handleNext} > - Continue + {t('schedulingWizard.continue')} ) : (
@@ -159,16 +161,16 @@ export const CSVUploader: React.FC = ({ {/* File info */} {fileName && (
-

File: {fileName}

+

{t('csvUploader.file', { fileName })}

- {validRowsCount} valid rows + {t('csvUploader.validRows', { count: validRowsCount })} {invalidRowsCount > 0 && ( - {invalidRowsCount} rows with errors + {t('csvUploader.rowsWithErrors', { count: invalidRowsCount })} )}
@@ -178,19 +180,19 @@ export const CSVUploader: React.FC = ({ {/* Preview Table */} {parsedData.length > 0 && (
-

Preview

+

{t('csvUploader.preview')}

EmployeeScheduled AmountReceive In{t('schedulingWizard.table.employee')}{t('schedulingWizard.table.scheduledAmount')}{t('schedulingWizard.table.receiveIn')}
- No employees found in organization. + {t('schedulingWizard.table.noEmployees')}
- - + + {Object.keys(parsedData[0]?.data || {}).map((col) => ( ))} - + @@ -227,7 +229,7 @@ export const CSVUploader: React.FC = ({ ))} ) : ( - OK + {t('csvUploader.ok')} )} From ba9b99c3f1a63d804c01413364c82351f52f5c97 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:39:06 -0400 Subject: [PATCH 09/61] fix(i18n): add en shared component copy --- frontend/src/locales/en/translation.json | 60 +++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index a7db9e02..d9371fc0 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -130,7 +130,12 @@ "authenticated": "Authenticated", "connect": "Connect", "wallet": "Wallet", - "exit": "Exit" + "exit": "Exit", + "connecting": "Connecting...", + "socialActive": "Social Active", + "sessionActive": "Session Active", + "stellar": "Stellar", + "signIn": "Sign In" }, "wallet": { "modalTitle": "Connect to PayD" @@ -442,5 +447,58 @@ "calculatePreview": "Calculate Preview", "generateReport": "Generate Report", "continue": "Continue" + }, + "contractError": { + "title": "Contract Invocation Failed", + "suggestedAction": "Suggested Action", + "rawTransactionResult": "Raw Transaction Result (XDR)", + "copyXdr": "Copy XDR", + "copy": "Copy", + "notAvailable": "N/A" + }, + "schedulingWizard": { + "steps": { + "schedule": "Step 1: Set Schedule", + "currencyPreferences": "Step 2: Currency Preferences", + "previewConfirm": "Step 3: Preview & Confirm" + }, + "loading": "Loading Organizational Data...", + "currencyIntro": "Set default currency payout outputs for each employee.", + "table": { + "employee": "Employee", + "scheduledAmount": "Scheduled Amount", + "receiveIn": "Receive In", + "noEmployees": "No employees found in organization." + }, + "currencies": { + "usdcStellar": "USDC (Stellar)", + "xlm": "XLM", + "eurc": "EURC" + }, + "frequencyLabel": "Frequency", + "frequency": { + "weekly": "Weekly", + "biweekly": "Biweekly", + "monthly": "Monthly" + }, + "dayOfWeek": "Day of Week", + "weekdays": { + "sunday": "Sunday", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday" + }, + "dayOfMonth": "Day of Month", + "runTime": "Run Time", + "scheduleOverview": "Schedule Overview", + "time": "Time", + "upcomingRuns": "Upcoming Runs", + "cancel": "Cancel", + "back": "Back", + "continue": "Continue", + "confirmSchedule": "Confirm Schedule" } } From 6708d16adc3e6c00598f63e2eb4425fd516e7f32 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:39:10 -0400 Subject: [PATCH 10/61] fix(i18n): add es shared component copy --- frontend/src/locales/es/translation.json | 60 +++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index c168c8e0..12c20072 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -130,7 +130,12 @@ "authenticated": "Autenticado", "connect": "Conectar", "wallet": "Billetera", - "exit": "Salir" + "exit": "Salir", + "connecting": "Conectando...", + "socialActive": "Acceso social activo", + "sessionActive": "Sesión activa", + "stellar": "Stellar", + "signIn": "Iniciar sesión" }, "wallet": { "modalTitle": "Conectar a PayD" @@ -442,5 +447,58 @@ "calculatePreview": "Calcular vista previa", "generateReport": "Generar informe", "continue": "Continuar" + }, + "contractError": { + "title": "Error al invocar el contrato", + "suggestedAction": "Acción sugerida", + "rawTransactionResult": "Resultado bruto de la transacción (XDR)", + "copyXdr": "Copiar XDR", + "copy": "Copiar", + "notAvailable": "N/D" + }, + "schedulingWizard": { + "steps": { + "schedule": "Paso 1: Configurar programación", + "currencyPreferences": "Paso 2: Preferencias de moneda", + "previewConfirm": "Paso 3: Vista previa y confirmación" + }, + "loading": "Cargando datos de la organización...", + "currencyIntro": "Configura la moneda de pago predeterminada para cada empleado.", + "table": { + "employee": "Empleado", + "scheduledAmount": "Importe programado", + "receiveIn": "Recibir en", + "noEmployees": "No se encontraron empleados en la organización." + }, + "currencies": { + "usdcStellar": "USDC (Stellar)", + "xlm": "XLM", + "eurc": "EURC" + }, + "frequencyLabel": "Frecuencia", + "frequency": { + "weekly": "Semanal", + "biweekly": "Quincenal", + "monthly": "Mensual" + }, + "dayOfWeek": "Día de la semana", + "weekdays": { + "sunday": "Domingo", + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado" + }, + "dayOfMonth": "Día del mes", + "runTime": "Hora de ejecución", + "scheduleOverview": "Resumen de la programación", + "time": "Hora", + "upcomingRuns": "Próximas ejecuciones", + "cancel": "Cancelar", + "back": "Atrás", + "continue": "Continuar", + "confirmSchedule": "Confirmar programación" } } From 852f82e0c7e50b706530c11e6aac42ddd77f387d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:39:37 -0400 Subject: [PATCH 11/61] fix(i18n): localize bulk payment status tracker --- .../components/BulkPaymentStatusTracker.tsx | 139 ++++++++++++------ 1 file changed, 95 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/BulkPaymentStatusTracker.tsx b/frontend/src/components/BulkPaymentStatusTracker.tsx index 4a305ae4..8c75ecdc 100644 --- a/frontend/src/components/BulkPaymentStatusTracker.tsx +++ b/frontend/src/components/BulkPaymentStatusTracker.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNotification } from '../hooks/useNotification'; import { useSocket } from '../hooks/useSocket'; import { useWallet } from '../hooks/useWallet'; @@ -28,10 +29,10 @@ function toRecipientStatus( return 'pending'; } -function getEmployeeName(recipient: PayrollRecipientStatus): string { +function getEmployeeName(recipient: PayrollRecipientStatus, fallback: string): string { const fullName = `${recipient.employee_first_name ?? ''} ${recipient.employee_last_name ?? ''}`.trim(); - return fullName || recipient.employee_email || `Employee #${recipient.employee_id}`; + return fullName || recipient.employee_email || fallback; } function findRunTxHash(summary?: PayrollRunSummary): string | null { @@ -72,6 +73,7 @@ function normalizeConfirmationPayload(payload: unknown): { } export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTrackerProps) { + const { t } = useTranslation(); const [runs, setRuns] = useState([]); const [summaries, setSummaries] = useState>({}); const [expandedRunId, setExpandedRunId] = useState(null); @@ -92,13 +94,16 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr const payload = await fetchPayrollRuns(organizationId, 1, 20); setRuns(payload.data); } catch (loadError) { - const message = loadError instanceof Error ? loadError.message : 'Failed to load bulk runs'; + const message = + loadError instanceof Error + ? loadError.message + : t('bulkPaymentStatusTracker.errors.loadRuns'); setError(message); - notifyError('Bulk payment load failed', message); + notifyError(t('bulkPaymentStatusTracker.errors.loadTitle'), message); } finally { setIsLoading(false); } - }, [notifyError, organizationId]); + }, [notifyError, organizationId, t]); useEffect(() => { void loadRuns(); @@ -114,11 +119,11 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr const message = summaryError instanceof Error ? summaryError.message - : 'Failed to load per-recipient status'; - notifyError('Failed to load batch details', message); + : t('bulkPaymentStatusTracker.errors.loadRecipientStatus'); + notifyError(t('bulkPaymentStatusTracker.errors.loadDetailsTitle'), message); } }, - [notifyError, summaries] + [notifyError, summaries, t] ); useEffect(() => { @@ -160,7 +165,10 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr const handleRetry = async (run: PayrollRunRecord) => { if (!address) { - notifyError('Wallet required', 'Connect a wallet before retrying failed recipients.'); + notifyError( + t('bulkPaymentStatusTracker.errors.walletRequiredTitle'), + t('bulkPaymentStatusTracker.errors.walletRequiredBody') + ); return; } @@ -176,7 +184,7 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr (import.meta.env.VITE_BULK_PAYMENT_CONTRACT_ID as string | undefined); if (!contractId) { - throw new Error('Bulk payment contract ID is unavailable.'); + throw new Error(t('bulkPaymentStatusTracker.errors.contractUnavailable')); } const { txHash } = await retryFailedBatch({ @@ -186,11 +194,20 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr signTransaction: sign, }); - notifySuccess('Retry submitted', `Batch ${run.batch_id} was re-invoked. TX: ${txHash}`); + notifySuccess( + t('bulkPaymentStatusTracker.notifications.retrySubmittedTitle'), + t('bulkPaymentStatusTracker.notifications.retrySubmittedBody', { + batchId: run.batch_id, + txHash, + }) + ); await loadSummary(run.id); } catch (retryError) { - const message = retryError instanceof Error ? retryError.message : 'Retry failed'; - notifyError('Retry failed', message); + const message = + retryError instanceof Error + ? retryError.message + : t('bulkPaymentStatusTracker.errors.retryFailed'); + notifyError(t('bulkPaymentStatusTracker.errors.retryFailed'), message); } finally { setIsRetryingBatchId(null); } @@ -218,7 +235,7 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr return (
-

Bulk Payment Status Tracker

+

{t('bulkPaymentStatusTracker.title')}

{isLoading ? ( -

Loading bulk payroll runs...

+

{t('bulkPaymentStatusTracker.loadingRuns')}

) : null} {error ?

{error}

: null} {!isLoading && rows.length === 0 ? ( -

No payroll batch runs found.

+

{t('bulkPaymentStatusTracker.emptyRuns')}

) : ( <> {/* Desktop Table View */} @@ -244,13 +261,13 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr
RowStatus{t('csvUploader.row')}{t('csvUploader.status')} {col} Errors{t('csvUploader.errors')}
- - - - - - - + + + + + + + @@ -296,8 +313,12 @@ export function BulkPaymentStatusTracker({ organizationId }: BulkPaymentStatusTr >
-
Batch: {run.batch_id}
-
{run.status}
+
+ {t('bulkPaymentStatusTracker.labels.batch', { batchId: run.batch_id })} +
+
+ {t(`bulkPaymentStatusTracker.status.${run.status}`)} +
- {run.status} + {t(`bulkPaymentStatusTracker.status.${run.status}`)}
- Employees: + {t('bulkPaymentStatusTracker.labels.employees')} {employeeCount}
- Total: + {t('bulkPaymentStatusTracker.labels.total')} {run.total_amount} {run.asset_code}
- Confirmations: + {t('bulkPaymentStatusTracker.labels.confirmations')} {confirmationCount}
{txHash && ( {expandedRunId === run.id && summary && (
- Successful: + {t('bulkPaymentStatusTracker.labels.successful')} {summary.items.filter((item) => item.status === 'completed').length}
- Failed: + {t('bulkPaymentStatusTracker.labels.failed')} {summary.items.filter((item) => item.status === 'failed').length}
{summary.items.filter((item) => item.status === 'failed').length > 0 && (
-
Failed Recipients:
+
{t('bulkPaymentStatusTracker.labels.failedRecipients')}
{summary.items .filter((item) => item.status === 'failed') .map((item) => (
- {getEmployeeName(item)} + {getEmployeeName( + item, + t('bulkPaymentStatusTracker.employeeFallback', { + id: item.employee_id, + }) + )}
))}
@@ -428,11 +458,15 @@ function FragmentRow({ onToggleExpand, onRetry, }: FragmentRowProps) { + const { t } = useTranslation(); + return ( <>
- + @@ -186,7 +194,7 @@ export const EmployeeList: React.FC = ({ {sortedEmployees.length === 0 ? ( ) : ( @@ -236,13 +244,13 @@ export const EmployeeList: React.FC = ({ employee.status === 'Active' ? 'bg-green-600' : 'bg-red-600' }`} /> - {employee.status || '-'} + {getStatusLabel(employee.status)}
BatchStatusEmployeesTotalConfirmationsTx HashActions{t('bulkPaymentStatusTracker.headers.batch')}{t('bulkPaymentStatusTracker.headers.status')}{t('bulkPaymentStatusTracker.headers.employees')}{t('bulkPaymentStatusTracker.headers.total')}{t('bulkPaymentStatusTracker.headers.confirmations')}{t('bulkPaymentStatusTracker.headers.txHash')}{t('bulkPaymentStatusTracker.headers.actions')}
{run.batch_id}{run.status} + {t(`bulkPaymentStatusTracker.status.${run.status}`)} + {employeeCount} {run.total_amount} {run.asset_code} @@ -449,7 +483,7 @@ function FragmentRow({ {txHash.slice(0, 10)}... ) : ( - N/A + {t('bulkPaymentStatusTracker.notAvailable')} )} @@ -459,7 +493,9 @@ function FragmentRow({ onClick={onToggleExpand} className="text-accent hover:text-accent/80" > - {expanded ? 'Hide' : 'Details'} + {expanded + ? t('bulkPaymentStatusTracker.actions.hide') + : t('bulkPaymentStatusTracker.actions.details')} {hasFailedRecipients ? ( ) : null} @@ -478,7 +516,9 @@ function FragmentRow({
{!summary ? ( -

Loading recipient statuses...

+

+ {t('bulkPaymentStatusTracker.loadingRecipientStatuses')} +

) : (
{summary.items.map((recipient) => ( @@ -486,11 +526,22 @@ function FragmentRow({ key={recipient.id} className="flex items-center justify-between rounded-md border border-hi/30 px-3 py-2 text-xs" > - {getEmployeeName(recipient)} + + {getEmployeeName( + recipient, + t('bulkPaymentStatusTracker.employeeFallback', { + id: recipient.employee_id, + }) + )} + {recipient.amount} {run.asset_code} - {toRecipientStatus(recipient.status)} + + {t( + `bulkPaymentStatusTracker.status.${toRecipientStatus(recipient.status)}` + )} +
))} From 05f995e26c02b83605dbc7cbad755e68ebeb7adb Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:40:20 -0400 Subject: [PATCH 12/61] fix(i18n): add shared component English strings --- frontend/src/locales/en/translation.json | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index d9371fc0..383ecb4e 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -500,5 +500,79 @@ "back": "Back", "continue": "Continue", "confirmSchedule": "Confirm Schedule" + }, + "csvUploader": { + "missingRequiredColumns": "Missing required columns: {{columns}}", + "missingRequiredField": "Missing required field: {{column}}", + "pleaseUploadCsv": "Please upload a CSV file", + "dragDrop": "Drag and drop your CSV file", + "browse": "or click to browse", + "requiredColumns": "Required columns: {{columns}}", + "file": "File: {{fileName}}", + "validRows": "{{count}} valid rows", + "rowsWithErrors": "{{count}} rows with errors", + "preview": "Preview", + "row": "Row", + "status": "Status", + "errors": "Errors", + "ok": "OK" + }, + "bulkPaymentStatusTracker": { + "title": "Bulk Payment Status Tracker", + "refresh": "Refresh", + "loadingRuns": "Loading bulk payroll runs...", + "emptyRuns": "No payroll batch runs found.", + "loadingRecipientStatuses": "Loading recipient statuses...", + "notAvailable": "N/A", + "employeeFallback": "Employee #{{id}}", + "headers": { + "batch": "Batch", + "status": "Status", + "employees": "Employees", + "total": "Total", + "confirmations": "Confirmations", + "txHash": "Tx Hash", + "actions": "Actions" + }, + "labels": { + "batch": "Batch: {{batchId}}", + "employees": "Employees:", + "total": "Total:", + "confirmations": "Confirmations:", + "txHash": "Tx Hash:", + "successful": "Successful:", + "failed": "Failed:", + "failedRecipients": "Failed Recipients:" + }, + "actions": { + "hideDetails": "Hide Details", + "showDetails": "Show Details", + "hide": "Hide", + "details": "Details", + "retrying": "Retrying...", + "retryFailed": "Retry Failed" + }, + "status": { + "draft": "Draft", + "pending": "Pending", + "processing": "Processing", + "completed": "Completed", + "failed": "Failed", + "confirmed": "Confirmed" + }, + "errors": { + "loadRuns": "Failed to load bulk runs", + "loadTitle": "Bulk payment load failed", + "loadRecipientStatus": "Failed to load per-recipient status", + "loadDetailsTitle": "Failed to load batch details", + "walletRequiredTitle": "Wallet required", + "walletRequiredBody": "Connect a wallet before retrying failed recipients.", + "contractUnavailable": "Bulk payment contract ID is unavailable.", + "retryFailed": "Retry failed" + }, + "notifications": { + "retrySubmittedTitle": "Retry submitted", + "retrySubmittedBody": "Batch {{batchId}} was re-invoked. TX: {{txHash}}" + } } } From 713572e1bedeb43675afa6d38d8d725a3d730e69 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:40:34 -0400 Subject: [PATCH 13/61] fix(i18n): add shared component Spanish strings --- frontend/src/locales/es/translation.json | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 12c20072..0c71fcfa 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -500,5 +500,79 @@ "back": "Atrás", "continue": "Continuar", "confirmSchedule": "Confirmar programación" + }, + "csvUploader": { + "missingRequiredColumns": "Faltan columnas obligatorias: {{columns}}", + "missingRequiredField": "Falta el campo obligatorio: {{column}}", + "pleaseUploadCsv": "Sube un archivo CSV", + "dragDrop": "Arrastra y suelta tu archivo CSV", + "browse": "o haz clic para buscar", + "requiredColumns": "Columnas obligatorias: {{columns}}", + "file": "Archivo: {{fileName}}", + "validRows": "{{count}} filas válidas", + "rowsWithErrors": "{{count}} filas con errores", + "preview": "Vista previa", + "row": "Fila", + "status": "Estado", + "errors": "Errores", + "ok": "Correcto" + }, + "bulkPaymentStatusTracker": { + "title": "Seguimiento del estado de pagos masivos", + "refresh": "Actualizar", + "loadingRuns": "Cargando ejecuciones de nómina masiva...", + "emptyRuns": "No se encontraron lotes de nómina.", + "loadingRecipientStatuses": "Cargando estados de destinatarios...", + "notAvailable": "N/D", + "employeeFallback": "Empleado #{{id}}", + "headers": { + "batch": "Lote", + "status": "Estado", + "employees": "Empleados", + "total": "Total", + "confirmations": "Confirmaciones", + "txHash": "Hash de transacción", + "actions": "Acciones" + }, + "labels": { + "batch": "Lote: {{batchId}}", + "employees": "Empleados:", + "total": "Total:", + "confirmations": "Confirmaciones:", + "txHash": "Hash de transacción:", + "successful": "Exitosos:", + "failed": "Fallidos:", + "failedRecipients": "Destinatarios fallidos:" + }, + "actions": { + "hideDetails": "Ocultar detalles", + "showDetails": "Mostrar detalles", + "hide": "Ocultar", + "details": "Detalles", + "retrying": "Reintentando...", + "retryFailed": "Reintentar fallidos" + }, + "status": { + "draft": "Borrador", + "pending": "Pendiente", + "processing": "Procesando", + "completed": "Completado", + "failed": "Fallido", + "confirmed": "Confirmado" + }, + "errors": { + "loadRuns": "No se pudieron cargar las ejecuciones masivas", + "loadTitle": "Error al cargar pagos masivos", + "loadRecipientStatus": "No se pudo cargar el estado por destinatario", + "loadDetailsTitle": "No se pudieron cargar los detalles del lote", + "walletRequiredTitle": "Billetera necesaria", + "walletRequiredBody": "Conecta una billetera antes de reintentar los destinatarios fallidos.", + "contractUnavailable": "El ID del contrato de pagos masivos no está disponible.", + "retryFailed": "Error al reintentar" + }, + "notifications": { + "retrySubmittedTitle": "Reintento enviado", + "retrySubmittedBody": "El lote {{batchId}} se volvió a ejecutar. TX: {{txHash}}" + } } } From dbf19281c44042dab5061777ea1775ec96f5029e Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:41:32 -0400 Subject: [PATCH 14/61] fix(i18n): localize app navigation --- frontend/src/components/AppNav.tsx | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/AppNav.tsx b/frontend/src/components/AppNav.tsx index ced8ef3b..3a3b3822 100644 --- a/frontend/src/components/AppNav.tsx +++ b/frontend/src/components/AppNav.tsx @@ -15,8 +15,10 @@ import { TrendingUp, } from 'lucide-react'; import { Avatar } from './Avatar'; +import { useTranslation } from 'react-i18next'; const AppNav: React.FC = () => { + const { t } = useTranslation(); const [mobileOpen, setMobileOpen] = useState(false); // Mock user data - replace with actual user context @@ -42,7 +44,7 @@ const AppNav: React.FC = () => { - Payroll + {t('nav.payroll')} { - Employees + {t('nav.employees')} { - My Portal + {t('nav.myPortal')} { - Reports + {t('nav.reports')} { - Cash Flow + {t('nav.cashFlow')} { - Cross-Asset + {t('nav.crossAsset')} { - History + {t('nav.history')} { - Bulk Payments + {t('nav.bulkPayments')}
@@ -174,7 +176,7 @@ const AppNav: React.FC = () => { } > - Admin + {t('nav.admin')} { onClick={() => setMobileOpen(false)} > - debugger + {t('nav.debugger')} { onClick={() => setMobileOpen(false)} className="text-(--link) text-[13px] font-semibold hover:text-(--accent) transition" > - Help + {t('nav.help')} ); @@ -210,7 +212,7 @@ const AppNav: React.FC = () => { {/* Mobile menu button */}
handleSort('position')} > - Role {sortKey === 'position' && (sortAsc ? '▲' : '▼')} + {t('employees.table.role')} {sortKey === 'position' && (sortAsc ? '▲' : '▼')} handleSort('wallet')} > - Wallet {sortKey === 'wallet' && (sortAsc ? '▲' : '▼')} + {t('employees.table.wallet')} {sortKey === 'wallet' && (sortAsc ? '▲' : '▼')} handleSort('salary')} > - Salary {sortKey === 'salary' && (sortAsc ? '▲' : '▼')} + {t('employees.table.salary')} {sortKey === 'salary' && (sortAsc ? '▲' : '▼')} handleSort('status')} > - Status {sortKey === 'status' && (sortAsc ? '▲' : '▼')} + {t('employees.table.status')} {sortKey === 'status' && (sortAsc ? '▲' : '▼')} - Actions + {t('employees.table.actions')}
- No employees found + {t('employees.empty')}
@@ -361,14 +369,14 @@ export const EmployeeList: React.FC = ({ {/* CSV Import */}
-

Need to migrate your legacy payroll system?

+

{t('employees.importPrompt')}

{!showCSVUploader && ( )} {showCSVUploader && ( @@ -384,7 +392,7 @@ export const EmployeeList: React.FC = ({ style={{ minHeight: '44px' }} disabled={csvData.length === 0} > - Add Employees from CSV + {t('employees.addEmployeesFromCsv')}
@@ -405,38 +413,38 @@ export const EmployeeList: React.FC = ({ {showAddModal && (
-

Add Employee

+

{t('employees.addModal.title')}

setNewEmployee({ ...newEmployee, name: e.target.value })} className="w-full mb-2 px-3 py-2 border rounded" /> setNewEmployee({ ...newEmployee, email: e.target.value })} className="w-full mb-2 px-3 py-2 border rounded" /> setNewEmployee({ ...newEmployee, wallet: e.target.value })} className="w-full mb-2 px-3 py-2 border rounded" /> setNewEmployee({ ...newEmployee, position: e.target.value })} className="w-full mb-2 px-3 py-2 border rounded" /> setNewEmployee({ ...newEmployee, salary: Number(e.target.value) })} className="w-full mb-2 px-3 py-2 border rounded" @@ -448,8 +456,8 @@ export const EmployeeList: React.FC = ({ } className="w-full mb-4 px-3 py-2 border rounded" > - - + +
@@ -474,7 +482,7 @@ export const EmployeeList: React.FC = ({ {showEditModal.open && showEditModal.employee && (
-

Edit Salary

+

{t('employees.editModal.title')}

{showEditModal.employee.name} {showEditModal.employee.position} @@ -491,14 +499,14 @@ export const EmployeeList: React.FC = ({ className="px-4 py-2 bg-(--surface-hi) text-(--text) rounded touch-manipulation" style={{ minHeight: '44px' }} > - Cancel + {t('employees.cancel')}
@@ -508,15 +516,15 @@ export const EmployeeList: React.FC = ({ {showDeleteConfirm.open && (
-

Confirm Removal

-

Are you sure you want to remove this employee?

+

{t('employees.deleteModal.title')}

+

{t('employees.deleteModal.body')}

From 8a3393f5dee687ee5a9add4db8f64d254458899d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:44:57 -0400 Subject: [PATCH 20/61] fix(i18n): localize employee portal refresh --- frontend/src/pages/EmployeePortal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/EmployeePortal.tsx b/frontend/src/pages/EmployeePortal.tsx index a646debd..dbb17af0 100644 --- a/frontend/src/pages/EmployeePortal.tsx +++ b/frontend/src/pages/EmployeePortal.tsx @@ -654,7 +654,7 @@ const EmployeePortal: React.FC = () => { disabled={isLoading} > - Refresh + {t('employeePortal.common.refresh')}
From 4ee070d5bad01b1d64c9965d6b10490e56423277 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:45:31 -0400 Subject: [PATCH 21/61] fix(i18n): translate theme toggle accessible labels --- frontend/src/components/ThemeToggle.tsx | 4 +++- frontend/src/locales/en/translation.json | 4 ++++ frontend/src/locales/es/translation.json | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ThemeToggle.tsx b/frontend/src/components/ThemeToggle.tsx index 84f2d166..e5bc652a 100644 --- a/frontend/src/components/ThemeToggle.tsx +++ b/frontend/src/components/ThemeToggle.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { Sun, Moon } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; +import { useTranslation } from 'react-i18next'; const ThemeToggle: React.FC = () => { const { theme, toggleTheme } = useTheme(); + const { t } = useTranslation(); return ( From 708b2064342b6e8423a6aa7b4bc1ce5956d15980 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:48:40 -0400 Subject: [PATCH 24/61] fix(i18n): localize dashboard top bar --- frontend/src/components/DashboardTopBar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DashboardTopBar.tsx b/frontend/src/components/DashboardTopBar.tsx index 617350d5..8d09ae5d 100644 --- a/frontend/src/components/DashboardTopBar.tsx +++ b/frontend/src/components/DashboardTopBar.tsx @@ -2,8 +2,10 @@ import React from 'react'; import ThemeToggle from './ThemeToggle'; import ConnectAccount from './ConnectAccount'; import { Text } from '@stellar/design-system'; +import { useTranslation } from 'react-i18next'; export const DashboardTopBar: React.FC = () => { + const { t } = useTranslation(); // Mock org data const orgName = 'FutureLabs Inc.'; const balance = '1,250.45 USDC'; @@ -18,7 +20,7 @@ export const DashboardTopBar: React.FC = () => { weight="bold" addlClassName="text-(--muted) uppercase tracking-widest text-[10px]" > - Organization + {t('dashboardTopBar.organization')} {orgName} @@ -34,7 +36,7 @@ export const DashboardTopBar: React.FC = () => { weight="bold" addlClassName="text-(--muted) uppercase tracking-widest text-[10px]" > - Available Balance + {t('dashboardTopBar.availableBalance')} {balance} From c730ab3b732069896f53288ed3c1a8120369c465 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:48:49 -0400 Subject: [PATCH 25/61] fix(i18n): localize employer shell --- frontend/src/components/EmployerLayout.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EmployerLayout.tsx b/frontend/src/components/EmployerLayout.tsx index 5cd117fc..9d098bb2 100644 --- a/frontend/src/components/EmployerLayout.tsx +++ b/frontend/src/components/EmployerLayout.tsx @@ -3,8 +3,10 @@ import { Outlet } from 'react-router-dom'; import { DashboardSidebar } from './DashboardSidebar'; import { DashboardTopBar } from './DashboardTopBar'; import { Menu, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; export const EmployerLayout: React.FC = () => { + const { t } = useTranslation(); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); @@ -49,7 +51,7 @@ export const EmployerLayout: React.FC = () => { @@ -64,11 +66,11 @@ export const EmployerLayout: React.FC = () => {
-

© {new Date().getFullYear()} PayD — Licensed under Apache 2.0

+

{t('employerLayout.license', { year: new Date().getFullYear() })}

- Stellar Testnet Node · V22.1.0 + {t('employerLayout.networkStatus', { version: 'V22.1.0' })}
From bf3324d4795cac99ff69ecccb27cbb132a967032 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:49:17 -0400 Subject: [PATCH 26/61] fix(i18n): add shared shell English copy --- frontend/src/locales/en/translation.json | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 4738ac08..69b7e952 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -631,5 +631,30 @@ "themeToggle": { "switchToLight": "Switch to light mode", "switchToDark": "Switch to dark mode" + }, + "appLayout": { + "beta": "BETA" + }, + "certificate": { + "missingInformation": "Missing Information", + "transactionHashRequired": "Transaction hash is required", + "identityUnavailable": "Unable to determine employee and organization. Please provide employeeId and organizationId.", + "downloadedTitle": "Certificate Downloaded", + "downloadedBody": "Payment certificate has been downloaded successfully", + "downloadFailed": "Download Failed", + "downloadFailedBody": "Failed to download certificate", + "downloadTitle": "Download Proof of Payment Certificate", + "generating": "Generating...", + "loading": "Loading...", + "label": "Certificate" + }, + "dashboardTopBar": { + "organization": "Organization", + "availableBalance": "Available Balance" + }, + "employerLayout": { + "toggleSidebar": "Toggle Sidebar", + "license": "© {{year}} PayD — Licensed under Apache 2.0", + "networkStatus": "Stellar Testnet Node · {{version}}" } } From 3230fc696db496a9f41efa51484634ef24eb00fb Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:49:26 -0400 Subject: [PATCH 27/61] fix(i18n): add shared shell Spanish copy --- frontend/src/locales/es/translation.json | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 413c2a0d..5b997b8a 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -631,5 +631,30 @@ "themeToggle": { "switchToLight": "Cambiar al modo claro", "switchToDark": "Cambiar al modo oscuro" + }, + "appLayout": { + "beta": "BETA" + }, + "certificate": { + "missingInformation": "Falta información", + "transactionHashRequired": "Se requiere el hash de la transacción", + "identityUnavailable": "No se pudo determinar el empleado y la organización. Proporciona employeeId y organizationId.", + "downloadedTitle": "Certificado descargado", + "downloadedBody": "El certificado de pago se descargó correctamente", + "downloadFailed": "Error de descarga", + "downloadFailedBody": "No se pudo descargar el certificado", + "downloadTitle": "Descargar certificado de comprobante de pago", + "generating": "Generando...", + "loading": "Cargando...", + "label": "Certificado" + }, + "dashboardTopBar": { + "organization": "Organización", + "availableBalance": "Saldo disponible" + }, + "employerLayout": { + "toggleSidebar": "Alternar barra lateral", + "license": "© {{year}} PayD — Licenciado bajo Apache 2.0", + "networkStatus": "Nodo de Stellar Testnet · {{version}}" } } From 0c949dd9fcd7256384c141d8f34b03688d0736bf Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:53:15 -0400 Subject: [PATCH 28/61] fix(i18n): localize login page --- frontend/src/pages/Login.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c869f0a7..95175887 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,6 +1,8 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; const Login: React.FC = () => { + const { t } = useTranslation(); const backendUrl = (import.meta.env.VITE_BACKEND_URL as string) || 'http://localhost:4000'; const handleLogin = (provider: 'google' | 'github') => { @@ -11,8 +13,8 @@ const Login: React.FC = () => {
PayD -

Welcome back

-

Connect your account to manage your payroll system

+

{t('login.welcomeBack')}

+

{t('login.subtitle')}

- Secure · Encrypted · Non-Custodial + {t('login.securityNote')}

From bd4d078640e39a1ac921954b05d81b0ab8cf1c45 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:53:26 -0400 Subject: [PATCH 29/61] fix(i18n): localize auth callback --- frontend/src/pages/AuthCallback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/AuthCallback.tsx b/frontend/src/pages/AuthCallback.tsx index 656bccfb..ceddff3b 100644 --- a/frontend/src/pages/AuthCallback.tsx +++ b/frontend/src/pages/AuthCallback.tsx @@ -89,7 +89,7 @@ const AuthCallback: React.FC = () => {
-

Authenticating...

+

{t('authCallback.authenticating')}

); From c06dd51074f2e048d418706dd1e4e9a9db65bd74 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:53:35 -0400 Subject: [PATCH 30/61] fix(i18n): localize vesting grant form --- .../src/components/vesting/VestingGrantForm.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/vesting/VestingGrantForm.tsx b/frontend/src/components/vesting/VestingGrantForm.tsx index 34705b93..24d0459c 100644 --- a/frontend/src/components/vesting/VestingGrantForm.tsx +++ b/frontend/src/components/vesting/VestingGrantForm.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { Card, Heading, Input, Button } from '@stellar/design-system'; +import { useTranslation } from 'react-i18next'; interface VestingGrantFormData { employeeAddress: string; @@ -15,6 +16,7 @@ interface VestingGrantFormProps { } export const VestingGrantForm: React.FC = ({ onSubmit, isSubmitting }) => { + const { t } = useTranslation(); const [formData, setFormData] = useState({ employeeAddress: '', totalAmount: '', @@ -37,12 +39,12 @@ export const VestingGrantForm: React.FC = ({ onSubmit, is
- Create New Vesting Grant + {t('vesting.form.title')}
= ({ onSubmit, is
= ({ onSubmit, is /> = ({ onSubmit, is
= ({ onSubmit, is /> = ({ onSubmit, is />
From 23a344175c38756d851c16f83f73915569380a1a Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:53:56 -0400 Subject: [PATCH 31/61] fix(i18n): localize employee entry flow --- frontend/src/pages/EmployeeEntry.tsx | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/pages/EmployeeEntry.tsx b/frontend/src/pages/EmployeeEntry.tsx index b89a5c83..11643ba5 100644 --- a/frontend/src/pages/EmployeeEntry.tsx +++ b/frontend/src/pages/EmployeeEntry.tsx @@ -76,7 +76,7 @@ export default function EmployeeEntry() { id: String(emp.id), name: `${emp.first_name} ${emp.last_name}`, email: emp.email, - position: emp.position ?? emp.job_title ?? 'Employee', + position: emp.position ?? emp.job_title ?? t('employeeEntry.defaultRole'), wallet: emp.wallet_address, status: emp.status === 'active' ? ('Active' as const) : ('Inactive' as const), })); @@ -86,7 +86,7 @@ export default function EmployeeEntry() { } finally { setLoading(false); } - }, []); + }, [t]); useEffect(() => { void fetchEmployees(); @@ -137,14 +137,14 @@ export default function EmployeeEntry() { await api.post('/employees', payload); notifySuccess( - `${formData.fullName} added successfully!`, - generatedWallet ? 'A new Stellar wallet was generated for this employee.' : undefined + t('employeeEntry.addedTitle', { name: formData.fullName }), + generatedWallet ? t('employeeEntry.walletGenerated') : undefined ); setNotification({ - message: `Employee ${formData.fullName} added successfully! ${ - generatedWallet ? 'A wallet was created for them.' : '' - }`, + message: generatedWallet + ? t('employeeEntry.addedWithWallet', { name: formData.fullName }) + : t('employeeEntry.addedMessage', { name: formData.fullName }), secretKey: generatedWallet?.secretKey, walletAddress, employeeName: formData.fullName, @@ -182,7 +182,7 @@ export default function EmployeeEntry() { @@ -193,7 +193,7 @@ export default function EmployeeEntry() { margin: 0, }} > - Add New Employee + {t('employeeEntry.title')}
@@ -219,17 +219,17 @@ export default function EmployeeEntry() { }} > - [SIMULATED EMAIL NOTIFICATION TO EMPLOYEE] + {t('employeeEntry.simulatedEmailLabel')} - Hello {formData.fullName}, your employer has added you to the payroll. + {t('employeeEntry.simulatedEmailGreeting', { name: notification.employeeName ?? formData.fullName })}
- A default Stellar wallet has been created for you to receive claimable balances. + {t('employeeEntry.simulatedEmailWalletBody')}
- Your Secret Key:{' '} + {t('employeeEntry.secretKeyLabel')}{' '} {notification.secretKey}
- Please save this secret key securely to claim your future salary. + {t('employeeEntry.secretKeyWarning')}
)} @@ -238,7 +238,7 @@ export default function EmployeeEntry() { {notification && !notification.walletAddress && (
- + {notification.message}
@@ -254,7 +254,7 @@ export default function EmployeeEntry() { From 3058c1ccb2141970820a35628557775a01e891be Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:54:16 -0400 Subject: [PATCH 32/61] fix(i18n): localize wallet QR guidance --- frontend/src/components/WalletQRCode.tsx | 69 +++++++++--------------- 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/WalletQRCode.tsx b/frontend/src/components/WalletQRCode.tsx index 208fce0d..2196c283 100644 --- a/frontend/src/components/WalletQRCode.tsx +++ b/frontend/src/components/WalletQRCode.tsx @@ -3,6 +3,7 @@ import { QRCodeSVG } from 'qrcode.react'; import { Button } from '@stellar/design-system'; import { Copy, Key, Eye, BookOpen, ChevronDown, Coins } from 'lucide-react'; import { useNotification } from '../hooks/useNotification'; +import { useTranslation } from 'react-i18next'; interface WalletQRCodeProps { walletAddress: string; @@ -11,29 +12,10 @@ interface WalletQRCodeProps { } const TRUSTLINE_STEPS = [ - { - step: 1, - title: 'Fund Your Wallet', - description: 'Add XLM to your wallet. You need at least 1 XLM to create a trustline.', - }, - { - step: 2, - title: 'Choose Your Asset', - description: - 'Decide which asset you want to receive (USDC, EURC, or XLM). Each asset requires a separate trustline.', - }, - { - step: 3, - title: 'Create Trustline', - description: - "Navigate to your wallet's asset settings and add a trustline for the chosen asset using its issuer address.", - }, - { - step: 4, - title: 'Verify Trustline', - description: - "After creation, verify the trustline appears in your wallet's asset list. You can now receive payments in that asset.", - }, + { step: 1, titleKey: 'walletQr.steps.fund.title', descriptionKey: 'walletQr.steps.fund.description' }, + { step: 2, titleKey: 'walletQr.steps.asset.title', descriptionKey: 'walletQr.steps.asset.description' }, + { step: 3, titleKey: 'walletQr.steps.create.title', descriptionKey: 'walletQr.steps.create.description' }, + { step: 4, titleKey: 'walletQr.steps.verify.title', descriptionKey: 'walletQr.steps.verify.description' }, ]; const ASSET_INFO = [ @@ -62,11 +44,12 @@ export const WalletQRCode: React.FC = ({ const [showSecret, setShowSecret] = useState(false); const [expandedStep, setExpandedStep] = useState(null); const { notifySuccess } = useNotification(); + const { t } = useTranslation(); const copyToClipboard = async (text: string, label: string) => { try { await navigator.clipboard.writeText(text); - notifySuccess(`${label} copied to clipboard!`); + notifySuccess(t('walletQr.copied', { label })); } catch { const textArea = document.createElement('textarea'); textArea.value = text; @@ -74,7 +57,7 @@ export const WalletQRCode: React.FC = ({ textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); - notifySuccess(`${label} copied to clipboard!`); + notifySuccess(t('walletQr.copied', { label })); } }; @@ -86,7 +69,7 @@ export const WalletQRCode: React.FC = ({ return (
-

Your Stellar Wallet Address

+

{t('walletQr.title')}

@@ -96,7 +79,7 @@ export const WalletQRCode: React.FC = ({
@@ -108,17 +91,17 @@ export const WalletQRCode: React.FC = ({ {secretKey && (
{showSecret ? ( @@ -129,10 +112,10 @@ export const WalletQRCode: React.FC = ({
) : ( @@ -141,7 +124,7 @@ export const WalletQRCode: React.FC = ({ className="text-yellow-400 hover:text-yellow-300 text-sm flex items-center gap-2" > - Click to reveal secret key + {t('walletQr.revealSecretKey')} )}
@@ -154,11 +137,10 @@ export const WalletQRCode: React.FC = ({

- Trustline Setup Guide + {t('walletQr.trustlineGuide')}

- To receive payments in different currencies, you need to set up trustlines. Follow these - steps: + {t('walletQr.trustlineIntro')}

@@ -171,7 +153,7 @@ export const WalletQRCode: React.FC = ({ {item.step} - {item.title} + {t(item.titleKey)} = ({ {expandedStep === item.step && (
- {item.description} + {t(item.descriptionKey)}
)}
@@ -192,7 +174,7 @@ export const WalletQRCode: React.FC = ({

- Supported Assets + {t('walletQr.supportedAssets')}

{ASSET_INFO.map((asset) => ( @@ -200,14 +182,14 @@ export const WalletQRCode: React.FC = ({
{asset.code}
- Issuer: + {t('walletQr.issuer')}:
{asset.issuer === 'Native' ? ( - 'Native Asset' + t('walletQr.nativeAsset') ) : (
From fcd6a985500369300cf52efe6e05a708fbf7bd70 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:54:29 -0400 Subject: [PATCH 33/61] fix(i18n): localize vesting grant list --- .../src/components/vesting/VestingGrantList.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/vesting/VestingGrantList.tsx b/frontend/src/components/vesting/VestingGrantList.tsx index 219f1187..4a46f1b6 100644 --- a/frontend/src/components/vesting/VestingGrantList.tsx +++ b/frontend/src/components/vesting/VestingGrantList.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Card, Heading, Text, Button } from '@stellar/design-system'; +import { useTranslation } from 'react-i18next'; interface VestingGrant { id: string; @@ -17,12 +18,14 @@ interface VestingGrantListProps { } export const VestingGrantList: React.FC = ({ grants, onClaim }) => { + const { t, i18n } = useTranslation(); + return (
{grants.length === 0 ? ( - No active vesting grants found. + {t('vesting.list.empty')} ) : ( @@ -37,7 +40,7 @@ export const VestingGrantList: React.FC = ({ grants, onCl {grant.employeeName} - Total: {grant.totalAmount} XLM | Vested: {grant.vestedAmount} XLM + {t('vesting.list.summary', { total: grant.totalAmount, vested: grant.vestedAmount })}
@@ -59,15 +62,15 @@ export const VestingGrantList: React.FC = ({ grants, onCl
- Cliff: + {t('vesting.list.cliff')}: - {new Date(grant.cliffDate).toLocaleDateString()} + {new Date(grant.cliffDate).toLocaleDateString(i18n.language)}
- Start: + {t('vesting.list.start')}: - {new Date(grant.startDate).toLocaleDateString()} + {new Date(grant.startDate).toLocaleDateString(i18n.language)}
From 65fae1d434329963716c28445dc628c9946835ff Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:54:42 -0400 Subject: [PATCH 34/61] fix(i18n): localize vesting escrow --- frontend/src/pages/VestingEscrow.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/VestingEscrow.tsx b/frontend/src/pages/VestingEscrow.tsx index d45f6338..5ee7bf26 100644 --- a/frontend/src/pages/VestingEscrow.tsx +++ b/frontend/src/pages/VestingEscrow.tsx @@ -5,6 +5,7 @@ import { useTransactionSimulation } from '../hooks/useTransactionSimulation'; import { TransactionSimulationPanel } from '../components/TransactionSimulationPanel'; import { VestingGrantList } from '../components/vesting/VestingGrantList'; import { VestingGrantForm } from '../components/vesting/VestingGrantForm'; +import { useTranslation } from 'react-i18next'; interface VestingGrantFormData { employeeAddress: string; @@ -25,6 +26,7 @@ interface VestingGrant { } export default function VestingEscrow() { + const { t } = useTranslation(); const { notifySuccess, notifyError } = useNotification(); const { simulate, @@ -71,16 +73,16 @@ export default function VestingEscrow() { const mockXdr = 'AAAAAgAAAABmF8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; await simulate({ envelopeXdr: mockXdr }); - notifySuccess('Grant simulation ready', 'Review the transaction details before confirming.'); + notifySuccess(t('vesting.notifications.simReadyTitle'), t('vesting.notifications.simReadyBody')); } catch { - notifyError('Grant simulation failed', 'Please check the contract parameters.'); + notifyError(t('vesting.notifications.simFailedTitle'), t('vesting.notifications.simFailedBody')); } finally { setIsSubmitting(false); } }; const handleClaim = () => { - notifySuccess('Claim Initialized', 'Simulating on-chain claim transaction...'); + notifySuccess(t('vesting.notifications.claimTitle'), t('vesting.notifications.claimBody')); // Real logic would invoke the claim entry point }; @@ -88,7 +90,7 @@ export default function VestingEscrow() {
- Vesting Escrow + {t('vesting.page.titlePrefix')} {t('vesting.page.titleHighlight')} - Manage employee token vesting schedules + {t('vesting.page.subtitle')}
@@ -117,12 +119,12 @@ export default function VestingEscrow() { className="btn btn-primary w-full py-4 text-lg font-bold" onClick={() => notifySuccess( - 'Vesting grant created!', - 'The transaction has been broadcast to the network.' + t('vesting.notifications.createdTitle'), + t('vesting.notifications.createdBody') ) } > - Confirm & Create Grant + {t('vesting.page.confirmCreate')}
)} From c38c8dd19d3ae2302c96004c027dbb2c2195a75f Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:55:05 -0400 Subject: [PATCH 35/61] fix(i18n): add identity wallet vesting English copy --- frontend/src/locales/en/translation.json | 108 +++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 69b7e952..1f131665 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -656,5 +656,113 @@ "toggleSidebar": "Toggle Sidebar", "license": "© {{year}} PayD — Licensed under Apache 2.0", "networkStatus": "Stellar Testnet Node · {{version}}" + }, + "login": { + "welcomeBack": "Welcome back", + "subtitle": "Connect your account to manage your payroll system", + "continueGoogle": "Continue with Google", + "continueGithub": "Continue with GitHub", + "securityNote": "Secure · Encrypted · Non-Custodial" + }, + "authCallback": { + "authenticating": "Authenticating..." + }, + "employeeEntry": { + "defaultRole": "Employee", + "addedTitle": "{{name}} added successfully!", + "walletGenerated": "A new Stellar wallet was generated for this employee.", + "addedWithWallet": "Employee {{name}} added successfully! A wallet was created for them.", + "addedMessage": "Employee {{name}} added successfully!", + "backToDirectory": "Back to Directory", + "title": "Add New Employee", + "simulatedEmailLabel": "[SIMULATED EMAIL NOTIFICATION TO EMPLOYEE]", + "simulatedEmailGreeting": "Hello {{name}}, your employer has added you to the payroll.", + "simulatedEmailWalletBody": "A default Stellar wallet has been created for you to receive claimable balances.", + "secretKeyLabel": "Your Secret Key:", + "secretKeyWarning": "Please save this secret key securely to claim your future salary.", + "successTitle": "Success", + "fullName": "Full Name", + "email": "Email Address", + "walletAddressOptional": "Stellar Wallet Address (Optional)", + "walletNote": "If no wallet is provided, a claimable balance will be created using a new wallet generated for them.", + "walletPlaceholder": "Leave blank to generate a wallet", + "role": "Role", + "roles": { + "contractor": "Contractor", + "fullTime": "Full Time", + "partTime": "Part Time" + }, + "preferredCurrency": "Preferred Currency", + "addEmployee": "Add Employee" + }, + "walletQr": { + "copied": "{{label}} copied to clipboard!", + "title": "Your Stellar Wallet Address", + "walletAddress": "Wallet Address", + "copyAddress": "Copy Address", + "secretKeySecure": "Secret Key (Save Securely!)", + "secretKey": "Secret key", + "copySecretKey": "Copy Secret Key", + "revealSecretKey": "Click to reveal secret key", + "trustlineGuide": "Trustline Setup Guide", + "trustlineIntro": "To receive payments in different currencies, you need to set up trustlines. Follow these steps:", + "steps": { + "fund": { + "title": "Fund Your Wallet", + "description": "Add XLM to your wallet. You need at least 1 XLM to create a trustline." + }, + "asset": { + "title": "Choose Your Asset", + "description": "Decide which asset you want to receive (USDC, EURC, or XLM). Each asset requires a separate trustline." + }, + "create": { + "title": "Create Trustline", + "description": "Navigate to your wallet's asset settings and add a trustline for the chosen asset using its issuer address." + }, + "verify": { + "title": "Verify Trustline", + "description": "After creation, verify the trustline appears in your wallet's asset list. You can now receive payments in that asset." + } + }, + "supportedAssets": "Supported Assets", + "issuer": "Issuer", + "nativeAsset": "Native Asset", + "assetIssuer": "{{asset}} issuer", + "shareQr": "Share this QR code with {{name}} so they can receive payments directly to their wallet." + }, + "vesting": { + "form": { + "title": "Create New Vesting Grant", + "employeeAddress": "Employee Wallet Address (G...)", + "totalAmount": "Total Grant Amount", + "durationYears": "Duration (Years)", + "startDate": "Start Date", + "cliffDate": "Cliff Date", + "simulating": "Simulating...", + "initialize": "Initialize Vesting Escrow" + }, + "list": { + "empty": "No active vesting grants found.", + "summary": "Total: {{total}} XLM | Vested: {{vested}} XLM", + "claim": "Claim", + "cliff": "Cliff", + "start": "Start" + }, + "notifications": { + "simReadyTitle": "Grant simulation ready", + "simReadyBody": "Review the transaction details before confirming.", + "simFailedTitle": "Grant simulation failed", + "simFailedBody": "Please check the contract parameters.", + "claimTitle": "Claim Initialized", + "claimBody": "Simulating on-chain claim transaction...", + "createdTitle": "Vesting grant created!", + "createdBody": "The transaction has been broadcast to the network." + }, + "page": { + "titlePrefix": "Vesting", + "titleHighlight": "Escrow", + "subtitle": "Manage employee token vesting schedules", + "confirmCreate": "Confirm & Create Grant" + } } } From 244f6999854235f2cbfefd6a5fb5b1a74ae58887 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:55:27 -0400 Subject: [PATCH 36/61] fix(i18n): add identity wallet vesting Spanish copy --- frontend/src/locales/es/translation.json | 108 +++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 5b997b8a..c32d58ba 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -656,5 +656,113 @@ "toggleSidebar": "Alternar barra lateral", "license": "© {{year}} PayD — Licenciado bajo Apache 2.0", "networkStatus": "Nodo de Stellar Testnet · {{version}}" + }, + "login": { + "welcomeBack": "Bienvenido de nuevo", + "subtitle": "Conecta tu cuenta para administrar tu sistema de nómina", + "continueGoogle": "Continuar con Google", + "continueGithub": "Continuar con GitHub", + "securityNote": "Seguro · Cifrado · Sin custodia" + }, + "authCallback": { + "authenticating": "Autenticando..." + }, + "employeeEntry": { + "defaultRole": "Empleado", + "addedTitle": "¡{{name}} se añadió correctamente!", + "walletGenerated": "Se generó una nueva billetera Stellar para este empleado.", + "addedWithWallet": "¡Empleado {{name}} añadido correctamente! Se creó una billetera para esta persona.", + "addedMessage": "¡Empleado {{name}} añadido correctamente!", + "backToDirectory": "Volver al directorio", + "title": "Añadir nuevo empleado", + "simulatedEmailLabel": "[NOTIFICACIÓN DE CORREO SIMULADA AL EMPLEADO]", + "simulatedEmailGreeting": "Hola {{name}}, tu empleador te ha añadido a la nómina.", + "simulatedEmailWalletBody": "Se ha creado una billetera Stellar predeterminada para que recibas saldos reclamables.", + "secretKeyLabel": "Tu clave secreta:", + "secretKeyWarning": "Guarda esta clave secreta de forma segura para reclamar tu salario futuro.", + "successTitle": "Correcto", + "fullName": "Nombre completo", + "email": "Correo electrónico", + "walletAddressOptional": "Dirección de billetera Stellar (opcional)", + "walletNote": "Si no se proporciona una billetera, se creará un saldo reclamable usando una billetera nueva generada para el empleado.", + "walletPlaceholder": "Deja en blanco para generar una billetera", + "role": "Rol", + "roles": { + "contractor": "Contratista", + "fullTime": "Tiempo completo", + "partTime": "Tiempo parcial" + }, + "preferredCurrency": "Moneda preferida", + "addEmployee": "Añadir empleado" + }, + "walletQr": { + "copied": "¡{{label}} copiado al portapapeles!", + "title": "Tu dirección de billetera Stellar", + "walletAddress": "Dirección de billetera", + "copyAddress": "Copiar dirección", + "secretKeySecure": "Clave secreta (¡guárdala de forma segura!)", + "secretKey": "Clave secreta", + "copySecretKey": "Copiar clave secreta", + "revealSecretKey": "Haz clic para mostrar la clave secreta", + "trustlineGuide": "Guía de configuración de líneas de confianza", + "trustlineIntro": "Para recibir pagos en distintas monedas, debes configurar líneas de confianza. Sigue estos pasos:", + "steps": { + "fund": { + "title": "Fondea tu billetera", + "description": "Añade XLM a tu billetera. Necesitas al menos 1 XLM para crear una línea de confianza." + }, + "asset": { + "title": "Elige tu activo", + "description": "Decide qué activo quieres recibir (USDC, EURC o XLM). Cada activo requiere una línea de confianza independiente." + }, + "create": { + "title": "Crea la línea de confianza", + "description": "Ve a la configuración de activos de tu billetera y añade una línea de confianza para el activo elegido usando la dirección de su emisor." + }, + "verify": { + "title": "Verifica la línea de confianza", + "description": "Después de crearla, verifica que aparezca en la lista de activos de tu billetera. Ya puedes recibir pagos en ese activo." + } + }, + "supportedAssets": "Activos compatibles", + "issuer": "Emisor", + "nativeAsset": "Activo nativo", + "assetIssuer": "Emisor de {{asset}}", + "shareQr": "Comparte este código QR con {{name}} para que pueda recibir pagos directamente en su billetera." + }, + "vesting": { + "form": { + "title": "Crear nueva concesión con consolidación", + "employeeAddress": "Dirección de billetera del empleado (G...)", + "totalAmount": "Importe total de la concesión", + "durationYears": "Duración (años)", + "startDate": "Fecha de inicio", + "cliffDate": "Fecha de carencia", + "simulating": "Simulando...", + "initialize": "Inicializar depósito de consolidación" + }, + "list": { + "empty": "No se encontraron concesiones de consolidación activas.", + "summary": "Total: {{total}} XLM | Consolidado: {{vested}} XLM", + "claim": "Reclamar", + "cliff": "Carencia", + "start": "Inicio" + }, + "notifications": { + "simReadyTitle": "Simulación de concesión lista", + "simReadyBody": "Revisa los detalles de la transacción antes de confirmar.", + "simFailedTitle": "Falló la simulación de la concesión", + "simFailedBody": "Comprueba los parámetros del contrato.", + "claimTitle": "Reclamación iniciada", + "claimBody": "Simulando la transacción de reclamación en cadena...", + "createdTitle": "¡Concesión de consolidación creada!", + "createdBody": "La transacción se ha transmitido a la red." + }, + "page": { + "titlePrefix": "Consolidación", + "titleHighlight": "en depósito", + "subtitle": "Administra los calendarios de consolidación de tokens de empleados", + "confirmCreate": "Confirmar y crear concesión" + } } } From c5e531286422c875fe17b9daab03f31686ec1f43 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:58:24 -0400 Subject: [PATCH 37/61] fix(i18n): localize security admin panel --- frontend/src/pages/AdminPanel.tsx | 245 +++++++++++++++--------------- 1 file changed, 124 insertions(+), 121 deletions(-) diff --git a/frontend/src/pages/AdminPanel.tsx b/frontend/src/pages/AdminPanel.tsx index a7952a1f..d946bfa1 100644 --- a/frontend/src/pages/AdminPanel.tsx +++ b/frontend/src/pages/AdminPanel.tsx @@ -12,6 +12,7 @@ import { import { useNotification } from '../hooks/useNotification'; import { useWallet } from '../hooks/useWallet'; import ContractUpgradeTab from '../components/ContractUpgradeTab'; +import { useTranslation } from 'react-i18next'; /** Centralized API base so URL changes happen in one place. */ const API_BASE = '/api/v1'; @@ -78,15 +79,16 @@ const INPUT_CLASS = const LABEL_CLASS = 'block text-xs font-bold uppercase tracking-widest text-muted mb-2 ml-1'; const TAB_LABELS: Record = { - account: 'Account Control', - global: 'Global Asset Control', - status: 'Status Check', - logs: 'Audit Logs', - contracts: 'Contract Upgrades', - clawback: 'Clawback', + account: 'admin.tabs.account', + global: 'admin.tabs.global', + status: 'admin.tabs.status', + logs: 'admin.tabs.logs', + contracts: 'admin.tabs.contracts', + clawback: 'admin.tabs.clawback', }; export default function AdminPanel() { + const { t, i18n } = useTranslation(); const { notifySuccess, notifyError } = useNotification(); const { address: adminAddress } = useWallet(); @@ -157,7 +159,7 @@ export default function AdminPanel() { setLogsTotal(data.total); } } catch { - notifyError('Fetch Error', 'Failed to load audit logs.'); + notifyError(t('admin.notifications.fetchError'), t('admin.notifications.auditLoadFailed')); } finally { setLogsLoading(false); } @@ -179,7 +181,7 @@ export default function AdminPanel() { setClawbackLogsTotal(data.total); } } catch { - notifyError('Fetch Error', 'Failed to load clawback logs.'); + notifyError(t('admin.notifications.fetchError'), t('admin.notifications.clawbackLoadFailed')); } finally { setClawbackLogsLoading(false); } @@ -191,7 +193,7 @@ export default function AdminPanel() { async function handleAccountAction(action: 'freeze' | 'unfreeze') { if (!accountTarget || !accountAsset || !accountSecret) { - notifyError('Missing fields', 'Target account, asset code, and issuer secret are required.'); + notifyError(t('admin.notifications.missingFields'), t('admin.notifications.accountRequired')); return; } setAccountLoading(true); @@ -207,12 +209,12 @@ export default function AdminPanel() { }), }); const data = (await res.json()) as ActionApiResponse; - if (!res.ok) throw new Error(data.error ?? 'Action failed'); - notifySuccess('Success', data.message); + if (!res.ok) throw new Error(data.error ?? t('admin.notifications.actionFailedBody')); + notifySuccess(t('admin.notifications.success'), data.message); setAccountSecret(''); setAccountReason(''); } catch (err: unknown) { - notifyError('Action Failed', err instanceof Error ? err.message : 'Action failed'); + notifyError(t('admin.notifications.actionFailed'), err instanceof Error ? err.message : t('admin.notifications.actionFailedBody')); } finally { setAccountLoading(false); } @@ -220,7 +222,7 @@ export default function AdminPanel() { async function handleGlobalAction(action: 'freeze' | 'unfreeze') { if (!globalAsset || !globalSecret) { - notifyError('Missing fields', 'Asset code and issuer secret are required.'); + notifyError(t('admin.notifications.missingFields'), t('admin.notifications.globalRequired')); return; } setGlobalLoading(true); @@ -235,12 +237,12 @@ export default function AdminPanel() { }), }); const data = (await res.json()) as ActionApiResponse; - if (!res.ok) throw new Error(data.error ?? 'Action failed'); - notifySuccess('Success', data.message); + if (!res.ok) throw new Error(data.error ?? t('admin.notifications.actionFailedBody')); + notifySuccess(t('admin.notifications.success'), data.message); setGlobalSecret(''); setGlobalReason(''); } catch (err: unknown) { - notifyError('Action Failed', err instanceof Error ? err.message : 'Action failed'); + notifyError(t('admin.notifications.actionFailed'), err instanceof Error ? err.message : t('admin.notifications.actionFailedBody')); } finally { setGlobalLoading(false); } @@ -248,7 +250,7 @@ export default function AdminPanel() { async function handleStatusCheck() { if (!statusTarget || !statusAsset || !statusIssuer) { - notifyError('Missing fields', 'Target account, asset code, and asset issuer are required.'); + notifyError(t('admin.notifications.missingFields'), t('admin.notifications.statusRequired')); return; } setStatusLoading(true); @@ -262,12 +264,12 @@ export default function AdminPanel() { `${API_BASE}/freeze/status/${encodeURIComponent(statusTarget)}?${params}` ); const data = (await res.json()) as StatusResult & { error?: string }; - if (!res.ok) throw new Error(data.error ?? 'Status check failed'); + if (!res.ok) throw new Error(data.error ?? t('admin.notifications.statusCheckFailedBody')); setStatusResult(data); } catch (err: unknown) { notifyError( - 'Status Check Failed', - err instanceof Error ? err.message : 'Status check failed' + t('admin.notifications.statusCheckFailed'), + err instanceof Error ? err.message : t('admin.notifications.statusCheckFailedBody') ); } finally { setStatusLoading(false); @@ -276,12 +278,12 @@ export default function AdminPanel() { async function handleClawback() { if (!clawbackTarget || !clawbackAmount || !clawbackSecret) { - notifyError('Missing fields', 'Target account, amount, and issuer secret are required.'); + notifyError(t('admin.notifications.missingFields'), t('admin.notifications.clawbackRequired')); return; } const parsed = parseFloat(clawbackAmount); if (isNaN(parsed) || parsed <= 0) { - notifyError('Invalid amount', 'Amount must be a positive number.'); + notifyError(t('admin.notifications.invalidAmount'), t('admin.notifications.positiveAmount')); return; } setClawbackLoading(true); @@ -297,8 +299,8 @@ export default function AdminPanel() { }), }); const data = (await res.json()) as { success: boolean; txHash?: string; error?: string }; - if (!res.ok) throw new Error(data.error ?? 'Clawback failed'); - notifySuccess('Clawback Submitted', `Tx: ${data.txHash?.slice(0, 16)}…`); + if (!res.ok) throw new Error(data.error ?? t('admin.notifications.clawbackFailedBody')); + notifySuccess(t('admin.notifications.clawbackSubmitted'), t('admin.notifications.tx', { hash: `${data.txHash?.slice(0, 16)}…` })); setClawbackTarget(''); setClawbackAmount(''); setClawbackSecret(''); @@ -307,7 +309,7 @@ export default function AdminPanel() { void loadClawbackLogs(1); setClawbackLogsPage(1); } catch (err: unknown) { - notifyError('Clawback Failed', err instanceof Error ? err.message : 'Clawback failed'); + notifyError(t('admin.notifications.clawbackFailed'), err instanceof Error ? err.message : t('admin.notifications.clawbackFailedBody')); } finally { setClawbackLoading(false); } @@ -336,10 +338,10 @@ export default function AdminPanel() {

- Security Center + {t('admin.titlePrefix')} {t('admin.titleHighlight')}

- Asset Freeze & Administrative Controls + {t('admin.subtitle')}

@@ -354,7 +356,7 @@ export default function AdminPanel() { style={{ minWidth: '44px' }} > {tab === 'contracts' && } - {TAB_LABELS[tab]} + {t(TAB_LABELS[tab])} ))}
@@ -365,16 +367,15 @@ export default function AdminPanel() { {activeTab === 'account' && (

- Account Level Freeze + {t('admin.account.title')}

- Instantly block or restore an individual account's ability to transact with your - asset. + {t('admin.account.description')}

- +
- +
- +
- + setAccountReason(e.target.value)} className={INPUT_CLASS} - placeholder="e.g. Suspicious activity detected" + placeholder={t('admin.placeholders.suspicious')} maxLength={500} />
@@ -428,14 +429,14 @@ export default function AdminPanel() { onClick={() => void handleAccountAction('freeze')} className="flex-1 py-3 sm:py-4 bg-red-500/20 text-red-500 border border-red-500/50 font-black rounded-xl hover:bg-red-500 hover:text-(--text) transition-all shadow-lg uppercase tracking-widest text-xs sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > - {accountLoading ? 'Processing...' : 'Freeze Account'} + {accountLoading ? t('admin.actions.processing') : t('admin.actions.freezeAccount')}
@@ -445,17 +446,16 @@ export default function AdminPanel() { {activeTab === 'global' && (

- Global Asset Freeze + {t('admin.global.title')}

- WARNING: This will freeze ALL accounts holding this asset. Reserve - for systemic security breaches only. + {t('admin.global.warningLabel')} {t('admin.global.warningBody')}
- +
- +
- + setGlobalReason(e.target.value)} className={INPUT_CLASS} - placeholder="Mandatory systemic freeze reason" + placeholder={t('admin.placeholders.systemic')} maxLength={500} />
@@ -496,14 +496,14 @@ export default function AdminPanel() { onClick={() => void handleGlobalAction('freeze')} className="flex-1 py-3 sm:py-4 bg-red-600/30 text-red-400 border border-red-500/50 font-black rounded-xl hover:bg-red-600 hover:text-(--text) transition-all shadow-lg uppercase tracking-widest text-xs sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > - {globalLoading ? 'Processing...' : 'Engage Global Freeze'} + {globalLoading ? t('admin.actions.processing') : t('admin.actions.engageGlobal')}
@@ -513,15 +513,15 @@ export default function AdminPanel() { {activeTab === 'status' && (

- Trustline Status + {t('admin.status.title')}

- Verify whether an account's trustline is currently frozen for a given asset. + {t('admin.status.description')}

- +
- +
- + void handleStatusCheck()} className="w-full sm:w-auto py-3 sm:py-4 px-6 bg-black/20 border border-hi font-black rounded-xl hover:bg-black/40 transition-all shadow-lg uppercase tracking-widest text-xs sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > - {statusLoading ? 'Checking...' : 'Check Status'} + {statusLoading ? t('admin.actions.checking') : t('admin.actions.checkStatus')} {statusResult && (
- Status + {t('admin.labels.status')} - {statusResult.isFrozen ? 'Frozen' : 'Active'} + {statusResult.isFrozen ? t('admin.status.frozen') : t('admin.status.active')}
-
Account
+
{t('admin.labels.account')}
{statusResult.targetAccount}
-
Asset
+
{t('admin.labels.asset')}
{statusResult.assetCode}
{statusResult.latestAction && ( <>
-
Last Action
-
{statusResult.latestAction.action}
+
{t('admin.labels.lastAction')}
+
{t(`admin.logAction.${statusResult.latestAction.action}`)}
-
Reason
+
{t('admin.labels.reason')}
{statusResult.latestAction.reason || '—'}
-
Timestamp
+
{t('admin.labels.timestamp')}
- {new Date(statusResult.latestAction.created_at).toLocaleString()} + {new Date(statusResult.latestAction.created_at).toLocaleString(i18n.language)}
@@ -624,13 +624,16 @@ export default function AdminPanel() {

- Freeze Audit Logs + {t('admin.logs.title')}

{logsTotal > 0 && ( - {(logsPage - 1) * LOGS_PER_PAGE + 1}– - {Math.min(logsPage * LOGS_PER_PAGE, logsTotal)} of {logsTotal} + {t('admin.pagination.range', { + start: (logsPage - 1) * LOGS_PER_PAGE + 1, + end: Math.min(logsPage * LOGS_PER_PAGE, logsTotal), + total: logsTotal, + })} )}
@@ -648,19 +651,19 @@ export default function AdminPanel() { - - - - - - + + + + + + {logs.length === 0 ? ( ) : ( @@ -670,7 +673,7 @@ export default function AdminPanel() { className="border-b border-hi/50 hover:bg-(--surface-hi) transition-colors" > - +
TimeTargetAssetActionScopeReason{t('admin.labels.time')}{t('admin.labels.target')}{t('admin.labels.asset')}{t('admin.labels.action')}{t('admin.labels.scope')}{t('admin.labels.reason')}
- {logsLoading ? 'Loading…' : 'No freeze logs found.'} + {logsLoading ? t('admin.actions.loading') : t('admin.logs.empty')}
- {new Date(log.created_at).toLocaleString()} + {new Date(log.created_at).toLocaleString(i18n.language)} {log.target_account.slice(0, 8)}…{log.target_account.slice(-4)} @@ -684,10 +687,10 @@ export default function AdminPanel() { : 'bg-emerald-500/20 text-emerald-500' }`} > - {log.action} + {t(`admin.logAction.${log.action}`)} {log.scope}{t(`admin.scope.${log.scope}`)} {logs.length === 0 ? (
- {logsLoading ? 'Loading…' : 'No freeze logs found.'} + {logsLoading ? t('admin.actions.loading') : t('admin.logs.empty')}
) : ( logs.map((log: FreezeLog) => ( @@ -716,7 +719,7 @@ export default function AdminPanel() {
- {new Date(log.created_at).toLocaleString()} + {new Date(log.created_at).toLocaleString(i18n.language)}
{log.target_account}
@@ -727,22 +730,22 @@ export default function AdminPanel() { : 'bg-emerald-500/20 text-emerald-500' }`} > - {log.action} + {t(`admin.logAction.${log.action}`)}
- Asset: + {t('admin.labels.asset')}: {log.asset_code}
- Scope: - {log.scope} + {t('admin.labels.scope')}: + {t(`admin.scope.${log.scope}`)}
{log.reason && (
- Reason: + {t('admin.labels.reason')}:
{log.reason}
)} @@ -760,17 +763,17 @@ export default function AdminPanel() { className="flex items-center gap-1 px-4 py-2.5 text-xs border border-hi rounded hover:bg-black/20 disabled:opacity-40 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > {' '} - Previous + {t('admin.actions.previous')} - Page {logsPage} of {totalPages} + {t('admin.pagination.page', { page: logsPage, total: totalPages })} @@ -784,19 +787,17 @@ export default function AdminPanel() { {/* ── Clawback Form ── */}

- Stellar Asset - Clawback + {t('admin.clawback.title')}

- Irreversible: Clawback burns the specified amount from the target - account. Use only for compliance or error-correction. Requires{' '} - auth_clawback_enabled on the - issuer account. + {t('admin.clawback.irreversibleLabel')} {t('admin.clawback.irreversiblePrefix')}{' '} + auth_clawback_enabled{' '} + {t('admin.clawback.irreversibleSuffix')}
- +
- +
- +
- + setClawbackReason(e.target.value)} className={INPUT_CLASS} - placeholder="e.g. Funds sent to incorrect address" + placeholder={t('admin.placeholders.funds')} maxLength={500} />
@@ -856,7 +857,7 @@ export default function AdminPanel() { onClick={() => void handleClawback()} className="w-full sm:w-auto py-3 sm:py-4 px-8 bg-orange-500/20 text-orange-400 border border-orange-500/50 font-black rounded-xl hover:bg-orange-500 hover:text-(--text) transition-all shadow-lg uppercase tracking-widest text-xs sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > - {clawbackLoading ? 'Submitting…' : 'Execute Clawback'} + {clawbackLoading ? t('admin.actions.submitting') : t('admin.actions.executeClawback')}
@@ -864,14 +865,16 @@ export default function AdminPanel() {

- Clawback History + {t('admin.clawback.history')}

{clawbackLogsTotal > 0 && ( - {(clawbackLogsPage - 1) * LOGS_PER_PAGE + 1}– - {Math.min(clawbackLogsPage * LOGS_PER_PAGE, clawbackLogsTotal)} of{' '} - {clawbackLogsTotal} + {t('admin.pagination.range', { + start: (clawbackLogsPage - 1) * LOGS_PER_PAGE + 1, + end: Math.min(clawbackLogsPage * LOGS_PER_PAGE, clawbackLogsTotal), + total: clawbackLogsTotal, + })} )}
@@ -889,19 +892,19 @@ export default function AdminPanel() { - - - - - - + + + + + + {clawbackLogs.length === 0 ? ( ) : ( @@ -911,7 +914,7 @@ export default function AdminPanel() { className="border-b border-hi/50 hover:bg-(--surface-hi) transition-colors" >
TimeFrom AccountAssetAmountTx HashReason{t('admin.labels.time')}{t('admin.labels.fromAccount')}{t('admin.labels.asset')}{t('admin.labels.amount')}{t('admin.labels.txHash')}{t('admin.labels.reason')}
- {clawbackLogsLoading ? 'Loading…' : 'No clawback records found.'} + {clawbackLogsLoading ? t('admin.actions.loading') : t('admin.clawback.empty')}
- {new Date(log.created_at).toLocaleString()} + {new Date(log.created_at).toLocaleString(i18n.language)} {log.from_account.slice(0, 8)}…{log.from_account.slice(-4)} @@ -938,7 +941,7 @@ export default function AdminPanel() {
{clawbackLogs.length === 0 ? (
- {clawbackLogsLoading ? 'Loading…' : 'No clawback records found.'} + {clawbackLogsLoading ? t('admin.actions.loading') : t('admin.clawback.empty')}
) : ( clawbackLogs.map((log: ClawbackLog) => ( @@ -949,21 +952,21 @@ export default function AdminPanel() {
- {new Date(log.created_at).toLocaleString()} + {new Date(log.created_at).toLocaleString(i18n.language)}
{log.from_account}
- Clawback + {t('admin.actions.clawback')}
- Asset: + {t('admin.labels.asset')}: {log.asset_code}
- Amount: + {t('admin.labels.amount')}: {log.amount}
@@ -971,11 +974,11 @@ export default function AdminPanel() { className="text-xs font-mono text-muted break-all" title={log.transaction_hash} > - Tx: {log.transaction_hash.slice(0, 16)}… + {t('admin.notifications.tx', { hash: `${log.transaction_hash.slice(0, 16)}…` })}
{log.reason && (
- Reason: + {t('admin.labels.reason')}:
{log.reason}
)} @@ -993,10 +996,10 @@ export default function AdminPanel() { className="flex items-center gap-1 px-4 py-2.5 text-xs border border-hi rounded hover:bg-black/20 disabled:opacity-40 disabled:cursor-not-allowed touch-manipulation min-h-[44px]" > {' '} - Previous + {t('admin.actions.previous')} - Page {clawbackLogsPage} of {clawbackTotalPages} + {t('admin.pagination.page', { page: clawbackLogsPage, total: clawbackTotalPages })} From 53c143da1151878f1871de0f1f33472c763556db Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 14:58:52 -0400 Subject: [PATCH 38/61] fix(i18n): localize contract upgrade registry --- .../src/components/ContractUpgradeTab.tsx | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/ContractUpgradeTab.tsx b/frontend/src/components/ContractUpgradeTab.tsx index c99bed60..77ab5fba 100644 --- a/frontend/src/components/ContractUpgradeTab.tsx +++ b/frontend/src/components/ContractUpgradeTab.tsx @@ -37,6 +37,7 @@ import { } from '../services/contractUpgrade'; import UpgradeConfirmModal from './UpgradeConfirmModal'; import { useNotification } from '../hooks/useNotification'; +import { useTranslation } from 'react-i18next'; // --------------------------------------------------------------------------- // Props @@ -78,6 +79,7 @@ function NetworkBadge({ network }: { network: string }) { /** Upgrade log status badge */ function StatusBadge({ status }: { status: UpgradeLog['status'] }) { + const { t } = useTranslation(); const colorMap: Record = { pending: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/30', simulated: 'bg-blue-500/10 text-blue-400 border-blue-500/30', @@ -91,7 +93,7 @@ function StatusBadge({ status }: { status: UpgradeLog['status'] }) { - {status} + {t(`contractUpgrade.status.${status}`)} ); } @@ -106,6 +108,7 @@ interface ContractCardProps { } function ContractCard({ contract, onUpgrade }: ContractCardProps) { + const { t, i18n } = useTranslation(); const [showHistory, setShowHistory] = useState(false); const [logs, setLogs] = useState([]); const [logsLoading, setLogsLoading] = useState(false); @@ -147,7 +150,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { className="shrink-0 flex items-center gap-1.5 px-3 py-2 bg-accent/15 text-accent border border-accent/30 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-accent hover:text-black transition-all" > - Upgrade + {t('contractUpgrade.upgrade')} @@ -156,7 +159,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {

- Contract ID + {t('contractUpgrade.contractId')}

{contract.contract_id} @@ -165,7 +168,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { target="_blank" rel="noreferrer" className="text-muted hover:text-accent transition-colors shrink-0" - title="View on Stellar Expert" + title={t('contractUpgrade.viewOnStellarExpert')} > @@ -174,7 +177,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {

- Current WASM Hash + {t('contractUpgrade.currentWasmHash')}

@@ -184,13 +187,13 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
- Last upgraded:{' '} + {t('contractUpgrade.lastUpgraded')}{' '} - {new Date(contract.last_upgraded_at).toLocaleString()} + {new Date(contract.last_upgraded_at).toLocaleString(i18n.language)} {contract.last_upgraded_by && ( <> - {' by '} + {' '}{t('contractUpgrade.by')}{' '} {contract.last_upgraded_by.slice(0, 8)}… )} @@ -208,7 +211,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { > - {logsLoading ? 'Loading history…' : 'Upgrade History'} + {logsLoading ? t('contractUpgrade.loadingHistory') : t('contractUpgrade.upgradeHistory')} {showHistory ? : } @@ -216,16 +219,16 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { {showHistory && (
{logs.length === 0 ? ( -

No upgrade history yet.

+

{t('contractUpgrade.noHistory')}

) : (
- - - - + + + + @@ -235,7 +238,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { className="border-b border-hi/40 hover:bg-(--surface-hi) transition-colors" >
DateNew HashStatusTX{t('contractUpgrade.date')}{t('contractUpgrade.newHash')}{t('contractUpgrade.statusLabel')}{t('contractUpgrade.tx')}
- {new Date(log.created_at).toLocaleDateString()} + {new Date(log.created_at).toLocaleDateString(i18n.language)} @@ -270,6 +273,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) { // --------------------------------------------------------------------------- export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabProps) { + const { t } = useTranslation(); const { notifyError } = useNotification(); const [contracts, setContracts] = useState([]); @@ -283,13 +287,13 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP setContracts(data); } catch (err: unknown) { notifyError( - 'Load Failed', - err instanceof Error ? err.message : 'Failed to load contract registry.' + t('contractUpgrade.loadFailed'), + err instanceof Error ? err.message : t('contractUpgrade.loadFailedBody') ); } finally { setLoading(false); } - }, [notifyError]); + }, [notifyError, t]); useEffect(() => { void loadContracts(); @@ -316,7 +320,7 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP {/* Section header */}

- Contract Registry + {t('contractUpgrade.registry')}

{/* Description */}

- Manage deployed Soroban smart contract upgrades. Each upgrade triggers a simulation, - multi-step confirmation, and on-chain execution followed by automated data migration. + {t('contractUpgrade.description')}

{/* No admin address warning */} @@ -339,7 +342,7 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP

- Connect your admin wallet to initiate contract upgrades. + {t('contractUpgrade.connectAdmin')}

)} @@ -361,9 +364,9 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP ) : contracts.length === 0 ? (
-

No contracts found in registry.

+

{t('contractUpgrade.noContracts')}

- Run the database migration to seed the contract registry. + {t('contractUpgrade.seedRegistry')}

) : ( From a31b462b82a22c1c861c24925e1faf50ae07a7f5 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:04:14 -0400 Subject: [PATCH 39/61] i18n: add payments and operations copy (en) --- frontend/src/locales/en/translation.json | 238 +++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 1f131665..3d9b64cb 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -764,5 +764,243 @@ "subtitle": "Manage employee token vesting schedules", "confirmCreate": "Confirm & Create Grant" } + }, + "withdrawalFlow": { + "selectMethod": "Select Withdrawal Method", + "selectMethodDescription": "Choose an anchor service to convert your ORGUSD to {{currency}}.", + "loadingAnchors": "Loading anchors...", + "noAnchors": "No withdrawal anchors available", + "varies": "Varies", + "back": "Back", + "amountOrgusd": "Amount (ORGUSD)", + "destinationType": "Destination Type", + "bankAccount": "Bank Account", + "mobileMoney": "Mobile Money", + "placeholders": { + "accountNumber": "Account Number", + "bankName": "Bank Name", + "phoneNumber": "Phone Number", + "provider": "Provider (e.g., M-Pesa, MTN)" + }, + "continue": "Continue", + "confirmWithdrawal": "Confirm Withdrawal", + "amount": "Amount", + "anchor": "Anchor", + "receive": "Receive", + "destination": "Destination", + "processing": "Processing...", + "processingTitle": "Processing Withdrawal", + "processingBody": "Please complete the withdrawal on the anchor's website.", + "openAnchor": "Open Anchor Website", + "status": "Status", + "transactionId": "Transaction ID", + "cancelWithdrawal": "Cancel Withdrawal", + "complete": "Withdrawal Complete", + "done": "Done", + "failed": "Withdrawal Failed", + "failureFallback": "Something went wrong with your withdrawal.", + "tryAgain": "Try Again", + "close": "Close", + "cashOut": "Cash Out" + }, + "crossAssetPayment": { + "waitingForSubmission": "Waiting for submission...", + "pathfindingFailed": "Pathfinding failed", + "pathfindingFailedBody": "Failed to fetch conversion paths.", + "updateStatus": "Update: {{status}}", + "paymentConfirmed": "Payment Confirmed", + "paymentConfirmedBody": "Your cross-asset payment was successful.", + "walletRequired": "Wallet required", + "walletRequiredBody": "Connect your wallet before submitting cross-asset payment.", + "noPath": "No path selected", + "noPathBody": "Select a conversion path before submitting.", + "invalidAmount": "Invalid amount", + "invalidAmountBody": "Enter a valid payment amount.", + "contractUnavailable": "Cross-asset contract ID is unavailable.", + "waitingForSettlement": "Submitted. Waiting for live settlement updates...", + "paymentSubmitted": "Payment submitted", + "paymentSubmittedBody": "On-chain transaction hash: {{hash}}", + "invocationFallback": "An unexpected error occurred during contract invocation.", + "paymentFailed": "Payment failed", + "paymentFailedBody": "A contract error occurred. Please review the details below.", + "title": "Cross-Asset Payment Settlement", + "subtitle": "Live pathfinding, Soroban simulation, and wallet-signed contract submission.", + "connectWallet": "Connect Wallet", + "sendAsset": "Send Asset", + "receiveAsset": "Receive Asset", + "amountToSend": "Amount to Send", + "receiverAddress": "Receiver Address", + "receiverPlaceholder": "G... recipient wallet", + "connectWalletToSwap": "Connect Wallet to Swap", + "simulateSubmit": "Simulate + Submit Payment", + "availablePaths": "Available Conversion Paths", + "fetchingPaths": "Fetching conversion paths...", + "settlementPreview": "Settlement Preview", + "expectedDelivery": "Expected Delivery", + "fee": "Fee", + "slippage": "Slippage", + "contractStatus": "Contract Status", + "authentication": "Authentication", + "signerReady": "Wallet connected and signer ready", + "initiation": "Initiation", + "simulatedSubmitted": "Contract call simulated and submitted", + "settlement": "Settlement", + "transactionHash": "Transaction Hash", + "changePrompt": "Change asset pair and amount to request path options from backend proxy." + }, + "payrollResidual": { + "scheduleConfigured": "Payroll schedule configured!", + "refId": "Ref ID: {{id}}", + "createFailed": "Failed to create schedule", + "tryAgainLater": "Please try again later.", + "paymentConfirmed": "Payment confirmed!", + "tx": "TX: {{id}}", + "missingRequired": "Missing required fields", + "missingRequiredBody": "Please provide employee name, amount, and wallet address.", + "trustlineMissing": "Trustline missing!", + "trustlineMissingBody": "{{name}} does not have a USDC trustline. You can proceed, but the payment will be held as a claimable balance.", + "claimFailed": "Failed to create claimable balance", + "invocationFailed": "Contract invocation failed", + "broadcastSuccess": "Broadcast successful!", + "broadcastSuccessBody": "Claimable balance created for {{name}}", + "broadcastFailed": "Broadcast failed", + "broadcastFailedBody": "A contract error occurred. Please review the details below.", + "scheduleCancelled": "Schedule cancelled", + "scheduleCancelledBody": "The automation has been halted.", + "cancellationFailed": "Cancellation failed", + "cancellationFailedBody": "Unable to reach the server.", + "openWizard": "Open scheduling wizard", + "automationActive": "Automation Active", + "nextRun": "Next Scheduled Run", + "employeePlaceholder": "e.g. Satoshi Nakamoto", + "simulating": "Simulating...", + "confirmBroadcast": "Confirm & Broadcast to Network", + "preflight": "Pre-flight Validation", + "trustlineMissingLabel": "Trustline Missing", + "trustlineWarning": "Employee does not have a USDC trustline. Payment will be held as a claimable balance.", + "preflightIntro": "All transactions are simulated via Stellar Horizon before submission. This catches common errors like:", + "preflightItems": { + "balance": "Insufficient XLM balance for fees", + "sequence": "Invalid sequence numbers", + "trustlines": "Missing trustlines for tokens", + "eligibility": "Account eligibility status" + }, + "scheduledAutomations": "Scheduled Automations", + "noSchedules": "No active payroll schedules found in the database.", + "nextRunLabel": "Next Run", + "recipients": "Recipients", + "cancelAutomation": "Cancel Automation", + "recentClaims": "Recent Manual Claims", + "noClaims": "No recent manual claimable balances.", + "cancel": "Cancel" + }, + "revenueSplit": { + "loadFailed": "Failed to load dashboard", + "loadFailedTitle": "Revenue split load failed", + "contractUnavailable": "Revenue split contract ID is unavailable.", + "walletRequired": "Wallet required", + "walletRequiredBody": "Connect a wallet before updating allocations.", + "invalidRecipient": "Invalid recipient", + "invalidRecipientBody": "Each allocation recipient must be a valid Stellar address.", + "invalidTotal": "Invalid allocation total", + "invalidTotalBody": "Allocation total must equal 100%. Current total: {{total}}%.", + "updated": "Allocations updated", + "updatedBody": "Submitted on-chain update transaction: {{hash}}", + "updateFailed": "Failed to update allocations", + "updateFailedTitle": "Allocation update failed", + "titlePrefix": "Revenue Split", + "titleHighlight": "Dashboard", + "subtitle": "Contract-backed allocations and distribution history", + "connectWallet": "Connect Wallet", + "loading": "Loading revenue split dashboard...", + "currentSplits": "Current Allocation Splits", + "noAllocations": "No allocation data loaded.", + "editAllocations": "Edit Allocations", + "addRecipient": "Add Recipient", + "recipientPlaceholder": "Recipient Stellar Address", + "remove": "Remove", + "totalMust100": "Total allocation must be exactly 100%.", + "submitting": "Submitting...", + "submitUpdate": "Submit On-Chain Update", + "liveBalances": "Live Recipient Balances", + "noDistributions": "No recipient distributions available yet.", + "history": "Historical Distribution Events", + "noEvents": "No backend indexed distribution events found.", + "date": "Date", + "recipient": "Recipient", + "action": "Action", + "amount": "Amount", + "tx": "Tx", + "notAvailable": "N/A", + "recipientLabel": "Recipient:", + "amountLabel": "Amount:" + }, + "transactionHistory": { + "loadFailed": "Failed to load transaction history", + "loadMoreFailed": "Failed to load more history", + "titlePrefix": "Transaction", + "titleHighlight": "History", + "subtitle": "Unified classic + contract event timeline", + "searchPlaceholder": "Search tx hash / actor", + "allStatuses": "All Statuses", + "confirmed": "Confirmed", + "pending": "Pending", + "failed": "Failed", + "employeePlaceholder": "Employee", + "assetPlaceholder": "Asset (USDC, XLM...)", + "empty": "No records found for current filters.", + "loading": "Loading...", + "loadMore": "Load More" + }, + "bulkPaymentTracker": { + "status": { + "confirmed": "Confirmed", + "pending": "Pending", + "partial": "Partial", + "failed": "Failed" + }, + "perRecipient": "Per-Recipient Breakdown", + "recipientCount_one": "{{count}} recipient", + "recipientCount_other": "{{count}} recipients", + "employee": "Employee", + "amount": "Amount", + "statusLabel": "Status", + "txHash": "Tx Hash", + "details": "Details", + "retryFailedBatch": "Retry failed batch", + "retryBatch": "Retry batch {{id}}", + "retry": "Retry", + "collapseRow": "Collapse row", + "expandRow": "Expand row", + "titlePrefix": "Bulk Payment", + "titleHighlight": "Status Tracker", + "subtitle": "Real-time on-chain confirmation for batch payroll runs", + "live": "Live", + "offline": "Offline", + "filterStatus": "Filter by status", + "allStatuses": "All Statuses", + "refresh": "Refresh", + "refreshAria": "Refresh batch list", + "totalBatches": "Total Batches", + "inProgress": "In Progress", + "recipients": "Recipients", + "volumePage": "Volume (page)", + "date": "Date", + "employees": "Employees", + "totalAmount": "Total Amount", + "confirmations": "Confs", + "actions": "Actions", + "empty": "No batch runs found", + "emptyFiltered": "No batches with status \"{{status}}\". Try a different filter.", + "emptyDefault": "Payroll batch runs will appear here once the first bulk payment is submitted.", + "previousPage": "Previous page", + "page": "Page {{page}}", + "nextPage": "Next page" + }, + "countdownTimer": { + "days": "Days", + "hours": "Hrs", + "minutes": "Min", + "seconds": "Sec" } } From 2457fdb3a4efde2a977336338992b0018fb1f67f Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:04:17 -0400 Subject: [PATCH 40/61] i18n: add payments and operations copy (es) --- frontend/src/locales/es/translation.json | 238 +++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index c32d58ba..749bdbb2 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -764,5 +764,243 @@ "subtitle": "Administra los calendarios de consolidación de tokens de empleados", "confirmCreate": "Confirmar y crear concesión" } + }, + "withdrawalFlow": { + "selectMethod": "Seleccionar método de retiro", + "selectMethodDescription": "Elige un servicio ancla para convertir tu ORGUSD a {{currency}}.", + "loadingAnchors": "Cargando anclas...", + "noAnchors": "No hay anclas de retiro disponibles", + "varies": "Varía", + "back": "Atrás", + "amountOrgusd": "Importe (ORGUSD)", + "destinationType": "Tipo de destino", + "bankAccount": "Cuenta bancaria", + "mobileMoney": "Dinero móvil", + "placeholders": { + "accountNumber": "Número de cuenta", + "bankName": "Nombre del banco", + "phoneNumber": "Número de teléfono", + "provider": "Proveedor (p. ej., M-Pesa, MTN)" + }, + "continue": "Continuar", + "confirmWithdrawal": "Confirmar retiro", + "amount": "Importe", + "anchor": "Ancla", + "receive": "Recibir", + "destination": "Destino", + "processing": "Procesando...", + "processingTitle": "Procesando retiro", + "processingBody": "Completa el retiro en el sitio web del ancla.", + "openAnchor": "Abrir sitio web del ancla", + "status": "Estado", + "transactionId": "ID de transacción", + "cancelWithdrawal": "Cancelar retiro", + "complete": "Retiro completado", + "done": "Listo", + "failed": "Error en el retiro", + "failureFallback": "Se produjo un error con tu retiro.", + "tryAgain": "Intentar de nuevo", + "close": "Cerrar", + "cashOut": "Retirar" + }, + "crossAssetPayment": { + "waitingForSubmission": "Esperando el envío...", + "pathfindingFailed": "Falló la búsqueda de ruta", + "pathfindingFailedBody": "No se pudieron obtener las rutas de conversión.", + "updateStatus": "Actualización: {{status}}", + "paymentConfirmed": "Pago confirmado", + "paymentConfirmedBody": "Tu pago entre activos se completó correctamente.", + "walletRequired": "Billetera necesaria", + "walletRequiredBody": "Conecta tu billetera antes de enviar el pago entre activos.", + "noPath": "No hay ruta seleccionada", + "noPathBody": "Selecciona una ruta de conversión antes de enviar.", + "invalidAmount": "Importe no válido", + "invalidAmountBody": "Introduce un importe de pago válido.", + "contractUnavailable": "El ID del contrato de pago entre activos no está disponible.", + "waitingForSettlement": "Enviado. Esperando actualizaciones de liquidación en vivo...", + "paymentSubmitted": "Pago enviado", + "paymentSubmittedBody": "Hash de transacción en cadena: {{hash}}", + "invocationFallback": "Se produjo un error inesperado al invocar el contrato.", + "paymentFailed": "Error en el pago", + "paymentFailedBody": "Se produjo un error de contrato. Revisa los detalles a continuación.", + "title": "Liquidación de pagos entre activos", + "subtitle": "Búsqueda de rutas en vivo, simulación Soroban y envío de contrato firmado por la billetera.", + "connectWallet": "Conectar billetera", + "sendAsset": "Activo a enviar", + "receiveAsset": "Activo a recibir", + "amountToSend": "Importe a enviar", + "receiverAddress": "Dirección del destinatario", + "receiverPlaceholder": "G... billetera del destinatario", + "connectWalletToSwap": "Conecta la billetera para intercambiar", + "simulateSubmit": "Simular + enviar pago", + "availablePaths": "Rutas de conversión disponibles", + "fetchingPaths": "Obteniendo rutas de conversión...", + "settlementPreview": "Vista previa de liquidación", + "expectedDelivery": "Entrega esperada", + "fee": "Comisión", + "slippage": "Deslizamiento", + "contractStatus": "Estado del contrato", + "authentication": "Autenticación", + "signerReady": "Billetera conectada y firmante listo", + "initiation": "Inicio", + "simulatedSubmitted": "Llamada al contrato simulada y enviada", + "settlement": "Liquidación", + "transactionHash": "Hash de transacción", + "changePrompt": "Cambia el par de activos y el importe para solicitar opciones de ruta al proxy del backend." + }, + "payrollResidual": { + "scheduleConfigured": "¡Programación de nómina configurada!", + "refId": "ID de referencia: {{id}}", + "createFailed": "No se pudo crear la programación", + "tryAgainLater": "Inténtalo de nuevo más tarde.", + "paymentConfirmed": "¡Pago confirmado!", + "tx": "TX: {{id}}", + "missingRequired": "Faltan campos obligatorios", + "missingRequiredBody": "Indica el nombre del empleado, el importe y la dirección de billetera.", + "trustlineMissing": "¡Falta la línea de confianza!", + "trustlineMissingBody": "{{name}} no tiene una línea de confianza USDC. Puedes continuar, pero el pago quedará retenido como saldo reclamable.", + "claimFailed": "No se pudo crear el saldo reclamable", + "invocationFailed": "Falló la invocación del contrato", + "broadcastSuccess": "¡Transmisión correcta!", + "broadcastSuccessBody": "Saldo reclamable creado para {{name}}", + "broadcastFailed": "Falló la transmisión", + "broadcastFailedBody": "Se produjo un error de contrato. Revisa los detalles a continuación.", + "scheduleCancelled": "Programación cancelada", + "scheduleCancelledBody": "La automatización se detuvo.", + "cancellationFailed": "Falló la cancelación", + "cancellationFailedBody": "No se pudo contactar con el servidor.", + "openWizard": "Abrir asistente de programación", + "automationActive": "Automatización activa", + "nextRun": "Próxima ejecución programada", + "employeePlaceholder": "p. ej., Satoshi Nakamoto", + "simulating": "Simulando...", + "confirmBroadcast": "Confirmar y transmitir a la red", + "preflight": "Validación previa", + "trustlineMissingLabel": "Falta la línea de confianza", + "trustlineWarning": "El empleado no tiene una línea de confianza USDC. El pago quedará retenido como saldo reclamable.", + "preflightIntro": "Todas las transacciones se simulan mediante Stellar Horizon antes del envío. Esto detecta errores habituales como:", + "preflightItems": { + "balance": "Saldo XLM insuficiente para comisiones", + "sequence": "Números de secuencia no válidos", + "trustlines": "Faltan líneas de confianza para tokens", + "eligibility": "Estado de elegibilidad de la cuenta" + }, + "scheduledAutomations": "Automatizaciones programadas", + "noSchedules": "No se encontraron programaciones de nómina activas en la base de datos.", + "nextRunLabel": "Próxima ejecución", + "recipients": "Destinatarios", + "cancelAutomation": "Cancelar automatización", + "recentClaims": "Saldos reclamables manuales recientes", + "noClaims": "No hay saldos reclamables manuales recientes.", + "cancel": "Cancelar" + }, + "revenueSplit": { + "loadFailed": "No se pudo cargar el panel", + "loadFailedTitle": "Falló la carga del reparto de ingresos", + "contractUnavailable": "El ID del contrato de reparto de ingresos no está disponible.", + "walletRequired": "Billetera necesaria", + "walletRequiredBody": "Conecta una billetera antes de actualizar las asignaciones.", + "invalidRecipient": "Destinatario no válido", + "invalidRecipientBody": "Cada destinatario de asignación debe ser una dirección Stellar válida.", + "invalidTotal": "Total de asignación no válido", + "invalidTotalBody": "El total de asignación debe ser 100 %. Total actual: {{total}} %.", + "updated": "Asignaciones actualizadas", + "updatedBody": "Transacción de actualización en cadena enviada: {{hash}}", + "updateFailed": "No se pudieron actualizar las asignaciones", + "updateFailedTitle": "Falló la actualización de asignaciones", + "titlePrefix": "Reparto de ingresos", + "titleHighlight": "Panel", + "subtitle": "Asignaciones respaldadas por contrato e historial de distribuciones", + "connectWallet": "Conectar billetera", + "loading": "Cargando panel de reparto de ingresos...", + "currentSplits": "Repartos de asignación actuales", + "noAllocations": "No se cargaron datos de asignación.", + "editAllocations": "Editar asignaciones", + "addRecipient": "Añadir destinatario", + "recipientPlaceholder": "Dirección Stellar del destinatario", + "remove": "Eliminar", + "totalMust100": "La asignación total debe ser exactamente 100 %.", + "submitting": "Enviando...", + "submitUpdate": "Enviar actualización en cadena", + "liveBalances": "Saldos de destinatarios en vivo", + "noDistributions": "Aún no hay distribuciones de destinatarios.", + "history": "Eventos históricos de distribución", + "noEvents": "No se encontraron eventos de distribución indexados por el backend.", + "date": "Fecha", + "recipient": "Destinatario", + "action": "Acción", + "amount": "Importe", + "tx": "Tx", + "notAvailable": "N/D", + "recipientLabel": "Destinatario:", + "amountLabel": "Importe:" + }, + "transactionHistory": { + "loadFailed": "No se pudo cargar el historial de transacciones", + "loadMoreFailed": "No se pudo cargar más historial", + "titlePrefix": "Historial de", + "titleHighlight": "transacciones", + "subtitle": "Cronología unificada de eventos clásicos y de contrato", + "searchPlaceholder": "Buscar hash de transacción / actor", + "allStatuses": "Todos los estados", + "confirmed": "Confirmado", + "pending": "Pendiente", + "failed": "Fallido", + "employeePlaceholder": "Empleado", + "assetPlaceholder": "Activo (USDC, XLM...)", + "empty": "No se encontraron registros para los filtros actuales.", + "loading": "Cargando...", + "loadMore": "Cargar más" + }, + "bulkPaymentTracker": { + "status": { + "confirmed": "Confirmado", + "pending": "Pendiente", + "partial": "Parcial", + "failed": "Fallido" + }, + "perRecipient": "Desglose por destinatario", + "recipientCount_one": "{{count}} destinatario", + "recipientCount_other": "{{count}} destinatarios", + "employee": "Empleado", + "amount": "Importe", + "statusLabel": "Estado", + "txHash": "Hash de transacción", + "details": "Detalles", + "retryFailedBatch": "Reintentar lote fallido", + "retryBatch": "Reintentar lote {{id}}", + "retry": "Reintentar", + "collapseRow": "Contraer fila", + "expandRow": "Expandir fila", + "titlePrefix": "Pago masivo", + "titleHighlight": "Seguimiento de estado", + "subtitle": "Confirmación en cadena en tiempo real para lotes de nómina", + "live": "En vivo", + "offline": "Sin conexión", + "filterStatus": "Filtrar por estado", + "allStatuses": "Todos los estados", + "refresh": "Actualizar", + "refreshAria": "Actualizar lista de lotes", + "totalBatches": "Total de lotes", + "inProgress": "En curso", + "recipients": "Destinatarios", + "volumePage": "Volumen (página)", + "date": "Fecha", + "employees": "Empleados", + "totalAmount": "Importe total", + "confirmations": "Conf.", + "actions": "Acciones", + "empty": "No se encontraron ejecuciones por lotes", + "emptyFiltered": "No hay lotes con el estado \"{{status}}\". Prueba otro filtro.", + "emptyDefault": "Las ejecuciones de nómina por lotes aparecerán aquí después de enviar el primer pago masivo.", + "previousPage": "Página anterior", + "page": "Página {{page}}", + "nextPage": "Página siguiente" + }, + "countdownTimer": { + "days": "Días", + "hours": "Hrs", + "minutes": "Min", + "seconds": "Seg" } } From f58fb8281c732adb980bac2b8c00982c3561b48f Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:04:44 -0400 Subject: [PATCH 41/61] i18n: complete payments ops labels (en) --- frontend/src/locales/en/translation.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 3d9b64cb..a574267e 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -933,7 +933,11 @@ "tx": "Tx", "notAvailable": "N/A", "recipientLabel": "Recipient:", - "amountLabel": "Amount:" + "amountLabel": "Amount:", + "connected": "Connected: {{address}}", + "total": "Total: {{total}}%", + "totalDistributed": "Total Distributed:", + "viewTransaction": "View Transaction: {{hash}}" }, "transactionHistory": { "loadFailed": "Failed to load transaction history", @@ -950,7 +954,16 @@ "assetPlaceholder": "Asset (USDC, XLM...)", "empty": "No records found for current filters.", "loading": "Loading...", - "loadMore": "Load More" + "loadMore": "Load More", + "filters": "Filters", + "actor": "Actor:", + "amountLabel": "Amount:", + "status": { + "confirmed": "Confirmed", + "pending": "Pending", + "failed": "Failed", + "indexed": "Indexed" + } }, "bulkPaymentTracker": { "status": { From 7c94509313ebb056e32b9884545029dbcf7124bc Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:04:47 -0400 Subject: [PATCH 42/61] i18n: complete payments ops labels (es) --- frontend/src/locales/es/translation.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 749bdbb2..bd7cfebe 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -933,7 +933,11 @@ "tx": "Tx", "notAvailable": "N/D", "recipientLabel": "Destinatario:", - "amountLabel": "Importe:" + "amountLabel": "Importe:", + "connected": "Conectado: {{address}}", + "total": "Total: {{total}} %", + "totalDistributed": "Total distribuido:", + "viewTransaction": "Ver transacción: {{hash}}" }, "transactionHistory": { "loadFailed": "No se pudo cargar el historial de transacciones", @@ -950,7 +954,16 @@ "assetPlaceholder": "Activo (USDC, XLM...)", "empty": "No se encontraron registros para los filtros actuales.", "loading": "Cargando...", - "loadMore": "Cargar más" + "loadMore": "Cargar más", + "filters": "Filtros", + "actor": "Actor:", + "amountLabel": "Importe:", + "status": { + "confirmed": "Confirmado", + "pending": "Pendiente", + "failed": "Fallido", + "indexed": "Indexado" + } }, "bulkPaymentTracker": { "status": { From f7ea5847fad64062a2476ed71d6b7c68d473ce4f Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:05:12 -0400 Subject: [PATCH 43/61] fix(i18n): localize countdown timer --- frontend/src/components/CountdownTimer.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/CountdownTimer.tsx b/frontend/src/components/CountdownTimer.tsx index 061e6432..c934f728 100644 --- a/frontend/src/components/CountdownTimer.tsx +++ b/frontend/src/components/CountdownTimer.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; export const CountdownTimer = ({ targetDate }: { targetDate: Date | null }) => { + const { t } = useTranslation(); const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, @@ -40,28 +42,28 @@ export const CountdownTimer = ({ targetDate }: { targetDate: Date | null }) => {
{timeLeft.days} - Days + {t('countdownTimer.days')}
:
{timeLeft.hours.toString().padStart(2, '0')} - Hrs + {t('countdownTimer.hours')}
:
{timeLeft.minutes.toString().padStart(2, '0')} - Min + {t('countdownTimer.minutes')}
:
{timeLeft.seconds.toString().padStart(2, '0')} - Sec + {t('countdownTimer.seconds')}
); From 7f437109065891bf09e7809d4cd9e06173b1c3ef Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:05:57 -0400 Subject: [PATCH 44/61] fix(i18n): localize transaction history --- frontend/src/pages/TransactionHistory.tsx | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/TransactionHistory.tsx b/frontend/src/pages/TransactionHistory.tsx index ffa01e77..055f6493 100644 --- a/frontend/src/pages/TransactionHistory.tsx +++ b/frontend/src/pages/TransactionHistory.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Activity, Calendar, Filter, Search } from 'lucide-react'; import { fetchHistoryPage, @@ -44,6 +45,7 @@ function TimelineSkeleton() { } export default function TransactionHistory() { + const { t, i18n } = useTranslation(); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [debouncedFilters, setDebouncedFilters] = useState(DEFAULT_FILTERS); const [items, setItems] = useState([]); @@ -79,7 +81,7 @@ export default function TransactionHistory() { setHasMore(result.hasMore); } catch (loadError) { setError( - loadError instanceof Error ? loadError.message : 'Failed to load transaction history' + loadError instanceof Error ? loadError.message : t('transactionHistory.loadFailed') ); } finally { setIsLoading(false); @@ -87,7 +89,7 @@ export default function TransactionHistory() { }; void load(); - }, [debouncedFilters]); + }, [debouncedFilters, t]); const activeFilterCount = useMemo( () => (Object.values(filters) as string[]).filter((value) => value.trim().length > 0).length, @@ -107,7 +109,7 @@ export default function TransactionHistory() { setPage(nextPage); setHasMore(result.hasMore); } catch (loadError) { - setError(loadError instanceof Error ? loadError.message : 'Failed to load more history'); + setError(loadError instanceof Error ? loadError.message : t('transactionHistory.loadMoreFailed')); } finally { setIsLoadingMore(false); } @@ -118,10 +120,10 @@ export default function TransactionHistory() {

- Transaction History + {t('transactionHistory.titlePrefix')} {t('transactionHistory.titleHighlight')}

- Unified classic + contract event timeline + {t('transactionHistory.subtitle')}

@@ -143,7 +145,7 @@ export default function TransactionHistory() { onChange={(event) => setFilters((prev) => ({ ...prev, search: event.target.value })) } - placeholder="Search tx hash / actor" + placeholder={t('transactionHistory.searchPlaceholder')} className="w-full bg-[#0a0a0c] border border-(--border) rounded-lg py-2.5 pl-10 pr-4 text-sm" /> @@ -153,10 +155,10 @@ export default function TransactionHistory() { onChange={(event) => setFilters((prev) => ({ ...prev, status: event.target.value }))} className="bg-[#0a0a0c] border border-(--border) rounded-lg px-3 py-2.5 text-sm" > - - - - + + + + setFilters((prev) => ({ ...prev, employee: event.target.value })) } - placeholder="Employee" + placeholder={t('transactionHistory.employeePlaceholder')} className="bg-[#0a0a0c] border border-(--border) rounded-lg px-3 py-2.5 text-sm" /> setFilters((prev) => ({ ...prev, asset: event.target.value }))} - placeholder="Asset (USDC, XLM...)" + placeholder={t('transactionHistory.assetPlaceholder')} className="bg-[#0a0a0c] border border-(--border) rounded-lg px-3 py-2.5 text-sm" /> @@ -209,7 +211,7 @@ export default function TransactionHistory() { {!isLoading && items.length === 0 ? (
-

No records found for current filters.

+

{t('transactionHistory.empty')}

) : null} @@ -227,16 +229,16 @@ export default function TransactionHistory() { - {item.status} + {t(`transactionHistory.status.${item.status}`, { defaultValue: item.status })} - {new Date(item.createdAt).toLocaleString()} + {new Date(item.createdAt).toLocaleString(i18n.language)}

{item.label}

-

Actor: {item.actor}

+

{t('transactionHistory.actor')} {item.actor}

- Amount: {item.amount} {item.asset} + {t('transactionHistory.amountLabel')} {item.amount} {item.asset}

{item.txHash ? ( <> @@ -262,7 +264,7 @@ export default function TransactionHistory() { disabled={isLoadingMore} className="px-6 py-3 rounded-lg bg-(--surface-hi) hover:bg-(--border) text-sm font-semibold disabled:opacity-70 transition-colors touch-manipulation min-h-[44px]" > - {isLoadingMore ? 'Loading...' : 'Load More'} + {isLoadingMore ? t('transactionHistory.loading') : t('transactionHistory.loadMore')} ) : null} From fecbbd3e4ba3a50bec35db702ba660a7a5e4b1f2 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:06:27 -0400 Subject: [PATCH 45/61] fix(i18n): localize revenue split dashboard --- frontend/src/pages/RevenueSplitDashboard.tsx | 88 ++++++++++---------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/RevenueSplitDashboard.tsx b/frontend/src/pages/RevenueSplitDashboard.tsx index 2d1f584a..0bafc372 100644 --- a/frontend/src/pages/RevenueSplitDashboard.tsx +++ b/frontend/src/pages/RevenueSplitDashboard.tsx @@ -1,5 +1,6 @@ // RevenueSplitDashboard component import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNotification } from '../hooks/useNotification.js'; import { useWallet } from '../hooks/useWallet.js'; import { useWalletSigning } from '../hooks/useWalletSigning.js'; @@ -36,6 +37,7 @@ function buildConicGradient(allocations: RevenueAllocation[]): string { } export default function RevenueSplitDashboard() { + const { t, i18n } = useTranslation(); const [allocations, setAllocations] = useState([]); const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -93,7 +95,7 @@ export default function RevenueSplitDashboard() { (import.meta.env.VITE_REVENUE_SPLIT_CONTRACT_ID as string | undefined); if (!contractId) { - throw new Error('Revenue split contract ID is unavailable.'); + throw new Error(t('revenueSplit.contractUnavailable')); } if (!address) { @@ -106,16 +108,16 @@ export default function RevenueSplitDashboard() { const distributionEvents = await fetchDistributionEvents(ORGANIZATION_ID, 1, 50); setEvents(distributionEvents); } catch (loadError) { - const message = loadError instanceof Error ? loadError.message : 'Failed to load dashboard'; + const message = loadError instanceof Error ? loadError.message : t('revenueSplit.loadFailed'); setError(message); - notifyError('Revenue split load failed', message); + notifyError(t('revenueSplit.loadFailedTitle'), message); } finally { setIsLoading(false); } }; void loadData(); - }, [address, notifyError]); + }, [address, notifyError, t]); const setAllocationField = (index: number, field: 'recipient' | 'percentage', value: string) => { setAllocations((prev) => @@ -140,23 +142,23 @@ export default function RevenueSplitDashboard() { const handleSaveAllocations = async () => { if (!address) { - notifyError('Wallet required', 'Connect a wallet before updating allocations.'); + notifyError(t('revenueSplit.walletRequired'), t('revenueSplit.walletRequiredBody')); return; } const hasInvalidAddress = allocations.some((entry) => !isLikelyStellarAddress(entry.recipient)); if (hasInvalidAddress) { notifyError( - 'Invalid recipient', - 'Each allocation recipient must be a valid Stellar address.' + t('revenueSplit.invalidRecipient'), + t('revenueSplit.invalidRecipientBody') ); return; } if (Math.abs(totalAllocation - 100) > 0.0001) { notifyError( - 'Invalid allocation total', - `Allocation total must equal 100%. Current total: ${totalAllocation.toFixed(2)}%.` + t('revenueSplit.invalidTotal'), + t('revenueSplit.invalidTotalBody', { total: totalAllocation.toFixed(2) }) ); return; } @@ -168,7 +170,7 @@ export default function RevenueSplitDashboard() { contractService.getContractId('revenue_split', 'testnet') || (import.meta.env.VITE_REVENUE_SPLIT_CONTRACT_ID as string | undefined); if (!contractId) { - throw new Error('Revenue split contract ID is unavailable.'); + throw new Error(t('revenueSplit.contractUnavailable')); } const { txHash } = await updateRevenueAllocations({ @@ -178,11 +180,11 @@ export default function RevenueSplitDashboard() { signTransaction: sign, }); - notifySuccess('Allocations updated', `Submitted on-chain update transaction: ${txHash}`); + notifySuccess(t('revenueSplit.updated'), t('revenueSplit.updatedBody', { hash: txHash })); } catch (saveError) { const message = - saveError instanceof Error ? saveError.message : 'Failed to update allocations'; - notifyError('Allocation update failed', message); + saveError instanceof Error ? saveError.message : t('revenueSplit.updateFailed'); + notifyError(t('revenueSplit.updateFailedTitle'), message); } finally { setIsSaving(false); } @@ -193,10 +195,10 @@ export default function RevenueSplitDashboard() {

- Revenue Split Dashboard + {t('revenueSplit.titlePrefix')} {t('revenueSplit.titleHighlight')}

- Contract-backed allocations and distribution history + {t('revenueSplit.subtitle')}

{!address ? ( @@ -208,23 +210,23 @@ export default function RevenueSplitDashboard() { className="px-4 py-2 rounded-full font-bold bg-accent text-on-accent touch-manipulation" style={{ minHeight: '44px' }} > - Connect Wallet + {t('revenueSplit.connectWallet')} ) : ( - Connected: {address.slice(0, 6)}...{address.slice(-4)} + {t('revenueSplit.connected', { address: `${address.slice(0, 6)}...${address.slice(-4)}` })} )}
{isLoading ? ( -

Loading revenue split dashboard...

+

{t('revenueSplit.loading')}

) : null} {error ?

{error}

: null}
-

Current Allocation Splits

+

{t('revenueSplit.currentSplits')}

{allocations.length === 0 ? ( -

No allocation data loaded.

+

{t('revenueSplit.noAllocations')}

) : ( allocations.map((entry, idx) => ( // eslint-disable-next-line react-x/no-array-index-key @@ -245,7 +247,7 @@ export default function RevenueSplitDashboard() {

- Total: {totalAllocation.toFixed(2)}% + {t('revenueSplit.total', { total: totalAllocation.toFixed(2) })}

@@ -253,14 +255,14 @@ export default function RevenueSplitDashboard() {
-

Edit Allocations

+

{t('revenueSplit.editAllocations')}

@@ -275,7 +277,7 @@ export default function RevenueSplitDashboard() { type="text" value={entry.recipient} onChange={(event) => setAllocationField(idx, 'recipient', event.target.value)} - placeholder="Recipient Stellar Address" + placeholder={t('revenueSplit.recipientPlaceholder')} className="md:col-span-8 bg-[#0a0a0c] border border-(--border) rounded-lg px-3 py-2 text-xs" style={{ minHeight: '44px' }} /> @@ -296,14 +298,14 @@ export default function RevenueSplitDashboard() { className="md:col-span-1 text-red-400 text-xs font-semibold touch-manipulation p-2" style={{ minHeight: '44px' }} > - Remove + {t('revenueSplit.remove')}
))}
-

Total allocation must be exactly 100%.

+

{t('revenueSplit.totalMust100')}

@@ -321,9 +323,9 @@ export default function RevenueSplitDashboard() {
-

Live Recipient Balances

+

{t('revenueSplit.liveBalances')}

{recipientBalances.length === 0 ? ( -

No recipient distributions available yet.

+

{t('revenueSplit.noDistributions')}

) : (
{recipientBalances.map((row) => ( @@ -342,15 +344,15 @@ export default function RevenueSplitDashboard() {
)}

- Total Distributed:{' '} + {t('revenueSplit.totalDistributed')}{' '} {formatAmount(totalDistributed, preferredStablecoin)}

-

Historical Distribution Events

+

{t('revenueSplit.history')}

{events.length === 0 ? ( -

No backend indexed distribution events found.

+

{t('revenueSplit.noEvents')}

) : ( <> {/* Desktop Table View */} @@ -358,18 +360,18 @@ export default function RevenueSplitDashboard() { - - - - - + + + + + {events.map((event) => ( @@ -387,7 +389,7 @@ export default function RevenueSplitDashboard() { {event.txHash.slice(0, 10)}... ) : ( - N/A + {t('revenueSplit.notAvailable')} )} @@ -405,7 +407,7 @@ export default function RevenueSplitDashboard() { >
- {new Date(event.createdAt).toLocaleDateString()} + {new Date(event.createdAt).toLocaleDateString(i18n.language)} {event.action} @@ -413,13 +415,13 @@ export default function RevenueSplitDashboard() {
- Recipient: + {t('revenueSplit.recipientLabel')} {event.recipientLabel}
- Amount: + {t('revenueSplit.amountLabel')} {formatAmount(event.amount, event.assetCode || preferredStablecoin)} @@ -432,7 +434,7 @@ export default function RevenueSplitDashboard() { rel="noreferrer" className="text-accent text-xs hover:underline" > - View Transaction: {event.txHash.slice(0, 8)}... + {t('revenueSplit.viewTransaction', { hash: `${event.txHash.slice(0, 8)}...` })}
)} From c48a0c59a4b80ef77b928a5b23ddff52b437fe73 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:06:50 -0400 Subject: [PATCH 46/61] fix(ci): drop invalid top-level retention-days from workflows GitHub Actions rejects retention-days at workflow root, so build, secrets-check, contract-release, and dapp-ipfs failed before any job started. Artifact retention stays on the upload-artifact steps that already set it. --- .github/workflows/build.yml | 5 +---- .github/workflows/contract-release.yml | 5 +---- .github/workflows/dapp-ipfs.yml | 5 +---- .github/workflows/secrets-check.yml | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52029992..7c034aad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: branches: ["main"] types: [opened, synchronize, reopened, ready_for_review] -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo setting, not a workflow key. Artifact steps set their own retention-days. env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig @@ -113,6 +113,3 @@ jobs: - name: Run Tests working-directory: ./frontend run: npm test --if-present - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index 2655fcbc..aeb66051 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -5,7 +5,7 @@ on: tags: - "v*" -# Retention policy: Keep successful runs for 90 days, failed/cancelled for 14 days +# Retention is a repo setting, not a workflow key. permissions: # required permissions for the workflow id-token: write contents: write # in order to create releases @@ -27,6 +27,3 @@ jobs: package: "..." secrets: release_token: ${{ secrets.GITHUB_TOKEN }} - -# Workflow run retention settings -retention-days: 90 \ No newline at end of file diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 29d16e55..6cb6ed9f 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo setting, not a workflow key. concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -65,6 +65,3 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- CID: ${{ steps.storacha.outputs.cid }}" >> "$GITHUB_STEP_SUMMARY" echo "- URL: ${{ steps.storacha.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index b8d3b7e8..387f7d09 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -10,7 +10,7 @@ on: paths: - "k8s/**" -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo setting, not a workflow key. jobs: check-secrets-placeholders: name: Verify no real secrets in k8s manifests @@ -20,6 +20,3 @@ jobs: - name: Check backend-secret.yaml for non-placeholder values run: ./scripts/check-k8s-secrets.sh - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file From 4ef3fe0015b034d1239d8c4f503db1c7363f1013 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:06:58 -0400 Subject: [PATCH 47/61] i18n: add remaining payments ops labels (en) --- frontend/src/locales/en/translation.json | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index a574267e..228122a2 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -801,7 +801,11 @@ "failureFallback": "Something went wrong with your withdrawal.", "tryAgain": "Try Again", "close": "Close", - "cashOut": "Cash Out" + "cashOut": "Cash Out", + "fee": "Fee:", + "minimum": "Min:", + "withdrawVia": "Withdraw via {{anchor}}", + "available": "Available: {{amount}}" }, "crossAssetPayment": { "waitingForSubmission": "Waiting for submission...", @@ -846,7 +850,8 @@ "simulatedSubmitted": "Contract call simulated and submitted", "settlement": "Settlement", "transactionHash": "Transaction Hash", - "changePrompt": "Change asset pair and amount to request path options from backend proxy." + "changePrompt": "Change asset pair and amount to request path options from backend proxy.", + "pathFeeSlippage": "Fee: {{fee}} {{asset}} | Slippage: {{slippage}}%" }, "payrollResidual": { "scheduleConfigured": "Payroll schedule configured!", @@ -892,7 +897,16 @@ "cancelAutomation": "Cancel Automation", "recentClaims": "Recent Manual Claims", "noClaims": "No recent manual claimable balances.", - "cancel": "Cancel" + "cancel": "Cancel", + "at": "at", + "amountLabel": "Amount:", + "scheduledLabel": "Scheduled:", + "toLabel": "To:", + "frequency": { + "weekly": "Weekly", + "biweekly": "Biweekly", + "monthly": "Monthly" + } }, "revenueSplit": { "loadFailed": "Failed to load dashboard", From 5ef523de205e08e7788f91fdd992515923783edc Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:07:01 -0400 Subject: [PATCH 48/61] i18n: add remaining payments ops labels (es) --- frontend/src/locales/es/translation.json | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index bd7cfebe..8b22455b 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -801,7 +801,11 @@ "failureFallback": "Se produjo un error con tu retiro.", "tryAgain": "Intentar de nuevo", "close": "Cerrar", - "cashOut": "Retirar" + "cashOut": "Retirar", + "fee": "Comisión:", + "minimum": "Mín.:", + "withdrawVia": "Retirar mediante {{anchor}}", + "available": "Disponible: {{amount}}" }, "crossAssetPayment": { "waitingForSubmission": "Esperando el envío...", @@ -846,7 +850,8 @@ "simulatedSubmitted": "Llamada al contrato simulada y enviada", "settlement": "Liquidación", "transactionHash": "Hash de transacción", - "changePrompt": "Cambia el par de activos y el importe para solicitar opciones de ruta al proxy del backend." + "changePrompt": "Cambia el par de activos y el importe para solicitar opciones de ruta al proxy del backend.", + "pathFeeSlippage": "Comisión: {{fee}} {{asset}} | Deslizamiento: {{slippage}} %" }, "payrollResidual": { "scheduleConfigured": "¡Programación de nómina configurada!", @@ -892,7 +897,16 @@ "cancelAutomation": "Cancelar automatización", "recentClaims": "Saldos reclamables manuales recientes", "noClaims": "No hay saldos reclamables manuales recientes.", - "cancel": "Cancelar" + "cancel": "Cancelar", + "at": "a las", + "amountLabel": "Importe:", + "scheduledLabel": "Programado:", + "toLabel": "A:", + "frequency": { + "weekly": "Semanal", + "biweekly": "Quincenal", + "monthly": "Mensual" + } }, "revenueSplit": { "loadFailed": "No se pudo cargar el panel", From 884813d4d7c876b3a66aef7422e27ff9deb7208a Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:07:50 -0400 Subject: [PATCH 49/61] fix(ci): drop invalid top-level retention-days from workflows GitHub Actions rejects retention-days at workflow root, so build, secrets-check, contract-release, and dapp-ipfs failed before any job started. Artifact retention stays on upload-artifact steps that already set it. From 3da2ebef2e884c2a3c24e36439372d9fc737be01 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:08:12 -0400 Subject: [PATCH 50/61] fix(i18n): localize withdrawal flow --- frontend/src/components/WithdrawalFlow.tsx | 85 +++++++++++----------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/WithdrawalFlow.tsx b/frontend/src/components/WithdrawalFlow.tsx index a19daaa6..8c71e994 100644 --- a/frontend/src/components/WithdrawalFlow.tsx +++ b/frontend/src/components/WithdrawalFlow.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { ArrowLeft, ArrowRight, @@ -29,8 +30,9 @@ const WithdrawalFlow: React.FC = ({ onClose, onSuccess, }) => { + const { t } = useTranslation(); const { - state, + state: setStep, selectAnchor, setAmount, @@ -72,20 +74,20 @@ const WithdrawalFlow: React.FC = ({ case 'select_anchor': return (
-

Select Withdrawal Method

+

{t('withdrawalFlow.selectMethod')}

- Choose an anchor service to convert your ORGUSD to {selectedCurrency}. + {t('withdrawalFlow.selectMethodDescription', { currency: selectedCurrency })}

{state.isLoading ? (
- Loading anchors... + {t('withdrawalFlow.loadingAnchors')}
) : state.anchors.length === 0 ? (
-

No withdrawal anchors available

+

{t('withdrawalFlow.noAnchors')}

) : (
@@ -99,7 +101,7 @@ const WithdrawalFlow: React.FC = ({
{anchor.name}
- Fee: {anchor.withdrawFee || 'Varies'} · Min:{' '} + {t('withdrawalFlow.fee')} {anchor.withdrawFee || t('withdrawalFlow.varies')} · {t('withdrawalFlow.minimum')}{' '} {anchor.withdrawMinAmount || 0} {selectedCurrency}
@@ -120,13 +122,13 @@ const WithdrawalFlow: React.FC = ({ className="flex items-center gap-2 text-sm text-[var(--muted)] hover:text-[var(--text)]" > - Back + {t('withdrawalFlow.back')} -

Withdraw via {state.selectedAnchor?.name}

+

{t('withdrawalFlow.withdrawVia', { anchor: state.selectedAnchor?.name })}

- + = ({ />
- Available: {formatCurrency(balance, 'USD')} + {t('withdrawalFlow.available', { amount: formatCurrency(balance, 'USD') })} ≈ {formatCurrency(state.estimatedReceive, selectedCurrency)} @@ -147,7 +149,7 @@ const WithdrawalFlow: React.FC = ({
- +
@@ -178,7 +180,7 @@ const WithdrawalFlow: React.FC = ({
setDestinationDetails((prev) => ({ ...prev, accountNumber: e.target.value })) @@ -187,7 +189,7 @@ const WithdrawalFlow: React.FC = ({ /> setDestinationDetails((prev) => ({ ...prev, bankName: e.target.value })) @@ -199,7 +201,7 @@ const WithdrawalFlow: React.FC = ({
setDestinationDetails((prev) => ({ ...prev, phoneNumber: e.target.value })) @@ -208,7 +210,7 @@ const WithdrawalFlow: React.FC = ({ /> setDestinationDetails((prev) => ({ ...prev, provider: e.target.value })) @@ -225,7 +227,7 @@ const WithdrawalFlow: React.FC = ({ } className="w-full p-3 rounded-lg bg-[var(--accent)] text-[var(--bg)] font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed" > - Continue + {t('withdrawalFlow.continue')}
); @@ -238,31 +240,31 @@ const WithdrawalFlow: React.FC = ({ className="flex items-center gap-2 text-sm text-[var(--muted)] hover:text-[var(--text)]" > - Back + {t('withdrawalFlow.back')} -

Confirm Withdrawal

+

{t('withdrawalFlow.confirmWithdrawal')}

- Amount + {t('withdrawalFlow.amount')} {formatCurrency(parseFloat(state.amount), 'USD')}
- Anchor + {t('withdrawalFlow.anchor')} {state.selectedAnchor?.name}
- Receive + {t('withdrawalFlow.receive')} ≈ {formatCurrency(state.estimatedReceive, selectedCurrency)}
- Destination - {destinationType.replace('_', ' ')} + {t('withdrawalFlow.destination')} + {destinationType === 'bank_account' ? t('withdrawalFlow.bankAccount') : t('withdrawalFlow.mobileMoney')}
@@ -274,10 +276,10 @@ const WithdrawalFlow: React.FC = ({ {state.isLoading ? ( - Processing... + {t('withdrawalFlow.processing')} ) : ( - 'Confirm Withdrawal' + t('withdrawalFlow.confirmWithdrawal') )}
@@ -290,9 +292,9 @@ const WithdrawalFlow: React.FC = ({
-

Processing Withdrawal

+

{t('withdrawalFlow.processingTitle')}

- Please complete the withdrawal on the anchor's website. + {t('withdrawalFlow.processingBody')}

- Status + {t('withdrawalFlow.status')} {state.transaction?.status.replace(/_/g, ' ')}
- Transaction ID + {t('withdrawalFlow.transactionId')} {state.transaction?.id}
@@ -321,7 +323,7 @@ const WithdrawalFlow: React.FC = ({ disabled={state.isLoading} className="text-sm text-[var(--muted)] hover:text-[var(--text)]" > - Cancel Withdrawal + {t('withdrawalFlow.cancelWithdrawal')}
); @@ -333,17 +335,16 @@ const WithdrawalFlow: React.FC = ({
-

Withdrawal Complete

+

{t('withdrawalFlow.complete')}

- Your withdrawal of {formatCurrency(parseFloat(state.amount), 'USD')} has been - processed successfully. + {t('withdrawalFlow.successProcessed', { amount: formatCurrency(parseFloat(state.amount), 'USD') })}

); @@ -355,9 +356,9 @@ const WithdrawalFlow: React.FC = ({ -

Withdrawal Failed

+

{t('withdrawalFlow.failed')}

- {state.error || 'Something went wrong with your withdrawal.'} + {state.error || t('withdrawalFlow.failureFallback')}

@@ -365,13 +366,13 @@ const WithdrawalFlow: React.FC = ({ onClick={handleReset} className="flex-1 p-3 rounded-lg border border-[var(--border)] hover:border-[var(--accent)]" > - Try Again + {t('withdrawalFlow.tryAgain')}
@@ -388,7 +389,7 @@ const WithdrawalFlow: React.FC = ({
{/* Header */}
-

Cash Out

+

{t('withdrawalFlow.cashOut')}

) : ( @@ -207,7 +209,7 @@ export default function CrossAssetPayment() {
setReceiver(e.target.value)} - placeholder="G... recipient wallet" + placeholder={t('crossAssetPayment.receiverPlaceholder')} className="w-full bg-[#0a0a0c] border border-(--border) rounded-xl px-4 py-3 outline-none" />
@@ -282,10 +284,10 @@ export default function CrossAssetPayment() { ) : !address ? ( <> - Connect Wallet to Swap + {t('crossAssetPayment.connectWalletToSwap')} ) : ( - 'Simulate + Submit Payment' + t('crossAssetPayment.simulateSubmit') )}
@@ -297,12 +299,12 @@ export default function CrossAssetPayment() {

- Available Conversion Paths + {t('crossAssetPayment.availablePaths')}

{isLoadingPaths ? (
- Fetching conversion paths... + {t('crossAssetPayment.fetchingPaths')}
) : (
@@ -319,12 +321,11 @@ export default function CrossAssetPayment() { {path.hops.join(' -> ')} - {path.rate.toFixed(4)} rate + {path.rate.toFixed(4)} {t('crossAssetPayment.rateLabel')}
- Fee: {path.fee.toFixed(4)} {assetOut} | Slippage:{' '} - {path.slippage.toFixed(2)}% + {t('crossAssetPayment.pathFeeSlippage', { fee: path.fee.toFixed(4), asset: assetOut, slippage: path.slippage.toFixed(2) })}
))} @@ -335,22 +336,22 @@ export default function CrossAssetPayment() { {selectedPath && (
-

Settlement Preview

+

{t('crossAssetPayment.settlementPreview')}

- Expected Delivery + {t('crossAssetPayment.expectedDelivery')} {selectedPath.estimatedDestinationAmount.toLocaleString()} {assetOut}
- Fee + {t('crossAssetPayment.fee')} {selectedPath.fee.toFixed(4)} {assetOut}
- Slippage + {t('crossAssetPayment.slippage')} {selectedPath.slippage.toFixed(2)}%
@@ -366,7 +367,7 @@ export default function CrossAssetPayment() { {status}
-

Contract Status

+

{t('crossAssetPayment.contractStatus')}

@@ -376,8 +377,8 @@ export default function CrossAssetPayment() {
-

Authentication

-

Wallet connected and signer ready

+

{t('crossAssetPayment.authentication')}

+

{t('crossAssetPayment.signerReady')}

@@ -392,9 +393,9 @@ export default function CrossAssetPayment() { )}
-

Initiation

+

{t('crossAssetPayment.initiation')}

- Contract call simulated and submitted + {t('crossAssetPayment.simulatedSubmitted')}

@@ -406,7 +407,7 @@ export default function CrossAssetPayment() {
-

Settlement

+

{t('crossAssetPayment.settlement')}

{liveStatusMessage}

@@ -415,7 +416,7 @@ export default function CrossAssetPayment() { {submissionTxHash && (

- Transaction Hash + {t('crossAssetPayment.transactionHash')}

{submissionTxHash}

@@ -427,7 +428,7 @@ export default function CrossAssetPayment() {

- Change asset pair and amount to request path options from backend proxy. + {t('crossAssetPayment.changePrompt')}

)} From b303a5c4049cd2b109f29caf5463a0e39d6bc63b Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:12:00 -0400 Subject: [PATCH 52/61] i18n: add remaining operations labels --- frontend/src/locales/en/translation.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 228122a2..cb175302 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -805,7 +805,8 @@ "fee": "Fee:", "minimum": "Min:", "withdrawVia": "Withdraw via {{anchor}}", - "available": "Available: {{amount}}" + "available": "Available: {{amount}}", + "successProcessed": "Your withdrawal of {{amount}} has been processed successfully." }, "crossAssetPayment": { "waitingForSubmission": "Waiting for submission...", @@ -851,7 +852,8 @@ "settlement": "Settlement", "transactionHash": "Transaction Hash", "changePrompt": "Change asset pair and amount to request path options from backend proxy.", - "pathFeeSlippage": "Fee: {{fee}} {{asset}} | Slippage: {{slippage}}%" + "pathFeeSlippage": "Fee: {{fee}} {{asset}} | Slippage: {{slippage}}%", + "rateLabel": "rate" }, "payrollResidual": { "scheduleConfigured": "Payroll schedule configured!", @@ -906,7 +908,11 @@ "weekly": "Weekly", "biweekly": "Biweekly", "monthly": "Monthly" - } + }, + "scheduledToRun": "Scheduled to run", + "broadcasting": "Broadcasting...", + "distribution": "Distribution", + "employees": "Employees" }, "revenueSplit": { "loadFailed": "Failed to load dashboard", From dd8033c151054d01f967cf620b742bacc95bdd55 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:12:09 -0400 Subject: [PATCH 53/61] i18n: add remaining operations labels --- frontend/src/locales/es/translation.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 8b22455b..1f7d5bc4 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -805,7 +805,8 @@ "fee": "Comisión:", "minimum": "Mín.:", "withdrawVia": "Retirar mediante {{anchor}}", - "available": "Disponible: {{amount}}" + "available": "Disponible: {{amount}}", + "successProcessed": "Tu retiro de {{amount}} se procesó correctamente." }, "crossAssetPayment": { "waitingForSubmission": "Esperando el envío...", @@ -851,7 +852,8 @@ "settlement": "Liquidación", "transactionHash": "Hash de transacción", "changePrompt": "Cambia el par de activos y el importe para solicitar opciones de ruta al proxy del backend.", - "pathFeeSlippage": "Comisión: {{fee}} {{asset}} | Deslizamiento: {{slippage}} %" + "pathFeeSlippage": "Comisión: {{fee}} {{asset}} | Deslizamiento: {{slippage}} %", + "rateLabel": "tasa" }, "payrollResidual": { "scheduleConfigured": "¡Programación de nómina configurada!", @@ -906,7 +908,11 @@ "weekly": "Semanal", "biweekly": "Quincenal", "monthly": "Mensual" - } + }, + "scheduledToRun": "Programado para ejecutarse", + "broadcasting": "Transmitiendo...", + "distribution": "Distribución", + "employees": "Empleados" }, "revenueSplit": { "loadFailed": "No se pudo cargar el panel", From 89f6f6fea7a3ab91f77c2ecd20634eb3ed8d550b Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:12:37 -0400 Subject: [PATCH 54/61] fix(i18n): localize payroll operations residuals --- frontend/src/pages/PayrollScheduler.tsx | 94 ++++++++++++------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/frontend/src/pages/PayrollScheduler.tsx b/frontend/src/pages/PayrollScheduler.tsx index e394d547..75c6e02e 100644 --- a/frontend/src/pages/PayrollScheduler.tsx +++ b/frontend/src/pages/PayrollScheduler.tsx @@ -171,12 +171,12 @@ export default function PayrollScheduler() { }; const result = await createSchedule(input); - notifySuccess('Payroll schedule configured!', `Ref ID: ${result.id}`); + notifySuccess(t('payrollResidual.scheduleConfigured'), t('payrollResidual.refId', { id: result.id })); setIsWizardOpen(false); void fetchActiveSchedules(); } catch (err) { console.error('Failed to create schedule:', err); - notifyError('Failed to create schedule', 'Please try again later.'); + notifyError(t('payrollResidual.createFailed'), t('payrollResidual.tryAgainLater')); } }; @@ -202,7 +202,7 @@ export default function PayrollScheduler() { ); if (data.status === 'confirmed') { - notifySuccess('Payment confirmed!', `TX: ${data.transactionId}`); + notifySuccess(t('payrollResidual.paymentConfirmed'), t('payrollResidual.tx', { id: data.transactionId })); } }; @@ -213,13 +213,13 @@ export default function PayrollScheduler() { return () => { activeSocket.off('transaction:update', handleTransactionUpdate); }; - }, [socket, notifySuccess]); + }, [socket, notifySuccess, t]); const handleInitialize = async () => { if (!formData.employeeName || !formData.amount || !formData.walletAddress) { notifyError( - 'Missing required fields', - 'Please provide employee name, amount, and wallet address.' + t('payrollResidual.missingRequired'), + t('payrollResidual.missingRequiredBody') ); return; } @@ -229,8 +229,8 @@ export default function PayrollScheduler() { if (!hasTrustline) { setTrustlineMissing(true); notifyWarning( - 'Trustline missing!', - `${formData.employeeName} does not have a USDC trustline. You can proceed, but the payment will be held as a claimable balance.` + t('payrollResidual.trustlineMissing'), + t('payrollResidual.trustlineMissingBody', { name: formData.employeeName }) ); } else { setTrustlineMissing(false); @@ -265,8 +265,8 @@ export default function PayrollScheduler() { ); if (!result.success) { - handleContractError(undefined, 'Failed to create claimable balance'); - throw new Error('Failed to create claimable balance'); + handleContractError(undefined, t('payrollResidual.claimFailed')); + throw new Error(t('payrollResidual.claimFailed')); } // Simulate a brief delay for network broadcast @@ -276,7 +276,7 @@ export default function PayrollScheduler() { if (formData.amount === '403') { const mockErrorXdr = 'AAAABAAAAAEAAAABAAAABQ=='; // ScvError(ScError{type: SCE_CONTRACT, code: 5}) handleContractError(mockErrorXdr); - throw new Error('Contract invocation failed'); + throw new Error(t('payrollResidual.invocationFailed')); } // Add to pending claims @@ -298,8 +298,8 @@ export default function PayrollScheduler() { subscribeToTransaction(newClaim.id); notifySuccess( - 'Broadcast successful!', - `Claimable balance created for ${formData.employeeName}` + t('payrollResidual.broadcastSuccess'), + t('payrollResidual.broadcastSuccessBody', { name: formData.employeeName }) ); // Trigger Webhook Event (Internal simulation) @@ -326,8 +326,8 @@ export default function PayrollScheduler() { } catch (err) { console.error(err); notifyError( - 'Broadcast failed', - 'A contract error occurred. Please review the details below.' + t('payrollResidual.broadcastFailed'), + t('payrollResidual.broadcastFailedBody') ); } finally { setIsBroadcasting(false); @@ -337,11 +337,11 @@ export default function PayrollScheduler() { const handleCancelSchedule = async (id: number) => { try { await deleteSchedule(id); - notifySuccess('Schedule cancelled', 'The automation has been halted.'); + notifySuccess(t('payrollResidual.scheduleCancelled'), t('payrollResidual.scheduleCancelledBody')); void fetchActiveSchedules(); } catch (err) { console.error('Failed to cancel schedule:', err); - notifyError('Cancellation failed', 'Unable to reach the server.'); + notifyError(t('payrollResidual.cancellationFailed'), t('payrollResidual.cancellationFailedBody')); } }; @@ -381,7 +381,7 @@ export default function PayrollScheduler() { onClick={() => setIsWizardOpen(true)} className="p-2.5 rounded-lg hover:bg-(--surface-hi) transition-colors touch-manipulation" style={{ minHeight: '44px', minWidth: '44px' }} - aria-label="Open scheduling wizard" + aria-label={t('payrollResidual.openWizard')} > - Automation Active + {t('payrollResidual.automationActive')}

- Scheduled to run{' '} - {activeSchedule.frequency} at{' '} + {t('payrollResidual.scheduledToRun')}{' '} + {t(`payrollResidual.frequency.${activeSchedule.frequency}`, { defaultValue: activeSchedule.frequency })}{' '}{t('payrollResidual.at')}{' '} {activeSchedule.timeOfDay}

- Next Scheduled Run + {t('payrollResidual.nextRun')}
@@ -461,7 +461,7 @@ export default function PayrollScheduler() { name="employeeName" value={formData.employeeName} onChange={handleChange} - placeholder="e.g. Satoshi Nakamoto" + placeholder={t('payrollResidual.employeePlaceholder')} /> @@ -527,7 +527,7 @@ export default function PayrollScheduler() { isFullWidth > {isSimulating - ? 'Simulating...' + ? t('payrollResidual.simulating') : t('payroll.submit', 'Initialize and Validate')} ) : ( @@ -541,7 +541,7 @@ export default function PayrollScheduler() { size="md" isFullWidth > - {isBroadcasting ? 'Broadcasting...' : 'Confirm & Broadcast to Network'} + {isBroadcasting ? t('payrollResidual.broadcasting') : t('payrollResidual.confirmBroadcast')} )} @@ -572,7 +572,7 @@ export default function PayrollScheduler() { - Pre-flight Validation + {t('payrollResidual.preflight')} {trustlineMissing && ( @@ -595,7 +595,7 @@ export default function PayrollScheduler() {
- Trustline Missing + {t('payrollResidual.trustlineMissingLabel')} - Employee does not have a USDC trustline. Payment will be held as a claimable - balance. + {t('payrollResidual.trustlineWarning')}
@@ -616,16 +615,15 @@ export default function PayrollScheduler() { weight="regular" addlClassName="text-muted leading-relaxed mb-4" > - All transactions are simulated via Stellar Horizon before submission. This catches - common errors like: + {t('payrollResidual.preflightIntro')}
    -
  • Insufficient XLM balance for fees
  • -
  • Invalid sequence numbers
  • +
  • {t('payrollResidual.preflightItems.balance')}
  • +
  • {t('payrollResidual.preflightItems.sequence')}
  • - Missing trustlines for tokens + {t('payrollResidual.preflightItems.trustlines')}
  • -
  • Account eligibility status
  • +
  • {t('payrollResidual.preflightItems.eligibility')}
@@ -634,7 +632,7 @@ export default function PayrollScheduler() {
- Scheduled Automations + {t('payrollResidual.scheduledAutomations')} {isLoadingSchedules ? (
@@ -660,7 +658,7 @@ export default function PayrollScheduler() { weight="regular" addlClassName="text-muted p-4 text-xs sm:text-sm" > - No active payroll schedules found in the database. + {t('payrollResidual.noSchedules')} ) : ( @@ -691,7 +689,7 @@ export default function PayrollScheduler() {

- {schedule.frequency} Distribution + {t(`payrollResidual.frequency.${schedule.frequency}`, { defaultValue: schedule.frequency })} {t('payrollResidual.distribution')}

{schedule.timeOfDay} UTC

@@ -699,15 +697,15 @@ export default function PayrollScheduler() {
- Next Run + {t('payrollResidual.nextRunLabel')} {new Date(schedule.nextRunTimestamp).toLocaleString()}
- Recipients + {t('payrollResidual.recipients')} - {schedule.paymentConfig.recipients.length} Employees + {schedule.paymentConfig.recipients.length} {t('payrollResidual.employees')}
@@ -718,7 +716,7 @@ export default function PayrollScheduler() { }} className="w-full py-3 bg-danger/10 hover:bg-danger/20 text-danger text-xs font-bold rounded-lg transition-colors touch-manipulation min-h-[44px]" > - Cancel Automation + {t('payrollResidual.cancelAutomation')}
@@ -729,7 +727,7 @@ export default function PayrollScheduler() {
- Recent Manual Claims + {t('payrollResidual.recentClaims')} {pendingClaims.length === 0 ? ( @@ -739,7 +737,7 @@ export default function PayrollScheduler() { weight="regular" addlClassName="text-muted p-4 text-xs sm:text-sm" > - No recent manual claimable balances. + {t('payrollResidual.noClaims')} ) : (
    @@ -756,10 +754,10 @@ export default function PayrollScheduler() {
    - Amount: {claim.amount} USDC + {t('payrollResidual.amountLabel')} {claim.amount} USDC - Scheduled: {formatDate(claim.dateScheduled)} + {t('payrollResidual.scheduledLabel')} {formatDate(claim.dateScheduled)} - To: {claim.walletAddress} + {t('payrollResidual.toLabel')} {claim.walletAddress}
    From 0a3bb7b2866cbe1033b3f10047287ae3849867f9 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:13:05 -0400 Subject: [PATCH 55/61] fix(i18n): localize bulk payment tracker --- frontend/src/pages/BulkPaymentTracker.tsx | 102 ++++++++++++---------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/frontend/src/pages/BulkPaymentTracker.tsx b/frontend/src/pages/BulkPaymentTracker.tsx index 2c6dac39..cec51abe 100644 --- a/frontend/src/pages/BulkPaymentTracker.tsx +++ b/frontend/src/pages/BulkPaymentTracker.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useTranslation } from 'react-i18next'; import { RefreshCw, ChevronDown, @@ -42,11 +43,12 @@ function shortHash(hash: string | null) { // ── Status Badge ───────────────────────────────────────────────────────────── function BatchStatusBadge({ status }: { status: BatchRun['status'] }) { + const { t } = useTranslation(); const map: Record = { - confirmed: { cls: styles.statusConfirmed, dot: styles.statusDotConfirmed, label: 'Confirmed' }, - pending: { cls: styles.statusPending, dot: styles.statusDotPending, label: 'Pending' }, - partial: { cls: styles.statusPartial, dot: styles.statusDotPartial, label: 'Partial' }, - failed: { cls: styles.statusFailed, dot: styles.statusDotFailed, label: 'Failed' }, + confirmed: { cls: styles.statusConfirmed, dot: styles.statusDotConfirmed, label: t('bulkPaymentTracker.status.confirmed') }, + pending: { cls: styles.statusPending, dot: styles.statusDotPending, label: t('bulkPaymentTracker.status.pending') }, + partial: { cls: styles.statusPartial, dot: styles.statusDotPartial, label: t('bulkPaymentTracker.status.partial') }, + failed: { cls: styles.statusFailed, dot: styles.statusDotFailed, label: t('bulkPaymentTracker.status.failed') }, }; const { cls, dot, label } = map[status]; return ( @@ -58,10 +60,11 @@ function BatchStatusBadge({ status }: { status: BatchRun['status'] }) { } function RecipientStatusBadge({ status }: { status: BatchRecipient['status'] }) { + const { t } = useTranslation(); const map: Record = { - confirmed: { cls: styles.statusConfirmed, dot: styles.statusDotConfirmed, label: 'Confirmed' }, - pending: { cls: styles.statusPending, dot: styles.statusDotPending, label: 'Pending' }, - failed: { cls: styles.statusFailed, dot: styles.statusDotFailed, label: 'Failed' }, + confirmed: { cls: styles.statusConfirmed, dot: styles.statusDotConfirmed, label: t('bulkPaymentTracker.status.confirmed') }, + pending: { cls: styles.statusPending, dot: styles.statusDotPending, label: t('bulkPaymentTracker.status.pending') }, + failed: { cls: styles.statusFailed, dot: styles.statusDotFailed, label: t('bulkPaymentTracker.status.failed') }, }; const { cls, dot, label } = map[status]; return ( @@ -75,22 +78,23 @@ function RecipientStatusBadge({ status }: { status: BatchRecipient['status'] }) // ── Recipient Expansion Panel ───────────────────────────────────────────────── function RecipientPanel({ recipients }: { recipients: BatchRecipient[] }) { + const { t } = useTranslation(); return (
    - Per-Recipient Breakdown + {t('bulkPaymentTracker.perRecipient')} - {recipients.length} recipient{recipients.length !== 1 ? 's' : ''} + {t('bulkPaymentTracker.recipientCount', { count: recipients.length })}
    {/* Column headers */}
    - Employee - Amount - Status - Tx Hash - Details + {t('bulkPaymentTracker.employee')} + {t('bulkPaymentTracker.amount')} + {t('bulkPaymentTracker.statusLabel')} + {t('bulkPaymentTracker.txHash')} + {t('bulkPaymentTracker.details')}
    {recipients.map((r) => ( @@ -162,6 +166,7 @@ const BatchRow: React.FC = ({ onToggle, onRetry, }) => { + const { t } = useTranslation(); const hasFailed = batch.status === 'failed' || batch.recipients.some((r: BatchRecipient) => r.status === 'failed'); @@ -232,21 +237,21 @@ const BatchRow: React.FC = ({ className={styles.retryBtn} onClick={onRetry} disabled={isRetrying} - title="Retry failed batch" - aria-label={`Retry batch ${batch.id}`} + title={t('bulkPaymentTracker.retryFailedBatch')} + aria-label={t('bulkPaymentTracker.retryBatch', { id: batch.id })} > {isRetrying ? ( ) : ( )} - Retry + {t('bulkPaymentTracker.retry')} )} @@ -261,6 +266,7 @@ const BatchRow: React.FC = ({ // ── Main Page ───────────────────────────────────────────────────────────────── export default function BulkPaymentTracker() { + const { t } = useTranslation(); const { batches, total, @@ -293,16 +299,16 @@ export default function BulkPaymentTracker() {

    - Bulk Payment Status Tracker + {t('bulkPaymentTracker.titlePrefix')} {t('bulkPaymentTracker.titleHighlight')}

    -

    Real-time on-chain confirmation for batch payroll runs

    +

    {t('bulkPaymentTracker.subtitle')}

    {/* Live indicator */}
    - {connected ? 'Live' : 'Offline'} + {connected ? t('bulkPaymentTracker.live') : t('bulkPaymentTracker.offline')}
    {connected ? ( @@ -320,13 +326,13 @@ export default function BulkPaymentTracker() { setStatusFilter(e.target.value); setPage(1); }} - aria-label="Filter by status" + aria-label={t('bulkPaymentTracker.filterStatus')} > - - - - - + + + + +
    @@ -356,7 +362,7 @@ export default function BulkPaymentTracker() {
    {total}
    -
    Total Batches
    +
    {t('bulkPaymentTracker.totalBatches')}
@@ -369,7 +375,7 @@ export default function BulkPaymentTracker() {
{confirmedCount}
-
Confirmed
+
{t('bulkPaymentTracker.status.confirmed')}
@@ -382,7 +388,7 @@ export default function BulkPaymentTracker() {
{pendingCount}
-
In Progress
+
{t('bulkPaymentTracker.inProgress')}
@@ -398,7 +404,7 @@ export default function BulkPaymentTracker() {
{failedCount}
-
Failed
+
{t('bulkPaymentTracker.status.failed')}
@@ -416,7 +422,7 @@ export default function BulkPaymentTracker() {
{batches.reduce((s: number, b: BatchRun) => s + b.employeeCount, 0)}
-
Recipients
+
{t('bulkPaymentTracker.recipients')}
@@ -436,7 +442,7 @@ export default function BulkPaymentTracker() { .reduce((s: number, b: BatchRun) => s + parseFloat(b.totalAmount), 0) .toLocaleString(undefined, { maximumFractionDigits: 0 })} -
Volume (page)
+
{t('bulkPaymentTracker.volumePage')}
@@ -453,13 +459,13 @@ export default function BulkPaymentTracker() {
{/* Table Header */}
- Date - Employees - Total Amount - Status - Confs - Tx Hash - Actions + {t('bulkPaymentTracker.date')} + {t('bulkPaymentTracker.employees')} + {t('bulkPaymentTracker.totalAmount')} + {t('bulkPaymentTracker.statusLabel')} + {t('bulkPaymentTracker.confirmations')} + {t('bulkPaymentTracker.txHash')} + {t('bulkPaymentTracker.actions')}
{/* Body */} @@ -470,11 +476,11 @@ export default function BulkPaymentTracker() { ) : batches.length === 0 ? (
-

No batch runs found

+

{t('bulkPaymentTracker.empty')}

{statusFilter !== 'all' - ? `No batches with status "${statusFilter}". Try a different filter.` - : 'Payroll batch runs will appear here once the first bulk payment is submitted.'} + ? t('bulkPaymentTracker.emptyFiltered', { status: statusFilter }) + : t('bulkPaymentTracker.emptyDefault')}

) : ( @@ -499,7 +505,7 @@ export default function BulkPaymentTracker() { className={styles.pageBtn} onClick={() => setPage(page - 1)} disabled={page <= 1 || isLoading} - aria-label="Previous page" + aria-label={t('bulkPaymentTracker.previousPage')} > ‹ @@ -518,7 +524,7 @@ export default function BulkPaymentTracker() { key={p} className={`${styles.pageBtn} ${p === page ? styles.pageBtnActive : ''}`} onClick={() => setPage(p)} - aria-label={`Page ${p}`} + aria-label={t('bulkPaymentTracker.page', { page: p })} aria-current={p === page ? 'page' : undefined} > {p} @@ -530,7 +536,7 @@ export default function BulkPaymentTracker() { className={styles.pageBtn} onClick={() => setPage(page + 1)} disabled={page >= totalPages || isLoading} - aria-label="Next page" + aria-label={t('bulkPaymentTracker.nextPage')} > › From 4d72dbe44d6a65bb7cc5fa723ecad1def7dfd862 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:13:35 -0400 Subject: [PATCH 56/61] i18n: add payroll wallet address label --- frontend/src/locales/en/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index cb175302..1838eb43 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -56,7 +56,8 @@ "frequencyWeekly": "Weekly", "frequencyMonthly": "Monthly", "commencementDate": "Commencement Date", - "submit": "Initialize Payroll Stream" + "submit": "Initialize Payroll Stream", + "walletAddress": "Wallet Address" }, "employees": { "title": "Workforce {{highlight}}", From 941b0f748e3219fd7ff065ad31f0968f565c1f75 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:13:37 -0400 Subject: [PATCH 57/61] i18n: add payroll wallet address label --- frontend/src/locales/es/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 1f7d5bc4..e23312e1 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -56,7 +56,8 @@ "frequencyWeekly": "Semanal", "frequencyMonthly": "Mensual", "commencementDate": "Fecha de inicio", - "submit": "Iniciar flujo de nómina" + "submit": "Iniciar flujo de nómina", + "walletAddress": "Dirección de billetera" }, "employees": { "title": "Directorio de {{highlight}}", From eefeab1af7fa6084aa9978d37252f40fd4170187 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Tue, 6 Oct 2026 15:14:40 -0400 Subject: [PATCH 58/61] i18n: rely on locale entries for scheduler labels --- frontend/src/pages/PayrollScheduler.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/PayrollScheduler.tsx b/frontend/src/pages/PayrollScheduler.tsx index 75c6e02e..aa72ad91 100644 --- a/frontend/src/pages/PayrollScheduler.tsx +++ b/frontend/src/pages/PayrollScheduler.tsx @@ -362,8 +362,8 @@ export default function PayrollScheduler() { weight="bold" addlClassName="mb-2 tracking-tight text-2xl sm:text-3xl lg:text-4xl" > - {t('payroll.title', 'Workforce')}{' '} - {t('payroll.titleHighlight', 'Scheduler')} + {t('payroll.title')}{' '} + {t('payroll.titleHighlight')} {} - {t('payroll.subtitle', 'Automated distribution engine')} + {t('payroll.subtitle')}
@@ -457,7 +457,7 @@ export default function PayrollScheduler() { {} - + {} - +
@@ -509,7 +509,7 @@ export default function PayrollScheduler() { {isSimulating ? t('payrollResidual.simulating') - : t('payroll.submit', 'Initialize and Validate')} + : t('payroll.submit')} ) : (
DateRecipientActionAmountTx{t('revenueSplit.date')}{t('revenueSplit.recipient')}{t('revenueSplit.action')}{t('revenueSplit.amount')}{t('revenueSplit.tx')}
- {new Date(event.createdAt).toLocaleString()} + {new Date(event.createdAt).toLocaleString(i18n.language)} {event.recipientLabel} {event.action}