diff --git a/icons/translations.png b/icons/translations.png new file mode 100644 index 00000000..3664bd79 Binary files /dev/null and b/icons/translations.png differ diff --git a/mob.ini b/mob.ini index ccf92a26..44b18874 100644 --- a/mob.ini +++ b/mob.ini @@ -18,9 +18,13 @@ install_message = never host = [aliases] -super = cmake_common modorganizer* githubpp +super = cmake_common modorganizer* plugins = check_fnis bsapacker bsa_extractor diagnose_basic installer_* plugin_python preview_base preview_bsa tool_* game_* +[translations] +mo2-translations = organizer uibase plugin_python +mo2-game-bethesda = game_creation game_enderal game_enderalse game_fallout3 game_fallout4 game_fallout4vr game_falloutNV game_gamebryo game_morrowind game_nehrim game_oblivion game_skyrim game_skyrimse game_skyrimvr game_ttw + [task] enabled = true mo_org = ModOrganizer2 @@ -109,6 +113,7 @@ ss_fallout4_trosski = v1.11 third_party = prefix = cache = +icons = licenses = build = install = @@ -118,6 +123,7 @@ install_libs = install_pdbs = install_dlls = install_plugins = +install_extensions = install_stylesheets = install_licenses = install_translations = diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b87b84b5..67237f48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ file(GLOB_RECURSE source_files *.cpp) file(GLOB_RECURSE header_files *.h) add_executable(mob ${source_files} ${header_files}) -set_target_properties(mob PROPERTIES CXX_STANDARD 20) +set_target_properties(mob PROPERTIES CXX_STANDARD 23) target_compile_definitions(mob PUBLIC NOMINMAX) target_compile_options(mob PUBLIC "/MT") diff --git a/src/core/conf.cpp b/src/core/conf.cpp index b4b85839..eb024e75 100644 --- a/src/core/conf.cpp +++ b/src/core/conf.cpp @@ -51,24 +51,32 @@ namespace mob::details { // returns a string from conf, bails out if it doesn't exist // - std::string get_string(std::string_view section, std::string_view key) + std::string get_string(std::string_view section, std::string_view key, + std::optional default_) { auto sitor = g_conf.find(section); if (sitor == g_conf.end()) gcx().bail_out(context::conf, "[{}] doesn't exist", section); auto kitor = sitor->second.find(key); - if (kitor == sitor->second.end()) - gcx().bail_out(context::conf, "no key '{}' in [{}]", key, section); + if (kitor == sitor->second.end()) { + if (!default_.has_value()) { + gcx().bail_out(context::conf, "no key '{}' in [{}]", key, section); + } + return *default_; + } return kitor->second; } // calls get_string(), converts to int // - int get_int(std::string_view section, std::string_view key) + int get_int(std::string_view section, std::string_view key, + std::optional default_) { - const auto s = get_string(section, key); + const auto s = get_string(section, key, default_.transform([](auto v) { + return std::to_string(v); + })); try { return std::stoi(s); @@ -80,9 +88,12 @@ namespace mob::details { // calls get_string(), converts to bool // - bool get_bool(std::string_view section, std::string_view key) + bool get_bool(std::string_view section, std::string_view key, + std::optional default_) { - const auto s = get_string(section, key); + const auto s = get_string(section, key, default_.transform([](auto v) { + return v ? "true" : "false"; + })); return bool_from_string(s); } @@ -428,13 +439,8 @@ namespace mob { MOB_ASSERT(!tasks.empty()); - for (auto& t : tasks) { - if (t->name() != task && - details::find_string_for_task(t->name(), key)) { - continue; - } + for (auto& t : tasks) details::set_string_for_task(t->name(), key, value); - } } else { // global task option @@ -512,6 +518,7 @@ namespace mob { set_path_if_empty("vcpkg", find_vcpkg); // set after vs as it will use the VS set_path_if_empty("qt_install", find_qt); set_path_if_empty("temp_dir", find_temp_dir); + set_path_if_empty("icons", find_in_root("icons")); set_path_if_empty("licenses", find_in_root("licenses")); set_path_if_empty("qt_bin", qt::installation_path() / "bin"); set_path_if_empty("qt_translations", qt::installation_path() / "translations"); @@ -537,6 +544,7 @@ namespace mob { resolve_path("install_licenses", p.install_bin(), "licenses"); resolve_path("install_stylesheets", p.install_bin(), "stylesheets"); resolve_path("install_translations", p.install_bin(), "translations"); + resolve_path("install_extensions", p.install_bin(), "extensions"); // finally, resolve the tools that are unlikely to be in PATH; all the // other tools (7z, jom, patch, etc.) are assumed to be in PATH (which @@ -682,6 +690,11 @@ namespace mob { return {}; } + conf_translations conf::translation() + { + return {}; + } + conf_versions conf::version() { return {}; @@ -770,6 +783,8 @@ namespace mob { conf_build_types::conf_build_types() : conf_section("build-types") {} + conf_translations::conf_translations() : conf_section("translations") {} + conf_prebuilt::conf_prebuilt() : conf_section("prebuilt") {} conf_paths::conf_paths() : conf_section("paths") {} diff --git a/src/core/conf.h b/src/core/conf.h index 4e147a32..cc18d63c 100644 --- a/src/core/conf.h +++ b/src/core/conf.h @@ -9,7 +9,8 @@ namespace mob::details { // returns an option named `key` from the given `section` // - std::string get_string(std::string_view section, std::string_view key); + std::string get_string(std::string_view section, std::string_view key, + std::optional default_ = {}); // convert a string to the given type template @@ -25,11 +26,13 @@ namespace mob::details { // calls get_string(), converts to bool // - bool get_bool(std::string_view section, std::string_view key); + bool get_bool(std::string_view section, std::string_view key, + std::optional default_ = {}); // calls get_string(), converts to in // - int get_int(std::string_view section, std::string_view key); + int get_int(std::string_view section, std::string_view key, + std::optional default_ = {}); // sets the given option, bails out if the option doesn't exist // @@ -60,9 +63,17 @@ namespace mob { template class conf_section { public: - DefaultType get(std::string_view key) const + DefaultType get(std::string_view key, + std::optional default_ = {}) const { - const auto value = details::get_string(name_, key); + const auto value = [=] { + if constexpr (std::is_same_v) { + return details::get_string(name_, key, default_); + } + else { + return details::get_string(name_, key); + } + }(); if constexpr (std::is_convertible_v) { return value; @@ -74,18 +85,18 @@ namespace mob { // undefined template - T get(std::string_view key) const; + T get(std::string_view key, std::optional default_ = {}) const; template <> - bool get(std::string_view key) const + bool get(std::string_view key, std::optional default_) const { - return details::get_bool(name_, key); + return details::get_bool(name_, key, default_); } template <> - int get(std::string_view key) const + int get(std::string_view key, std::optional default_) const { - return details::get_int(name_, key); + return details::get_int(name_, key, default_); } void set(std::string_view key, std::string_view value) @@ -223,6 +234,13 @@ namespace mob { conf_build_types(); }; + // options in [translations] + // + class conf_translations : public conf_section { + public: + conf_translations(); + }; + // options in [prebuilt] // class conf_prebuilt : public conf_section { @@ -245,6 +263,7 @@ namespace mob { VALUE(third_party); VALUE(prefix); VALUE(cache); + VALUE(icons); VALUE(licenses); VALUE(build); @@ -255,7 +274,7 @@ namespace mob { VALUE(install_pdbs); VALUE(install_dlls); - VALUE(install_plugins); + VALUE(install_extensions); VALUE(install_stylesheets); VALUE(install_licenses); VALUE(install_translations); @@ -283,6 +302,7 @@ namespace mob { conf_cmake cmake(); conf_tools tool(); conf_transifex transifex(); + conf_translations translation(); conf_prebuilt prebuilt(); conf_versions version(); conf_build_types build_types(); diff --git a/src/core/op.cpp b/src/core/op.cpp index bf0a0510..62fc7e9f 100644 --- a/src/core/op.cpp +++ b/src/core/op.cpp @@ -116,6 +116,30 @@ namespace mob::op { } } + void delete_file_glob_recurse(const context& cx, const fs::path& directory, + const fs::path& glob, flags f) + { + cx.trace(context::fs, "deleting glob {}", glob); + + const auto native = glob.native(); + + if (!fs::exists(directory)) + return; + + for (auto&& e : fs::recursive_directory_iterator(directory)) { + const auto p = e.path(); + const auto name = p.filename().native(); + + if (!PathMatchSpecW(name.c_str(), native.c_str())) { + cx.trace(context::fs, "{} did not match {}; skipping", name, glob); + + continue; + } + + delete_file(cx, p, f); + } + } + void remove_readonly(const context& cx, const fs::path& dir, flags f) { cx.trace(context::fs, "removing read-only from {}", dir); diff --git a/src/core/op.h b/src/core/op.h index eafd58ad..15d0a4b9 100644 --- a/src/core/op.h +++ b/src/core/op.h @@ -65,6 +65,11 @@ namespace mob::op { // void delete_file_glob(const context& cx, const fs::path& glob, flags f = noflags); + // deletes all files matching the glob in the given directory and its subdirectories + // + void delete_file_glob_recurse(const context& cx, const fs::path& directory, + const fs::path& glob, flags f = noflags); + // removes the readonly flag for all files in `dir`, recursive // void remove_readonly(const context& cx, const fs::path& dir, flags f = noflags); diff --git a/src/main.cpp b/src/main.cpp index 7119c6ec..c0e71f16 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,6 @@ namespace mob { // // mob doesn't have a concept of task dependencies, just task ordering, so // if a task depends on another, it has to be earlier in the order - // super tasks using mo = modorganizer; @@ -42,13 +41,12 @@ namespace mob { .add_task("modorganizer-bsatk") .add_task("modorganizer-nxmhandler") .add_task("modorganizer-helper") + .add_task({"modorganizer-bsapacker", "bsa_packer"}) + .add_task("modorganizer-preview_bsa") .add_task("modorganizer-game_bethesda"); add_task() - .add_task({"modorganizer-bsapacker", "bsa_packer"}) .add_task({"modorganizer-tool_inieditor", "inieditor"}) - .add_task({"modorganizer-tool_inibakery", "inibakery"}) - .add_task("modorganizer-preview_bsa") .add_task("modorganizer-preview_base") .add_task("modorganizer-diagnose_basic") .add_task("modorganizer-check_fnis") diff --git a/src/tasks/translations.cpp b/src/tasks/translations.cpp index 6910b19f..bb0980b8 100644 --- a/src/tasks/translations.cpp +++ b/src/tasks/translations.cpp @@ -1,6 +1,8 @@ #include "pch.h" #include "../core/env.h" +#include "../utility/string.h" #include "../utility/threading.h" +#include "nlohmann/json.hpp" #include "task_manager.h" #include "tasks.h" @@ -197,8 +199,8 @@ namespace mob::tasks { // remove the .qm files in the translations/ directory if (is_set(c, clean::rebuild)) { - op::delete_file_glob(cx(), conf().path().install_translations() / "*.qm", - op::optional); + op::delete_file_glob_recurse(cx(), conf().path().install_extensions(), + "*.qm", op::optional); } } @@ -251,46 +253,139 @@ namespace mob::tasks { } } + void generate_translations_metadata( + std::filesystem::path const& path, + std::vector const& languages) + { + using json = nlohmann::ordered_json; + + json metadata = json::parse(R"( +{ + "id": "mo2-translations", + "name": "Translations for ModOrganizer2", + "version": "1.0.0", + "description": "Multi-language translations for ModOrganizer2 itself.", + "author": { + "name": "Mod Organizer 2", + "homepage": "https://www.modorganizer.org/" + }, + "icon": "translations.png", + "type": "translation", + "content": { + "translations": {} + } +} +)"); + + // fix version + json translations; + for (auto&& lang : languages) { + std::vector files; + files.push_back("translations/" + lang.name + "/*.qm"); + json jsonlang; + jsonlang["files"] = files; + translations[lang.name] = jsonlang; + } + metadata["content"]["translations"] = translations; + + std::ofstream ofs(path); + ofs << metadata.dump(2); + } + void translations::do_build_and_install() { // 1) build the list of projects, languages and .ts files // 2) run `lrelease` for every language in every project // 3) copy builtin qt translations - const auto root = source_path() / "translations"; - const auto dest = conf().path().install_translations(); + const auto root = source_path() / "translations"; + const auto extensions = conf().path().install_extensions(); const projects ps(root); - op::create_directories(cx(), dest); + op::create_directories(cx(), extensions / "mo2-translations"); // log all the warnings added while walking the projects for (auto&& w : ps.warnings()) cx().warning(context::generic, "{}", w); + std::map project_to_extension; + + // go through the list of extensions and find the matching projects + for (auto& p : fs::directory_iterator(extensions)) { + if (!fs::is_directory(p)) + continue; + + const auto name = + mob::replace_all(p.path().filename().string(), "mo2-", ""); + + project_to_extension[name] = p.path().filename(); + project_to_extension[mob::replace_all(name, "-", "_")] = + p.path().filename(); + + const auto s_projects = + conf().translation().get(p.path().filename().string(), ""); + if (!s_projects.empty()) { + const auto extension_projects = mob::split(s_projects, " "); + for (const auto& project : extension_projects) { + project_to_extension[project] = p.path().filename(); + } + } + } + // run `lrelease` in a thread pool parallel_functions v; // for each project for (auto& p : ps.get()) { + if (!project_to_extension.contains(p.name)) { + cx().warning(context::generic, + "found project {} but no matching extension", p.name); + continue; + } + + const auto base = extensions / project_to_extension[p.name]; + + if (!fs::exists(base)) { + cx().warning( + context::generic, + "found project {} for extension {} but extension is not built", + p.name, project_to_extension[p.name]); + continue; + } + + const auto dest = base / "translations"; + op::create_directories(cx(), dest); + // for each language for (auto& lg : p.langs) { + op::create_directories(cx(), dest / lg.name); // add a functor that will run lrelease - v.push_back( - {lg.name + "." + p.name, [&] { - // run release for the given project name and list of .ts files - run_tool( - lrelease().project(p.name).sources(lg.ts_files).out(dest)); - }}); + v.push_back({lg.name + "." + p.name, [=] { + // run release for the given project name and list of + // .ts files + run_tool(lrelease() + .project(p.name) + .sources(lg.ts_files) + .out(dest / lg.name)); + }}); } } // run all the functors in parallel parallel(v); - if (auto p = ps.find("organizer")) - copy_builtin_qt_translations(*p, dest); + if (auto p = ps.find("organizer")) { + // copy Qt builting translation, icon and generate metadata + copy_builtin_qt_translations(*p, extensions / "mo2-translations" / + "translations"); + op::copy_file_to_file_if_better( + cx(), conf().path().icons() / "translations.png", + extensions / "mo2-translations" / "translations.png", op::unsafe); + generate_translations_metadata( + extensions / "mo2-translations" / "metadata.json", p->langs); + } else - cx().bail_out(context::generic, "organizer project not found"); + cx().warning(context::generic, "organizer project not found"); } void translations::copy_builtin_qt_translations(const projects::project& p, @@ -307,7 +402,8 @@ namespace mob::tasks { if (!fs::exists(src)) return false; - op::copy_file_to_dir_if_better(cx(), src, dest, op::unsafe); + op::create_directories(cx(), dest / lang); + op::copy_file_to_dir_if_better(cx(), src, dest / lang, op::unsafe); return true; };