diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index 8d65f770b1..4ec6945915 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -658,6 +658,18 @@ void Host::VSyncOnCPUThread() GSRunner::PumpPlatformMessages(); } +s32 Host::Internal::GetTranslatedStringImpl( + const std::string_view& context, const std::string_view& msg, char* tbuf, size_t tbuf_space) +{ + if (msg.size() > tbuf_space) + return -1; + else if (msg.empty()) + return 0; + + std::memcpy(tbuf, msg.data(), msg.size()); + return static_cast(msg.size()); +} + ////////////////////////////////////////////////////////////////////////// // Platform specific code ////////////////////////////////////////////////////////////////////////// diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 16e8d371e8..897d0f2a58 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -1179,6 +1179,23 @@ void Host::SetFullscreen(bool enabled) g_emu_thread->setFullscreen(enabled, true); } +s32 Host::Internal::GetTranslatedStringImpl( + const std::string_view& context, const std::string_view& msg, char* tbuf, size_t tbuf_space) +{ + // This is really awful. Thankfully we're caching the results... + const std::string temp_context(context); + const std::string temp_msg(msg); + const QString translated_msg = qApp->translate(temp_context.c_str(), temp_msg.c_str()); + const QByteArray translated_utf8 = translated_msg.toUtf8(); + const size_t translated_size = translated_utf8.size(); + if (translated_size > tbuf_space) + return -1; + else if (translated_size > 0) + std::memcpy(tbuf, translated_utf8.constData(), translated_size); + + return static_cast(translated_size); +} + alignas(16) static SysMtgsThread s_mtgs_thread; SysMtgsThread& GetMTGS() diff --git a/pcsx2/Host.cpp b/pcsx2/Host.cpp index 2cb3a2f1a7..a9ce81924c 100644 --- a/pcsx2/Host.cpp +++ b/pcsx2/Host.cpp @@ -26,13 +26,122 @@ #include "common/Assertions.h" #include "common/CrashHandler.h" #include "common/FileSystem.h" +#include "common/HeterogeneousContainers.h" #include "common/Path.h" #include "common/StringUtil.h" #include +#include -static std::mutex s_settings_mutex; -static LayeredSettingsInterface s_layered_settings_interface; +namespace Host +{ + static std::pair LookupTranslationString( + const std::string_view& context, const std::string_view& msg); + + static std::mutex s_settings_mutex; + static LayeredSettingsInterface s_layered_settings_interface; + + static constexpr u32 TRANSLATION_STRING_CACHE_SIZE = 4 * 1024 * 1024; + using TranslationStringMap = UnorderedStringMap>; + using TranslationStringContextMap = UnorderedStringMap; + static std::shared_mutex s_translation_string_mutex; + static TranslationStringContextMap s_translation_string_map; + static std::vector s_translation_string_cache; + static u32 s_translation_string_cache_pos; +} // namespace Host + +std::pair Host::LookupTranslationString(const std::string_view& context, const std::string_view& msg) +{ + // TODO: TranslatableString, compile-time hashing. + + TranslationStringContextMap::iterator ctx_it; + TranslationStringMap::iterator msg_it; + std::pair ret; + s32 len; + + // Shouldn't happen, but just in case someone tries to translate an empty string. + if (unlikely(msg.empty())) + { + ret.first = &s_translation_string_cache[0]; + ret.second = 0; + return ret; + } + + s_translation_string_mutex.lock_shared(); + ctx_it = UnorderedStringMapFind(s_translation_string_map, context); + + if (unlikely(ctx_it == s_translation_string_map.end())) + goto add_string; + + msg_it = UnorderedStringMapFind(ctx_it->second, msg); + if (unlikely(msg_it == ctx_it->second.end())) + goto add_string; + + ret.first = &s_translation_string_cache[msg_it->second.first]; + ret.second = msg_it->second.second; + s_translation_string_mutex.unlock_shared(); + return ret; + +add_string: + s_translation_string_mutex.unlock_shared(); + s_translation_string_mutex.lock(); + + if (unlikely(s_translation_string_cache.empty())) + { + // First element is always an empty string. + s_translation_string_cache.resize(TRANSLATION_STRING_CACHE_SIZE); + s_translation_string_cache[0] = '\0'; + s_translation_string_cache_pos = 0; + } + + if ((len = Internal::GetTranslatedStringImpl(context, msg, + &s_translation_string_cache[s_translation_string_cache_pos], + TRANSLATION_STRING_CACHE_SIZE - 1 - s_translation_string_cache_pos)) < 0) + { + Console.Error("WARNING: Clearing translation string cache, it might need to be larger."); + s_translation_string_cache_pos = 0; + if ((len = Internal::GetTranslatedStringImpl(context, msg, + &s_translation_string_cache[s_translation_string_cache_pos], + TRANSLATION_STRING_CACHE_SIZE - 1 - s_translation_string_cache_pos)) < 0) + { + pxFailRel("Failed to get translated string after clearing cache."); + len = 0; + } + } + + // New context? + if (ctx_it == s_translation_string_map.end()) + ctx_it = s_translation_string_map.emplace(context, TranslationStringMap()).first; + + // Impl doesn't null terminate, we need that for C strings. + // TODO: do we want to consider aligning the buffer? + const u32 insert_pos = s_translation_string_cache_pos; + s_translation_string_cache[insert_pos + static_cast(len)] = 0; + + ctx_it->second.emplace(msg, std::pair(insert_pos, static_cast(len))); + s_translation_string_cache_pos = insert_pos + static_cast(len) + 1; + + ret.first = &s_translation_string_cache[insert_pos]; + ret.second = static_cast(len); + s_translation_string_mutex.unlock(); + return ret; +} + +const char* Host::TranslateToCString(const std::string_view& context, const std::string_view& msg) +{ + return LookupTranslationString(context, msg).first; +} + +std::string_view Host::TranslateToStringView(const std::string_view& context, const std::string_view& msg) +{ + const auto mp = LookupTranslationString(context, msg); + return std::string_view(mp.first, mp.second); +} + +std::string Host::TranslateToString(const std::string_view& context, const std::string_view& msg) +{ + return std::string(TranslateToStringView(context, msg)); +} void Host::ReportFormattedErrorAsync(const std::string_view& title, const char* format, ...) { diff --git a/pcsx2/Host.h b/pcsx2/Host.h index 070bc0ffa8..8c323791e3 100644 --- a/pcsx2/Host.h +++ b/pcsx2/Host.h @@ -17,6 +17,8 @@ #include "common/Pcsx2Defs.h" +#include "fmt/format.h" + #include #include #include @@ -48,6 +50,24 @@ namespace Host /// Returns the modified time of a resource. std::optional GetResourceFileTimestamp(const char* filename); + /// Returns a localized version of the specified string within the specified context. + /// The pointer is guaranteed to be valid until the next language change. + const char* TranslateToCString(const std::string_view& context, const std::string_view& msg); + + /// Returns a localized version of the specified string within the specified context. + /// The view is guaranteed to be valid until the next language change. + /// NOTE: When passing this to fmt, positional arguments should be used in the base string, as + /// not all locales follow the same word ordering. + std::string_view TranslateToStringView(const std::string_view& context, const std::string_view& msg); + + /// Returns a localized version of the specified string within the specified context. + std::string TranslateToString(const std::string_view& context, const std::string_view& msg); + + /// Returns a localized version of the specified string, after formatting. + + template + std::string TranslateAndFmt(const std::string_view& context, const std::string_view& format, T&&... args); + /// Adds OSD messages, duration is in seconds. void AddOSDMessage(std::string message, float duration = 2.0f); void AddKeyedOSDMessage(std::string key, std::string message, float duration = 2.0f); @@ -155,5 +175,16 @@ namespace Host /// Sets the input profile settings layer. Called by VMManager when the game changes. void SetInputSettingsLayer(SettingsInterface* sif); + + /// Implementation to retrieve a translated string. + s32 GetTranslatedStringImpl(const std::string_view& context, const std::string_view& msg, char* tbuf, size_t tbuf_space); } // namespace Internal } // namespace Host + +// Helper macros for retrieving translated strings. +#define TRANSLATE(context, msg) Host::TranslateToCString(context, msg) +#define TRANSLATE_SV(context, msg) Host::TranslateToStringView(context, msg) +#define TRANSLATE_STR(context, msg) Host::TranslateToString(context, msg) + +// Does not translate the string at runtime, but allows the UI to in its own way. +#define TRANSLATE_NOOP(context, msg) msg