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:
dan_s
2026-03-11 00:40:50 -05:00
parent cc617dd5be
commit 96c27bb949
71 changed files with 43567 additions and 5267 deletions

View File

@@ -27,6 +27,14 @@
namespace dragonx {
namespace ui {
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
static std::string TrId(const char* tr_key, const char* id) {
std::string s = TR(tr_key);
s += "##";
s += id;
return s;
}
// Settings state - these get loaded from Settings on window open
static bool s_initialized = false;
static int s_language_index = 0;
@@ -141,17 +149,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
if (!material::BeginOverlayDialog("Settings", p_open, win.width, 0.94f)) {
if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) {
return;
}
if (ImGui::BeginTabBar("SettingsTabs")) {
// General settings tab
if (ImGui::BeginTabItem("General")) {
if (ImGui::BeginTabItem(TR("general"))) {
ImGui::Spacing();
// Skin/theme selection
ImGui::Text("Theme:");
ImGui::Text("%s", TR("theme"));
ImGui::SameLine(lbl.position);
// Active skin combo (populated from SkinManager)
@@ -172,7 +180,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
// Bundled themes header
ImGui::TextDisabled("Built-in");
ImGui::TextDisabled("%s", TR("settings_builtin"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
@@ -198,7 +206,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::TextDisabled("%s", TR("settings_custom"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
@@ -236,7 +244,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
// Show indicator if custom theme is active
if (active_is_custom) {
@@ -245,7 +253,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Custom theme active");
ImGui::SetTooltip("%s", TR("tt_custom_theme"));
}
}
@@ -257,14 +265,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
ImGui::SetTooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::Spacing();
// Language selection
ImGui::Text("Language:");
ImGui::Text("%s", TR("language"));
ImGui::SameLine(lbl.position);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
@@ -283,17 +291,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
std::advance(it, s_language_index);
i18n.loadLanguage(it->first);
}
ImGui::TextDisabled(" Note: Some text requires restart to update");
ImGui::TextDisabled(" %s", TR("settings_language_note"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Acrylic Effects settings
ImGui::Text("Visual Effects");
ImGui::Text("%s", TR("settings_visual_effects"));
ImGui::Spacing();
ImGui::Text("Acrylic Level:");
ImGui::Text("%s", TR("settings_acrylic_level"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
@@ -309,10 +317,10 @@ void RenderSettingsWindow(App* app, bool* p_open)
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
}
}
ImGui::TextDisabled(" Blur amount (0%% = off, 100%% = maximum)");
ImGui::TextDisabled(" %s", TR("tt_blur"));
ImGui::Spacing();
ImGui::Text("Noise Opacity:");
ImGui::Text("%s", TR("settings_noise_opacity"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
@@ -326,86 +334,86 @@ void RenderSettingsWindow(App* app, bool* p_open)
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
}
}
ImGui::TextDisabled(" Grain texture intensity (0%% = off, 100%% = maximum)");
ImGui::TextDisabled(" %s", TR("tt_noise"));
ImGui::Spacing();
// Accessibility: Reduced transparency
if (ImGui::Checkbox("Reduce transparency", &s_reduced_transparency)) {
if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
}
ImGui::TextDisabled(" Use solid colors instead of blur effects (accessibility)");
ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc"));
ImGui::Spacing();
if (ImGui::Checkbox("Simple background", &s_gradient_background)) {
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_gradient_background);
}
ImGui::TextDisabled(" Replace textured backgrounds with smooth gradients");
ImGui::TextDisabled(" %s", TR("settings_gradient_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Privacy settings
ImGui::Text("Privacy");
ImGui::Text("%s", TR("settings_privacy"));
ImGui::Spacing();
ImGui::Checkbox("Save shielded transaction history locally", &s_save_ztxs);
ImGui::TextDisabled(" Stores z-addr transactions in a local file for viewing");
ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs);
ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc"));
ImGui::Spacing();
ImGui::Checkbox("Auto-shield transparent funds", &s_auto_shield);
ImGui::TextDisabled(" Automatically move transparent funds to shielded addresses");
ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield);
ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc"));
ImGui::Spacing();
ImGui::Checkbox("Use Tor for network connections", &s_use_tor);
ImGui::TextDisabled(" Route all connections through Tor for enhanced privacy");
ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &s_use_tor);
ImGui::TextDisabled(" %s", TR("settings_tor_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Other settings
ImGui::Text("Other");
ImGui::Text("%s", TR("settings_other"));
ImGui::Spacing();
ImGui::Checkbox("Allow custom transaction fees", &s_allow_custom_fees);
ImGui::Checkbox("Fetch price data from CoinGecko", &s_fetch_prices);
ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees);
ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices);
ImGui::EndTabItem();
}
// Connection settings tab
if (ImGui::BeginTabItem("Connection")) {
if (ImGui::BeginTabItem(TR("settings_connection"))) {
ImGui::Spacing();
ImGui::Text("RPC Connection");
ImGui::TextDisabled("Configure connection to dragonxd daemon");
ImGui::Text("%s", TR("settings_rpc_connection"));
ImGui::TextDisabled("%s", TR("settings_configure_rpc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Host:");
ImGui::Text("%s", TR("rpc_host"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
ImGui::Text("Port:");
ImGui::Text("%s", TR("rpc_port"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(portInput.width);
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
ImGui::Spacing();
ImGui::Text("Username:");
ImGui::Text("%s", TR("rpc_user"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
ImGui::Text("Password:");
ImGui::Text("%s", TR("rpc_pass"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
@@ -415,11 +423,11 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Note: Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::TextDisabled("%s", TR("settings_rpc_note"));
ImGui::Spacing();
if (material::StyledButton("Test Connection", ImVec2(0,0), S.resolveFont("button"))) {
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
@@ -439,15 +447,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
// Wallet tab
if (ImGui::BeginTabItem("Wallet")) {
if (ImGui::BeginTabItem(TR("wallet"))) {
ImGui::Spacing();
ImGui::Text("Wallet Maintenance");
ImGui::Text("%s", TR("settings_wallet_maintenance"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (material::StyledButton("Rescan Blockchain", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (app->rpc()) {
// Start rescan from block 0
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
@@ -465,45 +473,41 @@ void RenderSettingsWindow(App* app, bool* p_open)
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::TextDisabled(" Rescan blockchain for missing transactions");
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
ImGui::Spacing();
static bool s_confirm_clear_ztx = false;
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
s_confirm_clear_ztx = true;
}
ImGui::TextDisabled(" Delete locally stored shielded transaction data");
ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc"));
// Confirmation dialog
if (s_confirm_clear_ztx) {
if (material::BeginOverlayDialog("Confirm Clear Z-Tx History", &s_confirm_clear_ztx, 480.0f, 0.94f)) {
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(material::Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Warning");
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped(
"Clearing z-transaction history may cause your shielded balance to show as 0 "
"until a wallet rescan is performed.");
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
ImGui::TextWrapped(
"If this happens, you will need to re-import your z-address private keys with "
"rescan enabled to recover your balance.");
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
s_confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Clear Anyway", ImVec2(btnW, 40))) {
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
@@ -521,7 +525,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Wallet Info");
ImGui::Text("%s", TR("settings_wallet_info"));
ImGui::Spacing();
// Get actual wallet size
@@ -529,35 +533,35 @@ void RenderSettingsWindow(App* app, bool* p_open)
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::Text("Wallet file size: %s", size_str.c_str());
ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str());
} else {
ImGui::TextDisabled("Wallet file not found");
ImGui::TextDisabled("%s", TR("settings_wallet_not_found"));
}
ImGui::Text("Wallet location: %s", wallet_path.c_str());
ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str());
ImGui::EndTabItem();
}
// Explorer tab
if (ImGui::BeginTabItem("Explorer")) {
if (ImGui::BeginTabItem(TR("explorer"))) {
ImGui::Spacing();
ImGui::Text("Block Explorer URLs");
ImGui::TextDisabled("Configure external block explorer links");
ImGui::Text("%s", TR("settings_block_explorer_urls"));
ImGui::TextDisabled("%s", TR("settings_configure_explorer"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Transaction URL:");
ImGui::Text("%s", TR("transaction_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
ImGui::Text("Address URL:");
ImGui::Text("%s", TR("address_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
ImGui::Spacing();
ImGui::TextDisabled("URLs should include a trailing slash. The txid/address will be appended.");
ImGui::TextDisabled("%s", TR("settings_explorer_hint"));
ImGui::EndTabItem();
}
@@ -570,13 +574,13 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Spacing();
// Save/Cancel buttons
if (material::StyledButton("Save", ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
*p_open = false;
}
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
// Reload settings to revert changes
loadSettingsToUI(app->settings());
// Revert skin to what was active when settings opened