#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 = nullptr; }; 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() { // Nothing to do yet } Signal<> I18n::OnReload{}; void I18n::ReloadLocales() { auto& state = I18nState::Get(); OnReload(); } 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 && 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()) { if (name == "$localized_name") { continue; } auto& value = root[name]; if (value.isString()) { state.CurrentEntries.insert(name, value.asCString()); } } } 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) // Assuming the string is null terminated, which it is here (because we store interally using std::string) // TODO properly use std::string_view when imgui supports it : mContent{ I18n::LookupUnwrap(key).data() } { } const char* 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; }