diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 58c3627..2831d28 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -26,6 +26,38 @@ namespace util { using json = nlohmann::json; +namespace { + +// Map a printf conversion char to its varargs argument class: 'i' (int-promoted), 'f' +// (double), 'p' (pointer/string). This is what matters for safety — reading a double slot +// as an int (or vice-versa) is undefined behavior; interchanging d/x is merely cosmetic. +char conversionClass(char c) +{ + switch (c) { + case 'd': case 'i': case 'o': case 'u': case 'x': case 'X': case 'c': return 'i'; + case 'f': case 'F': case 'e': case 'E': case 'g': case 'G': case 'a': case 'A': return 'f'; + case 's': case 'p': return 'p'; + default: return '?'; + } +} + +// Ordered argument signature of a printf-style format string (e.g. "%d / %.0f%%" -> "if"). +// "%%" is a literal and contributes nothing. +std::string formatSignature(const std::string& s) +{ + std::string sig; + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] != '%') continue; + if (++i >= s.size()) break; + if (s[i] == '%') continue; // literal "%%" + while (i < s.size() && std::strchr("-+ #0123456789.*lhLzjtq", s[i])) ++i; + if (i < s.size()) sig.push_back(conversionClass(s[i])); + } + return sig; +} + +} // namespace + I18n::I18n() { // Register built-in languages @@ -51,6 +83,27 @@ I18n& I18n::instance() return instance; } +void I18n::overlayTranslations(const nlohmann::json& translations) +{ + for (auto& [key, value] : translations.items()) { + if (!value.is_string()) continue; + const std::string translated = value.get(); + // If the English base for this key carries printf specifiers, the translation must + // keep the same argument signature — otherwise it would be passed to printf/ImGui + // with mismatched varargs (UB). On mismatch, keep the safe English string. + const auto base = strings_.find(key); + if (base != strings_.end()) { + const std::string baseSig = formatSignature(base->second); + if (!baseSig.empty() && baseSig != formatSignature(translated)) { + DEBUG_LOGF("i18n: dropping '%s' — format mismatch (\"%s\" vs English)\n", + key.c_str(), translated.c_str()); + continue; + } + } + strings_[key] = translated; + } +} + bool I18n::loadLanguage(const std::string& locale) { // Try to load from file: CWD-relative first, then exe-relative @@ -72,11 +125,7 @@ bool I18n::loadLanguage(const std::string& locale) } else { strings_.clear(); } - for (auto& [key, value] : j.items()) { - if (value.is_string()) { - strings_[key] = value.get(); - } - } + overlayTranslations(j); current_locale_ = locale; DEBUG_LOGF("Loaded language file: %s (%zu strings)\n", lang_file.c_str(), strings_.size()); @@ -128,12 +177,8 @@ bool I18n::loadLanguage(const std::string& locale) } else { strings_.clear(); } - for (auto& [key, value] : j.items()) { - if (value.is_string()) { - strings_[key] = value.get(); - } - } - + overlayTranslations(j); + current_locale_ = locale; DEBUG_LOGF("Loaded embedded language: %s (%zu strings)\n", locale.c_str(), strings_.size()); return true; @@ -1199,6 +1244,7 @@ void I18n::loadBuiltinEnglish() strings_["send_tx_failed"] = "Transaction failed"; strings_["send_tx_sent"] = "Transaction sent!"; strings_["send_tx_success"] = "Transaction sent successfully!"; + strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed"; strings_["send_txid_copied"] = "TxID copied to clipboard"; strings_["send_txid_label"] = "TxID: %s"; strings_["send_valid_shielded"] = "Valid shielded address"; diff --git a/src/util/i18n.h b/src/util/i18n.h index 3924cd5..0349eb5 100644 --- a/src/util/i18n.h +++ b/src/util/i18n.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace dragonx { namespace util { @@ -58,7 +59,12 @@ public: 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"; std::unordered_map strings_; std::vector> available_languages_;