From 262d5f068d30b6af5fc7db07f5700dd4187f48c5 Mon Sep 17 00:00:00 2001 From: rasputin Date: Fri, 12 Jun 2026 20:50:36 +0200 Subject: [PATCH] localisation: nwc --- .../gudariwallet/i18n/EnStrings.kt | 60 ++++++++++++++- .../gudariwallet/i18n/EsStrings.kt | 58 +++++++++++++++ .../gudariwallet/i18n/EuStrings.kt | 60 ++++++++++++++- .../bitcointxoko/gudariwallet/i18n/Strings.kt | 73 +++++++++++++++++-- .../gudariwallet/ui/WalletScreen.kt | 4 +- .../ui/nwc/NwcAddConnectionSheet.kt | 68 +++++++++++------ .../gudariwallet/ui/nwc/NwcConnectionRow.kt | 29 +++++--- .../gudariwallet/ui/nwc/NwcModels.kt | 25 +++---- .../gudariwallet/ui/nwc/NwcPairingDialog.kt | 16 ++-- .../gudariwallet/ui/nwc/NwcScreen.kt | 35 ++++++--- .../gudariwallet/ui/nwc/NwcStates.kt | 11 +-- .../gudariwallet/ui/nwc/NwcViewModel.kt | 29 ++++---- .../gudariwallet/util/DateTimeUtils.kt | 14 ++++ 13 files changed, 387 insertions(+), 95 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 1abce6b..67c9492 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EnStrings.kt @@ -379,6 +379,9 @@ val EnHomeStrings = AppStrings( connectionRowExpires = "Expires", connectionRowPubkey = "Public key", connectionNeverUsed = "Never", + lastUsedToday = "Last used today", + lastUsedYesterday = "Last used yesterday", + lastUsedDate = "Last used %s", connectionNoExpiry = "Never", connectionExpired = { date -> "Expired · $date" }, connectionSectionPermissions = "Permissions", @@ -400,6 +403,10 @@ val EnHomeStrings = AppStrings( permSignMessage = "Sign messages", permPay = "Send payments", permInvoice = "Create invoices", + permLookup = "Lookup invoice status", + permHistory = "Read transaction history", + permBalance = "Read wallet balance", + permInfo = "Read account info", permLookupHistory = "Lookup status & transaction history", permBalanceInfo = "Read balance & account info", permSendPayments = "Send payments", @@ -412,6 +419,57 @@ val EnHomeStrings = AppStrings( revokeDialogBody = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.", revokeDialogConfirm = "Revoke", revokeDialogDismiss = "Cancel", - ) + refreshDaily = "Daily", + refreshWeekly = "Weekly", + refreshMonthly = "Monthly", + refreshYearly = "Yearly", + refreshNever = "Never", + + newConnection = "New connection", + labelHint = "Label (e.g. Amethyst, Bitrefill)", + expires = "Expires", + pickDate = "Pick date", + pickTime = "Pick time", + never = "Never", + budgets = "Budgets", + addBudget = "Add budget", + removeBudget = "Remove budget", + permissions = "Permissions", + unnamed = "Unnamed", + creating = "Creating…", + createConnection = "Create connection", + ok = "OK", + sats = "Sats", + resets = "Resets", + + deleteConnection = "Delete connection", + unnamedConnection = "Unnamed connection", + neverUsed = "Never used", + expired = "Expired", + + connectionCreated = "Connection created", + scanOrCopyHint = "Scan or copy this connection string into your app. It will not be shown again.", + nwcPairingQrCode = "NWC pairing QR code", + nwcConnectionString = "NWC connection string", + shareNwcConnectionString = "Share NWC connection string", + openInApp = "Open in app", + + walletConnect = "Wallet Connect", + addConnection = "Add connection", + noCompatibleAppFound = "No compatible app found", + removeConnection = "Remove connection?", + removeConnectionDetail = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.", + remove = "Remove", + + noConnectionsYet = "No connections yet", + tapToAddHint = "Tap + to connect an app via Nostr Wallet Connect.", + somethingWentWrong = "Something went wrong", + retry = "Retry", + + failedToLoadConnections = "Failed to load connections", + failedToCreateConnection = "Failed to create connection", + failedToDeleteConnection = "Failed to delete connection", + failedToLoadConnection = "Failed to load connection", + ) ) 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 c4491f5..0845e5c 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EsStrings.kt @@ -380,6 +380,9 @@ val EsHomeStrings = AppStrings( connectionRowExpires = "Vence", connectionRowPubkey = "Clave pública", connectionNeverUsed = "Nunca", + lastUsedToday = "Usada hoy", + lastUsedYesterday = "Usada ayer", + lastUsedDate = "Usada %s", connectionNoExpiry = "Sin vencimiento", connectionExpired = { date -> "Vencido · $date" }, connectionSectionPermissions = "Permisos", @@ -401,6 +404,10 @@ val EsHomeStrings = AppStrings( permSignMessage = "Firmar mensajes", permPay = "Enviar pagos", permInvoice = "Crear facturas", + permLookup = "Consultar estado de facturas", + permHistory = "Leer historial de transacciones", + permBalance = "Leer saldo del monedero", + permInfo = "Leer información de cuenta", permLookupHistory = "Consultar estado e historial de transacciones", permBalanceInfo = "Ver saldo e información de cuenta", permSendPayments = "Enviar pagos", @@ -413,5 +420,56 @@ val EsHomeStrings = AppStrings( revokeDialogBody = "Esto revocará permanentemente esta clave Nostr Wallet Connect. Cualquier app que la use perderá el acceso.", revokeDialogConfirm = "Revocar", revokeDialogDismiss = "Cancelar", + refreshDaily = "Diario", + refreshWeekly = "Semanal", + refreshMonthly = "Mensual", + refreshYearly = "Anual", + refreshNever = "Nunca", + + newConnection = "Nueva conexión", + labelHint = "Etiqueta (p.ej. Amethyst, Bitrefill)", + expires = "Caduca", + pickDate = "Elegir fecha", + pickTime = "Elegir hora", + never = "Nunca", + budgets = "Presupuestos", + addBudget = "Añadir presupuesto", + removeBudget = "Eliminar presupuesto", + permissions = "Permisos", + unnamed = "Sin nombre", + creating = "Creando…", + createConnection = "Crear conexión", + ok = "OK", + sats = "Sats", + resets = "Se reinicia", + + deleteConnection = "Eliminar conexión", + unnamedConnection = "Conexión sin nombre", + neverUsed = "Nunca usada", + expired = "Caducada", + + connectionCreated = "Conexión creada", + scanOrCopyHint = "Escanea o copia esta cadena de conexión en tu app. No se volverá a mostrar.", + nwcPairingQrCode = "Código QR de emparejamiento NWC", + nwcConnectionString = "Cadena de conexión NWC", + shareNwcConnectionString = "Compartir cadena de conexión NWC", + openInApp = "Abrir en app", + + walletConnect = "Wallet Connect", + addConnection = "Añadir conexión", + noCompatibleAppFound = "No se encontró una app compatible", + removeConnection = "¿Eliminar conexión?", + removeConnectionDetail = "Esto revocará permanentemente esta clave de Nostr Wallet Connect. Cualquier app que la use perderá el acceso.", + remove = "Eliminar", + + noConnectionsYet = "Aún no hay conexiones", + tapToAddHint = "Toca + para conectar una app vía Nostr Wallet Connect.", + somethingWentWrong = "Algo salió mal", + retry = "Reintentar", + + failedToLoadConnections = "Error al cargar las conexiones", + failedToCreateConnection = "Error al crear la conexión", + failedToDeleteConnection = "Error al eliminar la conexión", + failedToLoadConnection = "Error al cargar la conexión", ) ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EuStrings.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EuStrings.kt index 81e06f2..389c4a9 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EuStrings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/EuStrings.kt @@ -376,6 +376,9 @@ val EuStrings = AppStrings( connectionSectionDetails = "Xehetasunak", connectionRowCreated = "Konektatuta", connectionRowLastUsed = "Azken erabilera", + lastUsedToday = "Duela gutxi erabilia", // "used recently" (idiomatic for today) + lastUsedYesterday = "Atzo erabilia", // "used yesterday" + lastUsedDate = "Duela %s erabilia", // "used [date] ago" connectionRowExpires = "Iraungitze-data", connectionRowPubkey = "Gako publikoa", connectionNeverUsed = "Inoiz ez", @@ -400,6 +403,10 @@ val EuStrings = AppStrings( permSignMessage = "Mezuak sinatu", permPay = "Ordainketak bidali", permInvoice = "Fakturak sortu", + permLookup = "Kontsultatu faktura-egoera", + permHistory = "Irakurri transakzio-historiala", + permBalance = "Irakurri zorroaren saldoa", + permInfo = "Irakurri kontu-informazioa", permLookupHistory = "Egoera eta transakzio-historia kontsultatu", permBalanceInfo = "Saldoa eta kontuaren informazioa ikusi", permSendPayments = "Ordainketak bidali", @@ -412,5 +419,56 @@ val EuStrings = AppStrings( revokeDialogBody = "Honek Nostr Wallet Connect gako hau behin betiko baliogabetuko du. Erabiltzen duen edozein aplikaziok sarbidea galduko du.", revokeDialogConfirm = "Baliogabetu", revokeDialogDismiss = "Utzi", - ) + refreshDaily = "Egunero", + refreshWeekly = "Astero", + refreshMonthly = "Hilabetero", + refreshYearly = "Urtero", + refreshNever = "Inoiz ez", + + newConnection = "Konexio berria", + labelHint = "Etiketa (adb. Amethyst, Bitrefill)", + expires = "Iraungitzen da", + pickDate = "Aukeratu data", + pickTime = "Aukeratu ordua", + never = "Inoiz ez", + budgets = "Aurrekontuak", + addBudget = "Gehitu aurrekontua", + removeBudget = "Kendu aurrekontua", + permissions = "Baimenak", + unnamed = "Izengabea", + creating = "Sortzen…", + createConnection = "Sortu konexioa", + ok = "Ados", + sats = "Sats", + resets = "Berrezarri", + + deleteConnection = "Ezabatu konexioa", + unnamedConnection = "Izengabeko konexioa", + neverUsed = "Inoiz erabili gabea", + expired = "Iraungitua", + + connectionCreated = "Konexioa sortuta", + scanOrCopyHint = "Eskaneatu edo kopiatu konexio-kate hau zure app-ean. Ez da berriro erakutsiko.", + nwcPairingQrCode = "NWC parekatzeko QR kodea", + nwcConnectionString = "NWC konexio-katea", + shareNwcConnectionString = "Partekatu NWC konexio-katea", + openInApp = "Ireki app-ean", + + walletConnect = "Wallet Connect", + addConnection = "Gehitu konexioa", + noCompatibleAppFound = "Ez da app bateragarririk aurkitu", + removeConnection = "Kendu konexioa?", + removeConnectionDetail = "Honek Nostr Wallet Connect gako hau behin betiko indargabetuko du. Erabiltzen duen edozein app-ek sarbidea galduko du.", + remove = "Kendu", + + noConnectionsYet = "Oraindik konexiorik ez", + tapToAddHint = "Sakatu + app bat Nostr Wallet Connect bidez konektatzeko.", + somethingWentWrong = "Zerbait gaizki atera da", + retry = "Saiatu berriro", + + failedToLoadConnections = "Huts egin du konexioak kargatzean", + failedToCreateConnection = "Huts egin du konexioa sortzean", + failedToDeleteConnection = "Huts egin du konexioa ezabatzean", + failedToLoadConnection = "Huts egin du konexioa kargatzean", + ) ) 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 514b930..955f6e4 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/i18n/Strings.kt @@ -383,6 +383,9 @@ data class NwcStrings( val connectionRowExpires : String, val connectionRowPubkey : String, val connectionNeverUsed : String, + val lastUsedToday : String, // e.g. "Last used today" + val lastUsedYesterday : String, // e.g. "Last used yesterday" + val lastUsedDate : String, // e.g. "Last used %s" — %s is a formatted date val connectionNoExpiry : String, /** "Expired · {date}" */ val connectionExpired : (date: String) -> String, @@ -406,12 +409,16 @@ data class NwcStrings( val permLookupInvoice : String, val permListTransactions : String, val permSignMessage : String, - val permPay : String, // "Send payments" - val permInvoice : String, // "Create invoices" - val permLookupHistory: String, // "Lookup status & transaction history" - val permBalanceInfo : String, // "Read balance & account info" - val permSendPayments: String, - val permCreateInvoices: String, + val permPay : String, // "Send payments" + val permInvoice : String, // "Create invoices" + val permLookup : String, + val permHistory : String, + val permBalance : String, + val permInfo : String, + val permLookupHistory : String, // "Lookup status & transaction history" + val permBalanceInfo : String, // "Read balance & account info" + val permSendPayments : String, + val permCreateInvoices : String, val permLookupInvoiceStatus: String, val permReadTransactionHistory: String, val permReadWalletBalance: String, @@ -420,4 +427,56 @@ data class NwcStrings( val revokeDialogBody : String, val revokeDialogConfirm : String, val revokeDialogDismiss : String, -) \ No newline at end of file + val refreshDaily : String, + val refreshWeekly : String, + val refreshMonthly : String, + val refreshYearly : String, + val refreshNever : String, + + // New Connection + val newConnection : String, + val labelHint : String, + val expires : String, + val pickDate : String, + val pickTime : String, + val never : String, + val budgets : String, + val addBudget : String, + val removeBudget : String, + val permissions : String, + val unnamed : String, + val creating : String, + val createConnection : String, + val ok : String, + val sats : String, + val resets : String, + + val deleteConnection : String, + val unnamedConnection : String, + val neverUsed : String, + val expired : String, + + val connectionCreated : String, + val scanOrCopyHint : String, + val nwcPairingQrCode : String, + val nwcConnectionString : String, + val shareNwcConnectionString : String, + val openInApp : String, + + val walletConnect : String, + val addConnection : String, + val noCompatibleAppFound : String, + val removeConnection : String, + val removeConnectionDetail: String, + val remove : String, + + val noConnectionsYet : String, + val tapToAddHint : String, + val somethingWentWrong : String, + val retry : String, + + val failedToLoadConnections : String, + val failedToCreateConnection : String, + val failedToDeleteConnection : String, + val failedToLoadConnection : String, + ) \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt index ddafcef..d6a4c2b 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt @@ -461,7 +461,7 @@ fun WalletScreen( val connectionState by nwcVm.connectionDetailState.collectAsStateWithLifecycle() - LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey) } + LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey, strings) } DisposableEffect(Unit) { onDispose { nwcVm.clearConnectionDetail() } } when (val state = connectionState) { @@ -491,7 +491,7 @@ fun WalletScreen( budgets = state.budgets, onBack = { navController.popBackStack() }, onRevoke = { - nwcVm.deleteConnection(pubkey) + nwcVm.deleteConnection(pubkey, strings) navController.popBackStack() } ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcAddConnectionSheet.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcAddConnectionSheet.kt index ac64ac2..037f0da 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcAddConnectionSheet.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcAddConnectionSheet.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.api.NwcNewBudget +import com.bitcointxoko.gudariwallet.i18n.AppStrings +import com.bitcointxoko.gudariwallet.strings import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -41,6 +43,16 @@ private class AddConnectionState { expiry.atZone(ZoneId.systemDefault()).toEpochSecond() } +private fun NwcPermission.displayLabel(strings: AppStrings): String = when (serverKey) { + "pay" -> strings.nwc.permPay + "invoice" -> strings.nwc.permInvoice + "lookup" -> strings.nwc.permLookup + "history" -> strings.nwc.permHistory + "balance" -> strings.nwc.permBalance + "info" -> strings.nwc.permInfo + else -> serverKey +} + // ── Sheet ───────────────────────────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class) @@ -58,6 +70,7 @@ internal fun NwcAddConnectionSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val state = remember { AddConnectionState() } + if (state.showDatePicker) { val initialMillis = state.expiry .atZone(ZoneId.systemDefault()) @@ -81,10 +94,10 @@ internal fun NwcAddConnectionSheet( .withDayOfMonth(picked.dayOfMonth) } state.showDatePicker = false - }) { Text("OK") } + }) { Text(strings.nwc.ok) } }, dismissButton = { - TextButton(onClick = { state.showDatePicker = false }) { Text("Cancel") } + TextButton(onClick = { state.showDatePicker = false }) { Text(strings.nwc.cancel) } } ) { DatePicker(state = datePickerState) } } @@ -105,10 +118,10 @@ internal fun NwcAddConnectionSheet( .withSecond(0) .withNano(0) state.showTimePicker = false - }) { Text("OK") } + }) { Text(strings.nwc.ok) } }, dismissButton = { - TextButton(onClick = { state.showTimePicker = false }) { Text("Cancel") } + TextButton(onClick = { state.showTimePicker = false }) { Text(strings.nwc.cancel) } }, text = { TimePicker(state = timePickerState) } ) @@ -127,7 +140,7 @@ internal fun NwcAddConnectionSheet( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = "New connection", + text = strings.nwc.newConnection, style = MaterialTheme.typography.titleMedium ) @@ -135,7 +148,7 @@ internal fun NwcAddConnectionSheet( OutlinedTextField( value = state.description, onValueChange = { state.description = it }, - label = { Text("Label (e.g. Amethyst, Bitrefill)") }, + label = { Text(strings.nwc.labelHint) }, singleLine = true, modifier = Modifier.fillMaxWidth() ) @@ -145,7 +158,7 @@ internal fun NwcAddConnectionSheet( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Text(text = "Expires", style = MaterialTheme.typography.bodyMedium) + Text(text = strings.nwc.expires, style = MaterialTheme.typography.bodyMedium) Spacer(Modifier.weight(1f)) if (!state.neverExpires) { @@ -158,7 +171,7 @@ internal fun NwcAddConnectionSheet( icon = { Icon( imageVector = Icons.Default.CalendarToday, - contentDescription = "Pick date", + contentDescription = strings.nwc.pickDate, modifier = Modifier.size(16.dp) ) } @@ -173,7 +186,7 @@ internal fun NwcAddConnectionSheet( icon = { Icon( imageVector = Icons.Default.Schedule, - contentDescription = "Pick time", + contentDescription = strings.nwc.pickTime, modifier = Modifier.size(16.dp) ) } @@ -185,7 +198,7 @@ internal fun NwcAddConnectionSheet( checked = state.neverExpires, onCheckedChange = { state.neverExpires = it } ) - Text(text = "Never", style = MaterialTheme.typography.bodyMedium) + Text(text = strings.nwc.never, style = MaterialTheme.typography.bodyMedium) } Row( @@ -193,7 +206,7 @@ internal fun NwcAddConnectionSheet( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Budgets", + text = strings.nwc.budgets, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -206,11 +219,11 @@ internal fun NwcAddConnectionSheet( ) { Icon( imageVector = Icons.Default.Add, - contentDescription = "Add budget", + contentDescription = strings.nwc.addBudget, modifier = Modifier.size(16.dp) ) Spacer(Modifier.width(4.dp)) - Text("Add budget") + Text(strings.nwc.addBudget) } } @@ -230,7 +243,7 @@ internal fun NwcAddConnectionSheet( // ── Permissions ────────────────────────────────────────────────── Text( - text = "Permissions", + text = strings.nwc.permissions, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -251,11 +264,11 @@ internal fun NwcAddConnectionSheet( } ) Spacer(Modifier.width(8.dp)) - Text(text = perm.displayLabel, style = MaterialTheme.typography.bodyMedium) + Text(text = perm.displayLabel(strings), style = MaterialTheme.typography.bodyMedium) } } } - + val unnamed = strings.nwc.unnamed // ── Create button ───────────────────────────────────────────────── Button( onClick = { @@ -263,7 +276,7 @@ internal fun NwcAddConnectionSheet( else state.expiryEpochSeconds() // #10 val validBudgets = state.budgets.mapNotNull { it.toNwcNewBudget() } onCreate( - state.description.trim().ifBlank { "Unnamed" }, + state.description.trim().ifBlank { unnamed }, state.selectedPermissions.toList(), expiresAtSecs, validBudgets @@ -280,7 +293,7 @@ internal fun NwcAddConnectionSheet( ) Spacer(Modifier.width(8.dp)) } - Text(if (isCreating) "Creating…" else "Create connection") + Text(if (isCreating) strings.nwc.creating else strings.nwc.createConnection) } } } @@ -288,6 +301,15 @@ internal fun NwcAddConnectionSheet( // ── Budget row ──────────────────────────────────────────────────────────────── +@Composable +fun BudgetRefreshWindow.displayLabel(strings: AppStrings): String = when (this) { + BudgetRefreshWindow.DAILY -> strings.nwc.refreshDaily + BudgetRefreshWindow.WEEKLY -> strings.nwc.refreshWeekly + BudgetRefreshWindow.MONTHLY -> strings.nwc.refreshMonthly + BudgetRefreshWindow.YEARLY -> strings.nwc.refreshYearly + BudgetRefreshWindow.NEVER -> strings.nwc.refreshNever +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun BudgetRow( @@ -313,7 +335,7 @@ private fun BudgetRow( OutlinedTextField( value = draft.amountSats, onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) }, - label = { Text("Sats") }, + label = { Text(strings.nwc.sats) }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f).height(fieldHeight) @@ -325,10 +347,10 @@ private fun BudgetRow( modifier = Modifier.weight(1f).height(fieldHeight) ) { OutlinedTextField( - value = draft.refreshWindow.label, + value = draft.refreshWindow.displayLabel(strings), onValueChange = {}, readOnly = true, - label = { Text("Resets") }, + label = { Text(strings.nwc.resets) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded) }, @@ -343,7 +365,7 @@ private fun BudgetRow( ) { BudgetRefreshWindow.entries.forEach { window -> DropdownMenuItem( - text = { Text(window.label) }, + text = { Text(window.displayLabel(strings)) }, onClick = { onChange(draft.copy(refreshWindow = window)) dropdownExpanded = false @@ -359,7 +381,7 @@ private fun BudgetRow( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Remove budget", + contentDescription = strings.nwc.removeBudget, tint = MaterialTheme.colorScheme.error ) } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt index 19b809a..80beef3 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt @@ -1,5 +1,7 @@ package com.bitcointxoko.gudariwallet.ui.nwc +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,16 +19,19 @@ import com.bitcointxoko.gudariwallet.api.NwcGetResponse import com.bitcointxoko.gudariwallet.strings import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.util.formatEpochShort +import com.bitcointxoko.gudariwallet.util.isToday +import com.bitcointxoko.gudariwallet.util.isYesterday import kotlinx.coroutines.launch import java.time.Instant +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Composable internal fun NwcConnectionRow( - connection : NwcGetResponse, - isDeleting : Boolean, - onDeleteConfirm : () -> Unit, + connection: NwcGetResponse, + isDeleting: Boolean, + onDeleteConfirm: () -> Unit, onConnectionClick: () -> Unit, - modifier : Modifier = Modifier + modifier: Modifier = Modifier ) { val key = connection.data val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond @@ -85,7 +90,7 @@ internal fun NwcConnectionRow( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete connection", + contentDescription = strings.nwc.deleteConnection, tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha) ) } @@ -111,7 +116,7 @@ internal fun NwcConnectionRow( Spacer(Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = key.description.ifBlank { "Unnamed connection" }, + text = key.description.ifBlank { strings.nwc.unnamedConnection }, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -121,9 +126,15 @@ internal fun NwcConnectionRow( MaterialTheme.colorScheme.onSurface ) Spacer(Modifier.height(2.dp)) + val lastUsedText = when { + key.last_used <= 0L -> strings.nwc.neverUsed + isToday(key.last_used) -> strings.nwc.lastUsedToday + isYesterday(key.last_used) -> strings.nwc.lastUsedYesterday + else -> strings.nwc.lastUsedDate.format(formatEpochShort(key.last_used, strings.today, strings.yesterday)) + } Text( - text = if (key.last_used <= 0L) "Never used" - else "Last used ${formatEpochShort(key.last_used, strings.today, strings.yesterday)}", + text = if (key.last_used <= 0L) strings.nwc.neverUsed + else lastUsedText, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -135,7 +146,7 @@ internal fun NwcConnectionRow( contentColor = semantic.onErrorContainer ) { Text( - text = "Expired", + text = strings.nwc.expired, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcModels.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcModels.kt index 2f0d66a..acbb426 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcModels.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcModels.kt @@ -5,17 +5,16 @@ import com.bitcointxoko.gudariwallet.api.NwcNewBudget // ── Available permissions ──────────────────────────────────────────── internal data class NwcPermission( val serverKey : String, // what we send in the permissions array - val displayLabel : String, // shown in the UI val default : Boolean // pre-checked by default ) internal val ALL_PERMISSIONS = listOf( - NwcPermission("pay", "Send payments", default = true), - NwcPermission("invoice", "Create invoices", default = false), - NwcPermission("lookup", "Lookup invoice status", default = false), - NwcPermission("history", "Read transaction history", default = false), - NwcPermission("balance", "Read wallet balance", default = false), - NwcPermission("info", "Read account info", default = false) + NwcPermission("pay", default = true), + NwcPermission("invoice", default = false), + NwcPermission("lookup", default = false), + NwcPermission("history", default = false), + NwcPermission("balance", default = false), + NwcPermission("info", default = false) ) // ── Budget ──────────────────────────────────────────────────────────── @@ -24,12 +23,12 @@ internal val ALL_PERMISSIONS = listOf( * Transient UI state for a single budget row in the "Add connection" sheet. * Converted to [NwcNewBudget] on submit. */ -enum class BudgetRefreshWindow(val label: String, val seconds: Int) { - DAILY ("Daily", 86_400), - WEEKLY ("Weekly", 604_800), - MONTHLY ("Monthly", 2_592_000), - YEARLY ("Yearly", 31_536_000), - NEVER ("Never", 0); +enum class BudgetRefreshWindow(val seconds: Int) { + DAILY (86_400), + WEEKLY (604_800), + MONTHLY (2_592_000), + YEARLY (31_536_000), + NEVER (0); } data class BudgetDraft( diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcPairingDialog.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcPairingDialog.kt index fdd03e9..68b7cca 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcPairingDialog.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcPairingDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri +import com.bitcointxoko.gudariwallet.strings import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController import com.bitcointxoko.gudariwallet.ui.common.QrDisplayCard @@ -29,20 +30,21 @@ internal fun NwcPairingUrlDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text("Connection created") }, + title = { Text(strings.nwc.connectionCreated) }, text = { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "Scan or copy this connection string into your app. It will not be shown again.", + text = strings.nwc.scanOrCopyHint, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) + val strings = strings QrDisplayCard( modifier = Modifier.fillMaxWidth(), content = url, - contentDescription = "NWC pairing QR code", - clipLabel = "NWC connection string", - shareTitle = "Share NWC connection string", + contentDescription = strings.nwc.nwcPairingQrCode, + clipLabel = strings.nwc.nwcConnectionString, + shareTitle = strings.nwc.shareNwcConnectionString, textToCopy = url, brightnessOn = brightness.isOn, onToggleBrightness = brightness.toggle, @@ -52,7 +54,7 @@ internal fun NwcPairingUrlDialog( val intent = Intent(Intent.ACTION_VIEW, url.toUri()) try { context.startActivity( - Intent.createChooser(intent, "Open in app") + Intent.createChooser(intent, strings.nwc.openInApp) ) } catch (e: ActivityNotFoundException) { onNoAppFound() // #17: no Toast — caller handles it @@ -63,7 +65,7 @@ internal fun NwcPairingUrlDialog( }, confirmButton = {}, // actions live inside QrDisplayCard dismissButton = { - TextButton(onClick = onDismiss) { Text("Done") } + TextButton(onClick = onDismiss) { Text(strings.nwc.done) } } ) } \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt index e1acbbe..d6e5b67 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt @@ -1,5 +1,7 @@ package com.bitcointxoko.gudariwallet.ui.nwc +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -13,8 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bitcointxoko.gudariwallet.LocalAppStrings import kotlinx.coroutines.launch +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @OptIn(ExperimentalMaterial3Api::class) @Composable fun NwcScreen( @@ -30,8 +34,13 @@ fun NwcScreen( val pendingDeletePubkey by viewModel.pendingDeletePubkey.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val strings = LocalAppStrings.current - val refresh = { viewModel.loadConnections(includeExpired = true) } + val refresh = { viewModel.loadConnections(includeExpired = true, strings) } + + LaunchedEffect(Unit) { + viewModel.loadConnections(includeExpired = true, strings) + } LaunchedEffect(pairingUrl) { if (pairingUrl != null) { @@ -42,17 +51,17 @@ fun NwcScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Wallet Connect") }, + title = { Text(strings.nwc.walletConnect) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.nwc.back) } } ) }, floatingActionButton = { FloatingActionButton(onClick = { viewModel.openAddSheet() }) { - Icon(Icons.Default.Add, contentDescription = "Add connection") + Icon(Icons.Default.Add, contentDescription = strings.nwc.addConnection) } }, snackbarHost = { SnackbarHost(snackbarHostState) } @@ -102,7 +111,7 @@ fun NwcScreen( connection, isDeleting, onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) }, - onConnectionClick = { onConnectionClick(connection.data.pubkey) } + { onConnectionClick(connection.data.pubkey) } ) HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), @@ -125,7 +134,8 @@ fun NwcScreen( description = description, permissions = permissions, expiresAt = expiresAt, - budgets = budgets + budgets = budgets, + strings = strings ) }, onDismiss = { viewModel.closeAddSheet() } @@ -134,12 +144,13 @@ fun NwcScreen( // ── Pairing URL dialog ───────────────────────────────────────────────────── pairingUrl?.let { url -> + val strings = strings NwcPairingUrlDialog( url = url, onDismiss = { viewModel.clearPairingUrl() }, onNoAppFound = { scope.launch { - snackbarHostState.showSnackbar("No compatible app found") + snackbarHostState.showSnackbar(strings.nwc.noCompatibleAppFound) } } ) @@ -149,15 +160,15 @@ fun NwcScreen( pendingDeletePubkey?.let { AlertDialog( onDismissRequest = { viewModel.cancelDelete() }, - title = { Text("Remove connection?") }, - text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") }, + title = { Text(strings.nwc.removeConnection) }, + text = { Text(strings.nwc.removeConnectionDetail) }, confirmButton = { TextButton( - onClick = { viewModel.confirmDelete() } - ) { Text("Remove", color = MaterialTheme.colorScheme.error) } + onClick = { viewModel.confirmDelete(strings) } + ) { Text(strings.nwc.remove, color = MaterialTheme.colorScheme.error) } }, dismissButton = { - TextButton(onClick = { viewModel.cancelDelete() }) { Text("Cancel") } + TextButton(onClick = { viewModel.cancelDelete() }) { Text(strings.nwc.cancel) } } ) } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcStates.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcStates.kt index 3b6a5ab..d3717f7 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcStates.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcStates.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.bitcointxoko.gudariwallet.strings // ── Empty state ────────────────────────────────────────────────────── @@ -21,10 +22,10 @@ internal fun NwcEmptyContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Text("No connections yet", style = MaterialTheme.typography.titleMedium) + Text(strings.nwc.noConnectionsYet, style = MaterialTheme.typography.titleMedium) Spacer(Modifier.height(8.dp)) Text( - "Tap + to connect an app via Nostr Wallet Connect.", + strings.nwc.tapToAddHint, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -32,7 +33,7 @@ internal fun NwcEmptyContent( Button(onClick = onAddNew) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text("Add connection") + Text(strings.nwc.addConnection) } } } @@ -51,7 +52,7 @@ internal fun NwcErrorContent( verticalArrangement = Arrangement.Center ) { Text( - "Something went wrong", + strings.nwc.somethingWentWrong, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.error ) @@ -62,6 +63,6 @@ internal fun NwcErrorContent( color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(Modifier.height(24.dp)) - OutlinedButton(onClick = onRetry) { Text("Retry") } + OutlinedButton(onClick = onRetry) { Text(strings.nwc.retry) } } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt index a98f4f0..a131a11 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt @@ -3,14 +3,16 @@ package com.bitcointxoko.gudariwallet.ui.nwc import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.api.NwcGetResponse import com.bitcointxoko.gudariwallet.api.NwcNewBudget import com.bitcointxoko.gudariwallet.api.NwcRegistrationRequest import com.bitcointxoko.gudariwallet.data.NwcCacheRepository import com.bitcointxoko.gudariwallet.data.WalletRepository -import com.bitcointxoko.gudariwallet.data.db.toDomain import com.bitcointxoko.gudariwallet.data.toDomain import com.bitcointxoko.gudariwallet.data.toGetResponse +import com.bitcointxoko.gudariwallet.i18n.AppStrings import com.bitcointxoko.gudariwallet.util.NwcKeyGenerator import com.bitcointxoko.gudariwallet.util.NwcKeypair import kotlinx.coroutines.Job @@ -95,18 +97,14 @@ class NwcViewModel( fun requestDelete(pubkey: String) { _pendingDeletePubkey.value = pubkey } fun cancelDelete() { _pendingDeletePubkey.value = null } // confirmDelete() calls the existing deleteConnection() then clears the pending key - fun confirmDelete() { - _pendingDeletePubkey.value?.let { deleteConnection(it) } + fun confirmDelete(strings: AppStrings) { + _pendingDeletePubkey.value?.let { deleteConnection(it, strings) } _pendingDeletePubkey.value = null } - init { - loadConnections(includeExpired = true) - } - // ── Load ────────────────────────────────────────────────────────────────── - fun loadConnections(includeExpired: Boolean = false) { + fun loadConnections(includeExpired: Boolean = false, strings: AppStrings) { Timber.d("[$TAG] loadConnections(includeExpired=$includeExpired)") isSyncing = true @@ -154,7 +152,7 @@ class NwcViewModel( isSyncing = false if (_uiState.value !is NwcUiState.Success) { _uiState.value = NwcUiState.Error( - e.message ?: "Failed to load connections" + e.message ?: strings.nwc.failedToLoadConnections ) } else { Timber.w("[$TAG] network failed but cache is showing — non-fatal") @@ -172,7 +170,8 @@ class NwcViewModel( description : String, permissions : List, expiresAt : Long = 0L, - budgets : List = emptyList() + budgets : List = emptyList(), + strings : AppStrings ) { Timber.d("[$TAG] createConnection description='$description' permissions=$permissions") viewModelScope.launch { @@ -213,7 +212,7 @@ class NwcViewModel( } .onFailure { e -> Timber.e(e, "[$TAG] createConnection failed") - _uiState.value = NwcUiState.Error(e.message ?: "Failed to create connection") + _uiState.value = NwcUiState.Error(e.message ?: strings.nwc.failedToCreateConnection) } } finally { _isCreating.value = false @@ -234,7 +233,7 @@ class NwcViewModel( * and the detail screen (revoke button). Sets [_isDeleting] for the duration * so the list screen can disable row interactions while in flight. */ - fun deleteConnection(pubkey: String) { + fun deleteConnection(pubkey: String, strings: AppStrings) { Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}…") viewModelScope.launch { _isDeleting.value = true @@ -254,7 +253,7 @@ class NwcViewModel( .onFailure { e -> Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}…") _uiState.value = NwcUiState.Error( - e.message ?: "Failed to delete connection" + e.message ?: strings.nwc.failedToDeleteConnection ) } } finally { @@ -264,7 +263,7 @@ class NwcViewModel( } - fun loadConnectionDetail(pubkey: String) { + fun loadConnectionDetail(pubkey: String, strings: AppStrings) { Timber.d("[$TAG] loadConnectionDetail pubkey=${pubkey.take(16)}…") // Cancel any subscription from a previous key — prevents stale Flow @@ -303,7 +302,7 @@ class NwcViewModel( Timber.e(e, "[$TAG] failed to load connection detail") if (_connectionDetailState.value !is ConnectionDetailState.Success) { _connectionDetailState.value = ConnectionDetailState.Error( - e.message ?: "Failed to load connection" + e.message ?: strings.nwc.failedToLoadConnection ) } else { Timber.w("[$TAG] detail network refresh failed — cache showing (non-fatal)") diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt index cffb53c..552add1 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt @@ -1,5 +1,7 @@ package com.bitcointxoko.gudariwallet.util +import android.os.Build +import androidx.annotation.RequiresApi import timber.log.Timber import java.time.Instant import java.time.LocalDate @@ -98,3 +100,15 @@ fun formatEpoch(epochSeconds: Long, todayLabel: String, yesterdayLabel: String): "—" } +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +private fun epochToLocalDate(epochSeconds: Long): LocalDate = + LocalDate.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault()) + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +internal fun isToday(epochSeconds: Long): Boolean = + epochToLocalDate(epochSeconds) == LocalDate.now(ZoneId.systemDefault()) + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +internal fun isYesterday(epochSeconds: Long): Boolean = + epochToLocalDate(epochSeconds) == LocalDate.now(ZoneId.systemDefault()).minusDays(1) +