From 50d952b4d77a2b802fefb65eb9e50e9c89953067 Mon Sep 17 00:00:00 2001 From: rasputin Date: Sat, 6 Jun 2026 01:12:20 +0200 Subject: [PATCH] feat: localisation with lyricist --- .../gudariwallet/i18n/EnStrings.kt | 138 +++++++++++++++++- .../gudariwallet/i18n/EsStrings.kt | 137 ++++++++++++++++- .../bitcointxoko/gudariwallet/i18n/Strings.kt | 123 ++++++++++++++++ .../gudariwallet/ui/OnboardingScreen.kt | 98 ++++++++----- .../gudariwallet/ui/WalletViewModel.kt | 30 +++- .../ui/history/FilterBottomSheet.kt | 76 ++++++---- .../gudariwallet/ui/history/HistoryScreen.kt | 75 +++++----- .../ui/history/PaymentDetailScreen.kt | 75 ++++++---- .../gudariwallet/ui/history/PaymentRow.kt | 12 +- .../ui/history/SearchInfoSheet.kt | 45 +++--- .../gudariwallet/ui/receive/AmountDisplay.kt | 12 +- .../ui/receive/BrightnessToggleButton.kt | 7 +- .../gudariwallet/ui/receive/QrDisplayCard.kt | 31 ++-- .../gudariwallet/ui/receive/ReceiveScreen.kt | 11 +- .../ui/receive/ReceiveViewModel.kt | 58 ++++++-- 15 files changed, 725 insertions(+), 203 deletions(-) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt index 6fdc48e..ae13ca7 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt @@ -94,7 +94,23 @@ val EnHomeStrings = AppStrings( // ── ReceiveScreen — success ─────────────────────────────────────────────── paymentReceived = "Payment Received!", - // ── SendScreen — general ────────────────────────────────────────────────── + // ── AmountDisplay ───────────────────────────────────────────────────────── + amountDisplaySats = { amount -> "$amount ${if (amount == 1L) "sat" else "sats"}" }, + + // ── BrightnessToggleButton & QrDisplayCard ──────────────────────────────── + brightnessReduce = "Reduce brightness", + brightnessBoost = "Boost brightness", + qrShare = "Share", + qrCopy = "Copy", + qrNfcStop = "Stop NFC sharing", + qrNfcStart = "Share via NFC", + + // ── ReceiveViewModel ────────────────────────────────────────────────────── + receiveVmFailedToCreateInvoice = "Failed to create invoice", + receiveVmWithdrawRejected = "Withdraw service rejected the invoice", + receiveVmWithdrawFailed = "Withdraw failed", + +// ── SendScreen — general ────────────────────────────────────────────────── send = "Send", paste = "Paste", continueButton = "Continue", @@ -159,4 +175,124 @@ val EnHomeStrings = AppStrings( routeHintHideDetails = "Hide details ▴", routeHintHopsHeader = "Private route hint hops", routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" }, + + // ── HistoryScreen ───────────────────────────────────────────────────────── + historyTitle = "History", + historyClose = "Close", + historySearchPayments = "Search payments", + historyFilterPayments = "Filter payments", + historyRetry = "Retry", + historyNoPayments = "No payments yet.", + historySearchPlaceholder = "Search transactions", + historySearchHelp = "Search help", + historySearchPaste = "Paste", + historySearchClear = "Clear search", + historySearchChip = { tail -> "search: …$tail" }, + historyClearSearch = "Clear search", + filterDirectionOutgoing = "Outgoing", + filterDirectionIncoming = "Incoming", + filterClearDirection = "Clear direction filter", + filterStatusCompleted = "Completed", + filterStatusPending = "Pending", + filterStatusFailed = "Failed", + filterRemoveStatus = "Remove status filter", + filterRemoveType = "Remove type filter", + filterAmountRange = { min, max -> "$min–$max sats" }, + filterAmountMin = { min -> "≥ $min sats" }, + filterAmountMax = { max -> "≤ $max sats" }, + filterClearAmount = "Clear amount filter", + filterDateThisWeek = "This week", + filterDateThisMonth = "This month", + filterDateThisYear = "This year", + filterDateRange = { from, to -> "$from–$to" }, + filterDateFrom = { from -> "From $from" }, + filterDateUntil = { until -> "Until $until" }, + filterClearDate = "Clear date filter", + + // ── FilterBottomSheet ───────────────────────────────────────────────────── + filterSheetTitle = "Filter payments", + filterSectionDirection = "Direction", + filterDirectionAll = "All", + filterSectionStatus = "Status", + filterSectionType = "Type", + filterSectionAmount = "Amount (sats)", + filterAmountMin_label = "Min", + filterAmountMax_label = "Max", + filterAmountSuffix = "sats", + filterSectionDate = "Date", + filterDateFrom_label = "From", + filterDateTo_label = "To", + filterDialogOk = "OK", + filterDialogCancel = "Cancel", + + // ── SearchInfoSheet ─────────────────────────────────────────────────────── + searchInfoTitle = "What can I search for?", + searchInfoSubtitle = "Type any part of the following to find a payment:", + searchInfoMemoTitle = "Memo", + searchInfoMemoDesc = "The message or description attached to a payment. " + + "For example, \"coffee\" or \"rent March\".", + searchInfoInvoiceTitle = "Invoice", + searchInfoInvoiceDesc = "The payment request you scanned or pasted to send a payment. " + + "It usually starts with \"lnbc…\". " + + "You can paste just the first or last few characters.", + searchInfoHashTitle = "Hash", + searchInfoHashDesc = "A unique code that identifies this payment on the " + + "Lightning Network. Useful if someone asks you to " + + "confirm a specific transaction.", + searchInfoPreimageTitle = "Preimage", + searchInfoPreimageDesc = "A secret code that proves a payment was received. " + + "Only available after a payment completes successfully.", + searchInfoNodeAliasTitle = "Node alias", + searchInfoNodeAliasDesc = "The name of the Lightning node you sent a payment to. " + + "For example, \"ACINQ\" or \"Wallet of Satoshi\". " + + "Aliases are resolved automatically for outgoing payments.", + searchInfoNodeIdTitle = "Node ID (pubkey)", + searchInfoNodeIdDesc = "The public key of the destination node. " + + "A 66-character hex string — you can paste just the first few characters.", + searchInfoFooter = "You can paste a full value or search with just a few characters — " + + "partial matches work too.", + + // ── PaymentDetailScreen ─────────────────────────────────────────────────── + detailOutgoingTitle = "Outgoing payment", + detailIncomingTitle = "Incoming payment", + detailBack = "Back", + detailCouldNotLoad = "Could not load full details", + detailFeeLabel = { feeSat, fiatPart, ppmPart -> + "Fee: $feeSat sats$fiatPart$ppmPart" + }, + detailSectionDetails = "Details", + detailRowDate = "Date", + detailRowMemo = "Memo", + detailRowMemoEmpty = "—", + detailRowType = "Type", + detailRowComment = "Comment", + detailSectionSuccessAction = "Success Action", + detailSuccessMessage = "Message", + detailSuccessUrl = "URL", + detailSuccessDescription = "Description", + detailSectionTechnical = "Technical", + detailRowPaymentHash = "Hash", + detailRowPreimage = "Preimage", + detailRowDestination = "Destination", + detailCopyNodeId = "Copy node ID", + detailRowBolt11 = "BOLT11", + + // ── PaymentRow ──────────────────────────────────────────────────────────── + paymentRowSentCD = "Sent", + paymentRowReceivedCD = "Received", + paymentRowNoMemo = "No memo", + paymentRowPending = "Pending", + + // ── OnboardingScreen ────────────────────────────────────────────────────── + onboardingTitle = "Connect to LNbits", + onboardingSubtitle = "Enter your LNbits server URL and API keys.", + onboardingServerUrl = "Server URL", + onboardingServerUrlHint = "https://bitcointxoko.org", + onboardingInvoiceKey = "Invoice Key (read-only)", + onboardingAdminKey = "Admin Key (for sending)", + onboardingHideAdminKey = "Hide", + onboardingShowAdminKey = "Show", + onboardingFieldsRequired = "All fields are required.", + onboardingConnect = "Connect", ) + diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt index ba5c842..b730d8a 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt @@ -94,7 +94,23 @@ val EsHomeStrings = AppStrings( // ── ReceiveScreen — success ─────────────────────────────────────────────── paymentReceived = "¡Pago recibido!", - // ── SendScreen — general ────────────────────────────────────────────────── + // ── AmountDisplay ───────────────────────────────────────────────────────── + amountDisplaySats = { amount -> "$amount ${if (amount == 1L) "sat" else "sats"}" }, + + // ── BrightnessToggleButton & QrDisplayCard ──────────────────────────────── + brightnessReduce = "Reducir brillo", + brightnessBoost = "Aumentar brillo", + qrShare = "Compartir", + qrCopy = "Copiar", + qrNfcStop = "Detener NFC", + qrNfcStart = "Compartir por NFC", + + // ── ReceiveViewModel ────────────────────────────────────────────────────── + receiveVmFailedToCreateInvoice = "No se pudo crear la factura", + receiveVmWithdrawRejected = "El servicio de retiro rechazó la factura", + receiveVmWithdrawFailed = "Error al retirar", + +// ── SendScreen — general ────────────────────────────────────────────────── send = "Enviar", paste = "Pegar", continueButton = "Continuar", @@ -159,4 +175,123 @@ val EsHomeStrings = AppStrings( routeHintHideDetails = "Ocultar detalles ▴", routeHintHopsHeader = "Saltos de ruta privada", routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" }, + + // ── HistoryScreen ───────────────────────────────────────────────────────── + historyTitle = "Historial", + historyClose = "Cerrar", + historySearchPayments = "Buscar pagos", + historyFilterPayments = "Filtrar pagos", + historyRetry = "Reintentar", + historyNoPayments = "Aún no hay pagos.", + historySearchPlaceholder = "Buscar transacciones", + historySearchHelp = "Ayuda de búsqueda", + historySearchPaste = "Pegar", + historySearchClear = "Limpiar búsqueda", + historySearchChip = { tail -> "búsqueda: …$tail" }, + historyClearSearch = "Limpiar búsqueda", + filterDirectionOutgoing = "Salientes", + filterDirectionIncoming = "Entrantes", + filterClearDirection = "Quitar filtro de dirección", + filterStatusCompleted = "Completado", + filterStatusPending = "Pendiente", + filterStatusFailed = "Fallido", + filterRemoveStatus = "Quitar filtro de estado", + filterRemoveType = "Quitar filtro de tipo", + filterAmountRange = { min, max -> "$min–$max sats" }, + filterAmountMin = { min -> "≥ $min sats" }, + filterAmountMax = { max -> "≤ $max sats" }, + filterClearAmount = "Quitar filtro de cantidad", + filterDateThisWeek = "Esta semana", + filterDateThisMonth = "Este mes", + filterDateThisYear = "Este año", + filterDateRange = { from, to -> "$from–$to" }, + filterDateFrom = { from -> "Desde $from" }, + filterDateUntil = { until -> "Hasta $until" }, + filterClearDate = "Quitar filtro de fecha", + + // ── FilterBottomSheet ───────────────────────────────────────────────────── + filterSheetTitle = "Filtrar pagos", + filterSectionDirection = "Dirección", + filterDirectionAll = "Todos", + filterSectionStatus = "Estado", + filterSectionType = "Tipo", + filterSectionAmount = "Cantidad (sats)", + filterAmountMin_label = "Mín.", + filterAmountMax_label = "Máx.", + filterAmountSuffix = "sats", + filterSectionDate = "Fecha", + filterDateFrom_label = "Desde", + filterDateTo_label = "Hasta", + filterDialogOk = "Aceptar", + filterDialogCancel = "Cancelar", + + // ── SearchInfoSheet ─────────────────────────────────────────────────────── + searchInfoTitle = "¿Qué puedo buscar?", + searchInfoSubtitle = "Escribe cualquier parte de lo siguiente para encontrar un pago:", + searchInfoMemoTitle = "Nota", + searchInfoMemoDesc = "El mensaje o descripción adjunto a un pago. " + + "Por ejemplo, \"café\" o \"alquiler marzo\".", + searchInfoInvoiceTitle = "Factura", + searchInfoInvoiceDesc = "La solicitud de pago que escaneaste o pegaste para enviar un pago. " + + "Normalmente empieza por \"lnbc…\". " + + "Puedes pegar solo los primeros o últimos caracteres.", + searchInfoHashTitle = "Hash", + searchInfoHashDesc = "Un código único que identifica este pago en la " + + "red Lightning. Útil si alguien te pide que " + + "confirmes una transacción específica.", + searchInfoPreimageTitle = "Preimagen", + searchInfoPreimageDesc = "Un código secreto que demuestra que se recibió un pago. " + + "Solo disponible después de que un pago se complete correctamente.", + searchInfoNodeAliasTitle = "Alias del nodo", + searchInfoNodeAliasDesc = "El nombre del nodo Lightning al que enviaste un pago. " + + "Por ejemplo, \"ACINQ\" o \"Wallet of Satoshi\". " + + "Los alias se resuelven automáticamente para pagos salientes.", + searchInfoNodeIdTitle = "ID del nodo (clave pública)", + searchInfoNodeIdDesc = "La clave pública del nodo de destino. " + + "Una cadena hexadecimal de 66 caracteres — puedes pegar solo los primeros.", + searchInfoFooter = "Puedes pegar un valor completo o buscar con solo unos pocos caracteres — " + + "las coincidencias parciales también funcionan.", + + // ── PaymentDetailScreen ─────────────────────────────────────────────────── + detailOutgoingTitle = "Pago saliente", + detailIncomingTitle = "Pago entrante", + detailBack = "Atrás", + detailCouldNotLoad = "No se pudieron cargar los detalles completos", + detailFeeLabel = { feeSat, fiatPart, ppmPart -> + "Comisión: $feeSat sats$fiatPart$ppmPart" + }, + detailSectionDetails = "Detalles", + detailRowDate = "Fecha", + detailRowMemo = "Nota", + detailRowMemoEmpty = "—", + detailRowType = "Tipo", + detailRowComment = "Comentario", + detailSectionSuccessAction = "Acción de éxito", + detailSuccessMessage = "Mensaje", + detailSuccessUrl = "URL", + detailSuccessDescription = "Descripción", + detailSectionTechnical = "Técnico", + detailRowPaymentHash = "Hash", + detailRowPreimage = "Preimagen", + detailRowDestination = "Destino", + detailCopyNodeId = "Copiar ID del nodo", + detailRowBolt11 = "BOLT11", + + // ── PaymentRow ──────────────────────────────────────────────────────────── + paymentRowSentCD = "Enviado", + paymentRowReceivedCD = "Recibido", + paymentRowNoMemo = "Sin nota", + paymentRowPending = "Pendiente", + + // ── OnboardingScreen ────────────────────────────────────────────────────── + onboardingTitle = "Conectar a LNbits", + onboardingSubtitle = "Introduce la URL de tu servidor LNbits y las claves API.", + onboardingServerUrl = "URL del servidor", + onboardingServerUrlHint = "https://bitcointxoko.org", + onboardingInvoiceKey = "Clave de factura (solo lectura)", + onboardingAdminKey = "Clave de administrador (para enviar)", + onboardingHideAdminKey = "Ocultar", + onboardingShowAdminKey = "Mostrar", + onboardingFieldsRequired = "Todos los campos son obligatorios.", + onboardingConnect = "Conectar", ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt index 55a8d16..f06b7b6 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt @@ -103,6 +103,22 @@ data class AppStrings( // ── ReceiveScreen — success ─────────────────────────────────────────────── val paymentReceived : String, + // ── AmountDisplay ───────────────────────────────────────────────────────── + val amountDisplaySats : (amount: Long) -> String, + + // ── BrightnessToggleButton & QrDisplayCard ──────────────────────────────── + val brightnessReduce : String, + val brightnessBoost : String, + val qrShare : String, + val qrCopy : String, + val qrNfcStop : String, + val qrNfcStart : String, + + // ── ReceiveViewModel ────────────────────────────────────────────────────── + val receiveVmFailedToCreateInvoice : String, + val receiveVmWithdrawRejected : String, + val receiveVmWithdrawFailed : String, + // ── SendScreen — general ────────────────────────────────────────────────── val send : String, val paste : String, @@ -168,4 +184,111 @@ data class AppStrings( val routeHintHideDetails : String, val routeHintHopsHeader : String, val routeHintHopFees : (ppm: Long, baseLabel: String) -> String, + + // ── HistoryScreen ───────────────────────────────────────────────────────── + val historyTitle : String, + val historyClose : String, + val historySearchPayments : String, + val historyFilterPayments : String, + val historyRetry : String, + val historyNoPayments : String, + val historySearchPlaceholder : String, + val historySearchHelp : String, + val historySearchPaste : String, + val historySearchClear : String, + val historySearchChip : (tail: String) -> String, + val historyClearSearch : String, + val filterDirectionOutgoing : String, + val filterDirectionIncoming : String, + val filterClearDirection : String, + val filterStatusCompleted : String, + val filterStatusPending : String, + val filterStatusFailed : String, + val filterRemoveStatus : String, + val filterRemoveType : String, + val filterAmountRange : (min: Long, max: Long) -> String, + val filterAmountMin : (min: Long) -> String, + val filterAmountMax : (max: Long) -> String, + val filterClearAmount : String, + val filterDateThisWeek : String, + val filterDateThisMonth : String, + val filterDateThisYear : String, + val filterDateRange : (from: String, to: String) -> String, + val filterDateFrom : (from: String) -> String, + val filterDateUntil : (until: String) -> String, + val filterClearDate : String, + + // ── FilterBottomSheet ───────────────────────────────────────────────────── + val filterSheetTitle : String, + val filterSectionDirection : String, + val filterDirectionAll : String, + val filterSectionStatus : String, + val filterSectionType : String, + val filterSectionAmount : String, + val filterAmountMin_label : String, + val filterAmountMax_label : String, + val filterAmountSuffix : String, + val filterSectionDate : String, + val filterDateFrom_label : String, + val filterDateTo_label : String, + val filterDialogOk : String, + val filterDialogCancel : String, + + // ── SearchInfoSheet ─────────────────────────────────────────────────────── + val searchInfoTitle : String, + val searchInfoSubtitle : String, + val searchInfoMemoTitle : String, + val searchInfoMemoDesc : String, + val searchInfoInvoiceTitle : String, + val searchInfoInvoiceDesc : String, + val searchInfoHashTitle : String, + val searchInfoHashDesc : String, + val searchInfoPreimageTitle : String, + val searchInfoPreimageDesc : String, + val searchInfoNodeAliasTitle : String, + val searchInfoNodeAliasDesc : String, + val searchInfoNodeIdTitle : String, + val searchInfoNodeIdDesc : String, + val searchInfoFooter : String, + + // ── PaymentDetailScreen ─────────────────────────────────────────────────── + val detailOutgoingTitle : String, + val detailIncomingTitle : String, + val detailBack : String, + val detailCouldNotLoad : String, + val detailFeeLabel : (feeSat: Long, fiatPart: String, ppmPart: String) -> String, + val detailSectionDetails : String, + val detailRowDate : String, + val detailRowMemo : String, + val detailRowMemoEmpty : String, + val detailRowType : String, + val detailRowComment : String, + val detailSectionSuccessAction: String, + val detailSuccessMessage : String, + val detailSuccessUrl : String, + val detailSuccessDescription : String, + val detailSectionTechnical : String, + val detailRowPaymentHash : String, + val detailRowPreimage : String, + val detailRowDestination : String, + val detailCopyNodeId : String, + val detailRowBolt11 : String, + + // ── PaymentRow ──────────────────────────────────────────────────────────── + val paymentRowSentCD : String, + val paymentRowReceivedCD : String, + val paymentRowNoMemo : String, + val paymentRowPending : String, + + // ── OnboardingScreen ────────────────────────────────────────────────────── + val onboardingTitle : String, + val onboardingSubtitle : String, + val onboardingServerUrl : String, + val onboardingServerUrlHint : String, + val onboardingInvoiceKey : String, + val onboardingAdminKey : String, + val onboardingHideAdminKey : String, + val onboardingShowAdminKey : String, + val onboardingFieldsRequired : String, + val onboardingConnect : String, ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/OnboardingScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/OnboardingScreen.kt index e65b5e5..ce15b86 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/OnboardingScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/OnboardingScreen.kt @@ -1,24 +1,45 @@ package com.bitcointxoko.gudariwallet.ui -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.security.SecretStore import kotlinx.coroutines.launch @Composable fun OnboardingScreen( secretStore: SecretStore, - onComplete: () -> Unit + onComplete : () -> Unit ) { + val strings = LocalAppStrings.current + var baseUrl by remember { mutableStateOf("https://bitcointxoko.org") } var invoiceKey by remember { mutableStateOf("") } var adminKey by remember { mutableStateOf("") } @@ -26,60 +47,63 @@ fun OnboardingScreen( var error by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() - Column( - modifier = Modifier + modifier = Modifier .fillMaxSize() .padding(32.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Connect to LNbits", style = MaterialTheme.typography.headlineMedium) + Text( + text = strings.onboardingTitle, // ← "Connect to LNbits" + style = MaterialTheme.typography.headlineMedium + ) Spacer(Modifier.height(8.dp)) Text( - "Enter your LNbits server URL and API keys.", + text = strings.onboardingSubtitle, // ← "Enter your LNbits server URL and API keys." style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(Modifier.height(32.dp)) OutlinedTextField( - value = baseUrl, - onValueChange = { baseUrl = it }, - label = { Text("Server URL") }, - placeholder = { Text("https://bitcointxoko.org") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), + value = baseUrl, + onValueChange = { baseUrl = it }, + label = { Text(strings.onboardingServerUrl) }, // ← "Server URL" + placeholder = { Text(strings.onboardingServerUrlHint) }, // ← "https://bitcointxoko.org" + singleLine = true, + modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri) ) Spacer(Modifier.height(16.dp)) OutlinedTextField( - value = invoiceKey, - onValueChange = { invoiceKey = it }, - label = { Text("Invoice Key (read-only)") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + value = invoiceKey, + onValueChange = { invoiceKey = it }, + label = { Text(strings.onboardingInvoiceKey) }, // ← "Invoice Key (read-only)" + singleLine = true, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), visualTransformation = PasswordVisualTransformation() ) Spacer(Modifier.height(16.dp)) OutlinedTextField( - value = adminKey, - onValueChange = { adminKey = it }, - label = { Text("Admin Key (for sending)") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + value = adminKey, + onValueChange = { adminKey = it }, + label = { Text(strings.onboardingAdminKey) }, // ← "Admin Key (for sending)" + singleLine = true, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), visualTransformation = if (adminVisible) VisualTransformation.None - else PasswordVisualTransformation(), + else PasswordVisualTransformation(), trailingIcon = { IconButton(onClick = { adminVisible = !adminVisible }) { Icon( - imageVector = if (adminVisible) Icons.Default.VisibilityOff - else Icons.Default.Visibility, - contentDescription = if (adminVisible) "Hide" else "Show" + imageVector = if (adminVisible) Icons.Default.VisibilityOff + else Icons.Default.Visibility, + contentDescription = if (adminVisible) strings.onboardingHideAdminKey // ← "Hide" + else strings.onboardingShowAdminKey // ← "Show" ) } } @@ -87,8 +111,11 @@ fun OnboardingScreen( error?.let { Spacer(Modifier.height(8.dp)) - Text(it, color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall) + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) } Spacer(Modifier.height(32.dp)) @@ -96,7 +123,7 @@ fun OnboardingScreen( Button( onClick = { if (baseUrl.isBlank() || invoiceKey.isBlank() || adminKey.isBlank()) { - error = "All fields are required." + error = strings.onboardingFieldsRequired // ← "All fields are required." return@Button } scope.launch { @@ -106,8 +133,7 @@ fun OnboardingScreen( }, modifier = Modifier.fillMaxWidth() ) { - Text("Connect") + Text(strings.onboardingConnect) // ← "Connect" } } } - diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index 3b8332e..d82caea 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -1,7 +1,6 @@ package com.bitcointxoko.gudariwallet.ui import android.util.Log -import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.bitcointxoko.gudariwallet.api.LnurlScanResponse @@ -47,10 +46,33 @@ class WalletViewModel( val fiatCurrency: String? get() = fiatVm.fiatCurrency val receiveState: StateFlow get() = receiveVm.receiveState val navigateToReceive: SharedFlow get() = receiveVm.navigateToReceive - fun createInvoice(amountSats: Long, memo: String) = receiveVm.createInvoice(amountSats, memo) + fun createInvoice( + amountSats : Long, + memo : String, + failedToCreateInvoice : String, + ) = receiveVm.createInvoice( + amountSats = amountSats, + memo = memo, + failedToCreateInvoice = failedToCreateInvoice, + ) fun resetReceiveState() = receiveVm.resetReceiveState() - fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) = - receiveVm.executeWithdraw(k1, callback, amountSats, memo) + fun executeWithdraw( + k1 : String, + callback : String, + amountSats : Long, + memo : String, + failedToCreateInvoice : String, + withdrawRejected : String, + withdrawFailed : String, + ) = receiveVm.executeWithdraw( + k1 = k1, + callback = callback, + amountSats = amountSats, + memo = memo, + failedToCreateInvoice = failedToCreateInvoice, + withdrawRejected = withdrawRejected, + withdrawFailed = withdrawFailed, + ) val sendState: StateFlow get() = sendVm.sendState fun refreshBalance() = balanceVm.refreshBalance() fun scan(rawInput: String) = sendVm.scan(rawInput) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt index e98e54c..56c1d88 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import com.bitcointxoko.gudariwallet.LocalAppStrings @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -47,6 +48,8 @@ internal fun FilterBottomSheet( onDateFilterSet : (min: Long?, max: Long?) -> Unit, onDateClearRequested: () -> Unit ) { + val strings = LocalAppStrings.current + ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState @@ -56,15 +59,16 @@ internal fun FilterBottomSheet( .fillMaxWidth() .padding(start = 24.dp, end = 24.dp, bottom = 32.dp) ) { + // ── Sheet title ─────────────────────────────────────────────────── Text( - text = "Filter payments", + text = strings.filterSheetTitle, // ← "Filter payments" style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(20.dp)) - // ── Direction ──────────────────────────────────────────────────── + // ── Direction ───────────────────────────────────────────────────── Text( - text = "Direction", + text = strings.filterSectionDirection, // ← "Direction" style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -72,9 +76,9 @@ internal fun FilterBottomSheet( Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { DirectionFilter.entries.forEach { option -> val label = when (option) { - DirectionFilter.ALL -> "All" - DirectionFilter.OUTGOING -> "Outgoing" - DirectionFilter.INCOMING -> "Incoming" + DirectionFilter.ALL -> strings.filterDirectionAll // ← "All" + DirectionFilter.OUTGOING -> strings.filterDirectionOutgoing // ← "Outgoing" + DirectionFilter.INCOMING -> strings.filterDirectionIncoming // ← "Incoming" } FilterChip( selected = currentFilter.direction == option, @@ -85,9 +89,9 @@ internal fun FilterBottomSheet( } Spacer(Modifier.height(20.dp)) - // ── Status ─────────────────────────────────────────────────────── + // ── Status ──────────────────────────────────────────────────────── Text( - text = "Status", + text = strings.filterSectionStatus, // ← "Status" style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -100,9 +104,9 @@ internal fun FilterBottomSheet( label = { Text( when (option) { - StatusFilter.COMPLETED -> "Completed" - StatusFilter.PENDING -> "Pending" - StatusFilter.FAILED -> "Failed" + StatusFilter.COMPLETED -> strings.filterStatusCompleted // ← "Completed" + StatusFilter.PENDING -> strings.filterStatusPending // ← "Pending" + StatusFilter.FAILED -> strings.filterStatusFailed // ← "Failed" } ) } @@ -110,11 +114,11 @@ internal fun FilterBottomSheet( } } - // ── Type (dynamic, only shown when tags exist) ─────────────────── + // ── Type (dynamic, only shown when tags exist) ──────────────────── if (availableTypes.isNotEmpty()) { Spacer(Modifier.height(20.dp)) Text( - text = "Type", + text = strings.filterSectionType, // ← "Type" style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -131,9 +135,9 @@ internal fun FilterBottomSheet( } Spacer(Modifier.height(20.dp)) - // ── Amount (sats) ──────────────────────────────────────────────── + // ── Amount (sats) ───────────────────────────────────────────────── Text( - text = "Amount (sats)", + text = strings.filterSectionAmount, // ← "Amount (sats)" style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -154,8 +158,8 @@ internal fun FilterBottomSheet( OutlinedTextField( value = minText, onValueChange = { minText = it.filter(Char::isDigit); commitAmount() }, - label = { Text("Min") }, - suffix = { Text("sats") }, + label = { Text(strings.filterAmountMin_label) }, // ← "Min" + suffix = { Text(strings.filterAmountSuffix) }, // ← "sats" singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f) @@ -164,8 +168,8 @@ internal fun FilterBottomSheet( OutlinedTextField( value = maxText, onValueChange = { maxText = it.filter(Char::isDigit); commitAmount() }, - label = { Text("Max") }, - suffix = { Text("sats") }, + label = { Text(strings.filterAmountMax_label) }, // ← "Max" + suffix = { Text(strings.filterAmountSuffix) }, // ← "sats" singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f) @@ -173,9 +177,9 @@ internal fun FilterBottomSheet( } Spacer(Modifier.height(20.dp)) - // ── Date ───────────────────────────────────────────────────────── + // ── Date ────────────────────────────────────────────────────────── Text( - text = "Date", + text = strings.filterSectionDate, // ← "Date" style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -185,9 +189,9 @@ internal fun FilterBottomSheet( Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { DatePreset.entries.forEach { preset -> val label = when (preset) { - DatePreset.THIS_WEEK -> "This week" - DatePreset.THIS_MONTH -> "This month" - DatePreset.THIS_YEAR -> "This year" + DatePreset.THIS_WEEK -> strings.filterDateThisWeek // ← "This week" + DatePreset.THIS_MONTH -> strings.filterDateThisMonth // ← "This month" + DatePreset.THIS_YEAR -> strings.filterDateThisYear // ← "This year" } FilterChip( selected = currentFilter.datePreset == preset, @@ -213,14 +217,22 @@ internal fun FilterBottomSheet( onClick = { showFromPicker = true }, modifier = Modifier.weight(1f) ) { - Text(currentFilter.minCreatedAt?.let { formatEpochShort(it) } ?: "From") + Text( + currentFilter.minCreatedAt + ?.let { formatEpochShort(it) } + ?: strings.filterDateFrom_label // ← "From" + ) } Text("–", style = MaterialTheme.typography.bodyLarge) OutlinedButton( onClick = { showToPicker = true }, modifier = Modifier.weight(1f) ) { - Text(currentFilter.maxCreatedAt?.let { formatEpochShort(it) } ?: "To") + Text( + currentFilter.maxCreatedAt + ?.let { formatEpochShort(it) } + ?: strings.filterDateTo_label // ← "To" + ) } } @@ -235,10 +247,12 @@ internal fun FilterBottomSheet( val epochSec = pickerState.selectedDateMillis?.div(1000) onDateFilterSet(epochSec, currentFilter.maxCreatedAt) showFromPicker = false - }) { Text("OK") } + }) { Text(strings.filterDialogOk) } // ← "OK" }, dismissButton = { - TextButton(onClick = { showFromPicker = false }) { Text("Cancel") } + TextButton(onClick = { showFromPicker = false }) { + Text(strings.filterDialogCancel) // ← "Cancel" + } } ) { DatePicker(state = pickerState) } } @@ -255,10 +269,12 @@ internal fun FilterBottomSheet( val epochSec = pickerState.selectedDateMillis?.div(1000)?.let { it + 86399L } onDateFilterSet(currentFilter.minCreatedAt, epochSec) showToPicker = false - }) { Text("OK") } + }) { Text(strings.filterDialogOk) } // ← "OK" }, dismissButton = { - TextButton(onClick = { showToPicker = false }) { Text("Cancel") } + TextButton(onClick = { showToPicker = false }) { + Text(strings.filterDialogCancel) // ← "Cancel" + } } ) { DatePicker(state = pickerState) } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt index 24dd1a1..97b7f1c 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.api.PaymentRecord +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.HistoryState import kotlinx.coroutines.launch @@ -68,6 +69,7 @@ fun HistoryScreen( onClose : () -> Unit, onPaymentClick : (PaymentRecord) -> Unit ) { + val strings = LocalAppStrings.current val state by vm.filteredState.collectAsState() val filter by vm.filter.collectAsState() val fiatCurrency by vm.fiatCurrency.collectAsState() @@ -86,12 +88,12 @@ fun HistoryScreen( Scaffold( topBar = { TopAppBar( - title = { Text("History") }, + title = { Text(strings.historyTitle) }, // ← "History" navigationIcon = { IconButton(onClick = onClose) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Close" + contentDescription = strings.historyClose // ← "Close" ) } }, @@ -105,7 +107,7 @@ fun HistoryScreen( }) { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search payments" + contentDescription = strings.historySearchPayments // ← "Search payments" ) } } @@ -115,7 +117,7 @@ fun HistoryScreen( IconButton(onClick = { filterSheetVisible = true }) { Icon( imageVector = Icons.Default.FilterList, - contentDescription = "Filter payments" + contentDescription = strings.historyFilterPayments // ← "Filter payments" ) } } @@ -154,8 +156,8 @@ fun HistoryScreen( val searchActive = filter.searchQuery.isNotBlank() if (isFiltered || searchActive) { ActiveFilterChips( - filter = filter, - searchActive = searchActive, + filter = filter, + searchActive = searchActive, onClearDirection = { vm.setDirectionFilter(DirectionFilter.ALL) }, onClearStatus = { vm.toggleStatusFilter(it) }, onClearType = { vm.toggleTypeFilter(it) }, @@ -187,7 +189,9 @@ fun HistoryScreen( style = MaterialTheme.typography.bodyMedium ) Spacer(Modifier.height(12.dp)) - Button(onClick = { vm.refresh() }) { Text("Retry") } + Button(onClick = { vm.refresh() }) { + Text(strings.historyRetry) // ← "Retry" + } } } @@ -205,7 +209,7 @@ fun HistoryScreen( if (s.payments.isEmpty()) { Text( - text = "No payments yet.", + text = strings.historyNoPayments, // ← "No payments yet." modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -294,6 +298,7 @@ private fun SearchBar( onClear : () -> Unit, onInfoClick : () -> Unit ) { + val strings = LocalAppStrings.current Row( modifier = Modifier .fillMaxWidth() @@ -303,13 +308,13 @@ private fun SearchBar( IconButton(onClick = onInfoClick) { Icon( imageVector = Icons.Default.Info, - contentDescription = "Search help" + contentDescription = strings.historySearchHelp // ← "Search help" ) } TextField( value = query, onValueChange = onQueryChange, - placeholder = { Text("Search transactions") }, + placeholder = { Text(strings.historySearchPlaceholder) }, // ← "Search transactions" singleLine = true, colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, @@ -330,7 +335,7 @@ private fun SearchBar( }) { Icon( imageVector = Icons.Default.ContentPaste, - contentDescription = "Paste" + contentDescription = strings.historySearchPaste // ← "Paste" ) } IconButton( @@ -339,7 +344,7 @@ private fun SearchBar( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear search", + contentDescription = strings.historySearchClear, // ← "Clear search" tint = if (query.isNotEmpty()) LocalContentColor.current else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) ) @@ -360,6 +365,7 @@ private fun ActiveFilterChips( onClearDate : () -> Unit, onClearSearch : () -> Unit ) { + val strings = LocalAppStrings.current FlowRow( modifier = Modifier .fillMaxWidth() @@ -374,15 +380,15 @@ private fun ActiveFilterChips( label = { Text( when (filter.direction) { - DirectionFilter.OUTGOING -> "Outgoing" - else -> "Incoming" + DirectionFilter.OUTGOING -> strings.filterDirectionOutgoing // ← "Outgoing" + else -> strings.filterDirectionIncoming // ← "Incoming" } ) }, trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear direction filter", + contentDescription = strings.filterClearDirection, // ← "Clear direction filter" modifier = Modifier.size(16.dp) ) } @@ -395,16 +401,16 @@ private fun ActiveFilterChips( label = { Text( when (s) { - StatusFilter.COMPLETED -> "Completed" - StatusFilter.PENDING -> "Pending" - StatusFilter.FAILED -> "Failed" + StatusFilter.COMPLETED -> strings.filterStatusCompleted // ← "Completed" + StatusFilter.PENDING -> strings.filterStatusPending // ← "Pending" + StatusFilter.FAILED -> strings.filterStatusFailed // ← "Failed" } ) }, trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Remove status filter", + contentDescription = strings.filterRemoveStatus, // ← "Remove status filter" modifier = Modifier.size(16.dp) ) } @@ -418,7 +424,7 @@ private fun ActiveFilterChips( trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Remove type filter", + contentDescription = strings.filterRemoveType, // ← "Remove type filter" modifier = Modifier.size(16.dp) ) } @@ -427,11 +433,11 @@ private fun ActiveFilterChips( if (filter.minAmountSat != null || filter.maxAmountSat != null) { val label = when { filter.minAmountSat != null && filter.maxAmountSat != null -> - "${filter.minAmountSat}–${filter.maxAmountSat} sats" + strings.filterAmountRange(filter.minAmountSat, filter.maxAmountSat) // ← "X–Y sats" filter.minAmountSat != null -> - "≥ ${filter.minAmountSat} sats" + strings.filterAmountMin(filter.minAmountSat) // ← "≥ X sats" else -> - "≤ ${filter.maxAmountSat} sats" + strings.filterAmountMax(filter.maxAmountSat!!) // ← "≤ X sats" } InputChip( selected = true, @@ -440,7 +446,7 @@ private fun ActiveFilterChips( trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear amount filter", + contentDescription = strings.filterClearAmount, // ← "Clear amount filter" modifier = Modifier.size(16.dp) ) } @@ -450,16 +456,19 @@ private fun ActiveFilterChips( val maxCreatedAt = filter.maxCreatedAt if (minCreatedAt != null || maxCreatedAt != null) { val label = when (filter.datePreset) { - DatePreset.THIS_WEEK -> "This week" - DatePreset.THIS_MONTH -> "This month" - DatePreset.THIS_YEAR -> "This year" + DatePreset.THIS_WEEK -> strings.filterDateThisWeek // ← "This week" + DatePreset.THIS_MONTH -> strings.filterDateThisMonth // ← "This month" + DatePreset.THIS_YEAR -> strings.filterDateThisYear // ← "This year" null -> when { minCreatedAt != null && maxCreatedAt != null -> - "${formatEpochShort(minCreatedAt)}–${formatEpochShort(maxCreatedAt)}" + strings.filterDateRange( // ← "X–Y" + formatEpochShort(minCreatedAt), + formatEpochShort(maxCreatedAt) + ) minCreatedAt != null -> - "From ${formatEpochShort(minCreatedAt)}" + strings.filterDateFrom(formatEpochShort(minCreatedAt)) // ← "From X" else -> - "Until ${formatEpochShort(maxCreatedAt!!)}" + strings.filterDateUntil(formatEpochShort(maxCreatedAt!!)) // ← "Until X" } } InputChip( @@ -469,7 +478,7 @@ private fun ActiveFilterChips( trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear date filter", + contentDescription = strings.filterClearDate, // ← "Clear date filter" modifier = Modifier.size(16.dp) ) } @@ -479,11 +488,11 @@ private fun ActiveFilterChips( InputChip( selected = true, onClick = onClearSearch, - label = { Text("search: …${filter.searchQuery.takeLast(8)}") }, + label = { Text(strings.historySearchChip(filter.searchQuery.takeLast(8))) }, // ← "search: …XXXXXXXX" trailingIcon = { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear search", + contentDescription = strings.historyClearSearch, // ← "Clear search" modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt index cb85da1..b5f652f 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.bitcointxoko.gudariwallet.api.PaymentRecord +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.DetailState import com.bitcointxoko.gudariwallet.util.feePpm import com.bitcointxoko.gudariwallet.util.formatFiatForSats @@ -57,6 +58,7 @@ fun PaymentDetailScreen( vm : HistoryViewModel, onBack : () -> Unit ) { + val strings = LocalAppStrings.current val detailState by vm.detailState.collectAsState() val fiatCurrency by vm.fiatCurrency.collectAsState() val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState() @@ -82,10 +84,18 @@ fun PaymentDetailScreen( Scaffold( topBar = { TopAppBar( - title = { Text(if (payment.isOutgoing) "Outgoing payment" else "Incoming payment") }, + title = { + Text( + if (payment.isOutgoing) strings.detailOutgoingTitle // ← "Outgoing payment" + else strings.detailIncomingTitle // ← "Incoming payment" + ) + }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = strings.detailBack // ← "Back" + ) } } ) @@ -106,7 +116,7 @@ fun PaymentDetailScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Could not load full details", + text = strings.detailCouldNotLoad, // ← "Could not load full details" color = MaterialTheme.colorScheme.onErrorContainer, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(12.dp) @@ -141,6 +151,7 @@ private fun PaymentDetailContent( fiatSatsPerUnit : Double?, modifier : Modifier = Modifier ) { + val strings = LocalAppStrings.current val amountColor = paymentAmountColor(payment.status, payment.isOutgoing) val context = LocalContext.current @@ -195,11 +206,12 @@ private fun PaymentDetailContent( amountMsat = kotlin.math.abs(payment.amountMsat), feeMsat = kotlin.math.abs(payment.feeMsat) ) - val feeLabel = buildString { - append("Fee: ${payment.feeSat} sats") - if (feeFiat != null) append(" ($feeFiat)") - if (ppm != null) append(" · $ppm ppm") - } + // Build the optional parts separately so the lambda stays clean + val fiatPart = if (feeFiat != null) " ($feeFiat)" else "" + val ppmPart = if (ppm != null) " · $ppm ppm" else "" + val feeLabel = strings.detailFeeLabel( // ← "Fee: X sats (fiat) · Y ppm" + payment.feeSat, fiatPart, ppmPart + ) Spacer(Modifier.height(4.dp)) Text( text = feeLabel, @@ -215,14 +227,17 @@ private fun PaymentDetailContent( // ── Basic info ─────────────────────────────────────────────────────── item { - DetailSection(title = "Details") { - DetailRow("Date", formatTimestamp(payment.createdAt, payment.time)) - DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—") + DetailSection(title = strings.detailSectionDetails) { // ← "Details" + DetailRow(strings.detailRowDate, formatTimestamp(payment.createdAt, payment.time)) // ← "Date" + DetailRow( + strings.detailRowMemo, // ← "Memo" + payment.memo?.takeIf { it.isNotBlank() } ?: strings.detailRowMemoEmpty // ← "—" + ) if (payment.extra?.tag != null) { - DetailRow("Type", payment.extra.tag) + DetailRow(strings.detailRowType, payment.extra.tag) // ← "Type" } if (payment.extra?.comment?.isNotBlank() == true) { - DetailRow("Comment", payment.extra.comment) + DetailRow(strings.detailRowComment, payment.extra.comment) // ← "Comment" } } } @@ -230,20 +245,20 @@ private fun PaymentDetailContent( // ── Success action (LNURL) ─────────────────────────────────────────── payment.extra?.successAction?.let { action -> item { - DetailSection(title = "Success Action") { - action.message?.let { DetailRow("Message", it) } - action.url?.let { DetailRow("URL", it) } - action.description?.let { DetailRow("Description", it) } + DetailSection(title = strings.detailSectionSuccessAction) { // ← "Success Action" + action.message?.let { DetailRow(strings.detailSuccessMessage, it) } // ← "Message" + action.url?.let { DetailRow(strings.detailSuccessUrl, it) } // ← "URL" + action.description?.let { DetailRow(strings.detailSuccessDescription, it) } // ← "Description" } } } // ── Technical ──────────────────────────────────────────────────────── item { - DetailSection(title = "Technical") { + DetailSection(title = strings.detailSectionTechnical) { // ← "Technical" DetailRow( - label = "Payment Hash", - value = payment.paymentHash, + label = strings.detailRowPaymentHash, // ← "Payment Hash" + value = payment.paymentHash, monospace = true, copyable = true, onCopy = { @@ -256,8 +271,8 @@ private fun PaymentDetailContent( ) if (!payment.preimage.isNullOrBlank() && payment.preimage != "0".repeat(64)) { DetailRow( - label = "Preimage", - value = payment.preimage, + label = strings.detailRowPreimage, // ← "Preimage" + value = payment.preimage, monospace = true, copyable = true, onCopy = { @@ -273,11 +288,11 @@ private fun PaymentDetailContent( val ambossUrl = "https://amboss.space/node/$pubkey" val label = alias ?: "${pubkey.take(8)}…${pubkey.takeLast(8)}" DetailRow( - label = "Destination", - value = label, - valueColor = MaterialTheme.colorScheme.primary, - textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline, - onValueClick = { + label = strings.detailRowDestination, // ← "Destination" + value = label, + valueColor = MaterialTheme.colorScheme.primary, + textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline, + onValueClick = { context.startActivity(Intent(Intent.ACTION_VIEW, ambossUrl.toUri())) }, trailingContent = { @@ -293,7 +308,7 @@ private fun PaymentDetailContent( ) { Icon( imageVector = Icons.Outlined.ContentCopy, - contentDescription = "Copy node ID", + contentDescription = strings.detailCopyNodeId, // ← "Copy node ID" modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -303,8 +318,8 @@ private fun PaymentDetailContent( } if (!payment.bolt11.isNullOrBlank()) { DetailRow( - label = "BOLT11", - value = payment.bolt11, + label = strings.detailRowBolt11, // ← "BOLT11" + value = payment.bolt11, monospace = true, copyable = true, maxLines = 3, diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt index d264ddb..eba07cd 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.api.PaymentRecord +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.util.formatFiatForSats @@ -33,8 +34,9 @@ internal fun PaymentRow( fiatSatsPerUnit : Double?, onClick : () -> Unit ) { + val strings = LocalAppStrings.current val isPending = isPendingStatus(payment.status) - val semantic = semanticColors() + val semantic = semanticColors() val amountColor = paymentAmountColor(payment.status, payment.isOutgoing) val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward @@ -54,7 +56,8 @@ internal fun PaymentRow( ) { Icon( imageVector = icon, - contentDescription = if (payment.isOutgoing) "Sent" else "Received", + contentDescription = if (payment.isOutgoing) strings.paymentRowSentCD // ← "Sent" + else strings.paymentRowReceivedCD, // ← "Received" tint = amountColor, modifier = Modifier.size(20.dp) ) @@ -62,7 +65,8 @@ internal fun PaymentRow( Column(modifier = Modifier.weight(1f)) { Text( - text = payment.memo?.takeIf { it.isNotBlank() } ?: "No memo", + text = payment.memo?.takeIf { it.isNotBlank() } + ?: strings.paymentRowNoMemo, // ← "No memo" style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -84,7 +88,7 @@ internal fun PaymentRow( contentColor = semantic.onWarningContainer ) { Text( - text = "Pending", + text = strings.paymentRowPending, // ← "Pending" style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/SearchInfoSheet.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/SearchInfoSheet.kt index 83d2880..7e7daca 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/SearchInfoSheet.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/SearchInfoSheet.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.bitcointxoko.gudariwallet.LocalAppStrings @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -20,6 +21,8 @@ internal fun SearchInfoSheet(visible: Boolean, onDismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) if (!visible) return + val strings = LocalAppStrings.current + ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState @@ -30,55 +33,45 @@ internal fun SearchInfoSheet(visible: Boolean, onDismiss: () -> Unit) { .padding(start = 24.dp, end = 24.dp, bottom = 40.dp) ) { Text( - text = "What can I search for?", + text = strings.searchInfoTitle, // ← "What can I search for?" style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(8.dp)) Text( - text = "Type any part of the following to find a payment:", + text = strings.searchInfoSubtitle, // ← "Type any part of the following…" style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(Modifier.height(20.dp)) SearchInfoItem( - title = "Memo", - description = "The message or description attached to a payment. " + - "For example, \"coffee\" or \"rent March\"." + title = strings.searchInfoMemoTitle, // ← "Memo" + description = strings.searchInfoMemoDesc ) SearchInfoItem( - title = "Invoice", - description = "The payment request you scanned or pasted to send " + - "a payment. It usually starts with \"lnbc…\". " + - "You can paste just the first or last few characters." + title = strings.searchInfoInvoiceTitle, // ← "Invoice" + description = strings.searchInfoInvoiceDesc ) SearchInfoItem( - title = "Hash", - description = "A unique code that identifies this payment on the " + - "Lightning Network. Useful if someone asks you to " + - "confirm a specific transaction." + title = strings.searchInfoHashTitle, // ← "Hash" + description = strings.searchInfoHashDesc ) SearchInfoItem( - title = "Preimage", - description = "A secret code that proves a payment was received. " + - "Only available after a payment completes successfully." + title = strings.searchInfoPreimageTitle, // ← "Preimage" + description = strings.searchInfoPreimageDesc ) SearchInfoItem( - title = "Node alias", - description = "The name of the Lightning node you sent a payment to. " + - "For example, \"ACINQ\" or \"Wallet of Satoshi\". " + - "Aliases are resolved automatically for outgoing payments." + title = strings.searchInfoNodeAliasTitle, // ← "Node alias" + description = strings.searchInfoNodeAliasDesc ) SearchInfoItem( - title = "Node ID (pubkey)", - description = "The public key of the destination node. " + - "A 66-character hex string — you can paste just the first few characters." + title = strings.searchInfoNodeIdTitle, // ← "Node ID (pubkey)" + description = strings.searchInfoNodeIdDesc ) Spacer(Modifier.height(16.dp)) Text( - text = "You can paste a full value or search with just a few characters — " + - "partial matches work too.", + text = strings.searchInfoFooter, // ← "You can paste a full value…" style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -100,4 +93,4 @@ private fun SearchInfoItem(title: String, description: String) { color = MaterialTheme.colorScheme.onSurfaceVariant ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/AmountDisplay.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/AmountDisplay.kt index dfe5052..0fd1940 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/AmountDisplay.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/AmountDisplay.kt @@ -9,19 +9,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.bitcointxoko.gudariwallet.LocalAppStrings /** * Displays a sats amount in bold with an optional fiat subtitle. + * Uses singular "sat" when amountSats == 1, plural "sats" otherwise. * Used in MemoStepContent, ReceiveInvoiceContent, and ReceiveSuccessContent. */ @Composable internal fun AmountDisplay( - amountSats : Long, - fiatLabel : String?, - style : TextStyle = MaterialTheme.typography.titleLarge + amountSats : Long, + fiatLabel : String?, + style : TextStyle = MaterialTheme.typography.titleLarge ) { + val strings = LocalAppStrings.current + Text( - text = "$amountSats sats", + text = strings.amountDisplaySats(amountSats), // ← "1 sat" or "X sats" style = style, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/BrightnessToggleButton.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/BrightnessToggleButton.kt index e02330b..55439fd 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/BrightnessToggleButton.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/BrightnessToggleButton.kt @@ -7,18 +7,21 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import com.bitcointxoko.gudariwallet.LocalAppStrings @Composable internal fun BrightnessToggleButton( isOn : Boolean, onToggle: () -> Unit ) { + val strings = LocalAppStrings.current + IconButton(onClick = onToggle) { Icon( imageVector = if (isOn) Icons.Filled.BrightnessHigh else Icons.Filled.BrightnessLow, - contentDescription = if (isOn) "Reduce brightness" - else "Boost brightness", + contentDescription = if (isOn) strings.brightnessReduce // ← "Reduce brightness" + else strings.brightnessBoost, // ← "Boost brightness" tint = if (isOn) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/QrDisplayCard.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/QrDisplayCard.kt index d207a0a..96fbe84 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/QrDisplayCard.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/QrDisplayCard.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.graphics.createBitmap import androidx.core.graphics.set +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.util.IntentUtils import com.google.zxing.BarcodeFormat import com.google.zxing.qrcode.QRCodeWriter @@ -69,6 +70,7 @@ internal fun QrDisplayCard( nfcEnabled : Boolean = true, qrModifier : Modifier = Modifier.size(260.dp) ) { + val strings = LocalAppStrings.current val bitmap = remember(content) { generateQrBitmap(content).asImageBitmap() } val context = LocalContext.current val clipboard = LocalClipboard.current @@ -79,7 +81,7 @@ internal fun QrDisplayCard( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // ── QR image ───────────────────────────────────────────────────────── + // ── QR image ────────────────────────────────────────────────────────── Image( bitmap = bitmap, contentDescription = contentDescription, @@ -101,7 +103,7 @@ internal fun QrDisplayCard( ) { Icon( imageVector = Icons.Filled.Share, - contentDescription = "Share", + contentDescription = strings.qrShare, // ← "Share" tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -118,34 +120,35 @@ internal fun QrDisplayCard( ) { Icon( imageVector = Icons.Filled.ContentCopy, - contentDescription = "Copy", + contentDescription = strings.qrCopy, // ← "Copy" tint = MaterialTheme.colorScheme.onSurfaceVariant ) } // Brightness - FilledTonalIconButton( - onClick = onToggleBrightness - ) { + FilledTonalIconButton(onClick = onToggleBrightness) { Icon( imageVector = Icons.Filled.WbSunny, - contentDescription = if (brightnessOn) "Reduce brightness" else "Boost brightness", + contentDescription = if (brightnessOn) strings.brightnessReduce // ← "Reduce brightness" + else strings.brightnessBoost, // ← "Boost brightness" tint = if (brightnessOn) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onSurfaceVariant ) } // NFC — only shown when caller passes isNfcEmulating if (isNfcEmulating != null && onToggleNfc != null) { FilledTonalIconButton( - onClick = onToggleNfc, - enabled = nfcEnabled + onClick = onToggleNfc, + enabled = nfcEnabled ) { Icon( - imageVector = if (isNfcEmulating) Icons.Filled.Nfc else Icons.Outlined.Nfc, - contentDescription = if (isNfcEmulating) "Stop NFC sharing" else "Share via NFC", + imageVector = if (isNfcEmulating) Icons.Filled.Nfc + else Icons.Outlined.Nfc, + contentDescription = if (isNfcEmulating) strings.qrNfcStop // ← "Stop NFC sharing" + else strings.qrNfcStart, // ← "Share via NFC" tint = if (isNfcEmulating) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -153,7 +156,7 @@ internal fun QrDisplayCard( } } -// ── QR generation ──────────────────────────────────────────────────────────── +// ── QR generation ───────────────────────────────────────────────────────────── private const val QR_BITMAP_SIZE = 512 diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveScreen.kt index 8ca52e3..cdb7ab5 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveScreen.kt @@ -133,7 +133,11 @@ fun ReceiveScreen( onToggleBrightness = onToggleBrightness, isLoading = receiveState is ReceiveState.AwaitingInvoice, onCreateInvoice = { amountSats, memo -> - vm.createInvoice(amountSats, memo) + vm.createInvoice( + amountSats, + memo, + failedToCreateInvoice = strings.receiveVmFailedToCreateInvoice + ) } ) } @@ -146,7 +150,10 @@ fun ReceiveScreen( k1 = state.k1, callback = state.callback, amountSats = amountSats, - memo = state.defaultDescription + memo = state.defaultDescription, + failedToCreateInvoice = strings.receiveVmFailedToCreateInvoice, + withdrawRejected = strings.receiveVmWithdrawRejected, + withdrawFailed = strings.receiveVmWithdrawFailed, ) }, onDismiss = { vm.resetReceiveState() } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveViewModel.kt index b6c092e..909c034 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/receive/ReceiveViewModel.kt @@ -21,7 +21,8 @@ class ReceiveViewModel( private val repo: WalletRepository, ) : ViewModel() { - private val _receiveState = MutableStateFlow(ReceiveState.Idle) + private val _receiveState = + MutableStateFlow(ReceiveState.Idle) val receiveState: StateFlow = _receiveState private val _navigateToReceive = MutableSharedFlow(extraBufferCapacity = 1) @@ -40,8 +41,16 @@ class ReceiveViewModel( } } - // ── Invoice creation ───────────────────────────────────────────────────── - fun createInvoice(amountSats: Long, memo: String) { + // ── Invoice creation ────────────────────────────────────────────────────── + /** + * @param failedToCreateInvoice Localised error string from the call site, + * e.g. `strings.receiveVmFailedToCreateInvoice` + */ + fun createInvoice( + amountSats : Long, + memo : String, + failedToCreateInvoice : String, // ← injected from Composable + ) { viewModelScope.launch { _receiveState.value = ReceiveState.AwaitingInvoice runCatching { repo.createInvoice(amountSats, memo) } @@ -64,17 +73,21 @@ class ReceiveViewModel( } .onFailure { e -> Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}") - _receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice")) + _receiveState.value = ReceiveState.Error( + parseApiError(e, failedToCreateInvoice) // ← localised + ) } } } fun resetReceiveState() { _receiveState.value = ReceiveState.Idle } - // ── LNURL-withdraw ─────────────────────────────────────────────────────── + // ── LNURL-withdraw ──────────────────────────────────────────────────────── - // Called from WalletViewModel's scan handlers when a withdrawRequest is detected. - fun handleWithdrawResponse(raw: com.bitcointxoko.gudariwallet.api.LnurlScanResponse, lnurl: String) { + fun handleWithdrawResponse( + raw : com.bitcointxoko.gudariwallet.api.LnurlScanResponse, + lnurl : String + ) { val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT _receiveState.value = ReceiveState.LnurlWithdrawReady( @@ -90,20 +103,31 @@ class ReceiveViewModel( Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive") } + /** + * @param failedToCreateInvoice `strings.receiveVmFailedToCreateInvoice` + * @param withdrawRejected `strings.receiveVmWithdrawRejected` + * @param withdrawFailed `strings.receiveVmWithdrawFailed` + */ fun executeWithdraw( - k1 : String, - callback : String, - amountSats: Long, - memo : String + k1 : String, + callback : String, + amountSats : Long, + memo : String, + failedToCreateInvoice : String, // ← injected from Composable + withdrawRejected : String, // ← injected from Composable + withdrawFailed : String, // ← injected from Composable ) { viewModelScope.launch { _receiveState.value = ReceiveState.AwaitingInvoice val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) } if (invoiceResult.isFailure) { - val e = invoiceResult.exceptionOrNull() ?: Exception("Failed to create invoice") + val e = invoiceResult.exceptionOrNull() + ?: Exception(failedToCreateInvoice) Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}") - _receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice")) + _receiveState.value = ReceiveState.Error( + parseApiError(e, failedToCreateInvoice) // ← localised + ) return@launch } val invoice = invoiceResult.getOrThrow() @@ -122,15 +146,17 @@ class ReceiveViewModel( ) } else { val reason = response.reason?.ifBlank { null } - ?: "Withdraw service rejected the invoice" + ?: withdrawRejected // ← localised Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason") _receiveState.value = ReceiveState.Error(reason) } } .onFailure { e -> Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}") - _receiveState.value = ReceiveState.Error(parseApiError(e, "Withdraw failed")) + _receiveState.value = ReceiveState.Error( + parseApiError(e, withdrawFailed) // ← localised + ) } } } -} \ No newline at end of file +}