diff --git a/src/app.cpp b/src/app.cpp index e0e460b..73fdbe1 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -6089,16 +6089,16 @@ bool App::startEmbeddedDaemon() // NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter. switch (state) { case daemon::EmbeddedDaemon::State::Starting: - setDaemonStatus(TR("sb_starting_daemon")); + setDaemonStatus(util::trCopy("sb_starting_daemon")); break; case daemon::EmbeddedDaemon::State::Running: - setDaemonStatus(TR("sb_dragonxd_running")); + setDaemonStatus(util::trCopy("sb_dragonxd_running")); break; case daemon::EmbeddedDaemon::State::Stopping: - setDaemonStatus(TR("sb_dragonxd_stopping")); + setDaemonStatus(util::trCopy("sb_dragonxd_stopping")); break; case daemon::EmbeddedDaemon::State::Stopped: - setDaemonStatus(TR("sb_dragonxd_stopped")); + setDaemonStatus(util::trCopy("sb_dragonxd_stopped")); break; case daemon::EmbeddedDaemon::State::Error: setDaemonStatus("Error: " + msg); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index c6b59b7..791d6dc 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -114,6 +114,11 @@ void I18n::overlayTranslations(const nlohmann::json& translations) bool I18n::loadLanguage(const std::string& locale) { + // Exclusive for the whole load: strings_ is cleared/rebuilt below, and translateCopy() readers on + // other threads (daemon monitor) must never observe it mid-rewrite. loadBuiltinEnglish() and + // overlayTranslations() are called only from here and do not lock (no recursion). + std::unique_lock lock(strings_mutex_); + // Try to load from file: CWD-relative first, then exe-relative std::string rel_path = "res/lang/" + locale + ".json"; const std::string exe_dir = Platform::getExecutableDirectory(); @@ -3116,6 +3121,15 @@ const char* I18n::translate(const char* key) const return key; } +std::string I18n::translateCopy(const char* key) const +{ + if (!key) return std::string(); + std::shared_lock lock(strings_mutex_); + auto it = strings_.find(key); + if (it != strings_.end()) return it->second; + return std::string(key); +} + void I18n::registerLanguage(const std::string& code, const std::string& name) { // Check if already registered diff --git a/src/util/i18n.h b/src/util/i18n.h index d49e89d..556b5c7 100644 --- a/src/util/i18n.h +++ b/src/util/i18n.h @@ -4,6 +4,8 @@ #pragma once +#include +#include #include #include #include @@ -38,7 +40,16 @@ public: * @return Translated string, or key if not found */ const char* translate(const char* key) const; - + + /** + * @brief Thread-safe translate: returns a COPY under a read lock. + * + * translate() hands back a pointer into the string table, which loadLanguage() rewrites on the + * main thread. Any caller off the main thread (e.g. the EmbeddedDaemon monitor thread) MUST use + * this instead; main-thread UI code keeps using translate()/TR(). + */ + std::string translateCopy(const char* key) const; + /** * @brief Get current locale */ @@ -66,6 +77,9 @@ private: void overlayTranslations(const nlohmann::json& translations); std::string current_locale_ = "en"; + // Guards strings_/current_locale_ against loadLanguage() (main thread, writer) racing + // translateCopy() (off-thread readers). The lock-free translate() is main-thread only. + mutable std::shared_mutex strings_mutex_; std::unordered_map strings_; std::vector> available_languages_; }; @@ -79,6 +93,13 @@ inline const char* tr(const char* key) { return I18n::instance().translate(key); } +/** + * @brief Thread-safe translation returning an owned copy. Use from any non-main thread. + */ +inline std::string trCopy(const char* key) { + return I18n::instance().translateCopy(key); +} + /** * @brief Map a raw daemon / JSON-RPC error string to a short, friendly, TRANSLATED explanation. *