feat(ui): say "Node busy" instead of "Launching dragonxd"; honest quit copy

A node rebuilding note witnesses for an hour was shown as "Starting
DragonX node... Launching dragonxd", which invited closing it.

- Overlay, a new NodeBusy banner and the status bar now say what the node
  is doing (rebuilding witnesses / catching up on blocks / up but not
  answering), with the elapsed time and Phase-1 block progress. A rebuild
  during startup warm-up replaces the stale "-28 Activating best chain"
  text, and the overlay re-applies its text once a second instead of only
  on a connect attempt (30s apart while the node is busy).
- Quit prompt: elapsed time, "Keep node running & quit" is the default,
  "Stop anyway & quit" becomes "Stop node & wait" (it no longer kills).
- A reinstall skipped because the node is still running now notifies.
- Status bar: don't repeat the warm-up status on the right, and don't
  print "Connected" there in non-English locales.
- i18n: 31 new keys in all 8 languages; 10 dead force-quit/old-modal keys
  removed; CJK subset rebuilt for the new glyphs.
- Tests: chooseDaemonStatus, NodeBusy banner priority, and every new key
  present in all 8 JSONs with matching printf conversions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 10:26:32 -05:00
co-authored by Claude Opus 5.5
parent 0f25c67600
commit 1215ded1a0
17 changed files with 822 additions and 133 deletions
+114 -10
View File
@@ -946,6 +946,18 @@ void App::update()
pumpRpcBusyCanary();
const bool rpcBusy = rpc_busy_gate_->busy();
// Keep the "node starting / warming up / busy" overlay current between connect attempts and polls,
// unless something else has replaced the status (see init_status_title_, applyWarmupText).
if (ImGui::GetTime() >= status_refresh_at_) {
status_refresh_at_ = ImGui::GetTime() + 1.0;
if (state_.warming_up && !warmup_rpc_message_.empty() && connection_status_ == state_.warmup_status) {
applyWarmupText(warmup_rpc_message_);
} else if (state_.daemon_initializing && !state_.connected && !init_status_title_.empty() &&
connection_status_ == init_status_title_) {
applyDaemonInitStatus(init_status_busy_);
}
}
// Fast refresh (mining stats + daemon memory) every second
// Skip when wallet is locked — no need to poll, and queued tasks
// would delay the PIN unlock worker task.
@@ -1629,6 +1641,8 @@ void App::render()
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
ui::Notifications::instance().success(deletedMsg);
}
if (pending_reinstall_node_busy_.exchange(false, std::memory_order_relaxed))
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_node_busy"), 12.0f);
}
@@ -2315,6 +2329,9 @@ void App::renderNodeStatusBanner()
in.lite_open_error = lite_open_error_;
in.tip_known = state_.sync.tip_known;
in.blocks = state_.sync.blocks;
const NodeBusyText busy = nodeBusyText();
in.node_busy = busy.busy;
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
if (!banner.show) return;
@@ -2349,6 +2366,8 @@ void App::renderNodeStatusBanner()
title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break;
case ui::NodeBannerReason::NoPeers:
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
case ui::NodeBannerReason::NodeBusy:
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
case ui::NodeBannerReason::FullNodeOffline:
default:
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
@@ -2672,6 +2691,9 @@ void App::renderStatusBar()
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", TR("sb_rescanning"), dotStr);
}
} else if (rpc_busy_gate_->busy()) {
// The node holds back our requests (a long operation holds its cs_main): data may be stale.
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Warning()), "%s", TR("sb_node_busy"));
} else if (state_.sync.syncing) {
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
int blocksLeft = chainTip - state_.sync.blocks;
@@ -2803,7 +2825,11 @@ void App::renderStatusBar()
// with a small gap.
float gap = sbSectionGap;
float occupiedX = versionX; // leftmost X used by the version + connection status so far
if (!connection_status_.empty() && connection_status_ != "Connected") {
// The left of the bar already shows the warmup / "node busy" status; don't print it twice.
const bool shownOnLeft = (state_.warming_up || state_.daemon_initializing) &&
connection_status_ == state_.warmup_status;
if (!connection_status_.empty() && connection_status_ != "Connected" &&
connection_status_ != TR("connected") && !shownOnLeft) {
// During a post-repair rescan the raw status is a scary "RPC request failed: Timeout" — show a
// calm line instead (the rescan legitimately can't answer RPC yet).
const std::string statusBase = post_recovery_rescan_ ? std::string(TR("sb_finishing_repair"))
@@ -5833,6 +5859,7 @@ void App::reinstallBundledDaemon()
if (stillUp()) {
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
"binaries left untouched and no new node started\n");
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
return;
}
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
@@ -6084,6 +6111,70 @@ void App::feedDaemonBusy(const std::string& chunk)
if (daemon_busy_reset_pending_.exchange(false)) daemon_busy_.reset();
daemon_busy_.feed(chunk);
node_rebuilding_ = daemon_busy_.rebuilding();
if (!daemon_busy_.busy()) busy_first_seen_ = {};
else if (busy_first_seen_ == std::chrono::steady_clock::time_point{}) busy_first_seen_ = std::chrono::steady_clock::now();
}
static std::string formatBusyDuration(double secs)
{
const long s = secs > 0 ? static_cast<long>(secs) : 0;
char buf[32];
if (s >= 3600) snprintf(buf, sizeof(buf), "%ld:%02ld:%02ld", s / 3600, (s / 60) % 60, s % 60);
else snprintf(buf, sizeof(buf), "%ld:%02ld", s / 60, s % 60);
return buf;
}
App::NodeBusyText App::nodeBusyText() const
{
NodeBusyText out;
// The tracker reads OUR node's output; once that process is gone its last state means nothing.
const bool ours = daemon_controller_ && daemon_controller_->isRunning();
const bool rebuilding = ours && daemon_busy_.rebuilding();
const bool replaying = ours && daemon_busy_.replaying() && !rebuilding;
const bool rpcBusy = rpc_busy_gate_->busy();
if (!rebuilding && !replaying && !rpcBusy) return out;
out.busy = true;
out.rebuilding = rebuilding;
// Running time: from the node's own (UTC) log timestamp when there is one, else since we first saw it.
double secs = rpc_busy_gate_->busySeconds();
if (rebuilding || replaying) {
const std::int64_t startUtc = rebuilding ? daemon_busy_.rebuildStartUtc() : daemon_busy_.replayStartUtc();
if (startUtc > 0) {
secs = static_cast<double>(static_cast<std::int64_t>(std::time(nullptr)) - startUtc);
} else if (busy_first_seen_ != std::chrono::steady_clock::time_point{}) {
secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - busy_first_seen_).count();
}
}
out.elapsed = formatBusyDuration(secs);
char buf[512];
std::string progress;
if (rebuilding) {
out.title = TR("node_busy_rebuild_title");
snprintf(buf, sizeof(buf), TR("node_busy_rebuild_desc"), out.elapsed.c_str());
out.detail = buf;
// Phase 1 logs a line per 5000 blocks read; Phase 2 (extending the witnesses) logs nothing.
const long done = daemon_busy_.readDone(), total = daemon_busy_.readTotal();
const bool reading = total > 0 && done + 5000 < total;
if (reading) snprintf(buf, sizeof(buf), TR("node_busy_rebuild_reading"), done, total);
else snprintf(buf, sizeof(buf), "%s", TR("node_busy_rebuild_extending"));
out.detail += " ";
out.detail += buf;
if (reading) progress = buf;
} else if (replaying) {
out.title = TR("node_busy_replay_title");
snprintf(buf, sizeof(buf), TR("node_busy_replay_desc"), out.elapsed.c_str());
out.detail = buf;
} else {
out.title = TR("node_busy_rpc_title");
snprintf(buf, sizeof(buf), TR("node_busy_rpc_desc"), out.elapsed.c_str());
out.detail = buf;
}
snprintf(buf, sizeof(buf), TR("node_busy_running"), out.elapsed.c_str());
out.compact = buf;
if (!progress.empty()) out.compact += " · " + progress;
return out;
}
void App::resetDaemonBusy()
@@ -6142,17 +6233,35 @@ void App::renderDaemonStopConfirm()
bool proceed = false;
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
// Honest about what each choice does now that the wallet never kills the node: stopping waits for
// it to finish or abort the rebuild (Phase 1 aborts and is redone on the next start; Phase 2 can't
// be interrupted). Keeping it running loses nothing, so it is the recommended, focused default.
const NodeBusyText busy = nodeBusyText();
std::string elapsed = busy.elapsed;
if (!busy.busy && rpc_busy_since_ != std::chrono::steady_clock::time_point{})
elapsed = formatBusyDuration(std::chrono::duration<double>(
std::chrono::steady_clock::now() - rpc_busy_since_).count());
const bool rebuilding = busy.rebuilding;
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
ImGui::TextUnformatted(TR(rebuilding ? "stopconfirm_rebuild_title" : "stopconfirm_busy_title"));
if (Type().subtitle1()) ImGui::PopFont();
ImGui::Spacing();
char body[1024];
snprintf(body, sizeof(body), TR(rebuilding ? "stopconfirm_rebuild_body" : "stopconfirm_busy_body"),
elapsed.empty() ? "0:00" : elapsed.c_str());
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
ImGui::TextUnformatted(body);
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::Spacing();
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 160)));
const bool keep = TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0));
ImGui::PopStyleColor(3);
ImGui::SetItemDefaultFocus();
if (keep) {
shutdown_keep_daemon_override_ = true;
shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false;
@@ -6160,12 +6269,7 @@ void App::renderDaemonStopConfirm()
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
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(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
ImGui::PopStyleColor(3);
if (stopAnyway) {
if (TactileButton(TR("stopconfirm_stop_wait"), ImVec2(0, 0))) {
shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false;
proceed = true;
+23
View File
@@ -1077,6 +1077,7 @@ private:
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
@@ -1186,6 +1187,17 @@ private:
void abortRpcBusyCanary();
void feedDaemonBusy(const std::string& chunk);
void resetDaemonBusy();
std::chrono::steady_clock::time_point busy_first_seen_{}; // when the tracker last turned busy (UI thread)
// What to tell the user while the node is busy (overlay, banner, quit prompt). busy=false: it isn't.
struct NodeBusyText {
bool busy = false;
bool rebuilding = false;
std::string title; // e.g. "Node busy: rebuilding note witnesses — do not close"
std::string detail; // running time, what it's doing, progress when the node reports any
std::string elapsed; // "12:03"
std::string compact; // one line for the banner: "Running for 12:03 · Reading blocks: 45438 / 62583"
};
NodeBusyText nodeBusyText() const;
// Export/Import state
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
@@ -1574,6 +1586,17 @@ private:
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
// false when the daemon is merely launching (not bound yet). Returns the status title.
std::string applyDaemonInitStatus(bool reachableButBusy);
// The overlay text applyDaemonInitStatus last produced. It runs only on a connect failure, which a busy
// node backs off to every 30s, so update() re-applies it once a second (the elapsed time and the
// node's own progress tick) for as long as nothing else has replaced the status.
std::string init_status_title_;
bool init_status_busy_ = false;
double status_refresh_at_ = 0.0;
// The warm-up overlay text, from the node's last -28 message — unless its own log says it is rebuilding
// witnesses: a startup rebuild runs under "Activating best chain...", which hides the step users must
// not interrupt. update() re-applies it once a second too (see init_status_title_).
void applyWarmupText(const std::string& rpcMessage);
std::string warmup_rpc_message_;
// Tear down a connection that died mid-session (daemon crash / restart / dropped
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
+51 -31
View File
@@ -533,16 +533,7 @@ void App::tryConnect()
// the UI shows status instead of a blocking overlay.
state_.warming_up = true;
if (connect_stall_since_ <= 0.0) connect_stall_since_ = ImGui::GetTime(); // start the "taking too long" clock
auto wt = translateWarmup(warmupStatus);
state_.warmup_status = wt.title;
state_.warmup_description = wt.description;
// Append current block height from daemon output
if (daemon_controller_) {
int h = daemon_controller_->lastBlockHeight();
if (h > 0)
state_.warmup_status += " (Block " + std::to_string(h) + ")";
}
connection_status_ = state_.warmup_status;
applyWarmupText(warmupStatus);
}
} else {
// HTTP 401 = authentication failure. The daemon is running
@@ -594,7 +585,9 @@ void App::tryConnect()
// a connect refusal means it hasn't bound the RPC port yet. Either way, show a
// clear "node initializing" overlay (status + phase + block height from the
// daemon's own console output) instead of a bare technical error.
const bool reachableButBusy = rpc::isRpcTimeout(connectKind);
// Reached the node but no answer (reply timeout / full queue / held back): up but busy.
// A refusal or connect timeout means it hasn't bound (or has already unbound) RPC.
const bool reachableButBusy = rpc::rpcKindMeansBusy(connectKind);
applyDaemonInitStatus(reachableButBusy);
const float retryIn = connect_backoff_.next(connectKind);
VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, retry in %.0fs\n",
@@ -605,7 +598,7 @@ void App::tryConnect()
// An external daemon is on the RPC port but not answering. A timeout means it's
// up and busy initializing; surface that as the init overlay (we can't read its
// console since we didn't launch it, so no phase line — just a clear message).
if (rpc::isRpcTimeout(connectKind)) {
if (rpc::rpcKindMeansBusy(connectKind)) {
applyDaemonInitStatus(/*reachableButBusy=*/true);
} else if (!connectErr.empty()) {
char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str());
@@ -699,6 +692,7 @@ void App::onConnected()
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::None));
}
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
init_status_title_.clear();
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
connect_stall_since_ = 0.0; // connected — clear the "taking too long" clock
// A repair's rescan finished and connected — retire the recovery session so its crash-toast/error-card
@@ -870,19 +864,33 @@ std::string App::applyDaemonInitStatus(bool reachableButBusy)
}
}
// Most specific first: the node's own log says it is rebuilding witnesses or replaying blocks (then
// "Launching dragonxd…" over a node that has been busy for an hour invited closing it), else an init
// phase line, else "reachable but busy", else not bound yet.
const auto kind = daemon::chooseDaemonStatus(!phaseLine.empty(), reachableButBusy, daemon_busy_);
std::string busyTitle, busyDetail;
WarmupText wt;
if (!phaseLine.empty()) {
switch (kind) {
case daemon::DaemonStatusKind::Rebuilding:
case daemon::DaemonStatusKind::Replaying: {
const NodeBusyText busy = nodeBusyText();
busyTitle = busy.title;
busyDetail = busy.detail;
wt = {busyTitle.c_str(), busyDetail.c_str()};
break;
}
case daemon::DaemonStatusKind::InitPhase:
wt = translateWarmup(phaseLine);
} else if (reachableButBusy) {
// The probe connected but got no RPC reply within the timeout: the node is up but busy
// initializing (it isn't printing a recognizable phase, or we didn't launch it).
wt = {"Starting DragonX node…",
"The node is reachable but still initializing and isn't answering yet. "
"This is normal after an update or on first launch — it can take a few minutes."};
} else {
break;
case daemon::DaemonStatusKind::Busy:
// The probe reached the node but got no answer (a reply timeout or a full queue): it is up but
// too busy to answer (it isn't printing a recognizable phase, or we didn't launch it).
wt = {TR("node_starting_title"), TR("node_starting_busy_desc")};
break;
case daemon::DaemonStatusKind::Launching:
// The daemon is launching but hasn't bound its RPC port yet.
wt = {"Starting DragonX node…",
"Launching dragonxd and waiting for it to come online…"};
wt = {TR("node_starting_title"), TR("node_starting_launch_desc")};
break;
}
std::string title = wt.title;
@@ -892,9 +900,29 @@ std::string App::applyDaemonInitStatus(bool reachableButBusy)
state_.warmup_status = title;
state_.warmup_description = wt.description ? wt.description : "";
connection_status_ = title;
init_status_title_ = title;
init_status_busy_ = reachableButBusy;
return title;
}
void App::applyWarmupText(const std::string& rpcMessage)
{
warmup_rpc_message_ = rpcMessage;
if (daemon_busy_.rebuilding()) {
const NodeBusyText busy = nodeBusyText();
state_.warmup_status = busy.title;
state_.warmup_description = busy.detail;
} else {
const WarmupText wt = translateWarmup(rpcMessage);
state_.warmup_status = wt.title;
state_.warmup_description = wt.description;
}
// Append current block height from daemon output
const int h = daemon_controller_ ? daemon_controller_->lastBlockHeight() : -1;
if (h > 0) state_.warmup_status += " (Block " + std::to_string(h) + ")";
connection_status_ = state_.warmup_status;
}
void App::handleLostConnection(const std::string& reason)
{
DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str());
@@ -1799,15 +1827,7 @@ void App::refreshCoreData()
refreshData();
} else {
// Still warming up — update status
auto wt = translateWarmup(result.errorMessage);
state_.warmup_status = wt.title;
state_.warmup_description = wt.description;
if (daemon_controller_) {
int h = daemon_controller_->lastBlockHeight();
if (h > 0)
state_.warmup_status += " (Block " + std::to_string(h) + ")";
}
connection_status_ = state_.warmup_status;
applyWarmupText(result.errorMessage);
VERBOSE_LOGF("[warmup] Still warming up: %s\n", result.errorMessage.c_str());
}
};
+19
View File
@@ -141,5 +141,24 @@ inline bool shouldConfirmStop(bool stopsDaemon, const DaemonBusyTracker& tracker
return stopsDaemon && (tracker.rebuilding() || rpcBusyWhileAlive);
}
// What the "node is starting / not answering" overlay should say, most specific first.
enum class DaemonStatusKind {
Rebuilding, // its own log says it's rebuilding note witnesses
Replaying, // its own log says it's catching up on blocks it saved before (startup replay)
InitPhase, // a recognisable init phase line (loading the block index, verifying, ...)
Busy, // it answers the connection but not the request (reply timeout / queue full)
Launching, // nothing bound to the RPC port yet
};
inline DaemonStatusKind chooseDaemonStatus(bool hasInitPhaseLine, bool reachableButBusy,
const DaemonBusyTracker& tracker)
{
if (tracker.rebuilding()) return DaemonStatusKind::Rebuilding;
if (tracker.replaying()) return DaemonStatusKind::Replaying;
if (hasInitPhaseLine) return DaemonStatusKind::InitPhase;
if (reachableButBusy) return DaemonStatusKind::Busy;
return DaemonStatusKind::Launching;
}
} // namespace daemon
} // namespace dragonx
+19
View File
@@ -32,6 +32,7 @@ enum class NodeBannerReason {
DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped
LiteOpenFailed, // lite build: the wallet failed to open
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
NodeBusy, // the node is up but busy (rebuilding witnesses / catching up / not answering)
};
struct NodeBannerState {
@@ -64,6 +65,12 @@ struct NodeBannerInputs {
bool daemon_running = false;
int daemon_crash_count = 0;
// The node is running but busy with something it can't be interrupted out of (a witness rebuild, the
// startup replay) or too busy to answer RPC. Wallet data may be stale; a reconnect would only add
// requests, so the banner offers no action. node_busy_detail is the live, translated description.
bool node_busy = false;
std::string node_busy_detail;
std::string connection_status; // human-readable status line (already translated)
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
std::string lite_open_error; // lite: last wallet-open failure reason
@@ -89,6 +96,18 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
return s;
}
// Full node, busy. Before the startup-phase returns below: the overlay sets daemon_initializing while
// the node is busy too, and the banner must still say why the wallet isn't updating. No action: a
// "Reconnect" would only queue more requests behind the busy node.
if (in.node_busy) {
s.show = true;
s.severity = NodeBannerSeverity::Warning;
s.reason = NodeBannerReason::NodeBusy;
s.action = NodeBannerAction::None;
s.detail = in.node_busy_detail;
return s;
}
// Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no
// banner. An active connect attempt likewise shows progress, not an error — don't flicker a
// banner over it. (These return even if `connected` has flipped early, matching prior behavior.)
+23 -12
View File
@@ -582,14 +582,9 @@ void I18n::loadBuiltinEnglish()
strings_["incorrect_passphrase"] = "Incorrect passphrase";
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
strings_["force_quit"] = "Force Quit";
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
strings_["force_quit_confirm_title"] = "Force Quit?";
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
strings_["force_quit_yes"] = "Force Quit";
// Stopping the node on quit: deliver stop, wait, never kill unasked (translations: S10 batch)
// Stopping the node on quit: deliver stop, wait, never kill unasked
strings_["shutdown_leave_running"] = "Close wallet — leave node running";
strings_["shutdown_leave_running_tip"] = "Closes the wallet now. The node keeps running and finishes shutting down on its own; nothing is lost.";
strings_["shutdown_leave_running_tip"] = "Closes the wallet now. The node keeps running on its own and finishes what it is doing; nothing is lost.";
strings_["shutdown_kill_node"] = "Kill node (unsafe)";
strings_["shutdown_kill_node_tip"] = "Ends the node at once. Anything it has not saved is lost, and its next start replays the blocks since its last save.";
strings_["shutdown_kill_confirm_title"] = "Kill the node?";
@@ -600,6 +595,27 @@ void I18n::loadBuiltinEnglish()
strings_["shutdown_stop_delivered"] = "The node is shutting down and saving its state";
strings_["shutdown_stop_cpu_suffix"] = " · CPU %d%%";
strings_["shutdown_stop_idle"] = "No CPU or disk activity for %d min — the node may be stuck";
// The node is busy (S9): overlay, banner, status bar
strings_["node_busy_rebuild_title"] = "Node busy: rebuilding note witnesses";
strings_["node_busy_running"] = "Running for %s";
strings_["node_busy_rebuild_desc"] = "Running for %s. Large wallets can take about an hour, and the node can't answer the wallet until it finishes.";
strings_["node_busy_rebuild_reading"] = "Reading blocks: %ld / %ld";
strings_["node_busy_rebuild_extending"] = "Extending witnesses (the node reports no progress for this step).";
strings_["node_busy_replay_title"] = "Node busy: catching up on blocks";
strings_["node_busy_replay_desc"] = "Running for %s. The node is re-applying blocks it received before it last stopped.";
strings_["node_busy_rpc_title"] = "Node busy: not answering yet";
strings_["node_busy_rpc_desc"] = "The node is running but has been too busy to answer the wallet for %s, usually because of a long operation such as a witness rebuild.";
strings_["node_starting_title"] = "Starting DragonX node…";
strings_["node_starting_busy_desc"] = "The node is reachable but busy and isn't answering yet. This is normal after an update or on first launch; it can take a few minutes.";
strings_["node_starting_launch_desc"] = "Launching dragonxd and waiting for it to come online…";
strings_["sb_node_busy"] = "Node busy";
// Quit while the node is busy (S10)
strings_["stopconfirm_rebuild_title"] = "The node is rebuilding note witnesses";
strings_["stopconfirm_rebuild_body"] = "It has been running for %s, and large wallets can take about an hour. Stopping the node now either waits for the rebuild to finish or makes its next start redo the rebuild and any catch-up.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
strings_["stopconfirm_busy_title"] = "The node is busy";
strings_["stopconfirm_busy_body"] = "It has been too busy to answer the wallet for %s, probably because of a long operation such as a witness rebuild. Stopping it now can make its next start redo that work.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
strings_["stopconfirm_stop_wait"] = "Stop node & wait";
strings_["appx_daemon_reinstall_node_busy"] = "The node is still running, so the daemon was not reinstalled. Try again once it has stopped.";
strings_["reduce_motion"] = "Reduce Motion";
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
strings_["ago"] = "ago";
@@ -2638,10 +2654,7 @@ void I18n::loadBuiltinEnglish()
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.";
@@ -2671,8 +2684,6 @@ void I18n::loadBuiltinEnglish()
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%%";