feat: localisation with lyricist

This commit is contained in:
2026-06-06 00:31:25 +02:00
parent 5af1dc32ca
commit e09c40e981
4 changed files with 93 additions and 45 deletions
@@ -4,6 +4,24 @@ import cafe.adriel.lyricist.LyricistStrings
@LyricistStrings(languageTag = "en", default = true)
val EnHomeStrings = AppStrings(
// ── WalletScreen ──────────────────────────────────────────────────────────
tabReceive = "Receive",
tabSend = "Send",
scanContentDescription = "Scan",
dismissContentDescription = "Dismiss",
clipboardInvoiceDetected = "Invoice detected in clipboard",
clipboardLnurlDetected = "LNURL detected in clipboard",
clipboardAddressDetected = "Lightning Address detected in clipboard",
clipboardPaymentDetected = "Payment detected in clipboard",
clipboardTapToOpen = "Tap to open",
clipboardTapToPay = "Tap to pay",
nfcInvoiceDetected = "Invoice detected via NFC",
nfcLnurlDetected = "LNURL detected via NFC",
nfcAddressDetected = "Lightning Address detected via NFC",
nfcPaymentDetected = "Payment detected via NFC",
nfcTapToOpen = "Tap to open",
nfcTapToPay = "Tap to pay",
// ── HomeTab ───────────────────────────────────────────────────────────────
loadingBalance = "Loading balance…",
sats = "sats",
@@ -4,6 +4,24 @@ import cafe.adriel.lyricist.LyricistStrings
@LyricistStrings(languageTag = "es")
val EsHomeStrings = AppStrings(
// ── WalletScreen ──────────────────────────────────────────────────────────
tabReceive = "Recibir",
tabSend = "Enviar",
scanContentDescription = "Escanear",
dismissContentDescription = "Cerrar",
clipboardInvoiceDetected = "Factura detectada en el portapapeles",
clipboardLnurlDetected = "LNURL detectado en el portapapeles",
clipboardAddressDetected = "Dirección Lightning detectada en el portapapeles",
clipboardPaymentDetected = "Pago detectado en el portapapeles",
clipboardTapToOpen = "Toca para abrir",
clipboardTapToPay = "Toca para pagar",
nfcInvoiceDetected = "Factura detectada por NFC",
nfcLnurlDetected = "LNURL detectado por NFC",
nfcAddressDetected = "Dirección Lightning detectada por NFC",
nfcPaymentDetected = "Pago detectado por NFC",
nfcTapToOpen = "Toca para abrir",
nfcTapToPay = "Toca para pagar",
// ── HomeTab ───────────────────────────────────────────────────────────────
loadingBalance = "Cargando saldo…",
sats = "sats",
@@ -1,7 +1,25 @@
package com.bitcointxoko.gudariwallet.i18n
data class AppStrings(
// ── Loading state ──────────────────────────────────────────────────────────
// ── WalletScreen ──────────────────────────────────────────────────────────
val tabReceive : String,
val tabSend : String,
val scanContentDescription : String,
val dismissContentDescription : String,
val clipboardInvoiceDetected : String,
val clipboardLnurlDetected : String,
val clipboardAddressDetected : String,
val clipboardPaymentDetected : String,
val clipboardTapToOpen : String,
val clipboardTapToPay : String,
val nfcInvoiceDetected : String,
val nfcLnurlDetected : String,
val nfcAddressDetected : String,
val nfcPaymentDetected : String,
val nfcTapToOpen : String,
val nfcTapToPay : String,
// ── HomeTab ───────────────────────────────────────────────────────────────
val loadingBalance: String,
// ── Balance display ────────────────────────────────────────────────────────
@@ -56,6 +56,7 @@ import androidx.navigation.navArgument
import cafe.adriel.lyricist.Lyricist
import com.bitcointxoko.gudariwallet.MainActivity
import com.bitcointxoko.gudariwallet.i18n.AppStrings
import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.ui.history.HistoryScreen
import com.bitcointxoko.gudariwallet.ui.history.HistoryViewModel
import com.bitcointxoko.gudariwallet.ui.history.HistoryViewModelFactory
@@ -67,10 +68,12 @@ import com.bitcointxoko.gudariwallet.util.SendInputType
import kotlinx.coroutines.delay
// ── Tab destinations ──────────────────────────────────────────────────────────
// Note: label and icon are now resolved at the call site via LocalAppStrings,
// so the sealed class carries only the stable route string and icon vector.
sealed class TabItem(val route: String, val label: String, val icon: ImageVector) {
data object Receive : TabItem("receive", "Receive", Icons.Filled.QrCode)
data object Send : TabItem("send", "Send", Icons.AutoMirrored.Filled.Send)
sealed class TabItem(val route: String, val icon: ImageVector) {
data object Receive : TabItem("receive", Icons.Filled.QrCode)
data object Send : TabItem("send", Icons.AutoMirrored.Filled.Send)
}
private const val ROUTE_HOME = "home"
@@ -81,11 +84,12 @@ private const val ROUTE_PAYMENT_DETAIL = "payment_detail/{checkingId}"
@Composable
fun WalletScreen(
vm : WalletViewModel,
nfcVm : NfcViewModel, // ← new
nfcVm : NfcViewModel,
pendingPaymentHash: MutableState<String?>,
pendingPaymentUri : MutableState<String?>,
lyricist : Lyricist<AppStrings>
) {
val strings = LocalAppStrings.current
val navController = rememberNavController()
val context = LocalContext.current
val historyFactory = remember {
@@ -179,7 +183,6 @@ fun WalletScreen(
Scaffold(
bottomBar = {
if (showBottomBar) {
// ── Floating island nav bar: Receive | [Scan] | Send ──────────
Box(
modifier = Modifier
.fillMaxWidth()
@@ -204,8 +207,8 @@ fun WalletScreen(
// Receive
NavigationBarItem(
selected = currentRoute == TabItem.Receive.route,
icon = { Icon(TabItem.Receive.icon, contentDescription = TabItem.Receive.label) },
label = { Text(TabItem.Receive.label) },
icon = { Icon(TabItem.Receive.icon, contentDescription = strings.tabReceive) }, // ← localized
label = { Text(strings.tabReceive) }, // ← localized
modifier = Modifier.weight(1f),
onClick = {
if (currentRoute != TabItem.Receive.route) {
@@ -239,7 +242,7 @@ fun WalletScreen(
) {
Icon(
imageVector = Icons.Filled.QrCodeScanner,
contentDescription = "Scan",
contentDescription = strings.scanContentDescription, // ← localized
modifier = Modifier.size(26.dp)
)
}
@@ -248,8 +251,8 @@ fun WalletScreen(
// Send
NavigationBarItem(
selected = currentRoute == TabItem.Send.route,
icon = { Icon(TabItem.Send.icon, contentDescription = TabItem.Send.label) },
label = { Text(TabItem.Send.label) },
icon = { Icon(TabItem.Send.icon, contentDescription = strings.tabSend) }, // ← localized
label = { Text(strings.tabSend) }, // ← localized
modifier = Modifier.weight(1f),
onClick = {
if (currentRoute != TabItem.Send.route) {
@@ -298,7 +301,7 @@ fun WalletScreen(
composable(TabItem.Receive.route) {
ReceiveScreen(
vm = vm,
nfcVm = nfcVm, // ← new
nfcVm = nfcVm,
onNavigateToPaymentDetail = { checkingId ->
navController.navigate("payment_detail/$checkingId")
}
@@ -306,7 +309,7 @@ fun WalletScreen(
}
composable(TabItem.Send.route) {
SendScreen(
vm = vm,
vm = vm,
nfcVm = nfcVm,
onViewDetails = { checkingId, record ->
historyVm.primePayment(record)
@@ -324,13 +327,12 @@ fun WalletScreen(
vm.scan(scanned)
val wentBack = navController.popBackStack(TabItem.Send.route, inclusive = false)
if (!wentBack) {
// Scan was triggered from a non-Send tab — navigate to Send
navController.navigate(TabItem.Send.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = false // don't restore old Idle state; VM already has new state
restoreState = false
}
}
},
@@ -341,11 +343,11 @@ fun WalletScreen(
// History — full screen, no bottom chrome, close → home
composable(
ROUTE_HISTORY,
enterTransition = { fadeIn(animationSpec = tween(150)) },
popExitTransition = { fadeOut(animationSpec = tween(150)) }
enterTransition = { fadeIn(animationSpec = tween(150)) },
popExitTransition = { fadeOut(animationSpec = tween(150)) }
) {
HistoryScreen(
vm = historyVm,
vm = historyVm,
onClose = {
navController.popBackStack(ROUTE_HOME, inclusive = false)
},
@@ -360,7 +362,7 @@ fun WalletScreen(
route = ROUTE_PAYMENT_DETAIL,
arguments = listOf(navArgument("checkingId") { type = NavType.StringType })
) { backStackEntry ->
val checkingId = backStackEntry.arguments?.getString("checkingId") ?: ""
val checkingId = backStackEntry.arguments?.getString("checkingId") ?: ""
val historyState by historyVm.filteredState.collectAsStateWithLifecycle()
val fallbackState by historyVm.state.collectAsStateWithLifecycle()
val payment = remember(historyState, fallbackState, checkingId) {
@@ -394,21 +396,21 @@ fun WalletScreen(
exit = slideOutVertically(targetOffsetY = { -it }),
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(2f) // above NFC banner
.zIndex(2f)
.fillMaxWidth()
.statusBarsPadding()
) {
if (clipboardOffer is ClipboardOfferState.Detected) {
val offer = clipboardOffer as ClipboardOfferState.Detected
val label = when (offer.inputType) {
is SendInputType.Bolt11 -> "Invoice"
is SendInputType.Lnurl -> "LNURL"
is SendInputType.LightningAddress -> "Lightning Address"
else -> "Payment"
is SendInputType.Bolt11 -> strings.clipboardInvoiceDetected // ← localized
is SendInputType.Lnurl -> strings.clipboardLnurlDetected // ← localized
is SendInputType.LightningAddress -> strings.clipboardAddressDetected // ← localized
else -> strings.clipboardPaymentDetected // ← localized
}
val subLabel = when (offer.inputType) {
is SendInputType.Lnurl -> "Tap to open"
else -> "Tap to pay"
is SendInputType.Lnurl -> strings.clipboardTapToOpen // ← localized
else -> strings.clipboardTapToPay // ← localized
}
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
@@ -429,10 +431,7 @@ fun WalletScreen(
Icon(Icons.Default.ContentPaste, contentDescription = null)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(
"$label detected in clipboard",
style = MaterialTheme.typography.bodyMedium
)
Text(label, style = MaterialTheme.typography.bodyMedium)
Text(
subLabel,
style = MaterialTheme.typography.bodySmall,
@@ -440,7 +439,7 @@ fun WalletScreen(
)
}
IconButton(onClick = { vm.dismissClipboardOffer() }) {
Icon(Icons.Default.Close, contentDescription = "Dismiss")
Icon(Icons.Default.Close, contentDescription = strings.dismissContentDescription) // ← localized
}
}
}
@@ -448,29 +447,27 @@ fun WalletScreen(
}
// ── NFC banner — floats over content, slides in from top ──────────
// Shown when an NFC tag is tapped while the app is foregrounded.
// Mirrors the clipboard banner pattern exactly.
AnimatedVisibility(
visible = nfcOffer is NfcOfferState.Detected,
enter = slideInVertically(initialOffsetY = { -it }),
exit = slideOutVertically(targetOffsetY = { -it }),
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(1f) // below clipboard banner if both show
.zIndex(1f)
.fillMaxWidth()
.statusBarsPadding()
) {
if (nfcOffer is NfcOfferState.Detected) {
val offer = nfcOffer as NfcOfferState.Detected
val label = when (offer.inputType) {
is SendInputType.Bolt11 -> "Invoice"
is SendInputType.Lnurl -> "LNURL"
is SendInputType.LightningAddress -> "Lightning Address"
else -> "Payment"
is SendInputType.Bolt11 -> strings.nfcInvoiceDetected // ← localized
is SendInputType.Lnurl -> strings.nfcLnurlDetected // ← localized
is SendInputType.LightningAddress -> strings.nfcAddressDetected // ← localized
else -> strings.nfcPaymentDetected // ← localized
}
val subLabel = when (offer.inputType) {
is SendInputType.Lnurl -> "Tap to open"
else -> "Tap to pay"
is SendInputType.Lnurl -> strings.nfcTapToOpen // ← localized
else -> strings.nfcTapToPay // ← localized
}
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
@@ -491,10 +488,7 @@ fun WalletScreen(
Icon(Icons.Default.Nfc, contentDescription = null)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(
"$label detected via NFC",
style = MaterialTheme.typography.bodyMedium
)
Text(label, style = MaterialTheme.typography.bodyMedium)
Text(
subLabel,
style = MaterialTheme.typography.bodySmall,
@@ -502,7 +496,7 @@ fun WalletScreen(
)
}
IconButton(onClick = { nfcVm.dismissNfcOffer() }) {
Icon(Icons.Default.Close, contentDescription = "Dismiss")
Icon(Icons.Default.Close, contentDescription = strings.dismissContentDescription) // ← localized
}
}
}