From 44f5fa5c8f258e8fc1f7d7e2e45e0485bd6cc490 Mon Sep 17 00:00:00 2001 From: rtk0c Date: Wed, 31 Mar 2021 20:19:18 -0700 Subject: Complete items tab (UI and serialization) --- core/src/Utils/I18n.cpp | 520 ++++++++++++++++++++++++++---------------------- 1 file changed, 280 insertions(+), 240 deletions(-) (limited to 'core/src/Utils/I18n.cpp') diff --git a/core/src/Utils/I18n.cpp b/core/src/Utils/I18n.cpp index 50edf67..edc5469 100644 --- a/core/src/Utils/I18n.cpp +++ b/core/src/Utils/I18n.cpp @@ -1,240 +1,280 @@ -#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; -} +#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; +} -- cgit v1.2.3-70-g09d2