mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-28 15:57:03 -05:00
<!-- Please provide as much information as possible about what your PR aims to do. PRs with no description will most likely be closed until more information is provided. If you're planing on changing fundamental behaviour or add big new features, please open a GitHub Issue first before starting to work on it. If it's not something big and you still want to contact us about it, feel free to do so ! --> ### Problem description <!-- Describe the bug that you fixed/feature request that you implemented, or link to an existing issue describing it --> ### Implementation description <!-- Explain what you did to correct the problem --> ### Screenshots <!-- If your change is visual, take a screenshot showing it. Ideally, make before/after sceenshots --> ### Additional things <!-- Anything else you would like to say -->
253 lines
9.1 KiB
C++
253 lines
9.1 KiB
C++
#include <hex/api/localization_manager.hpp>
|
|
|
|
#include <hex/helpers/auto_reset.hpp>
|
|
#include <hex/helpers/logger.hpp>
|
|
#include <hex/helpers/utils.hpp>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <mutex>
|
|
#include <hex/helpers/debugging.hpp>
|
|
|
|
namespace hex {
|
|
|
|
namespace LocalizationManager {
|
|
|
|
constexpr static auto FallbackLanguageId = "en-US";
|
|
|
|
namespace {
|
|
|
|
AutoReset<std::map<LanguageId, LanguageDefinition>> s_languageDefinitions;
|
|
AutoReset<std::unordered_map<std::size_t, std::string>> s_localizations;
|
|
AutoReset<LanguageId> s_selectedLanguageId;
|
|
|
|
}
|
|
|
|
void addLanguages(const std::string_view &languageList, std::function<std::string_view(const std::string &path)> callback) {
|
|
const auto json = nlohmann::json::parse(languageList);
|
|
|
|
for (const auto &item : json) {
|
|
if (!item.contains("code") || !item.contains("path")) {
|
|
log::error("Invalid language definition: {}", item.dump(4));
|
|
continue;
|
|
}
|
|
|
|
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>();
|
|
}
|
|
|
|
if (definition.nativeName.empty() && item.contains("native_name")) {
|
|
definition.nativeName = item["native_name"].get<std::string>();
|
|
}
|
|
|
|
if (definition.fallbackLanguageId.empty() && item.contains("fallback")) {
|
|
definition.fallbackLanguageId = item["fallback"].get<std::string>();
|
|
}
|
|
|
|
if (item.contains("hidden") && item["hidden"].get<bool>()) {
|
|
definition.hidden = true;
|
|
}
|
|
|
|
const auto path = item["path"].get<std::string>();
|
|
|
|
definition.languageFilePaths.emplace_back(PathEntry{ .path=path, .callback=callback });
|
|
}
|
|
}
|
|
|
|
static LanguageId findBestLanguageMatch(LanguageId languageId) {
|
|
if (s_languageDefinitions->contains(languageId))
|
|
return languageId;
|
|
|
|
if (const auto pos = languageId.find('_'); pos != std::string::npos) {
|
|
// Turn language Ids like "en_US" into "en-US"
|
|
languageId[pos] = '-';
|
|
}
|
|
|
|
if (const auto pos = languageId.find('-'); pos != std::string::npos) {
|
|
// Try to find a match with the language code without region
|
|
languageId = languageId.substr(0, pos);
|
|
|
|
for (const auto &definition : *s_languageDefinitions) {
|
|
if (definition.first.starts_with(languageId) || definition.first.starts_with(toLower(languageId))) {
|
|
return definition.first;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to English if no better match was found
|
|
return "en-US";
|
|
}
|
|
|
|
static void populateLocalization(LanguageId languageId, std::unordered_map<std::size_t, std::string> &localizations) {
|
|
if (languageId.empty())
|
|
return;
|
|
|
|
languageId = findBestLanguageMatch(languageId);
|
|
|
|
if (const auto it = s_languageDefinitions->find(languageId); it == s_languageDefinitions->end()) {
|
|
log::error("No language definition found for language: {}", languageId);
|
|
|
|
if (languageId != FallbackLanguageId)
|
|
populateLocalization(FallbackLanguageId, localizations);
|
|
} else {
|
|
const auto &definition = it->second;
|
|
for (const auto &path : definition.languageFilePaths) {
|
|
try {
|
|
const auto translation = path.callback(path.path);
|
|
const auto json = nlohmann::json::parse(translation);
|
|
|
|
for (const auto &entry : json.items()) {
|
|
auto value = entry.value().get<std::string>();
|
|
|
|
// Skip empty values
|
|
if (value.empty())
|
|
continue;
|
|
|
|
// Handle references to files
|
|
if (value.starts_with("#@")) {
|
|
try {
|
|
value = path.callback(value.substr(2));
|
|
} catch (std::exception &e) {
|
|
log::error("Failed to load localization file reference '{}': {}", entry.key(), e.what());
|
|
continue;
|
|
}
|
|
}
|
|
|
|
localizations.try_emplace(LangConst::hash(entry.key()), std::move(value));
|
|
}
|
|
} catch (std::exception &e) {
|
|
log::error("Failed to load localization file '{}': {}", path.path, e.what());
|
|
}
|
|
}
|
|
|
|
populateLocalization(definition.fallbackLanguageId, localizations);
|
|
}
|
|
}
|
|
|
|
void setLanguage(const LanguageId &languageId) {
|
|
if (languageId == "native") {
|
|
setLanguage(hex::getOSLanguage().value_or(FallbackLanguageId));
|
|
s_selectedLanguageId = languageId;
|
|
return;
|
|
}
|
|
|
|
if (*s_selectedLanguageId == languageId)
|
|
return;
|
|
|
|
s_localizations->clear();
|
|
s_selectedLanguageId = languageId;
|
|
|
|
populateLocalization(languageId, s_localizations);
|
|
}
|
|
|
|
[[nodiscard]] const std::string& getSelectedLanguageId() {
|
|
return *s_selectedLanguageId;
|
|
}
|
|
|
|
[[nodiscard]] const std::string& get(const LanguageId &languageId, const UnlocalizedString &unlocalizedString) {
|
|
static AutoReset<LanguageId> currentLanguageId;
|
|
static AutoReset<std::unordered_map<std::size_t, std::string>> loadedLocalization;
|
|
static std::mutex mutex;
|
|
|
|
std::scoped_lock lock(mutex);
|
|
if (*currentLanguageId != languageId) {
|
|
currentLanguageId = languageId;
|
|
loadedLocalization->clear();
|
|
populateLocalization(languageId, *loadedLocalization);
|
|
}
|
|
|
|
return (*loadedLocalization)[LangConst::hash(unlocalizedString.get())];
|
|
}
|
|
|
|
const std::map<std::string, LanguageDefinition>& getLanguageDefinitions() {
|
|
return *s_languageDefinitions;
|
|
}
|
|
|
|
const LanguageDefinition& getLanguageDefinition(const LanguageId &languageId) {
|
|
const auto bestMatch = findBestLanguageMatch(languageId);
|
|
const auto &result = (*s_languageDefinitions)[bestMatch];
|
|
|
|
if (!dbg::debugModeEnabled()) {
|
|
if (result.hidden)
|
|
return getLanguageDefinition(result.fallbackLanguageId);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
static AutoReset<std::map<std::size_t, std::string>> s_unlocalizedNames;
|
|
|
|
Lang::Lang(std::string_view unlocalizedString) : m_entryHash(LangConst::hash(unlocalizedString)) {
|
|
if (!s_unlocalizedNames->contains(m_entryHash)) [[unlikely]] {
|
|
s_unlocalizedNames->emplace(m_entryHash, unlocalizedString);
|
|
}
|
|
}
|
|
Lang::Lang(const char *unlocalizedString) : Lang(std::string_view(unlocalizedString)) { }
|
|
Lang::Lang(const std::string &unlocalizedString) : Lang(std::string_view(unlocalizedString)) { }
|
|
Lang::Lang(const LangConst &localizedString) : m_entryHash(localizedString.m_entryHash) {
|
|
if (!s_unlocalizedNames->contains(m_entryHash)) [[unlikely]] {
|
|
s_unlocalizedNames->emplace(m_entryHash, localizedString.m_unlocalizedString);
|
|
}
|
|
}
|
|
Lang::Lang(const UnlocalizedString &unlocalizedString) : Lang(unlocalizedString.get()) { }
|
|
|
|
Lang::operator std::string() const {
|
|
return get();
|
|
}
|
|
|
|
Lang::operator std::string_view() const {
|
|
return get();
|
|
}
|
|
|
|
Lang::operator const char *() const {
|
|
return get();
|
|
}
|
|
|
|
const char *Lang::get() const {
|
|
const auto &lang = *LocalizationManager::s_localizations;
|
|
|
|
const auto it = lang.find(m_entryHash);
|
|
if (it == lang.end()) {
|
|
if (auto unlocalizedIt = s_unlocalizedNames->find(m_entryHash); unlocalizedIt != s_unlocalizedNames->end()) {
|
|
return unlocalizedIt->second.c_str();
|
|
} else {
|
|
return "<unlocalized>";
|
|
}
|
|
} else {
|
|
return it->second.c_str();
|
|
}
|
|
}
|
|
|
|
LangConst::operator std::string() const {
|
|
return get();
|
|
}
|
|
|
|
LangConst::operator std::string_view() const {
|
|
return get();
|
|
}
|
|
|
|
LangConst::operator const char *() const {
|
|
return get();
|
|
}
|
|
|
|
const char *LangConst::get() const {
|
|
const auto &lang = *LocalizationManager::s_localizations;
|
|
|
|
const auto it = lang.find(m_entryHash);
|
|
if (it == lang.end()) {
|
|
return m_unlocalizedString;
|
|
} else {
|
|
return it->second.c_str();
|
|
}
|
|
}
|
|
|
|
}
|