feat: Full UI internationalization, pool hashrate stats, and layout caching

- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
This commit is contained in:
2026-03-11 00:40:50 -05:00
parent f416ff3d09
commit 2c5a658ea5
71 changed files with 43567 additions and 5267 deletions

View File

@@ -248,8 +248,18 @@ void App::preFrame()
ui::schema::UISchema::instance().applyIfDirty();
}
// Refresh balance layout config after schema reload
ui::RefreshBalanceLayoutConfig();
// Refresh the per-frame layout cache (reads TOML values once)
ui::Layout::beginFrame();
// Refresh balance layout config only when schema changes
{
static uint32_t s_balanceLayoutGen = 0;
uint32_t gen = ui::schema::UISchema::instance().generation();
if (gen != s_balanceLayoutGen) {
s_balanceLayoutGen = gen;
ui::RefreshBalanceLayoutConfig();
}
}
// If font sizes changed in the TOML, rebuild the font atlas
if (ui::schema::UISchema::instance().consumeFontsChanged()) {
@@ -374,6 +384,7 @@ void App::update()
double memMB = xmrig_manager_->getMemoryUsageMB();
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
ps.threads_active = xs.threads_active;
ps.pool_hashrate = xs.pool_hashrate;
ps.log_lines = xmrig_manager_->getRecentLines(30);
// Record hashrate sample for the chart
@@ -390,91 +401,90 @@ void App::update()
state_.mining.log_lines = embedded_daemon_->getRecentLines(50);
}
// Check daemon output for rescan progress
// Check daemon output for rescan progress (offloaded to worker)
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
if (!newOutput.empty()) {
// Look for rescan progress patterns in new output
// Hush patterns: "Still rescanning. At block X. Progress=Y" or "Rescanning..." with percentage
bool foundRescan = false;
float rescanPct = 0.0f;
// Search line by line for rescan info
size_t pos = 0;
while (pos < newOutput.size()) {
size_t eol = newOutput.find('\n', pos);
if (eol == std::string::npos) eol = newOutput.size();
std::string line = newOutput.substr(pos, eol - pos);
pos = eol + 1;
// Check for "Rescanning from height" (rescan starting)
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
state_.sync.rescan_status = line;
}
// Check for "Still rescanning" with progress
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
// Try to extract progress (Progress=0.XXXX)
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9; // strlen("Progress=")
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
if (!newOutput.empty() && fast_worker_) {
fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb {
// Parse on worker thread — pure string work, no shared state access
bool foundRescan = false;
bool finished = false;
float rescanPct = 0.0f;
std::string lastStatus;
size_t pos = 0;
while (pos < output.size()) {
size_t eol = output.find('\n', pos);
if (eol == std::string::npos) eol = output.size();
std::string line = output.substr(pos, eol - pos);
pos = eol + 1;
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
lastStatus = line;
}
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9;
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
if (numEnd > numStart) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f;
} catch (...) {}
lastStatus = line;
}
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
finished = true;
}
}
// Return callback to apply results on main thread
return [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() {
if (finished) {
if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete");
}
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
} else if (foundRescan) {
state_.sync.rescanning = true;
if (rescanPct > 0.0f) {
state_.sync.rescan_progress = rescanPct / 100.0f;
}
if (!status.empty()) {
state_.sync.rescan_status = status;
}
}
state_.sync.rescan_status = line;
}
// Check for "Rescanning..." with percentage (ShowProgress output)
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
// Try to extract percentage
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
// Walk backwards to find the number
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart));
} catch (...) {}
}
}
state_.sync.rescan_status = line;
}
// Check for "Done rescanning" (rescan complete)
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete");
}
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
}
}
if (foundRescan) {
state_.sync.rescanning = true;
if (rescanPct > 0.0f) {
state_.sync.rescan_progress = rescanPct / 100.0f;
}
}
};
});
}
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
// Clear rescan state if daemon is not running (but preserve during restart)
@@ -899,11 +909,11 @@ void App::render()
if (gradient_tex_ != 0) {
sbStatus.gradientTexID = gradient_tex_;
}
// Count unconfirmed transactions
// Count unconfirmed transactions (pending in mempool, not conflicted/orphaned)
{
int unconf = 0;
for (const auto& tx : state_.transactions) {
if (!tx.isConfirmed()) ++unconf;
if (tx.confirmations == 0) ++unconf;
}
sbStatus.unconfirmedTxCount = unconf;
}
@@ -1350,7 +1360,8 @@ void App::renderStatusBar()
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning%s", dotStr);
}
} else if (state_.sync.syncing) {
int blocksLeft = state_.sync.headers - state_.sync.blocks;
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
int blocksLeft = chainTip - state_.sync.blocks;
if (blocksLeft < 0) blocksLeft = 0;
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
state_.sync.verification_progress * 100.0, blocksLeft);