feat: CJK font rendering, force quit confirmation, settings i18n

- Rebuild CJK font subset (1421 glyphs) and convert CFF→TTF for
  stb_truetype compatibility, fixing Chinese/Japanese/Korean rendering
- Add force quit confirmation dialog with cancel/confirm actions
- Show force quit tooltip immediately on hover (no delay)
- Translate hardcoded English strings in settings dropdowns
  (auto-lock timeouts, slider "Off" labels)
- Fix mojibake en-dashes in 7 translation JSON files
- Add helper scripts: build_cjk_subset, convert_cjk_to_ttf,
  check_font_coverage, fix_mojibake
This commit is contained in:
2026-04-12 10:32:58 -05:00
parent 821c54ba2b
commit fbdba1a001
28 changed files with 5471 additions and 4909 deletions
+40 -2
View File
@@ -2535,12 +2535,50 @@ void App::renderShutdownScreen()
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(forceLabel, btnSize)) {
DEBUG_LOGF("Force quit requested by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
force_quit_confirm_ = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("force_quit_warning"));
}
ImGui::PopStyleColor(3);
}
// Force Quit confirmation popup
if (force_quit_confirm_) {
ImGui::OpenPopup("##ForceQuitConfirm");
force_quit_confirm_ = false;
}
ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f);
ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted(TR("force_quit_confirm_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("force_quit_confirm_msg"));
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
ImGui::CloseCurrentPopup();
}
ImGui::PopStyleColor(3);
ImGui::EndPopup();
}
ImGui::Spacing();
ImGui::Spacing();
+1
View File
@@ -372,6 +372,7 @@ private:
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
+1 -1
View File
@@ -12,4 +12,4 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
+668 -668
View File
File diff suppressed because it is too large Load Diff
+704 -704
View File
File diff suppressed because it is too large Load Diff
+730 -730
View File
File diff suppressed because it is too large Load Diff
+624 -625
View File
File diff suppressed because it is too large Load Diff
+663 -663
View File
File diff suppressed because it is too large Load Diff
+929 -929
View File
File diff suppressed because it is too large Load Diff
+574 -574
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -300,7 +300,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
cjkCfg.GlyphRanges = cjkRanges;
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
if (mergeResult) {
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
g_noto_cjk_subset_size, name);
} else {
DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n",
name, g_noto_cjk_subset_size);
}
}
} else {
DEBUG_LOGF("Typography: Failed to load %s\n", name);
+5 -5
View File
@@ -713,7 +713,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -735,7 +735,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -1001,7 +1001,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -1019,7 +1019,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -1586,7 +1586,7 @@ void RenderSettingsPage(App* app) {
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
int timeout = app->settings()->getAutoLockTimeout();
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
int selTimeout = 0;
for (int i = 0; i < 6; i++) {
+11
View File
@@ -261,6 +261,10 @@ void I18n::loadBuiltinEnglish()
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
strings_["force_quit"] = "Force Quit";
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
strings_["force_quit_confirm_title"] = "Force Quit?";
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
strings_["force_quit_yes"] = "Force Quit";
strings_["reduce_motion"] = "Reduce Motion";
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
strings_["ago"] = "ago";
@@ -281,6 +285,13 @@ void I18n::loadBuiltinEnglish()
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
strings_["settings_configure_explorer"] = "Configure external block explorer links";
strings_["settings_auto_lock"] = "AUTO-LOCK";
strings_["timeout_off"] = "Off";
strings_["timeout_1min"] = "1 min";
strings_["timeout_5min"] = "5 min";
strings_["timeout_15min"] = "15 min";
strings_["timeout_30min"] = "30 min";
strings_["timeout_1hour"] = "1 hour";
strings_["slider_off"] = "Off";
strings_["settings_wallet_file_size"] = "Wallet file size: %s";
strings_["settings_wallet_location"] = "Wallet location: %s";
strings_["settings_rpc_note"] = "Note: Connection settings are usually auto-detected from DRAGONX.conf";