From 4ade751caf9fd39c1010ada439c44a37ab4d42f6 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sun, 17 Aug 2025 15:50:27 +0200 Subject: [PATCH] feat: Added support for choosing the system-native language --- .../include/hex/api/localization_manager.hpp | 3 +- lib/libimhex/include/hex/helpers/utils.hpp | 1 + .../source/api/localization_manager.cpp | 18 ++++++- lib/libimhex/source/helpers/utils.cpp | 50 +++++++++++++++++++ .../source/content/out_of_box_experience.cpp | 21 ++++++-- .../source/content/settings_entries.cpp | 25 +++++++--- 6 files changed, 104 insertions(+), 14 deletions(-) diff --git a/lib/libimhex/include/hex/api/localization_manager.hpp b/lib/libimhex/include/hex/api/localization_manager.hpp index a9f5fcab0..a55061cbe 100644 --- a/lib/libimhex/include/hex/api/localization_manager.hpp +++ b/lib/libimhex/include/hex/api/localization_manager.hpp @@ -26,8 +26,6 @@ EXPORT_MODULE namespace hex { struct LanguageDefinition { LanguageId id; std::string name, nativeName; - std::string flag; - std::string filePath; LanguageId fallbackLanguageId; std::vector languageFilePaths; @@ -38,6 +36,7 @@ EXPORT_MODULE namespace hex { [[nodiscard]] const LanguageId& getSelectedLanguageId(); [[nodiscard]] const std::string& get(const LanguageId& languageId, const UnlocalizedString &unlocalizedString); [[nodiscard]] const std::map& getLanguageDefinitions(); + [[nodiscard]] const LanguageDefinition& getLanguageDefinition(const LanguageId &languageId); } diff --git a/lib/libimhex/include/hex/helpers/utils.hpp b/lib/libimhex/include/hex/helpers/utils.hpp index 37d9ae868..bec2f428c 100644 --- a/lib/libimhex/include/hex/helpers/utils.hpp +++ b/lib/libimhex/include/hex/helpers/utils.hpp @@ -384,4 +384,5 @@ namespace hex { [[nodiscard]] std::optional blendColors(const std::optional &a, const std::optional &b); std::optional parseTime(std::string_view format, const std::string &timeString); + std::optional getOSLanguage(); } diff --git a/lib/libimhex/source/api/localization_manager.cpp b/lib/libimhex/source/api/localization_manager.cpp index 399b233be..4304eae54 100644 --- a/lib/libimhex/source/api/localization_manager.cpp +++ b/lib/libimhex/source/api/localization_manager.cpp @@ -10,6 +10,8 @@ namespace hex { namespace LocalizationManager { + constexpr static auto FallbackLanguageId = "en-US"; + namespace { AutoReset> s_languageDefinitions; @@ -29,6 +31,10 @@ namespace hex { auto &definition = (*s_languageDefinitions)[item["code"].get()]; + if (definition.id.empty()) { + definition.id = item["code"].get(); + } + if (definition.name.empty() && item.contains("name")) { definition.name = item["name"].get(); } @@ -80,7 +86,6 @@ namespace hex { if (const auto it = s_languageDefinitions->find(languageId); it == s_languageDefinitions->end()) { log::error("No language definition found for language: {}", languageId); - constexpr static auto FallbackLanguageId = "en-US"; if (languageId != FallbackLanguageId) populateLocalization(FallbackLanguageId, localizations); } else { @@ -107,6 +112,12 @@ namespace hex { } void setLanguage(const LanguageId &languageId) { + if (languageId == "native") { + setLanguage(hex::getOSLanguage().value_or(FallbackLanguageId)); + s_selectedLanguageId = languageId; + return; + } + if (*s_selectedLanguageId == languageId) return; @@ -139,6 +150,11 @@ namespace hex { return *s_languageDefinitions; } + const LanguageDefinition& getLanguageDefinition(const LanguageId &languageId) { + const auto bestMatch = findBestLanguageMatch(languageId); + return (*s_languageDefinitions)[bestMatch]; + } + } Lang::Lang(const char *unlocalizedString) : m_entryHash(LangConst::hash(unlocalizedString)), m_unlocalizedString(unlocalizedString) { } diff --git a/lib/libimhex/source/helpers/utils.cpp b/lib/libimhex/source/helpers/utils.cpp index 3bdd11833..2d9634a9b 100644 --- a/lib/libimhex/source/helpers/utils.cpp +++ b/lib/libimhex/source/helpers/utils.cpp @@ -787,6 +787,56 @@ namespace hex { return std::chrono::system_clock::from_time_t(std::mktime(&time)); } + std::optional getOSLanguage() { + const static auto osLanguage = [] -> std::optional { + #if defined(OS_WINDOWS) + const auto langId = ::GetUserDefaultUILanguage(); + std::array localeName; + if (::LCIDToLocaleName(MAKELCID(langId, SORT_DEFAULT), localeName.data(), localeName.size(), 0) > 0) { + return utf16ToUtf8(localeName.data()); + } + + return std::nullopt; + #elif defined(OS_MACOS) + const auto langs = CFLocaleCopyPreferredLanguages(); + if (langs == nullptr || CFArrayGetCount(langs) == 0) + return std::nullopt; + + ON_SCOPE_EXIT { CFRelease(langs); }; + + const auto lang = (CFStringRef)CFArrayGetValueAtIndex(langs, 0); + std::array buffer; + if (CFStringGetCString(lang, buffer.data(), buffer.size(), kCFStringEncodingUTF8)) { + return std::string(buffer.data()); + } + + return std::nullopt; + #elif defined(OS_LINUX) + auto lang = getEnvironmentVariable("LC_ALL"); + if (!lang.has_value()) lang = getEnvironmentVariable("LC_MESSAGES"); + if (!lang.has_value()) lang = getEnvironmentVariable("LANG"); + + if (lang.has_value() && !lang->empty() && *lang != "C" && *lang != "C.UTF-8") { + auto parts = wolv::util::splitString(*lang, "."); + if (parts.size() > 0) + return parts[0]; + else + return *lang; + } + + return std::nullopt; + #elif defined(OS_WEB) + return toLower(EM_ASM_INT({ + return (int)navigator.language.length > 0 ? navigator.language : navigator.languages[0]; + })); + #else + return std::nullopt; + #endif + }(); + + return osLanguage; + } + extern "C" void macOSCloseButtonPressed() { EventCloseButtonPressed::post(); } diff --git a/plugins/builtin/source/content/out_of_box_experience.cpp b/plugins/builtin/source/content/out_of_box_experience.cpp index 166dcac33..2384bd7cb 100644 --- a/plugins/builtin/source/content/out_of_box_experience.cpp +++ b/plugins/builtin/source/content/out_of_box_experience.cpp @@ -192,7 +192,7 @@ namespace hex::plugin::builtin { } // Continue button - const auto buttonSize = scaled({ 100, 50 }); + const auto buttonSize = scaled({ 130, 50 }); ImGui::SetCursorPos(ImHexApi::System::getMainWindowSize() - buttonSize - scaled({ 10, 10 })); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, buttonFadeIn); if (ImGuiExt::DimmedButton(fmt::format("{} {}", "hex.ui.common.continue"_lang, ICON_VS_ARROW_RIGHT).c_str(), buttonSize)) @@ -236,7 +236,7 @@ namespace hex::plugin::builtin { // Draw globe image const auto imageSize = s_compassTexture->getSize() / (1.5F * (1.0F / ImHexApi::System::getGlobalScale())); - ImGui::SetCursorPos((ImGui::GetWindowSize() / 2 - imageSize / 2) - ImVec2(0, 50_scaled)); + ImGui::SetCursorPos((ImGui::GetWindowSize() / 2 - imageSize / 2) - ImVec2(0, 100_scaled)); ImGui::Image(*s_globeTexture, imageSize); ImGui::NewLine(); @@ -249,7 +249,9 @@ namespace hex::plugin::builtin { const auto availableSize = ImGui::GetContentRegionAvail(); if (ImGui::BeginChild("##language_text", ImVec2(availableSize.x, 30_scaled))) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_Text, textFadeIn - textFadeOut)); + fonts::Default().push(1.2); ImGuiExt::TextFormattedCentered("{}", LocalizationManager::get(currLanguage->first, "hex.builtin.setting.interface.language")); + fonts::Default().pop(); ImGui::PopStyleColor(); } ImGui::EndChild(); @@ -260,6 +262,17 @@ namespace hex::plugin::builtin { ImGui::SetCursorPosX(availableSize.x / 3); if (ImGuiExt::BeginSubWindow(ICON_VS_GLOBE, nullptr, ImVec2(availableSize.x / 3, availableSize.y - ImGui::GetTextLineHeightWithSpacing() * 3))) { if (ImGui::BeginListBox("##language", ImGui::GetContentRegionAvail())) { + { + const auto osLanguage = hex::getOSLanguage(); + if (osLanguage.has_value()) { + const auto &languageDefinition = LocalizationManager::getLanguageDefinition(osLanguage.value()); + if (ImGui::Selectable(fmt::format("Native ({})", languageDefinition.nativeName).c_str(), LocalizationManager::getSelectedLanguageId() == "native")) { + LocalizationManager::setLanguage("native"); + } + ImGui::Separator(); + } + } + for (const auto &[langId, definition] : LocalizationManager::getLanguageDefinitions()) { if (ImGui::Selectable(definition.name.c_str(), langId == LocalizationManager::getSelectedLanguageId())) { LocalizationManager::setLanguage(langId); @@ -271,7 +284,7 @@ namespace hex::plugin::builtin { ImGuiExt::EndSubWindow(); // Continue button - const auto buttonSize = scaled({ 100, 50 }); + const auto buttonSize = scaled({ 130, 50 }); ImGui::SetCursorPos(ImHexApi::System::getMainWindowSize() - buttonSize - scaled({ 10, 10 })); if (ImGuiExt::DimmedButton(fmt::format("{} {}", "hex.ui.common.continue"_lang, ICON_VS_ARROW_RIGHT).c_str(), buttonSize)) page += 1; @@ -397,7 +410,7 @@ namespace hex::plugin::builtin { ImGuiExt::TextFormattedCentered("hex.builtin.oobe.tutorial_question"_lang); // Draw no button - const auto buttonSize = scaled({ 100, 50 }); + const auto buttonSize = scaled({ 130, 50 }); ImGui::SetCursorPos(ImHexApi::System::getMainWindowSize() - ImVec2(buttonSize.x * 2 + 20, buttonSize.y + 10)); if (ImGuiExt::DimmedButton("hex.ui.common.no"_lang, buttonSize)) { oobeDone = true; diff --git a/plugins/builtin/source/content/settings_entries.cpp b/plugins/builtin/source/content/settings_entries.cpp index dab50b11f..ebb30a25b 100644 --- a/plugins/builtin/source/content/settings_entries.cpp +++ b/plugins/builtin/source/content/settings_entries.cpp @@ -830,16 +830,27 @@ namespace hex::plugin::builtin { ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.style", "hex.builtin.setting.interface.show_header_command_palette", true); ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.style", "hex.builtin.setting.interface.display_shortcut_highlights", true); - std::vector languageNames; - std::vector languageCodes; + { + std::vector languageNames; + std::vector languageCodes; - for (auto &[languageCode, definition] : LocalizationManager::getLanguageDefinitions()) { - languageNames.emplace_back(fmt::format("{} ({})", definition.nativeName, definition.name)); - languageCodes.emplace_back(languageCode); + { + const auto osLanguage = hex::getOSLanguage(); + if (osLanguage.has_value()) { + const auto &languageDefinition = LocalizationManager::getLanguageDefinition(osLanguage.value()); + languageNames.emplace_back(fmt::format("Native ({})", languageDefinition.nativeName)); + languageCodes.emplace_back("native"); + } + } + + for (auto &[languageCode, definition] : LocalizationManager::getLanguageDefinitions()) { + languageNames.emplace_back(fmt::format("{} ({})", definition.nativeName, definition.name)); + languageCodes.emplace_back(languageCode); + } + + ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", "hex.builtin.setting.interface.language", languageNames, languageCodes, "en-US"); } - ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", "hex.builtin.setting.interface.language", languageNames, languageCodes, "en-US"); - ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", "hex.builtin.setting.interface.wiki_explain_language", "en"); ContentRegistry::Settings::add("hex.builtin.setting.interface", "hex.builtin.setting.interface.window", "hex.builtin.setting.interface.fps");