#include "I18n.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using namespace std::literals::string_view_literals; namespace { struct LanguageInfo { std::string CodeName; std::string LocaleName; fs::path File; }; class I18nState { public: static I18nState& Get() { static I18nState instance; return instance; } public: tsl::array_map LocaleInfos; tsl::array_map CurrentEntries; LanguageInfo* CurrentLanguage = nullptr; bool Unloaded = false; void Unload() { Unloaded = true; CurrentEntries = {}; } void EnsureLoaded() { if (Unloaded) { Unloaded = false; Reload(); } } void Reload() { if (!CurrentLanguage) return; std::ifstream ifs(CurrentLanguage->File); Json::Value root; ifs >> root; for (auto name : root.getMemberNames()) { if (name == "$localized_name") { continue; } auto& value = root[name]; if (value.isString()) { CurrentEntries.insert(name, value.asCString()); } } } }; std::string FindLocalizedName(const fs::path& localeFile) { std::ifstream ifs(localeFile); if (!ifs) { throw std::runtime_error("Failed to open locale file."); } Json::Value root; ifs >> root; if (auto& name = root["$localized_name"]; name.isString()) { return std::string(name.asCString()); } else { throw std::runtime_error("Failed to find $localized_name in language file."); } } } // namespace void I18n::Init() { auto& state = I18nState::Get(); auto dir = fs::current_path() / "locale"; if (!fs::exists(dir)) { throw std::runtime_error("Failed to find locale directory."); } for (auto& elm : fs::directory_iterator{ dir }) { if (!elm.is_regular_file()) continue; auto& path = elm.path(); auto codeName = path.stem().string(); state.LocaleInfos.emplace( codeName, LanguageInfo{ .CodeName = codeName, .LocaleName = FindLocalizedName(path), .File = path, }); } } void I18n::Shutdown() { auto& state = I18nState::Get(); state.LocaleInfos.clear(); state.CurrentEntries.clear(); state.CurrentLanguage = nullptr; state.Unloaded = false; } void I18n::Unload() { auto& state = I18nState::Get(); state.Unload(); OnUnload(); } std::string_view I18n::GetLanguage() { auto& state = I18nState::Get(); return state.CurrentLanguage->CodeName; } bool I18n::SetLanguage(std::string_view lang) { auto& state = I18nState::Get(); if (state.CurrentLanguage && state.CurrentLanguage->CodeName == lang) { return false; } if (auto iter = state.LocaleInfos.find(lang); iter != state.LocaleInfos.end()) { state.CurrentLanguage = &iter.value(); state.Reload(); } OnLanguageChange(); return true; } std::optional I18n::Lookup(std::string_view key) { auto& state = I18nState::Get(); state.EnsureLoaded(); auto iter = state.CurrentEntries.find(key); if (iter != state.CurrentEntries.end()) { return iter.value(); } else { return std::nullopt; } } std::string_view I18n::LookupUnwrap(std::string_view key) { auto o = Lookup(key); if (!o) { std::string msg; msg.append("Unable to find locale for '"); msg.append(key); msg.append("'."); throw std::runtime_error(std::move(msg)); }; return o.value(); } std::string_view I18n::LookupLanguage(std::string_view lang) { auto& state = I18nState::Get(); auto iter = state.LocaleInfos.find(lang); if (iter != state.LocaleInfos.end()) { return iter.value().LocaleName; } else { return ""sv; } } BasicTranslation::BasicTranslation(std::string_view key) : mContent{ I18n::LookupUnwrap(key) } { } const std::string& BasicTranslation::GetString() const { return mContent; } const char* BasicTranslation::Get() const { return mContent.c_str(); } FormattedTranslation::FormattedTranslation(std::string_view key) { auto src = I18n::LookupUnwrap(key); mMinimumResultLen = 0; bool escape = false; bool matchingCloseBrace = false; std::string buf; for (char c : src) { switch (c) { case '\\': { // Disallow double (or more) escaping if (escape) { buf += '\\'; escape = false; break; } escape = true; } break; case '{': { // Escaping an opening brace cause the whole "argument" (if any) gets parsed as a part of the previous literal if (escape) { buf += '{'; escape = false; break; } // Generate literal mMinimumResultLen += buf.size(); mParsedElements.push_back(Element{ std::move(buf) }); // Should also clear buf matchingCloseBrace = true; } break; case '}': { if (escape) { throw std::runtime_error("Cannot escape '}', put \\ before the '{' if intended to escape braces."); } // If there is no pairing '{', simply treat this as a normal character // (escaping for closing braces) if (!matchingCloseBrace) { buf += '}'; break; } // Generate argument if (buf.empty()) { // No index given, default to use current argument's index auto currArgIdx = (int)mNumArguments; mParsedElements.push_back(Element{ currArgIdx }); } else { // Use provided index int argIdx = std::stoi(buf); mParsedElements.push_back(Element{ argIdx }); buf.clear(); } } break; default: { if (escape) { throw std::runtime_error("Cannot escape normal character '" + std::to_string(c) + "'."); } buf += c; } break; } } } std::string FormattedTranslation::Format(std::span args) { if (args.size() != mNumArguments) { throw std::runtime_error("Invalid number of arguments for FormattedTranslation::Format, expected " + std::to_string(mNumArguments) + " but found " + std::to_string(args.size()) + "."); } std::string result; result.reserve(mMinimumResultLen); for (auto& elm : mParsedElements) { if (auto literal = std::get_if(&elm)) { result.append(*literal); } if (auto idx = std::get_if(&elm)) { result.append(args[*idx]); } } return result; }