diff --git a/mobile-app/l10n.yaml b/mobile-app/l10n.yaml new file mode 100644 index 00000000..4e6692e5 --- /dev/null +++ b/mobile-app/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/mobile-app/lib/app.dart b/mobile-app/lib/app.dart index fb48440a..51c6cd47 100644 --- a/mobile-app/lib/app.dart +++ b/mobile-app/lib/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/wallet_initializer.dart'; import 'package:resonance_network_wallet/v2/screens/auth/auth_wrapper.dart'; import 'package:resonance_network_wallet/v2/theme/app_theme.dart'; @@ -8,6 +9,7 @@ import 'package:resonance_network_wallet/services/notification_integration_servi import 'package:resonance_network_wallet/services/referral_service.dart'; import 'package:resonance_network_wallet/services/telemetry_navigator_observer.dart'; import 'package:resonance_network_wallet/services/deep_link_service.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'dart:io' show Platform; class ResonanceWalletApp extends ConsumerStatefulWidget { @@ -35,8 +37,14 @@ class _ResonanceWalletAppState extends ConsumerState { @override Widget build(BuildContext context) { + final appLocale = ref.watch(selectedAppLocaleProvider); + return MaterialApp( title: 'Quantus Wallet', + locale: appLocale.flutterLocale, + // Framework widgets only; app strings use l10nProvider (see l10n_provider.dart). + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, navigatorObservers: [TelemetryNavigatorObserver()], initialRoute: '/', routes: {'/': (context) => const WalletInitializer()}, diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb new file mode 100644 index 00000000..737f9982 --- /dev/null +++ b/mobile-app/lib/l10n/app_en.arb @@ -0,0 +1,1565 @@ +{ + "walletInitErrorTitle": "Wallet Error", + "@walletInitErrorTitle": { + "description": "Title for the error dialog when the wallet is not found" + }, + "walletInitErrorMessage": "Unable to find secret phrase. Please restore your wallet.", + "@walletInitErrorMessage": { + "description": "Message for the error dialog when the wallet is not found" + }, + "walletInitErrorButtonLabel": "OK", + "@walletInitErrorButtonLabel": { + "description": "Label for the button on the error dialog when the wallet is not found" + }, + + "authUseDeviceBiometricsToUnlock": "Use device biometrics to unlock", + "@authUseDeviceBiometricsToUnlock": { + "description": "Text for the text on the lock screen when using device biometrics to unlock" + }, + "authAuthenticating": "Authenticating...", + "@authAuthenticating": { + "description": "Text for the text on the lock screen when authenticating" + }, + "authUnlockWallet": "Unlock Wallet", + "@authUnlockWallet": { + "description": "Text for the button on the lock screen to unlock the wallet" + }, + "authAuthorizationRequired": "Authorization \n Required", + "@authAuthorizationRequired": { + "description": "Text for displayed on the lock screen when authorization is required" + }, + + "welcomeTagline": "Quantum Secure Encrypted Money", + "@welcomeTagline": { + "description": "Tagline on the welcome screen" + }, + "welcomeCreateNewWallet": "Create New Wallet", + "@welcomeCreateNewWallet": { + "description": "Button to start creating a new wallet on the welcome screen" + }, + "welcomeImportWallet": "Import Wallet", + "@welcomeImportWallet": { + "description": "Button to import an existing wallet on the welcome screen" + }, + + "createWalletAppBarTitle": "Create Wallet", + "@createWalletAppBarTitle": { + "description": "App bar title for the create wallet flow" + }, + "createWalletCautionHeadline": "Keep your Recovery Phrase Secret", + "@createWalletCautionHeadline": { + "description": "Headline on the recovery phrase caution screen during wallet creation" + }, + "createWalletCautionBullet1": "If you lose this device, your recovery phrase is the only way back", + "@createWalletCautionBullet1": { + "description": "First bullet on the recovery phrase caution screen" + }, + "createWalletCautionBullet2": "Anyone who gets hold of it has complete control over your funds, permanently", + "@createWalletCautionBullet2": { + "description": "Second bullet on the recovery phrase caution screen" + }, + "createWalletCautionBullet3": "Write it down and keep it somewhere safe. Do not save it digitally", + "@createWalletCautionBullet3": { + "description": "Third bullet on the recovery phrase caution screen" + }, + "createWalletCautionCheckboxLabel": "I understand that anyone with my recovery phrase can access my wallet. I will store it safely.", + "@createWalletCautionCheckboxLabel": { + "description": "Checkbox label on the recovery phrase caution screen" + }, + "createWalletRecoveryPhraseNext": "Next", + "@createWalletRecoveryPhraseNext": { + "description": "Primary button on the new wallet recovery phrase screen" + }, + "createWalletRecoveryPhraseFailedGenerate": "Failed to generate: {error}", + "@createWalletRecoveryPhraseFailedGenerate": { + "description": "Error when mnemonic generation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "createWalletRecoveryPhraseSaveError": "Error saving wallet: {error}", + "@createWalletRecoveryPhraseSaveError": { + "description": "Error when saving a new wallet fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "recoveryPhraseBodyInstructions": "Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.", + "@recoveryPhraseBodyInstructions": { + "description": "Instructions above the recovery phrase word grid" + }, + "recoveryPhraseBodyCopy": "Copy", + "@recoveryPhraseBodyCopy": { + "description": "Copy button on the recovery phrase screen" + }, + "recoveryPhraseBodyCopiedMessage": "Recovery phrase copied to clipboard", + "@recoveryPhraseBodyCopiedMessage": { + "description": "Toast when recovery phrase is copied" + }, + + "accountReadyAccountCreated": "Account Created", + "@accountReadyAccountCreated": { + "description": "Title when a new account is created" + }, + "accountReadyWalletCreated": "Wallet Created", + "@accountReadyWalletCreated": { + "description": "Title when a new wallet is created" + }, + "accountReadyWalletImported": "Wallet Imported", + "@accountReadyWalletImported": { + "description": "Title when a wallet is imported" + }, + "accountReadyDone": "Done", + "@accountReadyDone": { + "description": "Done button on the account ready screen" + }, + + "importWalletAppBarTitle": "Import Wallet", + "@importWalletAppBarTitle": { + "description": "App bar title on the import wallet screen" + }, + "importWalletDescription": "Restore an existing wallet with your 12 or 24 words recovery phrase", + "@importWalletDescription": { + "description": "Description on the import wallet screen" + }, + "importWalletHint": "Type in or paste your recovery phrase. Separate words with spaces.", + "@importWalletHint": { + "description": "Hint for the recovery phrase text field" + }, + "importWalletButton": "Import", + "@importWalletButton": { + "description": "Import button on the import wallet screen" + }, + "importWalletValidationError": "Recovery phrase must be 12 or 24 words", + "@importWalletValidationError": { + "description": "Validation error when recovery phrase word count is invalid" + }, + + "homeError": "Error: {error}", + "@homeError": { + "description": "Error message on the home screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "homeNoActiveAccount": "No active account", + "@homeNoActiveAccount": { + "description": "Shown when no account is active on the home screen" + }, + "homeCharge": "Charge", + "@homeCharge": { + "description": "POS charge button on the home screen bottom bar" + }, + "homeGetTestnetTokens": "Get Testnet Tokens ↗", + "@homeGetTestnetTokens": { + "description": "Faucet button when balance is zero on the home screen" + }, + "homeErrorLoadingBalance": "Error loading balance", + "@homeErrorLoadingBalance": { + "description": "Error when balance fails to load on the home screen" + }, + "homeReceive": "Receive", + "@homeReceive": { + "description": "Receive action button on the home screen" + }, + "homeSend": "Send", + "@homeSend": { + "description": "Send action button on the home screen" + }, + "homeSwap": "Swap", + "@homeSwap": { + "description": "Swap action button on the home screen" + }, + + "homeActivityTitle": "Activity", + "@homeActivityTitle": { + "description": "Section title for recent activity on the home screen" + }, + "homeActivityViewAll": "View All", + "@homeActivityViewAll": { + "description": "Link to full activity screen from home" + }, + "homeActivityErrorLoading": "Error loading transactions", + "@homeActivityErrorLoading": { + "description": "Error when transactions fail to load in home activity section" + }, + "homeActivityRetry": "Retry", + "@homeActivityRetry": { + "description": "Retry link in home activity section error state" + }, + "homeActivityEmptyTitle": "No Transactions Yet", + "@homeActivityEmptyTitle": { + "description": "Empty state title in home activity section" + }, + "homeActivityEmptyMessage": "Your activity will appear here once you send or receive QUAN.", + "@homeActivityEmptyMessage": { + "description": "Empty state message in home activity section" + }, + + "accountsSheetTitle": "Accounts", + "@accountsSheetTitle": { + "description": "Title of the accounts bottom sheet" + }, + "accountsSheetFailedLoadAccounts": "Failed to load accounts.", + "@accountsSheetFailedLoadAccounts": { + "description": "Error when accounts list fails to load" + }, + "accountsSheetFailedLoadActiveAccount": "Failed to load active account.", + "@accountsSheetFailedLoadActiveAccount": { + "description": "Error when active account fails to load" + }, + "accountsSheetNoAccountsFound": "No accounts found.", + "@accountsSheetNoAccountsFound": { + "description": "Empty state in accounts sheet" + }, + "accountsSheetAddAccount": "Add Account", + "@accountsSheetAddAccount": { + "description": "Button to add a new account" + }, + "accountsSheetBalanceUnavailable": "Balance unavailable", + "@accountsSheetBalanceUnavailable": { + "description": "When account balance fails to load" + }, + "accountsSheetBalance": "{balance} {symbol}", + "@accountsSheetBalance": { + "description": "Formatted balance with token symbol", + "placeholders": { + "balance": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + + "addAccountMenuTitle": "Add Account", + "@addAccountMenuTitle": { + "description": "App bar title on add account menu" + }, + "addAccountMenuCreateTitle": "Create New Account", + "@addAccountMenuCreateTitle": { + "description": "Create new account menu row title" + }, + "addAccountMenuCreateSubtitle": "Generate a fresh wallet address", + "@addAccountMenuCreateSubtitle": { + "description": "Create new account menu row subtitle" + }, + "addAccountMenuImportTitle": "Import Wallet", + "@addAccountMenuImportTitle": { + "description": "Import wallet menu row title" + }, + "addAccountMenuImportSubtitle": "Use a recovery phrase to import", + "@addAccountMenuImportSubtitle": { + "description": "Import wallet menu row subtitle" + }, + + "createAccountAppBarTitle": "Account Name", + "@createAccountAppBarTitle": { + "description": "App bar title when creating an account" + }, + "createAccountSubtitle": "Give this account a name you'll recognize. You can change it anytime.", + "@createAccountSubtitle": { + "description": "Subtitle on create account name field" + }, + "createAccountButton": "Create", + "@createAccountButton": { + "description": "Create account button" + }, + "createAccountErrorCouldNotAdd": "Could not add account.", + "@createAccountErrorCouldNotAdd": { + "description": "Error when account creation fails" + }, + "createAccountDefaultName": "Account {number}", + "@createAccountDefaultName": { + "description": "Default name for a new account", + "placeholders": { + "number": { + "type": "int" + } + } + }, + + "editAccountAppBarTitle": "Account Name", + "@editAccountAppBarTitle": { + "description": "App bar title when editing account name" + }, + "editAccountDone": "Done", + "@editAccountDone": { + "description": "Done button on edit account screen" + }, + "editAccountNameEmpty": "Account name can't be empty", + "@editAccountNameEmpty": { + "description": "Validation error when account name is empty" + }, + "editAccountRenameFailed": "Failed to rename account.", + "@editAccountRenameFailed": { + "description": "Error when renaming account fails" + }, + + "accountMenuTitle": "Accounts", + "@accountMenuTitle": { + "description": "App bar title on account menu screen" + }, + "accountMenuAccountName": "Account Name", + "@accountMenuAccountName": { + "description": "Account name menu row label" + }, + "accountMenuAddressDetails": "Address Details", + "@accountMenuAddressDetails": { + "description": "Address details menu row label" + }, + "accountMenuShowRecoveryPhrase": "Show Recovery Phrase", + "@accountMenuShowRecoveryPhrase": { + "description": "Show recovery phrase menu row label" + }, + "accountMenuNotFound": "Account not found", + "@accountMenuNotFound": { + "description": "When account is not found on menu screen" + }, + + "accountDetailsTitle": "Address Details", + "@accountDetailsTitle": { + "description": "App bar title on account details screen" + }, + + "addHardwareAccountAddWallet": "Add Hardware Wallet", + "@addHardwareAccountAddWallet": { + "description": "Title when adding a new hardware wallet" + }, + "addHardwareAccountAddAccount": "Add Hardware Account", + "@addHardwareAccountAddAccount": { + "description": "Title when adding a hardware account to existing wallet" + }, + "addHardwareAccountNameLabel": "NAME", + "@addHardwareAccountNameLabel": { + "description": "Name field label" + }, + "addHardwareAccountNameHintWallet": "Hardware Wallet", + "@addHardwareAccountNameHintWallet": { + "description": "Name field hint for new hardware wallet" + }, + "addHardwareAccountNameHintAccount": "Account", + "@addHardwareAccountNameHintAccount": { + "description": "Name field hint for hardware account" + }, + "addHardwareAccountAddressLabel": "ADDRESS", + "@addHardwareAccountAddressLabel": { + "description": "Address field label" + }, + "addHardwareAccountAddressHint": "SS58 address", + "@addHardwareAccountAddressHint": { + "description": "Address field hint" + }, + "addHardwareAccountDebugFill": "Debug Fill", + "@addHardwareAccountDebugFill": { + "description": "Debug fill button" + }, + "addHardwareAccountNameRequired": "Name is required", + "@addHardwareAccountNameRequired": { + "description": "Validation when name is empty" + }, + "addHardwareAccountInvalidAddress": "Invalid address", + "@addHardwareAccountInvalidAddress": { + "description": "Validation when address is invalid" + }, + + "sendTitle": "Send", + "@sendTitle": { + "description": "Send flow app bar title" + }, + "sendPayTitle": "Pay", + "@sendPayTitle": { + "description": "Pay flow app bar title" + }, + "sendEnterAddress": "Enter Address", + "@sendEnterAddress": { + "description": "Button label when recipient address is missing" + }, + + "sendSelectRecipientSendTo": "Send To", + "@sendSelectRecipientSendTo": { + "description": "Section label on select recipient screen" + }, + "sendSelectRecipientSearchHint": "Search {symbol} Address", + "@sendSelectRecipientSearchHint": { + "description": "Hint for recipient search field", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + "sendSelectRecipientScanTitle": "Scan QR code", + "@sendSelectRecipientScanTitle": { + "description": "Scan QR row title" + }, + "sendSelectRecipientScanSubtitle": "Tap to scan a {symbol} Address", + "@sendSelectRecipientScanSubtitle": { + "description": "Scan QR row subtitle", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + "sendSelectRecipientRecents": "Recents", + "@sendSelectRecipientRecents": { + "description": "Recents section title" + }, + "sendSelectRecipientContinue": "Continue", + "@sendSelectRecipientContinue": { + "description": "Continue button on select recipient screen" + }, + + "sendInputAmountSendTo": "SEND TO", + "@sendInputAmountSendTo": { + "description": "Recipient card label on input amount screen" + }, + "sendInputAmountAvailableBalance": "Available Balance:", + "@sendInputAmountAvailableBalance": { + "description": "Available balance label" + }, + "sendInputAmountNetworkFee": "Network Fee:", + "@sendInputAmountNetworkFee": { + "description": "Network fee label" + }, + "sendInputAmountMax": "Max", + "@sendInputAmountMax": { + "description": "Max amount button" + }, + "sendInputAmountInvalidAmount": "Please enter a valid amount", + "@sendInputAmountInvalidAmount": { + "description": "Error when amount input is invalid" + }, + "sendInputAmountChecksumRequired": "Recipient checksum is required", + "@sendInputAmountChecksumRequired": { + "description": "Error when recipient checksum is missing" + }, + + "sendReviewSending": "SENDING", + "@sendReviewSending": { + "description": "Sending section label on review screen" + }, + "sendReviewTo": "TO", + "@sendReviewTo": { + "description": "To section label on review screen" + }, + "sendReviewAmount": "AMOUNT", + "@sendReviewAmount": { + "description": "Amount row label on review screen" + }, + "sendReviewNetworkFee": "NETWORK FEE", + "@sendReviewNetworkFee": { + "description": "Network fee row label on review screen" + }, + "sendReviewYouPay": "YOU PAY", + "@sendReviewYouPay": { + "description": "Total you pay row label on review screen" + }, + "sendReviewConfirm": "Confirm", + "@sendReviewConfirm": { + "description": "Confirm button on review screen" + }, + "sendReviewAuthReason": "Authenticate to confirm transaction", + "@sendReviewAuthReason": { + "description": "Biometric auth prompt on review screen" + }, + "sendReviewAuthRequired": "Authentication required to send", + "@sendReviewAuthRequired": { + "description": "Error when auth fails on review screen" + }, + "sendReviewSubmitFailed": "Failed submitting transaction", + "@sendReviewSubmitFailed": { + "description": "Error when transaction submission fails" + }, + + "sendTxSubmittedHeadlinePaid": "{amount} {symbol} paid", + "@sendTxSubmittedHeadlinePaid": { + "description": "Success headline when payment completed", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "sendTxSubmittedHeadlineSent": "{amount} {symbol} sent", + "@sendTxSubmittedHeadlineSent": { + "description": "Success headline when send completed", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "sendTxSubmittedOnItsWay": "On its way", + "@sendTxSubmittedOnItsWay": { + "description": "Subtitle on transaction submitted screen" + }, + "sendTxSubmittedToLabel": "To", + "@sendTxSubmittedToLabel": { + "description": "Recipient label on transaction submitted screen" + }, + "sendTxSubmittedDone": "Done", + "@sendTxSubmittedDone": { + "description": "Done button on transaction submitted screen" + }, + + "sendLogicCantSelfTransfer": "Can't Self Transfer", + "@sendLogicCantSelfTransfer": { + "description": "Button label when sending to own address" + }, + "sendLogicEnterAmount": "Enter Amount", + "@sendLogicEnterAmount": { + "description": "Button label when amount is zero" + }, + "sendLogicInvalidAmount": "Invalid Amount", + "@sendLogicInvalidAmount": { + "description": "Button label when amount is negative" + }, + "sendLogicBelowExistentialDeposit": "Below Existential Deposit", + "@sendLogicBelowExistentialDeposit": { + "description": "Button label when amount is below existential deposit" + }, + "sendLogicInsufficientBalance": "Insufficient Balance", + "@sendLogicInsufficientBalance": { + "description": "Button label when balance is insufficient" + }, + "sendLogicReviewSend": "Review Send", + "@sendLogicReviewSend": { + "description": "Button label to proceed to review" + }, + + "activityTitle": "Activity", + "@activityTitle": { + "description": "App bar title on activity screen" + }, + "activityError": "Error: {error}", + "@activityError": { + "description": "Error message on activity screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "activityNoAccount": "No account", + "@activityNoAccount": { + "description": "Shown when no account is active on activity screen" + }, + "activityEmpty": "No transactions yet", + "@activityEmpty": { + "description": "Empty state on activity screen" + }, + "activityFilterAll": "All", + "@activityFilterAll": { + "description": "Filter button for all transactions" + }, + "activityFilterSend": "Send", + "@activityFilterSend": { + "description": "Filter button for sent transactions" + }, + "activityFilterReceive": "Receive", + "@activityFilterReceive": { + "description": "Filter button for received transactions" + }, + "activityDateToday": "Today", + "@activityDateToday": { + "description": "Date group label for today" + }, + "activityDateYesterday": "Yesterday", + "@activityDateYesterday": { + "description": "Date group label for yesterday" + }, + + "activityTxSending": "Sending", + "@activityTxSending": { + "description": "Transaction row label for pending send" + }, + "activityTxReceiving": "Receiving", + "@activityTxReceiving": { + "description": "Transaction row label for pending or scheduled receive" + }, + "activityTxPending": "Pending", + "@activityTxPending": { + "description": "Transaction row label for scheduled send" + }, + "activityTxSent": "Sent", + "@activityTxSent": { + "description": "Transaction row label for completed send" + }, + "activityTxReceived": "Received", + "@activityTxReceived": { + "description": "Transaction row label for completed receive" + }, + "activityTxTo": "To", + "@activityTxTo": { + "description": "Counterparty direction label for send" + }, + "activityTxFrom": "From", + "@activityTxFrom": { + "description": "Counterparty direction label for receive" + }, + "activityTxTimeNow": "now", + "@activityTxTimeNow": { + "description": "Time label for just now" + }, + "activityTxTimeMinutesAgo": "{minutes}m ago", + "@activityTxTimeMinutesAgo": { + "description": "Time label for minutes ago", + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "activityTxTimeHoursAgo": "{hours}h ago", + "@activityTxTimeHoursAgo": { + "description": "Time label for hours ago", + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "activityTxTimeDaysAgo": "{days}d ago", + "@activityTxTimeDaysAgo": { + "description": "Time label for days ago", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "activityTxTimeRemaining": "{days}d:{hours}h:{minutes}m", + "@activityTxTimeRemaining": { + "description": "Time remaining for scheduled transaction", + "placeholders": { + "days": { + "type": "String" + }, + "hours": { + "type": "String" + }, + "minutes": { + "type": "String" + } + } + }, + + "activityDetailTitleSending": "Sending", + "@activityDetailTitleSending": { + "description": "Detail sheet title for pending send" + }, + "activityDetailTitleScheduled": "Scheduled", + "@activityDetailTitleScheduled": { + "description": "Detail sheet title for scheduled send" + }, + "activityDetailTitleReceiving": "Receiving", + "@activityDetailTitleReceiving": { + "description": "Detail sheet title for receiving" + }, + "activityDetailTitleSent": "Sent", + "@activityDetailTitleSent": { + "description": "Detail sheet title for completed send" + }, + "activityDetailTitleReceived": "Received", + "@activityDetailTitleReceived": { + "description": "Detail sheet title for completed receive" + }, + "activityDetailStatusInProcess": "In Process", + "@activityDetailStatusInProcess": { + "description": "Status label for in-process transaction" + }, + "activityDetailStatusScheduled": "Scheduled", + "@activityDetailStatusScheduled": { + "description": "Status label for scheduled transaction" + }, + "activityDetailStatusCompleted": "Completed", + "@activityDetailStatusCompleted": { + "description": "Status label for completed transaction" + }, + "activityDetailStatus": "STATUS", + "@activityDetailStatus": { + "description": "Status row label on detail sheet" + }, + "activityDetailTo": "TO", + "@activityDetailTo": { + "description": "To row label on detail sheet" + }, + "activityDetailFrom": "FROM", + "@activityDetailFrom": { + "description": "From row label on detail sheet" + }, + "activityDetailDate": "DATE", + "@activityDetailDate": { + "description": "Date row label on detail sheet" + }, + "activityDetailNetworkFee": "NETWORK FEE", + "@activityDetailNetworkFee": { + "description": "Network fee row label on detail sheet" + }, + "activityDetailTxHash": "TX HASH", + "@activityDetailTxHash": { + "description": "Transaction hash row label on detail sheet" + }, + "activityDetailViewExplorer": "View in Explorer ↗", + "@activityDetailViewExplorer": { + "description": "Link to view transaction in explorer" + }, + + "receiveTitle": "Receive", + "@receiveTitle": { + "description": "App bar title on receive screen" + }, + "receiveTabQrCode": "QR Code", + "@receiveTabQrCode": { + "description": "QR Code tab on receive screen" + }, + "receiveTabAddress": "Address", + "@receiveTabAddress": { + "description": "Address tab on receive screen" + }, + "receiveCopy": "Copy", + "@receiveCopy": { + "description": "Copy button on receive screen" + }, + "receiveErrorLoadingAccount": "Error loading account data: {error}", + "@receiveErrorLoadingAccount": { + "description": "Error when account data fails to load on receive screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "receiveClipboardContent": "Account Id:\n{accountId}\n\nCheckphrase:\n{checksum}", + "@receiveClipboardContent": { + "description": "Clipboard content when copying account details", + "placeholders": { + "accountId": { + "type": "String" + }, + "checksum": { + "type": "String" + } + } + }, + "receiveCopiedMessage": "Account details copied to clipboard", + "@receiveCopiedMessage": { + "description": "Toast when account details are copied" + }, + + "posAmountTitle": "New Charge", + "@posAmountTitle": { + "description": "App bar title on POS amount screen" + }, + "posAmountCharge": "Charge {amount}", + "@posAmountCharge": { + "description": "Charge button with formatted amount", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "posAmountEnterAmount": "Enter Amount", + "@posAmountEnterAmount": { + "description": "Charge button when amount is empty" + }, + + "posQrTitleScanToPay": "Scan to Pay", + "@posQrTitleScanToPay": { + "description": "App bar title while waiting for payment" + }, + "posQrTitlePaymentReceived": "Payment Received", + "@posQrTitlePaymentReceived": { + "description": "App bar title when payment is received" + }, + "posQrError": "Error: {error}", + "@posQrError": { + "description": "Error message on POS QR screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "posQrNoActiveAccount": "No active account", + "@posQrNoActiveAccount": { + "description": "Shown when no active account on POS QR screen" + }, + "posQrInvalidAmount": "Invalid amount. Tap to retry.", + "@posQrInvalidAmount": { + "description": "Error when amount cannot be parsed" + }, + "posQrConnectionLost": "Connection lost. Tap to retry.", + "@posQrConnectionLost": { + "description": "Error when payment watch connection is lost" + }, + "posQrTimedOut": "Timed out. Tap to retry.", + "@posQrTimedOut": { + "description": "Error when payment watch times out" + }, + "posQrNewCharge": "New Charge", + "@posQrNewCharge": { + "description": "New charge button on POS QR screen" + }, + "posQrDone": "Done", + "@posQrDone": { + "description": "Done button after payment received" + }, + "posQrAmountReceived": "{amount} received", + "@posQrAmountReceived": { + "description": "Headline when payment is received", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "posQrFrom": "From:", + "@posQrFrom": { + "description": "Sender label on payment received screen" + }, + "posQrWaitingForPayment": "Waiting for payment", + "@posQrWaitingForPayment": { + "description": "Status while waiting for payment" + }, + "posQrNetworkError": "Network Error", + "@posQrNetworkError": { + "description": "Network error title on POS QR screen" + }, + "posQrTryAgain": "Try Again", + "@posQrTryAgain": { + "description": "Retry button on POS QR screen" + }, + "posQrPaidAt": "At {time}", + "@posQrPaidAt": { + "description": "Paid at timestamp on payment received screen", + "placeholders": { + "time": { + "type": "String" + } + } + }, + + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "App bar title on settings hub" + }, + "settingsWalletTitle": "Wallet", + "@settingsWalletTitle": { + "description": "Wallet row title on settings hub" + }, + "settingsWalletSubtitle": "Recovery Phrase, Reset Wallet", + "@settingsWalletSubtitle": { + "description": "Wallet row subtitle on settings hub" + }, + "settingsPreferencesTitle": "Preferences", + "@settingsPreferencesTitle": { + "description": "Preferences row title on settings hub" + }, + "settingsPreferencesSubtitle": "Language, currency, POS mode, notifications", + "@settingsPreferencesSubtitle": { + "description": "Preferences row subtitle on settings hub" + }, + "settingsMiningRewards": "Mining Rewards", + "@settingsMiningRewards": { + "description": "Mining rewards row title on settings hub" + }, + "settingsMiningRewardsSubtitle": "{count} blocks mined", + "@settingsMiningRewardsSubtitle": { + "description": "Mining rewards row subtitle when data loaded", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsMiningRewardsError": "Error getting mining rewards", + "@settingsMiningRewardsError": { + "description": "Mining rewards row subtitle on error" + }, + "settingsAccountTypeTitle": "Account Type", + "@settingsAccountTypeTitle": { + "description": "Account type row title on settings hub" + }, + "settingsAccountTypeSubtitle": "Advanced Account Features", + "@settingsAccountTypeSubtitle": { + "description": "Account type row subtitle on settings hub" + }, + "settingsHelpTitle": "Help & Support", + "@settingsHelpTitle": { + "description": "Help row title on settings hub" + }, + "settingsHelpSubtitle": "FAQs, Contact the team", + "@settingsHelpSubtitle": { + "description": "Help row subtitle on settings hub" + }, + "settingsAboutTitle": "About Quantus", + "@settingsAboutTitle": { + "description": "About row title on settings hub" + }, + "settingsAboutHubSubtitle": "Version {version} ({build})", + "@settingsAboutHubSubtitle": { + "description": "About row subtitle on settings hub", + "placeholders": { + "version": { + "type": "String" + }, + "build": { + "type": "String" + } + } + }, + + "settingsWalletRecoveryPhrase": "Recovery Phrase", + "@settingsWalletRecoveryPhrase": { + "description": "Recovery phrase row on wallet settings" + }, + "settingsWalletRecoveryPhraseSubtitle": "View your 24-word Backup Password", + "@settingsWalletRecoveryPhraseSubtitle": { + "description": "Recovery phrase row subtitle" + }, + "settingsWalletReset": "Reset Wallet", + "@settingsWalletReset": { + "description": "Reset wallet row title" + }, + "settingsWalletResetSubtitle": "Removes all data from this device", + "@settingsWalletResetSubtitle": { + "description": "Reset wallet row subtitle" + }, + "settingsWalletNoWalletsFound": "No wallets found", + "@settingsWalletNoWalletsFound": { + "description": "Error when no wallets exist" + }, + "settingsWalletFailedToLoad": "Failed to load wallets", + "@settingsWalletFailedToLoad": { + "description": "Error when wallet list fails to load" + }, + + "settingsSelectWalletTitle": "Select Wallet", + "@settingsSelectWalletTitle": { + "description": "App bar on select wallet screen" + }, + "settingsSelectWalletNoWallets": "No wallets found", + "@settingsSelectWalletNoWallets": { + "description": "Empty state on select wallet screen" + }, + "settingsSelectWalletItem": "Wallet {number}", + "@settingsSelectWalletItem": { + "description": "Wallet list item label on select wallet screen", + "placeholders": { + "number": { + "type": "int" + } + } + }, + + "settingsRecoveryConfirmAuthReason": "Authenticate to see recovery phrase", + "@settingsRecoveryConfirmAuthReason": { + "description": "Biometric prompt when viewing recovery phrase" + }, + "settingsRecoveryConfirmAuthRequired": "Authentication required to see recovery phrase", + "@settingsRecoveryConfirmAuthRequired": { + "description": "Toaster when auth fails for recovery phrase" + }, + + "settingsRecoveryPhraseTitle": "Recovery Phrase", + "@settingsRecoveryPhraseTitle": { + "description": "App bar on recovery phrase screen" + }, + "settingsRecoveryPhraseDone": "Done", + "@settingsRecoveryPhraseDone": { + "description": "Done button on recovery phrase screen" + }, + + "settingsResetTitle": "Reset Wallet", + "@settingsResetTitle": { + "description": "App bar on reset wallet caution screen" + }, + "settingsResetAuthReason": "Authenticate to reset wallet", + "@settingsResetAuthReason": { + "description": "Biometric prompt when resetting wallet" + }, + "settingsResetFailed": "Failed to reset wallet: {error}", + "@settingsResetFailed": { + "description": "Toaster when wallet reset fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "settingsResetAuthRequired": "Authentication required to reset wallet", + "@settingsResetAuthRequired": { + "description": "Toaster when auth fails for wallet reset" + }, + "settingsResetCautionHeadline": "This will erase\nyour wallet", + "@settingsResetCautionHeadline": { + "description": "Headline on wallet reset caution screen" + }, + "settingsResetCautionBullet1": "All wallet data will be permanently removed from this device", + "@settingsResetCautionBullet1": { + "description": "First bullet on wallet reset caution" + }, + "settingsResetCautionBullet2": "Your funds stay on the blockchain but only your recovery phrase can restore access", + "@settingsResetCautionBullet2": { + "description": "Second bullet on wallet reset caution" + }, + "settingsResetCautionBullet3": "Without it, your funds are gone forever", + "@settingsResetCautionBullet3": { + "description": "Third bullet on wallet reset caution" + }, + "settingsResetCautionCheckbox": "I've backed up my recovery phrase", + "@settingsResetCautionCheckbox": { + "description": "Checkbox label on wallet reset caution" + }, + + "settingsPreferencesCurrency": "Currency", + "@settingsPreferencesCurrency": { + "description": "Currency row on preferences screen" + }, + "settingsPreferencesCurrencySubtitle": "Fiat display preference", + "@settingsPreferencesCurrencySubtitle": { + "description": "Currency row subtitle" + }, + "settingsPreferencesLanguage": "Language", + "@settingsPreferencesLanguage": { + "description": "Language row on preferences screen" + }, + "settingsPreferencesLanguageSubtitle": "App display language", + "@settingsPreferencesLanguageSubtitle": { + "description": "Language row subtitle" + }, + "settingsPreferencesPosMode": "POS Mode", + "@settingsPreferencesPosMode": { + "description": "POS mode row on preferences" + }, + "settingsPreferencesPosModeSubtitle": "Point of sale features", + "@settingsPreferencesPosModeSubtitle": { + "description": "POS mode row subtitle" + }, + "settingsPreferencesNotifications": "Notifications", + "@settingsPreferencesNotifications": { + "description": "Notifications row on preferences" + }, + "settingsPreferencesNotificationsSubtitle": "Transaction and wallet alerts", + "@settingsPreferencesNotificationsSubtitle": { + "description": "Notifications row subtitle" + }, + + "settingsCurrencyTitle": "Currency", + "@settingsCurrencyTitle": { + "description": "App bar on currency picker" + }, + "settingsCurrencySearchHint": "Search", + "@settingsCurrencySearchHint": { + "description": "Search field hint on currency picker" + }, + "settingsCurrencyNoMatch": "No currencies match your search", + "@settingsCurrencyNoMatch": { + "description": "Empty state when search has no results" + }, + "settingsCurrencyError": "Error selecting currency: {error}", + "@settingsCurrencyError": { + "description": "Error when currency selection fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "settingsLanguageTitle": "Language", + "@settingsLanguageTitle": { + "description": "App bar on language picker" + }, + "settingsLanguageSearchHint": "Search", + "@settingsLanguageSearchHint": { + "description": "Search field hint on language picker" + }, + "settingsLanguageNoMatch": "No languages match your search", + "@settingsLanguageNoMatch": { + "description": "Empty state when language search has no results" + }, + "settingsLanguageError": "Error selecting language: {error}", + "@settingsLanguageError": { + "description": "Error when language selection fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "settingsMiningTitle": "Mining Rewards", + "@settingsMiningTitle": { + "description": "App bar on mining rewards screen" + }, + "settingsMiningRedeem": "Redeem", + "@settingsMiningRedeem": { + "description": "Redeem button on mining rewards" + }, + "settingsMiningStatusMining": "Mining", + "@settingsMiningStatusMining": { + "description": "Active mining status label" + }, + "settingsMiningStatusPending": "Pending", + "@settingsMiningStatusPending": { + "description": "Pending mining status label" + }, + "settingsMiningBlocksMined": "BLOCKS MINED", + "@settingsMiningBlocksMined": { + "description": "Blocks mined stat label" + }, + "settingsMiningBlocksAcrossTestnets": "blocks across all testnets", + "@settingsMiningBlocksAcrossTestnets": { + "description": "Subtitle under blocks mined count" + }, + "settingsMiningStatTestnetBlocks": "TESTNET BLOCKS", + "@settingsMiningStatTestnetBlocks": { + "description": "Testnet blocks stat label" + }, + "settingsMiningStatTestnetRewards": "TESTNET REWARDS", + "@settingsMiningStatTestnetRewards": { + "description": "Testnet rewards stat label" + }, + "settingsMiningStatRedeemed": "REDEEMED", + "@settingsMiningStatRedeemed": { + "description": "Redeemed rewards stat label" + }, + "settingsMiningStatRedeemable": "REDEEMABLE", + "@settingsMiningStatRedeemable": { + "description": "Redeemable rewards stat label" + }, + "settingsMiningQuanEarned": "QUAN EARNED", + "@settingsMiningQuanEarned": { + "description": "QUAN earned stat label" + }, + "settingsMiningViewTelemetry": "View Telemetry ↗", + "@settingsMiningViewTelemetry": { + "description": "Link to mining telemetry" + }, + "settingsMiningNoDataTitle": "No mining data yet", + "@settingsMiningNoDataTitle": { + "description": "Empty state title on mining rewards" + }, + "settingsMiningNoDataBody": "Set up a Quantus mining node to start earning rewards.", + "@settingsMiningNoDataBody": { + "description": "Empty state body on mining rewards" + }, + "settingsMiningSetupGuide": "Mining Setup Guide ↗", + "@settingsMiningSetupGuide": { + "description": "Link to mining setup guide" + }, + "settingsMiningLoadError": "Failed to load mining rewards", + "@settingsMiningLoadError": { + "description": "Error title on mining rewards screen" + }, + "settingsMiningCheckConnection": "Please check your connection", + "@settingsMiningCheckConnection": { + "description": "Error subtitle when connection fails" + }, + "settingsMiningTestnetBlocks": "blocks", + "@settingsMiningTestnetBlocks": { + "description": "Blocks label on testnet row" + }, + "settingsMiningDiracSince": "Nov 2025", + "@settingsMiningDiracSince": { + "description": "Dirac testnet active since date" + }, + "settingsMiningSchrodingerSince": "Oct 2025", + "@settingsMiningSchrodingerSince": { + "description": "Schrödinger testnet active since date" + }, + "settingsMiningResonanceSince": "Jul 2025", + "@settingsMiningResonanceSince": { + "description": "Resonance testnet active since date" + }, + + "settingsTestnetTitle": "Testnet Rewards", + "@settingsTestnetTitle": { + "description": "App bar on testnet rewards screen" + }, + "settingsTestnetLoadError": "Failed to load testnet rewards", + "@settingsTestnetLoadError": { + "description": "Error title on testnet rewards" + }, + "settingsTestnetTotalBlocks": "{count} blocks", + "@settingsTestnetTotalBlocks": { + "description": "Total blocks headline on testnet rewards", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsTestnetTotalDescription": "Total blocks mined across all testnets", + "@settingsTestnetTotalDescription": { + "description": "Description under total blocks" + }, + "settingsTestnetBreakdown": "Breakdown", + "@settingsTestnetBreakdown": { + "description": "Breakdown section header" + }, + "settingsTestnetRowBlocks": "{count} blocks", + "@settingsTestnetRowBlocks": { + "description": "Blocks count in testnet breakdown row", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "settingsHelpScreenTitle": "Help & Support", + "@settingsHelpScreenTitle": { + "description": "App bar on help and support screen" + }, + "settingsHelpEmail": "Email Support", + "@settingsHelpEmail": { + "description": "Email support row title" + }, + "settingsHelpTelegram": "Telegram", + "@settingsHelpTelegram": { + "description": "Telegram row title" + }, + + "settingsAboutScreenTitle": "About", + "@settingsAboutScreenTitle": { + "description": "App bar on about screen" + }, + "settingsAboutIntro": "Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.", + "@settingsAboutIntro": { + "description": "Intro paragraph on about screen" + }, + "settingsAboutTerms": "Terms of Service", + "@settingsAboutTerms": { + "description": "Terms of service link title" + }, + "settingsAboutTermsSubtitle": "quantus.com/terms/", + "@settingsAboutTermsSubtitle": { + "description": "Terms of service link subtitle" + }, + "settingsAboutPrivacy": "Privacy policy", + "@settingsAboutPrivacy": { + "description": "Privacy policy link title" + }, + "settingsAboutPrivacySubtitle": "quantus.com/privacy-policy/", + "@settingsAboutPrivacySubtitle": { + "description": "Privacy policy link subtitle" + }, + "settingsAboutWebsite": "Visit Website", + "@settingsAboutWebsite": { + "description": "Website link title" + }, + "settingsAboutWebsiteSubtitle": "quantus.com", + "@settingsAboutWebsiteSubtitle": { + "description": "Website link subtitle" + }, + "settingsAboutVersion": "Version {version} ({build})", + "@settingsAboutVersion": { + "description": "Version label on about screen", + "placeholders": { + "version": { + "type": "String" + }, + "build": { + "type": "String" + } + } + }, + + "settingsAccountTypeScreenTitle": "Account Type", + "@settingsAccountTypeScreenTitle": { + "description": "App bar on account type settings" + }, + "settingsAccountTypeIntro": "Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.", + "@settingsAccountTypeIntro": { + "description": "Intro on account type settings" + }, + "settingsAccountTypeReversibleTitle": "Reversible Transactions", + "@settingsAccountTypeReversibleTitle": { + "description": "Reversible transactions feature title" + }, + "settingsAccountTypeReversibleSubtitle": "Reverse your sends within a time window", + "@settingsAccountTypeReversibleSubtitle": { + "description": "Reversible transactions feature subtitle" + }, + "settingsAccountTypeHighSecurityTitle": "High Security Account", + "@settingsAccountTypeHighSecurityTitle": { + "description": "High security account feature title" + }, + "settingsAccountTypeHighSecuritySubtitle": "Guardian approval required", + "@settingsAccountTypeHighSecuritySubtitle": { + "description": "High security account feature subtitle" + }, + "settingsAccountTypeMultiSigTitle": "Multi-Signature", + "@settingsAccountTypeMultiSigTitle": { + "description": "Multi-signature feature title" + }, + "settingsAccountTypeMultiSigSubtitle": "Multiple approvals required", + "@settingsAccountTypeMultiSigSubtitle": { + "description": "Multi-signature feature subtitle" + }, + "settingsAccountTypeHardwareTitle": "Hardware Wallet", + "@settingsAccountTypeHardwareTitle": { + "description": "Hardware wallet feature title" + }, + "settingsAccountTypeHardwareSubtitle": "Pair a hardware device", + "@settingsAccountTypeHardwareSubtitle": { + "description": "Hardware wallet feature subtitle" + }, + "settingsAccountTypeComingSoon": "Coming Soon", + "@settingsAccountTypeComingSoon": { + "description": "Coming soon badge on account type features" + }, + + "swapTitle": "Swap", + "@swapTitle": { + "description": "App bar title on swap screens" + }, + "swapFrom": "From", + "@swapFrom": { + "description": "From token section label on swap screen" + }, + "swapTo": "To", + "@swapTo": { + "description": "To token section label on swap screen" + }, + "swapRefundAddress": "Refund Address", + "@swapRefundAddress": { + "description": "Refund address field label on swap screen" + }, + "swapRefundAddressHint": "{network} Address", + "@swapRefundAddressHint": { + "description": "Refund address field hint", + "placeholders": { + "network": { + "type": "String" + } + } + }, + "swapSlippageTolerance": "Slippage Tolerance", + "@swapSlippageTolerance": { + "description": "Slippage tolerance label on swap screen" + }, + "swapRate": "Rate", + "@swapRate": { + "description": "Exchange rate label on swap screen" + }, + "swapGetQuote": "Get a Quote", + "@swapGetQuote": { + "description": "Get quote button on swap screen" + }, + "swapRateLabel": "1 QUAN = {amount} {symbol}", + "@swapRateLabel": { + "description": "Exchange rate display", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "swapRateZero": "1 QUAN = 0 {symbol}", + "@swapRateZero": { + "description": "Exchange rate when amount is zero", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + + "swapTokenPickerTitle": "Select Token", + "@swapTokenPickerTitle": { + "description": "Title on token picker sheet" + }, + "swapTokenPickerLoadError": "Failed to load tokens", + "@swapTokenPickerLoadError": { + "description": "Error when token list fails to load" + }, + + "swapReviewTitle": "Review Quote", + "@swapReviewTitle": { + "description": "Title on review quote sheet" + }, + "swapReviewTotalFees": "Total fees", + "@swapReviewTotalFees": { + "description": "Total fees row on review quote sheet" + }, + "swapReviewTotalAmount": "Total Amount", + "@swapReviewTotalAmount": { + "description": "Total amount row on review quote sheet" + }, + "swapReviewSlippageWarning": "You could receive up to ${amount} less based on the {percent}% slippage you set", + "@swapReviewSlippageWarning": { + "description": "Slippage warning on review quote sheet", + "placeholders": { + "amount": { + "type": "String" + }, + "percent": { + "type": "String" + } + } + }, + "swapReviewConfirm": "Confirm", + "@swapReviewConfirm": { + "description": "Confirm button on review quote sheet" + }, + + "swapDepositAmount": "Deposit Amount", + "@swapDepositAmount": { + "description": "Deposit amount label on deposit screen" + }, + "swapDepositAmountCopied": "Deposit amount copied to clipboard", + "@swapDepositAmountCopied": { + "description": "Toast when deposit amount is copied" + }, + "swapDepositDemoWarning": "For demo purposes only - do not send funds!", + "@swapDepositDemoWarning": { + "description": "Demo warning on deposit screen" + }, + "swapDepositShareQr": "Share QR", + "@swapDepositShareQr": { + "description": "Share QR button on deposit screen" + }, + "swapDepositShareContent": "Network: {network}\nToken: {token}\nAddress: {address}", + "@swapDepositShareContent": { + "description": "Share text for deposit details", + "placeholders": { + "network": { + "type": "String" + }, + "token": { + "type": "String" + }, + "address": { + "type": "String" + } + } + }, + "swapDepositNotice": "Use your {symbol} or {network} wallet to deposit funds. Depositing other assets may result in loss of funds.", + "@swapDepositNotice": { + "description": "Deposit wallet notice on deposit screen", + "placeholders": { + "symbol": { + "type": "String" + }, + "network": { + "type": "String" + } + } + }, + "swapDepositProcessingTitle": "Processing Swap", + "@swapDepositProcessingTitle": { + "description": "Title while swap is processing" + }, + "swapDepositProcessingBody": "This may take a few minutes...", + "@swapDepositProcessingBody": { + "description": "Body while swap is processing" + }, + "swapDepositCompleteTitle": "Swap Complete", + "@swapDepositCompleteTitle": { + "description": "Title when swap is complete" + }, + "swapDepositCompleteBody": "Your swap for {amount} QUAN is complete.", + "@swapDepositCompleteBody": { + "description": "Body when swap is complete", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "swapDepositTestnetBanner": "DEMO ONLY - WE ARE STILL ON TESTNET", + "@swapDepositTestnetBanner": { + "description": "Testnet demo banner on deposit screen" + }, + "swapDepositSentFunds": "I've sent the funds", + "@swapDepositSentFunds": { + "description": "Button to confirm funds sent" + }, + "swapDepositDone": "Done", + "@swapDepositDone": { + "description": "Done button after swap completes" + }, + + "swapRefundPickerTitle": "Refund Addresses", + "@swapRefundPickerTitle": { + "description": "Title on refund address picker sheet" + }, + "swapRefundPickerEmpty": "No recent refund addresses", + "@swapRefundPickerEmpty": { + "description": "Empty state on refund address picker" + }, + + "componentQrScannerTitle": "Scan QR Code", + "@componentQrScannerTitle": { + "description": "Text for app bar or button label on QR scanner component" + }, + "componentQrScannerNoCode": "No QR code found in image", + "@componentQrScannerNoCode": { + "description": "Snackbar when gallery image has no QR code" + }, + "componentShare": "Share", + "@componentShare": { + "description": "Share button label on account screens" + }, + "componentAddressLabel": "ADDRESS", + "@componentAddressLabel": { + "description": "Address field label on address details card" + }, + "componentCheckphraseLabel": "CHECKPHRASE", + "@componentCheckphraseLabel": { + "description": "Checkphrase field label on address details card" + }, + "componentCheckphraseCopied": "Checkphrase copied", + "@componentCheckphraseCopied": { + "description": "Toast when checkphrase is copied" + }, + "componentNameFieldHint": "Enter a name for your account", + "@componentNameFieldHint": { + "description": "Hint text on account name field" + }, + + "commonLoading": "Loading...", + "@commonLoading": { + "description": "Text for generic loading state" + }, + "commonAmountBalance": "{balance} {symbol}", + "@commonAmountBalance": { + "description": "Formatted balance with token symbol", + "placeholders": { + "balance": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "commonContinue": "Continue", + "@commonContinue": { + "description": "Continue button on various screens" + } + } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb new file mode 100644 index 00000000..73450e04 --- /dev/null +++ b/mobile-app/lib/l10n/app_id.arb @@ -0,0 +1,372 @@ +{ + "walletInitErrorTitle": "Wallet Bermasalah", + "walletInitErrorMessage": "Gagal mencari secret phrase. Coba pulihkan wallet anda.", + "walletInitErrorButtonLabel": "OK", + + "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk mengakses wallet", + "authAuthenticating": "Mengotentikasi...", + "authUnlockWallet": "Buka Wallet", + "authAuthorizationRequired": "Otorisasi \n Diperlukan", + + "welcomeTagline": "Uang Terenkripsi Aman Kuantum", + "welcomeCreateNewWallet": "Buat Wallet Baru", + "welcomeImportWallet": "Impor Wallet", + + "createWalletAppBarTitle": "Buat Wallet", + "createWalletCautionHeadline": "Jaga Kerahasiaan Recovery Phrase Anda", + "createWalletCautionBullet1": "Jika Anda kehilangan perangkat ini, recovery phrase adalah satu-satunya cara kembali", + "createWalletCautionBullet2": "Siapa pun yang mendapatkannya akan memiliki kendali penuh atas dana Anda, secara permanen", + "createWalletCautionBullet3": "Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital", + "createWalletCautionCheckboxLabel": "Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.", + "createWalletRecoveryPhraseNext": "Berikutnya", + "createWalletRecoveryPhraseFailedGenerate": "Gagal membuat: {error}", + "createWalletRecoveryPhraseSaveError": "Gagal menyimpan wallet: {error}", + + "recoveryPhraseBodyInstructions": "Tuliskan kata-kata ini secara berurutan dan simpan di tempat yang hanya Anda yang bisa akses. Jangan screenshot atau salin ke aplikasi catatan.", + "recoveryPhraseBodyCopy": "Salin", + "recoveryPhraseBodyCopiedMessage": "Recovery phrase disalin ke clipboard", + + "accountReadyAccountCreated": "Akun Dibuat", + "accountReadyWalletCreated": "Wallet Dibuat", + "accountReadyWalletImported": "Wallet Diimpor", + "accountReadyDone": "Selesai", + + "importWalletAppBarTitle": "Impor Wallet", + "importWalletDescription": "Pulihkan wallet yang ada dengan recovery phrase 12 atau 24 kata Anda", + "importWalletHint": "Ketik atau tempel recovery phrase Anda. Pisahkan kata dengan spasi.", + "importWalletButton": "Impor", + "importWalletValidationError": "Recovery phrase harus 12 atau 24 kata", + + "homeError": "Gagal: {error}", + "homeNoActiveAccount": "Tidak ada akun aktif", + "homeCharge": "Tagih", + "homeGetTestnetTokens": "Dapatkan Token Testnet ↗", + "homeErrorLoadingBalance": "Gagal memuat saldo", + "homeReceive": "Terima", + "homeSend": "Kirim", + "homeSwap": "Tukar", + + "homeActivityTitle": "Aktivitas", + "homeActivityViewAll": "Lihat Semua", + "homeActivityErrorLoading": "Gagal memuat transaksi", + "homeActivityRetry": "Coba Lagi", + "homeActivityEmptyTitle": "Belum Ada Transaksi", + "homeActivityEmptyMessage": "Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN.", + + "accountsSheetTitle": "Akun", + "accountsSheetFailedLoadAccounts": "Gagal memuat akun.", + "accountsSheetFailedLoadActiveAccount": "Gagal memuat akun aktif.", + "accountsSheetNoAccountsFound": "Tidak ada akun ditemukan.", + "accountsSheetAddAccount": "Tambah Akun", + "accountsSheetBalanceUnavailable": "Saldo tidak tersedia", + "accountsSheetBalance": "{balance} {symbol}", + + "addAccountMenuTitle": "Tambah Akun", + "addAccountMenuCreateTitle": "Buat Akun Baru", + "addAccountMenuCreateSubtitle": "Buat alamat wallet baru", + "addAccountMenuImportTitle": "Impor Wallet", + "addAccountMenuImportSubtitle": "Gunakan recovery phrase untuk mengimpor", + + "createAccountAppBarTitle": "Nama Akun", + "createAccountSubtitle": "Berikan nama yang mudah Anda kenali. Anda bisa mengubahnya kapan saja.", + "createAccountButton": "Buat", + "createAccountErrorCouldNotAdd": "Gagal menambahkan akun.", + "createAccountDefaultName": "Akun {number}", + + "editAccountAppBarTitle": "Nama Akun", + "editAccountDone": "Selesai", + "editAccountNameEmpty": "Nama akun tidak boleh kosong", + "editAccountRenameFailed": "Gagal mengganti nama akun.", + + "accountMenuTitle": "Akun", + "accountMenuAccountName": "Nama Akun", + "accountMenuAddressDetails": "Detail Alamat", + "accountMenuShowRecoveryPhrase": "Tampilkan Recovery Phrase", + "accountMenuNotFound": "Akun tidak ditemukan", + + "accountDetailsTitle": "Detail Alamat", + + "addHardwareAccountAddWallet": "Tambah Hardware Wallet", + "addHardwareAccountAddAccount": "Tambah Akun Hardware", + "addHardwareAccountNameLabel": "NAMA", + "addHardwareAccountNameHintWallet": "Hardware Wallet", + "addHardwareAccountNameHintAccount": "Akun", + "addHardwareAccountAddressLabel": "ALAMAT", + "addHardwareAccountAddressHint": "Alamat SS58", + "addHardwareAccountDebugFill": "Isi Debug", + "addHardwareAccountNameRequired": "Nama wajib diisi", + "addHardwareAccountInvalidAddress": "Alamat tidak valid", + + "sendTitle": "Kirim", + "sendPayTitle": "Bayar", + "sendEnterAddress": "Masukkan Alamat", + + "sendSelectRecipientSendTo": "Kirim Ke", + "sendSelectRecipientSearchHint": "Cari Alamat {symbol}", + "sendSelectRecipientScanTitle": "Pindai kode QR", + "sendSelectRecipientScanSubtitle": "Ketuk untuk memindai Alamat {symbol}", + "sendSelectRecipientRecents": "Terbaru", + "sendSelectRecipientContinue": "Lanjutkan", + + "sendInputAmountSendTo": "KIRIM KE", + "sendInputAmountAvailableBalance": "Saldo Tersedia:", + "sendInputAmountNetworkFee": "Biaya Jaringan:", + "sendInputAmountMax": "Maks", + "sendInputAmountInvalidAmount": "Masukkan jumlah yang valid", + "sendInputAmountChecksumRequired": "Checksum penerima diperlukan", + + "sendReviewSending": "MENGIRIM", + "sendReviewTo": "KE", + "sendReviewAmount": "JUMLAH", + "sendReviewNetworkFee": "BIAYA JARINGAN", + "sendReviewYouPay": "ANDA BAYAR", + "sendReviewConfirm": "Konfirmasi", + "sendReviewAuthReason": "Autentikasi untuk mengonfirmasi transaksi", + "sendReviewAuthRequired": "Autentikasi diperlukan untuk mengirim", + "sendReviewSubmitFailed": "Gagal mengirim transaksi", + + "sendTxSubmittedHeadlinePaid": "{amount} {symbol} dibayar", + "sendTxSubmittedHeadlineSent": "{amount} {symbol} terkirim", + "sendTxSubmittedOnItsWay": "Sedang dalam perjalanan", + "sendTxSubmittedToLabel": "Ke", + "sendTxSubmittedDone": "Selesai", + + "sendLogicCantSelfTransfer": "Tidak Bisa Transfer ke Diri Sendiri", + "sendLogicEnterAmount": "Masukkan Jumlah", + "sendLogicInvalidAmount": "Jumlah Tidak Valid", + "sendLogicBelowExistentialDeposit": "Di Bawah Deposit Eksistensial", + "sendLogicInsufficientBalance": "Saldo Tidak Cukup", + "sendLogicReviewSend": "Tinjau Pengiriman", + + "activityTitle": "Aktivitas", + "activityError": "Gagal: {error}", + "activityNoAccount": "Tidak ada akun", + "activityEmpty": "Belum ada transaksi", + "activityFilterAll": "Semua", + "activityFilterSend": "Kirim", + "activityFilterReceive": "Terima", + "activityDateToday": "Hari Ini", + "activityDateYesterday": "Kemarin", + + "activityTxSending": "Mengirim", + "activityTxReceiving": "Menerima", + "activityTxPending": "Tertunda", + "activityTxSent": "Terkirim", + "activityTxReceived": "Diterima", + "activityTxTo": "Ke", + "activityTxFrom": "Dari", + "activityTxTimeNow": "sekarang", + "activityTxTimeMinutesAgo": "{minutes}m lalu", + "activityTxTimeHoursAgo": "{hours}j lalu", + "activityTxTimeDaysAgo": "{days}h lalu", + "activityTxTimeRemaining": "{days}h:{hours}j:{minutes}m", + + "activityDetailTitleSending": "Mengirim", + "activityDetailTitleScheduled": "Terjadwal", + "activityDetailTitleReceiving": "Menerima", + "activityDetailTitleSent": "Terkirim", + "activityDetailTitleReceived": "Diterima", + "activityDetailStatusInProcess": "Diproses", + "activityDetailStatusScheduled": "Terjadwal", + "activityDetailStatusCompleted": "Selesai", + "activityDetailStatus": "STATUS", + "activityDetailTo": "KE", + "activityDetailFrom": "DARI", + "activityDetailDate": "TANGGAL", + "activityDetailNetworkFee": "BIAYA JARINGAN", + "activityDetailTxHash": "HASH TX", + "activityDetailViewExplorer": "Lihat di Explorer ↗", + + "receiveTitle": "Terima", + "receiveTabQrCode": "Kode QR", + "receiveTabAddress": "Alamat", + "receiveCopy": "Salin", + "receiveErrorLoadingAccount": "Gagal memuat data akun: {error}", + "receiveClipboardContent": "ID Akun:\n{accountId}\n\nCheckphrase:\n{checksum}", + "receiveCopiedMessage": "Detail akun disalin ke clipboard", + + "posAmountTitle": "Tagihan Baru", + "posAmountCharge": "Tagih {amount}", + "posAmountEnterAmount": "Masukkan Jumlah", + + "posQrTitleScanToPay": "Pindai untuk Bayar", + "posQrTitlePaymentReceived": "Pembayaran Diterima", + "posQrError": "Gagal: {error}", + "posQrNoActiveAccount": "Tidak ada akun aktif", + "posQrInvalidAmount": "Jumlah tidak valid. Ketuk untuk coba lagi.", + "posQrConnectionLost": "Koneksi terputus. Ketuk untuk coba lagi.", + "posQrTimedOut": "Waktu habis. Ketuk untuk coba lagi.", + "posQrNewCharge": "Tagihan Baru", + "posQrDone": "Selesai", + "posQrAmountReceived": "{amount} diterima", + "posQrFrom": "Dari:", + "posQrWaitingForPayment": "Menunggu pembayaran", + "posQrNetworkError": "Jaringan Bermasalah", + "posQrTryAgain": "Coba Lagi", + "posQrPaidAt": "Pada {time}", + + "settingsTitle": "Pengaturan", + "settingsWalletTitle": "Dompet", + "settingsWalletSubtitle": "Frasa Pemulihan, Reset Dompet", + "settingsPreferencesTitle": "Preferensi", + "settingsPreferencesSubtitle": "Bahasa, mata uang, mode POS, notifikasi", + "settingsMiningRewards": "Hadiah Mining", + "settingsMiningRewardsSubtitle": "{count} blok ditambang", + "settingsMiningRewardsError": "Gagal memuat hadiah mining", + "settingsAccountTypeTitle": "Jenis Akun", + "settingsAccountTypeSubtitle": "Fitur Akun Lanjutan", + "settingsHelpTitle": "Bantuan & Dukungan", + "settingsHelpSubtitle": "FAQ, Hubungi tim", + "settingsAboutTitle": "Tentang Quantus", + "settingsAboutHubSubtitle": "Versi {version} ({build})", + + "settingsWalletRecoveryPhrase": "Frasa Pemulihan", + "settingsWalletRecoveryPhraseSubtitle": "Lihat Kata Sandi Cadangan 24 kata Anda", + "settingsWalletReset": "Reset Dompet", + "settingsWalletResetSubtitle": "Menghapus semua data dari perangkat ini", + "settingsWalletNoWalletsFound": "Tidak ada dompet ditemukan", + "settingsWalletFailedToLoad": "Gagal memuat dompet", + + "settingsSelectWalletTitle": "Pilih Dompet", + "settingsSelectWalletNoWallets": "Tidak ada dompet ditemukan", + "settingsSelectWalletItem": "Dompet {number}", + + "settingsRecoveryConfirmAuthReason": "Autentikasi untuk melihat frasa pemulihan", + "settingsRecoveryConfirmAuthRequired": "Autentikasi diperlukan untuk melihat frasa pemulihan", + + "settingsRecoveryPhraseTitle": "Frasa Pemulihan", + "settingsRecoveryPhraseDone": "Selesai", + + "settingsResetTitle": "Reset Dompet", + "settingsResetAuthReason": "Autentikasi untuk mereset dompet", + "settingsResetFailed": "Gagal mereset dompet: {error}", + "settingsResetAuthRequired": "Autentikasi diperlukan untuk mereset dompet", + "settingsResetCautionHeadline": "Ini akan menghapus\ndompet Anda", + "settingsResetCautionBullet1": "Semua data dompet akan dihapus permanen dari perangkat ini", + "settingsResetCautionBullet2": "Dana Anda tetap di blockchain tetapi hanya frasa pemulihan yang dapat memulihkan akses", + "settingsResetCautionBullet3": "Tanpa frasa pemulihan, dana Anda hilang selamanya", + "settingsResetCautionCheckbox": "Saya sudah mencadangkan frasa pemulihan saya", + + "settingsPreferencesLanguage": "Bahasa", + "settingsPreferencesLanguageSubtitle": "Bahasa tampilan aplikasi", + "settingsPreferencesCurrency": "Mata Uang", + "settingsPreferencesCurrencySubtitle": "Preferensi tampilan fiat", + "settingsPreferencesPosMode": "Mode POS", + "settingsPreferencesPosModeSubtitle": "Fitur point of sale", + "settingsPreferencesNotifications": "Notifikasi", + "settingsPreferencesNotificationsSubtitle": "Peringatan transaksi dan dompet", + + "settingsCurrencyTitle": "Mata Uang", + "settingsCurrencySearchHint": "Cari", + "settingsCurrencyNoMatch": "Tidak ada mata uang yang cocok dengan pencarian Anda", + "settingsCurrencyError": "Gagal memilih mata uang: {error}", + + "settingsLanguageTitle": "Bahasa", + "settingsLanguageSearchHint": "Cari", + "settingsLanguageNoMatch": "Tidak ada bahasa yang cocok dengan pencarian Anda", + "settingsLanguageError": "Gagal memilih bahasa: {error}", + + "settingsMiningTitle": "Hadiah Mining", + "settingsMiningRedeem": "Tukar", + "settingsMiningStatusMining": "Mining", + "settingsMiningStatusPending": "Menunggu", + "settingsMiningBlocksMined": "BLOK DITAMBANG", + "settingsMiningBlocksAcrossTestnets": "blok di semua testnet", + "settingsMiningStatTestnetBlocks": "BLOK TESTNET", + "settingsMiningStatTestnetRewards": "HADIAH TESTNET", + "settingsMiningStatRedeemed": "DITUKAR", + "settingsMiningStatRedeemable": "DAPAT DITUKAR", + "settingsMiningQuanEarned": "QUAN DIHASILKAN", + "settingsMiningViewTelemetry": "Lihat Telemetri ↗", + "settingsMiningNoDataTitle": "Belum ada data mining", + "settingsMiningNoDataBody": "Siapkan node mining Quantus untuk mulai mendapatkan hadiah.", + "settingsMiningSetupGuide": "Panduan Setup Mining ↗", + "settingsMiningLoadError": "Gagal memuat hadiah mining", + "settingsMiningCheckConnection": "Periksa koneksi Anda", + "settingsMiningTestnetBlocks": "blok", + "settingsMiningDiracSince": "Nov 2025", + "settingsMiningSchrodingerSince": "Okt 2025", + "settingsMiningResonanceSince": "Jul 2025", + + "settingsTestnetTitle": "Hadiah Testnet", + "settingsTestnetLoadError": "Gagal memuat hadiah testnet", + "settingsTestnetTotalBlocks": "{count} blok", + "settingsTestnetTotalDescription": "Total blok ditambang di semua testnet", + "settingsTestnetBreakdown": "Rincian", + "settingsTestnetRowBlocks": "{count} blok", + + "settingsHelpScreenTitle": "Bantuan & Dukungan", + "settingsHelpEmail": "Dukungan Email", + "settingsHelpTelegram": "Telegram", + + "settingsAboutScreenTitle": "Tentang", + "settingsAboutIntro": "Quantus adalah blockchain Layer 1 yang diamankan oleh ML-DSA Dilithium-5, standar emas enkripsi tahan kuantum. Dibangun untuk masa depan di mana kriptografi klasik tidak lagi cukup. Kriptografi pasca-kuantum untuk semua orang.", + "settingsAboutTerms": "Ketentuan Layanan", + "settingsAboutTermsSubtitle": "quantus.com/terms/", + "settingsAboutPrivacy": "Kebijakan privasi", + "settingsAboutPrivacySubtitle": "quantus.com/privacy-policy/", + "settingsAboutWebsite": "Kunjungi Situs Web", + "settingsAboutWebsiteSubtitle": "quantus.com", + "settingsAboutVersion": "Versi {version} ({build})", + + "settingsAccountTypeScreenTitle": "Jenis Akun", + "settingsAccountTypeIntro": "Fitur akun lanjutan akan segera hadir. Fitur ini memberi Anda kontrol lebih besar atas cara transaksi diotorisasi dan diamankan.", + "settingsAccountTypeReversibleTitle": "Transaksi Reversible", + "settingsAccountTypeReversibleSubtitle": "Batalkan pengiriman dalam jangka waktu tertentu", + "settingsAccountTypeHighSecurityTitle": "Akun Keamanan Tinggi", + "settingsAccountTypeHighSecuritySubtitle": "Persetujuan guardian diperlukan", + "settingsAccountTypeMultiSigTitle": "Multi-Tanda Tangan", + "settingsAccountTypeMultiSigSubtitle": "Beberapa persetujuan diperlukan", + "settingsAccountTypeHardwareTitle": "Dompet Hardware", + "settingsAccountTypeHardwareSubtitle": "Pasangkan perangkat hardware", + "settingsAccountTypeComingSoon": "Segera Hadir", + + "swapTitle": "Tukar", + "swapFrom": "Dari", + "swapTo": "Ke", + "swapRefundAddress": "Alamat Refund", + "swapRefundAddressHint": "Alamat {network}", + "swapSlippageTolerance": "Toleransi Slippage", + "swapRate": "Kurs", + "swapGetQuote": "Dapatkan Penawaran", + "swapRateLabel": "1 QUAN = {amount} {symbol}", + "swapRateZero": "1 QUAN = 0 {symbol}", + + "swapTokenPickerTitle": "Pilih Token", + "swapTokenPickerLoadError": "Gagal memuat token", + + "swapReviewTitle": "Tinjau Penawaran", + "swapReviewTotalFees": "Total biaya", + "swapReviewTotalAmount": "Jumlah Total", + "swapReviewSlippageWarning": "Anda bisa menerima hingga ${amount} lebih sedikit berdasarkan slippage {percent}% yang Anda atur", + "swapReviewConfirm": "Konfirmasi", + + "swapDepositAmount": "Jumlah Deposit", + "swapDepositAmountCopied": "Jumlah deposit disalin ke clipboard", + "swapDepositDemoWarning": "Hanya untuk demo - jangan kirim dana!", + "swapDepositShareQr": "Bagikan QR", + "swapDepositShareContent": "Jaringan: {network}\nToken: {token}\nAlamat: {address}", + "swapDepositNotice": "Gunakan dompet {symbol} atau {network} Anda untuk deposit. Menyetor aset lain dapat mengakibatkan kehilangan dana.", + "swapDepositProcessingTitle": "Memproses Swap", + "swapDepositProcessingBody": "Ini mungkin memakan waktu beberapa menit...", + "swapDepositCompleteTitle": "Swap Selesai", + "swapDepositCompleteBody": "Swap Anda untuk {amount} QUAN telah selesai.", + "swapDepositTestnetBanner": "HANYA DEMO - KAMI MASIH DI TESTNET", + "swapDepositSentFunds": "Saya sudah mengirim dana", + "swapDepositDone": "Selesai", + + "swapRefundPickerTitle": "Alamat Refund", + "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru", + + "componentQrScannerTitle": "Pindai Kode QR", + "componentQrScannerNoCode": "Tidak ada kode QR pada gambar", + "componentShare": "Bagikan", + "componentAddressLabel": "ALAMAT", + "componentCheckphraseLabel": "CHECKPHRASE", + "componentCheckphraseCopied": "Checkphrase disalin", + "componentNameFieldHint": "Masukkan nama untuk akun Anda", + + "commonLoading": "Memuat...", + "commonAmountBalance": "{balance} {symbol}", + "commonContinue": "Lanjutkan" +} diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart new file mode 100644 index 00000000..dcd5b871 --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -0,0 +1,2058 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_id.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [Locale('en'), Locale('id')]; + + /// Title for the error dialog when the wallet is not found + /// + /// In en, this message translates to: + /// **'Wallet Error'** + String get walletInitErrorTitle; + + /// Message for the error dialog when the wallet is not found + /// + /// In en, this message translates to: + /// **'Unable to find secret phrase. Please restore your wallet.'** + String get walletInitErrorMessage; + + /// Label for the button on the error dialog when the wallet is not found + /// + /// In en, this message translates to: + /// **'OK'** + String get walletInitErrorButtonLabel; + + /// Text for the text on the lock screen when using device biometrics to unlock + /// + /// In en, this message translates to: + /// **'Use device biometrics to unlock'** + String get authUseDeviceBiometricsToUnlock; + + /// Text for the text on the lock screen when authenticating + /// + /// In en, this message translates to: + /// **'Authenticating...'** + String get authAuthenticating; + + /// Text for the button on the lock screen to unlock the wallet + /// + /// In en, this message translates to: + /// **'Unlock Wallet'** + String get authUnlockWallet; + + /// Text for displayed on the lock screen when authorization is required + /// + /// In en, this message translates to: + /// **'Authorization \n Required'** + String get authAuthorizationRequired; + + /// Tagline on the welcome screen + /// + /// In en, this message translates to: + /// **'Quantum Secure Encrypted Money'** + String get welcomeTagline; + + /// Button to start creating a new wallet on the welcome screen + /// + /// In en, this message translates to: + /// **'Create New Wallet'** + String get welcomeCreateNewWallet; + + /// Button to import an existing wallet on the welcome screen + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get welcomeImportWallet; + + /// App bar title for the create wallet flow + /// + /// In en, this message translates to: + /// **'Create Wallet'** + String get createWalletAppBarTitle; + + /// Headline on the recovery phrase caution screen during wallet creation + /// + /// In en, this message translates to: + /// **'Keep your Recovery Phrase Secret'** + String get createWalletCautionHeadline; + + /// First bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'If you lose this device, your recovery phrase is the only way back'** + String get createWalletCautionBullet1; + + /// Second bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'Anyone who gets hold of it has complete control over your funds, permanently'** + String get createWalletCautionBullet2; + + /// Third bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'Write it down and keep it somewhere safe. Do not save it digitally'** + String get createWalletCautionBullet3; + + /// Checkbox label on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'** + String get createWalletCautionCheckboxLabel; + + /// Primary button on the new wallet recovery phrase screen + /// + /// In en, this message translates to: + /// **'Next'** + String get createWalletRecoveryPhraseNext; + + /// Error when mnemonic generation fails + /// + /// In en, this message translates to: + /// **'Failed to generate: {error}'** + String createWalletRecoveryPhraseFailedGenerate(String error); + + /// Error when saving a new wallet fails + /// + /// In en, this message translates to: + /// **'Error saving wallet: {error}'** + String createWalletRecoveryPhraseSaveError(String error); + + /// Instructions above the recovery phrase word grid + /// + /// In en, this message translates to: + /// **'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.'** + String get recoveryPhraseBodyInstructions; + + /// Copy button on the recovery phrase screen + /// + /// In en, this message translates to: + /// **'Copy'** + String get recoveryPhraseBodyCopy; + + /// Toast when recovery phrase is copied + /// + /// In en, this message translates to: + /// **'Recovery phrase copied to clipboard'** + String get recoveryPhraseBodyCopiedMessage; + + /// Title when a new account is created + /// + /// In en, this message translates to: + /// **'Account Created'** + String get accountReadyAccountCreated; + + /// Title when a new wallet is created + /// + /// In en, this message translates to: + /// **'Wallet Created'** + String get accountReadyWalletCreated; + + /// Title when a wallet is imported + /// + /// In en, this message translates to: + /// **'Wallet Imported'** + String get accountReadyWalletImported; + + /// Done button on the account ready screen + /// + /// In en, this message translates to: + /// **'Done'** + String get accountReadyDone; + + /// App bar title on the import wallet screen + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get importWalletAppBarTitle; + + /// Description on the import wallet screen + /// + /// In en, this message translates to: + /// **'Restore an existing wallet with your 12 or 24 words recovery phrase'** + String get importWalletDescription; + + /// Hint for the recovery phrase text field + /// + /// In en, this message translates to: + /// **'Type in or paste your recovery phrase. Separate words with spaces.'** + String get importWalletHint; + + /// Import button on the import wallet screen + /// + /// In en, this message translates to: + /// **'Import'** + String get importWalletButton; + + /// Validation error when recovery phrase word count is invalid + /// + /// In en, this message translates to: + /// **'Recovery phrase must be 12 or 24 words'** + String get importWalletValidationError; + + /// Error message on the home screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String homeError(String error); + + /// Shown when no account is active on the home screen + /// + /// In en, this message translates to: + /// **'No active account'** + String get homeNoActiveAccount; + + /// POS charge button on the home screen bottom bar + /// + /// In en, this message translates to: + /// **'Charge'** + String get homeCharge; + + /// Faucet button when balance is zero on the home screen + /// + /// In en, this message translates to: + /// **'Get Testnet Tokens ↗'** + String get homeGetTestnetTokens; + + /// Error when balance fails to load on the home screen + /// + /// In en, this message translates to: + /// **'Error loading balance'** + String get homeErrorLoadingBalance; + + /// Receive action button on the home screen + /// + /// In en, this message translates to: + /// **'Receive'** + String get homeReceive; + + /// Send action button on the home screen + /// + /// In en, this message translates to: + /// **'Send'** + String get homeSend; + + /// Swap action button on the home screen + /// + /// In en, this message translates to: + /// **'Swap'** + String get homeSwap; + + /// Section title for recent activity on the home screen + /// + /// In en, this message translates to: + /// **'Activity'** + String get homeActivityTitle; + + /// Link to full activity screen from home + /// + /// In en, this message translates to: + /// **'View All'** + String get homeActivityViewAll; + + /// Error when transactions fail to load in home activity section + /// + /// In en, this message translates to: + /// **'Error loading transactions'** + String get homeActivityErrorLoading; + + /// Retry link in home activity section error state + /// + /// In en, this message translates to: + /// **'Retry'** + String get homeActivityRetry; + + /// Empty state title in home activity section + /// + /// In en, this message translates to: + /// **'No Transactions Yet'** + String get homeActivityEmptyTitle; + + /// Empty state message in home activity section + /// + /// In en, this message translates to: + /// **'Your activity will appear here once you send or receive QUAN.'** + String get homeActivityEmptyMessage; + + /// Title of the accounts bottom sheet + /// + /// In en, this message translates to: + /// **'Accounts'** + String get accountsSheetTitle; + + /// Error when accounts list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load accounts.'** + String get accountsSheetFailedLoadAccounts; + + /// Error when active account fails to load + /// + /// In en, this message translates to: + /// **'Failed to load active account.'** + String get accountsSheetFailedLoadActiveAccount; + + /// Empty state in accounts sheet + /// + /// In en, this message translates to: + /// **'No accounts found.'** + String get accountsSheetNoAccountsFound; + + /// Button to add a new account + /// + /// In en, this message translates to: + /// **'Add Account'** + String get accountsSheetAddAccount; + + /// When account balance fails to load + /// + /// In en, this message translates to: + /// **'Balance unavailable'** + String get accountsSheetBalanceUnavailable; + + /// Formatted balance with token symbol + /// + /// In en, this message translates to: + /// **'{balance} {symbol}'** + String accountsSheetBalance(String balance, String symbol); + + /// App bar title on add account menu + /// + /// In en, this message translates to: + /// **'Add Account'** + String get addAccountMenuTitle; + + /// Create new account menu row title + /// + /// In en, this message translates to: + /// **'Create New Account'** + String get addAccountMenuCreateTitle; + + /// Create new account menu row subtitle + /// + /// In en, this message translates to: + /// **'Generate a fresh wallet address'** + String get addAccountMenuCreateSubtitle; + + /// Import wallet menu row title + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get addAccountMenuImportTitle; + + /// Import wallet menu row subtitle + /// + /// In en, this message translates to: + /// **'Use a recovery phrase to import'** + String get addAccountMenuImportSubtitle; + + /// App bar title when creating an account + /// + /// In en, this message translates to: + /// **'Account Name'** + String get createAccountAppBarTitle; + + /// Subtitle on create account name field + /// + /// In en, this message translates to: + /// **'Give this account a name you\'ll recognize. You can change it anytime.'** + String get createAccountSubtitle; + + /// Create account button + /// + /// In en, this message translates to: + /// **'Create'** + String get createAccountButton; + + /// Error when account creation fails + /// + /// In en, this message translates to: + /// **'Could not add account.'** + String get createAccountErrorCouldNotAdd; + + /// Default name for a new account + /// + /// In en, this message translates to: + /// **'Account {number}'** + String createAccountDefaultName(int number); + + /// App bar title when editing account name + /// + /// In en, this message translates to: + /// **'Account Name'** + String get editAccountAppBarTitle; + + /// Done button on edit account screen + /// + /// In en, this message translates to: + /// **'Done'** + String get editAccountDone; + + /// Validation error when account name is empty + /// + /// In en, this message translates to: + /// **'Account name can\'t be empty'** + String get editAccountNameEmpty; + + /// Error when renaming account fails + /// + /// In en, this message translates to: + /// **'Failed to rename account.'** + String get editAccountRenameFailed; + + /// App bar title on account menu screen + /// + /// In en, this message translates to: + /// **'Accounts'** + String get accountMenuTitle; + + /// Account name menu row label + /// + /// In en, this message translates to: + /// **'Account Name'** + String get accountMenuAccountName; + + /// Address details menu row label + /// + /// In en, this message translates to: + /// **'Address Details'** + String get accountMenuAddressDetails; + + /// Show recovery phrase menu row label + /// + /// In en, this message translates to: + /// **'Show Recovery Phrase'** + String get accountMenuShowRecoveryPhrase; + + /// When account is not found on menu screen + /// + /// In en, this message translates to: + /// **'Account not found'** + String get accountMenuNotFound; + + /// App bar title on account details screen + /// + /// In en, this message translates to: + /// **'Address Details'** + String get accountDetailsTitle; + + /// Title when adding a new hardware wallet + /// + /// In en, this message translates to: + /// **'Add Hardware Wallet'** + String get addHardwareAccountAddWallet; + + /// Title when adding a hardware account to existing wallet + /// + /// In en, this message translates to: + /// **'Add Hardware Account'** + String get addHardwareAccountAddAccount; + + /// Name field label + /// + /// In en, this message translates to: + /// **'NAME'** + String get addHardwareAccountNameLabel; + + /// Name field hint for new hardware wallet + /// + /// In en, this message translates to: + /// **'Hardware Wallet'** + String get addHardwareAccountNameHintWallet; + + /// Name field hint for hardware account + /// + /// In en, this message translates to: + /// **'Account'** + String get addHardwareAccountNameHintAccount; + + /// Address field label + /// + /// In en, this message translates to: + /// **'ADDRESS'** + String get addHardwareAccountAddressLabel; + + /// Address field hint + /// + /// In en, this message translates to: + /// **'SS58 address'** + String get addHardwareAccountAddressHint; + + /// Debug fill button + /// + /// In en, this message translates to: + /// **'Debug Fill'** + String get addHardwareAccountDebugFill; + + /// Validation when name is empty + /// + /// In en, this message translates to: + /// **'Name is required'** + String get addHardwareAccountNameRequired; + + /// Validation when address is invalid + /// + /// In en, this message translates to: + /// **'Invalid address'** + String get addHardwareAccountInvalidAddress; + + /// Send flow app bar title + /// + /// In en, this message translates to: + /// **'Send'** + String get sendTitle; + + /// Pay flow app bar title + /// + /// In en, this message translates to: + /// **'Pay'** + String get sendPayTitle; + + /// Button label when recipient address is missing + /// + /// In en, this message translates to: + /// **'Enter Address'** + String get sendEnterAddress; + + /// Section label on select recipient screen + /// + /// In en, this message translates to: + /// **'Send To'** + String get sendSelectRecipientSendTo; + + /// Hint for recipient search field + /// + /// In en, this message translates to: + /// **'Search {symbol} Address'** + String sendSelectRecipientSearchHint(String symbol); + + /// Scan QR row title + /// + /// In en, this message translates to: + /// **'Scan QR code'** + String get sendSelectRecipientScanTitle; + + /// Scan QR row subtitle + /// + /// In en, this message translates to: + /// **'Tap to scan a {symbol} Address'** + String sendSelectRecipientScanSubtitle(String symbol); + + /// Recents section title + /// + /// In en, this message translates to: + /// **'Recents'** + String get sendSelectRecipientRecents; + + /// Continue button on select recipient screen + /// + /// In en, this message translates to: + /// **'Continue'** + String get sendSelectRecipientContinue; + + /// Recipient card label on input amount screen + /// + /// In en, this message translates to: + /// **'SEND TO'** + String get sendInputAmountSendTo; + + /// Available balance label + /// + /// In en, this message translates to: + /// **'Available Balance:'** + String get sendInputAmountAvailableBalance; + + /// Network fee label + /// + /// In en, this message translates to: + /// **'Network Fee:'** + String get sendInputAmountNetworkFee; + + /// Max amount button + /// + /// In en, this message translates to: + /// **'Max'** + String get sendInputAmountMax; + + /// Error when amount input is invalid + /// + /// In en, this message translates to: + /// **'Please enter a valid amount'** + String get sendInputAmountInvalidAmount; + + /// Error when recipient checksum is missing + /// + /// In en, this message translates to: + /// **'Recipient checksum is required'** + String get sendInputAmountChecksumRequired; + + /// Sending section label on review screen + /// + /// In en, this message translates to: + /// **'SENDING'** + String get sendReviewSending; + + /// To section label on review screen + /// + /// In en, this message translates to: + /// **'TO'** + String get sendReviewTo; + + /// Amount row label on review screen + /// + /// In en, this message translates to: + /// **'AMOUNT'** + String get sendReviewAmount; + + /// Network fee row label on review screen + /// + /// In en, this message translates to: + /// **'NETWORK FEE'** + String get sendReviewNetworkFee; + + /// Total you pay row label on review screen + /// + /// In en, this message translates to: + /// **'YOU PAY'** + String get sendReviewYouPay; + + /// Confirm button on review screen + /// + /// In en, this message translates to: + /// **'Confirm'** + String get sendReviewConfirm; + + /// Biometric auth prompt on review screen + /// + /// In en, this message translates to: + /// **'Authenticate to confirm transaction'** + String get sendReviewAuthReason; + + /// Error when auth fails on review screen + /// + /// In en, this message translates to: + /// **'Authentication required to send'** + String get sendReviewAuthRequired; + + /// Error when transaction submission fails + /// + /// In en, this message translates to: + /// **'Failed submitting transaction'** + String get sendReviewSubmitFailed; + + /// Success headline when payment completed + /// + /// In en, this message translates to: + /// **'{amount} {symbol} paid'** + String sendTxSubmittedHeadlinePaid(String amount, String symbol); + + /// Success headline when send completed + /// + /// In en, this message translates to: + /// **'{amount} {symbol} sent'** + String sendTxSubmittedHeadlineSent(String amount, String symbol); + + /// Subtitle on transaction submitted screen + /// + /// In en, this message translates to: + /// **'On its way'** + String get sendTxSubmittedOnItsWay; + + /// Recipient label on transaction submitted screen + /// + /// In en, this message translates to: + /// **'To'** + String get sendTxSubmittedToLabel; + + /// Done button on transaction submitted screen + /// + /// In en, this message translates to: + /// **'Done'** + String get sendTxSubmittedDone; + + /// Button label when sending to own address + /// + /// In en, this message translates to: + /// **'Can\'t Self Transfer'** + String get sendLogicCantSelfTransfer; + + /// Button label when amount is zero + /// + /// In en, this message translates to: + /// **'Enter Amount'** + String get sendLogicEnterAmount; + + /// Button label when amount is negative + /// + /// In en, this message translates to: + /// **'Invalid Amount'** + String get sendLogicInvalidAmount; + + /// Button label when amount is below existential deposit + /// + /// In en, this message translates to: + /// **'Below Existential Deposit'** + String get sendLogicBelowExistentialDeposit; + + /// Button label when balance is insufficient + /// + /// In en, this message translates to: + /// **'Insufficient Balance'** + String get sendLogicInsufficientBalance; + + /// Button label to proceed to review + /// + /// In en, this message translates to: + /// **'Review Send'** + String get sendLogicReviewSend; + + /// App bar title on activity screen + /// + /// In en, this message translates to: + /// **'Activity'** + String get activityTitle; + + /// Error message on activity screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String activityError(String error); + + /// Shown when no account is active on activity screen + /// + /// In en, this message translates to: + /// **'No account'** + String get activityNoAccount; + + /// Empty state on activity screen + /// + /// In en, this message translates to: + /// **'No transactions yet'** + String get activityEmpty; + + /// Filter button for all transactions + /// + /// In en, this message translates to: + /// **'All'** + String get activityFilterAll; + + /// Filter button for sent transactions + /// + /// In en, this message translates to: + /// **'Send'** + String get activityFilterSend; + + /// Filter button for received transactions + /// + /// In en, this message translates to: + /// **'Receive'** + String get activityFilterReceive; + + /// Date group label for today + /// + /// In en, this message translates to: + /// **'Today'** + String get activityDateToday; + + /// Date group label for yesterday + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get activityDateYesterday; + + /// Transaction row label for pending send + /// + /// In en, this message translates to: + /// **'Sending'** + String get activityTxSending; + + /// Transaction row label for pending or scheduled receive + /// + /// In en, this message translates to: + /// **'Receiving'** + String get activityTxReceiving; + + /// Transaction row label for scheduled send + /// + /// In en, this message translates to: + /// **'Pending'** + String get activityTxPending; + + /// Transaction row label for completed send + /// + /// In en, this message translates to: + /// **'Sent'** + String get activityTxSent; + + /// Transaction row label for completed receive + /// + /// In en, this message translates to: + /// **'Received'** + String get activityTxReceived; + + /// Counterparty direction label for send + /// + /// In en, this message translates to: + /// **'To'** + String get activityTxTo; + + /// Counterparty direction label for receive + /// + /// In en, this message translates to: + /// **'From'** + String get activityTxFrom; + + /// Time label for just now + /// + /// In en, this message translates to: + /// **'now'** + String get activityTxTimeNow; + + /// Time label for minutes ago + /// + /// In en, this message translates to: + /// **'{minutes}m ago'** + String activityTxTimeMinutesAgo(int minutes); + + /// Time label for hours ago + /// + /// In en, this message translates to: + /// **'{hours}h ago'** + String activityTxTimeHoursAgo(int hours); + + /// Time label for days ago + /// + /// In en, this message translates to: + /// **'{days}d ago'** + String activityTxTimeDaysAgo(int days); + + /// Time remaining for scheduled transaction + /// + /// In en, this message translates to: + /// **'{days}d:{hours}h:{minutes}m'** + String activityTxTimeRemaining(String days, String hours, String minutes); + + /// Detail sheet title for pending send + /// + /// In en, this message translates to: + /// **'Sending'** + String get activityDetailTitleSending; + + /// Detail sheet title for scheduled send + /// + /// In en, this message translates to: + /// **'Scheduled'** + String get activityDetailTitleScheduled; + + /// Detail sheet title for receiving + /// + /// In en, this message translates to: + /// **'Receiving'** + String get activityDetailTitleReceiving; + + /// Detail sheet title for completed send + /// + /// In en, this message translates to: + /// **'Sent'** + String get activityDetailTitleSent; + + /// Detail sheet title for completed receive + /// + /// In en, this message translates to: + /// **'Received'** + String get activityDetailTitleReceived; + + /// Status label for in-process transaction + /// + /// In en, this message translates to: + /// **'In Process'** + String get activityDetailStatusInProcess; + + /// Status label for scheduled transaction + /// + /// In en, this message translates to: + /// **'Scheduled'** + String get activityDetailStatusScheduled; + + /// Status label for completed transaction + /// + /// In en, this message translates to: + /// **'Completed'** + String get activityDetailStatusCompleted; + + /// Status row label on detail sheet + /// + /// In en, this message translates to: + /// **'STATUS'** + String get activityDetailStatus; + + /// To row label on detail sheet + /// + /// In en, this message translates to: + /// **'TO'** + String get activityDetailTo; + + /// From row label on detail sheet + /// + /// In en, this message translates to: + /// **'FROM'** + String get activityDetailFrom; + + /// Date row label on detail sheet + /// + /// In en, this message translates to: + /// **'DATE'** + String get activityDetailDate; + + /// Network fee row label on detail sheet + /// + /// In en, this message translates to: + /// **'NETWORK FEE'** + String get activityDetailNetworkFee; + + /// Transaction hash row label on detail sheet + /// + /// In en, this message translates to: + /// **'TX HASH'** + String get activityDetailTxHash; + + /// Link to view transaction in explorer + /// + /// In en, this message translates to: + /// **'View in Explorer ↗'** + String get activityDetailViewExplorer; + + /// App bar title on receive screen + /// + /// In en, this message translates to: + /// **'Receive'** + String get receiveTitle; + + /// QR Code tab on receive screen + /// + /// In en, this message translates to: + /// **'QR Code'** + String get receiveTabQrCode; + + /// Address tab on receive screen + /// + /// In en, this message translates to: + /// **'Address'** + String get receiveTabAddress; + + /// Copy button on receive screen + /// + /// In en, this message translates to: + /// **'Copy'** + String get receiveCopy; + + /// Error when account data fails to load on receive screen + /// + /// In en, this message translates to: + /// **'Error loading account data: {error}'** + String receiveErrorLoadingAccount(String error); + + /// Clipboard content when copying account details + /// + /// In en, this message translates to: + /// **'Account Id:\n{accountId}\n\nCheckphrase:\n{checksum}'** + String receiveClipboardContent(String accountId, String checksum); + + /// Toast when account details are copied + /// + /// In en, this message translates to: + /// **'Account details copied to clipboard'** + String get receiveCopiedMessage; + + /// App bar title on POS amount screen + /// + /// In en, this message translates to: + /// **'New Charge'** + String get posAmountTitle; + + /// Charge button with formatted amount + /// + /// In en, this message translates to: + /// **'Charge {amount}'** + String posAmountCharge(String amount); + + /// Charge button when amount is empty + /// + /// In en, this message translates to: + /// **'Enter Amount'** + String get posAmountEnterAmount; + + /// App bar title while waiting for payment + /// + /// In en, this message translates to: + /// **'Scan to Pay'** + String get posQrTitleScanToPay; + + /// App bar title when payment is received + /// + /// In en, this message translates to: + /// **'Payment Received'** + String get posQrTitlePaymentReceived; + + /// Error message on POS QR screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String posQrError(String error); + + /// Shown when no active account on POS QR screen + /// + /// In en, this message translates to: + /// **'No active account'** + String get posQrNoActiveAccount; + + /// Error when amount cannot be parsed + /// + /// In en, this message translates to: + /// **'Invalid amount. Tap to retry.'** + String get posQrInvalidAmount; + + /// Error when payment watch connection is lost + /// + /// In en, this message translates to: + /// **'Connection lost. Tap to retry.'** + String get posQrConnectionLost; + + /// Error when payment watch times out + /// + /// In en, this message translates to: + /// **'Timed out. Tap to retry.'** + String get posQrTimedOut; + + /// New charge button on POS QR screen + /// + /// In en, this message translates to: + /// **'New Charge'** + String get posQrNewCharge; + + /// Done button after payment received + /// + /// In en, this message translates to: + /// **'Done'** + String get posQrDone; + + /// Headline when payment is received + /// + /// In en, this message translates to: + /// **'{amount} received'** + String posQrAmountReceived(String amount); + + /// Sender label on payment received screen + /// + /// In en, this message translates to: + /// **'From:'** + String get posQrFrom; + + /// Status while waiting for payment + /// + /// In en, this message translates to: + /// **'Waiting for payment'** + String get posQrWaitingForPayment; + + /// Network error title on POS QR screen + /// + /// In en, this message translates to: + /// **'Network Error'** + String get posQrNetworkError; + + /// Retry button on POS QR screen + /// + /// In en, this message translates to: + /// **'Try Again'** + String get posQrTryAgain; + + /// Paid at timestamp on payment received screen + /// + /// In en, this message translates to: + /// **'At {time}'** + String posQrPaidAt(String time); + + /// App bar title on settings hub + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// Wallet row title on settings hub + /// + /// In en, this message translates to: + /// **'Wallet'** + String get settingsWalletTitle; + + /// Wallet row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Recovery Phrase, Reset Wallet'** + String get settingsWalletSubtitle; + + /// Preferences row title on settings hub + /// + /// In en, this message translates to: + /// **'Preferences'** + String get settingsPreferencesTitle; + + /// Preferences row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Language, currency, POS mode, notifications'** + String get settingsPreferencesSubtitle; + + /// Mining rewards row title on settings hub + /// + /// In en, this message translates to: + /// **'Mining Rewards'** + String get settingsMiningRewards; + + /// Mining rewards row subtitle when data loaded + /// + /// In en, this message translates to: + /// **'{count} blocks mined'** + String settingsMiningRewardsSubtitle(int count); + + /// Mining rewards row subtitle on error + /// + /// In en, this message translates to: + /// **'Error getting mining rewards'** + String get settingsMiningRewardsError; + + /// Account type row title on settings hub + /// + /// In en, this message translates to: + /// **'Account Type'** + String get settingsAccountTypeTitle; + + /// Account type row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Advanced Account Features'** + String get settingsAccountTypeSubtitle; + + /// Help row title on settings hub + /// + /// In en, this message translates to: + /// **'Help & Support'** + String get settingsHelpTitle; + + /// Help row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'FAQs, Contact the team'** + String get settingsHelpSubtitle; + + /// About row title on settings hub + /// + /// In en, this message translates to: + /// **'About Quantus'** + String get settingsAboutTitle; + + /// About row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Version {version} ({build})'** + String settingsAboutHubSubtitle(String version, String build); + + /// Recovery phrase row on wallet settings + /// + /// In en, this message translates to: + /// **'Recovery Phrase'** + String get settingsWalletRecoveryPhrase; + + /// Recovery phrase row subtitle + /// + /// In en, this message translates to: + /// **'View your 24-word Backup Password'** + String get settingsWalletRecoveryPhraseSubtitle; + + /// Reset wallet row title + /// + /// In en, this message translates to: + /// **'Reset Wallet'** + String get settingsWalletReset; + + /// Reset wallet row subtitle + /// + /// In en, this message translates to: + /// **'Removes all data from this device'** + String get settingsWalletResetSubtitle; + + /// Error when no wallets exist + /// + /// In en, this message translates to: + /// **'No wallets found'** + String get settingsWalletNoWalletsFound; + + /// Error when wallet list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load wallets'** + String get settingsWalletFailedToLoad; + + /// App bar on select wallet screen + /// + /// In en, this message translates to: + /// **'Select Wallet'** + String get settingsSelectWalletTitle; + + /// Empty state on select wallet screen + /// + /// In en, this message translates to: + /// **'No wallets found'** + String get settingsSelectWalletNoWallets; + + /// Wallet list item label on select wallet screen + /// + /// In en, this message translates to: + /// **'Wallet {number}'** + String settingsSelectWalletItem(int number); + + /// Biometric prompt when viewing recovery phrase + /// + /// In en, this message translates to: + /// **'Authenticate to see recovery phrase'** + String get settingsRecoveryConfirmAuthReason; + + /// Toaster when auth fails for recovery phrase + /// + /// In en, this message translates to: + /// **'Authentication required to see recovery phrase'** + String get settingsRecoveryConfirmAuthRequired; + + /// App bar on recovery phrase screen + /// + /// In en, this message translates to: + /// **'Recovery Phrase'** + String get settingsRecoveryPhraseTitle; + + /// Done button on recovery phrase screen + /// + /// In en, this message translates to: + /// **'Done'** + String get settingsRecoveryPhraseDone; + + /// App bar on reset wallet caution screen + /// + /// In en, this message translates to: + /// **'Reset Wallet'** + String get settingsResetTitle; + + /// Biometric prompt when resetting wallet + /// + /// In en, this message translates to: + /// **'Authenticate to reset wallet'** + String get settingsResetAuthReason; + + /// Toaster when wallet reset fails + /// + /// In en, this message translates to: + /// **'Failed to reset wallet: {error}'** + String settingsResetFailed(String error); + + /// Toaster when auth fails for wallet reset + /// + /// In en, this message translates to: + /// **'Authentication required to reset wallet'** + String get settingsResetAuthRequired; + + /// Headline on wallet reset caution screen + /// + /// In en, this message translates to: + /// **'This will erase\nyour wallet'** + String get settingsResetCautionHeadline; + + /// First bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'All wallet data will be permanently removed from this device'** + String get settingsResetCautionBullet1; + + /// Second bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'Your funds stay on the blockchain but only your recovery phrase can restore access'** + String get settingsResetCautionBullet2; + + /// Third bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'Without it, your funds are gone forever'** + String get settingsResetCautionBullet3; + + /// Checkbox label on wallet reset caution + /// + /// In en, this message translates to: + /// **'I\'ve backed up my recovery phrase'** + String get settingsResetCautionCheckbox; + + /// Currency row on preferences screen + /// + /// In en, this message translates to: + /// **'Currency'** + String get settingsPreferencesCurrency; + + /// Currency row subtitle + /// + /// In en, this message translates to: + /// **'Fiat display preference'** + String get settingsPreferencesCurrencySubtitle; + + /// Language row on preferences screen + /// + /// In en, this message translates to: + /// **'Language'** + String get settingsPreferencesLanguage; + + /// Language row subtitle + /// + /// In en, this message translates to: + /// **'App display language'** + String get settingsPreferencesLanguageSubtitle; + + /// POS mode row on preferences + /// + /// In en, this message translates to: + /// **'POS Mode'** + String get settingsPreferencesPosMode; + + /// POS mode row subtitle + /// + /// In en, this message translates to: + /// **'Point of sale features'** + String get settingsPreferencesPosModeSubtitle; + + /// Notifications row on preferences + /// + /// In en, this message translates to: + /// **'Notifications'** + String get settingsPreferencesNotifications; + + /// Notifications row subtitle + /// + /// In en, this message translates to: + /// **'Transaction and wallet alerts'** + String get settingsPreferencesNotificationsSubtitle; + + /// App bar on currency picker + /// + /// In en, this message translates to: + /// **'Currency'** + String get settingsCurrencyTitle; + + /// Search field hint on currency picker + /// + /// In en, this message translates to: + /// **'Search'** + String get settingsCurrencySearchHint; + + /// Empty state when search has no results + /// + /// In en, this message translates to: + /// **'No currencies match your search'** + String get settingsCurrencyNoMatch; + + /// Error when currency selection fails + /// + /// In en, this message translates to: + /// **'Error selecting currency: {error}'** + String settingsCurrencyError(String error); + + /// App bar on language picker + /// + /// In en, this message translates to: + /// **'Language'** + String get settingsLanguageTitle; + + /// Search field hint on language picker + /// + /// In en, this message translates to: + /// **'Search'** + String get settingsLanguageSearchHint; + + /// Empty state when language search has no results + /// + /// In en, this message translates to: + /// **'No languages match your search'** + String get settingsLanguageNoMatch; + + /// Error when language selection fails + /// + /// In en, this message translates to: + /// **'Error selecting language: {error}'** + String settingsLanguageError(String error); + + /// App bar on mining rewards screen + /// + /// In en, this message translates to: + /// **'Mining Rewards'** + String get settingsMiningTitle; + + /// Redeem button on mining rewards + /// + /// In en, this message translates to: + /// **'Redeem'** + String get settingsMiningRedeem; + + /// Active mining status label + /// + /// In en, this message translates to: + /// **'Mining'** + String get settingsMiningStatusMining; + + /// Pending mining status label + /// + /// In en, this message translates to: + /// **'Pending'** + String get settingsMiningStatusPending; + + /// Blocks mined stat label + /// + /// In en, this message translates to: + /// **'BLOCKS MINED'** + String get settingsMiningBlocksMined; + + /// Subtitle under blocks mined count + /// + /// In en, this message translates to: + /// **'blocks across all testnets'** + String get settingsMiningBlocksAcrossTestnets; + + /// Testnet blocks stat label + /// + /// In en, this message translates to: + /// **'TESTNET BLOCKS'** + String get settingsMiningStatTestnetBlocks; + + /// Testnet rewards stat label + /// + /// In en, this message translates to: + /// **'TESTNET REWARDS'** + String get settingsMiningStatTestnetRewards; + + /// Redeemed rewards stat label + /// + /// In en, this message translates to: + /// **'REDEEMED'** + String get settingsMiningStatRedeemed; + + /// Redeemable rewards stat label + /// + /// In en, this message translates to: + /// **'REDEEMABLE'** + String get settingsMiningStatRedeemable; + + /// QUAN earned stat label + /// + /// In en, this message translates to: + /// **'QUAN EARNED'** + String get settingsMiningQuanEarned; + + /// Link to mining telemetry + /// + /// In en, this message translates to: + /// **'View Telemetry ↗'** + String get settingsMiningViewTelemetry; + + /// Empty state title on mining rewards + /// + /// In en, this message translates to: + /// **'No mining data yet'** + String get settingsMiningNoDataTitle; + + /// Empty state body on mining rewards + /// + /// In en, this message translates to: + /// **'Set up a Quantus mining node to start earning rewards.'** + String get settingsMiningNoDataBody; + + /// Link to mining setup guide + /// + /// In en, this message translates to: + /// **'Mining Setup Guide ↗'** + String get settingsMiningSetupGuide; + + /// Error title on mining rewards screen + /// + /// In en, this message translates to: + /// **'Failed to load mining rewards'** + String get settingsMiningLoadError; + + /// Error subtitle when connection fails + /// + /// In en, this message translates to: + /// **'Please check your connection'** + String get settingsMiningCheckConnection; + + /// Blocks label on testnet row + /// + /// In en, this message translates to: + /// **'blocks'** + String get settingsMiningTestnetBlocks; + + /// Dirac testnet active since date + /// + /// In en, this message translates to: + /// **'Nov 2025'** + String get settingsMiningDiracSince; + + /// Schrödinger testnet active since date + /// + /// In en, this message translates to: + /// **'Oct 2025'** + String get settingsMiningSchrodingerSince; + + /// Resonance testnet active since date + /// + /// In en, this message translates to: + /// **'Jul 2025'** + String get settingsMiningResonanceSince; + + /// App bar on testnet rewards screen + /// + /// In en, this message translates to: + /// **'Testnet Rewards'** + String get settingsTestnetTitle; + + /// Error title on testnet rewards + /// + /// In en, this message translates to: + /// **'Failed to load testnet rewards'** + String get settingsTestnetLoadError; + + /// Total blocks headline on testnet rewards + /// + /// In en, this message translates to: + /// **'{count} blocks'** + String settingsTestnetTotalBlocks(int count); + + /// Description under total blocks + /// + /// In en, this message translates to: + /// **'Total blocks mined across all testnets'** + String get settingsTestnetTotalDescription; + + /// Breakdown section header + /// + /// In en, this message translates to: + /// **'Breakdown'** + String get settingsTestnetBreakdown; + + /// Blocks count in testnet breakdown row + /// + /// In en, this message translates to: + /// **'{count} blocks'** + String settingsTestnetRowBlocks(int count); + + /// App bar on help and support screen + /// + /// In en, this message translates to: + /// **'Help & Support'** + String get settingsHelpScreenTitle; + + /// Email support row title + /// + /// In en, this message translates to: + /// **'Email Support'** + String get settingsHelpEmail; + + /// Telegram row title + /// + /// In en, this message translates to: + /// **'Telegram'** + String get settingsHelpTelegram; + + /// App bar on about screen + /// + /// In en, this message translates to: + /// **'About'** + String get settingsAboutScreenTitle; + + /// Intro paragraph on about screen + /// + /// In en, this message translates to: + /// **'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'** + String get settingsAboutIntro; + + /// Terms of service link title + /// + /// In en, this message translates to: + /// **'Terms of Service'** + String get settingsAboutTerms; + + /// Terms of service link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com/terms/'** + String get settingsAboutTermsSubtitle; + + /// Privacy policy link title + /// + /// In en, this message translates to: + /// **'Privacy policy'** + String get settingsAboutPrivacy; + + /// Privacy policy link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com/privacy-policy/'** + String get settingsAboutPrivacySubtitle; + + /// Website link title + /// + /// In en, this message translates to: + /// **'Visit Website'** + String get settingsAboutWebsite; + + /// Website link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com'** + String get settingsAboutWebsiteSubtitle; + + /// Version label on about screen + /// + /// In en, this message translates to: + /// **'Version {version} ({build})'** + String settingsAboutVersion(String version, String build); + + /// App bar on account type settings + /// + /// In en, this message translates to: + /// **'Account Type'** + String get settingsAccountTypeScreenTitle; + + /// Intro on account type settings + /// + /// In en, this message translates to: + /// **'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'** + String get settingsAccountTypeIntro; + + /// Reversible transactions feature title + /// + /// In en, this message translates to: + /// **'Reversible Transactions'** + String get settingsAccountTypeReversibleTitle; + + /// Reversible transactions feature subtitle + /// + /// In en, this message translates to: + /// **'Reverse your sends within a time window'** + String get settingsAccountTypeReversibleSubtitle; + + /// High security account feature title + /// + /// In en, this message translates to: + /// **'High Security Account'** + String get settingsAccountTypeHighSecurityTitle; + + /// High security account feature subtitle + /// + /// In en, this message translates to: + /// **'Guardian approval required'** + String get settingsAccountTypeHighSecuritySubtitle; + + /// Multi-signature feature title + /// + /// In en, this message translates to: + /// **'Multi-Signature'** + String get settingsAccountTypeMultiSigTitle; + + /// Multi-signature feature subtitle + /// + /// In en, this message translates to: + /// **'Multiple approvals required'** + String get settingsAccountTypeMultiSigSubtitle; + + /// Hardware wallet feature title + /// + /// In en, this message translates to: + /// **'Hardware Wallet'** + String get settingsAccountTypeHardwareTitle; + + /// Hardware wallet feature subtitle + /// + /// In en, this message translates to: + /// **'Pair a hardware device'** + String get settingsAccountTypeHardwareSubtitle; + + /// Coming soon badge on account type features + /// + /// In en, this message translates to: + /// **'Coming Soon'** + String get settingsAccountTypeComingSoon; + + /// App bar title on swap screens + /// + /// In en, this message translates to: + /// **'Swap'** + String get swapTitle; + + /// From token section label on swap screen + /// + /// In en, this message translates to: + /// **'From'** + String get swapFrom; + + /// To token section label on swap screen + /// + /// In en, this message translates to: + /// **'To'** + String get swapTo; + + /// Refund address field label on swap screen + /// + /// In en, this message translates to: + /// **'Refund Address'** + String get swapRefundAddress; + + /// Refund address field hint + /// + /// In en, this message translates to: + /// **'{network} Address'** + String swapRefundAddressHint(String network); + + /// Slippage tolerance label on swap screen + /// + /// In en, this message translates to: + /// **'Slippage Tolerance'** + String get swapSlippageTolerance; + + /// Exchange rate label on swap screen + /// + /// In en, this message translates to: + /// **'Rate'** + String get swapRate; + + /// Get quote button on swap screen + /// + /// In en, this message translates to: + /// **'Get a Quote'** + String get swapGetQuote; + + /// Exchange rate display + /// + /// In en, this message translates to: + /// **'1 QUAN = {amount} {symbol}'** + String swapRateLabel(String amount, String symbol); + + /// Exchange rate when amount is zero + /// + /// In en, this message translates to: + /// **'1 QUAN = 0 {symbol}'** + String swapRateZero(String symbol); + + /// Title on token picker sheet + /// + /// In en, this message translates to: + /// **'Select Token'** + String get swapTokenPickerTitle; + + /// Error when token list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load tokens'** + String get swapTokenPickerLoadError; + + /// Title on review quote sheet + /// + /// In en, this message translates to: + /// **'Review Quote'** + String get swapReviewTitle; + + /// Total fees row on review quote sheet + /// + /// In en, this message translates to: + /// **'Total fees'** + String get swapReviewTotalFees; + + /// Total amount row on review quote sheet + /// + /// In en, this message translates to: + /// **'Total Amount'** + String get swapReviewTotalAmount; + + /// Slippage warning on review quote sheet + /// + /// In en, this message translates to: + /// **'You could receive up to \${amount} less based on the {percent}% slippage you set'** + String swapReviewSlippageWarning(String amount, String percent); + + /// Confirm button on review quote sheet + /// + /// In en, this message translates to: + /// **'Confirm'** + String get swapReviewConfirm; + + /// Deposit amount label on deposit screen + /// + /// In en, this message translates to: + /// **'Deposit Amount'** + String get swapDepositAmount; + + /// Toast when deposit amount is copied + /// + /// In en, this message translates to: + /// **'Deposit amount copied to clipboard'** + String get swapDepositAmountCopied; + + /// Demo warning on deposit screen + /// + /// In en, this message translates to: + /// **'For demo purposes only - do not send funds!'** + String get swapDepositDemoWarning; + + /// Share QR button on deposit screen + /// + /// In en, this message translates to: + /// **'Share QR'** + String get swapDepositShareQr; + + /// Share text for deposit details + /// + /// In en, this message translates to: + /// **'Network: {network}\nToken: {token}\nAddress: {address}'** + String swapDepositShareContent(String network, String token, String address); + + /// Deposit wallet notice on deposit screen + /// + /// In en, this message translates to: + /// **'Use your {symbol} or {network} wallet to deposit funds. Depositing other assets may result in loss of funds.'** + String swapDepositNotice(String symbol, String network); + + /// Title while swap is processing + /// + /// In en, this message translates to: + /// **'Processing Swap'** + String get swapDepositProcessingTitle; + + /// Body while swap is processing + /// + /// In en, this message translates to: + /// **'This may take a few minutes...'** + String get swapDepositProcessingBody; + + /// Title when swap is complete + /// + /// In en, this message translates to: + /// **'Swap Complete'** + String get swapDepositCompleteTitle; + + /// Body when swap is complete + /// + /// In en, this message translates to: + /// **'Your swap for {amount} QUAN is complete.'** + String swapDepositCompleteBody(String amount); + + /// Testnet demo banner on deposit screen + /// + /// In en, this message translates to: + /// **'DEMO ONLY - WE ARE STILL ON TESTNET'** + String get swapDepositTestnetBanner; + + /// Button to confirm funds sent + /// + /// In en, this message translates to: + /// **'I\'ve sent the funds'** + String get swapDepositSentFunds; + + /// Done button after swap completes + /// + /// In en, this message translates to: + /// **'Done'** + String get swapDepositDone; + + /// Title on refund address picker sheet + /// + /// In en, this message translates to: + /// **'Refund Addresses'** + String get swapRefundPickerTitle; + + /// Empty state on refund address picker + /// + /// In en, this message translates to: + /// **'No recent refund addresses'** + String get swapRefundPickerEmpty; + + /// Text for app bar or button label on QR scanner component + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get componentQrScannerTitle; + + /// Snackbar when gallery image has no QR code + /// + /// In en, this message translates to: + /// **'No QR code found in image'** + String get componentQrScannerNoCode; + + /// Share button label on account screens + /// + /// In en, this message translates to: + /// **'Share'** + String get componentShare; + + /// Address field label on address details card + /// + /// In en, this message translates to: + /// **'ADDRESS'** + String get componentAddressLabel; + + /// Checkphrase field label on address details card + /// + /// In en, this message translates to: + /// **'CHECKPHRASE'** + String get componentCheckphraseLabel; + + /// Toast when checkphrase is copied + /// + /// In en, this message translates to: + /// **'Checkphrase copied'** + String get componentCheckphraseCopied; + + /// Hint text on account name field + /// + /// In en, this message translates to: + /// **'Enter a name for your account'** + String get componentNameFieldHint; + + /// Text for generic loading state + /// + /// In en, this message translates to: + /// **'Loading...'** + String get commonLoading; + + /// Formatted balance with token symbol + /// + /// In en, this message translates to: + /// **'{balance} {symbol}'** + String commonAmountBalance(String balance, String symbol); + + /// Continue button on various screens + /// + /// In en, this message translates to: + /// **'Continue'** + String get commonContinue; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en', 'id'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'id': + return AppLocalizationsId(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart new file mode 100644 index 00000000..0d497542 --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -0,0 +1,1056 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get walletInitErrorTitle => 'Wallet Error'; + + @override + String get walletInitErrorMessage => 'Unable to find secret phrase. Please restore your wallet.'; + + @override + String get walletInitErrorButtonLabel => 'OK'; + + @override + String get authUseDeviceBiometricsToUnlock => 'Use device biometrics to unlock'; + + @override + String get authAuthenticating => 'Authenticating...'; + + @override + String get authUnlockWallet => 'Unlock Wallet'; + + @override + String get authAuthorizationRequired => 'Authorization \n Required'; + + @override + String get welcomeTagline => 'Quantum Secure Encrypted Money'; + + @override + String get welcomeCreateNewWallet => 'Create New Wallet'; + + @override + String get welcomeImportWallet => 'Import Wallet'; + + @override + String get createWalletAppBarTitle => 'Create Wallet'; + + @override + String get createWalletCautionHeadline => 'Keep your Recovery Phrase Secret'; + + @override + String get createWalletCautionBullet1 => 'If you lose this device, your recovery phrase is the only way back'; + + @override + String get createWalletCautionBullet2 => + 'Anyone who gets hold of it has complete control over your funds, permanently'; + + @override + String get createWalletCautionBullet3 => 'Write it down and keep it somewhere safe. Do not save it digitally'; + + @override + String get createWalletCautionCheckboxLabel => + 'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'; + + @override + String get createWalletRecoveryPhraseNext => 'Next'; + + @override + String createWalletRecoveryPhraseFailedGenerate(String error) { + return 'Failed to generate: $error'; + } + + @override + String createWalletRecoveryPhraseSaveError(String error) { + return 'Error saving wallet: $error'; + } + + @override + String get recoveryPhraseBodyInstructions => + 'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.'; + + @override + String get recoveryPhraseBodyCopy => 'Copy'; + + @override + String get recoveryPhraseBodyCopiedMessage => 'Recovery phrase copied to clipboard'; + + @override + String get accountReadyAccountCreated => 'Account Created'; + + @override + String get accountReadyWalletCreated => 'Wallet Created'; + + @override + String get accountReadyWalletImported => 'Wallet Imported'; + + @override + String get accountReadyDone => 'Done'; + + @override + String get importWalletAppBarTitle => 'Import Wallet'; + + @override + String get importWalletDescription => 'Restore an existing wallet with your 12 or 24 words recovery phrase'; + + @override + String get importWalletHint => 'Type in or paste your recovery phrase. Separate words with spaces.'; + + @override + String get importWalletButton => 'Import'; + + @override + String get importWalletValidationError => 'Recovery phrase must be 12 or 24 words'; + + @override + String homeError(String error) { + return 'Error: $error'; + } + + @override + String get homeNoActiveAccount => 'No active account'; + + @override + String get homeCharge => 'Charge'; + + @override + String get homeGetTestnetTokens => 'Get Testnet Tokens ↗'; + + @override + String get homeErrorLoadingBalance => 'Error loading balance'; + + @override + String get homeReceive => 'Receive'; + + @override + String get homeSend => 'Send'; + + @override + String get homeSwap => 'Swap'; + + @override + String get homeActivityTitle => 'Activity'; + + @override + String get homeActivityViewAll => 'View All'; + + @override + String get homeActivityErrorLoading => 'Error loading transactions'; + + @override + String get homeActivityRetry => 'Retry'; + + @override + String get homeActivityEmptyTitle => 'No Transactions Yet'; + + @override + String get homeActivityEmptyMessage => 'Your activity will appear here once you send or receive QUAN.'; + + @override + String get accountsSheetTitle => 'Accounts'; + + @override + String get accountsSheetFailedLoadAccounts => 'Failed to load accounts.'; + + @override + String get accountsSheetFailedLoadActiveAccount => 'Failed to load active account.'; + + @override + String get accountsSheetNoAccountsFound => 'No accounts found.'; + + @override + String get accountsSheetAddAccount => 'Add Account'; + + @override + String get accountsSheetBalanceUnavailable => 'Balance unavailable'; + + @override + String accountsSheetBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get addAccountMenuTitle => 'Add Account'; + + @override + String get addAccountMenuCreateTitle => 'Create New Account'; + + @override + String get addAccountMenuCreateSubtitle => 'Generate a fresh wallet address'; + + @override + String get addAccountMenuImportTitle => 'Import Wallet'; + + @override + String get addAccountMenuImportSubtitle => 'Use a recovery phrase to import'; + + @override + String get createAccountAppBarTitle => 'Account Name'; + + @override + String get createAccountSubtitle => 'Give this account a name you\'ll recognize. You can change it anytime.'; + + @override + String get createAccountButton => 'Create'; + + @override + String get createAccountErrorCouldNotAdd => 'Could not add account.'; + + @override + String createAccountDefaultName(int number) { + return 'Account $number'; + } + + @override + String get editAccountAppBarTitle => 'Account Name'; + + @override + String get editAccountDone => 'Done'; + + @override + String get editAccountNameEmpty => 'Account name can\'t be empty'; + + @override + String get editAccountRenameFailed => 'Failed to rename account.'; + + @override + String get accountMenuTitle => 'Accounts'; + + @override + String get accountMenuAccountName => 'Account Name'; + + @override + String get accountMenuAddressDetails => 'Address Details'; + + @override + String get accountMenuShowRecoveryPhrase => 'Show Recovery Phrase'; + + @override + String get accountMenuNotFound => 'Account not found'; + + @override + String get accountDetailsTitle => 'Address Details'; + + @override + String get addHardwareAccountAddWallet => 'Add Hardware Wallet'; + + @override + String get addHardwareAccountAddAccount => 'Add Hardware Account'; + + @override + String get addHardwareAccountNameLabel => 'NAME'; + + @override + String get addHardwareAccountNameHintWallet => 'Hardware Wallet'; + + @override + String get addHardwareAccountNameHintAccount => 'Account'; + + @override + String get addHardwareAccountAddressLabel => 'ADDRESS'; + + @override + String get addHardwareAccountAddressHint => 'SS58 address'; + + @override + String get addHardwareAccountDebugFill => 'Debug Fill'; + + @override + String get addHardwareAccountNameRequired => 'Name is required'; + + @override + String get addHardwareAccountInvalidAddress => 'Invalid address'; + + @override + String get sendTitle => 'Send'; + + @override + String get sendPayTitle => 'Pay'; + + @override + String get sendEnterAddress => 'Enter Address'; + + @override + String get sendSelectRecipientSendTo => 'Send To'; + + @override + String sendSelectRecipientSearchHint(String symbol) { + return 'Search $symbol Address'; + } + + @override + String get sendSelectRecipientScanTitle => 'Scan QR code'; + + @override + String sendSelectRecipientScanSubtitle(String symbol) { + return 'Tap to scan a $symbol Address'; + } + + @override + String get sendSelectRecipientRecents => 'Recents'; + + @override + String get sendSelectRecipientContinue => 'Continue'; + + @override + String get sendInputAmountSendTo => 'SEND TO'; + + @override + String get sendInputAmountAvailableBalance => 'Available Balance:'; + + @override + String get sendInputAmountNetworkFee => 'Network Fee:'; + + @override + String get sendInputAmountMax => 'Max'; + + @override + String get sendInputAmountInvalidAmount => 'Please enter a valid amount'; + + @override + String get sendInputAmountChecksumRequired => 'Recipient checksum is required'; + + @override + String get sendReviewSending => 'SENDING'; + + @override + String get sendReviewTo => 'TO'; + + @override + String get sendReviewAmount => 'AMOUNT'; + + @override + String get sendReviewNetworkFee => 'NETWORK FEE'; + + @override + String get sendReviewYouPay => 'YOU PAY'; + + @override + String get sendReviewConfirm => 'Confirm'; + + @override + String get sendReviewAuthReason => 'Authenticate to confirm transaction'; + + @override + String get sendReviewAuthRequired => 'Authentication required to send'; + + @override + String get sendReviewSubmitFailed => 'Failed submitting transaction'; + + @override + String sendTxSubmittedHeadlinePaid(String amount, String symbol) { + return '$amount $symbol paid'; + } + + @override + String sendTxSubmittedHeadlineSent(String amount, String symbol) { + return '$amount $symbol sent'; + } + + @override + String get sendTxSubmittedOnItsWay => 'On its way'; + + @override + String get sendTxSubmittedToLabel => 'To'; + + @override + String get sendTxSubmittedDone => 'Done'; + + @override + String get sendLogicCantSelfTransfer => 'Can\'t Self Transfer'; + + @override + String get sendLogicEnterAmount => 'Enter Amount'; + + @override + String get sendLogicInvalidAmount => 'Invalid Amount'; + + @override + String get sendLogicBelowExistentialDeposit => 'Below Existential Deposit'; + + @override + String get sendLogicInsufficientBalance => 'Insufficient Balance'; + + @override + String get sendLogicReviewSend => 'Review Send'; + + @override + String get activityTitle => 'Activity'; + + @override + String activityError(String error) { + return 'Error: $error'; + } + + @override + String get activityNoAccount => 'No account'; + + @override + String get activityEmpty => 'No transactions yet'; + + @override + String get activityFilterAll => 'All'; + + @override + String get activityFilterSend => 'Send'; + + @override + String get activityFilterReceive => 'Receive'; + + @override + String get activityDateToday => 'Today'; + + @override + String get activityDateYesterday => 'Yesterday'; + + @override + String get activityTxSending => 'Sending'; + + @override + String get activityTxReceiving => 'Receiving'; + + @override + String get activityTxPending => 'Pending'; + + @override + String get activityTxSent => 'Sent'; + + @override + String get activityTxReceived => 'Received'; + + @override + String get activityTxTo => 'To'; + + @override + String get activityTxFrom => 'From'; + + @override + String get activityTxTimeNow => 'now'; + + @override + String activityTxTimeMinutesAgo(int minutes) { + return '${minutes}m ago'; + } + + @override + String activityTxTimeHoursAgo(int hours) { + return '${hours}h ago'; + } + + @override + String activityTxTimeDaysAgo(int days) { + return '${days}d ago'; + } + + @override + String activityTxTimeRemaining(String days, String hours, String minutes) { + return '${days}d:${hours}h:${minutes}m'; + } + + @override + String get activityDetailTitleSending => 'Sending'; + + @override + String get activityDetailTitleScheduled => 'Scheduled'; + + @override + String get activityDetailTitleReceiving => 'Receiving'; + + @override + String get activityDetailTitleSent => 'Sent'; + + @override + String get activityDetailTitleReceived => 'Received'; + + @override + String get activityDetailStatusInProcess => 'In Process'; + + @override + String get activityDetailStatusScheduled => 'Scheduled'; + + @override + String get activityDetailStatusCompleted => 'Completed'; + + @override + String get activityDetailStatus => 'STATUS'; + + @override + String get activityDetailTo => 'TO'; + + @override + String get activityDetailFrom => 'FROM'; + + @override + String get activityDetailDate => 'DATE'; + + @override + String get activityDetailNetworkFee => 'NETWORK FEE'; + + @override + String get activityDetailTxHash => 'TX HASH'; + + @override + String get activityDetailViewExplorer => 'View in Explorer ↗'; + + @override + String get receiveTitle => 'Receive'; + + @override + String get receiveTabQrCode => 'QR Code'; + + @override + String get receiveTabAddress => 'Address'; + + @override + String get receiveCopy => 'Copy'; + + @override + String receiveErrorLoadingAccount(String error) { + return 'Error loading account data: $error'; + } + + @override + String receiveClipboardContent(String accountId, String checksum) { + return 'Account Id:\n$accountId\n\nCheckphrase:\n$checksum'; + } + + @override + String get receiveCopiedMessage => 'Account details copied to clipboard'; + + @override + String get posAmountTitle => 'New Charge'; + + @override + String posAmountCharge(String amount) { + return 'Charge $amount'; + } + + @override + String get posAmountEnterAmount => 'Enter Amount'; + + @override + String get posQrTitleScanToPay => 'Scan to Pay'; + + @override + String get posQrTitlePaymentReceived => 'Payment Received'; + + @override + String posQrError(String error) { + return 'Error: $error'; + } + + @override + String get posQrNoActiveAccount => 'No active account'; + + @override + String get posQrInvalidAmount => 'Invalid amount. Tap to retry.'; + + @override + String get posQrConnectionLost => 'Connection lost. Tap to retry.'; + + @override + String get posQrTimedOut => 'Timed out. Tap to retry.'; + + @override + String get posQrNewCharge => 'New Charge'; + + @override + String get posQrDone => 'Done'; + + @override + String posQrAmountReceived(String amount) { + return '$amount received'; + } + + @override + String get posQrFrom => 'From:'; + + @override + String get posQrWaitingForPayment => 'Waiting for payment'; + + @override + String get posQrNetworkError => 'Network Error'; + + @override + String get posQrTryAgain => 'Try Again'; + + @override + String posQrPaidAt(String time) { + return 'At $time'; + } + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsWalletTitle => 'Wallet'; + + @override + String get settingsWalletSubtitle => 'Recovery Phrase, Reset Wallet'; + + @override + String get settingsPreferencesTitle => 'Preferences'; + + @override + String get settingsPreferencesSubtitle => 'Language, currency, POS mode, notifications'; + + @override + String get settingsMiningRewards => 'Mining Rewards'; + + @override + String settingsMiningRewardsSubtitle(int count) { + return '$count blocks mined'; + } + + @override + String get settingsMiningRewardsError => 'Error getting mining rewards'; + + @override + String get settingsAccountTypeTitle => 'Account Type'; + + @override + String get settingsAccountTypeSubtitle => 'Advanced Account Features'; + + @override + String get settingsHelpTitle => 'Help & Support'; + + @override + String get settingsHelpSubtitle => 'FAQs, Contact the team'; + + @override + String get settingsAboutTitle => 'About Quantus'; + + @override + String settingsAboutHubSubtitle(String version, String build) { + return 'Version $version ($build)'; + } + + @override + String get settingsWalletRecoveryPhrase => 'Recovery Phrase'; + + @override + String get settingsWalletRecoveryPhraseSubtitle => 'View your 24-word Backup Password'; + + @override + String get settingsWalletReset => 'Reset Wallet'; + + @override + String get settingsWalletResetSubtitle => 'Removes all data from this device'; + + @override + String get settingsWalletNoWalletsFound => 'No wallets found'; + + @override + String get settingsWalletFailedToLoad => 'Failed to load wallets'; + + @override + String get settingsSelectWalletTitle => 'Select Wallet'; + + @override + String get settingsSelectWalletNoWallets => 'No wallets found'; + + @override + String settingsSelectWalletItem(int number) { + return 'Wallet $number'; + } + + @override + String get settingsRecoveryConfirmAuthReason => 'Authenticate to see recovery phrase'; + + @override + String get settingsRecoveryConfirmAuthRequired => 'Authentication required to see recovery phrase'; + + @override + String get settingsRecoveryPhraseTitle => 'Recovery Phrase'; + + @override + String get settingsRecoveryPhraseDone => 'Done'; + + @override + String get settingsResetTitle => 'Reset Wallet'; + + @override + String get settingsResetAuthReason => 'Authenticate to reset wallet'; + + @override + String settingsResetFailed(String error) { + return 'Failed to reset wallet: $error'; + } + + @override + String get settingsResetAuthRequired => 'Authentication required to reset wallet'; + + @override + String get settingsResetCautionHeadline => 'This will erase\nyour wallet'; + + @override + String get settingsResetCautionBullet1 => 'All wallet data will be permanently removed from this device'; + + @override + String get settingsResetCautionBullet2 => + 'Your funds stay on the blockchain but only your recovery phrase can restore access'; + + @override + String get settingsResetCautionBullet3 => 'Without it, your funds are gone forever'; + + @override + String get settingsResetCautionCheckbox => 'I\'ve backed up my recovery phrase'; + + @override + String get settingsPreferencesCurrency => 'Currency'; + + @override + String get settingsPreferencesCurrencySubtitle => 'Fiat display preference'; + + @override + String get settingsPreferencesLanguage => 'Language'; + + @override + String get settingsPreferencesLanguageSubtitle => 'App display language'; + + @override + String get settingsPreferencesPosMode => 'POS Mode'; + + @override + String get settingsPreferencesPosModeSubtitle => 'Point of sale features'; + + @override + String get settingsPreferencesNotifications => 'Notifications'; + + @override + String get settingsPreferencesNotificationsSubtitle => 'Transaction and wallet alerts'; + + @override + String get settingsCurrencyTitle => 'Currency'; + + @override + String get settingsCurrencySearchHint => 'Search'; + + @override + String get settingsCurrencyNoMatch => 'No currencies match your search'; + + @override + String settingsCurrencyError(String error) { + return 'Error selecting currency: $error'; + } + + @override + String get settingsLanguageTitle => 'Language'; + + @override + String get settingsLanguageSearchHint => 'Search'; + + @override + String get settingsLanguageNoMatch => 'No languages match your search'; + + @override + String settingsLanguageError(String error) { + return 'Error selecting language: $error'; + } + + @override + String get settingsMiningTitle => 'Mining Rewards'; + + @override + String get settingsMiningRedeem => 'Redeem'; + + @override + String get settingsMiningStatusMining => 'Mining'; + + @override + String get settingsMiningStatusPending => 'Pending'; + + @override + String get settingsMiningBlocksMined => 'BLOCKS MINED'; + + @override + String get settingsMiningBlocksAcrossTestnets => 'blocks across all testnets'; + + @override + String get settingsMiningStatTestnetBlocks => 'TESTNET BLOCKS'; + + @override + String get settingsMiningStatTestnetRewards => 'TESTNET REWARDS'; + + @override + String get settingsMiningStatRedeemed => 'REDEEMED'; + + @override + String get settingsMiningStatRedeemable => 'REDEEMABLE'; + + @override + String get settingsMiningQuanEarned => 'QUAN EARNED'; + + @override + String get settingsMiningViewTelemetry => 'View Telemetry ↗'; + + @override + String get settingsMiningNoDataTitle => 'No mining data yet'; + + @override + String get settingsMiningNoDataBody => 'Set up a Quantus mining node to start earning rewards.'; + + @override + String get settingsMiningSetupGuide => 'Mining Setup Guide ↗'; + + @override + String get settingsMiningLoadError => 'Failed to load mining rewards'; + + @override + String get settingsMiningCheckConnection => 'Please check your connection'; + + @override + String get settingsMiningTestnetBlocks => 'blocks'; + + @override + String get settingsMiningDiracSince => 'Nov 2025'; + + @override + String get settingsMiningSchrodingerSince => 'Oct 2025'; + + @override + String get settingsMiningResonanceSince => 'Jul 2025'; + + @override + String get settingsTestnetTitle => 'Testnet Rewards'; + + @override + String get settingsTestnetLoadError => 'Failed to load testnet rewards'; + + @override + String settingsTestnetTotalBlocks(int count) { + return '$count blocks'; + } + + @override + String get settingsTestnetTotalDescription => 'Total blocks mined across all testnets'; + + @override + String get settingsTestnetBreakdown => 'Breakdown'; + + @override + String settingsTestnetRowBlocks(int count) { + return '$count blocks'; + } + + @override + String get settingsHelpScreenTitle => 'Help & Support'; + + @override + String get settingsHelpEmail => 'Email Support'; + + @override + String get settingsHelpTelegram => 'Telegram'; + + @override + String get settingsAboutScreenTitle => 'About'; + + @override + String get settingsAboutIntro => + 'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'; + + @override + String get settingsAboutTerms => 'Terms of Service'; + + @override + String get settingsAboutTermsSubtitle => 'quantus.com/terms/'; + + @override + String get settingsAboutPrivacy => 'Privacy policy'; + + @override + String get settingsAboutPrivacySubtitle => 'quantus.com/privacy-policy/'; + + @override + String get settingsAboutWebsite => 'Visit Website'; + + @override + String get settingsAboutWebsiteSubtitle => 'quantus.com'; + + @override + String settingsAboutVersion(String version, String build) { + return 'Version $version ($build)'; + } + + @override + String get settingsAccountTypeScreenTitle => 'Account Type'; + + @override + String get settingsAccountTypeIntro => + 'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'; + + @override + String get settingsAccountTypeReversibleTitle => 'Reversible Transactions'; + + @override + String get settingsAccountTypeReversibleSubtitle => 'Reverse your sends within a time window'; + + @override + String get settingsAccountTypeHighSecurityTitle => 'High Security Account'; + + @override + String get settingsAccountTypeHighSecuritySubtitle => 'Guardian approval required'; + + @override + String get settingsAccountTypeMultiSigTitle => 'Multi-Signature'; + + @override + String get settingsAccountTypeMultiSigSubtitle => 'Multiple approvals required'; + + @override + String get settingsAccountTypeHardwareTitle => 'Hardware Wallet'; + + @override + String get settingsAccountTypeHardwareSubtitle => 'Pair a hardware device'; + + @override + String get settingsAccountTypeComingSoon => 'Coming Soon'; + + @override + String get swapTitle => 'Swap'; + + @override + String get swapFrom => 'From'; + + @override + String get swapTo => 'To'; + + @override + String get swapRefundAddress => 'Refund Address'; + + @override + String swapRefundAddressHint(String network) { + return '$network Address'; + } + + @override + String get swapSlippageTolerance => 'Slippage Tolerance'; + + @override + String get swapRate => 'Rate'; + + @override + String get swapGetQuote => 'Get a Quote'; + + @override + String swapRateLabel(String amount, String symbol) { + return '1 QUAN = $amount $symbol'; + } + + @override + String swapRateZero(String symbol) { + return '1 QUAN = 0 $symbol'; + } + + @override + String get swapTokenPickerTitle => 'Select Token'; + + @override + String get swapTokenPickerLoadError => 'Failed to load tokens'; + + @override + String get swapReviewTitle => 'Review Quote'; + + @override + String get swapReviewTotalFees => 'Total fees'; + + @override + String get swapReviewTotalAmount => 'Total Amount'; + + @override + String swapReviewSlippageWarning(String amount, String percent) { + return 'You could receive up to \$$amount less based on the $percent% slippage you set'; + } + + @override + String get swapReviewConfirm => 'Confirm'; + + @override + String get swapDepositAmount => 'Deposit Amount'; + + @override + String get swapDepositAmountCopied => 'Deposit amount copied to clipboard'; + + @override + String get swapDepositDemoWarning => 'For demo purposes only - do not send funds!'; + + @override + String get swapDepositShareQr => 'Share QR'; + + @override + String swapDepositShareContent(String network, String token, String address) { + return 'Network: $network\nToken: $token\nAddress: $address'; + } + + @override + String swapDepositNotice(String symbol, String network) { + return 'Use your $symbol or $network wallet to deposit funds. Depositing other assets may result in loss of funds.'; + } + + @override + String get swapDepositProcessingTitle => 'Processing Swap'; + + @override + String get swapDepositProcessingBody => 'This may take a few minutes...'; + + @override + String get swapDepositCompleteTitle => 'Swap Complete'; + + @override + String swapDepositCompleteBody(String amount) { + return 'Your swap for $amount QUAN is complete.'; + } + + @override + String get swapDepositTestnetBanner => 'DEMO ONLY - WE ARE STILL ON TESTNET'; + + @override + String get swapDepositSentFunds => 'I\'ve sent the funds'; + + @override + String get swapDepositDone => 'Done'; + + @override + String get swapRefundPickerTitle => 'Refund Addresses'; + + @override + String get swapRefundPickerEmpty => 'No recent refund addresses'; + + @override + String get componentQrScannerTitle => 'Scan QR Code'; + + @override + String get componentQrScannerNoCode => 'No QR code found in image'; + + @override + String get componentShare => 'Share'; + + @override + String get componentAddressLabel => 'ADDRESS'; + + @override + String get componentCheckphraseLabel => 'CHECKPHRASE'; + + @override + String get componentCheckphraseCopied => 'Checkphrase copied'; + + @override + String get componentNameFieldHint => 'Enter a name for your account'; + + @override + String get commonLoading => 'Loading...'; + + @override + String commonAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get commonContinue => 'Continue'; +} diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart new file mode 100644 index 00000000..f35b2555 --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -0,0 +1,1057 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Indonesian (`id`). +class AppLocalizationsId extends AppLocalizations { + AppLocalizationsId([String locale = 'id']) : super(locale); + + @override + String get walletInitErrorTitle => 'Wallet Bermasalah'; + + @override + String get walletInitErrorMessage => 'Gagal mencari secret phrase. Coba pulihkan wallet anda.'; + + @override + String get walletInitErrorButtonLabel => 'OK'; + + @override + String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk mengakses wallet'; + + @override + String get authAuthenticating => 'Mengotentikasi...'; + + @override + String get authUnlockWallet => 'Buka Wallet'; + + @override + String get authAuthorizationRequired => 'Otorisasi \n Diperlukan'; + + @override + String get welcomeTagline => 'Uang Terenkripsi Aman Kuantum'; + + @override + String get welcomeCreateNewWallet => 'Buat Wallet Baru'; + + @override + String get welcomeImportWallet => 'Impor Wallet'; + + @override + String get createWalletAppBarTitle => 'Buat Wallet'; + + @override + String get createWalletCautionHeadline => 'Jaga Kerahasiaan Recovery Phrase Anda'; + + @override + String get createWalletCautionBullet1 => + 'Jika Anda kehilangan perangkat ini, recovery phrase adalah satu-satunya cara kembali'; + + @override + String get createWalletCautionBullet2 => + 'Siapa pun yang mendapatkannya akan memiliki kendali penuh atas dana Anda, secara permanen'; + + @override + String get createWalletCautionBullet3 => 'Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital'; + + @override + String get createWalletCautionCheckboxLabel => + 'Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.'; + + @override + String get createWalletRecoveryPhraseNext => 'Berikutnya'; + + @override + String createWalletRecoveryPhraseFailedGenerate(String error) { + return 'Gagal membuat: $error'; + } + + @override + String createWalletRecoveryPhraseSaveError(String error) { + return 'Gagal menyimpan wallet: $error'; + } + + @override + String get recoveryPhraseBodyInstructions => + 'Tuliskan kata-kata ini secara berurutan dan simpan di tempat yang hanya Anda yang bisa akses. Jangan screenshot atau salin ke aplikasi catatan.'; + + @override + String get recoveryPhraseBodyCopy => 'Salin'; + + @override + String get recoveryPhraseBodyCopiedMessage => 'Recovery phrase disalin ke clipboard'; + + @override + String get accountReadyAccountCreated => 'Akun Dibuat'; + + @override + String get accountReadyWalletCreated => 'Wallet Dibuat'; + + @override + String get accountReadyWalletImported => 'Wallet Diimpor'; + + @override + String get accountReadyDone => 'Selesai'; + + @override + String get importWalletAppBarTitle => 'Impor Wallet'; + + @override + String get importWalletDescription => 'Pulihkan wallet yang ada dengan recovery phrase 12 atau 24 kata Anda'; + + @override + String get importWalletHint => 'Ketik atau tempel recovery phrase Anda. Pisahkan kata dengan spasi.'; + + @override + String get importWalletButton => 'Impor'; + + @override + String get importWalletValidationError => 'Recovery phrase harus 12 atau 24 kata'; + + @override + String homeError(String error) { + return 'Gagal: $error'; + } + + @override + String get homeNoActiveAccount => 'Tidak ada akun aktif'; + + @override + String get homeCharge => 'Tagih'; + + @override + String get homeGetTestnetTokens => 'Dapatkan Token Testnet ↗'; + + @override + String get homeErrorLoadingBalance => 'Gagal memuat saldo'; + + @override + String get homeReceive => 'Terima'; + + @override + String get homeSend => 'Kirim'; + + @override + String get homeSwap => 'Tukar'; + + @override + String get homeActivityTitle => 'Aktivitas'; + + @override + String get homeActivityViewAll => 'Lihat Semua'; + + @override + String get homeActivityErrorLoading => 'Gagal memuat transaksi'; + + @override + String get homeActivityRetry => 'Coba Lagi'; + + @override + String get homeActivityEmptyTitle => 'Belum Ada Transaksi'; + + @override + String get homeActivityEmptyMessage => 'Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN.'; + + @override + String get accountsSheetTitle => 'Akun'; + + @override + String get accountsSheetFailedLoadAccounts => 'Gagal memuat akun.'; + + @override + String get accountsSheetFailedLoadActiveAccount => 'Gagal memuat akun aktif.'; + + @override + String get accountsSheetNoAccountsFound => 'Tidak ada akun ditemukan.'; + + @override + String get accountsSheetAddAccount => 'Tambah Akun'; + + @override + String get accountsSheetBalanceUnavailable => 'Saldo tidak tersedia'; + + @override + String accountsSheetBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get addAccountMenuTitle => 'Tambah Akun'; + + @override + String get addAccountMenuCreateTitle => 'Buat Akun Baru'; + + @override + String get addAccountMenuCreateSubtitle => 'Buat alamat wallet baru'; + + @override + String get addAccountMenuImportTitle => 'Impor Wallet'; + + @override + String get addAccountMenuImportSubtitle => 'Gunakan recovery phrase untuk mengimpor'; + + @override + String get createAccountAppBarTitle => 'Nama Akun'; + + @override + String get createAccountSubtitle => 'Berikan nama yang mudah Anda kenali. Anda bisa mengubahnya kapan saja.'; + + @override + String get createAccountButton => 'Buat'; + + @override + String get createAccountErrorCouldNotAdd => 'Gagal menambahkan akun.'; + + @override + String createAccountDefaultName(int number) { + return 'Akun $number'; + } + + @override + String get editAccountAppBarTitle => 'Nama Akun'; + + @override + String get editAccountDone => 'Selesai'; + + @override + String get editAccountNameEmpty => 'Nama akun tidak boleh kosong'; + + @override + String get editAccountRenameFailed => 'Gagal mengganti nama akun.'; + + @override + String get accountMenuTitle => 'Akun'; + + @override + String get accountMenuAccountName => 'Nama Akun'; + + @override + String get accountMenuAddressDetails => 'Detail Alamat'; + + @override + String get accountMenuShowRecoveryPhrase => 'Tampilkan Recovery Phrase'; + + @override + String get accountMenuNotFound => 'Akun tidak ditemukan'; + + @override + String get accountDetailsTitle => 'Detail Alamat'; + + @override + String get addHardwareAccountAddWallet => 'Tambah Hardware Wallet'; + + @override + String get addHardwareAccountAddAccount => 'Tambah Akun Hardware'; + + @override + String get addHardwareAccountNameLabel => 'NAMA'; + + @override + String get addHardwareAccountNameHintWallet => 'Hardware Wallet'; + + @override + String get addHardwareAccountNameHintAccount => 'Akun'; + + @override + String get addHardwareAccountAddressLabel => 'ALAMAT'; + + @override + String get addHardwareAccountAddressHint => 'Alamat SS58'; + + @override + String get addHardwareAccountDebugFill => 'Isi Debug'; + + @override + String get addHardwareAccountNameRequired => 'Nama wajib diisi'; + + @override + String get addHardwareAccountInvalidAddress => 'Alamat tidak valid'; + + @override + String get sendTitle => 'Kirim'; + + @override + String get sendPayTitle => 'Bayar'; + + @override + String get sendEnterAddress => 'Masukkan Alamat'; + + @override + String get sendSelectRecipientSendTo => 'Kirim Ke'; + + @override + String sendSelectRecipientSearchHint(String symbol) { + return 'Cari Alamat $symbol'; + } + + @override + String get sendSelectRecipientScanTitle => 'Pindai kode QR'; + + @override + String sendSelectRecipientScanSubtitle(String symbol) { + return 'Ketuk untuk memindai Alamat $symbol'; + } + + @override + String get sendSelectRecipientRecents => 'Terbaru'; + + @override + String get sendSelectRecipientContinue => 'Lanjutkan'; + + @override + String get sendInputAmountSendTo => 'KIRIM KE'; + + @override + String get sendInputAmountAvailableBalance => 'Saldo Tersedia:'; + + @override + String get sendInputAmountNetworkFee => 'Biaya Jaringan:'; + + @override + String get sendInputAmountMax => 'Maks'; + + @override + String get sendInputAmountInvalidAmount => 'Masukkan jumlah yang valid'; + + @override + String get sendInputAmountChecksumRequired => 'Checksum penerima diperlukan'; + + @override + String get sendReviewSending => 'MENGIRIM'; + + @override + String get sendReviewTo => 'KE'; + + @override + String get sendReviewAmount => 'JUMLAH'; + + @override + String get sendReviewNetworkFee => 'BIAYA JARINGAN'; + + @override + String get sendReviewYouPay => 'ANDA BAYAR'; + + @override + String get sendReviewConfirm => 'Konfirmasi'; + + @override + String get sendReviewAuthReason => 'Autentikasi untuk mengonfirmasi transaksi'; + + @override + String get sendReviewAuthRequired => 'Autentikasi diperlukan untuk mengirim'; + + @override + String get sendReviewSubmitFailed => 'Gagal mengirim transaksi'; + + @override + String sendTxSubmittedHeadlinePaid(String amount, String symbol) { + return '$amount $symbol dibayar'; + } + + @override + String sendTxSubmittedHeadlineSent(String amount, String symbol) { + return '$amount $symbol terkirim'; + } + + @override + String get sendTxSubmittedOnItsWay => 'Sedang dalam perjalanan'; + + @override + String get sendTxSubmittedToLabel => 'Ke'; + + @override + String get sendTxSubmittedDone => 'Selesai'; + + @override + String get sendLogicCantSelfTransfer => 'Tidak Bisa Transfer ke Diri Sendiri'; + + @override + String get sendLogicEnterAmount => 'Masukkan Jumlah'; + + @override + String get sendLogicInvalidAmount => 'Jumlah Tidak Valid'; + + @override + String get sendLogicBelowExistentialDeposit => 'Di Bawah Deposit Eksistensial'; + + @override + String get sendLogicInsufficientBalance => 'Saldo Tidak Cukup'; + + @override + String get sendLogicReviewSend => 'Tinjau Pengiriman'; + + @override + String get activityTitle => 'Aktivitas'; + + @override + String activityError(String error) { + return 'Gagal: $error'; + } + + @override + String get activityNoAccount => 'Tidak ada akun'; + + @override + String get activityEmpty => 'Belum ada transaksi'; + + @override + String get activityFilterAll => 'Semua'; + + @override + String get activityFilterSend => 'Kirim'; + + @override + String get activityFilterReceive => 'Terima'; + + @override + String get activityDateToday => 'Hari Ini'; + + @override + String get activityDateYesterday => 'Kemarin'; + + @override + String get activityTxSending => 'Mengirim'; + + @override + String get activityTxReceiving => 'Menerima'; + + @override + String get activityTxPending => 'Tertunda'; + + @override + String get activityTxSent => 'Terkirim'; + + @override + String get activityTxReceived => 'Diterima'; + + @override + String get activityTxTo => 'Ke'; + + @override + String get activityTxFrom => 'Dari'; + + @override + String get activityTxTimeNow => 'sekarang'; + + @override + String activityTxTimeMinutesAgo(int minutes) { + return '${minutes}m lalu'; + } + + @override + String activityTxTimeHoursAgo(int hours) { + return '${hours}j lalu'; + } + + @override + String activityTxTimeDaysAgo(int days) { + return '${days}h lalu'; + } + + @override + String activityTxTimeRemaining(String days, String hours, String minutes) { + return '${days}h:${hours}j:${minutes}m'; + } + + @override + String get activityDetailTitleSending => 'Mengirim'; + + @override + String get activityDetailTitleScheduled => 'Terjadwal'; + + @override + String get activityDetailTitleReceiving => 'Menerima'; + + @override + String get activityDetailTitleSent => 'Terkirim'; + + @override + String get activityDetailTitleReceived => 'Diterima'; + + @override + String get activityDetailStatusInProcess => 'Diproses'; + + @override + String get activityDetailStatusScheduled => 'Terjadwal'; + + @override + String get activityDetailStatusCompleted => 'Selesai'; + + @override + String get activityDetailStatus => 'STATUS'; + + @override + String get activityDetailTo => 'KE'; + + @override + String get activityDetailFrom => 'DARI'; + + @override + String get activityDetailDate => 'TANGGAL'; + + @override + String get activityDetailNetworkFee => 'BIAYA JARINGAN'; + + @override + String get activityDetailTxHash => 'HASH TX'; + + @override + String get activityDetailViewExplorer => 'Lihat di Explorer ↗'; + + @override + String get receiveTitle => 'Terima'; + + @override + String get receiveTabQrCode => 'Kode QR'; + + @override + String get receiveTabAddress => 'Alamat'; + + @override + String get receiveCopy => 'Salin'; + + @override + String receiveErrorLoadingAccount(String error) { + return 'Gagal memuat data akun: $error'; + } + + @override + String receiveClipboardContent(String accountId, String checksum) { + return 'ID Akun:\n$accountId\n\nCheckphrase:\n$checksum'; + } + + @override + String get receiveCopiedMessage => 'Detail akun disalin ke clipboard'; + + @override + String get posAmountTitle => 'Tagihan Baru'; + + @override + String posAmountCharge(String amount) { + return 'Tagih $amount'; + } + + @override + String get posAmountEnterAmount => 'Masukkan Jumlah'; + + @override + String get posQrTitleScanToPay => 'Pindai untuk Bayar'; + + @override + String get posQrTitlePaymentReceived => 'Pembayaran Diterima'; + + @override + String posQrError(String error) { + return 'Gagal: $error'; + } + + @override + String get posQrNoActiveAccount => 'Tidak ada akun aktif'; + + @override + String get posQrInvalidAmount => 'Jumlah tidak valid. Ketuk untuk coba lagi.'; + + @override + String get posQrConnectionLost => 'Koneksi terputus. Ketuk untuk coba lagi.'; + + @override + String get posQrTimedOut => 'Waktu habis. Ketuk untuk coba lagi.'; + + @override + String get posQrNewCharge => 'Tagihan Baru'; + + @override + String get posQrDone => 'Selesai'; + + @override + String posQrAmountReceived(String amount) { + return '$amount diterima'; + } + + @override + String get posQrFrom => 'Dari:'; + + @override + String get posQrWaitingForPayment => 'Menunggu pembayaran'; + + @override + String get posQrNetworkError => 'Jaringan Bermasalah'; + + @override + String get posQrTryAgain => 'Coba Lagi'; + + @override + String posQrPaidAt(String time) { + return 'Pada $time'; + } + + @override + String get settingsTitle => 'Pengaturan'; + + @override + String get settingsWalletTitle => 'Dompet'; + + @override + String get settingsWalletSubtitle => 'Frasa Pemulihan, Reset Dompet'; + + @override + String get settingsPreferencesTitle => 'Preferensi'; + + @override + String get settingsPreferencesSubtitle => 'Bahasa, mata uang, mode POS, notifikasi'; + + @override + String get settingsMiningRewards => 'Hadiah Mining'; + + @override + String settingsMiningRewardsSubtitle(int count) { + return '$count blok ditambang'; + } + + @override + String get settingsMiningRewardsError => 'Gagal memuat hadiah mining'; + + @override + String get settingsAccountTypeTitle => 'Jenis Akun'; + + @override + String get settingsAccountTypeSubtitle => 'Fitur Akun Lanjutan'; + + @override + String get settingsHelpTitle => 'Bantuan & Dukungan'; + + @override + String get settingsHelpSubtitle => 'FAQ, Hubungi tim'; + + @override + String get settingsAboutTitle => 'Tentang Quantus'; + + @override + String settingsAboutHubSubtitle(String version, String build) { + return 'Versi $version ($build)'; + } + + @override + String get settingsWalletRecoveryPhrase => 'Frasa Pemulihan'; + + @override + String get settingsWalletRecoveryPhraseSubtitle => 'Lihat Kata Sandi Cadangan 24 kata Anda'; + + @override + String get settingsWalletReset => 'Reset Dompet'; + + @override + String get settingsWalletResetSubtitle => 'Menghapus semua data dari perangkat ini'; + + @override + String get settingsWalletNoWalletsFound => 'Tidak ada dompet ditemukan'; + + @override + String get settingsWalletFailedToLoad => 'Gagal memuat dompet'; + + @override + String get settingsSelectWalletTitle => 'Pilih Dompet'; + + @override + String get settingsSelectWalletNoWallets => 'Tidak ada dompet ditemukan'; + + @override + String settingsSelectWalletItem(int number) { + return 'Dompet $number'; + } + + @override + String get settingsRecoveryConfirmAuthReason => 'Autentikasi untuk melihat frasa pemulihan'; + + @override + String get settingsRecoveryConfirmAuthRequired => 'Autentikasi diperlukan untuk melihat frasa pemulihan'; + + @override + String get settingsRecoveryPhraseTitle => 'Frasa Pemulihan'; + + @override + String get settingsRecoveryPhraseDone => 'Selesai'; + + @override + String get settingsResetTitle => 'Reset Dompet'; + + @override + String get settingsResetAuthReason => 'Autentikasi untuk mereset dompet'; + + @override + String settingsResetFailed(String error) { + return 'Gagal mereset dompet: $error'; + } + + @override + String get settingsResetAuthRequired => 'Autentikasi diperlukan untuk mereset dompet'; + + @override + String get settingsResetCautionHeadline => 'Ini akan menghapus\ndompet Anda'; + + @override + String get settingsResetCautionBullet1 => 'Semua data dompet akan dihapus permanen dari perangkat ini'; + + @override + String get settingsResetCautionBullet2 => + 'Dana Anda tetap di blockchain tetapi hanya frasa pemulihan yang dapat memulihkan akses'; + + @override + String get settingsResetCautionBullet3 => 'Tanpa frasa pemulihan, dana Anda hilang selamanya'; + + @override + String get settingsResetCautionCheckbox => 'Saya sudah mencadangkan frasa pemulihan saya'; + + @override + String get settingsPreferencesCurrency => 'Mata Uang'; + + @override + String get settingsPreferencesCurrencySubtitle => 'Preferensi tampilan fiat'; + + @override + String get settingsPreferencesLanguage => 'Bahasa'; + + @override + String get settingsPreferencesLanguageSubtitle => 'Bahasa tampilan aplikasi'; + + @override + String get settingsPreferencesPosMode => 'Mode POS'; + + @override + String get settingsPreferencesPosModeSubtitle => 'Fitur point of sale'; + + @override + String get settingsPreferencesNotifications => 'Notifikasi'; + + @override + String get settingsPreferencesNotificationsSubtitle => 'Peringatan transaksi dan dompet'; + + @override + String get settingsCurrencyTitle => 'Mata Uang'; + + @override + String get settingsCurrencySearchHint => 'Cari'; + + @override + String get settingsCurrencyNoMatch => 'Tidak ada mata uang yang cocok dengan pencarian Anda'; + + @override + String settingsCurrencyError(String error) { + return 'Gagal memilih mata uang: $error'; + } + + @override + String get settingsLanguageTitle => 'Bahasa'; + + @override + String get settingsLanguageSearchHint => 'Cari'; + + @override + String get settingsLanguageNoMatch => 'Tidak ada bahasa yang cocok dengan pencarian Anda'; + + @override + String settingsLanguageError(String error) { + return 'Gagal memilih bahasa: $error'; + } + + @override + String get settingsMiningTitle => 'Hadiah Mining'; + + @override + String get settingsMiningRedeem => 'Tukar'; + + @override + String get settingsMiningStatusMining => 'Mining'; + + @override + String get settingsMiningStatusPending => 'Menunggu'; + + @override + String get settingsMiningBlocksMined => 'BLOK DITAMBANG'; + + @override + String get settingsMiningBlocksAcrossTestnets => 'blok di semua testnet'; + + @override + String get settingsMiningStatTestnetBlocks => 'BLOK TESTNET'; + + @override + String get settingsMiningStatTestnetRewards => 'HADIAH TESTNET'; + + @override + String get settingsMiningStatRedeemed => 'DITUKAR'; + + @override + String get settingsMiningStatRedeemable => 'DAPAT DITUKAR'; + + @override + String get settingsMiningQuanEarned => 'QUAN DIHASILKAN'; + + @override + String get settingsMiningViewTelemetry => 'Lihat Telemetri ↗'; + + @override + String get settingsMiningNoDataTitle => 'Belum ada data mining'; + + @override + String get settingsMiningNoDataBody => 'Siapkan node mining Quantus untuk mulai mendapatkan hadiah.'; + + @override + String get settingsMiningSetupGuide => 'Panduan Setup Mining ↗'; + + @override + String get settingsMiningLoadError => 'Gagal memuat hadiah mining'; + + @override + String get settingsMiningCheckConnection => 'Periksa koneksi Anda'; + + @override + String get settingsMiningTestnetBlocks => 'blok'; + + @override + String get settingsMiningDiracSince => 'Nov 2025'; + + @override + String get settingsMiningSchrodingerSince => 'Okt 2025'; + + @override + String get settingsMiningResonanceSince => 'Jul 2025'; + + @override + String get settingsTestnetTitle => 'Hadiah Testnet'; + + @override + String get settingsTestnetLoadError => 'Gagal memuat hadiah testnet'; + + @override + String settingsTestnetTotalBlocks(int count) { + return '$count blok'; + } + + @override + String get settingsTestnetTotalDescription => 'Total blok ditambang di semua testnet'; + + @override + String get settingsTestnetBreakdown => 'Rincian'; + + @override + String settingsTestnetRowBlocks(int count) { + return '$count blok'; + } + + @override + String get settingsHelpScreenTitle => 'Bantuan & Dukungan'; + + @override + String get settingsHelpEmail => 'Dukungan Email'; + + @override + String get settingsHelpTelegram => 'Telegram'; + + @override + String get settingsAboutScreenTitle => 'Tentang'; + + @override + String get settingsAboutIntro => + 'Quantus adalah blockchain Layer 1 yang diamankan oleh ML-DSA Dilithium-5, standar emas enkripsi tahan kuantum. Dibangun untuk masa depan di mana kriptografi klasik tidak lagi cukup. Kriptografi pasca-kuantum untuk semua orang.'; + + @override + String get settingsAboutTerms => 'Ketentuan Layanan'; + + @override + String get settingsAboutTermsSubtitle => 'quantus.com/terms/'; + + @override + String get settingsAboutPrivacy => 'Kebijakan privasi'; + + @override + String get settingsAboutPrivacySubtitle => 'quantus.com/privacy-policy/'; + + @override + String get settingsAboutWebsite => 'Kunjungi Situs Web'; + + @override + String get settingsAboutWebsiteSubtitle => 'quantus.com'; + + @override + String settingsAboutVersion(String version, String build) { + return 'Versi $version ($build)'; + } + + @override + String get settingsAccountTypeScreenTitle => 'Jenis Akun'; + + @override + String get settingsAccountTypeIntro => + 'Fitur akun lanjutan akan segera hadir. Fitur ini memberi Anda kontrol lebih besar atas cara transaksi diotorisasi dan diamankan.'; + + @override + String get settingsAccountTypeReversibleTitle => 'Transaksi Reversible'; + + @override + String get settingsAccountTypeReversibleSubtitle => 'Batalkan pengiriman dalam jangka waktu tertentu'; + + @override + String get settingsAccountTypeHighSecurityTitle => 'Akun Keamanan Tinggi'; + + @override + String get settingsAccountTypeHighSecuritySubtitle => 'Persetujuan guardian diperlukan'; + + @override + String get settingsAccountTypeMultiSigTitle => 'Multi-Tanda Tangan'; + + @override + String get settingsAccountTypeMultiSigSubtitle => 'Beberapa persetujuan diperlukan'; + + @override + String get settingsAccountTypeHardwareTitle => 'Dompet Hardware'; + + @override + String get settingsAccountTypeHardwareSubtitle => 'Pasangkan perangkat hardware'; + + @override + String get settingsAccountTypeComingSoon => 'Segera Hadir'; + + @override + String get swapTitle => 'Tukar'; + + @override + String get swapFrom => 'Dari'; + + @override + String get swapTo => 'Ke'; + + @override + String get swapRefundAddress => 'Alamat Refund'; + + @override + String swapRefundAddressHint(String network) { + return 'Alamat $network'; + } + + @override + String get swapSlippageTolerance => 'Toleransi Slippage'; + + @override + String get swapRate => 'Kurs'; + + @override + String get swapGetQuote => 'Dapatkan Penawaran'; + + @override + String swapRateLabel(String amount, String symbol) { + return '1 QUAN = $amount $symbol'; + } + + @override + String swapRateZero(String symbol) { + return '1 QUAN = 0 $symbol'; + } + + @override + String get swapTokenPickerTitle => 'Pilih Token'; + + @override + String get swapTokenPickerLoadError => 'Gagal memuat token'; + + @override + String get swapReviewTitle => 'Tinjau Penawaran'; + + @override + String get swapReviewTotalFees => 'Total biaya'; + + @override + String get swapReviewTotalAmount => 'Jumlah Total'; + + @override + String swapReviewSlippageWarning(String amount, String percent) { + return 'Anda bisa menerima hingga \$$amount lebih sedikit berdasarkan slippage $percent% yang Anda atur'; + } + + @override + String get swapReviewConfirm => 'Konfirmasi'; + + @override + String get swapDepositAmount => 'Jumlah Deposit'; + + @override + String get swapDepositAmountCopied => 'Jumlah deposit disalin ke clipboard'; + + @override + String get swapDepositDemoWarning => 'Hanya untuk demo - jangan kirim dana!'; + + @override + String get swapDepositShareQr => 'Bagikan QR'; + + @override + String swapDepositShareContent(String network, String token, String address) { + return 'Jaringan: $network\nToken: $token\nAlamat: $address'; + } + + @override + String swapDepositNotice(String symbol, String network) { + return 'Gunakan dompet $symbol atau $network Anda untuk deposit. Menyetor aset lain dapat mengakibatkan kehilangan dana.'; + } + + @override + String get swapDepositProcessingTitle => 'Memproses Swap'; + + @override + String get swapDepositProcessingBody => 'Ini mungkin memakan waktu beberapa menit...'; + + @override + String get swapDepositCompleteTitle => 'Swap Selesai'; + + @override + String swapDepositCompleteBody(String amount) { + return 'Swap Anda untuk $amount QUAN telah selesai.'; + } + + @override + String get swapDepositTestnetBanner => 'HANYA DEMO - KAMI MASIH DI TESTNET'; + + @override + String get swapDepositSentFunds => 'Saya sudah mengirim dana'; + + @override + String get swapDepositDone => 'Selesai'; + + @override + String get swapRefundPickerTitle => 'Alamat Refund'; + + @override + String get swapRefundPickerEmpty => 'Tidak ada alamat refund terbaru'; + + @override + String get componentQrScannerTitle => 'Pindai Kode QR'; + + @override + String get componentQrScannerNoCode => 'Tidak ada kode QR pada gambar'; + + @override + String get componentShare => 'Bagikan'; + + @override + String get componentAddressLabel => 'ALAMAT'; + + @override + String get componentCheckphraseLabel => 'CHECKPHRASE'; + + @override + String get componentCheckphraseCopied => 'Checkphrase disalin'; + + @override + String get componentNameFieldHint => 'Masukkan nama untuk akun Anda'; + + @override + String get commonLoading => 'Memuat...'; + + @override + String commonAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get commonContinue => 'Lanjutkan'; +} diff --git a/mobile-app/lib/models/app_locale.dart b/mobile-app/lib/models/app_locale.dart new file mode 100644 index 00000000..03ab8aff --- /dev/null +++ b/mobile-app/lib/models/app_locale.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +/// App UI locales the user can select in settings. +enum AppLocale { + en(languageCode: 'en', displayName: 'English', numberFormatLocale: 'en_US'), + id(languageCode: 'id', displayName: 'Bahasa Indonesia', numberFormatLocale: 'id_ID'); + + const AppLocale({required this.languageCode, required this.displayName, required this.numberFormatLocale}); + + final String languageCode; + final String displayName; + final String numberFormatLocale; + + Locale get flutterLocale => Locale(languageCode); + + static AppLocale fromCode(String code, {AppLocale fallback = AppLocale.en}) { + return AppLocale.values.firstWhere((l) => l.languageCode == code, orElse: () => fallback); + } +} diff --git a/mobile-app/lib/providers/currency_display_provider.dart b/mobile-app/lib/providers/currency_display_provider.dart index 3f1acf56..7d1c57f1 100644 --- a/mobile-app/lib/providers/currency_display_provider.dart +++ b/mobile-app/lib/providers/currency_display_provider.dart @@ -145,6 +145,8 @@ final selectedFiatCurrencyProvider = StateNotifierProvider { final SettingsService _settings; + static final FiatCurrency _defaultCurrency = FiatCurrency.usd; + SelectedFiatCurrencyNotifier(this._settings) : super(_load(_settings)); /// Persists and applies [currency] as the active fiat currency. @@ -153,9 +155,14 @@ class SelectedFiatCurrencyNotifier extends StateNotifier { state = currency; } + Future reset() async { + await _settings.clearSelectedFiatCurrency(); + state = _defaultCurrency; + } + static FiatCurrency _load(SettingsService settings) { final code = settings.getSelectedFiatCurrency(); - if (code == null) return FiatCurrency.usd; + if (code == null) return _defaultCurrency; return FiatCurrency.fromCode(code); } } diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart new file mode 100644 index 00000000..feb9e9ab --- /dev/null +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -0,0 +1,85 @@ +/// App locale persistence and localized strings via Riverpod. +/// +/// ## App strings (canonical) +/// +/// Use [l10nProvider] for all user-facing copy. Do **not** use +/// `AppLocalizations.of(context)` in widgets. +/// +/// - **`ref.watch(l10nProvider)`** — in `build` (or anywhere the UI must +/// rebuild when the user changes language). +/// - **`ref.read(l10nProvider)`** — one-off access in callbacks, timers, or +/// `try/catch` handlers where a subscription is wrong or wasteful. Capture +/// localized strings early if a callback can outlive a locale change. +/// +/// ```dart +/// // build — rebuilds on locale change +/// final l10n = ref.watch(l10nProvider); +/// +/// // callback — no subscription +/// onTap: () => context.showErrorToaster( +/// message: ref.read(l10nProvider).someError, +/// ); +/// ``` +/// +/// ## Framework localization +/// +/// [MaterialApp] in `app.dart` sets `locale`, `localizationsDelegates`, and +/// `supportedLocales` so Material/Cupertino built-ins (date pickers, etc.) +/// respect the active locale. App copy still comes from [l10nProvider]. +/// +/// ## Pure logic and tests +/// +/// Pass [AppLocalizations] as a parameter, or call +/// `lookupAppLocalizations(const Locale('en'))` in unit tests. +library; + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/models/app_locale.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; + +/// Persists and exposes the user's chosen app locale. +/// Defaults to [AppLocale.en] when no preference has been saved. +/// +/// To change the active locale (e.g. from a settings screen): +/// ref.read(selectedAppLocaleProvider.notifier).select(AppLocale.id); +final selectedAppLocaleProvider = StateNotifierProvider((ref) { + final settings = ref.watch(settingsServiceProvider); + return SelectedAppLocaleNotifier(settings); +}); + +class SelectedAppLocaleNotifier extends StateNotifier { + final SettingsService _settings; + + static final String _defaultLocaleCode = Platform.localeName.split('_').first.toLowerCase(); + + SelectedAppLocaleNotifier(this._settings) : super(_load(_settings)); + + Future select(AppLocale locale) async { + await _settings.setSelectedAppLocale(locale.languageCode); + state = locale; + } + + Future reset() async { + await _settings.clearSelectedAppLocale(); + state = AppLocale.fromCode(_defaultLocaleCode); + } + + static AppLocale _load(SettingsService settings) { + String? code = settings.getSelectedAppLocale(); + code ??= _defaultLocaleCode; + + return AppLocale.fromCode(code); + } +} + +/// Localized strings for the active app locale. +final l10nProvider = Provider((ref) { + final locale = ref.watch(selectedAppLocaleProvider).flutterLocale; + return lookupAppLocalizations(locale); +}); diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index e838d0df..943f0c42 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -1,9 +1,8 @@ -import 'dart:io' show Platform; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; final settingsServiceProvider = Provider((ref) { @@ -26,10 +25,9 @@ final recentAddressesServiceProvider = Provider((ref) { return RecentAddressesService(); }); -/// Caveat: snapshots [Platform.localeName] at provider creation time. -/// A mid-session locale change (rare) won't be picked up until app restart. final localeNumberConfigProvider = Provider((ref) { - return LocaleNumberConfig.fromLocale(Platform.localeName); + final appLocale = ref.watch(selectedAppLocaleProvider); + return LocaleNumberConfig.fromLocale(appLocale.numberFormatLocale); }); final numberFormattingServiceProvider = Provider((ref) { diff --git a/mobile-app/lib/services/logout_service.dart b/mobile-app/lib/services/logout_service.dart index f6075005..405c17dd 100644 --- a/mobile-app/lib/services/logout_service.dart +++ b/mobile-app/lib/services/logout_service.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; @@ -20,15 +22,18 @@ class LogoutService { _ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } - SettingsService().clearAll(); - SubstrateService().logout(); + await SubstrateService().logout(); _ref.read(pendingTransactionsProvider.notifier).clear(); _ref.read(miningRewardsServiceProvider).clearCachedRewardsData(); _ref.invalidate(miningRewardsProvider); _ref.read(accountsProvider.notifier).reset(); _ref.read(activeAccountProvider.notifier).reset(); _ref.read(accountAssociationsProvider.notifier).reset(); + await _ref.read(selectedAppLocaleProvider.notifier).reset(); + await _ref.read(selectedFiatCurrencyProvider.notifier).reset(); - Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); + if (context.mounted) { + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); + } } } diff --git a/mobile-app/lib/shared/utils/print.dart b/mobile-app/lib/shared/utils/print.dart new file mode 100644 index 00000000..9738a366 --- /dev/null +++ b/mobile-app/lib/shared/utils/print.dart @@ -0,0 +1,7 @@ +import 'package:flutter/foundation.dart'; + +void quantusDebugPrint(String message) { + if (kDebugMode) { + debugPrint(message); + } +} diff --git a/mobile-app/lib/v2/components/address_details_card.dart b/mobile-app/lib/v2/components/address_details_card.dart index 06769d9b..c8eb5b67 100644 --- a/mobile-app/lib/v2/components/address_details_card.dart +++ b/mobile-app/lib/v2/components/address_details_card.dart @@ -1,22 +1,25 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/split_card.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AddressDetailsCard extends StatefulWidget { +class AddressDetailsCard extends ConsumerStatefulWidget { final String accountId; final String? checksum; const AddressDetailsCard({super.key, required this.accountId, this.checksum}); @override - State createState() => _AddressDetailsCardState(); + ConsumerState createState() => _AddressDetailsCardState(); } -class _AddressDetailsCardState extends State { +class _AddressDetailsCardState extends ConsumerState { bool _addressCopied = false; bool _checksumCopied = false; Timer? _resetTimer; @@ -26,10 +29,10 @@ class _AddressDetailsCardState extends State { _triggerCopied(isAddress: true); } - void _copyChecksum(BuildContext context) { + void _copyChecksum(BuildContext context, AppLocalizations l10n) { if (widget.checksum == null) return; - context.copyTextWithToaster(widget.checksum!, message: 'Checkphrase copied'); + context.copyTextWithToaster(widget.checksum!, message: l10n.componentCheckphraseCopied); _triggerCopied(isAddress: false); } @@ -67,17 +70,19 @@ class _AddressDetailsCardState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return SplitCard( topChild: InkWell( onTap: () => _copyAddress(context), - child: _buildItem(context, 'ADDRESS', widget.accountId, isCopied: _addressCopied), + child: _buildItem(context, l10n.componentAddressLabel, widget.accountId, isCopied: _addressCopied), ), bottomChild: InkWell( - onTap: () => _copyChecksum(context), + onTap: () => _copyChecksum(context, l10n), child: _buildItem( context, - 'CHECKPHRASE', - widget.checksum ?? 'Loading...', + l10n.componentCheckphraseLabel, + widget.checksum ?? l10n.commonLoading, isCheckphrase: true, isCopied: _checksumCopied, ), @@ -109,9 +114,7 @@ class _AddressDetailsCardState extends State { ], ), ), - const SizedBox(width: 32), - _copyButton(isCopied: isCopied), ], ); diff --git a/mobile-app/lib/v2/components/name_field.dart b/mobile-app/lib/v2/components/name_field.dart index a63ec2a9..bcdeaae2 100644 --- a/mobile-app/lib/v2/components/name_field.dart +++ b/mobile-app/lib/v2/components/name_field.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class NameField extends StatelessWidget { +class NameField extends ConsumerWidget { final TextEditingController controller; final String? subtitle; final String? error; @@ -10,7 +12,8 @@ class NameField extends StatelessWidget { const NameField({super.key, required this.controller, this.subtitle, this.error}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final textStyle = context.themeText.smallTitle!.copyWith(fontWeight: FontWeight.w400); return Column( @@ -25,7 +28,7 @@ class NameField extends StatelessWidget { controller: controller, style: textStyle, decoration: InputDecoration.collapsed( - hintText: 'Enter a name for your account', + hintText: l10n.componentNameFieldHint, hintStyle: textStyle.copyWith(color: context.colors.textSecondary), ), ), diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart index 4c8f228c..84fc849d 100644 --- a/mobile-app/lib/v2/components/qr_scanner_page.dart +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class QrScannerPage extends StatefulWidget { +class QrScannerPage extends ConsumerStatefulWidget { final bool Function(String)? validator; const QrScannerPage({super.key, this.validator}); @override - State createState() => _QrScannerPageState(); + ConsumerState createState() => _QrScannerPageState(); } -class _QrScannerPageState extends State { +class _QrScannerPageState extends ConsumerState { final _controller = MobileScannerController(); bool _scanned = false; @@ -40,12 +42,14 @@ class _QrScannerPageState extends State { if (capture != null) { _onDetect(capture); } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No QR code found in image'))); + final l10n = ref.read(l10nProvider); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l10n.componentQrScannerNoCode))); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final screen = MediaQuery.of(context).size; final frameSize = (screen.width - 112).clamp(220.0, 280.0); @@ -83,7 +87,7 @@ class _QrScannerPageState extends State { ], ), ), - const Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: 'Scan QR Code')), + Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: l10n.componentQrScannerTitle)), ], ), ); diff --git a/mobile-app/lib/v2/components/recovery_phrase_body.dart b/mobile-app/lib/v2/components/recovery_phrase_body.dart index 3a6bc4b2..50907991 100644 --- a/mobile-app/lib/v2/components/recovery_phrase_body.dart +++ b/mobile-app/lib/v2/components/recovery_phrase_body.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/features/components/mnemonic_grid.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -9,7 +11,7 @@ import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class RecoveryPhraseBody extends StatelessWidget { +class RecoveryPhraseBody extends ConsumerWidget { final String appBarTitle; final List words; final String primaryButtonLabel; @@ -29,12 +31,14 @@ class RecoveryPhraseBody extends StatelessWidget { this.isPrimaryButtonLoading = false, }); - void _copyToClipboard(BuildContext context) { - context.copyTextWithToaster(words.join(' '), message: 'Recovery phrase copied to clipboard'); + void _copyToClipboard(BuildContext context, WidgetRef ref) { + final l10n = ref.read(l10nProvider); + context.copyTextWithToaster(words.join(' '), message: l10n.recoveryPhraseBodyCopiedMessage); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; @@ -43,10 +47,7 @@ class RecoveryPhraseBody extends StatelessWidget { mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - 'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.', - style: text.smallParagraph?.copyWith(color: colors.textTertiary), - ), + Text(l10n.recoveryPhraseBodyInstructions, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), const SizedBox(height: 24), Expanded( child: isGridLoading @@ -55,21 +56,25 @@ class RecoveryPhraseBody extends StatelessWidget { ), ], ), - bottomContent: _bottomBar(context, colors), + bottomContent: _bottomBar(context, ref, colors), ); } - Widget _bottomBar(BuildContext context, AppColorsV2 colors) { + Widget _bottomBar(BuildContext context, WidgetRef ref, AppColorsV2 colors) { + final l10n = ref.watch(l10nProvider); + final padding = const EdgeInsets.symmetric(horizontal: 24, vertical: 12); + return ScaffoldBaseBottomContent( child: Row( children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.recoveryPhraseBodyCopy, icon: Icon(Icons.copy, color: colors.textPrimary, size: 14), iconPlacement: IconPlacement.leading, - onTap: () => _copyToClipboard(context), + onTap: () => _copyToClipboard(context, ref), variant: ButtonVariant.secondary, + padding: padding, ), ), const SizedBox(width: 24), @@ -80,6 +85,7 @@ class RecoveryPhraseBody extends StatelessWidget { isLoading: isPrimaryButtonLoading, onTap: onPrimary, variant: ButtonVariant.primary, + padding: padding, ), ), ], diff --git a/mobile-app/lib/v2/components/share_account_button.dart b/mobile-app/lib/v2/components/share_account_button.dart index e8c7a3be..12dea41a 100644 --- a/mobile-app/lib/v2/components/share_account_button.dart +++ b/mobile-app/lib/v2/components/share_account_button.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class ShareAccountButton extends StatelessWidget { +class ShareAccountButton extends ConsumerWidget { final VoidCallback onTap; final bool isDisabled; const ShareAccountButton({super.key, required this.onTap, this.isDisabled = false}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); + return QuantusButton.simple( - label: 'Share', + label: l10n.componentShare, onTap: onTap, icon: Icon(Icons.shortcut_rounded, size: 20, color: context.colors.background), iconPlacement: IconPlacement.leading, diff --git a/mobile-app/lib/v2/screens/accounts/account_details_screen.dart b/mobile-app/lib/v2/screens/accounts/account_details_screen.dart index cd9b75e7..59653f69 100644 --- a/mobile-app/lib/v2/screens/accounts/account_details_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_details_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/components/address_details_card.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -7,16 +9,16 @@ import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_cont import 'package:resonance_network_wallet/v2/components/share_account_button.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; -class AccountDetailsScreen extends StatefulWidget { +class AccountDetailsScreen extends ConsumerStatefulWidget { final Account account; const AccountDetailsScreen({super.key, required this.account}); @override - State createState() => _AccountDetailsScreenState(); + ConsumerState createState() => _AccountDetailsScreenState(); } -class _AccountDetailsScreenState extends State { +class _AccountDetailsScreenState extends ConsumerState { final _checksumService = HumanReadableChecksumService(); String? _checksum; bool _isLoading = true; @@ -45,9 +47,11 @@ class _AccountDetailsScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final account = widget.account; + return ScaffoldBase( - appBar: const V2AppBar(title: 'Address Details'), + appBar: V2AppBar(title: l10n.accountDetailsTitle), mainContent: AddressDetailsCard(accountId: account.accountId, checksum: _checksum), bottomContent: ScaffoldBaseBottomContent( child: ShareAccountButton(onTap: _share, isDisabled: _isLoading), diff --git a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart index bb155345..44729c0c 100644 --- a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/account_badge.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -18,6 +19,7 @@ class AccountMenuScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; @@ -25,7 +27,7 @@ class AccountMenuScreen extends ConsumerWidget { final account = accounts.value?.firstWhere((a) => a.accountId == initialAccount.accountId); return ScaffoldBase( - appBar: const V2AppBar(title: 'Accounts'), + appBar: V2AppBar(title: l10n.accountMenuTitle), mainContent: account != null ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -34,17 +36,17 @@ class AccountMenuScreen extends ConsumerWidget { _ProfileHeader(account: account, colors: colors, text: text), const SizedBox(height: 80), _MenuRow( - label: 'Account Name', + label: l10n.accountMenuAccountName, value: account.name, onTap: () => _openNameEditor(context, ref, account), ), Divider(color: colors.toasterBackground, height: 1), - _MenuRow(label: 'Address Details', onTap: () => _openAddressDetails(context, account)), + _MenuRow(label: l10n.accountMenuAddressDetails, onTap: () => _openAddressDetails(context, account)), Divider(color: colors.toasterBackground, height: 1), - _MenuRow(label: 'Show Recovery Phrase', onTap: () => _openRecoveryPhrase(context, account)), + _MenuRow(label: l10n.accountMenuShowRecoveryPhrase, onTap: () => _openRecoveryPhrase(context, account)), ], ) - : const Center(child: Text('Account not found')), + : Center(child: Text(l10n.accountMenuNotFound)), ); } diff --git a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart index fc4b1959..49b974cf 100644 --- a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -11,7 +14,7 @@ import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; enum AccountReadyOverviewOrigin { accountCreated, walletCreated, walletImported } -class AccountReadyScreen extends StatelessWidget { +class AccountReadyScreen extends ConsumerWidget { const AccountReadyScreen({ super.key, required this.origin, @@ -33,17 +36,18 @@ class AccountReadyScreen extends StatelessWidget { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (_) => false); } - String get _appBarTitle => switch (origin) { - AccountReadyOverviewOrigin.accountCreated => 'Account Created', - AccountReadyOverviewOrigin.walletCreated => 'Wallet Created', - AccountReadyOverviewOrigin.walletImported => 'Wallet Imported', + String _appBarTitle(AppLocalizations l10n) => switch (origin) { + AccountReadyOverviewOrigin.accountCreated => l10n.accountReadyAccountCreated, + AccountReadyOverviewOrigin.walletCreated => l10n.accountReadyWalletCreated, + AccountReadyOverviewOrigin.walletImported => l10n.accountReadyWalletImported, }; bool get isWalletRelated => origin == AccountReadyOverviewOrigin.walletCreated || origin == AccountReadyOverviewOrigin.walletImported; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final formattedAddress = AddressFormattingService.formatAddress( @@ -52,7 +56,8 @@ class AccountReadyScreen extends StatelessWidget { ellipses: '.......', postFix: 10, ); - final headline = isWalletRelated ? _appBarTitle : accountName; + final appBarTitle = _appBarTitle(l10n); + final headline = isWalletRelated ? appBarTitle : accountName; return PopScope( canPop: false, @@ -63,7 +68,7 @@ class AccountReadyScreen extends StatelessWidget { }, child: ScaffoldBase( appBar: V2AppBar( - title: _appBarTitle, + title: appBarTitle, leading: AppBackButton(onTap: () => _goHome(context)), ), mainContent: Column( @@ -130,7 +135,11 @@ class AccountReadyScreen extends StatelessWidget { ], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: 'Done', onTap: () => _goHome(context), variant: ButtonVariant.primary), + child: QuantusButton.simple( + label: l10n.accountReadyDone, + onTap: () => _goHome(context), + variant: ButtonVariant.primary, + ), ), ), ); diff --git a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart index a838dca8..94471aca 100644 --- a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart @@ -7,7 +7,9 @@ import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/account_menu_screen.dart'; @@ -56,6 +58,7 @@ class _AccountsScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final accountsAsync = ref.watch(accountsProvider); final activeDisplayAccountAsync = ref.watch(activeAccountProvider); @@ -69,9 +72,10 @@ class _AccountsScreenState extends ConsumerState { final sheetHeight = math.min(610.0, maxHeight); return BottomSheetContainer( - title: 'Accounts', + title: l10n.accountsSheetTitle, height: sheetHeight, child: _buildContent( + l10n: l10n, accountsAsync: accountsAsync, activeDisplayAccountAsync: activeDisplayAccountAsync, displayAccounts: displayAccounts, @@ -81,6 +85,7 @@ class _AccountsScreenState extends ConsumerState { } Widget _buildContent({ + required AppLocalizations l10n, required AsyncValue> accountsAsync, required AsyncValue activeDisplayAccountAsync, required List displayAccounts, @@ -93,7 +98,7 @@ class _AccountsScreenState extends ConsumerState { if (accountsAsync.hasError) { return Center( child: Text( - 'Failed to load accounts.', + l10n.accountsSheetFailedLoadAccounts, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ); @@ -102,16 +107,16 @@ class _AccountsScreenState extends ConsumerState { if (activeDisplayAccountAsync.hasError) { return Center( child: Text( - 'Failed to load active account.', + l10n.accountsSheetFailedLoadActiveAccount, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ); } - return _buildAccountsListView(displayAccounts, activeAccountId); + return _buildAccountsListView(l10n, displayAccounts, activeAccountId); } - Widget _buildAccountsListView(List displayAccounts, String? activeAccountId) { + Widget _buildAccountsListView(AppLocalizations l10n, List displayAccounts, String? activeAccountId) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -119,7 +124,7 @@ class _AccountsScreenState extends ConsumerState { child: displayAccounts.isEmpty ? Center( child: Text( - 'No accounts found.', + l10n.accountsSheetNoAccountsFound, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ) @@ -130,23 +135,27 @@ class _AccountsScreenState extends ConsumerState { final account = displayAccounts[index]; final isActive = account.accountId == activeAccountId; - return _buildAccountRow(account, isActive); + return _buildAccountRow(l10n, account, isActive); }, ), ), const SizedBox(height: 24), - QuantusButton.simple(label: 'Add Account', onTap: _openAddAccountMenu, variant: ButtonVariant.primary), + QuantusButton.simple( + label: l10n.accountsSheetAddAccount, + onTap: _openAddAccountMenu, + variant: ButtonVariant.primary, + ), ], ); } - Widget _buildAccountRow(Account account, bool isActive) { + Widget _buildAccountRow(AppLocalizations l10n, Account account, bool isActive) { final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); final formattingService = ref.watch(numberFormattingServiceProvider); final balanceText = balanceAsync.when( - loading: () => 'Loading...', - error: (_, _) => 'Balance unavailable', - data: (balance) => '${formattingService.formatBalance(balance)} ${AppConstants.tokenSymbol}', + loading: () => l10n.commonLoading, + error: (_, _) => l10n.accountsSheetBalanceUnavailable, + data: (balance) => l10n.accountsSheetBalance(formattingService.formatBalance(balance), AppConstants.tokenSymbol), ); final colors = context.colors; diff --git a/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart index cc5ee035..5accf86e 100644 --- a/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -31,17 +32,18 @@ class _AddAccountMenuScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; return ScaffoldBase( - appBar: const V2AppBar(title: 'Add Account'), + appBar: V2AppBar(title: l10n.addAccountMenuTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _AddMenuRow( icon: Icons.add, - title: 'Create New Account', - subtitle: 'Generate a fresh wallet address', + title: l10n.addAccountMenuCreateTitle, + subtitle: l10n.addAccountMenuCreateSubtitle, onTap: _onCreateNewAccount, colors: colors, text: context.themeText, @@ -51,8 +53,8 @@ class _AddAccountMenuScreenState extends ConsumerState { const SizedBox(height: 24), _AddMenuRow( icon: Icons.save_alt, - title: 'Import Wallet', - subtitle: 'Use a recovery phrase to import', + title: l10n.addAccountMenuImportTitle, + subtitle: l10n.addAccountMenuImportSubtitle, onTap: _onImportWallet, colors: colors, text: context.themeText, diff --git a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart index 42539d4a..e61d2beb 100644 --- a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart @@ -9,6 +9,7 @@ import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; class AddHardwareAccountScreen extends ConsumerStatefulWidget { @@ -52,12 +53,13 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = 'Name is required'); + setState(() => _error = l10n.addHardwareAccountNameRequired); return; } if (!_substrateService.isValidSS58Address(address)) { - setState(() => _error = 'Invalid address'); + setState(() => _error = l10n.addHardwareAccountInvalidAddress); return; } @@ -95,7 +97,9 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = null); }, @@ -115,8 +121,8 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = null); }, @@ -126,7 +132,7 @@ class _AddHardwareAccountScreenState extends ConsumerState { } } catch (_) { if (mounted) { - context.showErrorToaster(message: 'Could not add account.'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.createAccountErrorCouldNotAdd); } } finally { if (mounted) { @@ -107,7 +109,8 @@ class _CreateAccountScreenState extends ConsumerState { _accounts = ref.read(accountsProvider).value ?? []; _walletIndex = _walletIndexForActiveAccount(_accounts, activeAccount); - _accountName.text = 'Account ${_accounts.length + 1}'; + final l10n = ref.read(l10nProvider); + _accountName.text = l10n.createAccountDefaultName(_accounts.length + 1); _accountName.addListener(() => setState(() {})); } @@ -119,16 +122,14 @@ class _CreateAccountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Name'), - mainContent: NameField( - controller: _accountName, - subtitle: "Give this account a name you'll recognize. You can change it anytime.", - error: _error, - ), + appBar: V2AppBar(title: l10n.createAccountAppBarTitle), + mainContent: NameField(controller: _accountName, subtitle: l10n.createAccountSubtitle, error: _error), bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Create', + label: l10n.createAccountButton, onTap: _createAccount, isLoading: _isLoading, isDisabled: _isDisabled, diff --git a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart index b59fc54f..6bb7ea76 100644 --- a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/components/name_field.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -39,7 +40,8 @@ class EditAccountScreenState extends ConsumerState { Future _save() async { final name = _controller.text.trim(); if (name.isEmpty) { - context.showErrorToaster(message: "Account name can't be empty"); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.editAccountNameEmpty); return; } if (name == widget.initialAccount.name) { @@ -56,7 +58,8 @@ class EditAccountScreenState extends ConsumerState { } } catch (_) { if (mounted) { - context.showErrorToaster(message: 'Failed to rename account.'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.editAccountRenameFailed); } } finally { if (mounted) setState(() => _saving = false); @@ -65,14 +68,21 @@ class EditAccountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Name'), + appBar: V2AppBar(title: l10n.editAccountAppBarTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [NameField(controller: _controller)], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(variant: ButtonVariant.primary, label: 'Done', onTap: _save, isLoading: _saving), + child: QuantusButton.simple( + variant: ButtonVariant.primary, + label: l10n.editAccountDone, + onTap: _save, + isLoading: _saving, + ), ), ); } diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index a83526f7..264c0ef2 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -33,8 +35,16 @@ class _ActivityScreenState extends ConsumerState { }); } + String _filterLabel(TransactionFilter filter, AppLocalizations l10n) => switch (filter) { + TransactionFilter.all => l10n.activityFilterAll, + TransactionFilter.send => l10n.activityFilterSend, + TransactionFilter.receive => l10n.activityFilterReceive, + }; + @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -43,13 +53,16 @@ class _ActivityScreenState extends ConsumerState { final filterButtons = TransactionFilter.values .map( - (e) => - _buildFilterButton(e.displayName, onTap: () => _onFilterOptionChanged(e), isSelected: _filterOption == e), + (e) => _buildFilterButton( + _filterLabel(e, l10n), + onTap: () => _onFilterOptionChanged(e), + isSelected: _filterOption == e, + ), ) .toList(); return ScaffoldBase( - appBar: const V2AppBar(title: 'Activity'), + appBar: V2AppBar(title: l10n.activityTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, @@ -58,17 +71,17 @@ class _ActivityScreenState extends ConsumerState { scrollDirection: Axis.horizontal, child: Row(spacing: 12, children: filterButtons), ), - const SizedBox(height: 40), - Expanded( child: accountAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.activityError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), ), data: (active) { - if (active == null) return const Center(child: Text('No account')); + if (active == null) { + return Center(child: Text(l10n.activityNoAccount)); + } return txAsync.when( loading: () => ListView.builder( itemCount: 3, @@ -76,10 +89,8 @@ class _ActivityScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (i > 0) const SizedBox(height: 32), - const Skeleton(width: 100, height: 24), const SizedBox(height: 12), - for (var j = 0; j < 3; j++) ...[ const TxItemSkeleton(), if (j < 2) Divider(color: colors.txItemSeparator, height: 24), @@ -88,7 +99,10 @@ class _ActivityScreenState extends ConsumerState { ), ), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text( + l10n.activityError(e.toString()), + style: text.detail?.copyWith(color: colors.textError), + ), ), data: (data) { final txService = ref.read(transactionServiceProvider); @@ -100,13 +114,10 @@ class _ActivityScreenState extends ConsumerState { ); if (all.isEmpty) { return Center( - child: Text( - 'No transactions yet', - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + child: Text(l10n.activityEmpty, style: text.paragraph?.copyWith(color: colors.textSecondary)), ); } - final grouped = _groupByDate(all); + final grouped = _groupByDate(all, l10n, appLocale.numberFormatLocale); return ListView.builder( padding: EdgeInsets.zero, @@ -119,13 +130,14 @@ class _ActivityScreenState extends ConsumerState { if (i > 0) const SizedBox(height: 32), Text(group.label, style: text.receiveLabel?.copyWith(color: colors.textTertiary)), ...group.transactions.mapIndexed((index, tx) { - final itemData = TxItemData.from(tx, active.account.accountId, colors); + final itemData = TxItemData.from(tx, active.account.accountId, colors, l10n); final isLastItem = index == group.transactions.length - 1; return buildTxItem( tx, itemData, colors, text, + l10n, formattedAmount: formatTxAmount(itemData.amount, isSend: itemData.isSend).primaryAmount, isLastItem: isLastItem, onTap: () { @@ -160,7 +172,7 @@ class _ActivityScreenState extends ConsumerState { ); } - List<_DateGroup> _groupByDate(List transactions) { + List<_DateGroup> _groupByDate(List transactions, AppLocalizations l10n, String localeName) { final Map> groups = {}; final Map labelMap = {}; @@ -169,7 +181,7 @@ class _ActivityScreenState extends ConsumerState { final key = '${day.year}-${day.month}-${day.day}'; groups.putIfAbsent(key, () => []); groups[key]!.add(tx); - labelMap.putIfAbsent(key, () => dateGroupLabel(tx.timestamp)); + labelMap.putIfAbsent(key, () => dateGroupLabel(tx.timestamp, l10n, localeName)); } return groups.entries.map((e) => _DateGroup(label: labelMap[e.key]!.toUpperCase(), transactions: e.value)).toList(); diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index 8f604959..52f04511 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/dotted_border.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/routes.dart'; import 'package:resonance_network_wallet/shared/extensions/current_route_extensions.dart'; @@ -23,7 +25,7 @@ void showTransactionDetailSheet(BuildContext context, TransactionEvent tx, Strin ); } -class _TransactionDetailSheet extends StatelessWidget { +class _TransactionDetailSheet extends ConsumerWidget { final TransactionEvent tx; final String activeAccountId; @@ -32,16 +34,18 @@ class _TransactionDetailSheet extends StatelessWidget { bool get _isSend => tx.from == activeAccountId; bool get _isPending => tx is PendingTransactionEvent; - String get _title { - if (_isPending) return 'Sending'; - if (tx.isReversibleScheduled) return _isSend ? 'Scheduled' : 'Receiving'; - return _isSend ? 'Sent' : 'Received'; + String _title(AppLocalizations l10n) { + if (_isPending) return l10n.activityDetailTitleSending; + if (tx.isReversibleScheduled) { + return _isSend ? l10n.activityDetailTitleScheduled : l10n.activityDetailTitleReceiving; + } + return _isSend ? l10n.activityDetailTitleSent : l10n.activityDetailTitleReceived; } - String get _statusLabel { - if (_isPending) return 'In Process'; - if (tx.isReversibleScheduled) return 'Scheduled'; - return 'Completed'; + String _statusLabel(AppLocalizations l10n) { + if (_isPending) return l10n.activityDetailStatusInProcess; + if (tx.isReversibleScheduled) return l10n.activityDetailStatusScheduled; + return l10n.activityDetailStatusCompleted; } Color _statusColor(AppColorsV2 colors) { @@ -50,12 +54,13 @@ class _TransactionDetailSheet extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return BottomSheetContainer( - title: _title, + title: _title(l10n), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -63,7 +68,12 @@ class _TransactionDetailSheet extends StatelessWidget { const SizedBox(height: 16), _AmountSection(tx: tx, isSend: _isSend, colors: colors), const SizedBox(height: 20), - _DetailRow(label: 'STATUS', value: _statusLabel, valueColor: _statusColor(colors), colors: colors), + _DetailRow( + label: l10n.activityDetailStatus, + value: _statusLabel(l10n), + valueColor: _statusColor(colors), + colors: colors, + ), const SizedBox(height: 8), DottedBorder( dashLength: 3, @@ -113,6 +123,7 @@ class _DetailsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final formattingService = ref.watch(numberFormattingServiceProvider); final counterparty = isSend ? tx.to : tx.from; @@ -123,7 +134,10 @@ class _DetailsSection extends ConsumerWidget { if (tx is TransferEvent) fee = (tx as TransferEvent).fee; if (tx is PendingTransactionEvent) fee = (tx as PendingTransactionEvent).fee; final feeStr = (fee != null && fee != BigInt.zero) - ? '${formattingService.formatBalance(fee, maxDecimals: AppConstants.decimals)} ${AppConstants.tokenSymbol}' + ? l10n.commonAmountBalance( + formattingService.formatBalance(fee, maxDecimals: AppConstants.decimals), + AppConstants.tokenSymbol, + ) : null; final txHash = tx.extrinsicHash != null @@ -132,10 +146,10 @@ class _DetailsSection extends ConsumerWidget { return Column( children: [ - _DetailRow(label: isSend ? 'TO' : 'FROM', value: address, colors: colors), - _DetailRow(label: 'DATE', value: dateTime, colors: colors), - if (feeStr != null) _DetailRow(label: 'NETWORK FEE', value: feeStr, colors: colors), - if (txHash != null) _DetailRow(label: 'TX HASH', value: txHash, colors: colors), + _DetailRow(label: isSend ? l10n.activityDetailTo : l10n.activityDetailFrom, value: address, colors: colors), + _DetailRow(label: l10n.activityDetailDate, value: dateTime, colors: colors), + if (feeStr != null) _DetailRow(label: l10n.activityDetailNetworkFee, value: feeStr, colors: colors), + if (txHash != null) _DetailRow(label: l10n.activityDetailTxHash, value: txHash, colors: colors), ], ); } @@ -169,7 +183,7 @@ class _DetailRow extends StatelessWidget { } } -class _ExplorerLink extends StatelessWidget { +class _ExplorerLink extends ConsumerWidget { final TransactionEvent tx; final AppColorsV2 colors; final AppTextTheme text; @@ -177,7 +191,8 @@ class _ExplorerLink extends StatelessWidget { const _ExplorerLink({required this.tx, required this.colors, required this.text}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final isPending = tx is PendingTransactionEvent; final color = isPending ? colors.accentOrange.withValues(alpha: 0.3) : colors.accentOrange; @@ -189,7 +204,7 @@ class _ExplorerLink extends StatelessWidget { border: Border(bottom: BorderSide(color: color, width: 1)), ), child: Text( - 'View in Explorer ↗', + l10n.activityDetailViewExplorer, style: text.smallParagraph?.copyWith(color: color, fontWeight: FontWeight.w400), ), ), diff --git a/mobile-app/lib/v2/screens/activity/tx_item.dart b/mobile-app/lib/v2/screens/activity/tx_item.dart index b88735f7..8c581c74 100644 --- a/mobile-app/lib/v2/screens/activity/tx_item.dart +++ b/mobile-app/lib/v2/screens/activity/tx_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -29,7 +30,7 @@ class TxItemData { required this.counterpartyAddr, }); - factory TxItemData.from(TransactionEvent tx, String accountId, AppColorsV2 colors) { + factory TxItemData.from(TransactionEvent tx, String accountId, AppColorsV2 colors, AppLocalizations l10n) { final isSend = tx.from == accountId; final isPending = tx is PendingTransactionEvent; final isScheduled = tx.isReversibleScheduled; @@ -37,32 +38,32 @@ class TxItemData { String getLabel() { if (isPending && isSend) { - return 'Sending'; + return l10n.activityTxSending; } if (isPending && !isSend) { - return 'Receiving'; + return l10n.activityTxReceiving; } if (isScheduled && isSend) { - return 'Pending'; + return l10n.activityTxPending; } if (isScheduled && !isSend) { - return 'Receiving'; + return l10n.activityTxReceiving; } if (isSend && !isScheduled) { - return 'Sent'; + return l10n.activityTxSent; } - return 'Received'; + return l10n.activityTxReceived; } String getTimeLabel() { if (isPending) { - return 'now'; + return l10n.activityTxTimeNow; } if (isScheduled) { - return _formatDuration(tx.timeRemaining); + return _formatDuration(tx.timeRemaining, l10n); } - return _timeAgo(tx.timestamp); + return _timeAgo(tx.timestamp, l10n); } Color getIconBg() { @@ -137,11 +138,14 @@ Widget buildTxItem( TransactionEvent tx, TxItemData data, AppColorsV2 colors, - AppTextTheme text, { + AppTextTheme text, + AppLocalizations l10n, { required String formattedAmount, required bool isLastItem, VoidCallback? onTap, }) { + final directionLabel = data.isSend ? l10n.activityTxTo : l10n.activityTxFrom; + return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, @@ -175,7 +179,6 @@ Widget buildTxItem( ], ), ), - Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -188,7 +191,7 @@ Widget buildTxItem( ), const SizedBox(height: 2), Text( - '${data.isSend ? "To" : "From"}: ${data.counterpartyAddr}', + '$directionLabel: ${data.counterpartyAddr}', style: text.detail?.copyWith(color: colors.textTertiary), ), ], @@ -202,28 +205,27 @@ Widget buildTxItem( ); } -String _formatDuration(Duration d) { - final days = d.inDays; - final hours = d.inHours % 24; - final mins = d.inMinutes % 60; - return '${days.toString().padLeft(2, '0')}d:${hours.toString().padLeft(2, '0')}h:${mins.toString().padLeft(2, '0')}m'; +String _formatDuration(Duration d, AppLocalizations l10n) { + final days = d.inDays.toString().padLeft(2, '0'); + final hours = (d.inHours % 24).toString().padLeft(2, '0'); + final mins = (d.inMinutes % 60).toString().padLeft(2, '0'); + return l10n.activityTxTimeRemaining(days, hours, mins); } -String _timeAgo(DateTime timestamp) { +String _timeAgo(DateTime timestamp, AppLocalizations l10n) { final diff = DateTime.now().difference(timestamp); - if (diff.inMinutes < 1) return 'now'; - if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; - if (diff.inHours < 24) return '${diff.inHours}h ago'; - return '${diff.inDays}d ago'; + if (diff.inMinutes < 1) return l10n.activityTxTimeNow; + if (diff.inMinutes < 60) return l10n.activityTxTimeMinutesAgo(diff.inMinutes); + if (diff.inHours < 24) return l10n.activityTxTimeHoursAgo(diff.inHours); + return l10n.activityTxTimeDaysAgo(diff.inDays); } -String dateGroupLabel(DateTime date) { +String dateGroupLabel(DateTime date, AppLocalizations l10n, String localeName) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final txDay = DateTime(date.year, date.month, date.day); final diff = today.difference(txDay).inDays; - if (diff == 0) return 'Today'; - if (diff == 1) return 'Yesterday'; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - return '${months[date.month - 1]} ${date.day}, ${date.year}'; + if (diff == 0) return l10n.activityDateToday; + if (diff == 1) return l10n.activityDateYesterday; + return DatetimeFormattingService.formatDateGroupLabel(date, localeName); } diff --git a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart index 3530efb0..c9ca2f3c 100644 --- a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart +++ b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/local_auth_provider.dart'; import 'package:resonance_network_wallet/v2/components/base_background.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -34,6 +35,8 @@ class AuthWrapper extends ConsumerWidget { } Widget _buildLockScreen(BuildContext context, WidgetRef ref, bool isAuthenticating) { + final l10n = ref.watch(l10nProvider); + return Scaffold( backgroundColor: context.colors.background, body: BaseBackground( @@ -45,7 +48,7 @@ class AuthWrapper extends ConsumerWidget { alignment: Alignment.center, children: [ Image.asset('assets/v2/auth_wrapper_bracket.png'), - Text('Authorization \n Required', style: context.themeText.lockTitle, textAlign: TextAlign.center), + Text(l10n.authAuthorizationRequired, style: context.themeText.lockTitle, textAlign: TextAlign.center), ], ), const SizedBox(height: 60), @@ -55,7 +58,7 @@ class AuthWrapper extends ConsumerWidget { Padding( padding: EdgeInsets.symmetric(horizontal: context.themeSize.screenPadding), child: QuantusButton.simple( - label: 'Unlock Wallet', + label: l10n.authUnlockWallet, onTap: () { ref.read(localAuthProvider.notifier).authenticate(); }, @@ -64,7 +67,7 @@ class AuthWrapper extends ConsumerWidget { ), const SizedBox(height: 40), Text( - isAuthenticating ? 'Authenticating...' : 'Use device biometrics to unlock', + isAuthenticating ? l10n.authAuthenticating : l10n.authUseDeviceBiometricsToUnlock, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), textAlign: TextAlign.center, ), diff --git a/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart b/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart index c05cb6e4..f811d769 100644 --- a/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart +++ b/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/services/wallet_creation_service.dart'; @@ -51,7 +52,8 @@ class _NewWalletRecoveryPhraseScreenState extends ConsumerState false, ); } catch (e) { - if (mounted) context.showErrorToaster(message: 'Error saving wallet: $e'); + if (mounted) { + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.createWalletRecoveryPhraseSaveError(e.toString())); + } } finally { if (mounted) setState(() => _isSubmitting = false); } @@ -105,10 +110,12 @@ class _NewWalletRecoveryPhraseScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final data = const SettingsCautionScaffoldData.recoveryPhrase(); + final l10n = ref.watch(l10nProvider); + final data = SettingsCautionScaffoldData( + headline: l10n.createWalletCautionHeadline, + bulletItems: [l10n.createWalletCautionBullet1, l10n.createWalletCautionBullet2, l10n.createWalletCautionBullet3], + checkboxLabel: l10n.createWalletCautionCheckboxLabel, + ); return SettingsCautionScaffold( - appBarTitle: 'Create Wallet', + appBarTitle: l10n.createWalletAppBarTitle, data: data, + continueLabel: l10n.commonContinue, checkboxChecked: _acknowledged, onCheckboxChanged: () => setState(() => _acknowledged = !_acknowledged), onContinue: _continue, diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 6cf1d986..43a9dc47 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/models/combined_transactions_list.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; import 'package:resonance_network_wallet/v2/screens/activity/activity_screen.dart'; @@ -27,6 +29,7 @@ class ActivitySection extends ConsumerStatefulWidget { class _ActivitySectionState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final formatTxAmount = ref.watch(txAmountDisplayProvider); final colors = context.colors; final text = context.themeText; @@ -44,18 +47,22 @@ class _ActivitySectionState extends ConsumerState { if (all.isEmpty) { return Column( - children: [const SizedBox(height: 40), _header(colors, text, context), _emptyState(text, colors)], + children: [ + const SizedBox(height: 40), + _header(colors, text, context, l10n), + _emptyState(text, colors, l10n), + ], ); } return Column( children: [ const SizedBox(height: 40), - _header(colors, text, context), + _header(colors, text, context, l10n), const SizedBox(height: 28), ...recentTransactions.mapIndexed((index, tx) { - final data = TxItemData.from(tx, widget.activeAccount.accountId, colors); + final data = TxItemData.from(tx, widget.activeAccount.accountId, colors, l10n); final isLastItem = index == recentTransactions.length - 1; return buildTxItem( @@ -63,6 +70,7 @@ class _ActivitySectionState extends ConsumerState { data, colors, text, + l10n, formattedAmount: formatTxAmount(data.amount, isSend: data.isSend).primaryAmount, isLastItem: isLastItem, onTap: () { @@ -78,7 +86,7 @@ class _ActivitySectionState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _header(colors, text, context), + _header(colors, text, context, l10n), const SizedBox(height: 24), for (var i = 0; i < 3; i++) ...[ const TxItemSkeleton(), @@ -91,7 +99,7 @@ class _ActivitySectionState extends ConsumerState { padding: const EdgeInsets.only(top: 40), child: Column( children: [ - Text('Error loading transactions', style: text.detail?.copyWith(color: colors.textError)), + Text(l10n.homeActivityErrorLoading, style: text.detail?.copyWith(color: colors.textError)), const SizedBox(height: 12), GestureDetector( behavior: HitTestBehavior.opaque, @@ -102,7 +110,7 @@ class _ActivitySectionState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Text( - 'Retry', + l10n.homeActivityRetry, style: text.smallParagraph?.copyWith(color: colors.textPrimary, decoration: TextDecoration.underline), ), ), @@ -113,21 +121,21 @@ class _ActivitySectionState extends ConsumerState { ); } - Widget _emptyState(AppTextTheme text, AppColorsV2 colors) { + Widget _emptyState(AppTextTheme text, AppColorsV2 colors, AppLocalizations l10n) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 24), child: Column( children: [ Text( - 'No Transactions Yet', + l10n.homeActivityEmptyTitle, style: text.mediumTitle?.copyWith(color: colors.textMuted, fontWeight: FontWeight.w400), ), const SizedBox(height: 8), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 240), child: Text( - 'Your activity will appear here once you send or receive QUAN.', + l10n.homeActivityEmptyMessage, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault), ), @@ -137,15 +145,15 @@ class _ActivitySectionState extends ConsumerState { ); } - Widget _header(AppColorsV2 colors, AppTextTheme text, BuildContext context) { + Widget _header(AppColorsV2 colors, AppTextTheme text, BuildContext context, AppLocalizations l10n) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Activity', style: text.smallTitle), + Text(l10n.homeActivityTitle, style: text.smallTitle), GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ActivityScreen())), child: Text( - 'View All', + l10n.homeActivityViewAll, style: text.smallTitle?.copyWith( color: colors.textMuted, decoration: TextDecoration.underline, diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index f9cecef1..8d1e64dd 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -24,7 +24,9 @@ import 'package:resonance_network_wallet/v2/screens/pos/pos_amount_screen.dart'; import 'package:resonance_network_wallet/v2/screens/swap/swap_screen.dart'; import 'package:resonance_network_wallet/models/filtered_transactions_params.dart'; import 'package:resonance_network_wallet/providers/account_id_list_cache.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/filtered_all_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; @@ -127,6 +129,7 @@ class _HomeScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final accountAsync = ref.watch(activeAccountProvider); final txAsync = ref.watch(activeAccountTransactionsProvider(TransactionFilter.all)); final colors = context.colors; @@ -136,36 +139,36 @@ class _HomeScreenState extends ConsumerState { loading: () => const ScaffoldBase(mainContent: Center(child: Loader())), error: (e, _) => ScaffoldBase( mainContent: Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.homeError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), ), ), data: (active) { if (active == null) { - return const ScaffoldBase(mainContent: Center(child: Text('No active account'))); + return ScaffoldBase(mainContent: Center(child: Text(l10n.homeNoActiveAccount))); } return ScaffoldBase.refreshable( onRefresh: _refresh, slivers: [ - _buildContent(active, colors, text), + _buildContent(active, colors, text, l10n), ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh), const SizedBox(height: 58), ], - bottomContent: _buildBottomContent(), + bottomContent: _buildBottomContent(l10n), ); }, ); } - Widget _buildContent(DisplayAccount active, AppColorsV2 colors, AppTextTheme text) { + Widget _buildContent(DisplayAccount active, AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), _buildTopBar(), const SizedBox(height: 40), - _buildBalance(colors, text), + _buildBalance(colors, text, l10n), const SizedBox(height: 40), - if (active is RegularAccount) ...[_buildActionButtons(), const SizedBox(height: 40)], + if (active is RegularAccount) ...[_buildActionButtons(l10n), const SizedBox(height: 40)], DottedBorder( dashLength: 3, gapLength: 5, @@ -176,14 +179,14 @@ class _HomeScreenState extends ConsumerState { ); } - Widget? _buildBottomContent() { + Widget? _buildBottomContent(AppLocalizations l10n) { final enablePos = ref.watch(posModeProvider); final balanceAsync = ref.watch(balanceProvider); if (enablePos) { return ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Charge', + label: l10n.homeCharge, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())), ), ); @@ -194,7 +197,7 @@ class _HomeScreenState extends ConsumerState { (balance) => balance == BigInt.zero ? ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Get Testnet Tokens ↗', + label: l10n.homeGetTestnetTokens, onTap: () => launchXPost(AppConstants.faucetUrl), ), ) @@ -232,7 +235,7 @@ class _HomeScreenState extends ConsumerState { ); } - Widget _buildBalance(AppColorsV2 colors, AppTextTheme text) { + Widget _buildBalance(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final currencyAsync = ref.watch(balanceDisplayProvider); return Column( @@ -256,30 +259,30 @@ class _HomeScreenState extends ConsumerState { Row(mainAxisAlignment: MainAxisAlignment.center, children: [Skeleton(width: 100, height: 18)]), ], ), - error: (_, _) => Text('Error loading balance', style: text.detail?.copyWith(color: colors.textError)), + error: (_, _) => Text(l10n.homeErrorLoadingBalance, style: text.detail?.copyWith(color: colors.textError)), ), ], ); } - Widget _buildActionButtons() { + Widget _buildActionButtons(AppLocalizations l10n) { final enableSwap = ref.watch(remoteConfigProvider).enableSwap; final receiveCard = _actionCard( iconAsset: 'assets/v2/action_receive.svg', - label: 'Receive', + label: l10n.homeReceive, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ReceiveScreen())), ); final sendCard = _actionCard( iconAsset: 'assets/v2/action_send.svg', - label: 'Send', + label: l10n.homeSend, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SelectRecipientScreen())), ); final swapCard = _actionCard( iconAsset: 'assets/v2/action_swap.svg', - label: 'Swap', + label: l10n.homeSwap, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())), ); diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index b8499ca5..55ae66f6 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; +import 'package:resonance_network_wallet/services/telemetry_service.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; @@ -64,7 +66,7 @@ class _ImportWalletScreenV2State extends ConsumerState { if (!mnemonic.startsWith('//')) { final words = mnemonic.split(' ').where((w) => w.isNotEmpty).toList(); if (words.length != 12 && words.length != 24) { - throw Exception('Recovery phrase must be 12 or 24 words'); + throw Exception(ref.read(l10nProvider).importWalletValidationError); } } @@ -113,27 +115,28 @@ class _ImportWalletScreenV2State extends ConsumerState { } ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); - } catch (_) {} + } catch (e, st) { + print('error discovering accounts: $e'); + TelemetryService().sendError('Error discovering accounts', error: e, stackTrace: st); + } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final fieldTextStyle = text.smallTitle?.copyWith(color: colors.checksum, fontWeight: FontWeight.w400); return ScaffoldBase( - appBar: const V2AppBar(title: 'Import Wallet'), + appBar: V2AppBar(title: l10n.importWalletAppBarTitle), mainContent: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), behavior: HitTestBehavior.opaque, child: SingleChildScrollView( child: Column( children: [ - Text( - 'Restore an existing wallet with your 12 or 24 words recovery phrase', - style: text.smallParagraph?.copyWith(color: colors.textSecondary), - ), + Text(l10n.importWalletDescription, style: text.smallParagraph?.copyWith(color: colors.textSecondary)), const SizedBox(height: 16), Container( height: 202, @@ -149,7 +152,7 @@ class _ImportWalletScreenV2State extends ConsumerState { onChanged: (_) => setState(() {}), style: fieldTextStyle, decoration: InputDecoration.collapsed( - hintText: 'Type in or paste your recovery phrase. Separate words with spaces.', + hintText: l10n.importWalletHint, hintStyle: fieldTextStyle?.copyWith(color: colors.textSecondary), ), maxLines: null, @@ -172,7 +175,7 @@ class _ImportWalletScreenV2State extends ConsumerState { bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( key: _buttonKey, - label: 'Import', + label: l10n.importWalletButton, onTap: _import, isLoading: _isLoading, isDisabled: !_hasInput, diff --git a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart index 09bbe2d5..26fba761 100644 --- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/models/fiat_currency.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/utils/amount_input_logic.dart'; @@ -47,7 +49,7 @@ class _PosAmountScreenState extends ConsumerState { setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped)); } on InvalidNumberInputException catch (e, stack) { debugPrint('Amount parse failed: $e\n$stack'); - context.showErrorToaster(message: 'Please enter a valid amount'); + context.showErrorToaster(message: ref.read(l10nProvider).sendInputAmountInvalidAmount); return; } } @@ -73,6 +75,7 @@ class _PosAmountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final primaryAmount = ref @@ -80,9 +83,9 @@ class _PosAmountScreenState extends ConsumerState { .primaryAmount; return ScaffoldBase( - appBar: const V2AppBar(title: 'New Charge'), + appBar: V2AppBar(title: l10n.posAmountTitle), mainContent: _amountCenter(colors, text), - bottomContent: _bottomContent(colors, text, primaryAmount), + bottomContent: _bottomContent(l10n, primaryAmount), ); } @@ -166,8 +169,8 @@ class _PosAmountScreenState extends ConsumerState { ); } - Widget _bottomContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) { - final label = _amount > BigInt.zero ? 'Charge $amountDisplay' : 'Enter Amount'; + Widget _bottomContent(AppLocalizations l10n, String amountDisplay) { + final label = _amount > BigInt.zero ? l10n.posAmountCharge(amountDisplay) : l10n.posAmountEnterAmount; return ScaffoldBaseBottomContent( child: QuantusButton.simple(label: label, onTap: _onCharge, isDisabled: !_isValid), diff --git a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart index 3f5023df..6b94e35a 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -3,13 +3,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/pending_transaction_polling_service.dart'; import 'package:resonance_network_wallet/services/pos_service.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; +import 'package:resonance_network_wallet/shared/utils/print.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart'; @@ -50,12 +53,13 @@ class _PosQrScreenState extends ConsumerState { } void _startWatching() { + final l10n = ref.read(l10nProvider); final active = ref.read(activeAccountProvider).value; if (active == null) return; if (widget.amountPlanck <= BigInt.zero) { - print('[PosQr] ERROR: invalid amount planck ${widget.amountPlanck}'); - if (mounted) setState(() => _watchError = 'Invalid amount. Tap to retry.'); + quantusDebugPrint('[PosQr] ERROR: invalid amount planck ${widget.amountPlanck}'); + if (mounted) setState(() => _watchError = l10n.posQrInvalidAmount); return; } @@ -64,15 +68,15 @@ class _PosQrScreenState extends ConsumerState { _watchError = null; }); - print('[PosQr] watching address=${active.account.accountId} expected=${widget.amountPlanck} planck'); + quantusDebugPrint('[PosQr] watching address=${active.account.accountId} expected=${widget.amountPlanck} planck'); _txWatch.watch( address: active.account.accountId, onTransfer: (tx) { - print('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); + quantusDebugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); if (_isPaid) return; final received = BigInt.tryParse(tx.amount); if (received != widget.amountPlanck) { - print('[PosQr] amount mismatch (received=$received expected=${widget.amountPlanck}), ignoring'); + quantusDebugPrint('[PosQr] amount mismatch (received=$received expected=${widget.amountPlanck}), ignoring'); return; } @@ -99,12 +103,13 @@ class _PosQrScreenState extends ConsumerState { } }, onError: (e) { + quantusDebugPrint('[PosQr] watch error: $e'); _txWatch.dispose(); _timeoutTimer?.cancel(); if (mounted) { setState(() { _watching = false; - _watchError = 'Connection lost. Tap to retry.'; + _watchError = ref.read(l10nProvider).posQrConnectionLost; }); } }, @@ -115,7 +120,7 @@ class _PosQrScreenState extends ConsumerState { if (mounted) { setState(() { _watching = false; - _watchError = 'Timed out. Tap to retry.'; + _watchError = ref.read(l10nProvider).posQrTimedOut; }); } }); @@ -156,6 +161,8 @@ class _PosQrScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -168,42 +175,47 @@ class _PosQrScreenState extends ConsumerState { ); return ScaffoldBase( - appBar: V2AppBar(title: _isPaid ? 'Payment Received' : 'Scan to Pay'), + appBar: V2AppBar(title: _isPaid ? l10n.posQrTitlePaymentReceived : l10n.posQrTitleScanToPay), mainContent: accountAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.posQrError('$e'), style: text.detail?.copyWith(color: colors.textError)), ), data: (active) { - if (active == null) return const Center(child: Text('No active account')); + if (active == null) return Center(child: Text(l10n.posQrNoActiveAccount)); _request ??= PosService( formattingService: formattingService, ).createPaymentRequest(accountId: active.account.accountId, amountPlanck: widget.amountPlanck); - if (_isPaid) return _buildPaidContent(colors, text, display.primaryAmount); - return _buildQrContent(_request!, colors, text, display); + if (_isPaid) _buildPaidContent(l10n, appLocale.numberFormatLocale, colors, text, display.primaryAmount); + return _buildQrContent(l10n, _request!, colors, text, display); }, ), - bottomContent: ScaffoldBaseBottomContent(child: _isPaid ? _buildPaidButtons() : _buildQrButton()), + bottomContent: ScaffoldBaseBottomContent(child: _isPaid ? _buildPaidButtons(l10n) : _buildQrButton(l10n)), ); } - Widget _buildQrButton() { - return QuantusButton.simple(label: 'New Charge', onTap: _newCharge, variant: ButtonVariant.primary); + Widget _buildQrButton(AppLocalizations l10n) { + return QuantusButton.simple(label: l10n.posQrNewCharge, onTap: _newCharge, variant: ButtonVariant.primary); } - Widget _buildPaidButtons() { + Widget _buildPaidButtons(AppLocalizations l10n) { final padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 20); return Row( spacing: 16, children: [ Expanded( - child: QuantusButton.simple(padding: padding, label: 'Done', onTap: _done, variant: ButtonVariant.secondary), + child: QuantusButton.simple( + padding: padding, + label: l10n.posQrDone, + onTap: _done, + variant: ButtonVariant.secondary, + ), ), Expanded( child: QuantusButton.simple( padding: padding, - label: 'New Charge', + label: l10n.posQrNewCharge, onTap: _newCharge, variant: ButtonVariant.primary, ), @@ -212,7 +224,13 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildPaidContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) { + Widget _buildPaidContent( + AppLocalizations l10n, + String localeName, + AppColorsV2 colors, + AppTextTheme text, + String amountDisplay, + ) { final transfer = _paidTransfer!; final formattedAddress = AddressFormattingService.formatAddress(transfer.from.trim()); @@ -223,21 +241,21 @@ class _PosQrScreenState extends ConsumerState { _buildSuccessCircle(colors), const SizedBox(height: 32), Text( - '$amountDisplay received', + l10n.posQrAmountReceived(amountDisplay), style: text.smallTitle?.copyWith(color: colors.textLightGray, fontSize: 32, fontWeight: FontWeight.w400), textAlign: TextAlign.center, ), const SizedBox(height: 4), if (_paidAt != null) Text( - _formatPaidAt(_paidAt!), + _formatPaidAt(_paidAt!, localeName, l10n), style: text.smallParagraph?.copyWith(color: colors.textTertiary, letterSpacing: 0.7), textAlign: TextAlign.center, ), const SizedBox(height: 32), - _buildFromSection(colors, text, formattedAddress), + _buildFromSection(l10n, colors, text, formattedAddress), const Spacer(), - _buildExplorerLink(colors, text), + _buildExplorerLink(l10n, colors, text), const SizedBox(height: 16), ], ); @@ -255,12 +273,12 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildFromSection(AppColorsV2 colors, AppTextTheme text, String formattedAddress) { + Widget _buildFromSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text, String formattedAddress) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'From:', + l10n.posQrFrom, style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), @@ -292,7 +310,7 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildExplorerLink(AppColorsV2 colors, AppTextTheme text) { + Widget _buildExplorerLink(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return GestureDetector( onTap: _openExplorer, child: Container( @@ -300,12 +318,13 @@ class _PosQrScreenState extends ConsumerState { border: Border(bottom: BorderSide(color: colors.textTertiary, width: 1)), ), padding: const EdgeInsets.only(bottom: 3), - child: Text('View in Explorer ↗', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + child: Text(l10n.activityDetailViewExplorer, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), ), ); } Widget _buildQrContent( + AppLocalizations l10n, PosPaymentRequest request, AppColorsV2 colors, AppTextTheme text, @@ -317,8 +336,8 @@ class _PosQrScreenState extends ConsumerState { const SizedBox(height: 16), QuantusQr(accountId: request.paymentUrl), const Spacer(), - if (!_watching && _watchError != null) _buildErrorSection(colors, text), - if (_watching) _buildWaitingPill(colors, text), + if (!_watching && _watchError != null) _buildErrorSection(l10n, colors, text), + if (_watching) _buildWaitingPill(l10n, colors, text), const SizedBox(height: 16), ], ); @@ -352,7 +371,7 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildWaitingPill(AppColorsV2 colors, AppTextTheme text) { + Widget _buildWaitingPill(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 9), decoration: BoxDecoration( @@ -365,19 +384,19 @@ class _PosQrScreenState extends ConsumerState { children: [ Loader(size: 14, color: colors.textMuted), const SizedBox(width: 9), - Text('Waiting for payment', style: text.detail?.copyWith(color: colors.textMuted)), + Text(l10n.posQrWaitingForPayment, style: text.detail?.copyWith(color: colors.textMuted)), ], ), ); } - Widget _buildErrorSection(AppColorsV2 colors, AppTextTheme text) { + Widget _buildErrorSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ - Text('Network Error', style: text.detail?.copyWith(color: colors.textError)), + Text(l10n.posQrNetworkError, style: text.detail?.copyWith(color: colors.textError)), const SizedBox(height: 8), QuantusButton.simple( - label: 'Try Again', + label: l10n.posQrTryAgain, padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 8), onTap: _startWatching, variant: ButtonVariant.secondary, @@ -386,28 +405,8 @@ class _PosQrScreenState extends ConsumerState { ); } - String _formatPaidAt(DateTime dt) { - final hour = dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour); - final minute = dt.minute.toString().padLeft(2, '0'); - final ampm = dt.hour >= 12 ? 'pm' : 'am'; - final ordinal = _ordinalSuffix(dt.day); - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - final month = months[dt.month - 1]; - final year = dt.year.toString().substring(2); - return "At $hour:$minute$ampm, ${dt.day}$ordinal $month'$year"; - } - - String _ordinalSuffix(int day) { - if (day >= 11 && day <= 13) return 'th'; - switch (day % 10) { - case 1: - return 'st'; - case 2: - return 'nd'; - case 3: - return 'rd'; - default: - return 'th'; - } + String _formatPaidAt(DateTime dt, String localeName, AppLocalizations l10n) { + final dateTime = DatetimeFormattingService.formatPaidAt(dt, localeName); + return l10n.posQrPaidAt(dateTime); } } diff --git a/mobile-app/lib/v2/screens/receive/receive_screen.dart b/mobile-app/lib/v2/screens/receive/receive_screen.dart index 6403a8d6..4bf175a3 100644 --- a/mobile-app/lib/v2/screens/receive/receive_screen.dart +++ b/mobile-app/lib/v2/screens/receive/receive_screen.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/components/address_details_card.dart'; @@ -53,7 +55,8 @@ class _ReceiveScreenState extends ConsumerState { debugPrint('Error loading account data: $e'); if (mounted) { - context.showErrorToaster(message: 'Error loading account data: $e'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.receiveErrorLoadingAccount('$e')); } } } @@ -65,23 +68,25 @@ class _ReceiveScreenState extends ConsumerState { } void _copyAccountDetails(BuildContext context) { + final l10n = ref.read(l10nProvider); context.copyTextWithToaster( - 'Account Id:\n$_accountId\n\nCheckphrase:\n$_checksum', - message: 'Account details copied to clipboard', + l10n.receiveClipboardContent(_accountId!, _checksum!), + message: l10n.receiveCopiedMessage, ); } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final tabs = [ - const SegmentedControlItem(label: 'QR Code', value: ReceiveTab.qrCode), - const SegmentedControlItem(label: 'Address', value: ReceiveTab.address), + SegmentedControlItem(label: l10n.receiveTabQrCode, value: ReceiveTab.qrCode), + SegmentedControlItem(label: l10n.receiveTabAddress, value: ReceiveTab.address), ]; final isLoading = _accountId == null || _checksum == null; return ScaffoldBase( - appBar: const V2AppBar(title: 'Receive'), + appBar: V2AppBar(title: l10n.receiveTitle), mainContent: Column( children: [ SegmentedControls( @@ -102,11 +107,11 @@ class _ReceiveScreenState extends ConsumerState { AddressTab(accountId: _accountId!, checksum: _checksum!), ], ), - bottomContent: _buildBottomContent(isLoading, _selectedTab), + bottomContent: _buildBottomContent(l10n, isLoading, _selectedTab), ); } - Widget? _buildBottomContent(bool isLoading, ReceiveTab selectedTab) { + Widget? _buildBottomContent(AppLocalizations l10n, bool isLoading, ReceiveTab selectedTab) { Widget content; if (isLoading) { @@ -120,7 +125,7 @@ class _ReceiveScreenState extends ConsumerState { children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.receiveCopy, onTap: () => _copyAccountDetails(context), isDisabled: isLoading, icon: Icon(Icons.copy, size: 20, color: context.colors.textPrimary), diff --git a/mobile-app/lib/v2/screens/send/input_amount_screen.dart b/mobile-app/lib/v2/screens/send/input_amount_screen.dart index a4a72255..d87df17e 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/models/fiat_currency.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -123,7 +125,8 @@ class _InputAmountScreenState extends ConsumerState { setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped)); } on InvalidNumberInputException catch (e, stack) { debugPrint('Amount parse failed: $e\n$stack'); - context.showErrorToaster(message: 'Please enter a valid amount'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.sendInputAmountInvalidAmount); return; } if (_amount > BigInt.zero) _feeDebouncer.run(_fetchFee); @@ -204,7 +207,8 @@ class _InputAmountScreenState extends ConsumerState { Future _openReview() async { if (_recipientChecksum == null) { - context.showErrorToaster(message: 'Recipient checksum is required'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.sendInputAmountChecksumRequired); return; } @@ -226,6 +230,7 @@ class _InputAmountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; @@ -245,6 +250,7 @@ class _InputAmountScreenState extends ConsumerState { activeAccountId: activeId, ); final btnText = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: amountStatus, recipientText: recipient, @@ -254,7 +260,7 @@ class _InputAmountScreenState extends ConsumerState { ); return ScaffoldBase( - appBar: V2AppBar(title: widget.isPayMode ? 'Pay' : 'Send'), + appBar: V2AppBar(title: widget.isPayMode ? l10n.sendPayTitle : l10n.sendTitle), mainContent: LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( controller: _scrollController, @@ -264,7 +270,7 @@ class _InputAmountScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _recipientCard(colors, text), + _recipientCard(colors, text, l10n), const SizedBox(height: 32), _amountCenter(colors, text), const SizedBox(height: 32), @@ -274,11 +280,11 @@ class _InputAmountScreenState extends ConsumerState { ), ), ), - bottomContent: _bottomSection(colors, text, btnText, balance, btnDisabled), + bottomContent: _bottomSection(colors, text, l10n, btnText, balance, btnDisabled), ); } - Widget _recipientCard(AppColorsV2 colors, AppTextTheme text) { + Widget _recipientCard(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final addr = widget.recipientAddress.trim(); final shortAddr = AddressFormattingService.formatAddress(addr); @@ -292,7 +298,10 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('SEND TO', style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel)), + Text( + l10n.sendInputAmountSendTo, + style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel), + ), const SizedBox(height: 16), if (_recipientChecksum != null) ...[ Text( @@ -426,6 +435,7 @@ class _InputAmountScreenState extends ConsumerState { Widget _bottomSection( AppColorsV2 colors, AppTextTheme text, + AppLocalizations l10n, String btnText, AsyncValue balance, bool btnDisabled, @@ -448,11 +458,14 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Available Balance:', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text( + l10n.sendInputAmountAvailableBalance, + style: text.smallParagraph?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 4), balance.when( data: (b) => Text( - '${formattingService.formatBalance(b)} ${AppConstants.tokenSymbol}', + l10n.commonAmountBalance(formattingService.formatBalance(b), AppConstants.tokenSymbol), style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), loading: () => Text('...', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), @@ -465,11 +478,17 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('Network Fee:', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text( + l10n.sendInputAmountNetworkFee, + style: text.smallParagraph?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 4), if (!_isFetchingFee) Text( - '${formattingService.formatBalance(_networkFee, maxDecimals: 5)} ${AppConstants.tokenSymbol}', + l10n.commonAmountBalance( + formattingService.formatBalance(_networkFee, maxDecimals: 5), + AppConstants.tokenSymbol, + ), style: text.smallParagraph?.copyWith(color: colors.textTertiary), ) else @@ -482,7 +501,7 @@ class _InputAmountScreenState extends ConsumerState { const SizedBox(height: 4), IntrinsicWidth( child: QuantusButton.simple( - label: 'Max', + label: l10n.sendInputAmountMax, onTap: _setMax, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), variant: ButtonVariant.transparent, diff --git a/mobile-app/lib/v2/screens/send/review_send_screen.dart b/mobile-app/lib/v2/screens/send/review_send_screen.dart index 4b40fc5f..5fe82048 100644 --- a/mobile-app/lib/v2/screens/send/review_send_screen.dart +++ b/mobile-app/lib/v2/screens/send/review_send_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/local_auth_service.dart'; @@ -55,11 +57,12 @@ class _ReviewSendScreenState extends ConsumerState { _errorMessage = null; }); - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to confirm transaction'); + final l10n = ref.read(l10nProvider); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.sendReviewAuthReason); if (!authed || !mounted) { setState(() { _submitting = false; - _errorMessage = 'Authentication required to send'; + _errorMessage = l10n.sendReviewAuthRequired; }); return; } @@ -104,7 +107,7 @@ class _ReviewSendScreenState extends ConsumerState { if (mounted) { setState(() { _submitting = false; - _errorMessage = 'Failed submitting transaction'; + _errorMessage = ref.read(l10nProvider).sendReviewSubmitFailed; }); } } @@ -112,6 +115,7 @@ class _ReviewSendScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; @@ -126,16 +130,16 @@ class _ReviewSendScreenState extends ConsumerState { final totalRaw = widget.amount + widget.networkFee; return ScaffoldBase( - appBar: V2AppBar(title: widget.isPayMode ? 'Pay' : 'Send'), + appBar: V2AppBar(title: widget.isPayMode ? l10n.sendPayTitle : l10n.sendTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _heroCard(colors, text, approxDisplay), + _heroCard(colors, text, l10n, approxDisplay), const SizedBox(height: 28), - _summarySection(addr, totalRaw), + _summarySection(l10n, addr, totalRaw), if (_errorMessage != null) ...[ const SizedBox(height: 16), Text(_errorMessage!, style: text.detail?.copyWith(color: colors.textError)), @@ -146,7 +150,7 @@ class _ReviewSendScreenState extends ConsumerState { ), bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Confirm', + label: l10n.sendReviewConfirm, variant: ButtonVariant.primary, isLoading: _submitting, isDisabled: _submitting, @@ -156,14 +160,14 @@ class _ReviewSendScreenState extends ConsumerState { ); } - Widget _heroCard(AppColorsV2 colors, AppTextTheme text, CurrencyDisplayState approxDisplay) { + Widget _heroCard(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n, CurrencyDisplayState approxDisplay) { final sectionLabelStyle = text.receiveLabel?.copyWith(color: colors.textLabel); return SplitCard( topChild: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('SENDING', style: sectionLabelStyle), + Text(l10n.sendReviewSending, style: sectionLabelStyle), const SizedBox(height: 16), AmountDisplayWithConversion( amountDisplay: approxDisplay, @@ -175,7 +179,7 @@ class _ReviewSendScreenState extends ConsumerState { bottomChild: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('TO', style: sectionLabelStyle), + Text(l10n.sendReviewTo, style: sectionLabelStyle), const SizedBox(height: 16), AddressCheckphraseWithInitial( recipientChecksum: widget.recipientChecksum, @@ -186,7 +190,7 @@ class _ReviewSendScreenState extends ConsumerState { ); } - Widget _summarySection(String addr, BigInt totalRaw) { + Widget _summarySection(AppLocalizations l10n, String addr, BigInt totalRaw) { final shownDecimals = AppConstants.decimals; final shortAddr = AddressFormattingService.formatAddress(addr); final formattingService = ref.watch(numberFormattingServiceProvider); @@ -195,23 +199,30 @@ class _ReviewSendScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 7), - _summaryRow(label: 'TO', value: shortAddr), + _summaryRow(label: l10n.sendReviewTo, value: shortAddr), const SizedBox(height: 7), _summaryRow( - label: 'AMOUNT', - value: - '${formattingService.formatBalance(widget.amount, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewAmount, + value: l10n.commonAmountBalance( + formattingService.formatBalance(widget.amount, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), _summaryRow( - label: 'NETWORK FEE', - value: - '${formattingService.formatBalance(widget.networkFee, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewNetworkFee, + value: l10n.commonAmountBalance( + formattingService.formatBalance(widget.networkFee, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), _summaryRow( - label: 'YOU PAY', - value: '${formattingService.formatBalance(totalRaw, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewYouPay, + value: l10n.commonAmountBalance( + formattingService.formatBalance(totalRaw, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), ], diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index 18f73b63..3d908b5f 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -4,6 +4,8 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/dotted_border.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/routes.dart'; @@ -180,23 +182,24 @@ class _SelectRecipientScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase( - appBar: const V2AppBar(title: 'Send'), + appBar: V2AppBar(title: l10n.sendTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Send To', style: text.sendSectionLabel?.copyWith(color: colors.textPrimary)), + Text(l10n.sendSelectRecipientSendTo, style: text.sendSectionLabel?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), - _buildRecipientField(colors, text), + _buildRecipientField(colors, text, l10n), const SizedBox(height: 28), - _buildScanRow(colors, text), + _buildScanRow(colors, text, l10n), const SizedBox(height: 28), DottedBorder( dashLength: 3, @@ -215,7 +218,10 @@ class _SelectRecipientScreenState extends ConsumerState { const SliverFillRemaining(hasScrollBody: false, child: Center(child: Loader())) else if (_recents.isNotEmpty) ...[ SliverToBoxAdapter( - child: Text('Recents', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + child: Text( + l10n.sendSelectRecipientRecents, + style: text.smallTitle?.copyWith(color: colors.textPrimary), + ), ), const SliverToBoxAdapter(child: SizedBox(height: 32)), SliverList( @@ -243,11 +249,11 @@ class _SelectRecipientScreenState extends ConsumerState { ), ], ), - bottomContent: _buildBottomButton(), + bottomContent: _buildBottomButton(l10n), ); } - Widget _buildRecipientField(AppColorsV2 colors, AppTextTheme text) { + Widget _buildRecipientField(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final hasValid = _recipientController.text.trim().isNotEmpty && !_hasAddressError; return SizedBox( @@ -277,7 +283,9 @@ class _SelectRecipientScreenState extends ConsumerState { textCapitalization: TextCapitalization.none, scrollPadding: const EdgeInsets.only(bottom: 120), style: text.smallParagraph?.copyWith(color: colors.textPrimary), - decoration: const InputDecoration(hintText: 'Search ${AppConstants.tokenSymbol} Address'), + decoration: InputDecoration( + hintText: l10n.sendSelectRecipientSearchHint(AppConstants.tokenSymbol), + ), ), ), ], @@ -318,7 +326,7 @@ class _SelectRecipientScreenState extends ConsumerState { ); } - Widget _buildScanRow(AppColorsV2 colors, AppTextTheme text) { + Widget _buildScanRow(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final iconContainerSize = 44.0; final iconSize = 24.0; @@ -344,10 +352,10 @@ class _SelectRecipientScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Scan QR code', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text(l10n.sendSelectRecipientScanTitle, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 4), Text( - 'Tap to scan a ${AppConstants.tokenSymbol} Address', + l10n.sendSelectRecipientScanSubtitle(AppConstants.tokenSymbol), style: text.detail?.copyWith(color: colors.textTertiary), ), ], @@ -375,8 +383,8 @@ class _SelectRecipientScreenState extends ConsumerState { ); } - Widget _buildBottomButton() { - final btnText = _canContinue ? 'Continue' : 'Enter Address'; + Widget _buildBottomButton(AppLocalizations l10n) { + final btnText = _canContinue ? l10n.sendSelectRecipientContinue : l10n.sendEnterAddress; return ScaffoldBaseBottomContent( child: QuantusButton.simple( diff --git a/mobile-app/lib/v2/screens/send/send_screen_logic.dart b/mobile-app/lib/v2/screens/send/send_screen_logic.dart index 31390e8f..da9a2c59 100644 --- a/mobile-app/lib/v2/screens/send/send_screen_logic.dart +++ b/mobile-app/lib/v2/screens/send/send_screen_logic.dart @@ -1,5 +1,6 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:quantus_sdk/generated/planck/pallets/balances.dart' as balances; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; enum AmountStatus { valid, negative, zero, belowExistential, insufficientBalance } @@ -36,6 +37,7 @@ class SendScreenLogic { } static String getButtonText({ + required AppLocalizations l10n, required bool hasAddressError, required AmountStatus amountStatus, required String recipientText, @@ -43,20 +45,20 @@ class SendScreenLogic { required String activeAccountId, required NumberFormattingService formattingService, }) { - if (hasAddressError || recipientText.isEmpty) return 'Enter Address'; - if (_isSelfTransfer(recipientText, activeAccountId)) return "Can't Self Transfer"; + if (hasAddressError || recipientText.isEmpty) return l10n.sendEnterAddress; + if (_isSelfTransfer(recipientText, activeAccountId)) return l10n.sendLogicCantSelfTransfer; switch (amountStatus) { case AmountStatus.zero: - return 'Enter Amount'; + return l10n.sendLogicEnterAmount; case AmountStatus.negative: - return 'Invalid Amount'; + return l10n.sendLogicInvalidAmount; case AmountStatus.belowExistential: - return 'Below Existential Deposit'; + return l10n.sendLogicBelowExistentialDeposit; case AmountStatus.insufficientBalance: - return 'Insufficient Balance'; + return l10n.sendLogicInsufficientBalance; case AmountStatus.valid: - return 'Review Send'; + return l10n.sendLogicReviewSend; } } diff --git a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart index 298b32f1..911c04bd 100644 --- a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart +++ b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -29,15 +31,17 @@ class TxSubmittedScreen extends ConsumerWidget { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); } - String _headline(WidgetRef ref) { + String _headline(WidgetRef ref, AppLocalizations l10n) { final formattingService = ref.watch(numberFormattingServiceProvider); final n = formattingService.formatBalance(amount, maxDecimals: 4); - final action = isPayMode ? 'paid' : 'sent'; - return '$n ${AppConstants.tokenSymbol} $action'; + return isPayMode + ? l10n.sendTxSubmittedHeadlinePaid(n, AppConstants.tokenSymbol) + : l10n.sendTxSubmittedHeadlineSent(n, AppConstants.tokenSymbol); } @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final addr = recipientAddress.trim(); @@ -51,7 +55,7 @@ class TxSubmittedScreen extends ConsumerWidget { }, child: ScaffoldBase( appBar: V2AppBar( - title: isPayMode ? 'Pay' : 'Send', + title: isPayMode ? l10n.sendPayTitle : l10n.sendTitle, leading: AppBackButton(onTap: () => _popToHome(context)), ), mainContent: Column( @@ -64,13 +68,13 @@ class TxSubmittedScreen extends ConsumerWidget { _successMark(colors), const SizedBox(height: 32), Text( - _headline(ref), + _headline(ref, l10n), textAlign: TextAlign.center, style: text.largeTitle?.copyWith(fontWeight: FontWeight.w400), ), const SizedBox(height: 4), Text( - 'On its way', + l10n.sendTxSubmittedOnItsWay, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.textTertiary, letterSpacing: 0.74), ), @@ -81,7 +85,7 @@ class TxSubmittedScreen extends ConsumerWidget { style: text.paragraph?.copyWith(color: colors.textPrimary), children: [ TextSpan( - text: 'To', + text: l10n.sendTxSubmittedToLabel, style: text.paragraph?.copyWith(fontWeight: FontWeight.w500), ), TextSpan( @@ -116,7 +120,11 @@ class TxSubmittedScreen extends ConsumerWidget { ], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: 'Done', variant: ButtonVariant.primary, onTap: () => _popToHome(context)), + child: QuantusButton.simple( + label: l10n.sendTxSubmittedDone, + variant: ButtonVariant.primary, + onTap: () => _popToHome(context), + ), ), ), ); diff --git a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart index e9c369b0..938f76f8 100644 --- a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart +++ b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -9,20 +12,9 @@ import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_r import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AboutQuantusScreenV2 extends StatelessWidget { +class AboutQuantusScreenV2 extends ConsumerWidget { const AboutQuantusScreenV2({super.key}); - static const _kIntro = - 'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. ' - 'Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'; - - /// [path] is a path under [AppConstants.websiteBaseUrl] (e.g. `/terms`), or empty for the site root. - static const _externalLinks = <({String title, String subtitle, String path})>[ - (title: 'Terms of Service', subtitle: 'quantus.com/terms/', path: '/terms'), - (title: 'Privacy policy', subtitle: 'quantus.com/privacy-policy/', path: '/privacy-policy'), - (title: 'Visit Website', subtitle: 'quantus.com', path: ''), - ]; - static Uri _uriForAboutLink(({String title, String subtitle, String path}) link) { if (link.path.isEmpty) { return Uri.parse(AppConstants.websiteBaseUrl); @@ -30,29 +22,42 @@ class AboutQuantusScreenV2 extends StatelessWidget { return Uri.parse('${AppConstants.websiteBaseUrl}${link.path}'); } + static List<({String title, String subtitle, String path})> _externalLinks(AppLocalizations l10n) { + return [ + (title: l10n.settingsAboutTerms, subtitle: l10n.settingsAboutTermsSubtitle, path: '/terms'), + (title: l10n.settingsAboutPrivacy, subtitle: l10n.settingsAboutPrivacySubtitle, path: '/privacy-policy'), + (title: l10n.settingsAboutWebsite, subtitle: l10n.settingsAboutWebsiteSubtitle, path: ''), + ]; + } + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; + final externalLinks = _externalLinks(l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'About'), + appBar: V2AppBar(title: l10n.settingsAboutScreenTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: ListView( children: [ - Text(_kIntro, style: text.smallParagraph?.copyWith(color: colors.textMuted, height: 1.35)), + Text( + l10n.settingsAboutIntro, + style: text.smallParagraph?.copyWith(color: colors.textMuted, height: 1.35), + ), const SizedBox(height: 40), - for (final entry in _externalLinks.asMap().entries) ...[ + for (final entry in externalLinks.asMap().entries) ...[ SettingsTappableRow( title: entry.value.title, subtitle: entry.value.subtitle, onTap: () => openUrl(_uriForAboutLink(entry.value).toString()), trailing: SettingsTappableRowUtils.externalLink(colors), ), - if (entry.key < _externalLinks.length - 1) const SettingsDivider(), + if (entry.key < externalLinks.length - 1) const SettingsDivider(), ], ], ), @@ -63,7 +68,7 @@ class AboutQuantusScreenV2 extends StatelessWidget { Image.asset('assets/v2/quantus_orange_logo.png', height: 40), const SizedBox(height: 14), Text( - 'Version $appVersion ($appBuildNumber)', + l10n.settingsAboutVersion(appVersion, appBuildNumber), textAlign: TextAlign.center, style: text.paragraph?.copyWith(color: colors.textMuted, fontSize: 16, height: 1.0), ), diff --git a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart index fadbdcf0..637b1961 100644 --- a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart @@ -1,41 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AccountTypeSettingsScreenV2 extends StatelessWidget { +class AccountTypeSettingsScreenV2 extends ConsumerWidget { const AccountTypeSettingsScreenV2({super.key}); - static const _intro = - 'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'; - - static const _upcomingFeatures = <({String title, String subtitle})>[ - (title: 'Reversible Transactions', subtitle: 'Reverse your sends within a time window'), - (title: 'High Security Account', subtitle: 'Guardian approval required'), - (title: 'Multi-Signature', subtitle: 'Multiple approvals required'), - (title: 'Hardware Wallet', subtitle: 'Pair a hardware device'), - ]; + static List<({String title, String subtitle})> _upcomingFeatures(AppLocalizations l10n) { + return [ + (title: l10n.settingsAccountTypeReversibleTitle, subtitle: l10n.settingsAccountTypeReversibleSubtitle), + (title: l10n.settingsAccountTypeHighSecurityTitle, subtitle: l10n.settingsAccountTypeHighSecuritySubtitle), + (title: l10n.settingsAccountTypeMultiSigTitle, subtitle: l10n.settingsAccountTypeMultiSigSubtitle), + (title: l10n.settingsAccountTypeHardwareTitle, subtitle: l10n.settingsAccountTypeHardwareSubtitle), + ]; + } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; + final upcomingFeatures = _upcomingFeatures(l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Type'), + appBar: V2AppBar(title: l10n.settingsAccountTypeScreenTitle), mainContent: ListView( children: [ - Text(_intro, style: text.smallParagraph?.copyWith(color: colors.textMuted)), + Text(l10n.settingsAccountTypeIntro, style: text.smallParagraph?.copyWith(color: colors.textMuted)), const SizedBox(height: 40), - for (var i = 0; i < _upcomingFeatures.length; i++) + for (var i = 0; i < upcomingFeatures.length; i++) _AccountFeatureBlock( colors: colors, text: text, - title: _upcomingFeatures[i].title, - subtitle: _upcomingFeatures[i].subtitle, - showDividerBelow: i < _upcomingFeatures.length - 1, + comingSoonLabel: l10n.settingsAccountTypeComingSoon, + title: upcomingFeatures[i].title, + subtitle: upcomingFeatures[i].subtitle, + showDividerBelow: i < upcomingFeatures.length - 1, ), ], ), @@ -47,6 +52,7 @@ class _AccountFeatureBlock extends StatelessWidget { const _AccountFeatureBlock({ required this.colors, required this.text, + required this.comingSoonLabel, required this.title, required this.subtitle, required this.showDividerBelow, @@ -54,6 +60,7 @@ class _AccountFeatureBlock extends StatelessWidget { final AppColorsV2 colors; final AppTextTheme text; + final String comingSoonLabel; final String title; final String subtitle; final bool showDividerBelow; @@ -80,7 +87,7 @@ class _AccountFeatureBlock extends StatelessWidget { ), ), const SizedBox(width: 24), - _ComingSoonBadge(colors: colors, text: text), + _ComingSoonBadge(colors: colors, text: text, label: comingSoonLabel), ], ), ), @@ -91,10 +98,11 @@ class _AccountFeatureBlock extends StatelessWidget { } class _ComingSoonBadge extends StatelessWidget { - const _ComingSoonBadge({required this.colors, required this.text}); + const _ComingSoonBadge({required this.colors, required this.text, required this.label}); final AppColorsV2 colors; final AppTextTheme text; + final String label; @override Widget build(BuildContext context) { @@ -106,7 +114,7 @@ class _ComingSoonBadge extends StatelessWidget { border: Border.all(color: colors.borderButton), ), alignment: Alignment.center, - child: Text('Coming Soon', style: text.detail?.copyWith(color: colors.textMuted)), + child: Text(label, style: text.detail?.copyWith(color: colors.textMuted)), ); } } diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index 18fdf261..1ac7994e 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -2,176 +2,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/models/fiat_currency.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; -import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; -import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; -import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; -import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_screen.dart'; -class CurrencyPickerScreenV2 extends ConsumerStatefulWidget { +class CurrencyPickerScreenV2 extends ConsumerWidget { const CurrencyPickerScreenV2({super.key}); @override - ConsumerState createState() => _CurrencyPickerScreenV2State(); -} - -class _CurrencyPickerScreenV2State extends ConsumerState { - final _searchController = TextEditingController(); - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - List _filtered(String query) { - final q = query.trim().toLowerCase(); - final list = List.from(FiatCurrency.values); - if (q.isEmpty) return list; - return list.where((c) { - final line = c.line.toLowerCase(); - return line.contains(q) || c.code.toLowerCase().contains(q); - }).toList(); - } - - Future _onSelect(FiatCurrency c) async { - await ref.read(selectedFiatCurrencyProvider.notifier).select(c); - if (mounted) Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { - final colors = context.colors; - final text = context.themeText; + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final selected = ref.watch(selectedFiatCurrencyProvider); - final filtered = _filtered(_searchController.text); - - return ScaffoldBase( - appBar: const V2AppBar(title: 'Currency'), - mainContent: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _SearchField(controller: _searchController, colors: colors, text: text, onChanged: (_) => setState(() {})), - const SizedBox(height: 24), - Expanded( - child: Container( - decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), - clipBehavior: Clip.antiAlias, - child: Scrollbar( - thumbVisibility: true, - thickness: 4, - radius: const Radius.circular(25), - child: filtered.isEmpty - ? Center( - child: Text( - 'No currencies match your search', - style: text.smallParagraph?.copyWith(color: colors.textMuted), - textAlign: TextAlign.center, - ), - ) - : ListView.separated( - itemCount: filtered.length, - separatorBuilder: (context, index) => - const SettingsDivider(style: SettingsDividerStyle.currencyList, padding: EdgeInsets.zero), - itemBuilder: (context, index) { - final c = filtered[index]; - final isSelected = c == selected; - - return _CurrencyListTile( - label: c.line, - selected: isSelected, - colors: colors, - text: text, - onTap: () => _onSelect(c), - ); - }, - ), - ), - ), - ), - const SizedBox(height: 40), - ], - ), - ); - } -} - -class _SearchField extends StatelessWidget { - const _SearchField({required this.controller, required this.colors, required this.text, required this.onChanged}); - - final TextEditingController controller; - final AppColorsV2 colors; - final AppTextTheme text; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 48, - child: Container( - padding: const EdgeInsets.only(left: 12, right: 8), - decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), - child: Row( - children: [ - Icon(Icons.search, size: 18, color: colors.textLabel), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: controller, - onChanged: onChanged, - style: text.smallParagraph, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'Search', - hintStyle: text.smallParagraph?.copyWith(color: colors.textLabel), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _CurrencyListTile extends StatelessWidget { - const _CurrencyListTile({ - required this.label, - required this.selected, - required this.colors, - required this.text, - required this.onTap, - }); - - final String label; - final bool selected; - final AppColorsV2 colors; - final AppTextTheme text; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final accent = colors.accentOrange; - final fg = selected ? accent : colors.textPrimary; - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text(label, style: text.paragraph?.copyWith(color: fg, height: 1.2)), - ), - if (selected) ...[const SizedBox(width: 12), Icon(Icons.check, size: 18, color: accent)], - ], - ), - ), - ), + return SettingsPickerScreen( + title: l10n.settingsCurrencyTitle, + searchHint: l10n.settingsCurrencySearchHint, + emptyMessage: l10n.settingsCurrencyNoMatch, + items: FiatCurrency.values, + selected: selected, + labelBuilder: (currency) => currency.line, + filter: (currency, query) { + final line = currency.line.toLowerCase(); + return line.contains(query) || currency.code.toLowerCase().contains(query); + }, + onSelect: ref.read(selectedFiatCurrencyProvider.notifier).select, + errorMessageBuilder: l10n.settingsCurrencyError, ); } } diff --git a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart index 2dc2c32b..466d2f17 100644 --- a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart +++ b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -7,25 +9,26 @@ import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.da import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class HelpAndSupportScreenV2 extends StatelessWidget { +class HelpAndSupportScreenV2 extends ConsumerWidget { const HelpAndSupportScreenV2({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; return ScaffoldBase( - appBar: const V2AppBar(title: 'Help & Support'), + appBar: V2AppBar(title: l10n.settingsHelpScreenTitle), mainContent: ListView( children: [ _contactBlock( - title: 'Email Support', + title: l10n.settingsHelpEmail, subtitle: AppConstants.emailSupport, colors: colors, onTap: () => openUrl('mailto:${AppConstants.emailSupport}'), ), _contactBlock( - title: 'Telegram', + title: l10n.settingsHelpTelegram, subtitle: AppConstants.telegramHandle, colors: colors, onTap: () => openUrl(AppConstants.communityUrl), diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart new file mode 100644 index 00000000..bf90b3a4 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/models/app_locale.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_screen.dart'; + +class LanguagePickerScreenV2 extends ConsumerWidget { + const LanguagePickerScreenV2({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); + final selected = ref.watch(selectedAppLocaleProvider); + + return SettingsPickerScreen( + title: l10n.settingsLanguageTitle, + searchHint: l10n.settingsLanguageSearchHint, + emptyMessage: l10n.settingsLanguageNoMatch, + items: AppLocale.values, + selected: selected, + labelBuilder: (locale) => locale.displayName, + filter: (locale, query) { + return locale.displayName.toLowerCase().contains(query) || locale.languageCode.toLowerCase().contains(query); + }, + onSelect: ref.read(selectedAppLocaleProvider.notifier).select, + errorMessageBuilder: l10n.settingsLanguageError, + ); + } +} diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index 9b2eaa11..2e103720 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; @@ -19,24 +21,25 @@ class MiningRewardsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final miningAsync = ref.watch(miningRewardsProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase.refreshable( - appBar: const V2AppBar(title: 'Mining Rewards'), + appBar: V2AppBar(title: l10n.settingsMiningTitle), onRefresh: () async => ref.invalidate(miningRewardsProvider), slivers: [ miningAsync.when( - data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : const _NoRewards(), - loading: () => const _NoRewards(isLoading: true), + data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : _NoRewards(l10n: l10n), + loading: () => _NoRewards(l10n: l10n, isLoading: true), error: (err, _) => - _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), + _ErrorState(colors: colors, text: text, l10n: l10n, onRetry: () => ref.invalidate(miningRewardsProvider)), ), ], bottomContent: miningAsync.when( data: (data) => data.totalBlocks > 0 - ? const ScaffoldBaseBottomContent(child: QuantusButton.simple(label: 'Redeem', onTap: null)) + ? ScaffoldBaseBottomContent(child: QuantusButton.simple(label: l10n.settingsMiningRedeem, onTap: null)) : null, loading: () => null, error: (err, _) => null, @@ -50,12 +53,9 @@ class _WithRewards extends ConsumerWidget { const _WithRewards({required this.data}); - static const _resonanceSince = 'Jul 2025'; - static const _schrodingerSince = 'Oct 2025'; - static const _diracSince = 'Nov 2025'; - @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final numberFmt = ref.watch(numberFormattingServiceProvider); final quanEarned = numberFmt.formatBalance(data.planckRewards, maxDecimals: 2, addSymbol: true); final redeemedRewards = numberFmt.formatBalance(data.redeemedRewards, maxDecimals: 2, addSymbol: true); @@ -65,19 +65,35 @@ class _WithRewards extends ConsumerWidget { final text = context.themeText; final testnets = [ - _TestnetEntry('Dirac', _diracSince, data.diracBlocks), - _TestnetEntry('Schrödinger', _schrodingerSince, data.schrodingerBlocks), - _TestnetEntry('Resonance', _resonanceSince, data.resonanceBlocks), + _TestnetEntry('Dirac', l10n.settingsMiningDiracSince, data.diracBlocks), + _TestnetEntry('Schrödinger', l10n.settingsMiningSchrodingerSince, data.schrodingerBlocks), + _TestnetEntry('Resonance', l10n.settingsMiningResonanceSince, data.resonanceBlocks), ]; final miningSummaryPairRows = [ _StatPairRow( - left: _MiningStatCell(label: 'TESTNET BLOCKS', value: '${data.totalBlocks}', valueColor: colors.textLightGray), - right: _MiningStatCell(label: 'TESTNET REWARDS', value: quanEarned, valueColor: colors.accentOrange), + left: _MiningStatCell( + label: l10n.settingsMiningStatTestnetBlocks, + value: '${data.totalBlocks}', + valueColor: colors.textLightGray, + ), + right: _MiningStatCell( + label: l10n.settingsMiningStatTestnetRewards, + value: quanEarned, + valueColor: colors.accentOrange, + ), ), _StatPairRow( - left: _MiningStatCell(label: 'REDEEMED', value: redeemedRewards, valueColor: colors.textLightGray), - right: _MiningStatCell(label: 'REDEEMABLE', value: redeemableRewards, valueColor: colors.success), + left: _MiningStatCell( + label: l10n.settingsMiningStatRedeemed, + value: redeemedRewards, + valueColor: colors.textLightGray, + ), + right: _MiningStatCell( + label: l10n.settingsMiningStatRedeemable, + value: redeemableRewards, + valueColor: colors.success, + ), ), ]; @@ -86,9 +102,10 @@ class _WithRewards extends ConsumerWidget { children: [ SplitCard( topChild: _CardTopSection( + l10n: l10n, totalBlocks: data.totalBlocks, totalBlocksColor: colors.textLightGray, - statusLabel: 'Mining', + statusLabel: l10n.settingsMiningStatusMining, statusColor: colors.success, ), bottomChild: Column( @@ -103,13 +120,13 @@ class _WithRewards extends ConsumerWidget { ), const SizedBox(height: 32), for (var i = 0; i < testnets.length; i++) ...[ - _TestnetRow(entry: testnets[i]), + _TestnetRow(entry: testnets[i], blocksLabel: l10n.settingsMiningTestnetBlocks), if (i < testnets.length - 1) Divider(color: colors.toasterBackground, height: 1, thickness: 1), ], const SizedBox(height: 48), Center( child: _OrangeLinkButton( - label: 'View Telemetry ↗', + label: l10n.settingsMiningViewTelemetry, text: text, onTap: () => openUrl(AppConstants.telemetryUrl), ), @@ -121,9 +138,10 @@ class _WithRewards extends ConsumerWidget { } class _NoRewards extends StatelessWidget { + final AppLocalizations l10n; final bool isLoading; - const _NoRewards({this.isLoading = false}); + const _NoRewards({required this.l10n, this.isLoading = false}); @override Widget build(BuildContext context) { @@ -135,14 +153,15 @@ class _NoRewards extends StatelessWidget { children: [ SplitCard( topChild: _CardTopSection( + l10n: l10n, totalBlocks: 0, totalBlocksColor: colors.textTertiary, - statusLabel: 'Pending', + statusLabel: l10n.settingsMiningStatusPending, statusColor: colors.textTertiary, isLoading: isLoading, ), bottomChild: _StatColumn( - label: 'QUAN EARNED', + label: l10n.settingsMiningQuanEarned, value: '0.00', valueColor: colors.textTertiary, isLoading: isLoading, @@ -175,18 +194,18 @@ class _NoRewards extends StatelessWidget { ] else ...[ const SizedBox(height: 64), Text( - 'No mining data yet', + l10n.settingsMiningNoDataTitle, style: text.mediumTitle?.copyWith(fontWeight: FontWeight.w400, color: colors.textMuted), ), const SizedBox(height: 8), Text( - 'Set up a Quantus mining node to start earning rewards.', + l10n.settingsMiningNoDataBody, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault, height: 1.35), ), const SizedBox(height: 64), _OrangeLinkButton( - label: 'Mining Setup Guide ↗', + label: l10n.settingsMiningSetupGuide, text: text, onTap: () => openUrl(AppConstants.miningSetupGuideUrl), ), @@ -198,6 +217,7 @@ class _NoRewards extends StatelessWidget { } class _CardTopSection extends StatelessWidget { + final AppLocalizations l10n; final int totalBlocks; final Color totalBlocksColor; final String statusLabel; @@ -205,6 +225,7 @@ class _CardTopSection extends StatelessWidget { final bool isLoading; const _CardTopSection({ + required this.l10n, required this.totalBlocks, required this.totalBlocksColor, required this.statusLabel, @@ -223,7 +244,7 @@ class _CardTopSection extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('BLOCKS MINED', style: text.receiveLabel?.copyWith(color: colors.textLabel)), + Text(l10n.settingsMiningBlocksMined, style: text.receiveLabel?.copyWith(color: colors.textLabel)), Row( children: [ Container( @@ -246,7 +267,7 @@ class _CardTopSection extends StatelessWidget { else Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), - Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), + Text(l10n.settingsMiningBlocksAcrossTestnets, style: text.detail?.copyWith(color: colors.textMuted)), ], ); } @@ -315,8 +336,9 @@ class _StatColumn extends StatelessWidget { class _TestnetRow extends StatelessWidget { final _TestnetEntry entry; + final String blocksLabel; - const _TestnetRow({required this.entry}); + const _TestnetRow({required this.entry, required this.blocksLabel}); @override Widget build(BuildContext context) { @@ -349,7 +371,7 @@ class _TestnetRow extends StatelessWidget { ), ), const SizedBox(height: 4), - Text('blocks', style: text.detail?.copyWith(color: colors.textMuted)), + Text(blocksLabel, style: text.detail?.copyWith(color: colors.textMuted)), ], ), ], @@ -385,9 +407,10 @@ class _OrangeLinkButton extends StatelessWidget { class _ErrorState extends StatelessWidget { final AppColorsV2 colors; final AppTextTheme text; + final AppLocalizations l10n; final VoidCallback onRetry; - const _ErrorState({required this.colors, required this.text, required this.onRetry}); + const _ErrorState({required this.colors, required this.text, required this.l10n, required this.onRetry}); @override Widget build(BuildContext context) { @@ -397,14 +420,14 @@ class _ErrorState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Failed to load mining rewards', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text(l10n.settingsMiningLoadError, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 8), - Text('Please check your connection', style: text.detail?.copyWith(color: colors.textTertiary)), + Text(l10n.settingsMiningCheckConnection, style: text.detail?.copyWith(color: colors.textTertiary)), const SizedBox(height: 20), GestureDetector( onTap: onRetry, child: Text( - 'Try Again', + l10n.posQrTryAgain, style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ), diff --git a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart index 7d4eb1cb..24b3fc21 100644 --- a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/notification_config_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/currency_picker_screen.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/language_picker_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -24,25 +26,45 @@ class _PreferencesSettingsScreenV2State extends ConsumerState const LanguagePickerScreenV2())); + } + void _openCurrencyPicker() { Navigator.push(context, MaterialPageRoute(builder: (_) => const CurrencyPickerScreenV2())); } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final notifConfig = ref.watch(notificationConfigProvider); final posMode = ref.watch(posModeProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); final fiat = ref.watch(selectedFiatCurrencyProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Preferences'), + appBar: V2AppBar(title: l10n.settingsPreferencesTitle), mainContent: ListView( children: [ SettingsTappableRow( - title: 'Currency', - subtitle: 'Fiat display preference', + title: l10n.settingsPreferencesLanguage, + subtitle: l10n.settingsPreferencesLanguageSubtitle, + onTap: _openLanguagePicker, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(appLocale.displayName, style: text.smallParagraph?.copyWith(color: colors.textMuted)), + const SizedBox(width: 4), + SettingsTappableRowUtils.chevron(colors, color: colors.textMuted, size: 18), + ], + ), + ), + const SettingsDivider(), + SettingsTappableRow( + title: l10n.settingsPreferencesCurrency, + subtitle: l10n.settingsPreferencesCurrencySubtitle, onTap: _openCurrencyPicker, trailing: Row( mainAxisSize: MainAxisSize.min, @@ -55,15 +77,15 @@ class _PreferencesSettingsScreenV2State extends ConsumerState ref.read(posModeProvider.notifier).setPosMode(v), ), const SettingsDivider(), SettingsSwitchRow( - title: 'Notifications', - subtitle: 'Transaction and wallet alerts', + title: l10n.settingsPreferencesNotifications, + subtitle: l10n.settingsPreferencesNotificationsSubtitle, value: notifConfig.enabled, onChanged: _toggleNotifications, ), diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart index 4d86afff..ab2b2bf3 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart @@ -1,23 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/local_auth_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_caution_scaffold.dart'; -class RecoveryPhraseConfirmationScreen extends StatefulWidget { +class RecoveryPhraseConfirmationScreen extends ConsumerStatefulWidget { const RecoveryPhraseConfirmationScreen({super.key, required this.walletIndex}); final int walletIndex; @override - State createState() => _RecoveryPhraseConfirmationScreenState(); + ConsumerState createState() => _RecoveryPhraseConfirmationScreenState(); } -class _RecoveryPhraseConfirmationScreenState extends State { +class _RecoveryPhraseConfirmationScreenState extends ConsumerState { bool _acknowledged = false; Future _onContinue() async { - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to see recovery phrase'); + final l10n = ref.read(l10nProvider); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.settingsRecoveryConfirmAuthReason); if (authed && mounted) { Navigator.of( @@ -25,20 +28,19 @@ class _RecoveryPhraseConfirmationScreenState extends State RecoveryPhraseScreen(walletIndex: widget.walletIndex))); } else { if (mounted) { - context.showErrorToaster(message: 'Authentication required to see recovery phrase'); + context.showErrorToaster(message: l10n.settingsRecoveryConfirmAuthRequired); } - - return; } } @override Widget build(BuildContext context) { - final data = const SettingsCautionScaffoldData.recoveryPhrase(); + final l10n = ref.watch(l10nProvider); return SettingsCautionScaffold( - appBarTitle: 'Recovery Phrase', - data: data, + appBarTitle: l10n.settingsRecoveryPhraseTitle, + data: SettingsCautionScaffoldData.recoveryPhrase(l10n), + continueLabel: l10n.commonContinue, checkboxChecked: _acknowledged, onCheckboxChanged: () => setState(() => _acknowledged = !_acknowledged), onContinue: _onContinue, diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart index c7c37546..57b86aff 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/recovery_phrase_body.dart'; -class RecoveryPhraseScreen extends StatefulWidget { +class RecoveryPhraseScreen extends ConsumerStatefulWidget { const RecoveryPhraseScreen({super.key, this.walletIndex = 0}); final int walletIndex; @override - State createState() => _RecoveryPhraseScreenState(); + ConsumerState createState() => _RecoveryPhraseScreenState(); } -class _RecoveryPhraseScreenState extends State { +class _RecoveryPhraseScreenState extends ConsumerState { final _settingsService = SettingsService(); List _words = []; @@ -32,10 +34,12 @@ class _RecoveryPhraseScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return RecoveryPhraseBody( - appBarTitle: 'Recovery Phrase', + appBarTitle: l10n.settingsRecoveryPhraseTitle, words: _words, - primaryButtonLabel: 'Done', + primaryButtonLabel: l10n.settingsRecoveryPhraseDone, onPrimary: () => Navigator.of(context).pop(), ); } diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart index aa6832b8..9ce29eb4 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/local_auth_service.dart'; import 'package:resonance_network_wallet/services/logout_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; @@ -18,31 +19,34 @@ class _ResetConfirmationScreenState extends ConsumerState _resetAndClearData() async { + final l10n = ref.read(l10nProvider); setState(() => _isResetting = true); - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to reset wallet'); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.settingsResetAuthReason); if (authed && mounted) { try { - await ref.read(logoutServiceProvider).logout(context); + ref.read(logoutServiceProvider).logout(context); } catch (e) { if (mounted) { - context.showErrorToaster(message: 'Failed to reset wallet: $e'); + context.showErrorToaster(message: l10n.settingsResetFailed('$e')); } setState(() => _isResetting = false); } } else if (mounted) { - context.showErrorToaster(message: 'Authentication required to reset wallet'); - + context.showErrorToaster(message: l10n.settingsResetAuthRequired); setState(() => _isResetting = false); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return SettingsCautionScaffold( - appBarTitle: 'Reset Wallet', - data: const SettingsCautionScaffoldData.walletReset(), + appBarTitle: l10n.settingsResetTitle, + data: SettingsCautionScaffoldData.walletReset(l10n), + continueLabel: l10n.commonContinue, betweenBulletsStyle: SettingsDividerStyle.sectionEmphasis, checkboxChecked: _backedUpChecked, onCheckboxChanged: () => setState(() => _backedUpChecked = !_backedUpChecked), diff --git a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart index f4a3eafb..2c001b8f 100644 --- a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -8,41 +9,52 @@ import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_confirmation_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; class SelectWalletScreen extends ConsumerWidget { const SelectWalletScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final accountsAsync = ref.watch(accountsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Select Wallet'), + appBar: V2AppBar(title: l10n.settingsSelectWalletTitle), mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Failed to load wallets', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text(l10n.settingsWalletFailedToLoad, style: text.paragraph?.copyWith(color: colors.textSecondary)), ), data: (accounts) { final indices = getNonHardwareWalletIndices(accounts); if (indices.isEmpty) { return Center( - child: Text('No wallets found', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text( + l10n.settingsSelectWalletNoWallets, + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ); } return ListView.separated( itemCount: indices.length, separatorBuilder: (_, _) => const SizedBox(height: 12), - itemBuilder: (_, i) => _walletItem(context, indices[i], colors, text), + itemBuilder: (_, i) => _walletItem(context, l10n, indices[i], colors, text), ); }, ), ); } - Widget _walletItem(BuildContext context, int walletIndex, AppColorsV2 colors, AppTextTheme text) { + Widget _walletItem( + BuildContext context, + AppLocalizations l10n, + int walletIndex, + AppColorsV2 colors, + AppTextTheme text, + ) { return GestureDetector( onTap: () => Navigator.push( context, @@ -55,7 +67,7 @@ class SelectWalletScreen extends ConsumerWidget { children: [ Expanded( child: Text( - 'Wallet ${walletIndex + 1}', + l10n.settingsSelectWalletItem(walletIndex + 1), style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), ), ), diff --git a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart index 840fd05e..85b614ba 100644 --- a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart +++ b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; @@ -16,31 +17,30 @@ class SettingsCautionScaffoldData { const SettingsCautionScaffoldData({required this.headline, required this.bulletItems, required this.checkboxLabel}); - const SettingsCautionScaffoldData.recoveryPhrase() - : this( - headline: 'Keep your Recovery Phrase Secret', - bulletItems: const [ - 'If you lose this device, your recovery phrase is the only way back', - 'Anyone who gets hold of it has complete control over your funds, permanently', - 'Write it down and keep it somewhere safe. Do not save it digitally', - ], - checkboxLabel: 'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.', - ); + factory SettingsCautionScaffoldData.recoveryPhrase(AppLocalizations l10n) { + return SettingsCautionScaffoldData( + headline: l10n.createWalletCautionHeadline, + bulletItems: [l10n.createWalletCautionBullet1, l10n.createWalletCautionBullet2, l10n.createWalletCautionBullet3], + checkboxLabel: l10n.createWalletCautionCheckboxLabel, + ); + } - const SettingsCautionScaffoldData.walletReset() - : this( - headline: 'This will erase\nyour wallet', - bulletItems: const [ - 'All wallet data will be permanently removed from this device', - 'Your funds stay on the blockchain but only your recovery phrase can restore access', - 'Without it, your funds are gone forever', - ], - checkboxLabel: "I've backed up my recovery phrase", - ); + factory SettingsCautionScaffoldData.walletReset(AppLocalizations l10n) { + return SettingsCautionScaffoldData( + headline: l10n.settingsResetCautionHeadline, + bulletItems: [ + l10n.settingsResetCautionBullet1, + l10n.settingsResetCautionBullet2, + l10n.settingsResetCautionBullet3, + ], + checkboxLabel: l10n.settingsResetCautionCheckbox, + ); + } } class SettingsCautionScaffold extends StatelessWidget { final String appBarTitle; + final String continueLabel; final bool checkboxChecked; final VoidCallback onCheckboxChanged; final VoidCallback onContinue; @@ -55,6 +55,7 @@ class SettingsCautionScaffold extends StatelessWidget { required this.onCheckboxChanged, required this.onContinue, required this.data, + required this.continueLabel, this.betweenBulletsStyle = SettingsDividerStyle.list, this.continueButtonLoading = false, }); @@ -85,6 +86,7 @@ class SettingsCautionScaffold extends StatelessWidget { ), bottomContent: _SettingsCautionBottom( checkboxLabel: data.checkboxLabel, + continueLabel: continueLabel, checked: checkboxChecked, onCheckboxChanged: onCheckboxChanged, onContinue: onContinue, @@ -97,6 +99,7 @@ class SettingsCautionScaffold extends StatelessWidget { class _SettingsCautionBottom extends StatelessWidget { const _SettingsCautionBottom({ required this.checkboxLabel, + required this.continueLabel, required this.checked, required this.onCheckboxChanged, required this.onContinue, @@ -104,6 +107,7 @@ class _SettingsCautionBottom extends StatelessWidget { }); final String checkboxLabel; + final String continueLabel; final bool checked; final VoidCallback onCheckboxChanged; final VoidCallback onContinue; @@ -118,7 +122,7 @@ class _SettingsCautionBottom extends StatelessWidget { SettingsCheckbox(checked: checked, label: checkboxLabel, onTap: onCheckboxChanged), const SizedBox(height: 32), QuantusButton.simple( - label: 'Continue', + label: continueLabel, onTap: onContinue, variant: ButtonVariant.primary, isDisabled: !checked, diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart new file mode 100644 index 00000000..a7419aa8 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/shared/utils/print.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +/// Searchable list for choosing a single settings value (language, currency, etc.). +class SettingsPickerScreen extends StatefulWidget { + const SettingsPickerScreen({ + super.key, + required this.title, + required this.searchHint, + required this.emptyMessage, + required this.items, + required this.selected, + required this.labelBuilder, + required this.onSelect, + required this.errorMessageBuilder, + this.filter, + }); + + final String title; + final String searchHint; + final String emptyMessage; + final List items; + final T selected; + final String Function(T) labelBuilder; + final bool Function(T item, String query)? filter; + final Future Function(T) onSelect; + final String Function(String error) errorMessageBuilder; + + @override + State> createState() => _SettingsPickerScreenState(); +} + +class _SettingsPickerScreenState extends State> { + final _searchController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List _filtered(String query) { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return List.from(widget.items); + return widget.items.where((item) { + if (widget.filter != null) { + return widget.filter!(item, q); + } + return widget.labelBuilder(item).toLowerCase().contains(q); + }).toList(); + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final filtered = _filtered(_searchController.text); + + return ScaffoldBase( + appBar: V2AppBar(title: widget.title), + mainContent: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SettingsPickerSearchField( + controller: _searchController, + colors: colors, + text: text, + hintText: widget.searchHint, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + Expanded( + child: Container( + decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), + clipBehavior: Clip.antiAlias, + child: Scrollbar( + thumbVisibility: true, + thickness: 4, + radius: const Radius.circular(25), + child: filtered.isEmpty + ? Center( + child: Text( + widget.emptyMessage, + style: text.smallParagraph?.copyWith(color: colors.textMuted), + textAlign: TextAlign.center, + ), + ) + : ListView.separated( + itemCount: filtered.length, + separatorBuilder: (context, index) => + const SettingsDivider(style: SettingsDividerStyle.currencyList, padding: EdgeInsets.zero), + itemBuilder: (context, index) { + final item = filtered[index]; + return SettingsPickerListTile( + label: widget.labelBuilder(item), + selected: item == widget.selected, + colors: colors, + text: text, + onTap: () async { + if (_isLoading) return; + + setState(() => _isLoading = true); + + try { + await widget.onSelect(item); + if (context.mounted) { + Navigator.pop(context); + } + } catch (e) { + quantusDebugPrint('[SettingsPickerScreen] error selecting item: $e'); + if (context.mounted) { + context.showErrorToaster(message: widget.errorMessageBuilder(e.toString())); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + }, + ); + }, + ), + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart b/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart new file mode 100644 index 00000000..7bfc3a40 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class SettingsPickerSearchField extends StatelessWidget { + const SettingsPickerSearchField({ + super.key, + required this.controller, + required this.colors, + required this.text, + required this.hintText, + required this.onChanged, + }); + + final TextEditingController controller; + final AppColorsV2 colors; + final AppTextTheme text; + final String hintText; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 48, + child: Container( + padding: const EdgeInsets.only(left: 12, right: 8), + decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), + child: Row( + children: [ + Icon(Icons.search, size: 18, color: colors.textLabel), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + onChanged: onChanged, + style: text.smallParagraph, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: hintText, + hintStyle: text.smallParagraph?.copyWith(color: colors.textLabel), + ), + ), + ), + ], + ), + ), + ); + } +} + +class SettingsPickerListTile extends StatelessWidget { + const SettingsPickerListTile({ + super.key, + required this.label, + required this.selected, + required this.colors, + required this.text, + required this.onTap, + }); + + final String label; + final bool selected; + final AppColorsV2 colors; + final AppTextTheme text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final accent = colors.accentOrange; + final fg = selected ? accent : colors.textPrimary; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text(label, style: text.paragraph?.copyWith(color: fg, height: 1.2)), + ), + if (selected) ...[const SizedBox(width: 12), Icon(Icons.check, size: 18, color: accent)], + ], + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 718bf4ab..f93172c8 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -15,8 +17,6 @@ import 'package:resonance_network_wallet/v2/screens/settings/mining_rewards_scre import 'package:resonance_network_wallet/v2/screens/settings/wallet_settings_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -const _miningRewardsTitle = 'Mining Rewards'; - class SettingsScreenV2 extends ConsumerStatefulWidget { const SettingsScreenV2({super.key}); @@ -27,27 +27,31 @@ class SettingsScreenV2 extends ConsumerStatefulWidget { class _SettingsScreenV2State extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final miningAsync = ref.watch(miningRewardsProvider); final colors = context.colors; final trailing = SettingsTappableRowUtils.chevron(colors); - final entries = _settingsHubItems(colors); + final entries = _settingsHubItems(colors, l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'Settings'), + appBar: V2AppBar(title: l10n.settingsTitle), mainContent: ListView( children: [ for (final e in entries.asMap().entries) ...[ - if (e.value.title == _miningRewardsTitle) + if (e.value.isMiningRewards) miningAsync.when( - data: (data) => - _buildTappableRow(e.value, subtitle: '${data.totalBlocks} blocks mined', trailing: trailing), - loading: () => _buildTappableRow(e.value, subtitle: 'Loading...', trailing: trailing), + data: (data) => _buildTappableRow( + e.value, + subtitle: l10n.settingsMiningRewardsSubtitle(data.totalBlocks), + trailing: trailing, + ), + loading: () => _buildTappableRow(e.value, subtitle: l10n.commonLoading, trailing: trailing), error: (err, st) { debugPrint('Error getting mining rewards: ${err.toString()}'); debugPrint('Stack trace: ${st.toString()}'); - return _buildTappableRow(e.value, subtitle: 'Error getting mining rewards', trailing: trailing); + return _buildTappableRow(e.value, subtitle: l10n.settingsMiningRewardsError, trailing: trailing); }, ) else @@ -69,50 +73,58 @@ class _SettingsScreenV2State extends ConsumerState { } class _SettingsHubItem { - const _SettingsHubItem({required this.leading, required this.title, required this.subtitle, required this.page}); + const _SettingsHubItem({ + required this.leading, + required this.title, + required this.subtitle, + required this.page, + this.isMiningRewards = false, + }); final Widget leading; final String title; final String subtitle; final Widget page; + final bool isMiningRewards; } -List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors) { +List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors, AppLocalizations l10n) { return [ _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.account_balance_wallet_outlined), - title: 'Wallet', - subtitle: 'Recovery Phrase, Reset Wallet', + title: l10n.settingsWalletTitle, + subtitle: l10n.settingsWalletSubtitle, page: const WalletSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.tune), - title: 'Preferences', - subtitle: 'Currency, POS mode, notifications', + title: l10n.settingsPreferencesTitle, + subtitle: l10n.settingsPreferencesSubtitle, page: const PreferencesSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/axe.svg', width: 18, height: 18)), - title: _miningRewardsTitle, - subtitle: 'Loading...', + title: l10n.settingsMiningRewards, + subtitle: l10n.commonLoading, page: const MiningRewardsScreen(), + isMiningRewards: true, ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.shield_outlined), - title: 'Account Type', - subtitle: 'Advanced Account Features', + title: l10n.settingsAccountTypeTitle, + subtitle: l10n.settingsAccountTypeSubtitle, page: const AccountTypeSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.help_outline), - title: 'Help & Support', - subtitle: 'FAQs, Contact the team', + title: l10n.settingsHelpTitle, + subtitle: l10n.settingsHelpSubtitle, page: const HelpAndSupportScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/uppercase_q.svg', width: 18, height: 18)), - title: 'About Quantus', - subtitle: 'Version $appVersion ($appBuildNumber)', + title: l10n.settingsAboutTitle, + subtitle: l10n.settingsAboutHubSubtitle(appVersion, appBuildNumber), page: const AboutQuantusScreenV2(), ), ]; diff --git a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart index 585603cd..aa26fe24 100644 --- a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -8,37 +9,39 @@ import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; class TestnetRewardsScreen extends ConsumerWidget { const TestnetRewardsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final miningAsync = ref.watch(miningRewardsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Testnet Rewards'), + appBar: V2AppBar(title: l10n.settingsTestnetTitle), mainContent: miningAsync.when( skipLoadingOnRefresh: false, data: (data) => RefreshIndicator( onRefresh: () async => ref.invalidate(miningRewardsProvider), - child: _buildContent(data, colors, text), + child: _buildContent(l10n, data, colors, text), ), loading: () => const Center(child: Loader()), error: (_, _) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Failed to load testnet rewards', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text(l10n.settingsTestnetLoadError, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 8), - Text('Please check your connection', style: text.detail?.copyWith(color: colors.textTertiary)), + Text(l10n.settingsMiningCheckConnection, style: text.detail?.copyWith(color: colors.textTertiary)), const SizedBox(height: 20), GestureDetector( onTap: () => ref.invalidate(miningRewardsProvider), child: Text( - 'Try Again', + l10n.posQrTryAgain, style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ), @@ -49,7 +52,7 @@ class TestnetRewardsScreen extends ConsumerWidget { ); } - Widget _buildContent(MiningRewardsData data, AppColorsV2 colors, AppTextTheme text) { + Widget _buildContent(AppLocalizations l10n, MiningRewardsData data, AppColorsV2 colors, AppTextTheme text) { final testnets = [ ('Planck', data.planckBlocks), ('Dirac', data.diracBlocks), @@ -69,11 +72,11 @@ class TestnetRewardsScreen extends ConsumerWidget { const Text('💰', style: TextStyle(fontSize: 40)), const SizedBox(height: 12), Text( - '${data.totalBlocks} blocks', + l10n.settingsTestnetTotalBlocks(data.totalBlocks), style: text.largeTitle?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), - Text('Total blocks mined across all testnets', style: text.detail?.copyWith(color: colors.textTertiary)), + Text(l10n.settingsTestnetTotalDescription, style: text.detail?.copyWith(color: colors.textTertiary)), ], ), ), @@ -81,7 +84,7 @@ class TestnetRewardsScreen extends ConsumerWidget { Padding( padding: const EdgeInsets.only(bottom: 16), child: Text( - 'Breakdown', + l10n.settingsTestnetBreakdown, style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w600), ), ), @@ -92,7 +95,6 @@ class TestnetRewardsScreen extends ConsumerWidget { children: [ for (var i = 0; i < testnets.length; i++) ...[ if (i > 0) const SettingsDivider(style: SettingsDividerStyle.cardInterior), - Row( children: [ Expanded( @@ -100,7 +102,7 @@ class TestnetRewardsScreen extends ConsumerWidget { ), const Text('💰 ', style: TextStyle(fontSize: 14)), Text( - '${testnets[i].$2} blocks', + l10n.settingsTestnetRowBlocks(testnets[i].$2), style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ], diff --git a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart index d956e94b..2b65a698 100644 --- a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -24,9 +25,10 @@ class WalletSettingsScreenV2 extends ConsumerStatefulWidget { class _WalletSettingsScreenV2State extends ConsumerState { void _navigateToRecoveryPhrase(List accounts) { + final l10n = ref.read(l10nProvider); final walletIndices = getNonHardwareWalletIndices(accounts); if (walletIndices.isEmpty) { - context.showErrorToaster(message: 'No wallets found'); + context.showErrorToaster(message: l10n.settingsWalletNoWalletsFound); return; } @@ -46,6 +48,7 @@ class _WalletSettingsScreenV2State extends ConsumerState @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final titleColor = colors.textError; @@ -54,25 +57,25 @@ class _WalletSettingsScreenV2State extends ConsumerState final accountsAsync = ref.watch(accountsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Wallet'), + appBar: V2AppBar(title: l10n.settingsWalletTitle), mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Failed to load wallets', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text(l10n.settingsWalletFailedToLoad, style: text.paragraph?.copyWith(color: colors.textSecondary)), ), data: (accounts) => ListView( children: [ SettingsTappableRow( - title: 'Recovery Phrase', - subtitle: 'View your 24-word Backup Password', + title: l10n.settingsWalletRecoveryPhrase, + subtitle: l10n.settingsWalletRecoveryPhraseSubtitle, onTap: () => _navigateToRecoveryPhrase(accounts), trailing: SettingsTappableRowUtils.chevron(colors), ), const SettingsDivider(style: SettingsDividerStyle.walletSection), SettingsTappableRow( - title: 'Reset Wallet', + title: l10n.settingsWalletReset, titleColor: titleColor, - subtitle: 'Removes all data from this device', + subtitle: l10n.settingsWalletResetSubtitle, subtitleColor: subtitleColor, onTap: _showResetConfirmation, trailing: SettingsTappableRowUtils.chevron(colors, color: titleColor), diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index 3d56652f..67e2a050 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -11,16 +13,17 @@ import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/components/success_check.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; -class DepositScreen extends StatefulWidget { +class DepositScreen extends ConsumerStatefulWidget { final SwapOrder order; const DepositScreen({super.key, required this.order}); @override - State createState() => _DepositScreenState(); + ConsumerState createState() => _DepositScreenState(); } -class _DepositScreenState extends State { +class _DepositScreenState extends ConsumerState { final _swapService = SwapService(); late SwapOrder _order; bool _confirming = false; @@ -42,6 +45,7 @@ class _DepositScreenState extends State { }); _pollStatus(); } catch (e) { + debugPrint('Confirm funds sent failed: $e'); setState(() => _confirming = false); } } @@ -54,21 +58,25 @@ class _DepositScreenState extends State { final updated = await _swapService.getSwapStatus(_order.orderId); if (!mounted) return; setState(() => _order = updated); - } catch (_) {} + } catch (e) { + debugPrint('Swap status poll failed: $e'); + } } } - String _getDepositAddress() { - // return _order.depositAddress - return 'For demo purposes only - do not send funds!'; + // Currently this is only for demo purposes + // We just return the demo warning for now + String _getDepositAddress(AppLocalizations l10n) { + return l10n.swapDepositDemoWarning; } - void _copyAddress() { - context.copyTextWithToaster(_getDepositAddress()); + void _copyAddress(AppLocalizations l10n) { + context.copyTextWithToaster(_getDepositAddress(l10n)); } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final quote = _order.quote; @@ -76,38 +84,40 @@ class _DepositScreenState extends State { return ScaffoldBase( appBar: V2AppBar( - title: 'Swap', + title: l10n.swapTitle, trailing: Icon(Icons.info_outline, color: colors.textPrimary, size: 24), ), mainContent: Column( children: [ if (_order.status == SwapStatus.complete) - _completedBody(colors, text) + _completedBody(l10n, colors, text) else if (_order.status == SwapStatus.processing) - _processingBody(colors, text) + _processingBody(l10n, colors, text) else - _depositBody(colors, text, quote, usd), + _depositBody(l10n, colors, text, quote, usd), const Spacer(), - if (_order.status == SwapStatus.depositing) _sentButton(colors, text), - if (_order.status == SwapStatus.complete) _doneButton(colors, text), + if (_order.status == SwapStatus.depositing) _sentButton(l10n, colors, text), + if (_order.status == SwapStatus.complete) _doneButton(l10n, colors, text), const SizedBox(height: 24), ], ), ); } - Widget _depositBody(AppColorsV2 colors, AppTextTheme text, SwapQuote quote, double usd) { + Widget _depositBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text, SwapQuote quote, double usd) { + final demoWarning = l10n.swapDepositDemoWarning; + return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Deposit Amount', style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), + Text(l10n.swapDepositAmount, style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), const SizedBox(width: 6), GestureDetector( onTap: () => context.copyTextWithToaster( SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken), - message: 'Deposit amount copied to clipboard', + message: l10n.swapDepositAmountCopied, ), child: Container( width: 20, @@ -141,9 +151,6 @@ class _DepositScreenState extends State { child: Container( color: Colors.white, padding: const EdgeInsets.all(8), - - /// for now this QR Code is invalid so people don't transfer by accident - // child: QrImageView(data: _getDepositAddress(), version: QrVersions.auto, size: 184), child: QrImageView(data: 'Quantum secure bitcoin - quantus!', version: QrVersions.auto, size: 184), ), ), @@ -152,10 +159,8 @@ class _DepositScreenState extends State { width: 264, child: Stack( children: [ - // for now put invalid address so people don't transfer by accident Text( - // _getDepositAddress().toLowerCase(), - 'For demo purposes only - do not send funds!', + demoWarning, style: text.smallParagraph?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w500, @@ -167,7 +172,7 @@ class _DepositScreenState extends State { right: 0, top: 19, child: GestureDetector( - onTap: _copyAddress, + onTap: () => _copyAddress(l10n), child: Container( width: 20, height: 20, @@ -184,22 +189,26 @@ class _DepositScreenState extends State { children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.receiveCopy, variant: ButtonVariant.transparent, - onTap: _copyAddress, + onTap: () => _copyAddress(l10n), icon: Icon(Icons.copy, color: colors.textPrimary, size: 20), ), ), const SizedBox(width: 16), Expanded( child: QuantusButton.simple( - label: 'Share QR', + label: l10n.swapDepositShareQr, icon: Icon(Icons.qr_code, color: colors.textPrimary, size: 20), variant: ButtonVariant.transparent, onTap: () { shareText( context, - 'Network: ${_order.quote.fromToken.network}\nToken: ${_order.quote.fromToken.symbol}\nAddress: ${_getDepositAddress()}', + l10n.swapDepositShareContent( + _order.quote.fromToken.network, + _order.quote.fromToken.symbol, + _getDepositAddress(l10n), + ), ); }, ), @@ -207,59 +216,50 @@ class _DepositScreenState extends State { ], ), const SizedBox(height: 40), - Text.rich( - TextSpan( - style: text.detail?.copyWith(color: colors.textSecondary, height: 1.35), - children: [ - const TextSpan(text: 'Use your '), - TextSpan( - text: quote.fromToken.symbol, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const TextSpan(text: ' or '), - TextSpan( - text: quote.fromToken.network, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const TextSpan(text: ' wallet to deposit funds. Depositing other assets may result in loss of funds.'), - ], - ), + Text( + l10n.swapDepositNotice(quote.fromToken.symbol, quote.fromToken.network), + style: text.detail?.copyWith(color: colors.textSecondary, height: 1.35), textAlign: TextAlign.center, ), ], ); } - Widget _processingBody(AppColorsV2 colors, AppTextTheme text) { + Widget _processingBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ const SizedBox(height: 80), Loader(color: colors.accentGreen), const SizedBox(height: 32), - Text('Processing Swap', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), + Text( + l10n.swapDepositProcessingTitle, + style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20), + ), const SizedBox(height: 12), - Text('This may take a few minutes...', style: text.paragraph?.copyWith(color: colors.textSecondary)), + Text(l10n.swapDepositProcessingBody, style: text.paragraph?.copyWith(color: colors.textSecondary)), ], ); } - Widget _completedBody(AppColorsV2 colors, AppTextTheme text) { + Widget _completedBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { + final amount = SwapService.formatTokenAmount(_order.quote.toAmount, _order.quote.toToken); + return Column( children: [ const SizedBox(height: 80), const SuccessCheck(), const SizedBox(height: 32), - Text('Swap Complete', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), + Text(l10n.swapDepositCompleteTitle, style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), const SizedBox(height: 12), Text( - 'Your swap for ${SwapService.formatTokenAmount(_order.quote.toAmount, _order.quote.toToken)} QUAN is complete.', + l10n.swapDepositCompleteBody(amount), style: text.paragraph?.copyWith(color: colors.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 40), if (AppConstants.stillOnTestnet) Text( - 'DEMO ONLY - WE ARE STILL ON TESTNET', + l10n.swapDepositTestnetBanner, style: text.paragraph?.copyWith(color: Colors.yellow), textAlign: TextAlign.center, ), @@ -267,18 +267,18 @@ class _DepositScreenState extends State { ); } - Widget _sentButton(AppColorsV2 colors, AppTextTheme text) { + Widget _sentButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: "I've sent the funds", + label: l10n.swapDepositSentFunds, onTap: _confirmSent, variant: ButtonVariant.secondary, isLoading: _confirming, ); } - Widget _doneButton(AppColorsV2 colors, AppTextTheme text) { + Widget _doneButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: 'Done', + label: l10n.swapDepositDone, onTap: () => Navigator.popUntil(context, (r) => r.isFirst), variant: ButtonVariant.secondary, ); diff --git a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart index fa58ecca..82dae312 100644 --- a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -8,15 +10,15 @@ Future showRefundAddressPickerSheet(BuildContext context, String networ return BottomSheetContainer.show(context, builder: (_) => _RefundAddressPickerContent(network: network)); } -class _RefundAddressPickerContent extends StatefulWidget { +class _RefundAddressPickerContent extends ConsumerStatefulWidget { final String network; const _RefundAddressPickerContent({required this.network}); @override - State<_RefundAddressPickerContent> createState() => _RefundAddressPickerContentState(); + ConsumerState<_RefundAddressPickerContent> createState() => _RefundAddressPickerContentState(); } -class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent> { +class _RefundAddressPickerContentState extends ConsumerState<_RefundAddressPickerContent> { List _addresses = []; @override @@ -32,11 +34,12 @@ class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return BottomSheetContainer( - title: 'Refund Addresses', + title: l10n.swapRefundPickerTitle, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -49,7 +52,7 @@ class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent if (_addresses.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 32), - child: Text('No recent refund addresses', style: text.detail?.copyWith(color: colors.textTertiary)), + child: Text(l10n.swapRefundPickerEmpty, style: text.detail?.copyWith(color: colors.textTertiary)), ) else ConstrainedBox( diff --git a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart index 9f80add9..422eaf7f 100644 --- a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/screens/swap/deposit_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; void showReviewQuoteSheet(BuildContext context, SwapQuote quote, String refundAddress) { BottomSheetContainer.show( @@ -14,13 +17,14 @@ void showReviewQuoteSheet(BuildContext context, SwapQuote quote, String refundAd ); } -class _ReviewQuoteContent extends StatelessWidget { +class _ReviewQuoteContent extends ConsumerWidget { final SwapQuote quote; final String refundAddress; const _ReviewQuoteContent({required this.quote, required this.refundAddress}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final swapService = SwapService(); @@ -28,7 +32,7 @@ class _ReviewQuoteContent extends StatelessWidget { final toUsd = quote.toAmount * swapService.getUsdPrice(quote.toToken); return BottomSheetContainer( - title: 'Review Quote', + title: l10n.swapReviewTitle, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -36,14 +40,14 @@ class _ReviewQuoteContent extends StatelessWidget { _swapVisual(context, colors, text, fromUsd, toUsd), const SizedBox(height: 48), _feeRow( - 'Total fees', + l10n.swapReviewTotalFees, '${SwapService.formatTokenAmount(quote.networkFee, quote.fromToken)} ${quote.fromToken.symbol}', colors, text, ), Divider(color: colors.separator, height: 32), _feeRow( - 'Total Amount', + l10n.swapReviewTotalAmount, '${SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken)} ${quote.fromToken.symbol}', colors, text, @@ -51,11 +55,14 @@ class _ReviewQuoteContent extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'You could receive up to \$${(quote.fromAmount * quote.slippageTolerance).toStringAsFixed(2)} less based on the ${(quote.slippageTolerance * 100).toStringAsFixed(0)}% slippage you set', + l10n.swapReviewSlippageWarning( + (quote.fromAmount * quote.slippageTolerance).toStringAsFixed(2), + (quote.slippageTolerance * 100).toStringAsFixed(0), + ), style: text.tiny?.copyWith(color: colors.textSecondary, height: 1.35), ), const SizedBox(height: 24), - _confirmButton(context, colors, text), + _confirmButton(context, l10n, colors, text), ], ), ); @@ -130,10 +137,9 @@ class _ReviewQuoteContent extends StatelessWidget { ); } - Widget _confirmButton(BuildContext context, AppColorsV2 colors, AppTextTheme text) { + Widget _confirmButton(BuildContext context, AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: 'Confirm', - + label: l10n.swapReviewConfirm, onTap: () async { final swapService = SwapService(); final order = await swapService.createSwap(quote); diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index 1fc1a4bc..6e07a6c6 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; @@ -12,14 +15,14 @@ import 'package:resonance_network_wallet/v2/screens/swap/token_picker_sheet.dart import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class SwapScreen extends StatefulWidget { +class SwapScreen extends ConsumerStatefulWidget { const SwapScreen({super.key}); @override - State createState() => _SwapScreenState(); + ConsumerState createState() => _SwapScreenState(); } -class _SwapScreenState extends State { +class _SwapScreenState extends ConsumerState { static const _qrIconAsset = 'assets/v2/swap_qr_code.svg'; static const _historyIconAsset = 'assets/v2/swap_clock_counter_clockwise.svg'; static const _swapDirectionIconAsset = 'assets/v2/swap_arrows_down_up.svg'; @@ -34,9 +37,10 @@ class _SwapScreenState extends State { bool _loading = false; double get _rate => _swapService.getRate(_fromToken); - String get _rateLabel { + + String _rateLabel(AppLocalizations l10n) { final val = 1 / _rate; - if (val == 0) return '1 QUAN = 0 ${_fromToken.symbol}'; + if (val == 0) return l10n.swapRateZero(_fromToken.symbol); final decimals = val >= 100 ? 2 : val >= 1 @@ -48,7 +52,7 @@ class _SwapScreenState extends State { : 10; var formatted = val.toStringAsFixed(decimals).replaceAll(RegExp(r'0+$'), ''); if (formatted.endsWith('.')) formatted = formatted.substring(0, formatted.length - 1); - return '1 QUAN = $formatted ${_fromToken.symbol}'; + return l10n.swapRateLabel(formatted, _fromToken.symbol); } @override @@ -90,6 +94,7 @@ class _SwapScreenState extends State { _swapService.addRefundAddress(_fromToken.network, _addressController.text.trim()); showReviewQuoteSheet(context, quote, _addressController.text); } catch (e) { + debugPrint('Swap quote failed: $e'); setState(() => _loading = false); } } @@ -117,12 +122,13 @@ class _SwapScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase( appBar: V2AppBar( - title: 'Swap', + title: l10n.swapTitle, trailing: Icon(Icons.info_outline, color: colors.textPrimary, size: 24), ), mainContent: Column( @@ -133,32 +139,32 @@ class _SwapScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _fromSection(colors, text), + _fromSection(l10n, colors, text), const SizedBox(height: 32), - _refundAddressSection(colors, text), + _refundAddressSection(l10n, colors, text), const SizedBox(height: 32), _swapDivider(colors), const SizedBox(height: 32), - _toSection(colors, text), + _toSection(l10n, colors, text), const SizedBox(height: 32), - _infoSection(colors, text), + _infoSection(l10n, colors, text), ], ), ), ), const SizedBox(height: 16), - _quoteButton(colors, text), + _quoteButton(l10n, colors, text), const SizedBox(height: 24), ], ), ); } - Widget _fromSection(AppColorsV2 colors, AppTextTheme text) { + Widget _fromSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('From', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapFrom, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), Row( children: [ @@ -234,13 +240,13 @@ class _SwapScreenState extends State { ); } - Widget _refundAddressSection(AppColorsV2 colors, AppTextTheme text) { + Widget _refundAddressSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Text('Refund Address', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapRefundAddress, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(width: 4), Icon(Icons.info_outline, color: colors.textSecondary, size: 14), ], @@ -256,7 +262,7 @@ class _SwapScreenState extends State { controller: _addressController, style: text.smallParagraph?.copyWith(color: colors.textPrimary), decoration: InputDecoration( - hintText: '${_fromToken.network} Address', + hintText: l10n.swapRefundAddressHint(_fromToken.network), hintStyle: text.smallParagraph?.copyWith(color: colors.textTertiary), border: InputBorder.none, isDense: true, @@ -322,11 +328,11 @@ class _SwapScreenState extends State { ); } - Widget _toSection(AppColorsV2 colors, AppTextTheme text) { + Widget _toSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('To', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapTo, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), Row( children: [ @@ -382,13 +388,13 @@ class _SwapScreenState extends State { ); } - Widget _infoSection(AppColorsV2 colors, AppTextTheme text) { + Widget _infoSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Slippage Tolerance', style: text.detail?.copyWith(color: colors.textSecondary)), + Text(l10n.swapSlippageTolerance, style: text.detail?.copyWith(color: colors.textSecondary)), Row( children: [ Text('1%', style: text.detail?.copyWith(color: colors.textSecondary)), @@ -402,9 +408,9 @@ class _SwapScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Rate', style: text.detail?.copyWith(color: colors.textSecondary)), + Text(l10n.swapRate, style: text.detail?.copyWith(color: colors.textSecondary)), Text( - _rateLabel, + _rateLabel(l10n), style: text.detail?.copyWith(color: colors.textSecondary, fontWeight: FontWeight.w500), ), ], @@ -413,10 +419,10 @@ class _SwapScreenState extends State { ); } - Widget _quoteButton(AppColorsV2 colors, AppTextTheme text) { + Widget _quoteButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { final enabled = _canGetQuote && !_loading; return QuantusButton.simple( - label: 'Get a Quote', + label: l10n.swapGetQuote, onTap: _getQuote, isDisabled: !enabled, variant: ButtonVariant.secondary, diff --git a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart index 7ab5ca73..b7af5c3a 100644 --- a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; typedef SwapTokenLoader = Future> Function({bool forceRefresh}); @@ -19,16 +22,16 @@ Future showTokenPickerSheet( ); } -class _TokenPickerContent extends StatefulWidget { +class _TokenPickerContent extends ConsumerStatefulWidget { final SwapTokenLoader loadTokens; final SwapToken current; const _TokenPickerContent({required this.loadTokens, required this.current}); @override - State<_TokenPickerContent> createState() => _TokenPickerContentState(); + ConsumerState<_TokenPickerContent> createState() => _TokenPickerContentState(); } -class _TokenPickerContentState extends State<_TokenPickerContent> { +class _TokenPickerContentState extends ConsumerState<_TokenPickerContent> { final _scrollController = ScrollController(); List _tokens = const []; bool _loading = true; @@ -58,17 +61,19 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { _tokens = tokens; _loading = false; }); - } catch (_) { + } catch (e) { + debugPrint('Token picker load failed: $e'); if (!mounted) return; setState(() { _loading = false; - _error = 'Failed to load tokens'; + _error = ref.read(l10nProvider).swapTokenPickerLoadError; }); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final size = MediaQuery.of(context).size; @@ -76,16 +81,16 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { final cardHeight = (height - 120).clamp(360.0, 506.0); return BottomSheetContainer( - title: 'Select Token', + title: l10n.swapTokenPickerTitle, height: cardHeight, child: DefaultTextStyle( style: const TextStyle(decoration: TextDecoration.none), - child: _content(colors, text), + child: _content(l10n, colors, text), ), ); } - Widget _content(AppColorsV2 colors, AppTextTheme text) { + Widget _content(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { if (_loading && _tokens.isEmpty) { return const Center(child: Loader()); } @@ -102,7 +107,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { GestureDetector( onTap: () => _loadTokens(forceRefresh: true), child: Text( - 'Retry', + l10n.posQrTryAgain, style: text.paragraph?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w600, @@ -128,7 +133,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { GestureDetector( onTap: () => _loadTokens(forceRefresh: true), child: Text( - 'Retry', + l10n.posQrTryAgain, style: text.detail?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w600, diff --git a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart index a98f7146..d568e341 100644 --- a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart +++ b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/screens/create/wallet_ready_screen.dart'; @@ -6,11 +8,13 @@ import 'package:resonance_network_wallet/v2/screens/import/import_wallet_screen. import 'package:resonance_network_wallet/v2/screens/welcome/onboarding_background.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class WelcomeScreenV2 extends StatelessWidget { +class WelcomeScreenV2 extends ConsumerWidget { const WelcomeScreenV2({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( backgroundWidget: const OnboardingBackground(), mainContent: Column( @@ -20,15 +24,11 @@ class WelcomeScreenV2 extends StatelessWidget { const SizedBox(height: 16), SizedBox( width: 210, - child: Text( - 'Quantum Secure Encrypted Money', - textAlign: TextAlign.center, - style: context.themeText.mediumTitle, - ), + child: Text(l10n.welcomeTagline, textAlign: TextAlign.center, style: context.themeText.mediumTitle), ), const SizedBox(height: 56), QuantusButton.simple( - label: 'Create New Wallet', + label: l10n.welcomeCreateNewWallet, onTap: () => Navigator.push( context, MaterialPageRoute( @@ -39,7 +39,7 @@ class WelcomeScreenV2 extends StatelessWidget { ), const SizedBox(height: 24), QuantusButton.simple( - label: 'Import Wallet', + label: l10n.welcomeImportWallet, onTap: () => Navigator.push( context, MaterialPageRoute( diff --git a/mobile-app/lib/wallet_initializer.dart b/mobile-app/lib/wallet_initializer.dart index 6771011c..349fdda4 100644 --- a/mobile-app/lib/wallet_initializer.dart +++ b/mobile-app/lib/wallet_initializer.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/migration_dialog.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -89,16 +90,22 @@ class WalletInitializerState extends ConsumerState { } Future _showMnemonicLostDialog() async { + final l10n = ref.read(l10nProvider); + await BottomSheetContainer.show( context, builder: (ctx) => BottomSheetContainer( - title: 'Wallet Error', + title: l10n.walletInitErrorTitle, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Unable to find secret phrase. Please restore your wallet.', style: ctx.themeText.smallParagraph), + Text(l10n.walletInitErrorMessage, style: ctx.themeText.smallParagraph), const SizedBox(height: 32), - QuantusButton.simple(label: 'OK', onTap: () => Navigator.pop(ctx), variant: ButtonVariant.secondary), + QuantusButton.simple( + label: l10n.walletInitErrorButtonLabel, + onTap: () => Navigator.pop(ctx), + variant: ButtonVariant.secondary, + ), ], ), ), diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 0bc5fbfc..2d64d1b4 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -566,6 +566,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct dev" description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 0d743e03..07ec68a2 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -12,7 +12,8 @@ environment: dependencies: flutter: sdk: flutter - + flutter_localizations: + sdk: flutter quantus_sdk: # Shared (canonical versions in melos.yaml) @@ -66,6 +67,7 @@ dev_dependencies: flutter_native_splash: ^2.4.7 flutter: + generate: true uses-material-design: true assets: - .env diff --git a/mobile-app/test/unit/send_screen_logic_test.dart b/mobile-app/test/unit/send_screen_logic_test.dart index b4eb3506..7b319cfc 100644 --- a/mobile-app/test/unit/send_screen_logic_test.dart +++ b/mobile-app/test/unit/send_screen_logic_test.dart @@ -1,14 +1,18 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/v2/screens/send/send_screen_logic.dart'; import 'package:quantus_sdk/generated/planck/pallets/balances.dart' as balances; void main() { group('SendScreenLogic', () { late NumberFormattingService formattingService; + late AppLocalizations l10n; setUp(() { formattingService = NumberFormattingService(localeConfig: LocaleNumberConfig.dotDecimal); + l10n = lookupAppLocalizations(const Locale('en')); }); group('getAmountStatus', () { @@ -116,6 +120,7 @@ void main() { group('getButtonText', () { test('returns "Enter Address" when address is empty', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, // Status doesn't matter if address is empty recipientText: '', @@ -123,11 +128,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Enter Address')); + expect(result, equals(l10n.sendEnterAddress)); }); test('returns "Enter Amount" when status is zeroOrNegative', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.zero, recipientText: 'valid_address', @@ -135,11 +141,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Enter Amount')); + expect(result, equals(l10n.sendLogicEnterAmount)); }); test('returns "Insufficient Balance" when status is insufficient', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.insufficientBalance, recipientText: 'valid_address', @@ -147,11 +154,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Insufficient Balance')); + expect(result, equals(l10n.sendLogicInsufficientBalance)); }); test('returns "Can\'t Self Transfer" for same address', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, recipientText: 'same_address', @@ -159,11 +167,12 @@ void main() { activeAccountId: 'same_address', formattingService: formattingService, ); - expect(result, equals("Can't Self Transfer")); + expect(result, equals(l10n.sendLogicCantSelfTransfer)); }); test('returns "Below Existential Deposit" when status matches', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.belowExistential, recipientText: 'valid_address', @@ -171,11 +180,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Below Existential Deposit')); + expect(result, equals(l10n.sendLogicBelowExistentialDeposit)); }); test('returns Review Send for valid status', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, recipientText: 'valid_address', @@ -183,7 +193,7 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Review Send')); + expect(result, equals(l10n.sendLogicReviewSend)); }); }); diff --git a/quantus_sdk/lib/src/services/datetime_formatting_service.dart b/quantus_sdk/lib/src/services/datetime_formatting_service.dart index 962e2b3a..2581acfc 100644 --- a/quantus_sdk/lib/src/services/datetime_formatting_service.dart +++ b/quantus_sdk/lib/src/services/datetime_formatting_service.dart @@ -142,4 +142,15 @@ class DatetimeFormattingService { return '$hours hr${hours != 1 ? 's' : ''}'; } } + + static String formatPaidAt(DateTime dt, String localeName) { + final date = DateFormat.yMMMd(localeName).format(dt); + final time = DateFormat.jm(localeName).format(dt); + + return '$date, $time'; + } + + static String formatDateGroupLabel(DateTime dt, String localeName) { + return DateFormat.yMMMd(localeName).format(dt); + } } diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index 684b5c61..54243bae 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -28,6 +28,7 @@ class SettingsService { static const String _balanceHiddenKey = 'balance_hidden'; static const String _currencyFlippedKey = 'currency_flipped'; static const String _selectedFiatCurrencyKey = 'selected_fiat_currency'; + static const String _selectedAppLocaleKey = 'selected_app_locale'; static const String _lastPausedTimeKey = 'last_paused_time'; @@ -308,12 +309,31 @@ class SettingsService { await _prefs.setString(_selectedFiatCurrencyKey, currencyCode); } + Future clearSelectedFiatCurrency() async { + await _prefs.remove(_selectedFiatCurrencyKey); + } + /// Returns the persisted fiat currency code (e.g. "USD"), or null when no /// preference has been saved yet (caller should fall back to the default). String? getSelectedFiatCurrency() { return _prefs.getString(_selectedFiatCurrencyKey); } + // Selected App Locale Settings + Future setSelectedAppLocale(String languageCode) async { + await _prefs.setString(_selectedAppLocaleKey, languageCode); + } + + Future clearSelectedAppLocale() async { + await _prefs.remove(_selectedAppLocaleKey); + } + + /// Returns the persisted language code (e.g. "en", "id"), or null when no + /// preference has been saved yet (caller should fall back to English). + String? getSelectedAppLocale() { + return _prefs.getString(_selectedAppLocaleKey); + } + // POS Mode Settings static const String _posModeEnabledKey = 'pos_mode_enabled'; diff --git a/quantus_sdk/lib/src/services/substrate_service.dart b/quantus_sdk/lib/src/services/substrate_service.dart index 941e1273..cec7c175 100644 --- a/quantus_sdk/lib/src/services/substrate_service.dart +++ b/quantus_sdk/lib/src/services/substrate_service.dart @@ -374,7 +374,7 @@ class SubstrateService { Future logout() async { print('Log out!'); await _settingsService.clearAll(); - await TaskmasterService().logout(); + TaskmasterService().logout(); } Future generateMnemonic() async { diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 16169bbe..f9ef454e 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -613,7 +613,7 @@ class TaskmasterService { return _authenticatedGet(uri, OptedInPosition.fromJson); } - Future logout() async { + void logout() { _clearToken(); }