aboutsummaryrefslogtreecommitdiff
path: root/core/src/Utils/I18n.cpp
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2021-03-27 23:01:07 -0700
committerrtk0c <[email protected]>2021-03-27 23:01:07 -0700
commit442d2d75d71bbc057e667edc301a79fa1cc813be (patch)
treeb5d1e5068a4d481bc6bcd72dca851ac7a85bf7e4 /core/src/Utils/I18n.cpp
Initial setup
Diffstat (limited to 'core/src/Utils/I18n.cpp')
-rw-r--r--core/src/Utils/I18n.cpp233
1 files changed, 233 insertions, 0 deletions
diff --git a/core/src/Utils/I18n.cpp b/core/src/Utils/I18n.cpp
new file mode 100644
index 0000000..645cfc5
--- /dev/null
+++ b/core/src/Utils/I18n.cpp
@@ -0,0 +1,233 @@
+#include "I18n.hpp"
+
+#include "Utils/String.hpp"
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <tsl/array_map.h>
+#include <filesystem>
+#include <fstream>
+#include <stdexcept>
+#include <string_view>
+#include <utility>
+
+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<char, LanguageInfo> localeInfos;
+ tsl::array_map<char, std::string> 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<std::string_view> 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<Argument> 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<std::string>(&elm)) {
+ result.append(*literal);
+ }
+ if (auto idx = std::get_if<int>(&elm)) {
+ result.append(args[*idx]);
+ }
+ }
+
+ return result;
+}