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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user