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:
@@ -11,6 +11,7 @@
|
||||
#include "receive_tab.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../ui/widgets/qr_code.h"
|
||||
@@ -35,6 +36,10 @@ namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
static std::string TrId(const char* key, const char* id) {
|
||||
return std::string(TR(key)) + "##" + id;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
@@ -110,7 +115,7 @@ static void RenderSyncBanner(const WalletState& state) {
|
||||
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
|
||||
char syncBuf[128];
|
||||
snprintf(syncBuf, sizeof(syncBuf),
|
||||
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
|
||||
TR("blockchain_syncing"), syncPct);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.receive", "sync-banner-bg-color").color)));
|
||||
float syncH = std::max(schema::UI().drawElement("tabs.receive", "sync-banner-min-height").size, schema::UI().drawElement("tabs.receive", "sync-banner-height").size * Layout::vScale());
|
||||
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, syncH),
|
||||
@@ -129,13 +134,13 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
char buf[256];
|
||||
|
||||
// Header row: label + address type toggle buttons
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));
|
||||
|
||||
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
|
||||
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
|
||||
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
|
||||
ImGui::SameLine(width - toggleTotalW);
|
||||
const char* filterLabels[] = { "All", "Z", "T" };
|
||||
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
bool isActive = (s_addr_type_filter == i);
|
||||
if (isActive) {
|
||||
@@ -190,7 +195,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
|
||||
// Build preview string
|
||||
if (!app->isConnected()) {
|
||||
s_source_preview = "Not connected to daemon";
|
||||
s_source_preview = TR("not_connected");
|
||||
} else if (s_selected_address_idx >= 0 &&
|
||||
s_selected_address_idx < (int)state.addresses.size()) {
|
||||
const auto& addr = state.addresses[s_selected_address_idx];
|
||||
@@ -202,7 +207,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = "Select a receiving address...";
|
||||
s_source_preview = TR("select_receiving_address");
|
||||
}
|
||||
|
||||
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
|
||||
@@ -212,7 +217,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
||||
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
|
||||
if (!app->isConnected() || state.addresses.empty()) {
|
||||
ImGui::TextDisabled("No addresses available");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||
} else {
|
||||
// Build filtered and sorted list
|
||||
std::vector<size_t> sortedIdx;
|
||||
@@ -229,7 +234,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
});
|
||||
|
||||
if (sortedIdx.empty()) {
|
||||
ImGui::TextDisabled("No addresses match filter");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_match"));
|
||||
} else {
|
||||
size_t addrTruncLen = static_cast<size_t>(std::max(schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-divisor").size));
|
||||
double now = ImGui::GetTime();
|
||||
@@ -287,10 +292,10 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!app->isConnected() || s_selected_address_idx < 0 ||
|
||||
s_selected_address_idx >= (int)state.addresses.size());
|
||||
if (TactileButton("Copy##recvAddr", ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TrId("copy", "recvAddr").c_str(), ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
|
||||
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
@@ -298,23 +303,23 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
// New address button on same line
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileButton("+ New##recv", ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (s_addr_type_filter != 2) {
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
if (addr.empty())
|
||||
Notifications::instance().error("Failed to create new shielded address");
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success("New shielded address created");
|
||||
Notifications::instance().success(TR("new_shielded_created"));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
if (addr.empty())
|
||||
Notifications::instance().error("Failed to create new transparent address");
|
||||
Notifications::instance().error(TR("failed_create_transparent"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success("New transparent address created");
|
||||
Notifications::instance().success(TR("new_transparent_created"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -330,10 +335,11 @@ static std::string recvTimeAgo(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;
|
||||
}
|
||||
|
||||
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
|
||||
@@ -353,7 +359,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
const WalletState& state, float width,
|
||||
ImFont* capFont, App* app) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT RECEIVED");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float hs = Layout::hScale(width);
|
||||
@@ -375,7 +381,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
}
|
||||
|
||||
if (recvs.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent receives");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -416,11 +422,11 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, "Received");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, TR("received_label"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = recvTimeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Received").x;
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("received_label")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
@@ -457,12 +463,12 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = "Pending"; statusCol = Warning();
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = "Confirmed"; statusCol = greenCol;
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
@@ -545,10 +551,10 @@ void RenderReceiveTab(App* app)
|
||||
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
|
||||
OnSurfaceDisabled(), "Waiting for daemon connection...");
|
||||
OnSurfaceDisabled(), TR("waiting_for_daemon"));
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + S.drawElement("tabs.receive", "empty-state-subtitle-gap").size),
|
||||
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
|
||||
OnSurfaceDisabled(), TR("addresses_appear_here"));
|
||||
ImGui::Dummy(ImVec2(formW, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
@@ -572,7 +578,7 @@ void RenderReceiveTab(App* app)
|
||||
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size),
|
||||
OnSurfaceDisabled(), "Loading addresses...");
|
||||
OnSurfaceDisabled(), TR("loading_addresses"));
|
||||
ImGui::Dummy(ImVec2(formW, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
@@ -639,7 +645,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// ---- PAYMENT REQUEST ----
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Amount input with currency toggle
|
||||
@@ -805,7 +811,7 @@ void RenderReceiveTab(App* app)
|
||||
// Memo (z-addresses only)
|
||||
if (isZ) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
|
||||
|
||||
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
|
||||
@@ -841,7 +847,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
|
||||
ImGui::BeginDisabled(!hasData);
|
||||
if (TactileSmallButton("Clear Request##recv", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TrId("clear_request", "recv").c_str(), S.resolveFont("button"))) {
|
||||
s_request_amount = 0.0;
|
||||
s_request_usd_amount = 0.0;
|
||||
s_request_memo[0] = '\0';
|
||||
@@ -880,20 +886,20 @@ void RenderReceiveTab(App* app)
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
|
||||
qrPanelMin.y + totalQrSize * 0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), "QR unavailable");
|
||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(qrPanelMin);
|
||||
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy %s",
|
||||
s_request_amount > 0 ? "payment URI" : "address");
|
||||
ImGui::SetTooltip("%s",
|
||||
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(qr_data.c_str());
|
||||
Notifications::instance().info(s_request_amount > 0
|
||||
? "Payment URI copied" : "Address copied");
|
||||
? TR("payment_uri_copied") : TR("address_copied"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(rx, qrPanelMax.y));
|
||||
@@ -926,9 +932,9 @@ void RenderReceiveTab(App* app)
|
||||
if (s_request_amount > 0) {
|
||||
if (!firstBtn) ImGui::SameLine(0, btnGap);
|
||||
firstBtn = false;
|
||||
if (TactileButton("Copy URI##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("copy_uri", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_cached_qr_data.c_str());
|
||||
Notifications::instance().info("Payment URI copied");
|
||||
Notifications::instance().info(TR("payment_uri_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,14 +945,16 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileButton("Share##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("share", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
char shareBuf[1024];
|
||||
snprintf(shareBuf, sizeof(shareBuf),
|
||||
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
|
||||
"%s\n%s: %.8f %s\n%s: %s\nURI: %s",
|
||||
TR("payment_request"), TR("amount"),
|
||||
s_request_amount, DRAGONX_TICKER,
|
||||
selected.address.c_str(), s_cached_qr_data.c_str());
|
||||
TR("address"), selected.address.c_str(),
|
||||
s_cached_qr_data.c_str());
|
||||
ImGui::SetClipboardText(shareBuf);
|
||||
Notifications::instance().info("Payment request copied");
|
||||
Notifications::instance().info(TR("payment_request_copied"));
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
@@ -959,7 +967,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileButton("Explorer##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
OpenExplorerURL(selected.address);
|
||||
}
|
||||
ImGui::PopStyleVar(); // FrameBorderSize
|
||||
@@ -972,7 +980,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
|
||||
if (TactileButton("Send \xe2\x86\x97##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("send", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
SetSendFromAddress(selected.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
@@ -1001,7 +1009,7 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
|
||||
ImGui::Dummy(ImVec2(formW, 0));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
Reference in New Issue
Block a user