feat: Added support for choosing the system-native language

This commit is contained in:
WerWolv 2025-08-17 15:50:27 +02:00
parent 9f24b35b1f
commit 4ade751caf
6 changed files with 104 additions and 14 deletions

View File

@ -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<PathEntry> 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<LanguageId, LanguageDefinition>& getLanguageDefinitions();
[[nodiscard]] const LanguageDefinition& getLanguageDefinition(const LanguageId &languageId);
}

View File

@ -384,4 +384,5 @@ namespace hex {
[[nodiscard]] std::optional<ImColor> blendColors(const std::optional<ImColor> &a, const std::optional<ImColor> &b);
std::optional<std::chrono::system_clock::time_point> parseTime(std::string_view format, const std::string &timeString);
std::optional<std::string> getOSLanguage();
}

View File

@ -10,6 +10,8 @@ namespace hex {
namespace LocalizationManager {
constexpr static auto FallbackLanguageId = "en-US";
namespace {
AutoReset<std::map<LanguageId, LanguageDefinition>> s_languageDefinitions;
@ -29,6 +31,10 @@ namespace hex {
auto &definition = (*s_languageDefinitions)[item["code"].get<std::string>()];
if (definition.id.empty()) {
definition.id = item["code"].get<std::string>();
}
if (definition.name.empty() && item.contains("name")) {
definition.name = item["name"].get<std::string>();
}
@ -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) { }

View File

@ -787,6 +787,56 @@ namespace hex {
return std::chrono::system_clock::from_time_t(std::mktime(&time));
}
std::optional<std::string> getOSLanguage() {
const static auto osLanguage = [] -> std::optional<std::string> {
#if defined(OS_WINDOWS)
const auto langId = ::GetUserDefaultUILanguage();
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> 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<char, 256> 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();
}

View File

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

View File

@ -830,15 +830,26 @@ namespace hex::plugin::builtin {
ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.interface", "hex.builtin.setting.interface.style", "hex.builtin.setting.interface.show_header_command_palette", true);
ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.interface", "hex.builtin.setting.interface.style", "hex.builtin.setting.interface.display_shortcut_highlights", true);
{
std::vector<std::string> languageNames;
std::vector<nlohmann::json> languageCodes;
{
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<Widgets::DropDown>("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", "hex.builtin.setting.interface.language", languageNames, languageCodes, "en-US");
}
ContentRegistry::Settings::add<Widgets::TextBox>("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", "hex.builtin.setting.interface.wiki_explain_language", "en");
ContentRegistry::Settings::add<FPSWidget>("hex.builtin.setting.interface", "hex.builtin.setting.interface.window", "hex.builtin.setting.interface.fps");