3 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00
DanSandClaude Opus 4.8 ed675f90d8 feat(i18n): add English source + 8-language translations for 193 UI strings
Adds the i18n source for 193 previously-hardcoded UI strings (the
security / PIN / lock flow, first-run seed-backup wizard, witness-rebuild
shutdown dialog, and balance / mining / explorer labels) so they can be
localized. The TR() call-site wrapping rides in the accompanying UI commit.

- 193 keys added to loadBuiltinEnglish() (the English source of truth),
  including one pre-existing missing key (copied_to_clipboard).
- Additive res/lang/{de,es,fr,ja,ko,pt,ru,zh}.json (+193 each); every
  translation's printf format-signature was validated against English
  (0 mismatches) so the runtime validator accepts them.
- Rebuilt res/fonts/NotoSansCJK-Subset.ttf to cover ~30 new Han/Hangul
  glyphs introduced by the zh/ja/ko translations (tofu-free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:50 -05:00
DanSandClaude Opus 4.8 4492aa3425 fix(sync): prioritize getblockchaininfo and pause chat scans while behind
The node kept falling behind the network near the tip because sync
DETECTION was starved: getblockchaininfo was queued behind the
O(mapWallet) wallet RPCs (z_gettotalbalance / z_listunspent), so
longestchain went stale, the wallet decided it was "synced", and it
resumed hammering cs_main — a feedback loop.

- Issue getblockchaininfo FIRST each cycle and skip the balance/address/
  tx refresh entirely while behind, so sync state (and kSyncProfile)
  updates before any heavy wallet scan runs.
- Gate the two chat note scans (refreshChatNoteBudgetNode /
  fastScanChatMemos) on effectivelySyncing() and the active page, so chat
  memo scanning no longer competes with block connection during sync.
- Windows debug.log tailer: reset the read offset when dragonxd truncates
  the log on startup (it was stranding at Block:0 with no witness/rescan
  progress).
- Tests cover the getblockchaininfo-first ordering and behind-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:30 -05:00
50 changed files with 3023 additions and 395 deletions
+4
View File
@@ -521,6 +521,8 @@ set(APP_SOURCES
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
src/ui/windows/faq_dialog.cpp
src/ui/windows/faq_content.cpp
src/ui/windows/key_export_dialog.cpp
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
@@ -657,6 +659,8 @@ set(APP_HEADERS
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/faq_dialog.h
src/ui/windows/faq_content.h
src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h
Binary file not shown.
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "Betrag:",
"animate_avatars": "Avatare animieren",
"appearance": "ERSCHEINUNGSBILD",
"appx_back": "Zurück",
"appx_back_up_seed_phrase_title": "Sichern Sie Ihre Seed-Phrase",
"appx_birthday_block_height": "Geburtstag (Blockhöhe): %llu — sichern Sie auch dies.",
"appx_blockchain_data_deleted": "Blockchain-Daten gelöscht (%d Elemente). Der Daemon wird neu gestartet, um sich erneut mit dem Netzwerk zu synchronisieren.",
"appx_blockchain_maintenance_in_progress": "Eine Blockchain-Wartung läuft bereits.",
"appx_blockchain_rescan_complete": "Blockchain-Neuscan abgeschlossen",
"appx_bootstrap_complete_reconciling": "Bootstrap abgeschlossen — Ihr Wallet wird mit den neuen Chain-Daten abgeglichen.",
"appx_cancel": "Abbrechen",
"appx_cleaning_up": "Aufräumen...",
"appx_confirm_your_backup": "Backup bestätigen",
"appx_copied_clipboard_autoclears": "Kopiert — Zwischenablage wird in 45s automatisch geleert",
"appx_copy": "Kopieren",
"appx_could_not_start_restore": "Wiederherstellung konnte nicht gestartet werden",
"appx_create_failed_prefix": "Erstellen fehlgeschlagen: ",
"appx_creating_your_wallet": "Ihr Wallet wird erstellt…",
"appx_daemon_error": "Daemon-Fehler",
"appx_daemon_reinstall_in_progress": "Die Daemon-Neuinstallation läuft bereits.",
"appx_disconnecting": "Verbindung wird getrennt...",
"appx_done": "Fertig",
"appx_dragonxd_output": "dragonxd-Ausgabe",
"appx_encrypting_wallet": "Wallet wird verschlüsselt...",
"appx_fullnode_lifecycle_unavailable_lite": "Full-Node-Lebenszyklusaktionen sind im Lite-Build nicht verfügbar",
"appx_installing_bundled_daemon": "Mitgelieferter Daemon wird installiert — der Node stoppt, aktualisiert und startet neu...",
"appx_invalid_payment_uri_prefix": "Ungültige Zahlungs-URI: ",
"appx_ive_written_it_down": "Ich habe sie notiert",
"appx_keep_node_running_and_quit": "Node weiterlaufen lassen & beenden",
"appx_last_block_n": "Letzter Block: %d",
"appx_last_used_wallet_not_found_prefix": "Ihre zuletzt verwendete Wallet-Datei (",
"appx_last_used_wallet_not_found_suffix": ") wurde nicht gefunden — stattdessen wurde das Standard-Wallet geöffnet. Wenn Sie sie verschoben haben, stellen Sie sie wieder her und wechseln Sie über die Wallet-Liste zurück.",
"appx_low_spec_mode_disabled": "Low-Spec-Modus deaktiviert",
"appx_low_spec_mode_enabled": "Low-Spec-Modus aktiviert",
"appx_miner_stopped_prefix": "Miner gestoppt: ",
"appx_miner_stopped_unexpectedly": "Miner wurde unerwartet gestoppt.",
"appx_n_min_n_sec": "%d Min %d Sek",
"appx_n_seconds": "%d Sekunden",
"appx_no_bundled_daemon_to_install": "Dieser Build enthält keinen mitgelieferten Daemon zur Installation",
"appx_no_embedded_daemon_to_install": "Dieser Build enthält keinen eingebetteten Daemon zur Installation",
"appx_node_busy_restarting": "Der Node ist mit dem Neustart beschäftigt — versuchen Sie es gleich erneut.",
"appx_node_rebuilding_witness_cache": "Node baut seinen Witness-Cache neu auf",
"appx_not_next_word": " — das ist nicht das nächste Wort",
"appx_payment_request_loaded": "Zahlungsanforderung geladen",
"appx_pool_miner_connected_and_hashing": "Pool-Miner verbunden und aktiv.",
"appx_progress_n_of_n": "Fortschritt: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Sapling-Note-Witnesses werden neu aufgebaut…",
"appx_rebuilding_witness_cache_blocks_left": "Witness-Cache wird neu aufgebaut %.0f%% — %d Blöcke verbleiben",
"appx_rebuilding_witness_cache_pct": "Witness-Cache wird neu aufgebaut %.0f%%",
"appx_recovery_phrase_word_count": "Die Wiederherstellungsphrase sollte 24 Wörter haben — Sie haben %d.",
"appx_restarting_daemon_rescan_flag": "Daemon wird mit -rescan-Flag neu gestartet...",
"appx_restarting_daemon_zapwallettxes": "Daemon wird mit -zapwallettxes=2 neu gestartet (Wallet-Reparatur)...",
"appx_restoring_your_wallet": "Ihr Wallet wird wiederhergestellt…",
"appx_seed_backup_warning": "Diese 24 Wörter sind die EINZIGE Möglichkeit, Ihr Wallet wiederherzustellen. Notieren Sie sie in der richtigen Reihenfolge, bewahren Sie sie offline auf und geben Sie sie niemals weiter. Wenn Sie sie verlieren, sind Ihre Guthaben für immer verloren.",
"appx_seed_not_backed_up_warning": "Sie haben Ihren Seed nicht gesichert — Guthaben könnten verloren gehen. Trotzdem überspringen?",
"appx_sending_stop_command_to_daemon": "Stopp-Befehl wird an Daemon gesendet...",
"appx_setting_initial_sapling_witnesses": "Initiale Sapling-Witnesses werden gesetzt %.0f%%",
"appx_shutdown_complete": "Herunterfahren abgeschlossen",
"appx_simple_background_disabled": "Einfacher Hintergrund deaktiviert",
"appx_simple_background_enabled": "Einfacher Hintergrund aktiviert",
"appx_skip": "Überspringen",
"appx_skip_anyway": "Trotzdem überspringen",
"appx_still_status_prefix": "Weiterhin \"",
"appx_still_status_suffix": "\" — ein erzwungenes Beenden jetzt kann die Chain-Daten beschädigen.",
"appx_stop_anyway_and_quit": "Trotzdem stoppen & beenden",
"appx_stopping_daemon_deleting_blockchain": "Daemon wird gestoppt und Blockchain-Daten werden gelöscht...",
"appx_stopping_node_discards_rebuild": "Wenn Sie den Node jetzt stoppen, wird der laufende Neuaufbau verworfen und beim nächsten Öffnen des Wallets neu gestartet (mehrere Minuten). Sie können den Node stattdessen weiterlaufen lassen.",
"appx_stopping_pool_miner": "Pool-Miner wird gestoppt...",
"appx_syncing_pct_block_n_of_n": "Synchronisierung %.1f%% — Block %d / %d",
"appx_tap_words_in_order": "Tippen Sie die Wörter in der richtigen Reihenfolge an, um zu bestätigen, dass Sie sie gespeichert haben.",
"appx_theme_effects_disabled": "Design-Effekte deaktiviert",
"appx_theme_effects_enabled": "Design-Effekte aktiviert",
"appx_theme_prefix": "Design: ",
"appx_use_settings_restart_daemon_hint": "Verwenden Sie Einstellungen > Daemon neu starten, um es erneut zu versuchen",
"appx_waiting_for_daemon_to_encrypt_wallet": "Warten, bis der Daemon das Wallet verschlüsselt...",
"appx_wallet_created_and_backed_up": "Wallet erstellt und gesichert.",
"appx_wallet_open_failed_prefix": "Öffnen des Wallets fehlgeschlagen: ",
"auto_shield": "Mining automatisch abschirmen",
"av_intro": "Mining-Software wird oft als potenziell unerwünscht eingestuft. Führen Sie diese Schritte aus, um das Pool-Mining zu aktivieren:",
"av_open_security": "Windows-Sicherheit öffnen",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "Abgeschirmt: %.8f",
"balance_syncing_pct": "Synchronisiere %.1f%%",
"balance_transparent_fmt": "Transparent: %.8f",
"baltab_market": "Markt",
"baltab_market_price_4dp": "Markt: $%.4f",
"baltab_market_price_8dp": "Markt: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% des Gesamtbetrags · %d Z-addr",
"baltab_shielded": "Geschützt",
"baltab_shielded_amount": "Geschützt %.8f",
"baltab_t_addresses_count": "%d T-Adressen",
"baltab_total_balance": "Gesamtguthaben",
"baltab_transparent": "Transparent",
"baltab_transparent_amount": "Transparent %.8f",
"ban": "Sperren",
"banned_peers": "Gesperrte Peers",
"block": "Block",
@@ -414,6 +499,7 @@
"contacts_shape_square": "Quadrat",
"contacts_shape_tab": "Reiter",
"copied": "Kopiert!",
"copied_to_clipboard": "In die Zwischenablage kopiert",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren",
@@ -593,6 +679,63 @@
"general": "Allgemein",
"generating": "Wird generiert",
"go_to_receive": "Zum Empfangen",
"grpa_current_block_paren": "(Aktuell: %d)",
"grpa_days_ago": "vor %lld Tagen",
"grpa_dbg_addrman": "Verfolgung und Verwaltung von Peer-Adressen",
"grpa_dbg_alert": "Meldungen des Alarmsystems",
"grpa_dbg_bench": "Benchmark-Zeiten für Operationen",
"grpa_dbg_coindb": "Lese-/Schreiboperationen der Coin-Datenbank",
"grpa_dbg_db": "Berkeley-DB-Operationen",
"grpa_dbg_estimatefee": "Algorithmus zur Gebührenschätzung",
"grpa_dbg_http": "Aktivität des HTTP-RPC-Servers",
"grpa_dbg_libevent": "Libevent-Netzwerkbibliothek",
"grpa_dbg_lock": "Debugging von Lock-Konflikten",
"grpa_dbg_mempool": "Aktivität des Transaktions-Mempools",
"grpa_dbg_net": "Netzwerkverbindungen und -nachrichten",
"grpa_dbg_paymentdisclosure": "Zahlungsoffenlegungsprotokoll",
"grpa_dbg_pow": "Proof-of-Work-Mining-Aktivität",
"grpa_dbg_proxy": "SOCKS5-Proxy-Verbindungen",
"grpa_dbg_prune": "Block-Pruning-Operationen",
"grpa_dbg_rand": "Zufallszahlengenerierung",
"grpa_dbg_reindex": "Fortschritt der Blockchain-Neuindizierung",
"grpa_dbg_rpc": "Verarbeitung von RPC-Befehlen",
"grpa_dbg_selectcoins": "Coin-Auswahl für Transaktionen",
"grpa_dbg_tor": "Tor-Integration und Circuit-Informationen",
"grpa_dbg_zmq": "ZeroMQ-Benachrichtigungssystem",
"grpa_dbg_zrpc": "Geschützte (z-addr) RPC-Operationen",
"grpa_enter_private_key_to_import": "Geben Sie einen privaten Schlüssel zum Importieren ein.",
"grpa_error_prefix": "Fehler: ",
"grpa_hr_ago": "vor %lld Std",
"grpa_invalid_response_from_daemon": "Ungültige Antwort vom Daemon",
"grpa_invalid_suffix": " (ungültig)",
"grpa_min_ago": "vor %lld Min",
"grpa_sec_ago": "vor %lld Sek",
"grpa_seed_demo_chat": "Seed-Demo-Chat",
"grpa_showing_first_100_of": "... die ersten 100 von %d werden angezeigt",
"grpa_tab_about": "Über",
"grpa_tab_appearance": "Darstellung",
"grpa_tab_backup_data": "Backup & Daten",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorer",
"grpa_tab_node_security": "Node & Sicherheit",
"grpa_tab_wallet": "Wallet",
"grpa_unexpected_getblockhash_result": "unerwartetes getblockhash-Ergebnis",
"grpb_copy": "Kopieren",
"grpb_max": "Max",
"grpb_new_badge_suffix": " [NEU]",
"grpb_preview_msg_payment_through": "Ist die Zahlung durchgegangen? 🙂",
"grpb_preview_msg_sending_rest": "Sende jetzt den Rest 👍",
"grpb_preview_msg_yep_confirmed": "Ja — gerade bestätigt ✅",
"grpb_selected_suffix": "\n(ausgewählt)",
"grpb_tooltip_address_balance": "%s\nGuthaben: %.8f %s%s",
"grpb_undo_clear": "Löschen rückgängig",
"grpc_benchmark_inconclusive": "Benchmark nicht aussagekräftig: es wurden keine Hashrate-Werte aufgezeichnet. Prüfen Sie die Pool-Verbindung und versuchen Sie es erneut.",
"grpc_benchmark_takes_secs": "Der Benchmark dauert ~%ds und unterbricht das Mining. Erneut klicken zum Starten.",
"grpc_bootstrap_failed": "Bootstrap fehlgeschlagen",
"grpc_bootstrap_not_initialized": "Bootstrap nicht initialisiert",
"grpc_hashrate_fee": "%s %s%% Gebühr",
"grpc_key_not_available": "Schlüssel für diese Adresse nicht verfügbar",
"grpc_na": "N/V",
"height": "Höhe",
"help": "Hilfe",
"hidden_tag": " (versteckt)",
@@ -1240,6 +1383,45 @@
"screenshot_sweep_full": "Vollständiger UI-Durchlauf",
"search_icons": "Symbole suchen...",
"search_placeholder": "Suchen...",
"sec_changing_passphrase": "Passphrase wird geändert...",
"sec_changing_pin": "PIN wird geändert...",
"sec_couldnt_lock_wallet": "Wallet konnte nicht gesperrt werden — es ist weiterhin entsperrt. Prüfen Sie die Daemon-Verbindung.",
"sec_encrypted_backup_suffix": "\nVerschlüsseltes Backup: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Wallet wird verschlüsselt...",
"sec_encryption_did_not_complete": "Die Wallet-Verschlüsselung wurde nicht abgeschlossen — Ihr Wallet ist NICHT verschlüsselt. Öffnen Sie die Einstellungen, um die Verschlüsselung abzuschließen.",
"sec_encryption_failed_prefix": "Verschlüsselung fehlgeschlagen: ",
"sec_failed_prefix": "Fehlgeschlagen: ",
"sec_failed_to_create_vault": "Tresor konnte nicht erstellt werden",
"sec_importing_keys_rescanning": "Schlüssel werden importiert & Blockchain neu gescannt — Wallet ist währenddessen nutzbar",
"sec_incorrect_current_pin": "Aktuelle PIN falsch",
"sec_incorrect_passphrase_decrypt": "Falsche Passphrase",
"sec_incorrect_passphrase_pin_setup": "Falsche Passphrase",
"sec_incorrect_pin_remove": "Falsche PIN",
"sec_internal_error_change_pin": "Interner Fehler",
"sec_internal_error_remove_pin": "Interner Fehler",
"sec_mode_passphrase": " Passphrase",
"sec_not_connected_to_daemon": "Nicht mit Daemon verbunden",
"sec_not_connected_to_daemon_pin": "Nicht mit Daemon verbunden",
"sec_passphrase_changed_successfully": "Passphrase erfolgreich geändert",
"sec_pin_changed_successfully": "PIN erfolgreich geändert",
"sec_pin_removed": "PIN entfernt",
"sec_pin_set_successfully": "PIN erfolgreich festgelegt",
"sec_restart_daemon_for_encryption": "Bitte starten Sie Ihren Daemon neu, damit die Verschlüsselung wirksam wird.",
"sec_too_many_attempts_wait": "Zu viele Versuche. Warten Sie %.0f Sekunden...",
"sec_total_elapsed_fmt": "Gesamtdauer: %dm %02ds",
"sec_unlock_button": "Entsperren",
"sec_unlock_failed_prefix": "Entsperren fehlgeschlagen: ",
"sec_unlocking_fmt": "Entsperren%s",
"sec_use_passphrase_instead": "Stattdessen Passphrase verwenden",
"sec_use_pin_instead": "Stattdessen PIN verwenden",
"sec_verifying_passphrase": "Passphrase wird überprüft...",
"sec_verifying_pin": "PIN wird überprüft...",
"sec_wallet_decrypted_all_keys_imported": "Wallet erfolgreich entschlüsselt! Alle Schlüssel importiert.",
"sec_wallet_encrypted_and_pin_set": "Wallet verschlüsselt & PIN festgelegt",
"sec_wallet_encrypted_but_pin_vault_failed": "Wallet verschlüsselt, aber PIN-Tresor fehlgeschlagen",
"sec_wallet_encrypted_restarting_daemon": "Wallet verschlüsselt. Daemon wird neu gestartet...",
"sec_wallet_encrypted_successfully": "Wallet erfolgreich verschlüsselt",
"sec_wallet_locked_title": "Wallet gesperrt",
"security": "SICHERHEIT",
"seed_backup_button": "Wiederherstellungsphrase",
"seed_backup_close": "Schließen",
@@ -1461,6 +1643,17 @@
"sweep_to": "Gefegt an:",
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
"sweep_tx": "Transaktion:",
"swin_connection_failed": "Verbindung fehlgeschlagen: ",
"swin_connection_successful": "Verbindung erfolgreich!\ndragonxd-Version: ",
"swin_invalid_suffix": " (ungültig)",
"swin_no_history_file_found": "Keine Verlaufsdatei gefunden",
"swin_rescan_failed": "Neuscan fehlgeschlagen: ",
"swin_rescan_started_from_block": "Neuscan gestartet ab Block ",
"swin_rescan_to": " bis ",
"swin_rpc_client_not_initialized": "RPC-Client nicht initialisiert",
"swin_settings_saved": "Einstellungen gespeichert",
"swin_theme_list_refreshed": "Design-Liste aktualisiert",
"swin_ztx_history_cleared": "Z-Transaktionsverlauf gelöscht",
"switch_corrupt_body": "Diese Wallet scheint beschädigt zu sein – der Knoten konnte sie nicht öffnen. Aus einem Backup wiederherstellen, neu erstellen oder eine Reparatur versuchen.",
"switch_corrupt_repair": "Reparatur versuchen (Salvage)",
"switch_progress_background": "Im Hintergrund fortsetzen",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "Cantidad:",
"animate_avatars": "Animar avatares",
"appearance": "APARIENCIA",
"appx_back": "Atrás",
"appx_back_up_seed_phrase_title": "Haz una copia de tu frase semilla",
"appx_birthday_block_height": "Fecha de creación (altura de bloque): %llu — guárdala también.",
"appx_blockchain_data_deleted": "Datos de la cadena de bloques eliminados (%d elementos). El daemon se está reiniciando para volver a sincronizar desde la red.",
"appx_blockchain_maintenance_in_progress": "Ya hay una operación de mantenimiento de la cadena de bloques en curso.",
"appx_blockchain_rescan_complete": "Reescaneo de la cadena de bloques completado",
"appx_bootstrap_complete_reconciling": "Arranque inicial completado; reconciliando tu cartera con los nuevos datos de la cadena.",
"appx_cancel": "Cancelar",
"appx_cleaning_up": "Limpiando...",
"appx_confirm_your_backup": "Confirma tu copia de seguridad",
"appx_copied_clipboard_autoclears": "Copiado; el portapapeles se borra automáticamente en 45 s",
"appx_copy": "Copiar",
"appx_could_not_start_restore": "No se pudo iniciar la restauración",
"appx_create_failed_prefix": "Error al crear: ",
"appx_creating_your_wallet": "Creando tu cartera…",
"appx_daemon_error": "Error del daemon",
"appx_daemon_reinstall_in_progress": "La reinstalación del daemon ya está en curso.",
"appx_disconnecting": "Desconectando...",
"appx_done": "Listo",
"appx_dragonxd_output": "Salida de dragonxd",
"appx_encrypting_wallet": "Cifrando la cartera...",
"appx_fullnode_lifecycle_unavailable_lite": "Las acciones de ciclo de vida del nodo completo no están disponibles en la versión lite",
"appx_installing_bundled_daemon": "Instalando el daemon incluido; el nodo se detendrá, se actualizará y se reiniciará...",
"appx_invalid_payment_uri_prefix": "URI de pago no válida: ",
"appx_ive_written_it_down": "Ya la anoté",
"appx_keep_node_running_and_quit": "Mantener el nodo y salir",
"appx_last_block_n": "Último bloque: %d",
"appx_last_used_wallet_not_found_prefix": "Tu último archivo de cartera usado (",
"appx_last_used_wallet_not_found_suffix": ") no se encontró; se abrió la cartera predeterminada en su lugar. Si lo moviste, restáuralo y vuelve a él desde la lista de carteras.",
"appx_low_spec_mode_disabled": "Modo de bajos recursos desactivado",
"appx_low_spec_mode_enabled": "Modo de bajos recursos activado",
"appx_miner_stopped_prefix": "Minero detenido: ",
"appx_miner_stopped_unexpectedly": "El minero se detuvo inesperadamente.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d segundos",
"appx_no_bundled_daemon_to_install": "Esta versión no incluye un daemon para instalar",
"appx_no_embedded_daemon_to_install": "Esta versión no incluye un daemon integrado para instalar",
"appx_node_busy_restarting": "El nodo está ocupado reiniciándose; inténtalo de nuevo en un momento.",
"appx_node_rebuilding_witness_cache": "El nodo está reconstruyendo su caché de testigos",
"appx_not_next_word": " — esa no es la siguiente palabra",
"appx_payment_request_loaded": "Solicitud de pago cargada",
"appx_pool_miner_connected_and_hashing": "Minero de pool conectado y calculando hashes.",
"appx_progress_n_of_n": "Progreso: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruyendo testigos de notas Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruyendo la caché de testigos %.0f%% — %d bloques restantes",
"appx_rebuilding_witness_cache_pct": "Reconstruyendo la caché de testigos %.0f%%",
"appx_recovery_phrase_word_count": "La frase de recuperación debe tener 24 palabras; tú tienes %d.",
"appx_restarting_daemon_rescan_flag": "Reiniciando el daemon con la opción -rescan...",
"appx_restarting_daemon_zapwallettxes": "Reiniciando el daemon con -zapwallettxes=2 (reparación de cartera)...",
"appx_restoring_your_wallet": "Restaurando tu cartera…",
"appx_seed_backup_warning": "Estas 24 palabras son la ÚNICA forma de restaurar tu cartera. Anótalas en orden, guárdalas sin conexión y nunca las compartas. Si las pierdes, tus fondos se perderán para siempre.",
"appx_seed_not_backed_up_warning": "No has hecho una copia de tu semilla; podrías perder tus fondos. ¿Omitir de todos modos?",
"appx_sending_stop_command_to_daemon": "Enviando el comando de parada al daemon...",
"appx_setting_initial_sapling_witnesses": "Estableciendo testigos Sapling iniciales %.0f%%",
"appx_shutdown_complete": "Apagado completado",
"appx_simple_background_disabled": "Fondo simple desactivado",
"appx_simple_background_enabled": "Fondo simple activado",
"appx_skip": "Omitir",
"appx_skip_anyway": "Omitir de todos modos",
"appx_still_status_prefix": "Aún \"",
"appx_still_status_suffix": "\" — forzar el cierre ahora podría dañar los datos de la cadena.",
"appx_stop_anyway_and_quit": "Detener de todos modos y salir",
"appx_stopping_daemon_deleting_blockchain": "Deteniendo el daemon y eliminando los datos de la cadena de bloques...",
"appx_stopping_node_discards_rebuild": "Detener el nodo ahora descarta la reconstrucción en curso y la reinicia (varios minutos) la próxima vez que abras la cartera. También puedes dejar el nodo en ejecución.",
"appx_stopping_pool_miner": "Deteniendo el minero de pool...",
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloque %d / %d",
"appx_tap_words_in_order": "Toca las palabras en el orden correcto para confirmar que las guardaste.",
"appx_theme_effects_disabled": "Efectos de tema desactivados",
"appx_theme_effects_enabled": "Efectos de tema activados",
"appx_theme_prefix": "Tema: ",
"appx_use_settings_restart_daemon_hint": "Usa Ajustes > Reiniciar daemon para intentarlo de nuevo",
"appx_waiting_for_daemon_to_encrypt_wallet": "Esperando a que el daemon cifre la cartera...",
"appx_wallet_created_and_backed_up": "Cartera creada y respaldada.",
"appx_wallet_open_failed_prefix": "Error al abrir la cartera: ",
"auto_shield": "Auto-proteger minería",
"av_intro": "El software de minería suele marcarse como potencialmente no deseado. Sigue estos pasos para habilitar la minería en pool:",
"av_open_security": "Abrir Seguridad de Windows",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "Protegido: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f",
"baltab_market": "Mercado",
"baltab_market_price_4dp": "Mercado: $%.4f",
"baltab_market_price_8dp": "Mercado: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% del total · %d Z-addr",
"baltab_shielded": "Blindado",
"baltab_shielded_amount": "Blindado %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Saldo total",
"baltab_transparent": "Transparente",
"baltab_transparent_amount": "Transparente %.8f",
"ban": "Bloquear",
"banned_peers": "Nodos Bloqueados",
"block": "Bloque",
@@ -414,6 +499,7 @@
"contacts_shape_square": "Cuadrado",
"contacts_shape_tab": "Pestaña",
"copied": "¡Copiado!",
"copied_to_clipboard": "Copiado al portapapeles",
"copy": "Copiar",
"copy_address": "Copiar Dirección Completa",
"copy_error": "Copiar Error",
@@ -593,6 +679,63 @@
"general": "General",
"generating": "Generando",
"go_to_receive": "Ir a Recibir",
"grpa_current_block_paren": "(Actual: %d)",
"grpa_days_ago": "hace %lld días",
"grpa_dbg_addrman": "Seguimiento y gestión de direcciones de pares",
"grpa_dbg_alert": "Mensajes del sistema de alertas",
"grpa_dbg_bench": "Tiempos de referencia de las operaciones",
"grpa_dbg_coindb": "Operaciones de lectura/escritura de la base de datos de monedas",
"grpa_dbg_db": "Operaciones de Berkeley DB",
"grpa_dbg_estimatefee": "Algoritmo de estimación de comisiones",
"grpa_dbg_http": "Actividad del servidor RPC HTTP",
"grpa_dbg_libevent": "Biblioteca de red Libevent",
"grpa_dbg_lock": "Depuración de contención de bloqueos",
"grpa_dbg_mempool": "Actividad del pool de memoria de transacciones",
"grpa_dbg_net": "Conexiones y mensajes de red",
"grpa_dbg_paymentdisclosure": "Protocolo de divulgación de pagos",
"grpa_dbg_pow": "Actividad de minería por prueba de trabajo",
"grpa_dbg_proxy": "Conexiones de proxy SOCKS5",
"grpa_dbg_prune": "Operaciones de poda de bloques",
"grpa_dbg_rand": "Generación de números aleatorios",
"grpa_dbg_reindex": "Progreso de reindexación de la cadena de bloques",
"grpa_dbg_rpc": "Procesamiento de comandos RPC",
"grpa_dbg_selectcoins": "Selección de monedas para transacciones",
"grpa_dbg_tor": "Integración de Tor e información de circuitos",
"grpa_dbg_zmq": "Sistema de notificaciones ZeroMQ",
"grpa_dbg_zrpc": "Operaciones RPC blindadas (z-addr)",
"grpa_enter_private_key_to_import": "Introduce una clave privada para importar.",
"grpa_error_prefix": "Error: ",
"grpa_hr_ago": "hace %lld h",
"grpa_invalid_response_from_daemon": "Respuesta no válida del daemon",
"grpa_invalid_suffix": " (no válido)",
"grpa_min_ago": "hace %lld min",
"grpa_sec_ago": "hace %lld s",
"grpa_seed_demo_chat": "Chat de demostración con semilla",
"grpa_showing_first_100_of": "... mostrando los primeros 100 de %d",
"grpa_tab_about": "Acerca de",
"grpa_tab_appearance": "Apariencia",
"grpa_tab_backup_data": "Copia de seguridad y datos",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorador",
"grpa_tab_node_security": "Nodo y seguridad",
"grpa_tab_wallet": "Cartera",
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
"grpb_copy": "Copiar",
"grpb_max": "Máx.",
"grpb_new_badge_suffix": " [NUEVO]",
"grpb_preview_msg_payment_through": "¿Se realizó el pago? 🙂",
"grpb_preview_msg_sending_rest": "Enviando el resto ahora 👍",
"grpb_preview_msg_yep_confirmed": "Sí — acabo de confirmarlo ✅",
"grpb_selected_suffix": "\n(seleccionado)",
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
"grpb_undo_clear": "Deshacer borrado",
"grpc_benchmark_inconclusive": "Prueba de rendimiento no concluyente: no se registraron muestras de tasa de hash. Comprueba la conexión con el pool e inténtalo de nuevo.",
"grpc_benchmark_takes_secs": "La prueba de rendimiento tarda ~%ds e interrumpe la minería. Haz clic de nuevo para empezar.",
"grpc_bootstrap_failed": "Error en el arranque inicial",
"grpc_bootstrap_not_initialized": "Arranque inicial no inicializado",
"grpc_hashrate_fee": "%s %s%% de comisión",
"grpc_key_not_available": "Clave no disponible para esta dirección",
"grpc_na": "N/D",
"height": "Altura",
"help": "Ayuda",
"hidden_tag": " (oculto)",
@@ -1240,6 +1383,45 @@
"screenshot_sweep_full": "Barrido completo de la interfaz",
"search_icons": "Buscar iconos...",
"search_placeholder": "Buscar...",
"sec_changing_passphrase": "Cambiando la frase de contraseña...",
"sec_changing_pin": "Cambiando el PIN...",
"sec_couldnt_lock_wallet": "No se pudo bloquear la cartera; sigue desbloqueada. Comprueba la conexión con el daemon.",
"sec_encrypted_backup_suffix": "\nCopia de seguridad cifrada: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Cifrando la cartera...",
"sec_encryption_did_not_complete": "El cifrado de la cartera no se completó; tu cartera NO está cifrada. Abre Ajustes para terminar de cifrarla.",
"sec_encryption_failed_prefix": "Error de cifrado: ",
"sec_failed_prefix": "Error: ",
"sec_failed_to_create_vault": "No se pudo crear la bóveda",
"sec_importing_keys_rescanning": "Importando claves y volviendo a escanear la cadena de bloques; la cartera se puede usar mientras tanto",
"sec_incorrect_current_pin": "PIN actual incorrecto",
"sec_incorrect_passphrase_decrypt": "Frase de contraseña incorrecta",
"sec_incorrect_passphrase_pin_setup": "Frase de contraseña incorrecta",
"sec_incorrect_pin_remove": "PIN incorrecto",
"sec_internal_error_change_pin": "Error interno",
"sec_internal_error_remove_pin": "Error interno",
"sec_mode_passphrase": " Frase de contraseña",
"sec_not_connected_to_daemon": "Sin conexión con el daemon",
"sec_not_connected_to_daemon_pin": "Sin conexión con el daemon",
"sec_passphrase_changed_successfully": "Frase de contraseña cambiada correctamente",
"sec_pin_changed_successfully": "PIN cambiado correctamente",
"sec_pin_removed": "PIN eliminado",
"sec_pin_set_successfully": "PIN configurado correctamente",
"sec_restart_daemon_for_encryption": "Reinicia el daemon para que el cifrado surta efecto.",
"sec_too_many_attempts_wait": "Demasiados intentos. Espera %.0f segundos...",
"sec_total_elapsed_fmt": "Tiempo total: %dm %02ds",
"sec_unlock_button": "Desbloquear",
"sec_unlock_failed_prefix": "Error al desbloquear: ",
"sec_unlocking_fmt": "Desbloqueando%s",
"sec_use_passphrase_instead": "Usar frase de contraseña en su lugar",
"sec_use_pin_instead": "Usar PIN en su lugar",
"sec_verifying_passphrase": "Verificando la frase de contraseña...",
"sec_verifying_pin": "Verificando el PIN...",
"sec_wallet_decrypted_all_keys_imported": "¡Cartera descifrada correctamente! Todas las claves importadas.",
"sec_wallet_encrypted_and_pin_set": "Cartera cifrada y PIN configurado",
"sec_wallet_encrypted_but_pin_vault_failed": "Cartera cifrada, pero falló la bóveda del PIN",
"sec_wallet_encrypted_restarting_daemon": "Cartera cifrada. Reiniciando el daemon...",
"sec_wallet_encrypted_successfully": "Cartera cifrada correctamente",
"sec_wallet_locked_title": "Cartera bloqueada",
"security": "SEGURIDAD",
"seed_backup_button": "Frase de recuperación",
"seed_backup_close": "Cerrar",
@@ -1461,6 +1643,17 @@
"sweep_to": "Barrido a:",
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
"sweep_tx": "Transacción:",
"swin_connection_failed": "Error de conexión: ",
"swin_connection_successful": "¡Conexión correcta!\nVersión de dragonxd: ",
"swin_invalid_suffix": " (no válido)",
"swin_no_history_file_found": "No se encontró ningún archivo de historial",
"swin_rescan_failed": "Error en el reescaneo: ",
"swin_rescan_started_from_block": "Reescaneo iniciado desde el bloque ",
"swin_rescan_to": " hasta ",
"swin_rpc_client_not_initialized": "Cliente RPC no inicializado",
"swin_settings_saved": "Ajustes guardados",
"swin_theme_list_refreshed": "Lista de temas actualizada",
"swin_ztx_history_cleared": "Historial de transacciones Z borrado",
"switch_corrupt_body": "Esta cartera parece dañada: el nodo no pudo abrirla. Restáurala desde una copia de seguridad, vuelve a crearla o intenta repararla.",
"switch_corrupt_repair": "Intentar reparar (salvage)",
"switch_progress_background": "Continuar en segundo plano",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "Montant :",
"animate_avatars": "Animer les avatars",
"appearance": "APPARENCE",
"appx_back": "Retour",
"appx_back_up_seed_phrase_title": "Sauvegardez votre phrase de récupération",
"appx_birthday_block_height": "Naissance (hauteur de bloc) : %llu — sauvegardez-la également.",
"appx_blockchain_data_deleted": "Données de la blockchain supprimées (%d éléments). Le démon redémarre pour se resynchroniser depuis le réseau.",
"appx_blockchain_maintenance_in_progress": "Une opération de maintenance de la blockchain est déjà en cours.",
"appx_blockchain_rescan_complete": "Nouvelle analyse de la blockchain terminée",
"appx_bootstrap_complete_reconciling": "Amorçage terminé — réconciliation de votre portefeuille avec les nouvelles données de la chaîne.",
"appx_cancel": "Annuler",
"appx_cleaning_up": "Nettoyage...",
"appx_confirm_your_backup": "Confirmez votre sauvegarde",
"appx_copied_clipboard_autoclears": "Copié — le presse-papiers s'efface automatiquement dans 45 s",
"appx_copy": "Copier",
"appx_could_not_start_restore": "Impossible de démarrer la restauration",
"appx_create_failed_prefix": "Échec de la création : ",
"appx_creating_your_wallet": "Création de votre portefeuille…",
"appx_daemon_error": "Erreur du démon",
"appx_daemon_reinstall_in_progress": "La réinstallation du démon est déjà en cours.",
"appx_disconnecting": "Déconnexion...",
"appx_done": "Terminé",
"appx_dragonxd_output": "Sortie de dragonxd",
"appx_encrypting_wallet": "Chiffrement du portefeuille...",
"appx_fullnode_lifecycle_unavailable_lite": "Les actions de cycle de vie du nœud complet ne sont pas disponibles dans la version allégée",
"appx_installing_bundled_daemon": "Installation du démon intégré — le nœud va s'arrêter, se mettre à jour, puis redémarrer...",
"appx_invalid_payment_uri_prefix": "URI de paiement invalide : ",
"appx_ive_written_it_down": "Je les ai notés",
"appx_keep_node_running_and_quit": "Laisser le nœud actif et quitter",
"appx_last_block_n": "Dernier bloc : %d",
"appx_last_used_wallet_not_found_prefix": "Votre dernier fichier de portefeuille utilisé (",
"appx_last_used_wallet_not_found_suffix": ") est introuvable — le portefeuille par défaut a été ouvert à la place. Si vous l'avez déplacé, restaurez-le et revenez à celui-ci depuis la liste des portefeuilles.",
"appx_low_spec_mode_disabled": "Mode faibles ressources désactivé",
"appx_low_spec_mode_enabled": "Mode faibles ressources activé",
"appx_miner_stopped_prefix": "Mineur arrêté : ",
"appx_miner_stopped_unexpectedly": "Le mineur s'est arrêté de manière inattendue.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d secondes",
"appx_no_bundled_daemon_to_install": "Cette version ne contient aucun démon intégré à installer",
"appx_no_embedded_daemon_to_install": "Cette version ne contient aucun démon embarqué à installer",
"appx_node_busy_restarting": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
"appx_node_rebuilding_witness_cache": "Le nœud reconstruit son cache de témoins",
"appx_not_next_word": " — ce n'est pas le mot suivant",
"appx_payment_request_loaded": "Demande de paiement chargée",
"appx_pool_miner_connected_and_hashing": "Mineur de pool connecté et en cours de hachage.",
"appx_progress_n_of_n": "Progression : %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruction des témoins de notes Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruction du cache de témoins %.0f%% — %d blocs restants",
"appx_rebuilding_witness_cache_pct": "Reconstruction du cache de témoins %.0f%%",
"appx_recovery_phrase_word_count": "La phrase de récupération doit comporter 24 mots — vous en avez %d.",
"appx_restarting_daemon_rescan_flag": "Redémarrage du démon avec l'option -rescan...",
"appx_restarting_daemon_zapwallettxes": "Redémarrage du démon avec -zapwallettxes=2 (réparation du portefeuille)...",
"appx_restoring_your_wallet": "Restauration de votre portefeuille…",
"appx_seed_backup_warning": "Ces 24 mots sont le SEUL moyen de restaurer votre portefeuille. Notez-les dans l'ordre, conservez-les hors ligne et ne les partagez jamais. Si vous les perdez, vos fonds seront perdus à jamais.",
"appx_seed_not_backed_up_warning": "Vous n'avez pas sauvegardé votre phrase de récupération — des fonds pourraient être perdus. Ignorer quand même ?",
"appx_sending_stop_command_to_daemon": "Envoi de la commande d'arrêt au démon...",
"appx_setting_initial_sapling_witnesses": "Définition des témoins Sapling initiaux %.0f%%",
"appx_shutdown_complete": "Arrêt terminé",
"appx_simple_background_disabled": "Arrière-plan simple désactivé",
"appx_simple_background_enabled": "Arrière-plan simple activé",
"appx_skip": "Ignorer",
"appx_skip_anyway": "Ignorer quand même",
"appx_still_status_prefix": "Toujours « ",
"appx_still_status_suffix": " » — forcer la fermeture maintenant peut corrompre les données de la chaîne.",
"appx_stop_anyway_and_quit": "Arrêter quand même et quitter",
"appx_stopping_daemon_deleting_blockchain": "Arrêt du démon et suppression des données de la blockchain...",
"appx_stopping_node_discards_rebuild": "Arrêter le nœud maintenant abandonne la reconstruction en cours et la relance (plusieurs minutes) à la prochaine ouverture du portefeuille. Vous pouvez plutôt laisser le nœud en marche.",
"appx_stopping_pool_miner": "Arrêt du mineur de pool...",
"appx_syncing_pct_block_n_of_n": "Synchronisation %.1f%% — Bloc %d / %d",
"appx_tap_words_in_order": "Touchez les mots dans le bon ordre pour confirmer que vous les avez enregistrés.",
"appx_theme_effects_disabled": "Effets de thème désactivés",
"appx_theme_effects_enabled": "Effets de thème activés",
"appx_theme_prefix": "Thème : ",
"appx_use_settings_restart_daemon_hint": "Utilisez Paramètres > Redémarrer le démon pour réessayer",
"appx_waiting_for_daemon_to_encrypt_wallet": "En attente du chiffrement du portefeuille par le démon...",
"appx_wallet_created_and_backed_up": "Portefeuille créé et sauvegardé.",
"appx_wallet_open_failed_prefix": "Échec de l'ouverture du portefeuille : ",
"auto_shield": "Auto-blindage du minage",
"av_intro": "Les logiciels de minage sont souvent signalés comme potentiellement indésirables. Suivez ces étapes pour activer le minage en pool :",
"av_open_security": "Ouvrir Sécurité Windows",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "Blindé : %.8f",
"balance_syncing_pct": "Synchronisation %.1f%%",
"balance_transparent_fmt": "Transparent : %.8f",
"baltab_market": "Marché",
"baltab_market_price_4dp": "Marché : %.4f $",
"baltab_market_price_8dp": "Marché : %.8f $",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% du total · %d Z-addr",
"baltab_shielded": "Protégé",
"baltab_shielded_amount": "Protégé %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Solde total",
"baltab_transparent": "Transparent",
"baltab_transparent_amount": "Transparent %.8f",
"ban": "Bannir",
"banned_peers": "Pairs bannis",
"block": "Bloc",
@@ -414,6 +499,7 @@
"contacts_shape_square": "Carré",
"contacts_shape_tab": "Onglet",
"copied": "Copié !",
"copied_to_clipboard": "Copié dans le presse-papiers",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur",
@@ -593,6 +679,63 @@
"general": "Général",
"generating": "Génération",
"go_to_receive": "Aller à Recevoir",
"grpa_current_block_paren": "(Actuel : %d)",
"grpa_days_ago": "il y a %lld jours",
"grpa_dbg_addrman": "Suivi et gestion des adresses des pairs",
"grpa_dbg_alert": "Messages du système d'alerte",
"grpa_dbg_bench": "Chronométrage des performances des opérations",
"grpa_dbg_coindb": "Opérations de lecture/écriture de la base de données des pièces",
"grpa_dbg_db": "Opérations Berkeley DB",
"grpa_dbg_estimatefee": "Algorithme d'estimation des frais",
"grpa_dbg_http": "Activité du serveur RPC HTTP",
"grpa_dbg_libevent": "Bibliothèque réseau Libevent",
"grpa_dbg_lock": "Débogage de la contention des verrous",
"grpa_dbg_mempool": "Activité du pool mémoire des transactions",
"grpa_dbg_net": "Connexions et messages réseau",
"grpa_dbg_paymentdisclosure": "Protocole de divulgation de paiement",
"grpa_dbg_pow": "Activité de minage par preuve de travail",
"grpa_dbg_proxy": "Connexions proxy SOCKS5",
"grpa_dbg_prune": "Opérations d'élagage des blocs",
"grpa_dbg_rand": "Génération de nombres aléatoires",
"grpa_dbg_reindex": "Progression de la réindexation de la blockchain",
"grpa_dbg_rpc": "Traitement des commandes RPC",
"grpa_dbg_selectcoins": "Sélection des pièces pour les transactions",
"grpa_dbg_tor": "Intégration de Tor et informations de circuit",
"grpa_dbg_zmq": "Système de notification ZeroMQ",
"grpa_dbg_zrpc": "Opérations RPC protégées (z-addr)",
"grpa_enter_private_key_to_import": "Saisissez une clé privée à importer.",
"grpa_error_prefix": "Erreur : ",
"grpa_hr_ago": "il y a %lld h",
"grpa_invalid_response_from_daemon": "Réponse invalide du démon",
"grpa_invalid_suffix": " (invalide)",
"grpa_min_ago": "il y a %lld min",
"grpa_sec_ago": "il y a %lld s",
"grpa_seed_demo_chat": "Chat de démonstration seed",
"grpa_showing_first_100_of": "... affichage des 100 premiers sur %d",
"grpa_tab_about": "À propos",
"grpa_tab_appearance": "Apparence",
"grpa_tab_backup_data": "Sauvegarde et données",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorateur",
"grpa_tab_node_security": "Nœud et sécurité",
"grpa_tab_wallet": "Portefeuille",
"grpa_unexpected_getblockhash_result": "résultat getblockhash inattendu",
"grpb_copy": "Copier",
"grpb_max": "Max",
"grpb_new_badge_suffix": " [NOUVEAU]",
"grpb_preview_msg_payment_through": "Le paiement est-il passé ? 🙂",
"grpb_preview_msg_sending_rest": "J'envoie le reste maintenant 👍",
"grpb_preview_msg_yep_confirmed": "Oui — je viens de confirmer ✅",
"grpb_selected_suffix": "\n(sélectionné)",
"grpb_tooltip_address_balance": "%s\nSolde : %.8f %s%s",
"grpb_undo_clear": "Annuler l'effacement",
"grpc_benchmark_inconclusive": "Benchmark non concluant : aucun échantillon de taux de hachage n'a été enregistré. Vérifiez la connexion au pool et réessayez.",
"grpc_benchmark_takes_secs": "Le benchmark prend environ %d s et interrompt le minage. Cliquez de nouveau pour démarrer.",
"grpc_bootstrap_failed": "Échec de l'amorçage",
"grpc_bootstrap_not_initialized": "Amorçage non initialisé",
"grpc_hashrate_fee": "%s %s%% de frais",
"grpc_key_not_available": "Clé non disponible pour cette adresse",
"grpc_na": "N/D",
"height": "Hauteur",
"help": "Aide",
"hidden_tag": " (masqué)",
@@ -1240,6 +1383,45 @@
"screenshot_sweep_full": "Balayage complet de l'interface",
"search_icons": "Rechercher des icônes...",
"search_placeholder": "Rechercher...",
"sec_changing_passphrase": "Modification de la phrase secrète...",
"sec_changing_pin": "Modification du PIN...",
"sec_couldnt_lock_wallet": "Impossible de verrouiller le portefeuille — il reste déverrouillé. Vérifiez la connexion au démon.",
"sec_encrypted_backup_suffix": "\nSauvegarde chiffrée : wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Chiffrement du portefeuille...",
"sec_encryption_did_not_complete": "Le chiffrement du portefeuille ne s'est pas terminé — votre portefeuille N'est PAS chiffré. Ouvrez les Paramètres pour terminer le chiffrement.",
"sec_encryption_failed_prefix": "Échec du chiffrement : ",
"sec_failed_prefix": "Échec : ",
"sec_failed_to_create_vault": "Échec de la création du coffre",
"sec_importing_keys_rescanning": "Importation des clés et nouvelle analyse de la blockchain — le portefeuille reste utilisable pendant l'opération",
"sec_incorrect_current_pin": "PIN actuel incorrect",
"sec_incorrect_passphrase_decrypt": "Phrase secrète incorrecte",
"sec_incorrect_passphrase_pin_setup": "Phrase secrète incorrecte",
"sec_incorrect_pin_remove": "PIN incorrect",
"sec_internal_error_change_pin": "Erreur interne",
"sec_internal_error_remove_pin": "Erreur interne",
"sec_mode_passphrase": " Phrase secrète",
"sec_not_connected_to_daemon": "Non connecté au démon",
"sec_not_connected_to_daemon_pin": "Non connecté au démon",
"sec_passphrase_changed_successfully": "Phrase secrète modifiée avec succès",
"sec_pin_changed_successfully": "PIN modifié avec succès",
"sec_pin_removed": "PIN supprimé",
"sec_pin_set_successfully": "PIN défini avec succès",
"sec_restart_daemon_for_encryption": "Veuillez redémarrer votre démon pour que le chiffrement prenne effet.",
"sec_too_many_attempts_wait": "Trop de tentatives. Patientez %.0f secondes...",
"sec_total_elapsed_fmt": "Temps total écoulé : %dm %02ds",
"sec_unlock_button": "Déverrouiller",
"sec_unlock_failed_prefix": "Échec du déverrouillage : ",
"sec_unlocking_fmt": "Déverrouillage%s",
"sec_use_passphrase_instead": "Utiliser une phrase secrète",
"sec_use_pin_instead": "Utiliser un PIN",
"sec_verifying_passphrase": "Vérification de la phrase secrète...",
"sec_verifying_pin": "Vérification du PIN...",
"sec_wallet_decrypted_all_keys_imported": "Portefeuille déchiffré avec succès ! Toutes les clés ont été importées.",
"sec_wallet_encrypted_and_pin_set": "Portefeuille chiffré et PIN défini",
"sec_wallet_encrypted_but_pin_vault_failed": "Portefeuille chiffré mais échec du coffre PIN",
"sec_wallet_encrypted_restarting_daemon": "Portefeuille chiffré. Redémarrage du démon...",
"sec_wallet_encrypted_successfully": "Portefeuille chiffré avec succès",
"sec_wallet_locked_title": "Portefeuille verrouillé",
"security": "SÉCURITÉ",
"seed_backup_button": "Phrase de récupération",
"seed_backup_close": "Fermer",
@@ -1461,6 +1643,17 @@
"sweep_to": "Balayé vers :",
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
"sweep_tx": "Transaction :",
"swin_connection_failed": "Échec de la connexion : ",
"swin_connection_successful": "Connexion réussie !\nVersion de dragonxd : ",
"swin_invalid_suffix": " (invalide)",
"swin_no_history_file_found": "Aucun fichier d'historique trouvé",
"swin_rescan_failed": "Échec de la nouvelle analyse : ",
"swin_rescan_started_from_block": "Nouvelle analyse démarrée à partir du bloc ",
"swin_rescan_to": " à ",
"swin_rpc_client_not_initialized": "Client RPC non initialisé",
"swin_settings_saved": "Paramètres enregistrés",
"swin_theme_list_refreshed": "Liste des thèmes actualisée",
"swin_ztx_history_cleared": "Historique des transactions Z effacé",
"switch_corrupt_body": "Ce portefeuille semble corrompu — le nœud n'a pas pu l'ouvrir. Restaurez-le depuis une sauvegarde, recréez-le ou tentez de le réparer.",
"switch_corrupt_repair": "Tenter une réparation (salvage)",
"switch_progress_background": "Continuer en arrière-plan",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "金額:",
"animate_avatars": "アバターをアニメーション",
"appearance": "外観",
"appx_back": "戻る",
"appx_back_up_seed_phrase_title": "シードフレーズをバックアップ",
"appx_birthday_block_height": "誕生日(ブロック高): %llu — これもバックアップしてください。",
"appx_blockchain_data_deleted": "ブロックチェーンデータを削除しました(%d件)。ネットワークから再同期するため、デーモンを再起動しています。",
"appx_blockchain_maintenance_in_progress": "ブロックチェーンのメンテナンス操作がすでに進行中です。",
"appx_blockchain_rescan_complete": "ブロックチェーンの再スキャンが完了しました",
"appx_bootstrap_complete_reconciling": "ブートストラップが完了しました。新しいチェーンデータとウォレットを照合しています。",
"appx_cancel": "キャンセル",
"appx_cleaning_up": "クリーンアップしています...",
"appx_confirm_your_backup": "バックアップを確認",
"appx_copied_clipboard_autoclears": "コピーしました — クリップボードは45秒後に自動的にクリアされます",
"appx_copy": "コピー",
"appx_could_not_start_restore": "復元を開始できませんでした",
"appx_create_failed_prefix": "作成に失敗しました: ",
"appx_creating_your_wallet": "ウォレットを作成しています…",
"appx_daemon_error": "デーモンエラー",
"appx_daemon_reinstall_in_progress": "デーモンの再インストールがすでに進行中です。",
"appx_disconnecting": "切断しています...",
"appx_done": "完了",
"appx_dragonxd_output": "dragonxd の出力",
"appx_encrypting_wallet": "ウォレットを暗号化しています...",
"appx_fullnode_lifecycle_unavailable_lite": "フルノードのライフサイクル操作はライトビルドでは利用できません",
"appx_installing_bundled_daemon": "同梱デーモンをインストールしています。ノードが停止、更新、再起動されます...",
"appx_invalid_payment_uri_prefix": "無効な支払いURI: ",
"appx_ive_written_it_down": "書き留めました",
"appx_keep_node_running_and_quit": "ノードを起動したまま終了",
"appx_last_block_n": "最新ブロック: %d",
"appx_last_used_wallet_not_found_prefix": "前回使用したウォレットファイル(",
"appx_last_used_wallet_not_found_suffix": ")が見つかりませんでした。代わりにデフォルトのウォレットを開きました。移動した場合は、元に戻してからウォレット一覧で切り替えてください。",
"appx_low_spec_mode_disabled": "低スペックモードを無効にしました",
"appx_low_spec_mode_enabled": "低スペックモードを有効にしました",
"appx_miner_stopped_prefix": "マイナーが停止しました: ",
"appx_miner_stopped_unexpectedly": "マイナーが予期せず停止しました。",
"appx_n_min_n_sec": "%d分 %d秒",
"appx_n_seconds": "%d秒",
"appx_no_bundled_daemon_to_install": "このビルドにはインストールできる同梱デーモンがありません",
"appx_no_embedded_daemon_to_install": "このビルドにはインストールできる組み込みデーモンがありません",
"appx_node_busy_restarting": "ノードが再起動中でビジー状態です。少し待ってから再試行してください。",
"appx_node_rebuilding_witness_cache": "ノードがウィットネスキャッシュを再構築しています",
"appx_not_next_word": " — 次の単語ではありません",
"appx_payment_request_loaded": "支払いリクエストを読み込みました",
"appx_pool_miner_connected_and_hashing": "プールマイナーが接続し、ハッシュ計算中です。",
"appx_progress_n_of_n": "進捗: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Saplingノートウィットネスを再構築しています…",
"appx_rebuilding_witness_cache_blocks_left": "ウィットネスキャッシュを再構築中 %.0f%% — 残り%dブロック",
"appx_rebuilding_witness_cache_pct": "ウィットネスキャッシュを再構築中 %.0f%%",
"appx_recovery_phrase_word_count": "リカバリーフレーズは24単語である必要があります。現在は%d単語です。",
"appx_restarting_daemon_rescan_flag": "-rescanフラグ付きでデーモンを再起動しています...",
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2(ウォレット修復)付きでデーモンを再起動しています...",
"appx_restoring_your_wallet": "ウォレットを復元しています…",
"appx_seed_backup_warning": "この24個の単語は、ウォレットを復元する唯一の手段です。順番どおりに書き留め、オフラインで保管し、決して他人に教えないでください。紛失すると、資金は永久に失われます。",
"appx_seed_not_backed_up_warning": "シードをバックアップしていません。資金を失う可能性があります。それでもスキップしますか?",
"appx_sending_stop_command_to_daemon": "デーモンに停止コマンドを送信しています...",
"appx_setting_initial_sapling_witnesses": "初期Saplingウィットネスを設定中 %.0f%%",
"appx_shutdown_complete": "シャットダウンが完了しました",
"appx_simple_background_disabled": "シンプル背景を無効にしました",
"appx_simple_background_enabled": "シンプル背景を有効にしました",
"appx_skip": "スキップ",
"appx_skip_anyway": "それでもスキップ",
"appx_still_status_prefix": "まだ「",
"appx_still_status_suffix": "」です — 今強制終了するとチェーンデータが破損する可能性があります。",
"appx_stop_anyway_and_quit": "それでも停止して終了",
"appx_stopping_daemon_deleting_blockchain": "デーモンを停止し、ブロックチェーンデータを削除しています...",
"appx_stopping_node_discards_rebuild": "今ノードを停止すると、進行中の再構築が破棄され、次回ウォレットを開いたときに再度実行されます(数分かかります)。代わりにノードを起動したままにすることもできます。",
"appx_stopping_pool_miner": "プールマイナーを停止しています...",
"appx_syncing_pct_block_n_of_n": "同期中 %.1f%% — ブロック %d / %d",
"appx_tap_words_in_order": "保存したことを確認するため、単語を正しい順番でタップしてください。",
"appx_theme_effects_disabled": "テーマ効果を無効にしました",
"appx_theme_effects_enabled": "テーマ効果を有効にしました",
"appx_theme_prefix": "テーマ: ",
"appx_use_settings_restart_daemon_hint": "設定 > デーモンを再起動 から再試行してください",
"appx_waiting_for_daemon_to_encrypt_wallet": "デーモンによるウォレットの暗号化を待っています...",
"appx_wallet_created_and_backed_up": "ウォレットを作成し、バックアップしました。",
"appx_wallet_open_failed_prefix": "ウォレットを開けませんでした: ",
"auto_shield": "マイニング自動シールド",
"av_intro": "マイニングソフトウェアは、望ましくない可能性があるものとしてフラグが立てられることがよくあります。プールマイニングを有効にするには、次の手順に従ってください。",
"av_open_security": "Windows セキュリティを開く",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "シールド: %.8f",
"balance_syncing_pct": "同期中 %.1f%%",
"balance_transparent_fmt": "透明: %.8f",
"baltab_market": "市場",
"baltab_market_price_4dp": "市場価格: $%.4f",
"baltab_market_price_8dp": "市場価格: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "全体の%.0f%% · %d Z-addr",
"baltab_shielded": "シールド",
"baltab_shielded_amount": "シールド %.8f",
"baltab_t_addresses_count": "%d 個のT-address",
"baltab_total_balance": "合計残高",
"baltab_transparent": "透明",
"baltab_transparent_amount": "透明 %.8f",
"ban": "ブロック",
"banned_peers": "ブロック済みピア",
"block": "ブロック",
@@ -414,6 +499,7 @@
"contacts_shape_square": "四角",
"contacts_shape_tab": "左タブ",
"copied": "コピーしました!",
"copied_to_clipboard": "クリップボードにコピーしました",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー",
@@ -593,6 +679,63 @@
"general": "一般",
"generating": "生成中",
"go_to_receive": "受信へ移動",
"grpa_current_block_paren": "(現在: %d)",
"grpa_days_ago": "%lld日前",
"grpa_dbg_addrman": "ピアアドレスの追跡と管理",
"grpa_dbg_alert": "アラートシステムのメッセージ",
"grpa_dbg_bench": "操作のベンチマーク計測",
"grpa_dbg_coindb": "コインデータベースの読み書き操作",
"grpa_dbg_db": "Berkeley DB の操作",
"grpa_dbg_estimatefee": "手数料見積もりアルゴリズム",
"grpa_dbg_http": "HTTP RPCサーバーの動作",
"grpa_dbg_libevent": "Libevent ネットワークライブラリ",
"grpa_dbg_lock": "ロック競合のデバッグ",
"grpa_dbg_mempool": "トランザクションメモリプールの動作",
"grpa_dbg_net": "ネットワーク接続とメッセージ",
"grpa_dbg_paymentdisclosure": "支払い開示プロトコル",
"grpa_dbg_pow": "プルーフオブワークのマイニング動作",
"grpa_dbg_proxy": "SOCKS5 プロキシ接続",
"grpa_dbg_prune": "ブロックのプルーニング操作",
"grpa_dbg_rand": "乱数生成",
"grpa_dbg_reindex": "ブロックチェーンの再インデックス進捗",
"grpa_dbg_rpc": "RPCコマンドの処理",
"grpa_dbg_selectcoins": "トランザクション用のコイン選択",
"grpa_dbg_tor": "Tor統合とサーキット情報",
"grpa_dbg_zmq": "ZeroMQ 通知システム",
"grpa_dbg_zrpc": "シールド(z-addr)RPC操作",
"grpa_enter_private_key_to_import": "インポートする秘密鍵を入力してください。",
"grpa_error_prefix": "エラー: ",
"grpa_hr_ago": "%lld時間前",
"grpa_invalid_response_from_daemon": "デーモンからの応答が無効です",
"grpa_invalid_suffix": "(無効)",
"grpa_min_ago": "%lld分前",
"grpa_sec_ago": "%lld秒前",
"grpa_seed_demo_chat": "デモチャットのシード",
"grpa_showing_first_100_of": "... %d件中の最初の100件を表示",
"grpa_tab_about": "情報",
"grpa_tab_appearance": "外観",
"grpa_tab_backup_data": "バックアップとデータ",
"grpa_tab_chat": "チャット",
"grpa_tab_explorer": "エクスプローラー",
"grpa_tab_node_security": "ノードとセキュリティ",
"grpa_tab_wallet": "ウォレット",
"grpa_unexpected_getblockhash_result": "予期しない getblockhash の結果",
"grpb_copy": "コピー",
"grpb_max": "最大",
"grpb_new_badge_suffix": " [新規]",
"grpb_preview_msg_payment_through": "支払いは完了しましたか? 🙂",
"grpb_preview_msg_sending_rest": "残りを今送金します 👍",
"grpb_preview_msg_yep_confirmed": "はい — たった今確認できました ✅",
"grpb_selected_suffix": "\n(選択済み)",
"grpb_tooltip_address_balance": "%s\n残高: %.8f %s%s",
"grpb_undo_clear": "消去を元に戻す",
"grpc_benchmark_inconclusive": "ベンチマークの結果が不明確です: ハッシュレートのサンプルが記録されませんでした。プール接続を確認して再試行してください。",
"grpc_benchmark_takes_secs": "ベンチマークには約%d秒かかり、マイニングが中断されます。開始するにはもう一度クリックしてください。",
"grpc_bootstrap_failed": "ブートストラップに失敗しました",
"grpc_bootstrap_not_initialized": "ブートストラップが初期化されていません",
"grpc_hashrate_fee": "%s 手数料 %s%%",
"grpc_key_not_available": "このアドレスの鍵は利用できません",
"grpc_na": "該当なし",
"height": "高さ",
"help": "ヘルプ",
"hidden_tag": " (非表示)",
@@ -1237,6 +1380,45 @@
"screenshot_sweep_full": "UI全体スイープ",
"search_icons": "アイコンを検索...",
"search_placeholder": "検索...",
"sec_changing_passphrase": "パスフレーズを変更しています...",
"sec_changing_pin": "PINを変更しています...",
"sec_couldnt_lock_wallet": "ウォレットをロックできませんでした。まだロック解除された状態です。デーモンの接続を確認してください。",
"sec_encrypted_backup_suffix": "\n暗号化バックアップ: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "ウォレットを暗号化しています...",
"sec_encryption_did_not_complete": "ウォレットの暗号化が完了しませんでした。ウォレットは暗号化されていません。設定を開いて暗号化を完了してください。",
"sec_encryption_failed_prefix": "暗号化に失敗しました: ",
"sec_failed_prefix": "失敗しました: ",
"sec_failed_to_create_vault": "ボールトの作成に失敗しました",
"sec_importing_keys_rescanning": "鍵をインポートし、ブロックチェーンを再スキャンしています。処理中もウォレットは使用できます",
"sec_incorrect_current_pin": "現在のPINが正しくありません",
"sec_incorrect_passphrase_decrypt": "パスフレーズが正しくありません",
"sec_incorrect_passphrase_pin_setup": "パスフレーズが正しくありません",
"sec_incorrect_pin_remove": "PINが正しくありません",
"sec_internal_error_change_pin": "内部エラー",
"sec_internal_error_remove_pin": "内部エラー",
"sec_mode_passphrase": " パスフレーズ",
"sec_not_connected_to_daemon": "デーモンに接続されていません",
"sec_not_connected_to_daemon_pin": "デーモンに接続されていません",
"sec_passphrase_changed_successfully": "パスフレーズを変更しました",
"sec_pin_changed_successfully": "PINを変更しました",
"sec_pin_removed": "PINを削除しました",
"sec_pin_set_successfully": "PINを設定しました",
"sec_restart_daemon_for_encryption": "暗号化を有効にするには、デーモンを再起動してください。",
"sec_too_many_attempts_wait": "試行回数が多すぎます。%.0f秒お待ちください...",
"sec_total_elapsed_fmt": "合計経過時間: %d分 %02d秒",
"sec_unlock_button": "ロック解除",
"sec_unlock_failed_prefix": "ロック解除に失敗しました: ",
"sec_unlocking_fmt": "ロック解除中%s",
"sec_use_passphrase_instead": "代わりにパスフレーズを使用",
"sec_use_pin_instead": "代わりにPINを使用",
"sec_verifying_passphrase": "パスフレーズを確認しています...",
"sec_verifying_pin": "PINを確認しています...",
"sec_wallet_decrypted_all_keys_imported": "ウォレットの復号に成功しました。すべての鍵をインポートしました。",
"sec_wallet_encrypted_and_pin_set": "ウォレットを暗号化し、PINを設定しました",
"sec_wallet_encrypted_but_pin_vault_failed": "ウォレットは暗号化されましたが、PINボールトの作成に失敗しました",
"sec_wallet_encrypted_restarting_daemon": "ウォレットを暗号化しました。デーモンを再起動しています...",
"sec_wallet_encrypted_successfully": "ウォレットを暗号化しました",
"sec_wallet_locked_title": "ウォレットがロックされています",
"security": "セキュリティ",
"seed_backup_button": "シードフレーズ",
"seed_backup_close": "閉じる",
@@ -1458,6 +1640,17 @@
"sweep_to": "集約先:",
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
"sweep_tx": "取引:",
"swin_connection_failed": "接続に失敗しました: ",
"swin_connection_successful": "接続に成功しました!\ndragonxd バージョン: ",
"swin_invalid_suffix": "(無効)",
"swin_no_history_file_found": "履歴ファイルが見つかりません",
"swin_rescan_failed": "再スキャンに失敗しました: ",
"swin_rescan_started_from_block": "再スキャンを開始しました。開始ブロック ",
"swin_rescan_to": " 〜 ",
"swin_rpc_client_not_initialized": "RPCクライアントが初期化されていません",
"swin_settings_saved": "設定を保存しました",
"swin_theme_list_refreshed": "テーマ一覧を更新しました",
"swin_ztx_history_cleared": "Zトランザクション履歴を消去しました",
"switch_corrupt_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。",
"switch_corrupt_repair": "修復を試す(salvage)",
"switch_progress_background": "バックグラウンドで続行",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "금액:",
"animate_avatars": "아바타 애니메이션",
"appearance": "외관",
"appx_back": "뒤로",
"appx_back_up_seed_phrase_title": "시드 문구 백업",
"appx_birthday_block_height": "생성 시점(블록 높이): %llu — 이것도 함께 백업하세요.",
"appx_blockchain_data_deleted": "블록체인 데이터를 삭제했습니다(%d개 항목). 네트워크에서 다시 동기화하기 위해 데몬을 재시작합니다.",
"appx_blockchain_maintenance_in_progress": "블록체인 유지 관리 작업이 이미 진행 중입니다.",
"appx_blockchain_rescan_complete": "블록체인 다시 스캔 완료",
"appx_bootstrap_complete_reconciling": "부트스트랩 완료 — 새 체인 데이터와 지갑을 대조하는 중입니다.",
"appx_cancel": "취소",
"appx_cleaning_up": "정리하는 중...",
"appx_confirm_your_backup": "백업 확인",
"appx_copied_clipboard_autoclears": "복사됨 — 클립보드가 45초 후 자동으로 지워집니다",
"appx_copy": "복사",
"appx_could_not_start_restore": "복구를 시작할 수 없습니다",
"appx_create_failed_prefix": "생성 실패: ",
"appx_creating_your_wallet": "지갑을 생성하는 중…",
"appx_daemon_error": "데몬 오류",
"appx_daemon_reinstall_in_progress": "데몬 재설치가 이미 진행 중입니다.",
"appx_disconnecting": "연결 해제 중...",
"appx_done": "완료",
"appx_dragonxd_output": "dragonxd 출력",
"appx_encrypting_wallet": "지갑 암호화 중...",
"appx_fullnode_lifecycle_unavailable_lite": "풀노드 수명 주기 작업은 라이트 빌드에서 사용할 수 없습니다",
"appx_installing_bundled_daemon": "번들 데몬을 설치하는 중 — 노드가 중지, 업데이트 후 다시 시작됩니다...",
"appx_invalid_payment_uri_prefix": "잘못된 결제 URI: ",
"appx_ive_written_it_down": "적어 두었습니다",
"appx_keep_node_running_and_quit": "노드 유지하고 종료",
"appx_last_block_n": "마지막 블록: %d",
"appx_last_used_wallet_not_found_prefix": "마지막으로 사용한 지갑 파일(",
"appx_last_used_wallet_not_found_suffix": ")을 찾을 수 없어 기본 지갑을 대신 열었습니다. 옮기셨다면 복원한 후 지갑 목록에서 다시 전환하세요.",
"appx_low_spec_mode_disabled": "저사양 모드 비활성화됨",
"appx_low_spec_mode_enabled": "저사양 모드 활성화됨",
"appx_miner_stopped_prefix": "채굴기 중지됨: ",
"appx_miner_stopped_unexpectedly": "채굴기가 예기치 않게 중지되었습니다.",
"appx_n_min_n_sec": "%d분 %d초",
"appx_n_seconds": "%d초",
"appx_no_bundled_daemon_to_install": "이 빌드에는 설치할 번들 데몬이 없습니다",
"appx_no_embedded_daemon_to_install": "이 빌드에는 설치할 내장 데몬이 없습니다",
"appx_node_busy_restarting": "노드가 재시작 중입니다 — 잠시 후 다시 시도하세요.",
"appx_node_rebuilding_witness_cache": "노드가 위트니스 캐시를 재구성하는 중입니다",
"appx_not_next_word": " — 다음 단어가 아닙니다",
"appx_payment_request_loaded": "결제 요청을 불러왔습니다",
"appx_pool_miner_connected_and_hashing": "풀 채굴기가 연결되어 해싱 중입니다.",
"appx_progress_n_of_n": "진행 상황: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Sapling 노트 위트니스 재구성 중…",
"appx_rebuilding_witness_cache_blocks_left": "위트니스 캐시 재구성 중 %.0f%% — %d개 블록 남음",
"appx_rebuilding_witness_cache_pct": "위트니스 캐시 재구성 중 %.0f%%",
"appx_recovery_phrase_word_count": "복구 문구는 24개 단어여야 합니다 — 현재 %d개입니다.",
"appx_restarting_daemon_rescan_flag": "-rescan 플래그로 데몬을 다시 시작하는 중...",
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2로 데몬을 다시 시작하는 중(지갑 복구)...",
"appx_restoring_your_wallet": "지갑을 복구하는 중…",
"appx_seed_backup_warning": "이 24개 단어는 지갑을 복구할 수 있는 유일한 방법입니다. 순서대로 적어 오프라인에 보관하고 절대 공유하지 마세요. 잃어버리면 자금을 영원히 되찾을 수 없습니다.",
"appx_seed_not_backed_up_warning": "시드를 아직 백업하지 않았습니다 — 자금을 잃을 수 있습니다. 그래도 건너뛰시겠습니까?",
"appx_sending_stop_command_to_daemon": "데몬에 중지 명령을 보내는 중...",
"appx_setting_initial_sapling_witnesses": "초기 Sapling 위트니스 설정 중 %.0f%%",
"appx_shutdown_complete": "종료 완료",
"appx_simple_background_disabled": "단순 배경 비활성화됨",
"appx_simple_background_enabled": "단순 배경 활성화됨",
"appx_skip": "건너뛰기",
"appx_skip_anyway": "그래도 건너뛰기",
"appx_still_status_prefix": "아직 \"",
"appx_still_status_suffix": "\" 상태입니다 — 지금 강제 종료하면 체인 데이터가 손상될 수 있습니다.",
"appx_stop_anyway_and_quit": "그래도 중지하고 종료",
"appx_stopping_daemon_deleting_blockchain": "데몬을 중지하고 블록체인 데이터를 삭제하는 중...",
"appx_stopping_node_discards_rebuild": "지금 노드를 중지하면 진행 중인 재구성이 취소되며, 다음에 지갑을 열 때 다시 시작됩니다(몇 분 소요). 대신 노드를 계속 실행할 수 있습니다.",
"appx_stopping_pool_miner": "풀 채굴기를 중지하는 중...",
"appx_syncing_pct_block_n_of_n": "동기화 중 %.1f%% — 블록 %d / %d",
"appx_tap_words_in_order": "저장한 내용을 확인하도록 단어를 올바른 순서대로 탭하세요.",
"appx_theme_effects_disabled": "테마 효과 비활성화됨",
"appx_theme_effects_enabled": "테마 효과 활성화됨",
"appx_theme_prefix": "테마: ",
"appx_use_settings_restart_daemon_hint": "설정 > 데몬 다시 시작을 사용해 다시 시도하세요",
"appx_waiting_for_daemon_to_encrypt_wallet": "데몬이 지갑을 암호화하기를 기다리는 중...",
"appx_wallet_created_and_backed_up": "지갑이 생성되고 백업되었습니다.",
"appx_wallet_open_failed_prefix": "지갑 열기 실패: ",
"auto_shield": "채굴 자동 차폐",
"av_intro": "채굴 소프트웨어는 종종 잠재적으로 원치 않는 항목으로 표시됩니다. 풀 채굴을 활성화하려면 다음 단계를 따르세요:",
"av_open_security": "Windows 보안 열기",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "차폐: %.8f",
"balance_syncing_pct": "동기화 중 %.1f%%",
"balance_transparent_fmt": "투명: %.8f",
"baltab_market": "시세",
"baltab_market_price_4dp": "시세: $%.4f",
"baltab_market_price_8dp": "시세: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24시간",
"baltab_pct_of_total_zaddr": "전체의 %.0f%% · Z-addr %d개",
"baltab_shielded": "실드됨",
"baltab_shielded_amount": "실드됨 %.8f",
"baltab_t_addresses_count": "T-address %d개",
"baltab_total_balance": "총 잔액",
"baltab_transparent": "투명",
"baltab_transparent_amount": "투명 %.8f",
"ban": "차단",
"banned_peers": "차단된 피어",
"block": "블록",
@@ -414,6 +499,7 @@
"contacts_shape_square": "사각형",
"contacts_shape_tab": "왼쪽 탭",
"copied": "복사됨!",
"copied_to_clipboard": "클립보드에 복사됨",
"copy": "복사",
"copy_address": "전체 주소 복사",
"copy_error": "오류 복사",
@@ -593,6 +679,63 @@
"general": "일반",
"generating": "생성 중",
"go_to_receive": "수신으로 이동",
"grpa_current_block_paren": "(현재: %d)",
"grpa_days_ago": "%lld일 전",
"grpa_dbg_addrman": "피어 주소 추적 및 관리",
"grpa_dbg_alert": "경고 시스템 메시지",
"grpa_dbg_bench": "작업 벤치마크 시간 측정",
"grpa_dbg_coindb": "코인 데이터베이스 읽기/쓰기 작업",
"grpa_dbg_db": "Berkeley DB 작업",
"grpa_dbg_estimatefee": "수수료 추정 알고리즘",
"grpa_dbg_http": "HTTP RPC 서버 활동",
"grpa_dbg_libevent": "Libevent 네트워킹 라이브러리",
"grpa_dbg_lock": "락 경합 디버깅",
"grpa_dbg_mempool": "트랜잭션 메모리 풀 활동",
"grpa_dbg_net": "네트워크 연결 및 메시지",
"grpa_dbg_paymentdisclosure": "결제 공개 프로토콜",
"grpa_dbg_pow": "작업 증명 채굴 활동",
"grpa_dbg_proxy": "SOCKS5 프록시 연결",
"grpa_dbg_prune": "블록 정리 작업",
"grpa_dbg_rand": "난수 생성",
"grpa_dbg_reindex": "블록체인 재색인 진행 상황",
"grpa_dbg_rpc": "RPC 명령 처리",
"grpa_dbg_selectcoins": "트랜잭션용 코인 선택",
"grpa_dbg_tor": "Tor 연동 및 회로 정보",
"grpa_dbg_zmq": "ZeroMQ 알림 시스템",
"grpa_dbg_zrpc": "실드(z-addr) RPC 작업",
"grpa_enter_private_key_to_import": "가져올 개인 키를 입력하세요.",
"grpa_error_prefix": "오류: ",
"grpa_hr_ago": "%lld시간 전",
"grpa_invalid_response_from_daemon": "데몬으로부터 잘못된 응답",
"grpa_invalid_suffix": " (유효하지 않음)",
"grpa_min_ago": "%lld분 전",
"grpa_sec_ago": "%lld초 전",
"grpa_seed_demo_chat": "시드 데모 채팅",
"grpa_showing_first_100_of": "... %d개 중 처음 100개 표시",
"grpa_tab_about": "정보",
"grpa_tab_appearance": "화면 표시",
"grpa_tab_backup_data": "백업 및 데이터",
"grpa_tab_chat": "채팅",
"grpa_tab_explorer": "탐색기",
"grpa_tab_node_security": "노드 및 보안",
"grpa_tab_wallet": "지갑",
"grpa_unexpected_getblockhash_result": "예기치 않은 getblockhash 결과",
"grpb_copy": "복사",
"grpb_max": "최대",
"grpb_new_badge_suffix": " [신규]",
"grpb_preview_msg_payment_through": "결제가 완료됐나요? 🙂",
"grpb_preview_msg_sending_rest": "지금 나머지를 보낼게요 👍",
"grpb_preview_msg_yep_confirmed": "네 — 방금 확인했어요 ✅",
"grpb_selected_suffix": "\n(선택됨)",
"grpb_tooltip_address_balance": "%s\n잔액: %.8f %s%s",
"grpb_undo_clear": "지우기 취소",
"grpc_benchmark_inconclusive": "벤치마크 결과가 불확실합니다: 해시레이트 샘플이 기록되지 않았습니다. 풀 연결을 확인한 후 다시 시도하세요.",
"grpc_benchmark_takes_secs": "벤치마크는 약 %d초가 걸리며 채굴을 중단합니다. 시작하려면 다시 클릭하세요.",
"grpc_bootstrap_failed": "부트스트랩 실패",
"grpc_bootstrap_not_initialized": "부트스트랩이 초기화되지 않았습니다",
"grpc_hashrate_fee": "%s 수수료 %s%%",
"grpc_key_not_available": "이 주소에 대한 키를 사용할 수 없습니다",
"grpc_na": "해당 없음",
"height": "높이",
"help": "도움말",
"hidden_tag": " (숨김)",
@@ -1239,6 +1382,45 @@
"screenshot_sweep_full": "전체 UI 스윕",
"search_icons": "아이콘 검색...",
"search_placeholder": "검색...",
"sec_changing_passphrase": "암호문 변경 중...",
"sec_changing_pin": "PIN 변경 중...",
"sec_couldnt_lock_wallet": "지갑을 잠글 수 없습니다 — 아직 잠금 해제 상태입니다. 데몬 연결을 확인하세요.",
"sec_encrypted_backup_suffix": "\n암호화된 백업: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "지갑 암호화 중...",
"sec_encryption_did_not_complete": "지갑 암호화가 완료되지 않았습니다 — 지갑이 암호화되지 않은 상태입니다. 설정을 열어 암호화를 완료하세요.",
"sec_encryption_failed_prefix": "암호화 실패: ",
"sec_failed_prefix": "실패: ",
"sec_failed_to_create_vault": "볼트 생성에 실패했습니다",
"sec_importing_keys_rescanning": "키를 가져오고 블록체인을 다시 스캔하는 중 — 진행 중에도 지갑을 사용할 수 있습니다",
"sec_incorrect_current_pin": "현재 PIN이 올바르지 않습니다",
"sec_incorrect_passphrase_decrypt": "잘못된 암호문",
"sec_incorrect_passphrase_pin_setup": "잘못된 암호문",
"sec_incorrect_pin_remove": "잘못된 PIN",
"sec_internal_error_change_pin": "내부 오류",
"sec_internal_error_remove_pin": "내부 오류",
"sec_mode_passphrase": " 암호문",
"sec_not_connected_to_daemon": "데몬에 연결되지 않음",
"sec_not_connected_to_daemon_pin": "데몬에 연결되지 않음",
"sec_passphrase_changed_successfully": "암호문이 성공적으로 변경되었습니다",
"sec_pin_changed_successfully": "PIN이 성공적으로 변경되었습니다",
"sec_pin_removed": "PIN이 제거되었습니다",
"sec_pin_set_successfully": "PIN이 성공적으로 설정되었습니다",
"sec_restart_daemon_for_encryption": "암호화를 적용하려면 데몬을 다시 시작하세요.",
"sec_too_many_attempts_wait": "시도 횟수가 너무 많습니다. %.0f초 기다리세요...",
"sec_total_elapsed_fmt": "총 경과 시간: %d분 %02d초",
"sec_unlock_button": "잠금 해제",
"sec_unlock_failed_prefix": "잠금 해제 실패: ",
"sec_unlocking_fmt": "잠금 해제 중%s",
"sec_use_passphrase_instead": "대신 암호문 사용",
"sec_use_pin_instead": "대신 PIN 사용",
"sec_verifying_passphrase": "암호문 확인 중...",
"sec_verifying_pin": "PIN 확인 중...",
"sec_wallet_decrypted_all_keys_imported": "지갑 복호화 완료! 모든 키를 가져왔습니다.",
"sec_wallet_encrypted_and_pin_set": "지갑 암호화 및 PIN 설정 완료",
"sec_wallet_encrypted_but_pin_vault_failed": "지갑은 암호화되었으나 PIN 볼트 설정에 실패했습니다",
"sec_wallet_encrypted_restarting_daemon": "지갑이 암호화되었습니다. 데몬을 다시 시작하는 중...",
"sec_wallet_encrypted_successfully": "지갑이 성공적으로 암호화되었습니다",
"sec_wallet_locked_title": "지갑 잠김",
"security": "보안",
"seed_backup_button": "시드 문구",
"seed_backup_close": "닫기",
@@ -1460,6 +1642,17 @@
"sweep_to": "쓸어담은 주소:",
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
"sweep_tx": "거래:",
"swin_connection_failed": "연결 실패: ",
"swin_connection_successful": "연결에 성공했습니다!\ndragonxd 버전: ",
"swin_invalid_suffix": " (유효하지 않음)",
"swin_no_history_file_found": "기록 파일을 찾을 수 없습니다",
"swin_rescan_failed": "다시 스캔 실패: ",
"swin_rescan_started_from_block": "다시 스캔이 시작된 블록 ",
"swin_rescan_to": " ~ ",
"swin_rpc_client_not_initialized": "RPC 클라이언트가 초기화되지 않았습니다",
"swin_settings_saved": "설정이 저장되었습니다",
"swin_theme_list_refreshed": "테마 목록을 새로고침했습니다",
"swin_ztx_history_cleared": "Z-트랜잭션 기록이 삭제되었습니다",
"switch_corrupt_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.",
"switch_corrupt_repair": "복구 시도(salvage)",
"switch_progress_background": "백그라운드에서 계속",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "Valor:",
"animate_avatars": "Animar avatares",
"appearance": "APARÊNCIA",
"appx_back": "Voltar",
"appx_back_up_seed_phrase_title": "Faça o backup da sua frase-semente",
"appx_birthday_block_height": "Nascimento (altura do bloco): %llu — faça o backup disto também.",
"appx_blockchain_data_deleted": "Dados da blockchain excluídos (%d itens). O daemon está reiniciando para ressincronizar com a rede.",
"appx_blockchain_maintenance_in_progress": "Uma operação de manutenção da blockchain já está em andamento.",
"appx_blockchain_rescan_complete": "Reescaneamento da blockchain concluído",
"appx_bootstrap_complete_reconciling": "Bootstrap concluído — reconciliando sua carteira com os novos dados da chain.",
"appx_cancel": "Cancelar",
"appx_cleaning_up": "Limpando...",
"appx_confirm_your_backup": "Confirme seu backup",
"appx_copied_clipboard_autoclears": "Copiado — a área de transferência será limpa em 45s",
"appx_copy": "Copiar",
"appx_could_not_start_restore": "Não foi possível iniciar a restauração",
"appx_create_failed_prefix": "Falha na criação: ",
"appx_creating_your_wallet": "Criando sua carteira…",
"appx_daemon_error": "Erro do Daemon",
"appx_daemon_reinstall_in_progress": "A reinstalação do daemon já está em andamento.",
"appx_disconnecting": "Desconectando...",
"appx_done": "Concluído",
"appx_dragonxd_output": "saída do dragonxd",
"appx_encrypting_wallet": "Criptografando a carteira...",
"appx_fullnode_lifecycle_unavailable_lite": "As ações de ciclo de vida do nó completo não estão disponíveis na versão lite",
"appx_installing_bundled_daemon": "Instalando o daemon incluído — o nó vai parar, atualizar e reiniciar...",
"appx_invalid_payment_uri_prefix": "URI de pagamento inválida: ",
"appx_ive_written_it_down": "Já anotei",
"appx_keep_node_running_and_quit": "Manter o nó em execução e sair",
"appx_last_block_n": "Último bloco: %d",
"appx_last_used_wallet_not_found_prefix": "Seu último arquivo de carteira usado (",
"appx_last_used_wallet_not_found_suffix": ") não foi encontrado — a carteira padrão foi aberta em seu lugar. Se você o moveu, restaure-o e volte a ele pela lista de carteiras.",
"appx_low_spec_mode_disabled": "Modo de baixo desempenho desativado",
"appx_low_spec_mode_enabled": "Modo de baixo desempenho ativado",
"appx_miner_stopped_prefix": "Minerador parado: ",
"appx_miner_stopped_unexpectedly": "O minerador parou inesperadamente.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d segundos",
"appx_no_bundled_daemon_to_install": "Esta versão não tem daemon incluído para instalar",
"appx_no_embedded_daemon_to_install": "Esta versão não tem daemon embutido para instalar",
"appx_node_busy_restarting": "O nó está ocupado reiniciando — tente novamente em instantes.",
"appx_node_rebuilding_witness_cache": "O nó está reconstruindo seu cache de testemunhas",
"appx_not_next_word": " — essa não é a próxima palavra",
"appx_payment_request_loaded": "Solicitação de pagamento carregada",
"appx_pool_miner_connected_and_hashing": "Minerador de pool conectado e processando hashes.",
"appx_progress_n_of_n": "Progresso: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruindo testemunhas de notas Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruindo cache de testemunhas %.0f%% — %d blocos restantes",
"appx_rebuilding_witness_cache_pct": "Reconstruindo cache de testemunhas %.0f%%",
"appx_recovery_phrase_word_count": "A frase de recuperação deve ter 24 palavras — você tem %d.",
"appx_restarting_daemon_rescan_flag": "Reiniciando o daemon com a flag -rescan...",
"appx_restarting_daemon_zapwallettxes": "Reiniciando o daemon com -zapwallettxes=2 (reparo da carteira)...",
"appx_restoring_your_wallet": "Restaurando sua carteira…",
"appx_seed_backup_warning": "Estas 24 palavras são a ÚNICA forma de restaurar sua carteira. Anote-as na ordem, guarde-as offline e nunca as compartilhe. Se perdê-las, seus fundos desaparecem para sempre.",
"appx_seed_not_backed_up_warning": "Você não fez o backup da sua semente — os fundos podem ser perdidos. Pular mesmo assim?",
"appx_sending_stop_command_to_daemon": "Enviando comando de parada ao daemon...",
"appx_setting_initial_sapling_witnesses": "Definindo testemunhas Sapling iniciais %.0f%%",
"appx_shutdown_complete": "Encerramento concluído",
"appx_simple_background_disabled": "Fundo simples desativado",
"appx_simple_background_enabled": "Fundo simples ativado",
"appx_skip": "Pular",
"appx_skip_anyway": "Pular mesmo assim",
"appx_still_status_prefix": "Ainda \"",
"appx_still_status_suffix": "\" — forçar o encerramento agora pode corromper os dados da chain.",
"appx_stop_anyway_and_quit": "Parar mesmo assim e sair",
"appx_stopping_daemon_deleting_blockchain": "Parando o daemon e excluindo os dados da blockchain...",
"appx_stopping_node_discards_rebuild": "Parar o nó agora descarta a reconstrução em andamento e a reinicia (vários minutos) na próxima vez que você abrir a carteira. Em vez disso, você pode manter o nó em execução.",
"appx_stopping_pool_miner": "Parando o minerador de pool...",
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloco %d / %d",
"appx_tap_words_in_order": "Toque nas palavras na ordem correta para confirmar que você as salvou.",
"appx_theme_effects_disabled": "Efeitos de tema desativados",
"appx_theme_effects_enabled": "Efeitos de tema ativados",
"appx_theme_prefix": "Tema: ",
"appx_use_settings_restart_daemon_hint": "Use Configurações > Reiniciar Daemon para tentar novamente",
"appx_waiting_for_daemon_to_encrypt_wallet": "Aguardando o daemon criptografar a carteira...",
"appx_wallet_created_and_backed_up": "Carteira criada e com backup feito.",
"appx_wallet_open_failed_prefix": "Falha ao abrir a carteira: ",
"auto_shield": "Auto-blindar mineração",
"av_intro": "Softwares de mineração costumam ser sinalizados como potencialmente indesejados. Siga estes passos para habilitar a mineração em pool:",
"av_open_security": "Abrir Segurança do Windows",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "Blindado: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f",
"baltab_market": "Mercado",
"baltab_market_price_4dp": "Mercado: $%.4f",
"baltab_market_price_8dp": "Mercado: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% do total · %d Z-addr",
"baltab_shielded": "Blindado",
"baltab_shielded_amount": "Blindado %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Saldo Total",
"baltab_transparent": "Transparente",
"baltab_transparent_amount": "Transparente %.8f",
"ban": "Banir",
"banned_peers": "Pares Banidos",
"block": "Bloco",
@@ -414,6 +499,7 @@
"contacts_shape_square": "Quadrado",
"contacts_shape_tab": "Aba",
"copied": "Copiado!",
"copied_to_clipboard": "Copiado para a área de transferência",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro",
@@ -593,6 +679,63 @@
"general": "Geral",
"generating": "Gerando",
"go_to_receive": "Ir para Receber",
"grpa_current_block_paren": "(Atual: %d)",
"grpa_days_ago": "há %lld dias",
"grpa_dbg_addrman": "Rastreamento e gerenciamento de endereços de pares",
"grpa_dbg_alert": "Mensagens do sistema de alertas",
"grpa_dbg_bench": "Medições de tempo de benchmark das operações",
"grpa_dbg_coindb": "Operações de leitura/gravação do banco de dados de moedas",
"grpa_dbg_db": "Operações do Berkeley DB",
"grpa_dbg_estimatefee": "Algoritmo de estimativa de taxa",
"grpa_dbg_http": "Atividade do servidor RPC HTTP",
"grpa_dbg_libevent": "Biblioteca de rede Libevent",
"grpa_dbg_lock": "Depuração de contenção de bloqueios",
"grpa_dbg_mempool": "Atividade do pool de memória de transações",
"grpa_dbg_net": "Conexões e mensagens de rede",
"grpa_dbg_paymentdisclosure": "Protocolo de divulgação de pagamento",
"grpa_dbg_pow": "Atividade de mineração de prova de trabalho",
"grpa_dbg_proxy": "Conexões de proxy SOCKS5",
"grpa_dbg_prune": "Operações de poda de blocos",
"grpa_dbg_rand": "Geração de números aleatórios",
"grpa_dbg_reindex": "Progresso da reindexação da blockchain",
"grpa_dbg_rpc": "Processamento de comandos RPC",
"grpa_dbg_selectcoins": "Seleção de moedas para transações",
"grpa_dbg_tor": "Integração com Tor e informações de circuito",
"grpa_dbg_zmq": "Sistema de notificações ZeroMQ",
"grpa_dbg_zrpc": "Operações RPC blindadas (z-address)",
"grpa_enter_private_key_to_import": "Insira uma chave privada para importar.",
"grpa_error_prefix": "Erro: ",
"grpa_hr_ago": "há %lld h",
"grpa_invalid_response_from_daemon": "Resposta inválida do daemon",
"grpa_invalid_suffix": " (inválido)",
"grpa_min_ago": "há %lld min",
"grpa_sec_ago": "há %lld s",
"grpa_seed_demo_chat": "Chat de demonstração da semente",
"grpa_showing_first_100_of": "... mostrando os primeiros 100 de %d",
"grpa_tab_about": "Sobre",
"grpa_tab_appearance": "Aparência",
"grpa_tab_backup_data": "Backup e Dados",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorer",
"grpa_tab_node_security": "Nó e Segurança",
"grpa_tab_wallet": "Carteira",
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
"grpb_copy": "Copiar",
"grpb_max": "Máx",
"grpb_new_badge_suffix": " [NOVO]",
"grpb_preview_msg_payment_through": "O pagamento foi concluído? 🙂",
"grpb_preview_msg_sending_rest": "Enviando o restante agora 👍",
"grpb_preview_msg_yep_confirmed": "Sim — acabei de confirmar ✅",
"grpb_selected_suffix": "\n(selecionado)",
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
"grpb_undo_clear": "Desfazer Limpar",
"grpc_benchmark_inconclusive": "Benchmark inconclusivo: nenhuma amostra de taxa de hash foi registrada. Verifique a conexão com a pool e tente novamente.",
"grpc_benchmark_takes_secs": "O benchmark leva cerca de %ds e interrompe a mineração. Clique novamente para iniciar.",
"grpc_bootstrap_failed": "Falha no bootstrap",
"grpc_bootstrap_not_initialized": "Bootstrap não inicializado",
"grpc_hashrate_fee": "%s %s%% de taxa",
"grpc_key_not_available": "Chave não disponível para este endereço",
"grpc_na": "N/D",
"height": "Altura",
"help": "Ajuda",
"hidden_tag": " (oculto)",
@@ -1240,6 +1383,45 @@
"screenshot_sweep_full": "Varredura completa da interface",
"search_icons": "Pesquisar ícones...",
"search_placeholder": "Pesquisar...",
"sec_changing_passphrase": "Alterando a frase-senha...",
"sec_changing_pin": "Alterando o PIN...",
"sec_couldnt_lock_wallet": "Não foi possível bloquear a carteira — ela continua desbloqueada. Verifique a conexão com o daemon.",
"sec_encrypted_backup_suffix": "\nBackup criptografado: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Criptografando a carteira...",
"sec_encryption_did_not_complete": "A criptografia da carteira não foi concluída — sua carteira NÃO está criptografada. Abra as Configurações para terminar de criptografá-la.",
"sec_encryption_failed_prefix": "Falha na criptografia: ",
"sec_failed_prefix": "Falhou: ",
"sec_failed_to_create_vault": "Falha ao criar o cofre",
"sec_importing_keys_rescanning": "Importando chaves e reescaneando a blockchain — a carteira pode ser usada durante o processo",
"sec_incorrect_current_pin": "PIN atual incorreto",
"sec_incorrect_passphrase_decrypt": "Frase-senha incorreta",
"sec_incorrect_passphrase_pin_setup": "Frase-senha incorreta",
"sec_incorrect_pin_remove": "PIN incorreto",
"sec_internal_error_change_pin": "Erro interno",
"sec_internal_error_remove_pin": "Erro interno",
"sec_mode_passphrase": " Frase-senha",
"sec_not_connected_to_daemon": "Não conectado ao daemon",
"sec_not_connected_to_daemon_pin": "Não conectado ao daemon",
"sec_passphrase_changed_successfully": "Frase-senha alterada com sucesso",
"sec_pin_changed_successfully": "PIN alterado com sucesso",
"sec_pin_removed": "PIN removido",
"sec_pin_set_successfully": "PIN definido com sucesso",
"sec_restart_daemon_for_encryption": "Reinicie o daemon para que a criptografia entre em vigor.",
"sec_too_many_attempts_wait": "Tentativas em excesso. Aguarde %.0f segundos...",
"sec_total_elapsed_fmt": "Tempo total: %dm %02ds",
"sec_unlock_button": "Desbloquear",
"sec_unlock_failed_prefix": "Falha ao desbloquear: ",
"sec_unlocking_fmt": "Desbloqueando%s",
"sec_use_passphrase_instead": "Usar frase-senha",
"sec_use_pin_instead": "Usar PIN",
"sec_verifying_passphrase": "Verificando a frase-senha...",
"sec_verifying_pin": "Verificando o PIN...",
"sec_wallet_decrypted_all_keys_imported": "Carteira descriptografada com sucesso! Todas as chaves importadas.",
"sec_wallet_encrypted_and_pin_set": "Carteira criptografada e PIN definido",
"sec_wallet_encrypted_but_pin_vault_failed": "Carteira criptografada, mas o cofre do PIN falhou",
"sec_wallet_encrypted_restarting_daemon": "Carteira criptografada. Reiniciando o daemon...",
"sec_wallet_encrypted_successfully": "Carteira criptografada com sucesso",
"sec_wallet_locked_title": "Carteira Bloqueada",
"security": "SEGURANÇA",
"seed_backup_button": "Frase de recuperação",
"seed_backup_close": "Fechar",
@@ -1461,6 +1643,17 @@
"sweep_to": "Varrido para:",
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
"sweep_tx": "Transação:",
"swin_connection_failed": "Falha na conexão: ",
"swin_connection_successful": "Conexão bem-sucedida!\nversão do dragonxd: ",
"swin_invalid_suffix": " (inválido)",
"swin_no_history_file_found": "Nenhum arquivo de histórico encontrado",
"swin_rescan_failed": "Falha no reescaneamento: ",
"swin_rescan_started_from_block": "Reescaneamento iniciado a partir do bloco ",
"swin_rescan_to": " até ",
"swin_rpc_client_not_initialized": "Cliente RPC não inicializado",
"swin_settings_saved": "Configurações salvas",
"swin_theme_list_refreshed": "Lista de temas atualizada",
"swin_ztx_history_cleared": "Histórico de transações Z limpo",
"switch_corrupt_body": "Esta carteira parece corrompida — o nó não conseguiu abri-la. Restaure de um backup, recrie-a ou tente repará-la.",
"switch_corrupt_repair": "Tentar reparar (salvage)",
"switch_progress_background": "Continuar em segundo plano",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "Сумма:",
"animate_avatars": "Анимировать аватары",
"appearance": "ВНЕШНИЙ ВИД",
"appx_back": "Назад",
"appx_back_up_seed_phrase_title": "Сохраните seed-фразу",
"appx_birthday_block_height": "Дата создания (высота блока): %llu — сохраните и её.",
"appx_blockchain_data_deleted": "Данные блокчейна удалены (%d элементов). Демон перезапускается для повторной синхронизации с сетью.",
"appx_blockchain_maintenance_in_progress": "Операция обслуживания блокчейна уже выполняется.",
"appx_blockchain_rescan_complete": "Повторное сканирование блокчейна завершено",
"appx_bootstrap_complete_reconciling": "Начальная загрузка завершена — сверка кошелька с новыми данными цепи.",
"appx_cancel": "Отмена",
"appx_cleaning_up": "Очистка...",
"appx_confirm_your_backup": "Подтвердите резервную копию",
"appx_copied_clipboard_autoclears": "Скопировано — буфер обмена очистится через 45 с",
"appx_copy": "Копировать",
"appx_could_not_start_restore": "Не удалось начать восстановление",
"appx_create_failed_prefix": "Ошибка создания: ",
"appx_creating_your_wallet": "Создание кошелька…",
"appx_daemon_error": "Ошибка демона",
"appx_daemon_reinstall_in_progress": "Переустановка демона уже выполняется.",
"appx_disconnecting": "Отключение...",
"appx_done": "Готово",
"appx_dragonxd_output": "вывод dragonxd",
"appx_encrypting_wallet": "Шифрование кошелька...",
"appx_fullnode_lifecycle_unavailable_lite": "Действия жизненного цикла полного узла недоступны в lite-сборке",
"appx_installing_bundled_daemon": "Установка встроенного демона — узел остановится, обновится и перезапустится...",
"appx_invalid_payment_uri_prefix": "Неверный платёжный URI: ",
"appx_ive_written_it_down": "Я записал её",
"appx_keep_node_running_and_quit": "Оставить узел и выйти",
"appx_last_block_n": "Последний блок: %d",
"appx_last_used_wallet_not_found_prefix": "Ваш последний использованный файл кошелька (",
"appx_last_used_wallet_not_found_suffix": ") не найден — вместо него открыт кошелёк по умолчанию. Если вы его переместили, восстановите файл и переключитесь обратно из списка кошельков.",
"appx_low_spec_mode_disabled": "Режим слабого оборудования выключен",
"appx_low_spec_mode_enabled": "Режим слабого оборудования включён",
"appx_miner_stopped_prefix": "Майнер остановлен: ",
"appx_miner_stopped_unexpectedly": "Майнер неожиданно остановился.",
"appx_n_min_n_sec": "%d мин %d сек",
"appx_n_seconds": "%d сек",
"appx_no_bundled_daemon_to_install": "В этой сборке нет встроенного демона для установки",
"appx_no_embedded_daemon_to_install": "В этой сборке нет встроенного демона для установки",
"appx_node_busy_restarting": "Узел занят перезапуском — попробуйте ещё раз через мгновение.",
"appx_node_rebuilding_witness_cache": "Узел перестраивает кэш свидетелей",
"appx_not_next_word": " — это не следующее слово",
"appx_payment_request_loaded": "Запрос на оплату загружен",
"appx_pool_miner_connected_and_hashing": "Пул-майнер подключён и вычисляет хеши.",
"appx_progress_n_of_n": "Прогресс: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Перестройка свидетелей заметок Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Перестройка кэша свидетелей %.0f%% — осталось блоков: %d",
"appx_rebuilding_witness_cache_pct": "Перестройка кэша свидетелей %.0f%%",
"appx_recovery_phrase_word_count": "Фраза восстановления должна содержать 24 слова — у вас %d.",
"appx_restarting_daemon_rescan_flag": "Перезапуск демона с флагом -rescan...",
"appx_restarting_daemon_zapwallettxes": "Перезапуск демона с -zapwallettxes=2 (восстановление кошелька)...",
"appx_restoring_your_wallet": "Восстановление кошелька…",
"appx_seed_backup_warning": "Эти 24 слова — ЕДИНСТВЕННЫЙ способ восстановить кошелёк. Запишите их по порядку, храните офлайн и никому не сообщайте. Если вы их потеряете, средства пропадут навсегда.",
"appx_seed_not_backed_up_warning": "Вы не сохранили seed-фразу — средства могут быть потеряны. Всё равно пропустить?",
"appx_sending_stop_command_to_daemon": "Отправка команды остановки демону...",
"appx_setting_initial_sapling_witnesses": "Установка начальных свидетелей Sapling %.0f%%",
"appx_shutdown_complete": "Завершение работы выполнено",
"appx_simple_background_disabled": "Простой фон выключен",
"appx_simple_background_enabled": "Простой фон включён",
"appx_skip": "Пропустить",
"appx_skip_anyway": "Всё равно пропустить",
"appx_still_status_prefix": "Всё ещё \"",
"appx_still_status_suffix": "\" — принудительный выход сейчас может повредить данные цепи.",
"appx_stop_anyway_and_quit": "Всё равно остановить и выйти",
"appx_stopping_daemon_deleting_blockchain": "Остановка демона и удаление данных блокчейна...",
"appx_stopping_node_discards_rebuild": "Остановка узла сейчас отменит текущую перестройку и запустит её заново (несколько минут) при следующем открытии кошелька. Вместо этого можно оставить узел работать.",
"appx_stopping_pool_miner": "Остановка пул-майнера...",
"appx_syncing_pct_block_n_of_n": "Синхронизация %.1f%% — Блок %d / %d",
"appx_tap_words_in_order": "Нажимайте слова в правильном порядке, чтобы подтвердить, что вы их сохранили.",
"appx_theme_effects_disabled": "Эффекты темы выключены",
"appx_theme_effects_enabled": "Эффекты темы включены",
"appx_theme_prefix": "Тема: ",
"appx_use_settings_restart_daemon_hint": "Откройте Настройки > Перезапустить демон, чтобы повторить попытку",
"appx_waiting_for_daemon_to_encrypt_wallet": "Ожидание шифрования кошелька демоном...",
"appx_wallet_created_and_backed_up": "Кошелёк создан, резервная копия сохранена.",
"appx_wallet_open_failed_prefix": "Не удалось открыть кошелёк: ",
"auto_shield": "Авто-экранирование майнинга",
"av_intro": "Программы для майнинга часто помечаются как потенциально нежелательные. Выполните эти шаги, чтобы включить пул-майнинг:",
"av_open_security": "Открыть Безопасность Windows",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "Экранировано: %.8f",
"balance_syncing_pct": "Синхронизация %.1f%%",
"balance_transparent_fmt": "Прозрачный: %.8f",
"baltab_market": "Рынок",
"baltab_market_price_4dp": "Рынок: $%.4f",
"baltab_market_price_8dp": "Рынок: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% за 24ч",
"baltab_pct_of_total_zaddr": "%.0f%% от общего · %d Z-addr",
"baltab_shielded": "Скрытый",
"baltab_shielded_amount": "Скрытый %.8f",
"baltab_t_addresses_count": "%d T-адресов",
"baltab_total_balance": "Общий баланс",
"baltab_transparent": "Прозрачный",
"baltab_transparent_amount": "Прозрачный %.8f",
"ban": "Заблокировать",
"banned_peers": "Заблокированные узлы",
"block": "Блок",
@@ -414,6 +499,7 @@
"contacts_shape_square": "Квадрат",
"contacts_shape_tab": "Вкладка",
"copied": "Скопировано!",
"copied_to_clipboard": "Скопировано в буфер обмена",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку",
@@ -593,6 +679,63 @@
"general": "Общие",
"generating": "Генерация",
"go_to_receive": "Перейти к получению",
"grpa_current_block_paren": "(Текущий: %d)",
"grpa_days_ago": "%lld дн назад",
"grpa_dbg_addrman": "Отслеживание и управление адресами узлов",
"grpa_dbg_alert": "Сообщения системы оповещений",
"grpa_dbg_bench": "Замеры производительности операций",
"grpa_dbg_coindb": "Операции чтения/записи базы монет",
"grpa_dbg_db": "Операции Berkeley DB",
"grpa_dbg_estimatefee": "Алгоритм оценки комиссии",
"grpa_dbg_http": "Активность HTTP RPC-сервера",
"grpa_dbg_libevent": "Сетевая библиотека Libevent",
"grpa_dbg_lock": "Отладка конкуренции блокировок",
"grpa_dbg_mempool": "Активность пула транзакций в памяти",
"grpa_dbg_net": "Сетевые соединения и сообщения",
"grpa_dbg_paymentdisclosure": "Протокол раскрытия платежей",
"grpa_dbg_pow": "Активность майнинга proof-of-work",
"grpa_dbg_proxy": "Соединения через прокси SOCKS5",
"grpa_dbg_prune": "Операции обрезки блоков",
"grpa_dbg_rand": "Генерация случайных чисел",
"grpa_dbg_reindex": "Прогресс переиндексации блокчейна",
"grpa_dbg_rpc": "Обработка RPC-команд",
"grpa_dbg_selectcoins": "Выбор монет для транзакций",
"grpa_dbg_tor": "Интеграция Tor и сведения о цепочках",
"grpa_dbg_zmq": "Система уведомлений ZeroMQ",
"grpa_dbg_zrpc": "Скрытые (z-addr) RPC-операции",
"grpa_enter_private_key_to_import": "Введите приватный ключ для импорта.",
"grpa_error_prefix": "Ошибка: ",
"grpa_hr_ago": "%lld ч назад",
"grpa_invalid_response_from_daemon": "Неверный ответ от демона",
"grpa_invalid_suffix": " (неверный)",
"grpa_min_ago": "%lld мин назад",
"grpa_sec_ago": "%lld сек назад",
"grpa_seed_demo_chat": "Демо-чат seed",
"grpa_showing_first_100_of": "... показаны первые 100 из %d",
"grpa_tab_about": "О программе",
"grpa_tab_appearance": "Внешний вид",
"grpa_tab_backup_data": "Резервное копирование и данные",
"grpa_tab_chat": "Чат",
"grpa_tab_explorer": "Обозреватель",
"grpa_tab_node_security": "Узел и безопасность",
"grpa_tab_wallet": "Кошелёк",
"grpa_unexpected_getblockhash_result": "неожиданный результат getblockhash",
"grpb_copy": "Копировать",
"grpb_max": "Макс",
"grpb_new_badge_suffix": " [НОВОЕ]",
"grpb_preview_msg_payment_through": "Платёж прошёл? 🙂",
"grpb_preview_msg_sending_rest": "Отправляю остаток 👍",
"grpb_preview_msg_yep_confirmed": "Да — только что подтвердил ✅",
"grpb_selected_suffix": "\n(выбрано)",
"grpb_tooltip_address_balance": "%s\nБаланс: %.8f %s%s",
"grpb_undo_clear": "Отменить очистку",
"grpc_benchmark_inconclusive": "Бенчмарк не дал результата: не записано ни одной выборки хешрейта. Проверьте соединение с пулом и повторите попытку.",
"grpc_benchmark_takes_secs": "Бенчмарк занимает ~%dс и прерывает майнинг. Нажмите ещё раз для запуска.",
"grpc_bootstrap_failed": "Ошибка начальной загрузки",
"grpc_bootstrap_not_initialized": "Начальная загрузка не инициализирована",
"grpc_hashrate_fee": "%s комиссия %s%%",
"grpc_key_not_available": "Ключ недоступен для этого адреса",
"grpc_na": "Н/Д",
"height": "Высота",
"help": "Справка",
"hidden_tag": " (скрыт)",
@@ -1240,6 +1383,45 @@
"screenshot_sweep_full": "Полный обход интерфейса",
"search_icons": "Поиск значков...",
"search_placeholder": "Поиск...",
"sec_changing_passphrase": "Смена парольной фразы...",
"sec_changing_pin": "Смена PIN...",
"sec_couldnt_lock_wallet": "Не удалось заблокировать кошелёк — он всё ещё разблокирован. Проверьте соединение с демоном.",
"sec_encrypted_backup_suffix": "\nЗашифрованная резервная копия: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Шифрование кошелька...",
"sec_encryption_did_not_complete": "Шифрование кошелька не завершено — ваш кошелёк НЕ зашифрован. Откройте настройки, чтобы завершить шифрование.",
"sec_encryption_failed_prefix": "Ошибка шифрования: ",
"sec_failed_prefix": "Ошибка: ",
"sec_failed_to_create_vault": "Не удалось создать хранилище",
"sec_importing_keys_rescanning": "Импорт ключей и повторное сканирование блокчейна — кошелёк доступен во время выполнения",
"sec_incorrect_current_pin": "Неверный текущий PIN",
"sec_incorrect_passphrase_decrypt": "Неверная парольная фраза",
"sec_incorrect_passphrase_pin_setup": "Неверная парольная фраза",
"sec_incorrect_pin_remove": "Неверный PIN",
"sec_internal_error_change_pin": "Внутренняя ошибка",
"sec_internal_error_remove_pin": "Внутренняя ошибка",
"sec_mode_passphrase": " Парольная фраза",
"sec_not_connected_to_daemon": "Нет соединения с демоном",
"sec_not_connected_to_daemon_pin": "Нет соединения с демоном",
"sec_passphrase_changed_successfully": "Парольная фраза успешно изменена",
"sec_pin_changed_successfully": "PIN успешно изменён",
"sec_pin_removed": "PIN удалён",
"sec_pin_set_successfully": "PIN успешно задан",
"sec_restart_daemon_for_encryption": "Перезапустите демон, чтобы шифрование вступило в силу.",
"sec_too_many_attempts_wait": "Слишком много попыток. Подождите %.0f сек...",
"sec_total_elapsed_fmt": "Всего прошло: %dм %02dс",
"sec_unlock_button": "Разблокировать",
"sec_unlock_failed_prefix": "Ошибка разблокировки: ",
"sec_unlocking_fmt": "Разблокировка%s",
"sec_use_passphrase_instead": "Использовать парольную фразу",
"sec_use_pin_instead": "Использовать PIN",
"sec_verifying_passphrase": "Проверка парольной фразы...",
"sec_verifying_pin": "Проверка PIN...",
"sec_wallet_decrypted_all_keys_imported": "Кошелёк успешно расшифрован! Все ключи импортированы.",
"sec_wallet_encrypted_and_pin_set": "Кошелёк зашифрован, PIN задан",
"sec_wallet_encrypted_but_pin_vault_failed": "Кошелёк зашифрован, но не удалось создать хранилище PIN",
"sec_wallet_encrypted_restarting_daemon": "Кошелёк зашифрован. Перезапуск демона...",
"sec_wallet_encrypted_successfully": "Кошелёк успешно зашифрован",
"sec_wallet_locked_title": "Кошелёк заблокирован",
"security": "БЕЗОПАСНОСТЬ",
"seed_backup_button": "Сид-фраза",
"seed_backup_close": "Закрыть",
@@ -1461,6 +1643,17 @@
"sweep_to": "Переведено на:",
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
"sweep_tx": "Транзакция:",
"swin_connection_failed": "Ошибка соединения: ",
"swin_connection_successful": "Соединение установлено!\nВерсия dragonxd: ",
"swin_invalid_suffix": " (неверный)",
"swin_no_history_file_found": "Файл истории не найден",
"swin_rescan_failed": "Ошибка повторного сканирования: ",
"swin_rescan_started_from_block": "Повторное сканирование начато с блока ",
"swin_rescan_to": " до ",
"swin_rpc_client_not_initialized": "RPC-клиент не инициализирован",
"swin_settings_saved": "Настройки сохранены",
"swin_theme_list_refreshed": "Список тем обновлён",
"swin_ztx_history_cleared": "История Z-транзакций очищена",
"switch_corrupt_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.",
"switch_corrupt_repair": "Попробовать восстановить (salvage)",
"switch_progress_background": "Продолжить в фоне",
+193
View File
@@ -60,6 +60,80 @@
"amount_label": "金额:",
"animate_avatars": "动画头像",
"appearance": "外观",
"appx_back": "返回",
"appx_back_up_seed_phrase_title": "备份你的助记词",
"appx_birthday_block_height": "生日(区块高度):%llu — 也请一并备份。",
"appx_blockchain_data_deleted": "区块链数据已删除(%d 项)。守护进程正在重启,以从网络重新同步。",
"appx_blockchain_maintenance_in_progress": "已有区块链维护操作正在进行中。",
"appx_blockchain_rescan_complete": "区块链重新扫描完成",
"appx_bootstrap_complete_reconciling": "引导数据完成——正在将你的钱包与新的链数据进行核对。",
"appx_cancel": "取消",
"appx_cleaning_up": "正在清理…",
"appx_confirm_your_backup": "确认你的备份",
"appx_copied_clipboard_autoclears": "已复制——剪贴板将在 45 秒后自动清空",
"appx_copy": "复制",
"appx_could_not_start_restore": "无法开始恢复",
"appx_create_failed_prefix": "创建失败:",
"appx_creating_your_wallet": "正在创建你的钱包…",
"appx_daemon_error": "守护进程错误",
"appx_daemon_reinstall_in_progress": "守护进程重新安装已在进行中。",
"appx_disconnecting": "正在断开连接…",
"appx_done": "完成",
"appx_dragonxd_output": "dragonxd 输出",
"appx_encrypting_wallet": "正在加密钱包…",
"appx_fullnode_lifecycle_unavailable_lite": "全节点生命周期操作在轻量版中不可用",
"appx_installing_bundled_daemon": "正在安装捆绑守护进程——节点将停止、更新并重启…",
"appx_invalid_payment_uri_prefix": "无效的支付 URI:",
"appx_ive_written_it_down": "我已抄写完毕",
"appx_keep_node_running_and_quit": "保持节点运行并退出",
"appx_last_block_n": "最新区块:%d",
"appx_last_used_wallet_not_found_prefix": "找不到你上次使用的钱包文件(",
"appx_last_used_wallet_not_found_suffix": ")——已改为打开默认钱包。如果你移动了它,请恢复该文件,然后从钱包列表中切换回来。",
"appx_low_spec_mode_disabled": "已禁用低配模式",
"appx_low_spec_mode_enabled": "已启用低配模式",
"appx_miner_stopped_prefix": "矿工已停止:",
"appx_miner_stopped_unexpectedly": "矿工意外停止。",
"appx_n_min_n_sec": "%d 分 %d 秒",
"appx_n_seconds": "%d 秒",
"appx_no_bundled_daemon_to_install": "此版本没有可安装的捆绑守护进程",
"appx_no_embedded_daemon_to_install": "此版本没有可安装的内嵌守护进程",
"appx_node_busy_restarting": "节点正忙于重启——请稍后再试。",
"appx_node_rebuilding_witness_cache": "节点正在重建其见证缓存",
"appx_not_next_word": " — 这不是下一个词",
"appx_payment_request_loaded": "支付请求已加载",
"appx_pool_miner_connected_and_hashing": "矿池矿工已连接并正在计算哈希。",
"appx_progress_n_of_n": "进度:%d / %d",
"appx_rebuilding_sapling_note_witnesses": "正在重建 Sapling 票据见证…",
"appx_rebuilding_witness_cache_blocks_left": "正在重建见证缓存 %.0f%% —— 剩余 %d 个区块",
"appx_rebuilding_witness_cache_pct": "正在重建见证缓存 %.0f%%",
"appx_recovery_phrase_word_count": "助记词应为 24 个——你输入了 %d 个。",
"appx_restarting_daemon_rescan_flag": "正在以 -rescan 标志重启守护进程…",
"appx_restarting_daemon_zapwallettxes": "正在以 -zapwallettxes=2 重启守护进程(钱包修复)…",
"appx_restoring_your_wallet": "正在恢复你的钱包…",
"appx_seed_backup_warning": "这 24 个词是恢复钱包的唯一方式。请按顺序抄写、离线保存,切勿泄露给他人。一旦丢失,你的资金将永远无法找回。",
"appx_seed_not_backed_up_warning": "你尚未备份助记词——资金可能丢失。仍要跳过吗?",
"appx_sending_stop_command_to_daemon": "正在向守护进程发送停止命令…",
"appx_setting_initial_sapling_witnesses": "正在设置初始 Sapling 见证 %.0f%%",
"appx_shutdown_complete": "关闭完成",
"appx_simple_background_disabled": "已禁用简约背景",
"appx_simple_background_enabled": "已启用简约背景",
"appx_skip": "跳过",
"appx_skip_anyway": "仍要跳过",
"appx_still_status_prefix": "仍处于“",
"appx_still_status_suffix": "”——现在强制退出可能损坏链数据。",
"appx_stop_anyway_and_quit": "仍要停止并退出",
"appx_stopping_daemon_deleting_blockchain": "正在停止守护进程并删除区块链数据…",
"appx_stopping_node_discards_rebuild": "现在停止节点会丢弃正在进行的重建,下次打开钱包时将重新开始(需要几分钟)。你也可以让节点继续运行。",
"appx_stopping_pool_miner": "正在停止矿池矿工…",
"appx_syncing_pct_block_n_of_n": "正在同步 %.1f%% —— 区块 %d / %d",
"appx_tap_words_in_order": "按正确顺序点击这些词,以确认你已保存。",
"appx_theme_effects_disabled": "已禁用主题特效",
"appx_theme_effects_enabled": "已启用主题特效",
"appx_theme_prefix": "主题:",
"appx_use_settings_restart_daemon_hint": "请使用 设置 > 重启守护进程 重试",
"appx_waiting_for_daemon_to_encrypt_wallet": "正在等待守护进程加密钱包…",
"appx_wallet_created_and_backed_up": "钱包已创建并完成备份。",
"appx_wallet_open_failed_prefix": "钱包打开失败:",
"auto_shield": "自动屏蔽挖矿",
"av_intro": "挖矿软件经常被标记为潜在有害程序。请按照以下步骤启用矿池挖矿:",
"av_open_security": "打开 Windows 安全中心",
@@ -100,6 +174,17 @@
"balance_shielded_fmt": "屏蔽:%.8f",
"balance_syncing_pct": "同步中 %.1f%%",
"balance_transparent_fmt": "透明:%.8f",
"baltab_market": "市价",
"baltab_market_price_4dp": "市价:$%.4f",
"baltab_market_price_8dp": "市价:$%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24小时",
"baltab_pct_of_total_zaddr": "占总额 %.0f%% · %d 个 Z-addr",
"baltab_shielded": "隐蔽",
"baltab_shielded_amount": "隐蔽 %.8f",
"baltab_t_addresses_count": "%d 个 T-address",
"baltab_total_balance": "总余额",
"baltab_transparent": "透明",
"baltab_transparent_amount": "透明 %.8f",
"ban": "封禁",
"banned_peers": "已封禁节点",
"block": "区块",
@@ -414,6 +499,7 @@
"contacts_shape_square": "方形",
"contacts_shape_tab": "左标签",
"copied": "已复制!",
"copied_to_clipboard": "已复制到剪贴板",
"copy": "复制",
"copy_address": "复制完整地址",
"copy_error": "复制错误",
@@ -593,6 +679,63 @@
"general": "常规",
"generating": "正在生成",
"go_to_receive": "前往接收",
"grpa_current_block_paren": "(当前:%d)",
"grpa_days_ago": "%lld 天前",
"grpa_dbg_addrman": "对等节点地址追踪与管理",
"grpa_dbg_alert": "警报系统消息",
"grpa_dbg_bench": "操作的基准计时",
"grpa_dbg_coindb": "币数据库读写操作",
"grpa_dbg_db": "Berkeley DB 操作",
"grpa_dbg_estimatefee": "手续费估算算法",
"grpa_dbg_http": "HTTP RPC 服务器活动",
"grpa_dbg_libevent": "Libevent 网络库",
"grpa_dbg_lock": "锁竞争调试",
"grpa_dbg_mempool": "交易内存池活动",
"grpa_dbg_net": "网络连接与消息",
"grpa_dbg_paymentdisclosure": "支付披露协议",
"grpa_dbg_pow": "工作量证明挖矿活动",
"grpa_dbg_proxy": "SOCKS5 代理连接",
"grpa_dbg_prune": "区块修剪操作",
"grpa_dbg_rand": "随机数生成",
"grpa_dbg_reindex": "区块链重新索引进度",
"grpa_dbg_rpc": "RPC 命令处理",
"grpa_dbg_selectcoins": "交易的币选择",
"grpa_dbg_tor": "Tor 集成与线路信息",
"grpa_dbg_zmq": "ZeroMQ 通知系统",
"grpa_dbg_zrpc": "隐蔽(z-addr)RPC 操作",
"grpa_enter_private_key_to_import": "请输入要导入的私钥。",
"grpa_error_prefix": "错误:",
"grpa_hr_ago": "%lld 小时前",
"grpa_invalid_response_from_daemon": "守护进程返回了无效响应",
"grpa_invalid_suffix": "(无效)",
"grpa_min_ago": "%lld 分钟前",
"grpa_sec_ago": "%lld 秒前",
"grpa_seed_demo_chat": "种子演示聊天",
"grpa_showing_first_100_of": "…显示 %d 项中的前 100 项",
"grpa_tab_about": "关于",
"grpa_tab_appearance": "外观",
"grpa_tab_backup_data": "备份与数据",
"grpa_tab_chat": "聊天",
"grpa_tab_explorer": "浏览器",
"grpa_tab_node_security": "节点与安全",
"grpa_tab_wallet": "钱包",
"grpa_unexpected_getblockhash_result": "意外的 getblockhash 结果",
"grpb_copy": "复制",
"grpb_max": "最大",
"grpb_new_badge_suffix": " [新]",
"grpb_preview_msg_payment_through": "付款到账了吗?🙂",
"grpb_preview_msg_sending_rest": "现在把剩下的发过去 👍",
"grpb_preview_msg_yep_confirmed": "到了——刚刚确认 ✅",
"grpb_selected_suffix": "\n(已选)",
"grpb_tooltip_address_balance": "%s\n余额:%.8f %s%s",
"grpb_undo_clear": "撤销清除",
"grpc_benchmark_inconclusive": "基准测试无结果:未记录到任何算力样本。请检查矿池连接后重试。",
"grpc_benchmark_takes_secs": "基准测试约需 %d 秒并会中断挖矿。再次点击以开始。",
"grpc_bootstrap_failed": "引导失败",
"grpc_bootstrap_not_initialized": "引导未初始化",
"grpc_hashrate_fee": "%s %s%% 手续费",
"grpc_key_not_available": "此地址无可用密钥",
"grpc_na": "不适用",
"height": "高度",
"help": "帮助",
"hidden_tag": " (已隐藏)",
@@ -1238,6 +1381,45 @@
"screenshot_sweep_full": "完整 UI 遍历",
"search_icons": "搜索图标...",
"search_placeholder": "搜索...",
"sec_changing_passphrase": "正在更改密码短语…",
"sec_changing_pin": "正在更改 PIN…",
"sec_couldnt_lock_wallet": "无法锁定钱包——它仍处于解锁状态。请检查守护进程连接。",
"sec_encrypted_backup_suffix": "\n加密备份:wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "正在加密钱包…",
"sec_encryption_did_not_complete": "钱包加密未完成——你的钱包尚未加密。请打开设置以完成加密。",
"sec_encryption_failed_prefix": "加密失败:",
"sec_failed_prefix": "失败:",
"sec_failed_to_create_vault": "创建保险库失败",
"sec_importing_keys_rescanning": "正在导入密钥并重新扫描区块链——期间钱包仍可使用",
"sec_incorrect_current_pin": "当前 PIN 错误",
"sec_incorrect_passphrase_decrypt": "密码短语错误",
"sec_incorrect_passphrase_pin_setup": "密码短语错误",
"sec_incorrect_pin_remove": "PIN 错误",
"sec_internal_error_change_pin": "内部错误",
"sec_internal_error_remove_pin": "内部错误",
"sec_mode_passphrase": " 密码短语",
"sec_not_connected_to_daemon": "未连接到守护进程",
"sec_not_connected_to_daemon_pin": "未连接到守护进程",
"sec_passphrase_changed_successfully": "密码短语更改成功",
"sec_pin_changed_successfully": "PIN 更改成功",
"sec_pin_removed": "PIN 已移除",
"sec_pin_set_successfully": "PIN 设置成功",
"sec_restart_daemon_for_encryption": "请重启守护进程以使加密生效。",
"sec_too_many_attempts_wait": "尝试次数过多。请等待 %.0f 秒…",
"sec_total_elapsed_fmt": "总耗时:%d 分 %02d 秒",
"sec_unlock_button": "解锁",
"sec_unlock_failed_prefix": "解锁失败:",
"sec_unlocking_fmt": "正在解锁%s",
"sec_use_passphrase_instead": "改用密码短语",
"sec_use_pin_instead": "改用 PIN",
"sec_verifying_passphrase": "正在验证密码短语…",
"sec_verifying_pin": "正在验证 PIN…",
"sec_wallet_decrypted_all_keys_imported": "钱包解密成功!所有密钥已导入。",
"sec_wallet_encrypted_and_pin_set": "钱包已加密并已设置 PIN",
"sec_wallet_encrypted_but_pin_vault_failed": "钱包已加密,但 PIN 保险库创建失败",
"sec_wallet_encrypted_restarting_daemon": "钱包已加密。正在重启守护进程…",
"sec_wallet_encrypted_successfully": "钱包加密成功",
"sec_wallet_locked_title": "钱包已锁定",
"security": "安全",
"seed_backup_button": "助记词",
"seed_backup_close": "关闭",
@@ -1459,6 +1641,17 @@
"sweep_to": "归集到:",
"sweep_toggle": "归集到我的钱包(不保留密钥)",
"sweep_tx": "交易:",
"swin_connection_failed": "连接失败:",
"swin_connection_successful": "连接成功!\ndragonxd 版本:",
"swin_invalid_suffix": "(无效)",
"swin_no_history_file_found": "未找到历史文件",
"swin_rescan_failed": "重新扫描失败:",
"swin_rescan_started_from_block": "重新扫描已开始,起始区块 ",
"swin_rescan_to": " 至 ",
"swin_rpc_client_not_initialized": "RPC 客户端未初始化",
"swin_settings_saved": "设置已保存",
"swin_theme_list_refreshed": "主题列表已刷新",
"swin_ztx_history_cleared": "Z 交易历史已清除",
"switch_corrupt_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。",
"switch_corrupt_repair": "尝试修复(salvage)",
"switch_progress_background": "在后台继续",
+5
View File
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings]
width = 600.0
height = 550.0
+156 -110
View File
@@ -37,6 +37,7 @@
#include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h"
#include "ui/windows/faq_dialog.h"
#include "embedded/IconsMaterialDesign.h"
#include "ui/windows/key_export_dialog.h"
#include "ui/windows/transaction_details_dialog.h"
@@ -410,8 +411,8 @@ bool App::init()
settings_->setActiveWalletFile("wallet.dat");
settings_->save();
ui::Notifications::instance().warning(
"Your last-used wallet file (" + active + ") was not found — opened the default wallet "
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f);
std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
}
}
}
@@ -805,7 +806,7 @@ void App::update()
const std::string& err = lite_wallet_->lastOpenError();
if (!err.empty() && err != lite_open_error_) {
lite_open_error_ = err;
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
ui::Notifications::instance().error(std::string(TR("appx_wallet_open_failed_prefix")) + err, 8.0f);
}
}
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
@@ -989,7 +990,7 @@ void App::update()
// poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked.
if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
resetWitnessRescanProgress();
@@ -1027,8 +1028,8 @@ void App::update()
state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
: ("Miner stopped: " + err));
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
: (std::string(TR("appx_miner_stopped_prefix")) + err));
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
@@ -1053,7 +1054,7 @@ void App::update()
// Pool mining has a connect delay — announce once it's actually connected/hashing.
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
pool_starting_.store(false, std::memory_order_relaxed);
ui::Notifications::instance().success("Pool miner connected and hashing.");
ui::Notifications::instance().success(TR("appx_pool_miner_connected_and_hashing"));
}
// Get memory directly from OS (more reliable than API)
double memMB = xmrig_manager_->getMemoryUsageMB();
@@ -1095,7 +1096,7 @@ void App::update()
const std::string& status = scan.lastStatus;
if (scan.finished) {
if (state_.sync.rescanning && user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
// Witness rebuild finishes with the rescan it's part of.
@@ -1301,7 +1302,7 @@ void App::update()
!runtime_rescan_active_ && !bootstrap_downloading_ &&
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
post_bootstrap_rescan_pending_ = false;
ui::Notifications::instance().info("Bootstrap complete — reconciling your wallet with the new chain data.");
ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
if (ok && !fullHistory) {
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
@@ -1323,7 +1324,16 @@ void App::update()
if (!state_.warming_up && !runtime_rescan_active_) {
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
refreshTransactionData();
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
// re-scan every block and starve connection near the tip. Bypass when there's an
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
// continue to completion), or an in-flight send — all need fresh data immediately.
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
hasTransactionSendProgress() || !send_txids_.empty();
if (txExplicitNeed || txRefreshDue()) {
refreshTransactionData();
}
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
refreshRecentTransactionData();
}
@@ -1339,7 +1349,13 @@ void App::update()
fastScanChatMemos();
}
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
// Explicit need (a changed address set, or an in-flight send tracking its change output)
// must refresh now; the routine periodic poll on a wallet page is throttled by the last
// address scan's measured cost (addressRefreshDue) — this is the z_listunspent hammering
// that was starving cs_main while synced on a large wallet.
if (addresses_dirty_ || hasTransactionSendProgress()) {
refreshAddressData();
} else if (walletDataPage && addressRefreshDue()) {
refreshAddressData();
}
}
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
settings_->setSkinId(skins[cur].id);
settings_->save();
}
ui::Notifications::instance().info("Theme: " + skins[cur].name);
ui::Notifications::instance().info(std::string(TR("appx_theme_prefix")) + skins[cur].name);
}
}
}
@@ -1433,7 +1449,7 @@ void App::handleGlobalShortcuts()
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
}
}
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
ui::Notifications::instance().info(newLow ? TR("appx_low_spec_mode_enabled") : TR("appx_low_spec_mode_disabled"));
}
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
settings_->setThemeEffectsEnabled(newState);
settings_->save();
}
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
ui::Notifications::instance().info(newState ? TR("appx_theme_effects_enabled") : TR("appx_theme_effects_disabled"));
}
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
settings_->setGradientBackground(newGrad);
ui::schema::SkinManager::instance().setGradientMode(newGrad);
settings_->save();
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
ui::Notifications::instance().info(newGrad ? TR("appx_simple_background_enabled") : TR("appx_simple_background_disabled"));
}
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
@@ -1595,8 +1611,9 @@ void App::render()
{
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
if (deletedN >= 0) {
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
" items). The daemon is restarting to re-sync from the network.");
char deletedMsg[160];
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
ui::Notifications::instance().success(deletedMsg);
}
}
@@ -2138,6 +2155,10 @@ void App::render()
ui::RenderAboutDialog(this, &show_about_);
}
if (show_faq_) {
ui::RenderFaqDialog(this, &show_faq_);
}
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
renderLiteFirstRunPrompt();
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
occupiedX = bellX;
}
// Help (?) — sits just left of the alert bell, on every platform. A plain, subtle question
// mark (no circle) that opens the FAQ.
{
ImFont* helpFont = ui::material::Type().iconSmall();
ImGui::PushFont(helpFont);
const float helpGlyphW = ImGui::CalcTextSize(ICON_MD_QUESTION_MARK).x;
ImGui::PopFont();
const float helpW = helpGlyphW + 10.0f * dp;
const float helpH = helpFont->LegacySize + 4.0f * dp;
const float helpX = occupiedX - helpW - gap;
ImGui::SameLine(helpX);
ui::material::IconButtonStyle hst;
hst.color = ui::material::OnSurfaceMedium();
hst.hoverColor = ui::material::OnSurface();
hst.hoverBg = ui::material::StateHover();
hst.bgRounding = 4.0f * dp;
hst.tooltip = TR("faq_open_tooltip");
if (ui::material::IconButton("##HelpFaq", ICON_MD_QUESTION_MARK, helpFont,
ImVec2(helpW, helpH), hst)) {
show_faq_ = true;
}
occupiedX = helpX;
}
// Version always at far right
ImGui::SameLine(versionX);
ImGui::Text("%s", versionBuf);
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float btnW = 170.0f;
const float dp = ui::Layout::dpiScale();
const float btnW = 170.0f * dp;
if (step == 0) {
// ── Welcome ──────────────────────────────────────────────────────────
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::TextUnformatted(TR("lite_welcome_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
ImGui::TextUnformatted(TR("lite_welcome_msg"));
ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
if (creating) {
// Async create (with server failover) is in flight — driven to completion by
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_creating_your_wallet"));
if (lite_wallet_->walletOpen()) {
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
if (s.ok && !s.seedPhrase.empty()) {
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
} else if (!lite_wallet_->openInProgress() &&
!lite_wallet_->lastOpenError().empty()) {
ui::Notifications::instance().warning(
std::string("Create failed: ") + lite_wallet_->lastOpenError());
std::string(TR("appx_create_failed_prefix")) + lite_wallet_->lastOpenError());
creating = false; // back to the buttons so the user can retry
}
} else {
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
} else if (step == 1) {
// ── Reveal the seed + birthday with backup warnings ─────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Back up your seed phrase");
ImGui::TextUnformatted(TR("appx_back_up_seed_phrase_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. "
"Write them down in order, store them offline, and never share "
"them. If you lose them, your funds are gone forever.");
ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
ImGui::Spacing();
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
char cell[96];
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
ImGui::TextUnformatted(cell);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f * dp);
}
ImGui::Spacing();
char bday[80];
snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday);
snprintf(bday, sizeof(bday), TR("appx_birthday_block_height"), birthday);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextUnformatted(bday);
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
chips.clear();
for (const auto& w : words) chips.emplace_back(w, false);
std::mt19937 rng{std::random_device{}()};
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
step = 2;
}
ImGui::SameLine();
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
} else if (step == 2) {
// ── Verify: tap the words in order ──────────────────────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Confirm your backup");
ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them.");
ImGui::TextUnformatted(TR("appx_tap_words_in_order"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size());
ImGui::TextDisabled(TR("appx_progress_n_of_n"), progress, (int)words.size());
if (ImGui::GetTime() < wrongFlashUntil) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted(" — that's not the next word");
ImGui::TextUnformatted(TR("appx_not_next_word"));
ImGui::PopStyleColor();
}
ImGui::Spacing();
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
ImGui::PushID((int)i);
if (chips[i].second) {
ImGui::BeginDisabled();
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0));
ImGui::EndDisabled();
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0))) {
if (progress < (int)words.size() && chips[i].first == words[progress]) {
chips[i].second = true; // correct next word
++progress;
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
const bool verified = progress == (int)words.size();
if (!verified) ImGui::BeginDisabled();
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
finish();
}
if (!verified) ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
if (restoring) {
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
ImGui::TextUnformatted("Restoring your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_restoring_your_wallet"));
if (lite_wallet_->walletOpen()) {
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
} else {
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f));
ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
if (restoreBirthday < 0) restoreBirthday = 0;
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
if (!seedTrim.empty() && !seedLenOk) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
std::to_string(seedWords) + ".").c_str());
char recoveryWordsMsg[96];
snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
TR("appx_recovery_phrase_word_count"), seedWords);
ImGui::TextUnformatted(recoveryWordsMsg);
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::Spacing();
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
restoring = true;
} else {
restoreErr = lite_wallet_->lastOpenError().empty()
? std::string("Could not start restore")
? std::string(TR("appx_could_not_start_restore"))
: lite_wallet_->lastOpenError();
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
sodium_memzero(restoreSeed, sizeof(restoreSeed));
restoreErr.clear();
step = 0;
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float dp = ui::Layout::dpiScale();
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted(TR("lite_unlock_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_unlock_msg"));
ImGui::Spacing();
ImGui::SetNextItemWidth(280.0f);
ImGui::SetNextItemWidth(280.0f * dp);
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
const float btnW = 130.0f;
const float btnW = 130.0f * dp;
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
if (doUnlock) {
const bool ok = lite_wallet_->unlockWallet(pass);
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
auto payment = util::parsePaymentURI(uri);
if (!payment.valid) {
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
ui::Notifications::instance().error(std::string(TR("appx_invalid_payment_uri_prefix")) + payment.error);
return;
}
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
setCurrentPage(ui::NavPage::Send);
// Notify user
std::string msg = "Payment request loaded";
std::string msg = TR("appx_payment_request_loaded");
if (payment.amount > 0) {
char buf[64];
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
void App::rescanBlockchain()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
// running would stomp each other — a second confirm must not launch a duplicate operation.
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
@@ -5491,7 +5537,7 @@ void App::rescanBlockchain()
void App::repairWallet()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5504,16 +5550,16 @@ void App::repairWallet()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
@@ -5542,11 +5588,11 @@ void App::repairWallet()
void App::reinstallBundledDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!resources::getBundledDaemonInfo().available) {
ui::Notifications::instance().warning("This build has no bundled daemon to install");
ui::Notifications::instance().warning(TR("appx_no_bundled_daemon_to_install"));
return;
}
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
@@ -5554,17 +5600,17 @@ void App::reinstallBundledDaemon()
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
if (!supportsEmbeddedDaemon()) {
ui::Notifications::instance().warning("This build has no embedded daemon to install");
ui::Notifications::instance().warning(TR("appx_no_embedded_daemon_to_install"));
return;
}
if (async_tasks_.isRunning("reinstall-daemon")) {
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
return;
}
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
void App::deleteBlockchainData()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
@@ -5719,7 +5765,7 @@ void App::beginShutdown()
// Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner...";
shutdown_status_ = TR("appx_stopping_pool_miner");
xmrig_manager_->stop(3000);
}
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
// Worker join + RPC disconnect happen in shutdown().
if (!daemon_controller_) {
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
shutdown_status_ = "Disconnecting...";
shutdown_status_ = TR("appx_disconnecting");
if (settings_) {
settings_->save();
}
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
// modal "Please wait" dialog).
shutdown_thread_ = std::thread([this]() {
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
shutdown_status_ = "Sending stop command to daemon...";
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
// Send RPC stop command
stopEmbeddedDaemon();
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
shutdown_status_ = "Cleaning up...";
shutdown_status_ = TR("appx_cleaning_up");
DEBUG_LOGF("shutdown thread: complete\n");
shutdown_status_ = "Shutdown complete";
shutdown_status_ = TR("appx_shutdown_complete");
shutdown_complete_ = true;
});
}
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted("Node is rebuilding its witness cache");
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
if (Type().subtitle1()) ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
ImGui::TextUnformatted(
"Stopping the node now discards the in-progress rebuild and restarts it (several minutes) "
"the next time you open the wallet. You can keep the node running instead.");
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::Spacing();
if (TactileButton("Keep node running & quit", ImVec2(0, 0))) {
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
shutdown_keep_daemon_override_ = true;
shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false;
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0));
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
ImGui::PopStyleColor(3);
if (stopAnyway) {
shutdown_confirmed_ = true;
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(0, 0))) {
if (TactileButton(TR("appx_cancel"), ImVec2(0, 0))) {
daemon_stop_confirm_open_ = false; // abort the quit; stay open
ImGui::CloseCurrentPopup();
}
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
void App::renderShutdownScreen()
{
using namespace ui::material;
const float dp = ui::Layout::dpiScale();
auto shutElem = [](const char* key, float fb) {
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
return v >= 0 ? v : fb;
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
// -------------------------------------------------------------------
float lineH = ImGui::GetTextLineHeightWithSpacing();
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
float spinnerD = shutElem("spinner-radius", 20.0f) * dp * 2.0f + 12.0f * dp;
float statusH = lineH * 2.0f;
float sepH = lineH;
float panelH = shutElem("panel-max-height", 160.0f);
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
// 2. Animated arc spinner
// -------------------------------------------------------------------
{
float r = shutElem("spinner-radius", 20.0f);
float thick = shutElem("spinner-thickness", 3.0f);
float r = shutElem("spinner-radius", 20.0f) * dp;
float thick = shutElem("spinner-thickness", 3.0f) * dp;
// Screen-space centre for draw list
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f * dp);
// Background ring (dim)
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
@@ -6031,7 +6076,7 @@ void App::renderShutdownScreen()
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
// Advance cursor past the spinner
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f * dp));
}
ImGui::Spacing();
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
char elapsed[64];
int secs = (int)shutdown_timer_;
if (secs < 60)
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
else
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_min_n_sec"), secs / 60, secs % 60);
ImGui::PushFont(Type().caption());
ImVec2 ts = ImGui::CalcTextSize(elapsed);
@@ -6078,7 +6123,8 @@ void App::renderShutdownScreen()
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
// chainstate; say so instead of a bare button.
if (shutdownStalled && !curShut.empty()) {
std::string stalledMsg = "Still \"" + curShut + "\" — force quitting now may corrupt chain data.";
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
TR("appx_still_status_suffix");
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
}
const char* forceLabel = TR("force_quit");
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float btnW = 120.0f * dp;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
ImGui::Dummy(ImVec2(0, 4.0f));
ImGui::Dummy(ImVec2(0, 4.0f * dp));
}
ImGui::Spacing();
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
ImGui::GetCursorPosY() + panelPad));
ImGui::PushFont(Type().caption());
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
ImGui::TextUnformatted("dragonxd output");
ImGui::TextUnformatted(TR("appx_dragonxd_output"));
ImGui::PopStyleColor();
ImGui::PopFont();
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
// Advance cursor past the panel
float panelBottom = panelMax.y - wp.y;
ImGui::SetCursorPosY(panelBottom + 4.0f);
ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
ImGui::Dummy(ImVec2(0, 0));
}
}
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
char wbuf[128];
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_blocks_left"),
progress * 100.0f, state_.sync.witness_remaining);
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_pct"), progress * 100.0f);
else if (progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_setting_initial_sapling_witnesses"), progress * 100.0f);
else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
@@ -6462,7 +6508,7 @@ void App::renderLoadingOverlay(float contentH)
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
snprintf(syncBuf, sizeof(syncBuf), TR("appx_syncing_pct_block_n_of_n"),
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
} else if (!state_.connected && state_.sync.blocks > 0) {
// Show last known block height while reconnecting
char blockBuf[64];
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
snprintf(blockBuf, sizeof(blockBuf), TR("appx_last_block_n"), state_.sync.blocks);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
@@ -6493,8 +6539,8 @@ void App::renderLoadingOverlay(float contentH)
if (!capFont) capFont = ImGui::GetFont();
const char* encLabel = encrypt_in_progress_
? "Encrypting wallet..."
: "Waiting for daemon to encrypt wallet...";
? TR("appx_encrypting_wallet")
: TR("appx_waiting_for_daemon_to_encrypt_wallet");
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
ImU32 encCol = IM_COL32(255, 218, 0, 200);
dl->AddText(capFont, capFont->LegacySize,
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
// Error title
const char* errTitle = "Daemon Error";
const char* errTitle = TR("appx_daemon_error");
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
dl->AddText(bodyFont2, bodyFont2->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
}
// Crash count hint
if (daemon_controller_->crashCount() >= 3) {
const char* hint = "Use Settings > Restart Daemon to try again";
const char* hint = TR("appx_use_settings_restart_daemon_hint");
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
@@ -6822,7 +6868,7 @@ void App::copySecretToClipboard(const std::string& secret)
clipboard_secret_hash_ = secret.empty() ? 0 : h;
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
if (!secret.empty())
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
}
void App::clearSecretClipboardIfArmed()
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
void App::restartDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
void App::reindexBlockDatabase()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!daemon_controller_) return;
+21
View File
@@ -462,6 +462,7 @@ public:
return 0;
}
void showAboutDialog() { show_about_ = true; }
void showFaqDialog() { show_faq_ = true; }
// Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab);
@@ -800,6 +801,7 @@ private:
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false;
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
@@ -809,6 +811,9 @@ private:
// Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
@@ -877,6 +882,8 @@ private:
void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
@@ -1028,6 +1035,7 @@ private:
bool show_demo_window_ = false;
bool show_settings_ = false;
bool show_about_ = false;
bool show_faq_ = false;
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
@@ -1143,6 +1151,14 @@ private:
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
// starving block connection near the tip (where effectivelySyncing() reads false). See
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
@@ -1498,6 +1514,11 @@ private:
void applyRefreshPolicy(ui::NavPage page);
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
+122 -12
View File
@@ -882,13 +882,32 @@ bool App::effectivelySyncing() const
// cheap wallet (sub-cadence cost) is unaffected — the tab's Core timer stays the real cadence; a ~20s
// scan on a large wallet backs off to roughly every ~200s instead of every 2s, freeing cs_main for
// block connection. A wallet mutation bypasses this via force_balance_refresh_.
bool App::scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const
{
constexpr double kScanDutyCycle = 0.10;
if (lastUpdate == 0) return true; // never fetched
if (lastScanMs <= 0.0) return true; // no cost measured yet
const double minInterval = (lastScanMs / 1000.0) / kScanDutyCycle;
return std::difftime(std::time(nullptr), static_cast<std::time_t>(lastUpdate)) >= minInterval;
}
bool App::balanceRefreshDue() const
{
constexpr double kBalanceDutyCycle = 0.10;
if (state_.last_balance_update == 0) return true; // never fetched
if (last_balance_scan_ms_ <= 0.0) return true; // no cost measured yet
const double minInterval = (last_balance_scan_ms_ / 1000.0) / kBalanceDutyCycle;
return std::difftime(std::time(nullptr), state_.last_balance_update) >= minInterval;
return scanRefreshDue(state_.last_balance_update, last_balance_scan_ms_);
}
// The address scan (z_listunspent) and history scan (z_listreceivedbyaddress) are just as O(mapWallet)
// and cs_main-bound as the balance scan, so they get the same adaptive back-off. This is what keeps a
// large synced wallet from re-scanning them every tab cadence — the near-tip starvation the balance-only
// throttle missed (only the periodic poll is gated; explicit refreshes call the refresh directly).
bool App::addressRefreshDue() const
{
return scanRefreshDue(state_.last_address_update, last_address_scan_ms_);
}
bool App::txRefreshDue() const
{
return scanRefreshDue(state_.last_tx_update, last_tx_scan_ms_);
}
bool App::currentPageNeedsWalletDataRefresh() const
@@ -1739,11 +1758,19 @@ void App::refreshCoreData()
// cost (0 when balance was skipped), and arm the settle window only on the
// syncing→caught-up edge so a wallet synced from the start is never throttled at connect.
if (result.balanceScanMs > 0.0) last_balance_scan_ms_ = result.balanceScanMs;
// Sticky-behind hysteresis: hold the low-impact profile for kSyncSettleSeconds after the
// LAST time we read "behind" — not only on the caught-up edge. While actually syncing we
// re-arm every Core tick, so the profile stays engaged continuously; once we truly reach
// the tip (within the 2-block tolerance in applyCoreRefreshResult) we stop re-arming and it
// lapses after the window. This is what breaks the near-tip feedback loop: a single stale or
// optimistic "caught up" reading can no longer immediately unleash the heavy O(mapWallet)
// scans and re-starve the tail (which dropped us behind again → oscillation). With the
// scans suppressed, kSyncProfile keeps sync detection itself cheap, so we keep getting fresh
// "behind" readings and stay latched until genuinely caught up. Window must exceed the
// worst-case Core-refresh interval so it can't lapse between two behind readings mid-catchup.
const bool nowSyncing = state_.sync.syncing;
if (nowSyncing) {
sync_settle_until_ = 0;
} else if (was_core_syncing_) {
constexpr double kSyncSettleSeconds = 8.0;
constexpr double kSyncSettleSeconds = 30.0;
if (nowSyncing || was_core_syncing_) {
sync_settle_until_ = std::time(nullptr) + static_cast<std::time_t>(kSyncSettleSeconds);
}
was_core_syncing_ = nowSyncing;
@@ -1872,7 +1899,13 @@ void App::refreshAddressData()
return [this, previousAddressCount, previousWalletIdentity, result = std::move(result)]() mutable {
const bool addrListOk = result.addressListOk; // capture before the move
const double scanMs = result.scanMs; // feed the adaptive throttle (addressRefreshDue)
if (scanMs > 0.0) last_address_scan_ms_ = scanMs;
// #3: this scan already listed the wallet's unspent notes — feed the chat send-budget from it
// (dedups the dedicated chat z_listunspent). Grab it before `result` is moved below.
auto unspentNotes = std::move(result.unspentNotes);
NetworkRefreshService::applyAddressRefreshResult(state_, std::move(result));
updateChatNoteBudgetFromUnspent(unspentNotes);
// Mark the address list as loaded ONLY if enumeration actually succeeded — a swallowed
// z_listaddresses/getaddressesbyaccount failure returns a falsely-short list, and stamping it
// would let the empty-wallet warning trust a spurious 0 count (see maybeWarnEmptyWallet…).
@@ -1931,6 +1964,13 @@ void App::refreshTransactionData()
transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_);
transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
// When fully synced, widen the re-scan tolerance so the O(mapWallet) per-address history scan runs
// every several blocks instead of ~every 2 — the heaviest residual synced-state cs_main cost. The
// balance poll still surfaces incoming funds on its own cadence; only the detailed history list lags a
// few minutes. (During catch-up the tx refresh is suppressed by kSyncProfile, so this is synced-only.)
if (!effectivelySyncing())
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -1941,6 +1981,7 @@ void App::refreshTransactionData()
return [this, result = std::move(result)]() mutable {
bool shieldedScanComplete = result.shieldedScanComplete;
if (result.scanMs > 0.0) last_tx_scan_ms_ = result.scanMs; // feed the adaptive throttle (txRefreshDue)
std::size_t nextShieldedScanStartIndex = result.nextShieldedScanStartIndex;
auto shieldedScanHeights = std::move(result.shieldedScanHeights);
NetworkRefreshService::TransactionCacheUpdate cacheUpdate{
@@ -2006,6 +2047,9 @@ void App::refreshRecentTransactionData()
transactionSnapshot.maxShieldedReceiveScans = 1;
transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
if (!effectivelySyncing()) // synced: widen the re-scan tolerance (see refreshTransactionData)
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -3535,9 +3579,25 @@ void App::refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model)
void App::refreshChatNoteBudgetNode() {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return;
// Never contend the daemon's cs_main while the node is catching up. z_listunspent is O(mapWallet)
// and holds cs_main for its whole duration — seconds on a large wallet — so an ungated per-few-second
// chat scan here starves block connection and keeps the node from ever pinning to the tip. Chat
// send-readiness can wait until we're synced; this budget is only advisory (the send path re-checks).
if (effectivelySyncing()) return;
// Only pre-warm the send-budget while the user is actually in chat. Off the Chat tab this heavy
// z_listunspent is unnecessary — and on a wallet-data tab, refreshAddressData's own z_listunspent
// already feeds the budget for free (updateChatNoteBudgetFromUnspent), so this dedicated scan is a
// fallback for the Chat tab (where the address refresh doesn't run).
if (current_page_ != ui::NavPage::Chat) return;
if (chat_note_scan_in_flight_) return; // one scan at a time
const double now = ImGui::GetTime();
if (now - chat_note_scan_last_ < 4.0) return; // rate limit; self-throttles via the in-flight flag too
// Adaptive back-off keyed on the last scan's own cost: on a large wallet where the scan takes many
// seconds, this stretches the interval so the scan can't occupy more than ~10% of wall-clock (same
// duty-cycle discipline as the balance/address/tx polls). A cheap wallet stays at the 4s floor.
double minInterval = 4.0;
if (chat_note_scan_ms_ > 0.0)
minInterval = std::max(4.0, (chat_note_scan_ms_ / 1000.0) / 0.10);
if (now - chat_note_scan_last_ < minInterval) return;
chat_note_scan_last_ = now;
chat_note_scan_in_flight_ = true;
@@ -3548,6 +3608,7 @@ void App::refreshChatNoteBudgetNode() {
int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0;
bool ok = false;
const auto scanStart = std::chrono::steady_clock::now();
try {
rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan");
nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing
@@ -3567,9 +3628,12 @@ void App::refreshChatNoteBudgetNode() {
}
}
} catch (const std::exception&) {}
return [this, scanGen, ok, verified, pipeline, verifiedZat]() {
const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, ok, verified, pipeline, verifiedZat, scanMs]() {
if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag)
chat_note_scan_in_flight_ = false;
if (scanMs > 0.0) chat_note_scan_ms_ = scanMs; // feed the adaptive back-off
if (!ok) return; // z_listunspent unavailable — leave caches as-is
chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified;
@@ -3584,6 +3648,36 @@ void App::refreshChatNoteBudgetNode() {
});
}
// #3: derive the chat send-budget from an already-collected z_listunspent (refreshAddressData's scan),
// so we don't issue a duplicate z_listunspent. Runs on the main thread; the notes are pre-parsed to the
// fields we need (amount / locked / TRUE confirmations), so there's no RPC and no worker hop. Mirrors the
// apply in refreshChatNoteBudgetNode and stamps chat_note_scan_last_ so the dedicated scan backs off.
void App::updateChatNoteBudgetFromUnspent(
const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes) {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
const int confsNeeded = chatConfsRequired();
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0;
for (const auto& nz : unspentNotes) {
if (nz.locked) continue; // tied up by an in-flight send
const std::uint64_t amtZat = chatDrgxToZat(nz.amount);
if (amtZat < minVal) continue; // skip dust below a fee
++pipeline; // unspent self-note (verified or maturing)
if (nz.confirmations >= confsNeeded) { ++verified; verifiedZat += amtZat; }
}
chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified;
chat_pipeline_note_count_ = pipeline;
chat_verified_shielded_zat_ = verifiedZat;
chat_note_scan_last_ = ImGui::GetTime(); // counts as a fresh scan → dedicated scan backs off
if (chat_split_outstanding_ &&
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
chat_split_outstanding_ = false;
}
}
void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) {
QueuedChatOp op;
op.kind = kind;
@@ -3825,15 +3919,28 @@ void App::fastScanChatMemos()
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_) return;
if (chat_fast_scan_in_flight_) return; // don't stack RPCs if a previous scan is still running
// z_listreceivedbyaddress is O(mapWallet) (the daemon iterates every wtx even when filtered to one
// address) — seconds on a large wallet — so this 0-conf poll must NOT run while the node is catching
// up, and even when synced it backs off to ~10% wall-clock keyed on its own cost. The block-tip
// harvest still ingests every message on confirmation; this only trades away mempool-speed delivery.
if (effectivelySyncing()) return;
// Mempool-speed 0-conf delivery is only worth its heavy cost while the user is watching chat. Off the
// Chat tab, incoming messages still arrive via the confirmed block-tip harvest (+ toast) within ~1 block.
if (current_page_ != ui::NavPage::Chat) return;
const double nowT = ImGui::GetTime();
if (chat_fast_scan_ms_ > 0.0 &&
nowT - chat_fast_scan_last_ < (chat_fast_scan_ms_ / 1000.0) / 0.10) return;
const std::string addr = chatReplyZaddr();
if (addr.empty()) return;
chat_fast_scan_last_ = nowT;
chat_fast_scan_in_flight_ = true;
const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks
worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb {
std::vector<chat::HushChatTransactionMetadata> metadata;
int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal)
std::string scanError;
const auto scanStart = std::chrono::steady_clock::now();
try {
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
@@ -3864,12 +3971,15 @@ void App::fastScanChatMemos()
}
} catch (const std::exception& e) { scanError = e.what(); }
const int parsedCount = static_cast<int>(metadata.size());
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError]() mutable {
const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError, scanMs]() mutable {
// The wallet was switched/locked between post and now — this metadata belongs to the previous
// session. resetChatSession already reset the in-flight flag, so just drop; clearing it here
// would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards).
if (scanGen != chat_session_generation_) return;
chat_fast_scan_in_flight_ = false;
if (scanMs > 0.0) chat_fast_scan_ms_ = scanMs; // feed the adaptive back-off
// Diagnostic (console App/chat channel): log when the memo-note count at the reply address
// CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages
// are reaching us at all, and how many parse as chat vs are unrelated memos.
+44 -45
View File
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
TR("sec_restart_daemon_for_encryption"));
}
}
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) {
return [this]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f);
TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error(
"Encryption failed: " + err);
std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure
if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
TR("sec_wallet_encrypted_but_pin_vault_failed"));
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
}
wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error("Encryption failed: " + err);
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
wallet_security_.clearDeferredEncryption();
};
}
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) {
lock_failure_warned_ = true;
ui::Notifications::instance().warning(
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
TR("sec_couldnt_lock_wallet"), 12.0f);
}
}
};
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Changing passphrase...";
encrypt_status_ = TR("sec_changing_passphrase");
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully");
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else {
encrypt_status_ = "Failed: " + err_msg;
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
}
util::SecureVault::secureZero(newPass.data(), newPass.size());
};
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. "
"Open Settings to finish encrypting it.", 30.0f);
TR("sec_encryption_did_not_complete"), 30.0f);
}
if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface();
{
const char* title = "Wallet Locked";
const char* title = TR("sec_wallet_locked_title");
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128];
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
const char* switchLabel = lock_use_pin_
? "Use passphrase instead"
: "Use PIN instead";
? TR("sec_use_passphrase_instead")
: TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) {
// Animated spinner dots
char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true;
} else {
rpcErr = "Not connected to daemon";
rpcErr = TR("sec_not_connected_to_daemon");
}
} catch (const std::exception& e) {
rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
applyUnlockFailure("Unlock failed: " + rpcErr);
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
}
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) {
return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase");
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
};
}
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
});
ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs",
TR("sec_importing_keys_rescanning"),
8.0f);
};
});
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport();
ui::Notifications::instance().error(
err +
"\nEncrypted backup: wallet.dat.encrypted.bak",
TR("sec_encrypted_backup_suffix"),
12.0f);
};
});
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo();
ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.",
TR("sec_wallet_decrypted_all_keys_imported"),
8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
}
// ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying passphrase...";
pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() {
pin_status_ = "Incorrect passphrase";
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false;
};
}
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully");
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else {
pin_status_ = "Failed to create vault";
pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Not connected to daemon";
pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false;
}
}
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Changing PIN...";
pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully");
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else {
pin_status_ = "Incorrect current PIN";
pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false;
}
}
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying PIN...";
pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed");
ui::Notifications::instance().info(TR("sec_pin_removed"));
} else {
pin_status_ = "Incorrect PIN";
pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false;
}
}
+15
View File
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
add("modal-faq", ui::NavPage::Overview,
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
@@ -708,9 +710,22 @@ void App::startSweepImpl(bool full)
if (sweep_current_theme_only_)
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
// under review instead of all surfaces x every skin.
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog();
if (!sweepOnlyStr.empty()) {
std::vector<SweepTarget> keep;
for (const auto& t : sweep_targets_)
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
sweep_targets_.swap(keep);
}
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
+11 -1
View File
@@ -816,11 +816,21 @@ void EmbeddedDaemon::drainOutput()
}
size_t currentSize = static_cast<size_t>(fileSize.QuadPart);
// Truncation / rotation detection. dragonxd shrinks debug.log on startup (ShrinkDebugFile keeps
// only the tail once it has grown large), so the file can become SMALLER than where we left off.
// Our offset was set to the PRE-shrink size at spawn, so without this reset it stays stranded ahead
// of the freshly-truncated file and we read NOTHING for the whole session — no block height (status
// bar shows "Block: 0") and, worse, no witness-rebuild progress, since the "Setting Initial Sapling
// Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
// restart from the new beginning; the parser is monotonic and converges as it reaches current output.
if (currentSize < debug_log_offset_) {
debug_log_offset_ = 0;
}
if (currentSize <= debug_log_offset_) {
CloseHandle(hFile);
return; // No new data
}
// Seek to where we left off
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(debug_log_offset_);
+53 -8
View File
@@ -297,7 +297,31 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
bool blockOk = false;
double balanceScanMs = 0.0;
if (includeBalance) {
// getblockchaininfo FIRST — it's cheap, and the sync state it returns gates everything else
// (the balance/address/tx throttles and kSyncProfile). Running it BEFORE the O(mapWallet) balance
// scan keeps sync detection from being delayed behind (or, under contention, starved by) that
// scan — the failure mode where the wallet kept reading "synced" while actually falling behind,
// so kSyncProfile never engaged. See effectivelySyncing()'s sticky-behind latch.
try {
blockInfo = rpc.call("getblockchaininfo", json::array());
blockOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
// If getblockchaininfo shows we're behind (same 2-block tolerance as applyCoreRefreshResult), skip
// the balance scan this cycle regardless of includeBalance: the balance is incomplete mid-sync
// anyway, and skipping it lets the sync-state update — and hence kSyncProfile — take effect at the
// end of THIS (now-cheap) task instead of being delayed ~20s behind the scan. This is what lets the
// sticky-behind latch engage promptly the first time the node falls behind.
bool behind = false;
if (blockOk && blockInfo.is_object()) {
const long long b = blockInfo.value("blocks", 0LL);
const long long lc = blockInfo.value("longestchain", 0LL);
if (lc > 0 && b < lc - 2) behind = true;
}
if (includeBalance && !behind) {
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
// (balanceRefreshDue()), keeping balance scans from starving block connection.
@@ -315,13 +339,6 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
std::chrono::steady_clock::now() - balanceStart).count();
}
try {
blockInfo = rpc.call("getblockchaininfo", json::array());
blockOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
result.balanceScanMs = balanceScanMs;
return result;
@@ -603,6 +620,10 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
const AddressRefreshSnapshot& snapshot)
{
AddressRefreshResult result;
// Time the whole scan — z_listunspent (and per-address z_getbalance fallback) hold the daemon's
// cs_main for the duration, seconds on a large shielded wallet. The measured cost feeds the
// caller's adaptive throttle so the address poll can't starve block connection.
const auto scanStart = std::chrono::steady_clock::now();
try {
json zList = rpc.call("z_listaddresses", json::array());
@@ -644,6 +665,22 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
try {
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
// Retain a minimal view so a downstream consumer (chat note-budget) can reuse this scan instead
// of issuing its own z_listunspent. rawconfirmations is the TRUE depth; `confirmations` is
// dPoW-clamped to 1 and understates it.
if (unspent.is_array()) {
result.unspentNotes.reserve(unspent.size());
for (const auto& nz : unspent) {
if (!nz.is_object()) continue;
UnspentNoteLite lite;
lite.amount = nz.value("amount", 0.0);
lite.locked = nz.value("locked", false);
lite.confirmations = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
? nz["rawconfirmations"].get<int>()
: nz.value("confirmations", 0);
result.unspentNotes.push_back(lite);
}
}
} catch (const std::exception& e) {
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
for (auto& info : result.shieldedAddresses) {
@@ -673,6 +710,8 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
DEBUG_LOGF("listunspent error: %s\n", e.what());
}
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return result;
}
@@ -864,6 +903,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.blockHeight = currentBlockHeight;
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
result.shieldedScanHeights = snapshot.shieldedScanHeights;
// Time the whole scan — the per-address z_listreceivedbyaddress pass is O(mapWallet) and holds the
// daemon's cs_main. The measured cost feeds the caller's adaptive throttle (txRefreshDue()) so the
// routine full history rescan can't starve block connection on a large wallet.
const auto scanStart = std::chrono::steady_clock::now();
std::set<std::string> knownTxids;
HushChatMemoOutputMap hushChatReceivedOutputs;
@@ -1044,6 +1087,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
sortTransactionsNewestFirst(result.transactions);
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return result;
}
+17
View File
@@ -147,6 +147,14 @@ public:
std::string errorMessage;
};
// Minimal per-note view of a z_listunspent entry — just what a downstream consumer needs to derive
// spendable-note budgets without re-scanning. Kept UI/feature-agnostic (no chat specifics here).
struct UnspentNoteLite {
double amount = 0.0; // note value, DRGX
bool locked = false; // tied up by an in-flight send
int confirmations = 0; // TRUE depth (rawconfirmations when present, else confirmations)
};
struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses;
@@ -154,6 +162,12 @@ public:
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
// empty-wallet warning) must not trust a 0 count unless this is true.
bool addressListOk = true;
// Wall-clock spent in the address scan (dominated by z_listunspent — O(mapWallet), holds the
// daemon's cs_main). Lets the caller throttle how often it polls (addressRefreshDue()).
double scanMs = 0.0;
// The wallet's unspent notes from this same z_listunspent scan, so a consumer (e.g. the chat
// note-budget) can be fed for free instead of running its own duplicate z_listunspent.
std::vector<UnspentNoteLite> unspentNotes;
};
struct AddressRefreshSnapshot {
@@ -205,6 +219,9 @@ public:
std::size_t shieldedAddressCount = 0;
std::unordered_map<std::string, int> shieldedScanHeights;
bool shieldedScanComplete = true;
// Wall-clock spent in the history scan (z_listreceivedbyaddress — O(mapWallet), holds cs_main).
// Lets the caller throttle the routine full rescan by its measured cost (txRefreshDue()).
double scanMs = 0.0;
};
struct OperationStatusPollResult {
+12 -8
View File
@@ -5,6 +5,7 @@
#include "theme_effects.h"
#include "low_spec.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
void ThemeEffects::loadFromTheme() {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
auto eff = [&](const char* name) {
return S.drawElement("effects", name);
};
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
// ---- Shimmer ----
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
// Shimmer color: read from the schema's color resolver
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
auto glowColorElem = eff("glow-pulse-color");
if (!glowColorElem.color.empty()) {
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
auto edgeColorElem = eff("edge-trace-color");
if (!edgeColorElem.color.empty()) {
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
auto emberColorElem = eff("ember-rise-color");
if (!emberColorElem.color.empty()) {
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
auto gbColorA = eff("gradient-border-color-a");
if (!gbColorA.color.empty()) {
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return;
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Deterministic pseudo-random x position per particle
// Simple hash: sin of large prime multiples
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
float x = pMin.x + w * xHash + xDrift;
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
ImGuiViewport* vp = ImGui::GetMainViewport();
float vpW = vp->WorkSize.x;
float vpH = vp->WorkSize.y;
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
xHash = xHash - (int)xHash; // fractional part
if (xHash < 0) xHash += 1.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
float x = vpX + vpW * xHash + xDrift;
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
+11 -10
View File
@@ -925,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
// Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
float stripeW = 4.0f;
float stripeW = 4.0f * Layout::dpiScale();
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft);
@@ -1282,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth();
float barHeight = 36.0f;
const float dp = Layout::dpiScale();
float barHeight = 36.0f * dp;
// Get accent color from theme if not provided
if (!accent_col) {
@@ -1306,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title);
float titleX = barMin.x + 16.0f;
float titleX = barMin.x + 16.0f * dp;
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont();
// Close button (X) on right side
if (p_open) {
float btnSize = 24.0f;
float btnX = barMax.x - btnSize - 12.0f;
float btnSize = 24.0f * dp;
float btnX = barMax.x - btnSize - 12.0f * dp;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
@@ -1327,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Button background on hover
if (hovered) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
}
// Draw X icon
@@ -1348,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
}
// Reserve space for title bar so content starts below it
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
return closeClicked;
}
@@ -1648,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
}
@@ -1662,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
// content overflowed the viewport); otherwise the child auto-resizes to its content.
+81 -58
View File
@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
static void renderSettingsTabBar(float availWidth) {
using namespace material;
struct T { int id; const char* label; const char* idstr; };
static const T tabs[] = {
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"},
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"},
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"},
{TAB_ABOUT, "About", "##stabT"},
const T tabs[] = {
{TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
{TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
{TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
{TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
};
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* f = Type().body2();
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string lbl = skin.name + " (invalid)";
std::string lbl = skin.name + TR("grpa_invalid_suffix");
ImGui::Selectable(lbl.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -1196,14 +1196,25 @@ void RenderSettingsPage(App* app) {
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
if (app && app->daemonAutoShieldProbed()) {
// This status must NOT flow with ImGui's cursor: the checkbox grid is absolutely positioned
// (SetCursorScreenPos at rowY), so a normal-flow Text would land under the auto-shield checkbox
// and the next grid row (Use Tor / Keep daemon) would draw on top of it. Instead, draw it on its
// own full-width row at the grid's current rowY, wrapped to the card, then advance rowY past it.
if (app && app->daemonAutoShieldProbed() &&
(app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) {
ImGui::SetCursorScreenPos(ImVec2(cx, rowY));
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
if (app->daemonAutoShieldActive()) {
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
if (!app->daemonAutoShieldAddress().empty())
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
} else {
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
}
ImGui::PopTextWrapPos();
last = ImGui::GetCursorScreenPos().y; // cursor is now below the status text
rowY = last + gp; // next grid row starts below it
c = 0;
}
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
@@ -1214,26 +1225,7 @@ void RenderSettingsPage(App* app) {
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
// O2: host a RandomX stratum pool from this node. Only offered on daemons that implement
// it (v1.3.0+, version encoded major*1e6+minor*1e4+rev*100+build), so we never show a
// toggle that does nothing. Takes effect on the next daemon start/restart. Blank allow-IP
// = loopback only (safe); a subnet opens it to that LAN.
if (app->state().daemon_version >= 1030000) {
if (CB(TrId("stratum_host", "strat_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
}
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_birthday_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_account_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
@@ -1889,7 +1881,7 @@ void RenderSettingsPage(App* app) {
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
if (liteKey.empty()) {
s_settingsState.lite_backup_status = "Enter a private key to import.";
s_settingsState.lite_backup_status = TR("grpa_enter_private_key_to_import");
} else {
const auto r = app->liteWallet()->importKey(liteKey);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
@@ -2650,6 +2642,10 @@ void RenderSettingsPage(App* app) {
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
lf.next(material::ActionButtonWidth(TR("faq"), ICON_MD_QUESTION_MARK));
if (material::ActionButton("##aboutfaq", TR("faq"), ICON_MD_QUESTION_MARK, AT::Secondary))
app->showFaqDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("faq_open_tooltip"));
}
const float rightBottom = ImGui::GetCursorScreenPos().y;
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.current_tab == TAB_NODE)
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// MINING POOL HOSTING — run this node's built-in RandomX stratum server so other miners can point
// at this machine. It's a node feature, so it belongs here (not the Wallet tab). Full-node only, and
// only on a daemon new enough to implement -stratum (v1.3.0+, version encoded
// major*1e6+minor*1e4+rev*100+build). Takes effect on the next daemon start/restart; a blank allow-IP
// keeps it loopback-only (safe), a subnet opens it to that LAN.
// ====================================================================
if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE &&
app->state().daemon_version >= 1030000) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("stratum_host_section"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
if (ImGui::Checkbox(TR("stratum_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// ====================================================================
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
@@ -2738,7 +2761,7 @@ void RenderSettingsPage(App* app) {
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
if (TactileButton(TR("grpa_seed_demo_chat"), ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
}
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
"paymentdisclosure", "pow", "proxy", "prune", "rand",
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
};
static const char* debugTips[] = {
"Peer address tracking and management",
"Alert system messages",
"Benchmark timings for operations",
"Coin database read/write operations",
"Berkeley DB operations",
"Fee estimation algorithm",
"HTTP RPC server activity",
"Libevent networking library",
"Lock contention debugging",
"Transaction memory pool activity",
"Network connections and messages",
"Payment disclosure protocol",
"Proof-of-work mining activity",
"SOCKS5 proxy connections",
"Block pruning operations",
"Random number generation",
"Blockchain reindexing progress",
"RPC command processing",
"Coin selection for transactions",
"Tor integration and circuit info",
"ZeroMQ notification system",
"Shielded (z-addr) RPC operations"
const char* debugTips[] = {
TR("grpa_dbg_addrman"),
TR("grpa_dbg_alert"),
TR("grpa_dbg_bench"),
TR("grpa_dbg_coindb"),
TR("grpa_dbg_db"),
TR("grpa_dbg_estimatefee"),
TR("grpa_dbg_http"),
TR("grpa_dbg_libevent"),
TR("grpa_dbg_lock"),
TR("grpa_dbg_mempool"),
TR("grpa_dbg_net"),
TR("grpa_dbg_paymentdisclosure"),
TR("grpa_dbg_pow"),
TR("grpa_dbg_proxy"),
TR("grpa_dbg_prune"),
TR("grpa_dbg_rand"),
TR("grpa_dbg_reindex"),
TR("grpa_dbg_rpc"),
TR("grpa_dbg_selectcoins"),
TR("grpa_dbg_tor"),
TR("grpa_dbg_zmq"),
TR("grpa_dbg_zrpc")
};
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
ImGui::Spacing();
ImGui::Text("%s", TR("rescan_from_height"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
} else {
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40 * dp))) {
s_settingsState.confirm_rescan = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(detecting);
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) {
if (bootstrapped) {
app->runtimeRescan(s_settingsState.rescan_start_height);
} else {
+7 -9
View File
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
float lineW = s_cc.lineW;
// --- Outer glow pass: wider, softer dark edge on top-left ---
float glowExpand = s_cc.glowExpand;
float glowExpand = s_cc.glowExpand * Layout::dpiScale();
int glowA = (int)s_cc.glowAlpha;
float glowLineW = s_cc.glowLineW;
{
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// inner pass gives crisp bevel edge. All use AddRect with rounding so
// every layer follows the rounded corners perfectly — no clip rects needed.
{
const float dp = Layout::dpiScale();
float cx = (mn.x + mx.x) * 0.5f;
float cy = (mn.y + mx.y) * 0.5f;
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
BevelPass passes[] = {
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
};
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
}
}
if (depth > s_ic.threshold) {
float baseInset = s_ic.inset;
float baseInset = s_ic.inset * Layout::dpiScale();
int shadowMax = (int)(s_ic.maxAlpha * depth);
float fadeRatio = s_ic.fadeRatio;
float bW = mx.x - mn.x - baseInset * 2.0f;
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap;
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (showLabels) {
curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot;
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
} else {
curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY;
@@ -653,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
fx.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
}
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd});
}
@@ -684,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f;
if (itemHasBadge) {
@@ -732,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
badgeCol = Warning(); badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; badgeCol = Success();
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
badgeCount = status.peerCount;
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
badgeCount = status.chatUnreadCount;
}
+3 -2
View File
@@ -93,7 +93,7 @@ public:
// Arrow
{
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f);
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos();
@@ -312,7 +312,8 @@ private:
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
const float dp = Layout::dpiScale();
float btnW = 120.0f * dp;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false;
+1 -1
View File
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
} else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (ch < 60.0f * dp) ch = 60.0f * dp;
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
+51 -51
View File
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
// Card height: must fit the Market card's content (overline + price + 24h)
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
float marketContentH = cardPadLg
+ ovFont->LegacySize + ovGap
+ sub1->LegacySize + 2.0f * dp
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
// We draw a full-size rounded rect (left corners only) and clip it to the
// stripe width so the shape itself follows the card rounding.
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
// Hover → explain what stale means and how old the data actually is.
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip"));
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
}
}
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
{
float privPct = (s_dispTotal > 1e-9)
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
privPct, (int)state.z_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
WithAlpha(Success(), 160), buf);
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
ImVec2 ts = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, buf);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
ImVec2 bMin(cMax.x - ts.x - bp * 3,
cMin.y + cardPadLg);
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
OnSurfaceMedium(), DRAGONX_TICKER);
cy += sub1->LegacySize + valGap;
snprintf(buf, sizeof(buf), "%d T-addresses",
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
(int)state.t_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
OnSurfaceDisabled(), buf);
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
[](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg;
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
bool pos = market.change_24h >= 0;
ImU32 chgCol = pos ? Success()
: Error();
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy), chgCol, buf);
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Market
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Market);
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
// Shielded legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
legendY += capFont->LegacySize + legendLineGap;
// Transparent legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
legendY += capFont->LegacySize + legendSectionGap;
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
const auto& market = state.market;
if (market.price_usd > 0) {
if (market.price_usd >= 0.01)
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
else
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
OnSurfaceMedium(), buf);
legendY += capFont->LegacySize + 4 * dp;
bool pos = market.change_24h >= 0;
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
pos ? Success() : Error(), buf);
}
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
// Bottom half: proportion bars
float barY = divY + Layout::spacingSm();
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
// Accent stripe — clipped to tile rounded corners
{
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
ImDrawFlags_RoundCornersLeft);
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
// Click
if (material::IsRectHovered(tMin, tMax)) {
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(tiles[i].nav);
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Font-content floor per row: icon + label + value must fit
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
+ Layout::spacingSm() * 2;
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
float stackH;
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
};
RowInfo rowInfos[4] = {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
};
for (int i = 0; i < 4; i++) {
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
if (i == 1 || i == 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float barH = std::max(
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
// Sparkline in the gap between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
ImFont* iconFont = Type().iconSmall();
// Font-content floor per row: caption text + vertical padding
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
float stackH;
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
CellInfo cells[2][2] = {
// Row 0: Total Balance (left), Shielded (right)
{
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
},
// Row 1: Market (left), Transparent (right)
{
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
},
};
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
// Sparkline between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
gaugeCy + sinf(needleAngle) * needleLen);
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
// Center text: percentage
ImFont* sub1 = Type().subtitle1();
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
}
RenderSyncBar(app, dl, vs);
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
// Row 2: 3 mini-cards inline
{
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
ImFont* capFont = Type().caption();
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
// Sparkline between price and percentage
if (market.price_history.size() >= 2) {
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = cx + priceSz.x + sparkGap;
float sparkRightEdge = sparkRight - sparkGap;
if (sparkLeft < sparkRightEdge) {
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImVec2 sepPos = ImGui::GetCursorScreenPos();
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
float x = sepPos.x;
float endX = sepPos.x + availW;
while (x < endX) {
+4 -4
View File
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_loading = false;
if (!error.empty()) {
s_error = "Error: " + error;
s_error = std::string(TR("grpa_error_prefix")) + error;
return;
}
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_has_data = true;
} else {
s_error = "Invalid response from daemon";
s_error = TR("grpa_invalid_response_from_daemon");
}
}
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
// Current block info
if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
}
ImGui::SameLine();
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) {
error = "unexpected getblockhash result";
error = TR("grpa_unexpected_getblockhash_result");
} else {
block = rpc->call("getblock", {hashResult.get<std::string>()});
}
+2 -2
View File
@@ -144,7 +144,7 @@ private:
if (!s_bootstrap) {
s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized";
s_errorMsg = TR("grpc_bootstrap_not_initialized");
return;
}
@@ -227,7 +227,7 @@ private:
s_state = State::Done;
} else {
s_errorMsg = finalProg.error;
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
s_state = State::Failed;
}
s_bootstrap.reset();
+3 -3
View File
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava";
const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
};
const int N = 3;
+1 -1
View File
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
float outputH = ComputeConsoleOutputHeight(
availHeight,
input_height,
schema::UI().drawElement("tabs.console", "output-min-height").size,
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
ImDrawList* dlOut = ImGui::GetWindowDrawList();
+6 -6
View File
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60)
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
else
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
return buf;
}
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
txDL->AddRectFilled(rowStart,
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
WithAlpha(OnSurface(), 10),
S.drawElement("tabs.explorer", "row-rounding").size);
S.drawElement("tabs.explorer", "row-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", txid.c_str());
}
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
}
if (s_detail_txids.size() > 100) {
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
ImGui::TextDisabled("%s", buf);
}
}
+115
View File
@@ -0,0 +1,115 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_content.h"
namespace dragonx {
namespace ui {
namespace faq {
// ── Wallet group ───────────────────────────────────────────────────────────
const std::vector<FaqSubcategory>& walletFaq()
{
static const std::vector<FaqSubcategory> kWallet = {
{ "faq_w_gs_title", {
{ "faq_w_gs_1_q", "faq_w_gs_1_a" },
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
}},
{ "faq_w_addr_title", {
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
}},
{ "faq_w_send_title", {
{ "faq_w_send_1_q", "faq_w_send_1_a" },
{ "faq_w_send_2_q", "faq_w_send_2_a" },
{ "faq_w_send_3_q", "faq_w_send_3_a" },
{ "faq_w_send_4_q", "faq_w_send_4_a" },
}},
{ "faq_w_bal_title", {
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
}},
{ "faq_w_sec_title", {
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
}},
{ "faq_w_seed_title", {
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
}},
{ "faq_w_chat_title", {
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
}},
{ "faq_w_set_title", {
{ "faq_w_set_1_q", "faq_w_set_1_a" },
{ "faq_w_set_2_q", "faq_w_set_2_a" },
{ "faq_w_set_3_q", "faq_w_set_3_a" },
{ "faq_w_set_4_q", "faq_w_set_4_a" },
}},
};
return kWallet;
}
// ── Daemon group (full-node only) ──────────────────────────────────────────
const std::vector<FaqSubcategory>& daemonFaq()
{
static const std::vector<FaqSubcategory> kDaemon = {
{ "faq_d_node_title", {
{ "faq_d_node_1_q", "faq_d_node_1_a" },
{ "faq_d_node_2_q", "faq_d_node_2_a" },
{ "faq_d_node_3_q", "faq_d_node_3_a" },
}},
{ "faq_d_sync_title", {
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
}},
{ "faq_d_mgmt_title", {
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
}},
{ "faq_d_upd_title", {
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
}},
{ "faq_d_mine_title", {
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
}},
{ "faq_d_net_title", {
{ "faq_d_net_1_q", "faq_d_net_1_a" },
{ "faq_d_net_2_q", "faq_d_net_2_a" },
}},
{ "faq_d_perf_title", {
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
}},
{ "faq_d_trbl_title", {
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
}},
};
return kDaemon;
}
} // namespace faq
} // namespace ui
} // namespace dragonx
+41
View File
@@ -0,0 +1,41 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
// and its two strings to loadBuiltinEnglish().
#pragma once
#include <vector>
namespace dragonx {
namespace ui {
namespace faq {
// One question and its answer, both i18n keys. The answer may contain "\n\n"
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
struct FaqEntry {
const char* questionKey;
const char* answerKey;
};
// A named group of questions. titleKey is an i18n key for the subcategory header.
struct FaqSubcategory {
const char* titleKey;
std::vector<FaqEntry> entries;
};
// The two top-level groups. daemonFaq() is full-node material and is only shown when
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
const std::vector<FaqSubcategory>& walletFaq();
const std::vector<FaqSubcategory>& daemonFaq();
} // namespace faq
} // namespace ui
} // namespace dragonx
+201
View File
@@ -0,0 +1,201 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_dialog.h"
#include "faq_content.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx {
namespace ui {
namespace {
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
struct FaqDialogState {
int group = 0;
char search[128] = "";
std::unordered_map<std::string, bool> expanded;
};
FaqDialogState s_faq;
bool entryMatches(const faq::FaqEntry& e, const char* query)
{
return util::containsIgnoreCase(TR(e.questionKey), query) ||
util::containsIgnoreCase(TR(e.answerKey), query);
}
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
void renderAnswer(const char* answerKey)
{
ImGui::Indent(Layout::spacingMd());
ImGui::PushFont(material::Type().body2());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR(answerKey));
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::Unindent(Layout::spacingMd());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
} // namespace
void RenderFaqDialog(App* app, bool* p_open)
{
auto& S = schema::UI();
auto win = S.window("dialogs.faq");
const float dp = Layout::dpiScale();
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
// viewport so the content flexes + scrolls on small / HiDPI screens.
const float vpH = ImGui::GetMainViewport()->Size.y;
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
material::OverlayDialogSpec spec;
spec.title = TR("faq_title");
spec.p_open = p_open;
spec.style = material::OverlayStyle::BlurFloat;
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
spec.idSuffix = "faq";
if (!material::BeginOverlayDialog(spec)) {
return;
}
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
// fires when the field isn't capturing it).
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const float contentW = ImGui::GetContentRegionAvail().x;
// ── Group tabs: Wallet | Daemon ──
{
const int nTabs = daemonAvailable ? 2 : 1;
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
auto tab = [&](const char* label, int idx) {
const bool active = (s_faq.group == idx);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
}
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
if (active) ImGui::PopStyleColor(2);
};
tab(TR("faq_group_wallet"), 0);
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ── Search ──
ImGui::SetNextItemWidth(contentW);
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
const bool searching = s_faq.search[0] != '\0';
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
material::ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
bool anyShown = false;
bool firstSection = true;
for (const auto& subcat : groups) {
// Collect the entries visible under the current search.
std::vector<const faq::FaqEntry*> visible;
for (const auto& e : subcat.entries) {
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
}
if (visible.empty()) continue;
// Section break: generous space above every section after the first, so topic groups read as
// clearly separated bands rather than one uniform list.
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
firstSection = false;
anyShown = true;
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
// above its (brighter, normal-case) question rows.
material::Type().textColored(material::TypeStyle::Overline,
material::Primary(), TR(subcat.titleKey));
{
const ImVec2 rp = ImGui::GetCursorScreenPos();
const float rw = ImGui::GetContentRegionAvail().x;
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
material::Divider(), 1.0f * dp);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
for (const auto* e : visible) {
const float rowW = ImGui::GetContentRegionAvail().x;
if (searching) {
// Search results: show question + answer directly (no collapsing).
ImGui::PushFont(material::Type().subtitle2());
ImGui::TextWrapped("%s", TR(e->questionKey));
ImGui::PopFont();
renderAnswer(e->answerKey);
} else {
bool& exp = s_faq.expanded[e->questionKey];
std::string id = std::string("##faq_") + e->questionKey;
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
material::Type().subtitle2(), material::OnSurface());
if (exp) renderAnswer(e->answerKey);
}
}
}
if (!anyShown) {
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("faq_no_results"));
ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
// Close button footer (BlurFloat has no ✕ in the heading).
const float closeW = 120.0f * dp;
material::BeginOverlayDialogFooter(closeW, false);
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
material::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx
+18
View File
@@ -0,0 +1,18 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
// hidden on builds without full-node lifecycle support.
void RenderFaqDialog(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx
+6 -5
View File
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive
} else {
s_error = r.ok ? std::string("Key not available for this address") : r.error;
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
}
wallet::secureWipeLiteSecret(found);
s_fetching = false;
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else {
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
const float gap = 12.0f;
const float dp = Layout::dpiScale();
const float gap = 12.0f * dp;
const float avail = ImGui::GetContentRegionAvail().x;
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
float qrSize = avail * 0.30f;
if (qrSize < 200.0f) qrSize = 200.0f;
if (qrSize > 340.0f) qrSize = 340.0f;
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
float keyW = avail - qrSize - gap;
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
if (!sideBySide) keyW = avail;
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
+3 -3
View File
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
} else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str());
}
}
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
: (tk == ticks - 1) ? plotRight - lblSz.x
: xpos - lblSz.x * 0.5f;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
}
}
+7 -2
View File
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
}
};
ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp;
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
// capFont+6px, shorter than the box, so they sat misaligned.
const float fieldH = ImGui::GetFrameHeight();
const float sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp;
const float g = 2.0f * dp;
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
s_benchConfirm = true;
char msg[128];
snprintf(msg, sizeof(msg),
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
TR("grpc_benchmark_takes_secs"),
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
Notifications::instance().warning(msg);
} else {
+2 -2
View File
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else
snprintf(estVal, sizeof(estVal), "N/A");
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else
snprintf(sysBuf, sizeof(sysBuf), "N/A");
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW;
+5 -4
View File
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
? it->second.feePercent
: kp.feePercent;
if (feePct >= 0.0)
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
FormatFeePercent(feePct).c_str());
else
snprintf(right, sizeof(right), "%s", hrStr.c_str());
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
// centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f;
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
const float chartBandX = rightMin.x + pad
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) {
+1 -2
View File
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
}
if (benchmarkUpdate.inconclusive) {
Notifications::instance().warning(
"Benchmark inconclusive: no hashrate samples were recorded. "
"Check the pool connection and try again.");
TR("grpc_benchmark_inconclusive"));
}
}
+4 -4
View File
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
}
{
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls"));
+11 -11
View File
@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(),
addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
} else {
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
}
ImGui::PushID(static_cast<int>(i));
@@ -272,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
s_cached_qr_data.clear(); // Force QR regeneration
}
if (ImGui::IsItemHovered()) {
material::Tooltip("%s\nBalance: %.8f %s%s",
material::Tooltip(TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? "\n(selected)" : "");
isCurrent ? TR("grpb_selected_suffix") : "");
}
ImGui::PopID();
}
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f;
float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss),
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; };
Preset presets[] = {
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), TR("qr_unavailable"));
+9 -6
View File
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
if (suggestions.empty()) return;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
const float dp = Layout::dpiScale();
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
for (int fi = 0; fi < 3; fi++) {
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
bool active = (s_fee_tier == fi);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
// Max button — use caption font to fit bar height
ImGui::SameLine(0, gap);
char maxId[32];
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
s_amount = maxAmount;
s_send_max = true;
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available,
const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const auto& state = app->getWalletState();
double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing();
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
ClearFormWithUndo();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
char undoId[32];
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot();
Notifications::instance().info(TR("send_form_restored"));
+12 -12
View File
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string label = skin.name + " (invalid)";
std::string label = skin.name + TR("swin_invalid_suffix");
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info("Theme list refreshed");
Notifications::instance().info(TR("swin_theme_list_refreshed"));
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version;
std::string msg = std::string(TR("swin_connection_successful")) + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Connection failed: " + error);
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) +
" to " + std::to_string(end);
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Rescan failed: " + error);
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else {
Notifications::instance().info("No history file found");
Notifications::instance().info(TR("swin_no_history_file_found"));
}
s_confirm_clear_ztx = false;
}
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false;
}
ImGui::SameLine();
@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::SameLine();
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.address.c_str());
}
}
+2 -2
View File
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
}
ImGui::Dummy(ImVec2(availWidth, cardH));
+1 -1
View File
@@ -100,7 +100,7 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin
const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
+381
View File
@@ -730,6 +730,7 @@ void I18n::loadBuiltinEnglish()
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
strings_["tt_keep_daemon"] = "Daemon will still stop when running the setup wizard";
strings_["tt_stop_external"] = "Applies when connecting to a daemon\nyou started outside this wallet";
strings_["stratum_host_section"] = "MINING POOL HOSTING";
strings_["stratum_host"] = "Host a mining pool (stratum)";
strings_["tt_stratum_host"] = "Run a RandomX stratum pool server on this node so other RandomX miners can point at this computer. Requires a v1.3.0+ node and a daemon restart to apply.";
strings_["stratum_host_hint"] = "Miners connect to this computer on port 22769 (RPC port + 1000) with a RandomX stratum miner. Restart the daemon to apply.";
@@ -1540,6 +1541,175 @@ void I18n::loadBuiltinEnglish()
strings_["about_version"] = "Version:";
strings_["about_website"] = "Website";
// --- Help & FAQ ---
strings_["faq"] = "FAQ";
strings_["faq_title"] = "Help & FAQ";
strings_["faq_intro"] = "Answers about your wallet and the DragonX node. Search, or browse by topic below.";
strings_["faq_search_hint"] = "Search the FAQ\xE2\x80\xA6";
strings_["faq_open_tooltip"] = "Help & FAQ";
strings_["faq_no_results"] = "No results. Try a different search term.";
strings_["faq_group_wallet"] = "Wallet";
strings_["faq_group_daemon"] = "Daemon";
// Wallet subcategory titles
strings_["faq_w_gs_title"] = "Getting Started";
strings_["faq_w_addr_title"] = "Addresses & Privacy";
strings_["faq_w_send_title"] = "Sending & Receiving";
strings_["faq_w_bal_title"] = "Balance & Sync";
strings_["faq_w_sec_title"] = "Security & Encryption";
strings_["faq_w_seed_title"] = "Seed Phrase & Backup";
strings_["faq_w_chat_title"] = "Chat & Contacts";
strings_["faq_w_set_title"] = "Settings & Appearance";
// Daemon subcategory titles
strings_["faq_d_node_title"] = "The Full Node";
strings_["faq_d_sync_title"] = "Sync & Blockchain";
strings_["faq_d_mgmt_title"] = "Node Management";
strings_["faq_d_upd_title"] = "Updating the Node";
strings_["faq_d_mine_title"] = "Mining & Pools";
strings_["faq_d_net_title"] = "Peers & Network";
strings_["faq_d_perf_title"] = "Storage & Performance";
strings_["faq_d_trbl_title"] = "Troubleshooting";
// Wallet > Getting Started
strings_["faq_w_gs_1_q"] = "What is ObsidianDragon?";
strings_["faq_w_gs_1_a"] = "ObsidianDragon is a full-node wallet for DragonX (DRGX). It manages your coins and, on the desktop build, runs a DragonX full node (the daemon) in the background so your wallet verifies the blockchain itself instead of trusting a third-party server.\n\nThink of it as two parts working together: the wallet (keys, balances, sending) and the node (the blockchain and network). The questions here are split the same way \xE2\x80\x94 see the Wallet and Daemon tabs above.";
strings_["faq_w_gs_2_q"] = "How do I create a new wallet?";
strings_["faq_w_gs_2_a"] = "On first launch the setup wizard creates a wallet for you automatically. New wallets are backed by a secret recovery phrase (a list of words). Write that phrase down and keep it offline \xE2\x80\x94 it is the only way to restore your funds if this computer is lost.\n\nYou can review or back up the phrase any time from Settings > Backup & Data > Seed phrase.";
strings_["faq_w_gs_3_q"] = "How do I restore a wallet I already have?";
strings_["faq_w_gs_3_a"] = "Use the seed-phrase restore flow if you have a recovery phrase, or copy an existing wallet.dat file into the wallet's data directory before launch.\n\nAfter restoring, the node re-scans the blockchain to find your past transactions, so your balance and history may take a while to appear the first time.";
strings_["faq_w_gs_4_q"] = "What does the first-run setup do?";
strings_["faq_w_gs_4_a"] = "The wizard lets you pick an appearance/theme, optionally download a bootstrap to speed up the initial blockchain sync, and set up encryption and a PIN. You can skip any step and change all of these later in Settings.";
// Wallet > Addresses & Privacy
strings_["faq_w_addr_1_q"] = "What's the difference between transparent and shielded addresses?";
strings_["faq_w_addr_1_a"] = "Transparent addresses (they start with R or t) work like most coins: the amounts and addresses are public on the blockchain.\n\nShielded addresses (z-addresses, starting with zs) use zero-knowledge cryptography \xE2\x80\x94 the amount, sender, and receiver are encrypted on-chain. Balances held in shielded addresses are private.";
strings_["faq_w_addr_2_q"] = "Which address type should I use?";
strings_["faq_w_addr_2_a"] = "Prefer shielded (z) addresses whenever possible \xE2\x80\x94 they keep your balance and payment history private. DragonX is a privacy coin and shielded is the default for received funds.\n\nUse a transparent address only when a service you interact with cannot handle shielded addresses.";
strings_["faq_w_addr_3_q"] = "Should I reuse an address or make a new one?";
strings_["faq_w_addr_3_a"] = "For shielded addresses, reuse is fine and does not leak history. For transparent addresses, using a fresh address per payment improves privacy. Create new addresses from the Receive tab.";
strings_["faq_w_addr_4_q"] = "What is 'shielding'?";
strings_["faq_w_addr_4_a"] = "Shielding moves coins from a transparent address into a shielded one, making them private. Newly mined coins arrive transparent and are shielded for you automatically.\n\nYou can also shield manually from the wallet; a shield is just a special transaction, so it takes a normal confirmation time to complete.";
// Wallet > Sending & Receiving
strings_["faq_w_send_1_q"] = "How do I send funds?";
strings_["faq_w_send_1_a"] = "Open the Send tab, paste the recipient's address, enter an amount, and confirm. If the wallet is encrypted you'll be asked to unlock it for the transaction.\n\nSends to shielded addresses are private; sends to transparent addresses are public.";
strings_["faq_w_send_2_q"] = "What fee do I pay?";
strings_["faq_w_send_2_a"] = "DragonX fees are very low and are set automatically. The fee is shown before you confirm. You do not normally need to change it.";
strings_["faq_w_send_3_q"] = "Can I attach a message to a payment?";
strings_["faq_w_send_3_a"] = "Yes \xE2\x80\x94 when sending to a shielded (z) address you can include an encrypted memo. Only the recipient can read it. Transparent addresses do not support memos.";
strings_["faq_w_send_4_q"] = "I sent a payment but it says pending \xE2\x80\x94 why?";
strings_["faq_w_send_4_a"] = "A transaction is 'pending' until it is mined into a block and gains confirmations. This usually takes a minute or two. Shielded transactions also need the node to be synced to build the proof.\n\nIf a send stays pending unusually long, check that the node is connected and synced (see the status bar at the bottom of the window).";
// Wallet > Balance & Sync
strings_["faq_w_bal_1_q"] = "Why is my balance 0 or not updating?";
strings_["faq_w_bal_1_a"] = "While the node is still syncing the blockchain, your balance is incomplete and may read 0 \xE2\x80\x94 the wallet hasn't scanned all of your transactions yet. It fills in once syncing finishes.\n\nWatch the status bar at the bottom: it shows the connection state and block height. Once the node is synced, your balance is accurate.";
strings_["faq_w_bal_2_q"] = "What are confirmations?";
strings_["faq_w_bal_2_a"] = "Each new block mined on top of the block containing your transaction adds one confirmation. More confirmations mean the payment is more firmly settled. Received funds become spendable after the first confirmation.";
strings_["faq_w_bal_3_q"] = "What's the difference between total and spendable balance?";
strings_["faq_w_bal_3_a"] = "Total includes funds that are still confirming or are temporarily locked (for example, coins in the middle of being shielded). Spendable is what you can send right now. They converge as transactions confirm.";
// Wallet > Security & Encryption
strings_["faq_w_sec_1_q"] = "How do I encrypt my wallet?";
strings_["faq_w_sec_1_a"] = "Open Settings > Node & Security and set a passphrase (the SECURITY section). Encryption protects your private keys on disk, so someone with access to the files still cannot spend your coins.\n\nChoose a strong passphrase and don't lose it \xE2\x80\x94 there is no way to recover an encrypted wallet without it.";
strings_["faq_w_sec_2_q"] = "What is the PIN / lock screen?";
strings_["faq_w_sec_2_a"] = "The PIN locks the app's screen so balances and actions are hidden when you step away. It's a convenience lock on top of encryption; you set it up during the wizard or in Settings > Node & Security (encrypt the wallet first).";
strings_["faq_w_sec_3_q"] = "I forgot my passphrase \xE2\x80\x94 can it be recovered?";
strings_["faq_w_sec_3_a"] = "No. Wallet encryption cannot be bypassed. If you still have your seed recovery phrase, you can restore the wallet from it into a fresh wallet and set a new passphrase. Without either the passphrase or the seed phrase, the funds cannot be recovered.";
// Wallet > Seed Phrase & Backup
strings_["faq_w_seed_1_q"] = "How do I back up my recovery phrase?";
strings_["faq_w_seed_1_a"] = "Go to Settings > Backup & Data > Seed phrase. Write the words down on paper in order and store them somewhere safe and offline. Anyone with the phrase can spend your coins, so never store it in a photo, email, or cloud note.";
strings_["faq_w_seed_2_q"] = "My wallet has no recovery phrase \xE2\x80\x94 can I add one?";
strings_["faq_w_seed_2_a"] = "Older (legacy) wallets weren't seed-based. The wallet can migrate a legacy wallet into a modern seed-backed one: it creates a new seed wallet and sweeps your funds into it. Look for 'Migrate to seed\xE2\x80\xA6' in Settings > Backup & Data. This feature needs the current daemon version.";
strings_["faq_w_seed_3_q"] = "How do I restore from my recovery phrase?";
strings_["faq_w_seed_3_a"] = "Use the seed-restore flow when setting up a wallet and enter your words in order. The node then re-scans the chain to rebuild your balance and history, which can take a while the first time.";
strings_["faq_w_seed_4_q"] = "Should I also back up wallet.dat?";
strings_["faq_w_seed_4_a"] = "Your seed phrase is the primary backup and is enough to restore everything. A copy of the wallet.dat file is a convenient secondary backup that also preserves labels and settings. Keep any backup offline and private.";
// Wallet > Chat & Contacts
strings_["faq_w_chat_1_q"] = "What is the Chat feature?";
strings_["faq_w_chat_1_a"] = "Chat is an encrypted, on-chain messenger built into the wallet. Messages are sent as private shielded memos, so only you and your contact can read them.";
strings_["faq_w_chat_2_q"] = "How does my chat identity work?";
strings_["faq_w_chat_2_a"] = "Your chat identity is derived from your wallet's recovery phrase, so it travels with your wallet \xE2\x80\x94 restore the wallet and your identity comes back. You don't create a separate account or password.";
strings_["faq_w_chat_3_q"] = "How do contacts work?";
strings_["faq_w_chat_3_a"] = "Add a contact by their address in the Contacts tab, optionally with a name and avatar. Contacts can be kept private to the current wallet or shared across all wallets you open on this computer.";
// Wallet > Settings & Appearance
strings_["faq_w_set_1_q"] = "How do I change the theme?";
strings_["faq_w_set_1_a"] = "Open Settings > Appearance to pick a theme and accent. Several light and dark skins are included, and effects like blur can be toggled off for lower-powered machines.";
strings_["faq_w_set_2_q"] = "Can I change the language?";
strings_["faq_w_set_2_a"] = "Yes \xE2\x80\x94 Settings > Appearance has a language selector. The wallet ships with several translations; anything not yet translated falls back to English.";
strings_["faq_w_set_3_q"] = "The text is too small (or too large) \xE2\x80\x94 can I scale it?";
strings_["faq_w_set_3_a"] = "Use the font-scale option in Settings > Appearance. It scales the whole interface, which is handy on high-resolution (HiDPI) displays where the app might otherwise render small.";
strings_["faq_w_set_4_q"] = "How do I show fiat prices?";
strings_["faq_w_set_4_a"] = "Enable price fetching in Settings. The wallet then shows an approximate fiat value alongside balances and can display a small market chart. Prices are informational only.";
// Daemon > The Full Node
strings_["faq_d_node_1_q"] = "What is the daemon (dragonxd)?";
strings_["faq_d_node_1_a"] = "The daemon, dragonxd, is the DragonX full node. It downloads and verifies the entire blockchain, relays transactions to the network, and answers the wallet's queries. Running your own node means you don't have to trust anyone else's server.";
strings_["faq_d_node_2_q"] = "Embedded vs external node \xE2\x80\x94 what's the difference?";
strings_["faq_d_node_2_a"] = "By default the wallet launches and manages its own bundled node (embedded) \xE2\x80\x94 you don't have to do anything. If you already run dragonxd yourself, the wallet can connect to that external node instead. The wallet detects which situation it's in and behaves accordingly.";
strings_["faq_d_node_3_q"] = "Why run a full node at all?";
strings_["faq_d_node_3_a"] = "A full node validates every rule of the blockchain independently, so your wallet trusts math instead of a third party. It also strengthens the network. The trade-off is disk space and an initial sync.";
// Daemon > Sync & Blockchain
strings_["faq_d_sync_1_q"] = "What does 'Processing blocks' at startup mean?";
strings_["faq_d_sync_1_a"] = "On launch the node loads and validates blocks before it can serve the wallet. The 'Processing blocks / Applying blocks to build the current chain state' screen is that warm-up. It's normal; the wallet becomes usable once the node finishes and reports its height.";
strings_["faq_d_sync_2_q"] = "What is 'Rebuilding witness cache'?";
strings_["faq_d_sync_2_a"] = "For your shielded funds the node keeps cryptographic 'witnesses' that let you spend privately. After certain restarts or updates it rebuilds this cache by re-scanning recent blocks. On a wallet with many transactions this can take several minutes and use extra memory.\n\nIt is not stuck \xE2\x80\x94 let it finish. Force-quitting in the middle just makes it restart the rebuild next time.";
strings_["faq_d_sync_3_q"] = "Why is syncing slow when I'm almost caught up?";
strings_["faq_d_sync_3_a"] = "Near the chain tip, work that scans your wallet (checking balances and notes) competes with the node for the same internal lock, which can slow down connecting the final blocks on a large wallet. Recent versions throttle that scanning so the node can catch up. Staying on a lighter tab (or just waiting) lets it finish faster.";
strings_["faq_d_sync_4_q"] = "How long does the initial sync take?";
strings_["faq_d_sync_4_a"] = "The first sync downloads and verifies the whole chain and can take a while depending on your connection and disk. You can speed it up dramatically by enabling the bootstrap download in the setup wizard, which fetches a recent verified copy of the chain data.";
// Daemon > Node Management
strings_["faq_d_mgmt_1_q"] = "How do I start or stop the node?";
strings_["faq_d_mgmt_1_a"] = "The embedded node starts automatically with the wallet and stops when appropriate, so you normally don't manage it by hand. Advanced controls (including restarting the daemon) live in Settings > Node & Security.";
strings_["faq_d_mgmt_2_q"] = "What does 'close external daemon on exit' do?";
strings_["faq_d_mgmt_2_a"] = "If you connect the wallet to a node you started yourself, this option decides whether closing the wallet also shuts that node down. Leave it off if you want your node to keep running after you close the wallet.";
strings_["faq_d_mgmt_3_q"] = "Where is the blockchain and wallet data stored?";
strings_["faq_d_mgmt_3_a"] = "Node and wallet data live in the DragonX data directory under your user profile (for example, in AppData on Windows or your home folder on Linux/macOS). The blockchain is the large part; keep enough free disk space for it to grow.";
// Daemon > Updating the Node
strings_["faq_d_upd_1_q"] = "How do I update the node?";
strings_["faq_d_upd_1_a"] = "Open Settings > Node & Security > Daemon binary and use 'Check for updates\xE2\x80\xA6'. The wallet downloads the latest verified node build and installs it; the new version takes effect the next time the daemon starts.";
strings_["faq_d_upd_2_q"] = "Can I install a specific node version?";
strings_["faq_d_upd_2_a"] = "Yes \xE2\x80\x94 the update dialog lists every release so you can pick a specific or older build. Be cautious downgrading: an older node may not accept blockchain data written by a newer one and could need a re-index.";
strings_["faq_d_upd_3_q"] = "Is the update safe?";
strings_["faq_d_upd_3_a"] = "Every downloaded node archive is verified against a checksum and a cryptographic signature before it's installed. If verification fails, the wallet refuses the update.";
// Daemon > Mining & Pools
strings_["faq_d_mine_1_q"] = "How do I mine DragonX?";
strings_["faq_d_mine_1_a"] = "The Mining tab lets you mine with your CPU. DragonX uses the RandomX algorithm, which is designed for regular processors. Set the number of threads and start \xE2\x80\x94 rewards accrue to your wallet.";
strings_["faq_d_mine_2_q"] = "Solo vs pool mining \xE2\x80\x94 which should I choose?";
strings_["faq_d_mine_2_a"] = "Solo mining sends rewards straight to you but pays only when you find a block, which is infrequent unless you have a lot of hashrate. Pool mining shares work with others for smaller, steadier payouts. The Mining tab supports both.";
strings_["faq_d_mine_3_q"] = "How do I update the miner?";
strings_["faq_d_mine_3_a"] = "The Mining tab's pool section has an 'Update miner\xE2\x80\xA6' button that downloads, verifies, and installs the latest optimized DragonX miner build. Like the node updater, each download is checksum- and signature-verified before install.";
strings_["faq_d_mine_4_q"] = "Can I host a mining pool?";
strings_["faq_d_mine_4_a"] = "The current node can run a built-in stratum server so other miners can point at your machine. There's a toggle for it under Settings > Node & Security. By default it only listens locally; exposing it to other computers requires opening it up deliberately.";
// Daemon > Peers & Network
strings_["faq_d_net_1_q"] = "The wallet shows no peers or connections \xE2\x80\x94 what's wrong?";
strings_["faq_d_net_1_a"] = "Right after launch the node needs a moment to find peers, so a brief '0 peers' is normal. If it persists, check your internet connection and that a firewall isn't blocking the node. The node finds peers automatically through built-in seeds.";
strings_["faq_d_net_2_q"] = "Is my connection to the network encrypted?";
strings_["faq_d_net_2_a"] = "Yes \xE2\x80\x94 DragonX nodes talk to each other over TLS, so peer connections are encrypted. The wallet also talks to its own node over a secure local channel.";
// Daemon > Storage & Performance
strings_["faq_d_perf_1_q"] = "How much disk does the node use, and can I tune memory?";
strings_["faq_d_perf_1_a"] = "The blockchain is the large item and grows over time, so keep several gigabytes free. The node uses a database cache for speed; the bundled defaults are tuned for typical machines. Advanced users can adjust the node's database cache by editing dbcache in its config file (DRAGONX.conf).";
strings_["faq_d_perf_2_q"] = "How do I make the initial sync faster?";
strings_["faq_d_perf_2_a"] = "Enable the bootstrap download (offered in the setup wizard) to fetch a recent, verified copy of the chain instead of validating every block from scratch. A fast disk (SSD) also helps a lot.";
// Daemon > Troubleshooting
strings_["faq_d_trbl_1_q"] = "The node seems stuck on 'Activating best chain' \xE2\x80\x94 is it frozen?";
strings_["faq_d_trbl_1_a"] = "Usually not. On a large wallet the node can spend several minutes rebuilding shielded witnesses or applying blocks, during which it looks paused and may be slow to answer. Give it time \xE2\x80\x94 the debug log (Settings) shows steady progress if it's working.\n\nAvoid force-quitting during this phase; it restarts the work from scratch next launch.";
strings_["faq_d_trbl_2_q"] = "What is 'degraded mode'?";
strings_["faq_d_trbl_2_a"] = "If a wallet file is recovered but part of its key data is missing, the node may open it in a limited 'degraded' mode where it can't derive new addresses or shield funds. The safest fix is to restore from your seed recovery phrase into a fresh wallet.";
strings_["faq_d_trbl_3_q"] = "When should I re-index or re-scan?";
strings_["faq_d_trbl_3_a"] = "A re-scan makes the wallet re-read the chain to rediscover your transactions (useful after importing keys). A re-index rebuilds the node's blockchain database and is only needed if that database is corrupted or incompatible after a version change. Both can take a while; start them from Settings > Node & Security.";
strings_["faq_d_trbl_4_q"] = "The node is using a lot of memory \xE2\x80\x94 is that normal?";
strings_["faq_d_trbl_4_a"] = "Memory use spikes during heavy work like a witness rebuild or an initial sync, then drops back down once it finishes. Sustained high memory at idle is unusual \xE2\x80\x94 restarting the wallet clears it. On a low-RAM machine, make sure other memory-heavy apps aren't competing.";
// --- Address Book Dialog ---
strings_["address_book_add"] = "Add Address";
strings_["address_book_add_new"] = "Add New";
@@ -2354,6 +2524,217 @@ void I18n::loadBuiltinEnglish()
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
strings_["explorer_no_results"] = "No matching cached blocks";
// ---- i18n audit 2026-09: wrap previously-hardcoded UI strings ----
// app_security.cpp — encryption / PIN / lock flow
strings_["sec_restart_daemon_for_encryption"] = "Please restart your daemon for encryption to take effect.";
strings_["sec_encrypting_wallet"] = "Encrypting wallet...";
strings_["sec_wallet_encrypted_restarting_daemon"] = "Wallet encrypted. Restarting daemon...";
strings_["sec_wallet_encrypted_successfully"] = "Wallet encrypted successfully";
strings_["sec_encryption_failed_prefix"] = "Encryption failed: ";
strings_["sec_wallet_encrypted_and_pin_set"] = "Wallet encrypted & PIN set";
strings_["sec_wallet_encrypted_but_pin_vault_failed"] = "Wallet encrypted but PIN vault failed";
strings_["sec_couldnt_lock_wallet"] = "Couldn't lock the wallet — it is still unlocked. Check the daemon connection.";
strings_["sec_changing_passphrase"] = "Changing passphrase...";
strings_["sec_passphrase_changed_successfully"] = "Passphrase changed successfully";
strings_["sec_failed_prefix"] = "Failed: ";
strings_["sec_encryption_did_not_complete"] = "Wallet encryption did not complete — your wallet is NOT encrypted. Open Settings to finish encrypting it.";
strings_["sec_wallet_locked_title"] = "Wallet Locked";
strings_["sec_too_many_attempts_wait"] = "Too many attempts. Wait %.0f seconds...";
strings_["sec_mode_passphrase"] = " Passphrase";
strings_["sec_use_passphrase_instead"] = "Use passphrase instead";
strings_["sec_use_pin_instead"] = "Use PIN instead";
strings_["sec_unlocking_fmt"] = "Unlocking%s";
strings_["sec_unlock_button"] = "Unlock";
strings_["sec_not_connected_to_daemon"] = "Not connected to daemon";
strings_["sec_unlock_failed_prefix"] = "Unlock failed: ";
strings_["sec_incorrect_passphrase_decrypt"] = "Incorrect passphrase";
strings_["sec_importing_keys_rescanning"] = "Importing keys & rescanning blockchain — wallet is usable while this runs";
strings_["sec_encrypted_backup_suffix"] = "\nEncrypted backup: wallet.dat.encrypted.bak";
strings_["sec_wallet_decrypted_all_keys_imported"] = "Wallet decrypted successfully! All keys imported.";
strings_["sec_total_elapsed_fmt"] = "Total elapsed: %dm %02ds";
strings_["sec_verifying_passphrase"] = "Verifying passphrase...";
strings_["sec_incorrect_passphrase_pin_setup"] = "Incorrect passphrase";
strings_["sec_pin_set_successfully"] = "PIN set successfully";
strings_["sec_failed_to_create_vault"] = "Failed to create vault";
strings_["sec_not_connected_to_daemon_pin"] = "Not connected to daemon";
strings_["sec_changing_pin"] = "Changing PIN...";
strings_["sec_pin_changed_successfully"] = "PIN changed successfully";
strings_["sec_incorrect_current_pin"] = "Incorrect current PIN";
strings_["sec_internal_error_change_pin"] = "Internal error";
strings_["sec_verifying_pin"] = "Verifying PIN...";
strings_["sec_pin_removed"] = "PIN removed";
strings_["sec_incorrect_pin_remove"] = "Incorrect PIN";
strings_["sec_internal_error_remove_pin"] = "Internal error";
// app.cpp — wizard, seed backup, shutdown, daemon/miner lifecycle
strings_["appx_creating_your_wallet"] = "Creating your wallet…";
strings_["appx_create_failed_prefix"] = "Create failed: ";
strings_["appx_back_up_seed_phrase_title"] = "Back up your seed phrase";
strings_["appx_seed_backup_warning"] = "These 24 words are the ONLY way to restore your wallet. Write them down in order, store them offline, and never share them. If you lose them, your funds are gone forever.";
strings_["appx_birthday_block_height"] = "Birthday (block height): %llu — back this up too.";
strings_["appx_ive_written_it_down"] = "I've written it down";
strings_["appx_copy"] = "Copy";
strings_["appx_skip_anyway"] = "Skip anyway";
strings_["appx_skip"] = "Skip";
strings_["appx_seed_not_backed_up_warning"] = "You have not backed up your seed — funds could be lost. Skip anyway?";
strings_["appx_confirm_your_backup"] = "Confirm your backup";
strings_["appx_tap_words_in_order"] = "Tap the words in the correct order to confirm you saved them.";
strings_["appx_progress_n_of_n"] = "Progress: %d / %d";
strings_["appx_not_next_word"] = " — that's not the next word";
strings_["appx_done"] = "Done";
strings_["appx_wallet_created_and_backed_up"] = "Wallet created and backed up.";
strings_["appx_back"] = "Back";
strings_["appx_restoring_your_wallet"] = "Restoring your wallet…";
strings_["appx_recovery_phrase_word_count"] = "Recovery phrase should be 24 words — you have %d.";
strings_["appx_could_not_start_restore"] = "Could not start restore";
strings_["appx_node_rebuilding_witness_cache"] = "Node is rebuilding its witness cache";
strings_["appx_stopping_node_discards_rebuild"] = "Stopping the node now discards the in-progress rebuild and restarts it (several minutes) the next time you open the wallet. You can keep the node running instead.";
strings_["appx_keep_node_running_and_quit"] = "Keep node running & quit";
strings_["appx_stop_anyway_and_quit"] = "Stop anyway & quit";
strings_["appx_cancel"] = "Cancel";
strings_["appx_last_used_wallet_not_found_prefix"] = "Your last-used wallet file (";
strings_["appx_last_used_wallet_not_found_suffix"] = ") was not found — opened the default wallet instead. If you moved it, restore it and switch back from the wallet list.";
strings_["appx_wallet_open_failed_prefix"] = "Wallet open failed: ";
strings_["appx_blockchain_rescan_complete"] = "Blockchain rescan complete";
strings_["appx_miner_stopped_unexpectedly"] = "Miner stopped unexpectedly.";
strings_["appx_miner_stopped_prefix"] = "Miner stopped: ";
strings_["appx_pool_miner_connected_and_hashing"] = "Pool miner connected and hashing.";
strings_["appx_bootstrap_complete_reconciling"] = "Bootstrap complete — reconciling your wallet with the new chain data.";
strings_["appx_blockchain_data_deleted"] = "Blockchain data deleted (%d items). The daemon is restarting to re-sync from the network.";
strings_["appx_invalid_payment_uri_prefix"] = "Invalid payment URI: ";
strings_["appx_payment_request_loaded"] = "Payment request loaded";
strings_["appx_fullnode_lifecycle_unavailable_lite"] = "Full-node lifecycle actions are unavailable in lite build";
strings_["appx_blockchain_maintenance_in_progress"] = "A blockchain maintenance operation is already in progress.";
strings_["appx_node_busy_restarting"] = "The node is busy restarting — try again in a moment.";
strings_["appx_restarting_daemon_rescan_flag"] = "Restarting daemon with -rescan flag...";
strings_["appx_restarting_daemon_zapwallettxes"] = "Restarting daemon with -zapwallettxes=2 (wallet repair)...";
strings_["appx_no_bundled_daemon_to_install"] = "This build has no bundled daemon to install";
strings_["appx_no_embedded_daemon_to_install"] = "This build has no embedded daemon to install";
strings_["appx_daemon_reinstall_in_progress"] = "The daemon reinstall is already in progress.";
strings_["appx_installing_bundled_daemon"] = "Installing bundled daemon — the node will stop, update, and restart...";
strings_["appx_stopping_daemon_deleting_blockchain"] = "Stopping daemon and deleting blockchain data...";
strings_["appx_stopping_pool_miner"] = "Stopping pool miner...";
strings_["appx_disconnecting"] = "Disconnecting...";
strings_["appx_sending_stop_command_to_daemon"] = "Sending stop command to daemon...";
strings_["appx_cleaning_up"] = "Cleaning up...";
strings_["appx_shutdown_complete"] = "Shutdown complete";
strings_["appx_n_seconds"] = "%d seconds";
strings_["appx_n_min_n_sec"] = "%d min %d sec";
strings_["appx_still_status_prefix"] = "Still \"";
strings_["appx_still_status_suffix"] = "\" — force quitting now may corrupt chain data.";
strings_["appx_rebuilding_witness_cache_blocks_left"] = "Rebuilding witness cache %.0f%% — %d blocks left";
strings_["appx_rebuilding_witness_cache_pct"] = "Rebuilding witness cache %.0f%%";
strings_["appx_setting_initial_sapling_witnesses"] = "Setting initial Sapling witnesses %.0f%%";
strings_["appx_rebuilding_sapling_note_witnesses"] = "Rebuilding Sapling note witnesses…";
strings_["appx_syncing_pct_block_n_of_n"] = "Syncing %.1f%% — Block %d / %d";
strings_["appx_last_block_n"] = "Last block: %d";
strings_["appx_encrypting_wallet"] = "Encrypting wallet...";
strings_["appx_waiting_for_daemon_to_encrypt_wallet"] = "Waiting for daemon to encrypt wallet...";
strings_["appx_daemon_error"] = "Daemon Error";
strings_["appx_use_settings_restart_daemon_hint"] = "Use Settings > Restart Daemon to try again";
strings_["appx_copied_clipboard_autoclears"] = "Copied — clipboard auto-clears in 45s";
strings_["appx_dragonxd_output"] = "dragonxd output";
strings_["appx_theme_prefix"] = "Theme: ";
strings_["appx_low_spec_mode_enabled"] = "Low-spec mode enabled";
strings_["appx_low_spec_mode_disabled"] = "Low-spec mode disabled";
strings_["appx_theme_effects_enabled"] = "Theme effects enabled";
strings_["appx_theme_effects_disabled"] = "Theme effects disabled";
strings_["appx_simple_background_enabled"] = "Simple background enabled";
strings_["appx_simple_background_disabled"] = "Simple background disabled";
// balance_tab.cpp
strings_["baltab_shielded_amount"] = "Shielded %.8f";
strings_["baltab_transparent_amount"] = "Transparent %.8f";
strings_["baltab_market_price_4dp"] = "Market: $%.4f";
strings_["baltab_market_price_8dp"] = "Market: $%.8f";
strings_["baltab_pct_of_total_zaddr"] = "%.0f%% of total · %d Z-addr";
strings_["baltab_t_addresses_count"] = "%d T-addresses";
strings_["baltab_pct_change_24h"] = "%s%.1f%% 24h";
strings_["baltab_total_balance"] = "Total Balance";
strings_["baltab_shielded"] = "Shielded";
strings_["baltab_transparent"] = "Transparent";
strings_["baltab_market"] = "Market";
// settings_window.cpp
strings_["swin_invalid_suffix"] = " (invalid)";
strings_["swin_theme_list_refreshed"] = "Theme list refreshed";
strings_["swin_connection_successful"] = "Connection successful!\ndragonxd version: ";
strings_["swin_connection_failed"] = "Connection failed: ";
strings_["swin_rpc_client_not_initialized"] = "RPC client not initialized";
strings_["swin_rescan_started_from_block"] = "Rescan started from block ";
strings_["swin_rescan_to"] = " to ";
strings_["swin_rescan_failed"] = "Rescan failed: ";
strings_["swin_ztx_history_cleared"] = "Z-transaction history cleared";
strings_["swin_no_history_file_found"] = "No history file found";
strings_["swin_settings_saved"] = "Settings saved";
// settings_page.cpp / explorer_tab.cpp / block_info_dialog.cpp
strings_["grpa_tab_appearance"] = "Appearance";
strings_["grpa_tab_wallet"] = "Wallet";
strings_["grpa_tab_backup_data"] = "Backup & Data";
strings_["grpa_tab_node_security"] = "Node & Security";
strings_["grpa_tab_explorer"] = "Explorer";
strings_["grpa_tab_chat"] = "Chat";
strings_["grpa_tab_about"] = "About";
strings_["grpa_enter_private_key_to_import"] = "Enter a private key to import.";
strings_["grpa_invalid_suffix"] = " (invalid)";
strings_["grpa_seed_demo_chat"] = "Seed demo chat";
strings_["grpa_dbg_addrman"] = "Peer address tracking and management";
strings_["grpa_dbg_alert"] = "Alert system messages";
strings_["grpa_dbg_bench"] = "Benchmark timings for operations";
strings_["grpa_dbg_coindb"] = "Coin database read/write operations";
strings_["grpa_dbg_db"] = "Berkeley DB operations";
strings_["grpa_dbg_estimatefee"] = "Fee estimation algorithm";
strings_["grpa_dbg_http"] = "HTTP RPC server activity";
strings_["grpa_dbg_libevent"] = "Libevent networking library";
strings_["grpa_dbg_lock"] = "Lock contention debugging";
strings_["grpa_dbg_mempool"] = "Transaction memory pool activity";
strings_["grpa_dbg_net"] = "Network connections and messages";
strings_["grpa_dbg_paymentdisclosure"] = "Payment disclosure protocol";
strings_["grpa_dbg_pow"] = "Proof-of-work mining activity";
strings_["grpa_dbg_proxy"] = "SOCKS5 proxy connections";
strings_["grpa_dbg_prune"] = "Block pruning operations";
strings_["grpa_dbg_rand"] = "Random number generation";
strings_["grpa_dbg_reindex"] = "Blockchain reindexing progress";
strings_["grpa_dbg_rpc"] = "RPC command processing";
strings_["grpa_dbg_selectcoins"] = "Coin selection for transactions";
strings_["grpa_dbg_tor"] = "Tor integration and circuit info";
strings_["grpa_dbg_zmq"] = "ZeroMQ notification system";
strings_["grpa_dbg_zrpc"] = "Shielded (z-addr) RPC operations";
strings_["grpa_sec_ago"] = "%lld sec ago";
strings_["grpa_min_ago"] = "%lld min ago";
strings_["grpa_hr_ago"] = "%lld hr ago";
strings_["grpa_days_ago"] = "%lld days ago";
strings_["grpa_showing_first_100_of"] = "... showing first 100 of %d";
strings_["grpa_error_prefix"] = "Error: ";
strings_["grpa_invalid_response_from_daemon"] = "Invalid response from daemon";
strings_["grpa_current_block_paren"] = "(Current: %d)";
strings_["grpa_unexpected_getblockhash_result"] = "unexpected getblockhash result";
// receive / chat / send / transaction_details
strings_["grpb_new_badge_suffix"] = " [NEW]";
strings_["grpb_tooltip_address_balance"] = "%s\nBalance: %.8f %s%s";
strings_["grpb_selected_suffix"] = "\n(selected)";
strings_["grpb_preview_msg_payment_through"] = "Did the payment go through? 🙂";
strings_["grpb_preview_msg_yep_confirmed"] = "Yep — just confirmed ✅";
strings_["grpb_preview_msg_sending_rest"] = "Sending the rest now 👍";
strings_["grpb_max"] = "Max";
strings_["grpb_undo_clear"] = "Undo Clear";
strings_["grpb_copy"] = "Copy";
// bootstrap / mining_* / key_export
strings_["grpc_bootstrap_not_initialized"] = "Bootstrap not initialized";
strings_["grpc_bootstrap_failed"] = "Bootstrap failed";
strings_["grpc_na"] = "N/A";
strings_["grpc_key_not_available"] = "Key not available for this address";
strings_["grpc_benchmark_takes_secs"] = "Benchmark takes ~%ds and interrupts mining. Click again to start.";
strings_["grpc_hashrate_fee"] = "%s %s%% fee";
strings_["grpc_benchmark_inconclusive"] = "Benchmark inconclusive: no hashrate samples were recorded. Check the pool connection and try again.";
// misc pre-existing missing keys
strings_["copied_to_clipboard"] = "Copied to clipboard";
}
const char* I18n::translate(const char* key) const
+28 -8
View File
@@ -1417,31 +1417,51 @@ void testNetworkRefreshRpcCollectors()
});
coreRpc.addResponse("getblockchaininfo", json{
{"blocks", 150},
{"headers", 155},
{"headers", 150},
{"bestblockhash", "core-best-150"},
{"verificationprogress", 0.80},
{"longestchain", 160},
{"verificationprogress", 1.0},
{"longestchain", 150}, // caught up → balance scan runs
{"notarized", 145}
});
auto core = Refresh::collectCoreRefreshResult(coreRpc);
// getblockchaininfo is issued FIRST — sync detection gates (and must not be delayed behind) the
// O(mapWallet) balance scan. When caught up, the balance scan follows: display (minconf=0) +
// spendable (minconf=1).
EXPECT_TRUE(coreRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo" // display (minconf=0) + spendable (minconf=1)
"getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
}));
EXPECT_EQ(coreRpc.calls[0].params, json::array({0}));
EXPECT_EQ(coreRpc.calls[1].params, json::array({1}));
EXPECT_EQ(coreRpc.calls[1].params, json::array({0}));
EXPECT_EQ(coreRpc.calls[2].params, json::array({1}));
EXPECT_TRUE(core.balanceOk);
EXPECT_TRUE(core.blockchainOk);
EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001);
EXPECT_EQ(*core.blocks, 150);
EXPECT_EQ(*core.bestBlockHash, std::string("core-best-150"));
EXPECT_EQ(*core.longestChain, 160);
EXPECT_EQ(*core.longestChain, 150);
// When getblockchaininfo shows the node is behind (blocks < longestchain - 2), the balance scan is
// skipped this cycle regardless of includeBalance — so sync-state (and hence kSyncProfile) updates
// promptly instead of after the multi-second scan. Only getblockchaininfo is issued.
MockRefreshRpc coreBehindRpc;
coreBehindRpc.addResponse("z_gettotalbalance", json{{"total", "4.25000000"}});
coreBehindRpc.addResponse("getblockchaininfo", json{
{"blocks", 150}, {"headers", 160}, {"longestchain", 160}
});
auto coreBehind = Refresh::collectCoreRefreshResult(coreBehindRpc);
EXPECT_TRUE(coreBehindRpc.methodNames() == std::vector<std::string>({"getblockchaininfo"}));
EXPECT_FALSE(coreBehind.balanceOk);
EXPECT_TRUE(coreBehind.blockchainOk);
EXPECT_EQ(*coreBehind.blocks, 150);
EXPECT_EQ(*coreBehind.longestChain, 160);
MockRefreshRpc coreFallbackRpc;
coreFallbackRpc.addFailure("z_gettotalbalance", "wallet warming up");
coreFallbackRpc.addResponse("getblockchaininfo", json{{"blocks", 8}, {"headers", 9}});
auto partialCore = Refresh::collectCoreRefreshResult(coreFallbackRpc);
// No longestchain in the response → not classified as "behind" → balance is still attempted (here it
// fails). getblockchaininfo is still issued first.
EXPECT_TRUE(coreFallbackRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo"
"getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
}));
EXPECT_FALSE(partialCore.balanceOk);
EXPECT_TRUE(partialCore.blockchainOk);