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

@@ -6,6 +6,7 @@
#include "transaction_details_dialog.h"
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../theme.h"
@@ -28,6 +29,10 @@ namespace ui {
using namespace material;
static std::string TrId(const char* key, const char* id) {
return std::string(TR(key)) + "##" + id;
}
// Helper to truncate strings
static std::string truncateString(const std::string& str, int maxLen = 16) {
if (str.length() <= static_cast<size_t>(maxLen)) return str;
@@ -66,7 +71,7 @@ struct DisplayTx {
};
std::string DisplayTx::getTimeString() const {
if (timestamp <= 0) return "Pending";
if (timestamp <= 0) return TR("pending");
std::time_t t = static_cast<std::time_t>(timestamp);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
@@ -79,10 +84,11 @@ static std::string timeAgo(int64_t timestamp) {
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
}
// Draw a small transaction-type icon
@@ -191,10 +197,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "RECEIVED");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", recvCount);
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -221,10 +227,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "SENT");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", sendCount);
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -251,10 +257,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "MINED");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", minedCount);
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -292,23 +298,23 @@ void RenderTransactionsTab(App* app)
ImGui::SetNextItemWidth(searchWidth);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
ImGui::InputTextWithHint("##TxSearch", "Search...", search_filter, sizeof(search_filter));
ImGui::InputTextWithHint("##TxSearch", TR("search_placeholder"), search_filter, sizeof(search_filter));
ImGui::PopStyleVar();
float filterGap = std::max(8.0f, ((filterGapEl.size > 0) ? filterGapEl.size : 20.0f) * hs);
ImGui::SameLine(0, filterGap);
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
ImGui::SetNextItemWidth(comboW);
const char* types[] = { "All", "Sent", "Received", "Mined" };
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
ImGui::SameLine(0, filterGap);
if (TactileButton("Refresh", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("refresh", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
app->refreshNow();
}
ImGui::SameLine(0, filterGap);
if (TactileButton("Export CSV", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("export_csv", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
ExportTransactionsDialog::show();
}
@@ -442,7 +448,7 @@ void RenderTransactionsTab(App* app)
// ---- Heading line: "TRANSACTIONS" left, pagination right ----
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTIONS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transactions_upper"));
if (totalPages > 1) {
float paginationH = ImGui::GetFrameHeight();
@@ -557,13 +563,13 @@ void RenderTransactionsTab(App* app)
{
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No transactions found");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
} else if (filtered_indices.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No matching transactions");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
} else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float innerW = ImGui::GetContentRegionAvail().x;
@@ -592,15 +598,15 @@ void RenderTransactionsTab(App* app)
ImU32 iconCol;
const char* typeStr;
if (tx.display_type == "shield") {
iconCol = Primary(); typeStr = "Shielded";
iconCol = Primary(); typeStr = TR("shielded_type");
} else if (tx.display_type == "receive") {
iconCol = greenCol; typeStr = "Recv";
iconCol = greenCol; typeStr = TR("recv_type");
} else if (tx.display_type == "send") {
iconCol = redCol; typeStr = "Sent";
iconCol = redCol; typeStr = TR("sent_type");
} else if (tx.display_type == "immature") {
iconCol = Warning(); typeStr = "Immature";
iconCol = Warning(); typeStr = TR("immature_type");
} else {
iconCol = goldCol; typeStr = "Mined";
iconCol = goldCol; typeStr = TR("mined_type");
}
// Expanded selection accent
@@ -669,14 +675,14 @@ void RenderTransactionsTab(App* app)
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < 10) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
statusStr = "Mature"; statusCol = greenCol;
statusStr = TR("mature"); statusCol = greenCol;
} else {
statusStr = "Confirmed"; statusCol = WithAlpha(Success(), 140);
statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
}
// Position status badge in the middle-right area
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
@@ -707,14 +713,14 @@ void RenderTransactionsTab(App* app)
// Context menu
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
if (ImGui::MenuItem("Copy Address") && !tx.address.empty()) {
if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
ImGui::SetClipboardText(tx.address.c_str());
}
if (ImGui::MenuItem("Copy TxID")) {
if (ImGui::MenuItem(TR("copy_txid"))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("View on Explorer")) {
if (ImGui::MenuItem(TR("view_on_explorer"))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
@@ -726,7 +732,7 @@ void RenderTransactionsTab(App* app)
system(cmd.c_str());
#endif
}
if (ImGui::MenuItem("View Details")) {
if (ImGui::MenuItem(TR("view_details"))) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
@@ -747,19 +753,19 @@ void RenderTransactionsTab(App* app)
// From address
if (!tx.from_address.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
ImGui::TextWrapped("%s", tx.from_address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// To address
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
tx.display_type == "send" ? "TO" : (tx.display_type == "shield" ? "SHIELDED TO" : "ADDRESS"));
tx.display_type == "send" ? TR("to_upper") : (tx.display_type == "shield" ? TR("shielded_to") : TR("address_upper")));
ImGui::TextWrapped("%s", tx.address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// TxID (full, copyable)
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTION ID");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
ImGui::TextWrapped("%s", tx.txid.c_str());
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
@@ -767,13 +773,13 @@ void RenderTransactionsTab(App* app)
// Memo
if (!tx.memo.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
ImGui::TextWrapped("%s", tx.memo.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// Confirmations + time
snprintf(buf, sizeof(buf), "%d confirmations | %s",
snprintf(buf, sizeof(buf), TR("confirmations_display"),
tx.confirmations, tx.getTimeString().c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -782,15 +788,15 @@ void RenderTransactionsTab(App* app)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
if (TactileSmallButton("Copy TxID##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::SameLine();
if (!tx.address.empty() && TactileSmallButton("Copy Address##detail", S.resolveFont("button"))) {
if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.address.c_str());
}
ImGui::SameLine();
if (TactileSmallButton("Explorer##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
@@ -803,7 +809,7 @@ void RenderTransactionsTab(App* app)
#endif
}
ImGui::SameLine();
if (TactileSmallButton("Full Details##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("full_details", "detail").c_str(), S.resolveFont("button"))) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
@@ -849,7 +855,7 @@ void RenderTransactionsTab(App* app)
}
// Status line with page info
snprintf(buf, sizeof(buf), "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)",
snprintf(buf, sizeof(buf), TR("showing_transactions"),
filtered_count > 0 ? pageStart + 1 : 0, pageEnd, filtered_count, state.transactions.size());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}