summaryrefslogtreecommitdiffstats
path: root/src/game/client/language.cc
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-06-29 22:24:42 +0500
committeruntodesu <kirill@untode.su>2025-06-29 22:24:42 +0500
commit6cd00aacfa22fed6a54a9b812f6b069ad16feec0 (patch)
treeb77f4e665da3dd235cdb01e7e6ea78c1c02ecf2e /src/game/client/language.cc
parentf440914e1ae453768d09383f332bc7844e0a700e (diff)
downloadvoxelius-6cd00aacfa22fed6a54a9b812f6b069ad16feec0.tar.bz2
voxelius-6cd00aacfa22fed6a54a9b812f6b069ad16feec0.zip
Move game sources into src subdirectory
Diffstat (limited to 'src/game/client/language.cc')
-rw-r--r--src/game/client/language.cc196
1 files changed, 196 insertions, 0 deletions
diff --git a/src/game/client/language.cc b/src/game/client/language.cc
new file mode 100644
index 0000000..2ae0bc6
--- /dev/null
+++ b/src/game/client/language.cc
@@ -0,0 +1,196 @@
+#include "client/pch.hh"
+
+#include "client/language.hh"
+
+#include "core/config.hh"
+
+#include "client/globals.hh"
+#include "client/settings.hh"
+
+constexpr static const char* DEFAULT_LANGUAGE = "en_US";
+
+// Available languages are kept in a special manifest file which
+// is essentially a key-value map of semi-IETF-compliant language tags
+// and the language's endonym; after reading the manifest, the translation
+// system knows what language it can load and will act accordingly
+constexpr static const char* MANIFEST_PATH = "lang/manifest.json";
+
+static LanguageManifest manifest;
+static LanguageIterator current_language;
+static std::unordered_map<std::string, std::string> language_map;
+static std::unordered_map<std::string, LanguageIterator> ietf_map;
+static ConfigString config_language(DEFAULT_LANGUAGE);
+
+static void send_language_event(LanguageIterator new_language)
+{
+ LanguageSetEvent event;
+ event.new_language = new_language;
+ globals::dispatcher.trigger(event);
+}
+
+void language::init(void)
+{
+ globals::client_config.add_value("language", config_language);
+
+ settings::add_language_select(0, settings_location::GENERAL, "language");
+
+ auto file = PHYSFS_openRead(MANIFEST_PATH);
+
+ if(file == nullptr) {
+ spdlog::critical("language: {}: {}", MANIFEST_PATH, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ auto source = std::string(PHYSFS_fileLength(file), char(0x00));
+ PHYSFS_readBytes(file, source.data(), source.size());
+ PHYSFS_close(file);
+
+ auto jsonv = json_parse_string(source.c_str());
+ const auto json = json_value_get_object(jsonv);
+ const auto count = json_object_get_count(json);
+
+ if((jsonv == nullptr) || (json == nullptr) || (count == 0)) {
+ spdlog::critical("language: {}: parse error", MANIFEST_PATH);
+ json_value_free(jsonv);
+ std::terminate();
+ }
+
+ for(std::size_t i = 0; i < count; ++i) {
+ const auto ietf = json_object_get_name(json, i);
+ const auto value = json_object_get_value_at(json, i);
+ const auto endonym = json_value_get_string(value);
+
+ if(ietf && endonym) {
+ LanguageInfo info;
+ info.ietf = std::string(ietf);
+ info.endonym = std::string(endonym);
+ info.display = std::format("{} ({})", endonym, ietf);
+ manifest.push_back(info);
+ }
+ }
+
+ for(auto it = manifest.cbegin(); it != manifest.cend(); ++it) {
+ ietf_map.emplace(it->ietf, it);
+ }
+
+ json_value_free(jsonv);
+
+ // This is temporary! This value will
+ // be overriden in init_late after the
+ // config system updates config_language
+ current_language = manifest.cend();
+}
+
+void language::init_late(void)
+{
+ auto user_language = ietf_map.find(config_language.get());
+
+ if(user_language != ietf_map.cend()) {
+ language::set(user_language->second);
+ return;
+ }
+
+ auto fallback = ietf_map.find(DEFAULT_LANGUAGE);
+
+ if(fallback != ietf_map.cend()) {
+ language::set(fallback->second);
+ return;
+ }
+
+ spdlog::critical("language: we're doomed!");
+ spdlog::critical("language: {} doesn't exist!", DEFAULT_LANGUAGE);
+ std::terminate();
+}
+
+void language::set(LanguageIterator new_language)
+{
+ if(new_language != manifest.cend()) {
+ auto path = std::format("lang/lang.{}.json", new_language->ietf);
+
+ auto file = PHYSFS_openRead(path.c_str());
+
+ if(file == nullptr) {
+ spdlog::warn("language: {}: {}", path, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ send_language_event(new_language);
+ return;
+ }
+
+ auto source = std::string(PHYSFS_fileLength(file), char(0x00));
+ PHYSFS_readBytes(file, source.data(), source.size());
+ PHYSFS_close(file);
+
+ auto jsonv = json_parse_string(source.c_str());
+ const auto json = json_value_get_object(jsonv);
+ const auto count = json_object_get_count(json);
+
+ if((jsonv == nullptr) || (json == nullptr) || (count == 0)) {
+ spdlog::warn("language: {}: parse error", path);
+ send_language_event(new_language);
+ json_value_free(jsonv);
+ return;
+ }
+
+ language_map.clear();
+
+ for(size_t i = 0; i < count; ++i) {
+ const auto key = json_object_get_name(json, i);
+ const auto value = json_object_get_value_at(json, i);
+ const auto value_str = json_value_get_string(value);
+
+ if(key && value_str) {
+ language_map.emplace(key, value_str);
+ continue;
+ }
+ }
+
+ json_value_free(jsonv);
+
+ current_language = new_language;
+ config_language.set(new_language->ietf.c_str());
+ }
+
+ send_language_event(new_language);
+}
+
+LanguageIterator language::get_current(void)
+{
+ return current_language;
+}
+
+LanguageIterator language::find(const char* ietf)
+{
+ const auto it = ietf_map.find(ietf);
+ if(it != ietf_map.cend()) {
+ return it->second;
+ } else {
+ return manifest.cend();
+ }
+}
+
+LanguageIterator language::cbegin(void)
+{
+ return manifest.cbegin();
+}
+
+LanguageIterator language::cend(void)
+{
+ return manifest.cend();
+}
+
+const char* language::resolve(const char* key)
+{
+ const auto it = language_map.find(key);
+ if(it != language_map.cend()) {
+ return it->second.c_str();
+ } else {
+ return key;
+ }
+}
+
+std::string language::resolve_gui(const char* key)
+{
+ // We need window tags to retain their hierarchy when a language
+ // dynamically changes; ImGui allows to provide hidden unique identifiers
+ // to GUI primitives that have their name change dynamically, so we're using this
+ return std::format("{}###{}", language::resolve(key), key);
+}