From 2fd0d0034e54b2ec974d3e7d8e4a536cfcb70555 Mon Sep 17 00:00:00 2001 From: SweetId <2630750+SweetId@users.noreply.github.com> Date: Fri, 20 Oct 2023 20:17:31 -0400 Subject: [PATCH] Initial commit --- .gitignore | 195 +++++++++++++++++++ LICENSE | 21 ++ README.md | 40 ++++ examples/Demo/localization.csv | 3 + examples/Demo/main.cpp | 34 ++++ examples/Demo/xmake.lua | 7 + examples/xmake.lua | 9 + include/NazaraLocalization/Config.hpp | 13 ++ include/NazaraLocalization/Localization.hpp | 46 +++++ include/NazaraLocalization/LocalizedText.hpp | 71 +++++++ src/NazaraLocalization/Localization.cpp | 87 +++++++++ src/NazaraLocalization/LocalizedText.cpp | 94 +++++++++ xmake.lua | 74 +++++++ 13 files changed, 694 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/Demo/localization.csv create mode 100644 examples/Demo/main.cpp create mode 100644 examples/Demo/xmake.lua create mode 100644 examples/xmake.lua create mode 100644 include/NazaraLocalization/Config.hpp create mode 100644 include/NazaraLocalization/Localization.hpp create mode 100644 include/NazaraLocalization/LocalizedText.hpp create mode 100644 src/NazaraLocalization/Localization.cpp create mode 100644 src/NazaraLocalization/LocalizedText.cpp create mode 100644 xmake.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a14811 --- /dev/null +++ b/.gitignore @@ -0,0 +1,195 @@ +# xmake-related files +.xmake/* +.vs/* +.vscode/* +CMakeLists.txt +Makefile +vs*/* +vsxmake*/* + +# Ignore assets (except shaders) +assets/* +!assets/examples_version.txt +!assets/readme.md +!assets/tests_version.txt +!assets/shaders + +# Coverage +coverage.out + +# Embed resources +src/Nazara/*/Resources/**/*.h + +# Nazara binaries +bin/* +!bin/resources +!bin/resources/* + +# Build files +build/* + +# Nazara libraries +lib/* + +# Self-hosted thirdparty libraries binaries +thirdparty/genlib/* + +# Nazara plugin libraries +plugins/lib/* + +# Nazara package +package/* + +# Example files +examples/bin/*.exe +examples/bin/*.pdb +examples/bin/*.dll +examples/bin/*.so +examples/bin/Demo* + +# Unit tests +tests/*.exe +tests/*.pdb +tests/*.dll +tests/*.so +tests/NazaraUnitTests* + +# Example generated files +examples/bin/HardwareInfo.txt + +# Example generated files +examples/bin/HardwareInfo.txt + +# Feature page +build/scripts/features/index.html + +# Documentation +doc + +# Codeblocks +*.save-failed +build/**/*.cbp +build/**/*.cbp +build/**/*.cbTemp +build/**/*.cscope_file_list +build/**/*.depend +build/**/*.layout +build/**/*.workspace + +# CodeLite +build/**/*.project + +# GMake +build/**/*.make +build/**/*.d + +# Visual Studio +build/**/*.pdb +build/**/*.filters +build/**/*.vcxproj +build/**/*.tlog +build/**/*.sln +build/**/*.vcxprojResolveAssemblyReference.cache +build/**/*.nativecodeanalysis.all.xml +build/**/*.nativecodeanalysis.xml +build/**/*.VC.opendb +build/**/*.VC.db* +build/**/*.json +build/**/*.sqlite +build/**/*.FileListAbsolute.txt +build/**/*.recipe + +# Compiled Object files +build/**/*.slo +build/**/*.lo +build/**/*.o +build/**/*.obj +build/**/*.obj.enc + +# Compiled Dynamic libraries +build/**/*.so + +# Compiled Static libraries +build/**/*.lai +build/**/*.la + +# Object files +build/**/*.o + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +[Tt]est[Rr]esult +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.idb +*.ilk +*.meta +*.pch +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +*.vssscc +.builds + +*.pidb + +*.log +*.scc +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ + +*.[Rr]e[Ss]harper + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a97122a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d17aad --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Nazara Localization + +Nazara Localization is a [Nazara Engine](https://github.com/NazaraEngine/NazaraEngine) module that allows you to add localized text to your project. + +You can use it in any kind of commercial and non-commercial applications without any restriction ([MIT license](http://opensource.org/licenses/MIT)). + +## Authors + +Sid - main developper + +## How to Use + +``` +// Add NazaraImgui.hpp to your includes +#include + +// main.cpp +{ + // Add Nz::Localization to the modules list + Nz::Modules nazara; + + Nz::LocalizedText hello = "LOC_HELLO_WORLD"; + printf("text: %s\n", hello.c_str()); +} +``` + + +## Contribute + +##### Don't hesitate to contribute to Nazara Engine by: +- Extending the [wiki](https://github.com/NazaraEngine/NazaraEngine/wiki) +- Submitting a patch to GitHub +- Post suggestions/bugs on the forum or the [GitHub tracker](https://github.com/NazaraEngine/NazaraEngine/issues) +- [Fork the project](https://github.com/NazaraEngine/NazaraEngine/fork) on GitHub and [push your changes](https://github.com/NazaraEngine/NazaraEngine/pulls) +- Talking about Nazara Engine to other people, spread the word! +- Doing anything else that might help us + +## Links + +[Discord](https://discord.gg/MvwNx73) \ No newline at end of file diff --git a/examples/Demo/localization.csv b/examples/Demo/localization.csv new file mode 100644 index 0000000..71ec2c1 --- /dev/null +++ b/examples/Demo/localization.csv @@ -0,0 +1,3 @@ +;fr-FR;en-US;pt-BR +LOC_HELLO_WORLD;Bonjour le monde!;Hello World!;Olá Mundo! +LOC_TEXT_ARG;{1:.2f} est la valeur de {0}!;{pi} is the value of {}!;O valor de {} é {}! diff --git a/examples/Demo/main.cpp b/examples/Demo/main.cpp new file mode 100644 index 0000000..f438139 --- /dev/null +++ b/examples/Demo/main.cpp @@ -0,0 +1,34 @@ + +#include + +#include +#include + +#include + +int main(int argc, char* argv[]) +{ + NazaraUnused(argc); + NazaraUnused(argv); + + Nz::Application nazara; + + Nz::LocalizedText helloWorld("LOC_HELLO_WORLD"); + Nz::LocalizedText textWithArguments = Nz::LocalizedText("LOC_TEXT_ARG").Arg("PI").Arg("pi", Nz::Pi); + + // Works with printf as if you were printing a string + // There is no localization data loaded so it will just print the key + printf("%s\n%s\n", helloWorld.c_str(), textWithArguments.c_str()); + + // We load the CSV containing our data + Nz::Localization::Instance()->LoadLocalizationFile("localization.csv"); + + Nz::Localization::Instance()->SetLocale("fr-FR"); + std::cout << helloWorld << std::endl << textWithArguments << std::endl; + Nz::Localization::Instance()->SetLocale("en-US"); + std::cout << helloWorld << std::endl << textWithArguments << std::endl; + Nz::Localization::Instance()->SetLocale("pt-BR"); + std::cout << helloWorld << std::endl << textWithArguments << std::endl; + + return 0; +} \ No newline at end of file diff --git a/examples/Demo/xmake.lua b/examples/Demo/xmake.lua new file mode 100644 index 0000000..8a61891 --- /dev/null +++ b/examples/Demo/xmake.lua @@ -0,0 +1,7 @@ + +target("NzLocalization-demo") + set_group("Examples") + add_files("main.cpp") + add_packages("nazara") + add_deps("NazaraLocalization") + set_rundir(".") \ No newline at end of file diff --git a/examples/xmake.lua b/examples/xmake.lua new file mode 100644 index 0000000..1f37028 --- /dev/null +++ b/examples/xmake.lua @@ -0,0 +1,9 @@ +option("examples") + set_default(false) + set_showmenu(true) + set_description("Build examples") +option_end() + +if has_config("examples") then + includes("*/xmake.lua") +end \ No newline at end of file diff --git a/include/NazaraLocalization/Config.hpp b/include/NazaraLocalization/Config.hpp new file mode 100644 index 0000000..d754d66 --- /dev/null +++ b/include/NazaraLocalization/Config.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +#if defined(NAZARA_LOCALIZATION_STATIC) + #define NAZARA_LOCALIZATION_API +#else + #ifdef NAZARA_LOCALIZATION_BUILD + #define NAZARA_LOCALIZATION_API NAZARA_EXPORT + #else + #define NAZARA_LOCALIZATION_API NAZARA_IMPORT + #endif +#endif \ No newline at end of file diff --git a/include/NazaraLocalization/Localization.hpp b/include/NazaraLocalization/Localization.hpp new file mode 100644 index 0000000..2f4f0f4 --- /dev/null +++ b/include/NazaraLocalization/Localization.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include + +namespace Nz +{ + class NAZARA_LOCALIZATION_API Localization : public Nz::ModuleBase + { + friend ModuleBase; + + public: + using Dependencies = TypeList; + struct Config {}; + + Localization(Config config); + ~Localization(); + + // Loads a CSV containing the localization strings + bool LoadLocalizationFile(const std::filesystem::path& filepath); + + // Changes the locale + bool SetLocale(const std::string& locale); + + bool FindIndexForKey(std::string_view key, size_t& index) const; + const std::string& GetStringAtIndex(size_t index) const; + + private: + struct Locale + { + std::string name; + std::vector localizedStrings; + }; + + std::vector m_locales; + std::unordered_map m_lookupTable; + + Locale* m_currentLocale; + + static Localization* s_instance; + + friend class LocalizedText; + }; +} diff --git a/include/NazaraLocalization/LocalizedText.hpp b/include/NazaraLocalization/LocalizedText.hpp new file mode 100644 index 0000000..1561db6 --- /dev/null +++ b/include/NazaraLocalization/LocalizedText.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace Nz +{ + class NAZARA_LOCALIZATION_API LocalizedText + { + public: + LocalizedText(); + LocalizedText(std::string_view str); + + template LocalizedText& Arg(T&& v) + { + NamedParameter p; + p.first = v; + m_parameters.push_back(std::move(p)); + return *this; + } + template <> LocalizedText& Arg(const char*&& v) { return Arg(std::string(v)); } + + template LocalizedText& Arg(std::string_view name, T&& v) + { + NamedParameter p; + p.first = v; + p.second = name; + m_parameters.push_back(std::move(p)); + return *this; + } + template <> LocalizedText& Arg(std::string_view name, const char*&& v) { return Arg(name, std::string(v)); } + + + const char* c_str() const; + const char* data() const; + size_t length() const; + size_t size() const; + + const std::string& ToString() const; + + friend std::ostream& operator<<(std::ostream& out, const Nz::LocalizedText& dt) + { + return out << dt.ToString(); + } + + private: + void Update() const; + std::string BuildFormattedString() const; + + // Cache for localization + mutable std::optional m_index; // index in the lookup array + mutable Localization::Locale* m_locale; // current locale to avoid fetching pointer every time + mutable std::optional m_cachedStr; // computed string (loca + parameters formatting) + + std::string m_str; + + using Parameter = std::variant< + Int8, Int16, Int32, Int64, + UInt8, UInt16, UInt32, UInt64, + float, double, + std::string + >; + using NamedParameter = std::pair>; + std::vector m_parameters; + }; +} diff --git a/src/NazaraLocalization/Localization.cpp b/src/NazaraLocalization/Localization.cpp new file mode 100644 index 0000000..63c7dfc --- /dev/null +++ b/src/NazaraLocalization/Localization.cpp @@ -0,0 +1,87 @@ +#include + +#include + +#include + +namespace Nz +{ + Localization* Localization::s_instance = nullptr; + + Localization::Localization(Config /*config*/) + : ModuleBase("Localization", this) + , m_currentLocale(nullptr) + { } + + Localization::~Localization() + {} + + bool Localization::LoadLocalizationFile(const std::filesystem::path& filepath) + { + std::ifstream file(filepath); + if (!file) + return false; + + // read header + std::string line; + std::getline(file, line); + + m_locales.clear(); + SplitString(line, ";", [this](std::string_view str) { + if (!str.empty()) + m_locales.push_back({ std::string(str) }); + return true; + }); + + size_t index = 0; + while (std::getline(file, line)) + { + std::vector values; + SplitString(line, ";", [&values](std::string_view str) { + values.push_back(str); + return true; + }); + + m_lookupTable[std::string(values[0])] = index++; + for (size_t i = 0; i < m_locales.size() && i < values.size() - 1; ++i) + { + m_locales[i].localizedStrings.push_back(std::string(values[i + 1])); + } + } + + return true; + } + + bool Localization::SetLocale(const std::string& locale) + { + auto it = std::find_if(m_locales.begin(), m_locales.end(), [locale](auto&& loc) { return loc.name == locale; }); + if (it == m_locales.end()) + { + m_currentLocale = nullptr; + return false; + } + + m_currentLocale = &(*it); + return true; + } + + bool Localization::FindIndexForKey(std::string_view key, size_t& index) const + { + auto it = m_lookupTable.find(std::string(key)); + if (it == m_lookupTable.end()) + return false; + + index = it->second; + return true; + } + + const std::string& Localization::GetStringAtIndex(size_t index) const + { + static std::string empty = ""; + if (m_currentLocale == nullptr) + return empty; + + return m_currentLocale->localizedStrings.at(index); + } + +} // end of namespace Nz diff --git a/src/NazaraLocalization/LocalizedText.cpp b/src/NazaraLocalization/LocalizedText.cpp new file mode 100644 index 0000000..79a945e --- /dev/null +++ b/src/NazaraLocalization/LocalizedText.cpp @@ -0,0 +1,94 @@ +#include +#include + +#include +#include + +namespace Nz +{ + + LocalizedText::LocalizedText() + : LocalizedText("") + { } + + LocalizedText::LocalizedText(std::string_view str) + : m_str(str) + , m_locale(nullptr) + {} + + void LocalizedText::Update() const + { + if (m_locale != Nz::Localization::Instance()->m_currentLocale) + { + m_cachedStr = {}; + m_locale = Nz::Localization::Instance()->m_currentLocale; + + if (!m_index.has_value()) + { + size_t index; + if (Nz::Localization::Instance()->FindIndexForKey(m_str, index)) + m_index = index; + } + + if (m_index.has_value()) + { + m_cachedStr = BuildFormattedString(); + } + } + } + + std::string LocalizedText::BuildFormattedString() const + { + if (m_parameters.empty()) // No parameters, no formatting + return m_locale->localizedStrings[m_index.value()]; + + fmt::dynamic_format_arg_store store; + for (auto& p : m_parameters) + { + if (p.second) + { + std::visit(Overloaded{ + [&](auto&& value) { + store.push_back(fmt::arg(p.second->c_str(), value)); + } + }, p.first); + } + else + { + std::visit(Overloaded{ + [&](auto&& value) { + store.push_back(value); + } + }, p.first); + } + } + return fmt::vformat(m_locale->localizedStrings[m_index.value()], store); + } + + const char* LocalizedText::c_str() const + { + Update(); + return (m_cachedStr) ? m_cachedStr->c_str() : m_str.c_str(); + } + const char* LocalizedText::data() const + { + Update(); + return (m_cachedStr) ? m_cachedStr->data() : m_str.data(); + } + size_t LocalizedText::length() const + { + Update(); + return (m_cachedStr) ? m_cachedStr->length() : m_str.length(); + } + size_t LocalizedText::size() const + { + Update(); + return (m_cachedStr) ? m_cachedStr->size() : m_str.size(); + } + + const std::string& LocalizedText::ToString() const + { + Update(); + return (m_cachedStr) ? m_cachedStr.value() : m_str; + } +} \ No newline at end of file diff --git a/xmake.lua b/xmake.lua new file mode 100644 index 0000000..2c5e874 --- /dev/null +++ b/xmake.lua @@ -0,0 +1,74 @@ +set_project("NazaraLocalization") + +add_rules("mode.asan", "mode.tsan", "mode.coverage", "mode.debug", "mode.releasedbg", "mode.release") +add_rules("plugin.vsxmake.autoupdate") + +add_repositories("nazara-engine-repo https://github.com/NazaraEngine/xmake-repo") +add_requires("nazaraengine", { alias = "nazara", debug = is_mode("debug") }) +add_requires("fmt") + +add_includedirs("include", "src") +set_languages("c89", "c++20") +set_rundir("./bin/$(plat)_$(arch)_$(mode)") +set_targetdir("./bin/$(plat)_$(arch)_$(mode)") + +if has_config("erronwarn") then + set_warnings("allextra", "error") +else + set_warnings("allextra") +end + +if is_plat("mingw", "linux") then + add_cxflags("-Wno-subobject-linkage") +end + +if is_plat("windows") then + add_defines("_CRT_SECURE_NO_WARNINGS") + add_cxxflags("/bigobj", "/permissive-", "/Zc:__cplusplus", "/Zc:externConstexpr", "/Zc:inline", "/Zc:lambda", "/Zc:preprocessor", "/Zc:referenceBinding", "/Zc:strictStrings", "/Zc:throwingNew") + add_cxflags("/w44062") -- Enable warning: switch case not handled + add_cxflags("/wd4251") -- Disable warning: class needs to have dll-interface to be used by clients of class blah blah blah + add_cxflags("/wd4275") -- Disable warning: DLL-interface class 'class_1' used as base for DLL-interface blah +elseif is_plat("mingw") then + add_cxflags("-Og", "-Wa,-mbig-obj") + add_ldflags("-Wa,-mbig-obj") +end + +set_runtimes(is_mode("debug") and "MDd" or "MD") + +if is_mode("asan", "tsan", "ubsan") then + set_optimize("none") -- by default xmake will optimize asan builds +elseif is_mode("coverage") then + if not is_plat("windows") then + add_links("gcov") + end +elseif is_mode("releasedbg") then + set_fpmodels("fast") + add_vectorexts("sse", "sse2", "sse3", "ssse3") +end + +target("NazaraLocalization") + add_packages("nazara", {public = true}) + add_packages("fmt", {public = false}) + set_kind("$(kind)") + set_group("Libraries") + add_headerfiles("include/(NazaraLocalization/**.hpp)") + add_headerfiles("include/(NazaraLocalization/**.inl)") + add_headerfiles("src/NazaraLocalization/**.h", { prefixdir = "private", install = false }) + add_headerfiles("src/NazaraLocalization/**.hpp", { prefixdir = "private", install = false }) + add_headerfiles("src/NazaraLocalization/**.inl", { prefixdir = "private", install = false }) + add_files("src/NazaraLocalization/**.cpp") + + -- for now only shared compilation is supported (except on platforms like wasm) + if not is_plat("wasm") then + set_kind("shared") + else + set_kind("static") + add_defines("NAZARA_LOCALIZATION_STATIC", { public = true }) + end + + add_defines("NAZARA_LOCALIZATION_BUILD") + if is_mode("debug") then + add_defines("NAZARA_LOCALIZATION_DEBUG") + end + +includes("examples/xmake.lua")