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

@@ -13,6 +13,7 @@
#include "../material/color_theme.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/i18n.h"
#include <imgui.h>
#include <cstring>
@@ -75,8 +76,8 @@ ConsoleTab::ConsoleTab()
refreshColors();
// Add welcome message
addLine("DragonX Wallet Console", COLOR_INFO);
addLine("Type 'help' for available commands", COLOR_INFO);
addLine(TR("console_welcome"), COLOR_INFO);
addLine(TR("console_type_help"), COLOR_INFO);
addLine("", COLOR_RESULT);
}
@@ -117,27 +118,38 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
if (current_state == daemon::EmbeddedDaemon::State::Starting &&
last_daemon_state_ == daemon::EmbeddedDaemon::State::Stopped) {
addLine("", COLOR_RESULT);
addLine("=== Starting DragonX Full Node ===", COLOR_INFO);
addLine("Capturing daemon output...", COLOR_INFO);
addLine(TR("console_starting_node"), COLOR_INFO);
addLine(TR("console_capturing_output"), COLOR_INFO);
addLine("", COLOR_RESULT);
shown_startup_message_ = true;
}
else if (current_state == daemon::EmbeddedDaemon::State::Running &&
last_daemon_state_ != daemon::EmbeddedDaemon::State::Running) {
addLine("=== Daemon is running ===", COLOR_INFO);
addLine(TR("console_daemon_started"), COLOR_INFO);
}
else if (current_state == daemon::EmbeddedDaemon::State::Stopped &&
last_daemon_state_ == daemon::EmbeddedDaemon::State::Running) {
addLine("", COLOR_RESULT);
addLine("=== Daemon stopped ===", COLOR_INFO);
addLine(TR("console_daemon_stopped"), COLOR_INFO);
}
else if (current_state == daemon::EmbeddedDaemon::State::Error) {
addLine("=== Daemon error: " + daemon->getLastError() + " ===", COLOR_ERROR);
addLine(std::string(TR("console_daemon_error")) + daemon->getLastError() + " ===", COLOR_ERROR);
}
last_daemon_state_ = current_state;
}
// Track RPC connection state and show a message when connected
if (rpc) {
bool connected_now = rpc->isConnected();
if (connected_now && !last_rpc_connected_) {
addLine(TR("console_connected"), COLOR_INFO);
} else if (!connected_now && last_rpc_connected_) {
addLine(TR("console_disconnected"), COLOR_ERROR);
}
last_rpc_connected_ = connected_now;
}
// Check for new daemon output — always capture so toggle works as a live filter
if (daemon) {
std::string new_output = daemon->getOutputSince(last_daemon_output_size_);
@@ -372,31 +384,31 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Daemon status with colored dot
if (daemon) {
auto state = daemon->getState();
const char* status_text = "Unknown";
const char* status_text = TR("console_status_unknown");
ImU32 dotCol = IM_COL32(150, 150, 150, 255);
bool pulse = false;
switch (state) {
case daemon::EmbeddedDaemon::State::Stopped:
status_text = "Stopped";
status_text = TR("console_status_stopped");
dotCol = IM_COL32(150, 150, 150, 255);
break;
case daemon::EmbeddedDaemon::State::Starting:
status_text = "Starting";
status_text = TR("console_status_starting");
dotCol = Warning();
pulse = true;
break;
case daemon::EmbeddedDaemon::State::Running:
status_text = "Running";
status_text = TR("console_status_running");
dotCol = Success();
break;
case daemon::EmbeddedDaemon::State::Stopping:
status_text = "Stopping";
status_text = TR("console_status_stopping");
dotCol = Warning();
pulse = true;
break;
case daemon::EmbeddedDaemon::State::Error:
status_text = "Error";
status_text = TR("console_status_error");
dotCol = Error();
break;
}
@@ -418,7 +430,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, dotCol, status_text);
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No daemon");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
}
ImGui::SameLine();
@@ -426,7 +438,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Auto-scroll toggle
if (ImGui::Checkbox("Auto-scroll", &auto_scroll_)) {
if (ImGui::Checkbox(TR("console_auto_scroll"), &auto_scroll_)) {
if (auto_scroll_) {
scroll_to_bottom_ = true;
new_lines_since_scroll_ = 0;
@@ -440,7 +452,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Daemon messages toggle
{
static bool s_prev_daemon_enabled = true;
ImGui::Checkbox("Daemon", &s_daemon_messages_enabled);
ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
// When toggling daemon filter while auto-scroll is active, scroll to bottom
if (s_prev_daemon_enabled != s_daemon_messages_enabled && auto_scroll_) {
scroll_to_bottom_ = true;
@@ -448,7 +460,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_prev_daemon_enabled = s_daemon_messages_enabled;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show daemon output messages");
ImGui::SetTooltip("%s", TR("console_show_daemon_output"));
}
ImGui::SameLine();
@@ -458,7 +470,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Errors-only toggle
{
static bool s_prev_errors_only = false;
ImGui::Checkbox("Errors", &s_errors_only_enabled);
ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
// When toggling errors filter while auto-scroll is active, scroll to bottom
if (s_prev_errors_only != s_errors_only_enabled && auto_scroll_) {
scroll_to_bottom_ = true;
@@ -466,7 +478,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_prev_errors_only = s_errors_only_enabled;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show only error messages");
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
}
ImGui::SameLine();
@@ -474,7 +486,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Clear button
if (TactileButton("Clear", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
clear();
clearSelection();
}
@@ -482,7 +494,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Copy button — material styled
if (TactileButton("Copy", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
std::lock_guard<std::mutex> lock(lines_mutex_);
if (has_selection_) {
std::string selected = getSelectedText();
@@ -501,17 +513,17 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(has_selection_ ? "Copy selected text" : "Copy all output");
ImGui::SetTooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
}
ImGui::SameLine();
// Commands reference button
if (TactileButton("Commands", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
show_commands_popup_ = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show RPC command reference");
ImGui::SetTooltip("%s", TR("console_show_rpc_ref"));
}
ImGui::SameLine();
@@ -519,7 +531,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Line count
{
std::lock_guard<std::mutex> lock(lines_mutex_);
ImGui::TextDisabled("(%zu lines)", lines_.size());
ImGui::TextDisabled(TR("console_line_count"), lines_.size());
}
ImGui::SameLine();
@@ -531,7 +543,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
ImGui::SetNextItemWidth(filterW);
ImGui::InputTextWithHint("##ConsoleFilter", "Filter output...", filter_text_, sizeof(filter_text_));
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
// Zoom +/- buttons (right side of toolbar)
ImGui::SameLine();
@@ -548,14 +560,14 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Zoom out (%.0f%%)", s_console_zoom * 100.0f);
ImGui::SetTooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
}
ImGui::SameLine();
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Zoom in (%.0f%%)", s_console_zoom * 100.0f);
ImGui::SetTooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
}
}
}
@@ -889,7 +901,7 @@ void ConsoleTab::renderOutput()
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
if (has_text_filter) {
char filterBuf[128];
snprintf(filterBuf, sizeof(filterBuf), "Showing %d of %zu lines",
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
visible_count, lines_.size());
ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddText(indicatorPos,
@@ -899,13 +911,13 @@ void ConsoleTab::renderOutput()
// Right-click context menu
if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
if (ImGui::MenuItem("Copy", "Ctrl+C", false, has_selection_)) {
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
std::string selected = getSelectedText();
if (!selected.empty()) {
ImGui::SetClipboardText(selected.c_str());
}
}
if (ImGui::MenuItem("Select All", "Ctrl+A")) {
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
if (!lines_.empty()) {
sel_anchor_ = {0, 0};
sel_end_ = {static_cast<int>(lines_.size()) - 1,
@@ -914,7 +926,7 @@ void ConsoleTab::renderOutput()
}
}
ImGui::Separator();
if (ImGui::MenuItem("Clear Console")) {
if (ImGui::MenuItem(TR("console_clear_console"))) {
// Can't call clear() here (already holding lock), just mark for clearing
lines_.clear();
has_selection_ = false;
@@ -938,7 +950,7 @@ void ConsoleTab::renderOutput()
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
char buf[48];
snprintf(buf, sizeof(buf), " %d new line%s",
snprintf(buf, sizeof(buf), TR("console_new_lines"),
new_lines_since_scroll_, new_lines_since_scroll_ != 1 ? "s" : "");
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
@@ -1229,7 +1241,7 @@ void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker)
data->InsertChars(0, matches[0]);
} else if (matches.size() > 1) {
// Multiple matches — show list in console and complete common prefix
console->addLine("Completions:", ConsoleTab::COLOR_INFO);
console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO);
std::string line = " ";
for (size_t m = 0; m < matches.size(); m++) {
if (m > 0) line += " ";
@@ -1284,17 +1296,17 @@ void ConsoleTab::renderCommandsPopup()
using namespace material;
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
if (!material::BeginOverlayDialog("RPC Command Reference", &show_commands_popup_, popW, 0.94f)) {
if (!material::BeginOverlayDialog(TR("console_rpc_reference"), &show_commands_popup_, popW, 0.94f)) {
return;
}
Type().text(TypeStyle::H6, "RPC Command Reference");
Type().text(TypeStyle::H6, TR("console_rpc_reference"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Search filter
static char cmdFilter[128] = {0};
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##CmdSearch", "Search commands...", cmdFilter, sizeof(cmdFilter));
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Command entries
@@ -1507,9 +1519,9 @@ void ConsoleTab::renderCommandsPopup()
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (cmd.params[0] != '\0')
ImGui::SetTooltip("Click to insert '%s %s' into input", cmd.name, cmd.params);
ImGui::SetTooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
else
ImGui::SetTooltip("Click to insert '%s' into input", cmd.name);
ImGui::SetTooltip(TR("console_click_insert"), cmd.name);
}
ImGui::PopStyleColor(3);
if (showParams) {
@@ -1575,7 +1587,7 @@ void ConsoleTab::renderCommandsPopup()
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Close button
if (ImGui::Button("Close", ImVec2(-1, 0))) {
if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
cmdFilter[0] = '\0';
show_commands_popup_ = false;
}
@@ -1605,28 +1617,28 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
}
if (cmd == "help") {
addLine("Available commands:", COLOR_INFO);
addLine(" clear - Clear console output", COLOR_RESULT);
addLine(" help - Show this help", COLOR_RESULT);
addLine(TR("console_available_commands"), COLOR_INFO);
addLine(TR("console_help_clear"), COLOR_RESULT);
addLine(TR("console_help_help"), COLOR_RESULT);
addLine("", COLOR_RESULT);
addLine("Common RPC commands (when connected):", COLOR_INFO);
addLine(" getinfo - Get daemon info", COLOR_RESULT);
addLine(" getbalance - Get transparent balance", COLOR_RESULT);
addLine(" z_gettotalbalance - Get total balance", COLOR_RESULT);
addLine(" getblockcount - Get current block height", COLOR_RESULT);
addLine(" getpeerinfo - Get connected peers", COLOR_RESULT);
addLine(" setgenerate true/false [threads] - Mining on/off", COLOR_RESULT);
addLine(" getmininginfo - Get mining status", COLOR_RESULT);
addLine(" stop - Stop the daemon", COLOR_RESULT);
addLine(TR("console_common_rpc"), COLOR_INFO);
addLine(TR("console_help_getinfo"), COLOR_RESULT);
addLine(TR("console_help_getbalance"), COLOR_RESULT);
addLine(TR("console_help_gettotalbalance"), COLOR_RESULT);
addLine(TR("console_help_getblockcount"), COLOR_RESULT);
addLine(TR("console_help_getpeerinfo"), COLOR_RESULT);
addLine(TR("console_help_setgenerate"), COLOR_RESULT);
addLine(TR("console_help_getmininginfo"), COLOR_RESULT);
addLine(TR("console_help_stop"), COLOR_RESULT);
addLine("", COLOR_RESULT);
addLine("Click 'Commands' in the toolbar for full RPC reference", COLOR_INFO);
addLine("Use Tab for command completion, Up/Down for history", COLOR_INFO);
addLine(TR("console_click_commands"), COLOR_INFO);
addLine(TR("console_tab_completion"), COLOR_INFO);
return;
}
// Execute RPC command
if (!rpc || !rpc->isConnected()) {
addLine("Error: Not connected to daemon", COLOR_ERROR);
addLine(TR("console_not_connected"), COLOR_ERROR);
return;
}
@@ -1839,7 +1851,7 @@ void ConsoleTab::clear()
last_xmrig_output_size_ = 0;
}
// addLine() takes the lock itself, so call it outside the locked scope
addLine("Console cleared", COLOR_INFO);
addLine(TR("console_cleared"), COLOR_INFO);
}
} // namespace ui