Concurrency audit: I18n::translate() returns a const char* pointing INTO strings_, and loadLanguage() clears/rebuilds strings_ on the main thread at a language switch. The EmbeddedDaemon state/monitor callback builds its status strings via TR() off the main thread, so a concurrent language switch could free the map node out from under that pointer — a use-after-free data race. - Add a shared_mutex guarding strings_: loadLanguage() takes it exclusively for the whole clear+rebuild (its helpers loadBuiltinEnglish/overlayTranslations are called only from there and don't lock, so no recursion); a new translateCopy()/trCopy() takes a shared lock and returns an owned std::string. - The lock-free translate()/TR() stays for main-thread UI (same thread as the writer, so never races). - The four daemon-state setDaemonStatus(TR(...)) calls in the monitor callback now use util::trCopy(...). Synchronization only; no fund-path call touched. Threading fix — clean build + ctest; the race is runtime-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
3.8 KiB
C++
118 lines
3.8 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <nlohmann/json_fwd.hpp>
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
/**
|
|
* @brief Internationalization support
|
|
*
|
|
* Simple string table implementation for translating UI text.
|
|
* Strings are stored in JSON files in res/lang/ directory.
|
|
*/
|
|
class I18n {
|
|
public:
|
|
/**
|
|
* @brief Get the singleton instance
|
|
*/
|
|
static I18n& instance();
|
|
|
|
/**
|
|
* @brief Load a language file
|
|
* @param locale Language code (e.g., "en", "es", "zh")
|
|
* @return true if loaded successfully
|
|
*/
|
|
bool loadLanguage(const std::string& locale);
|
|
|
|
/**
|
|
* @brief Translate a string
|
|
* @param key The string key (usually English text)
|
|
* @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
|
|
*/
|
|
const std::string& getCurrentLocale() const { return current_locale_; }
|
|
|
|
/**
|
|
* @brief Get list of available languages
|
|
*/
|
|
const std::vector<std::pair<std::string, std::string>>& getAvailableLanguages() const { return available_languages_; }
|
|
|
|
/**
|
|
* @brief Register an available language
|
|
* @param code Language code (e.g., "en")
|
|
* @param name Display name (e.g., "English")
|
|
*/
|
|
void registerLanguage(const std::string& code, const std::string& name);
|
|
|
|
private:
|
|
I18n();
|
|
void loadBuiltinEnglish();
|
|
// Overlay translated strings onto the English base, but reject any value whose printf
|
|
// format-specifier signature differs from the English source (keep English instead).
|
|
// Many strings are used directly as printf/ImGui format strings, so a mismatched
|
|
// translation (dropped/changed specifier) is undefined behavior — this prevents it.
|
|
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<std::string, std::string> strings_;
|
|
std::vector<std::pair<std::string, std::string>> available_languages_;
|
|
};
|
|
|
|
/**
|
|
* @brief Convenience function for translation
|
|
* @param key The string key
|
|
* @return Translated string
|
|
*/
|
|
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.
|
|
*
|
|
* Recognizes the common cases a user actually hits — a warming-up node, a locked wallet, an RPC the
|
|
* daemon is too old to have, insufficient funds, and an unreachable node — and returns plain guidance
|
|
* with a next step. Falls back to the raw text (trimmed) when nothing matches, so detail is never lost.
|
|
* Keep the raw text available elsewhere (a Details view / the console) for diagnostics.
|
|
*/
|
|
std::string friendlyRpcError(const std::string& raw);
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|
|
|
|
// Convenience macro for translations
|
|
#define TR(key) dragonx::util::tr(key)
|