#include "I18n.hpp" #include "Utils/String.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; 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; }; std::string findLocalizedName(const fs::path& localeFile) { std::ifstream ifs{ localeFile }; if (!ifs) { // TODO log error 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 { // TODO log error 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)) { // TODO log error return; } 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, }); } SetLanguage("en_US"); } void I18n::Shutdown() { // Nothing to do yet } Signal<> I18n::reloadSignal{}; void I18n::ReloadLocales() { auto& state = I18nState::Get(); reloadSignal(); } std::string_view I18n::GetLanguage() { auto& state = I18nState::Get(); return state.currentLanguage->localeName; } bool I18n::SetLanguage(std::string_view lang) { auto& state = I18nState::Get(); if (!state.currentLanguage) return false; if (state.currentLanguage->codeName == lang) return false; if (auto iter = state.localeInfos.find(lang); iter != state.localeInfos.end()) { state.currentLanguage = &iter.value(); state.currentEntries.clear(); auto& file = state.currentLanguage->file; std::ifstream ifs{ file }; Json::Value root; ifs >> root; for (auto name : root.getMemberNames()) { auto& value = root[name]; } } ReloadLocales(); return true; } std::optional I18n::Lookup(std::string_view key) { auto& state = I18nState::Get(); 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(); } BasicTranslation::BasicTranslation(std::string_view key) : mContent{ I18n::LookupUnwrap(key) } { } std::string_view BasicTranslation::Get() const { return mContent; } 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) throw std::runtime_error("Cannot escape '\\'."); escape = true; continue; } case '{': { // Escaping an opeing brace cause the whole "argument" (if any) gets parsed as a part of the previous literal if (escape) { buf += '{'; 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; } escape = false; } } 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; }