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

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();

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)

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");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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);

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++) {

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";