From 1b64a6cd0f5ade892680158802acef705b3e0834 Mon Sep 17 00:00:00 2001 From: Neucrack Date: Fri, 12 Jul 2024 22:20:25 +0800 Subject: [PATCH] update i18n support load from yaml files, add trans search tool `maixtool i18n` command --- components/basic/CMakeLists.txt | 2 +- components/basic/include/maix_fs.hpp | 4 +- components/basic/include/maix_i18n.hpp | 114 +++++++++++++++++----- components/basic/src/maix_fs.cpp | 125 +++++++++++++++---------- components/basic/src/maix_i18n.cpp | 67 +++++++++++++ examples/i18n/README.md | 12 +++ examples/i18n/app.yaml | 9 ++ examples/i18n/locales/en.yaml | 2 + examples/i18n/locales/zh.yaml | 2 + examples/i18n/main/src/main.cpp | 42 +++++---- tools/cmake/copy_assets.py | 49 ++++++++++ tools/cmake/gen_binary.cmake | 3 + tools/maixtool/maixtool/i18n.py | 101 ++++++++++++++++++++ tools/maixtool/maixtool/maixtool.py | 33 +++++-- tools/maixtool/maixtool/version.py | 2 +- 15 files changed, 463 insertions(+), 104 deletions(-) create mode 100644 examples/i18n/README.md create mode 100644 examples/i18n/app.yaml create mode 100644 examples/i18n/locales/en.yaml create mode 100644 examples/i18n/locales/zh.yaml create mode 100644 tools/cmake/copy_assets.py create mode 100644 tools/maixtool/maixtool/i18n.py diff --git a/components/basic/CMakeLists.txt b/components/basic/CMakeLists.txt index 9fda0653..76b42455 100644 --- a/components/basic/CMakeLists.txt +++ b/components/basic/CMakeLists.txt @@ -29,7 +29,7 @@ endif() ###### Add required/dependent components ###### -list(APPEND ADD_REQUIREMENTS pthread stdc++fs ini) +list(APPEND ADD_REQUIREMENTS pthread stdc++fs ini yaml) ############################################### ###### Add link search path for requirements/libs ###### diff --git a/components/basic/include/maix_fs.hpp b/components/basic/include/maix_fs.hpp index f0075077..63510d59 100644 --- a/components/basic/include/maix_fs.hpp +++ b/components/basic/include/maix_fs.hpp @@ -168,10 +168,10 @@ namespace maix::fs /** * Get file extension * @param path path to get extension - * @return extension if success, empty string if failed + * @return prefix_path and extension list if success, empty string if failed * @maixpy maix.fs.splitext */ - std::string splitext(const std::string &path); + std::vector splitext(const std::string &path); /** * List files in directory diff --git a/components/basic/include/maix_i18n.hpp b/components/basic/include/maix_i18n.hpp index 47763332..8a4c8eb1 100644 --- a/components/basic/include/maix_i18n.hpp +++ b/components/basic/include/maix_i18n.hpp @@ -10,6 +10,8 @@ #include #include #include +#include "maix_err.hpp" +#include "maix_log.hpp" using namespace std; @@ -18,43 +20,58 @@ namespace maix::i18n /** * i18n locales list * @maixpy maix.i18n.locales - */ + */ static std::vector locales = { "en", "zh", "zh-tw", - "ja" - }; + "ja"}; /** * i18n language names list * @maixpy maix.i18n.names - */ + */ const static std::vector names = { "English", "简体中文", "繁體中文", - "日本語" - }; + "日本語"}; /** * Get system config of locale. * @return language locale, e.g. en, zh, zh_CN, zh_TW, etc. * @maixpy maix.i18n.get_locale - */ + */ string get_locale(); /** * Get system config of language name. * @return language name, e.g. English, 简体中文, 繁體中文, etc. * @maixpy maix.i18n.get_language_name - */ + */ string get_language_name(); + /** + * Load translations from yaml files. + * @param locales_dir translation yaml files directory. + * @return A dict contains all translations, e.g. {"zh":{"hello": "你好"}, "en":{"hello": "hello"}}, you should delete it after use in C++. + * @maixpy maix.i18n.load_trans_yaml + */ + const std::map> *load_trans_yaml(const std::string &locales_dir); + + /** + * Load translations from yaml files. + * @param locales_dir translation yaml files directory. + * @param dict dict to store key values. A dict contains all translations, e.g. {"zh":{"hello": "你好"}, "en":{"hello": "hello"}}, you should delete it after use in C++. + * @return err::ERR + * @maixcdk maix.i18n.load_trans_yaml + */ + err::Err load_trans_yaml(const std::string &locales_dir, std::map> &dict); + /** * Translate helper class. * @maixpy maix.i18n.Trans - */ + */ class Trans { public: @@ -65,27 +82,81 @@ namespace maix::i18n * @param locales_dict locales dict, e.g. {"zh": {"Confirm": "确认", "OK": "好的"}, "en": {"Confirm": "Confirm", "OK": "OK"}} * @maixpy maix.i18n.Trans.__init__ * @maixcdk maix.i18n.Trans.Trans - */ - Trans(const std::map> &locales_dict) - :locales_dict(locales_dict) + */ + Trans(const std::map> &locales_dict = std::map>()) + : locales_dict_const(locales_dict) { this->locale = ""; // DO NOT load from file system here, will coredump } + /** + * Load translation from yaml files generated by `maixtool i18n` command. + * @param locales_dir the translation files directory. + * @return err.Err type, no error will return err.Err.ERR_NONE. + * @maixpy maix.i18n.Trans.load + */ + err::Err load(const std::string &locales_dir) + { + return load_trans_yaml(locales_dir, locales_dict); + } + + /** + * Update translation dict. + * @param dict the new translation dict. + * @return err.Err type, no error will return err.Err.ERR_NONE. + * @maixpy maix.i18n.Trans.update_dict + */ + err::Err update_dict(const std::map> &dict) + { + try + { + for (const auto &file_pair : dict) + { + const std::string &filename = file_pair.first; + for (const auto &kv_pair : file_pair.second) + { + locales_dict[filename][kv_pair.first] = kv_pair.second; + } + } + return err::Err(); + } + catch (const std::exception &e) + { + log::error("copy dict value failed"); + return err::Err::ERR_ARGS; + } + } + /** * Translate string by key. * @param key string key, e.g. "Confirm" * @param locale locale name, if not assign, use default locale set by system settings or set_locale function. * @return translated string, if find translation, return it, or return key, e.g. "确认", "Confirm", etc. * @maixpy maix.i18n.Trans.tr - */ + */ string tr(const string &key, const string locale = "") { - if(this->locale.empty()) + if (this->locale.empty()) this->locale = i18n::get_locale(); // if locale not in locales_dict, return key - const std::map>::const_iterator iter = locales_dict.find(locale.empty() ? this->locale : locale); - if (iter == locales_dict.end()) + if (!locales_dict.empty()) + { + const std::map>::const_iterator iter = locales_dict.find(locale.empty() ? this->locale : locale); + if (iter == locales_dict.end()) + { + return key; + } + // get key value from locales_dict[locale][key], default value is key + const std::map dict = iter->second; + const std::map::const_iterator iter2 = dict.find(key); + if (iter2 == dict.end()) + { + return key; + } + return iter2->second; + } + const std::map>::const_iterator iter = locales_dict_const.find(locale.empty() ? this->locale : locale); + if (iter == locales_dict_const.end()) { return key; } @@ -103,7 +174,7 @@ namespace maix::i18n * Set locale temporarily, will not affect system settings. * @param locale locale name, e.g. "zh", "en", etc. @see maix.i18n.locales * @maixpy maix.i18n.Trans.set_locale - */ + */ void set_locale(const string &locale) { this->locale = locale; @@ -113,16 +184,17 @@ namespace maix::i18n * Get current locale. * @return locale name, e.g. "zh", "en", etc. @see maix.i18n.locales * @maixpy maix.i18n.Trans.get_locale - */ + */ string get_locale() { - if(this->locale.empty()) + if (this->locale.empty()) this->locale = i18n::get_locale(); return this->locale; } private: - std::map> locales_dict; // copy dict to here to avoid memory problem, e.g. For python + std::map> locales_dict; // copy dict to here to avoid memory problem, e.g. For python + std::map> locales_dict_const; string locale; }; -} \ No newline at end of file +} diff --git a/components/basic/src/maix_fs.cpp b/components/basic/src/maix_fs.cpp index 65af2698..42b99d0f 100644 --- a/components/basic/src/maix_fs.cpp +++ b/components/basic/src/maix_fs.cpp @@ -48,27 +48,35 @@ namespace maix::fs err::Err symlink(const std::string &src, const std::string &link, bool force) { // 检查源文件是否存在 - if (!fs::exists(src)) { + if (!fs::exists(src)) + { return err::Err::ERR_NOT_FOUND; } // 删除已存在的软链接 - if (fs::exists(link)) { - if(!force) + if (fs::exists(link)) + { + if (!force) { return err::Err::ERR_ALREAY_EXIST; } - try { + try + { fs::remove(link); - } catch(...) { + } + catch (...) + { return err::Err::ERR_IO; } } // 创建软链接 - try { + try + { fs_sys::create_symlink(src, link); - } catch(...) { + } + catch (...) + { return err::Err::ERR_IO; } return err::Err::ERR_NONE; @@ -84,7 +92,7 @@ namespace maix::fs { // create a directory use fs_sys::create_directories // if exist_ok is true, also return true if directory already exists - if(!exist_ok && fs_sys::exists(path)) + if (!exist_ok && fs_sys::exists(path)) { return err::ERR_ALREAY_EXIST; } @@ -103,7 +111,7 @@ namespace maix::fs err::Err rmdir(const std::string &path, bool recursive) { // remove a directory use fs_sys::remove_all - if(!fs_sys::exists(path)) + if (!fs_sys::exists(path)) { return err::ERR_NOT_FOUND; } @@ -122,7 +130,7 @@ namespace maix::fs err::Err remove(const std::string &path) { // remove a file use fs_sys::remove - if(!fs_sys::exists(path)) + if (!fs_sys::exists(path)) { return err::ERR_NOT_FOUND; } @@ -133,7 +141,7 @@ namespace maix::fs err::Err rename(const std::string &src, const std::string &dst) { // rename a file or directory use fs_sys::rename - if(!fs_sys::exists(src)) + if (!fs_sys::exists(src)) { return err::ERR_NOT_FOUND; } @@ -149,7 +157,7 @@ namespace maix::fs int getsize(const std::string &path) { // get file size use fs_sys::file_size - if(!fs_sys::exists(path)) + if (!fs_sys::exists(path)) { return -err::ERR_NOT_FOUND; } @@ -160,7 +168,7 @@ namespace maix::fs { fs_sys::path p(path); std::string ret = p.parent_path().string(); - if(ret.empty()) + if (ret.empty()) { ret = "."; } @@ -188,23 +196,38 @@ namespace maix::fs return fs_sys::canonical(path).string(); } - std::string splitext(const std::string &path) + std::vector splitext(const std::string &path) { fs_sys::path p(path); - return p.extension().string(); + std::vector result; + + // 获取文件的后缀名 + std::string extension = p.extension().string(); + + // 获取文件的前缀 + std::string stem = p.stem().string(); + std::string parent_path = p.parent_path().string(); + + // 拼接前缀路径和文件名 + std::string prefix = parent_path.empty() ? stem : parent_path + fs_sys::path::preferred_separator + stem; + + result.push_back(prefix); + result.push_back(extension); + + return result; } std::vector *listdir(const std::string &path, bool recursive, bool full_path) { // list directory use fs_sys::directory_iterator - if(!fs_sys::exists(path)) + if (!fs_sys::exists(path)) { return nullptr; } std::vector *list = new std::vector(); if (recursive) { - if(full_path) + if (full_path) for (auto &p : fs_sys::recursive_directory_iterator(path)) { list->push_back(p.path().string()); @@ -217,7 +240,7 @@ namespace maix::fs } else { - if(full_path) + if (full_path) for (auto &p : fs_sys::directory_iterator(path)) { list->push_back(p.path().string()); @@ -236,7 +259,7 @@ namespace maix::fs err::Err error = err::ERR_NONE; fs::File *file = new fs::File(); error = file->open(path, mode); - if(error != err::ERR_NONE) + if (error != err::ERR_NONE) { log::error("open file %s failed, error code: %d\n", path.c_str(), error); delete file; @@ -250,16 +273,15 @@ namespace maix::fs return fs_sys::temp_directory_path().string(); } - err::Err File::open(const std::string &path, const std::string &mode) { // open file use std::fopen - if(_fp != nullptr) + if (_fp != nullptr) { return err::ERR_NOT_READY; } _fp = std::fopen(path.c_str(), mode.c_str()); - if(_fp == nullptr) + if (_fp == nullptr) { log::error("open file %s failed\n", path.c_str()); return err::ERR_ARGS; @@ -270,9 +292,9 @@ namespace maix::fs void File::close() { // close file use std::fclose - if(_fp != nullptr) + if (_fp != nullptr) { - std::fclose((FILE*)_fp); + std::fclose((FILE *)_fp); _fp = nullptr; } } @@ -280,24 +302,24 @@ namespace maix::fs int File::read(void *buf, int size) { // read data from file use std::fread - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_READY; } - return std::fread(buf, 1, size, (FILE*)_fp); + return std::fread(buf, 1, size, (FILE *)_fp); } std::vector *File::read(int size) { // read data from file use std::fread - if(_fp == nullptr) + if (_fp == nullptr) { log::error("file not opened\n"); return nullptr; } std::vector *buf = new std::vector(size); - int read_size = std::fread(buf->data(), 1, size, (FILE*)_fp); - if(read_size < 0) + int read_size = std::fread(buf->data(), 1, size, (FILE *)_fp); + if (read_size < 0) { delete buf; return nullptr; @@ -309,12 +331,12 @@ namespace maix::fs int File::readline(std::string &line) { // read line from file use std::fgets - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_OPEN; } char buf[1024] = {0}; - if(std::fgets(buf, 1024, (FILE*)_fp) == nullptr) + if (std::fgets(buf, 1024, (FILE *)_fp) == nullptr) { return 0; } @@ -325,12 +347,12 @@ namespace maix::fs std::string *File::readline() { // read line from file use std::fgets - if(_fp == nullptr) + if (_fp == nullptr) { throw err::Exception(err::ERR_NOT_OPEN, "file not opened"); } char buf[1024] = {0}; - if(std::fgets(buf, 1024, (FILE*)_fp) == nullptr) + if (std::fgets(buf, 1024, (FILE *)_fp) == nullptr) { return new std::string(); } @@ -339,65 +361,64 @@ namespace maix::fs } /** - * End of file or not - * @return 0 if not reach end of file, else eof. - * @maixpy maix.fs.File.eof - */ - int File::eof() - { - return std::feof((FILE*)_fp); - } + * End of file or not + * @return 0 if not reach end of file, else eof. + * @maixpy maix.fs.File.eof + */ + int File::eof() + { + return std::feof((FILE *)_fp); + } int File::write(const void *buf, int size) { // write data to file use std::fwrite - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_READY; } - return std::fwrite(buf, 1, size, (FILE*)_fp); + return std::fwrite(buf, 1, size, (FILE *)_fp); } int File::write(const std::vector &buf) { // write data to file use std::fwrite - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_READY; } - return std::fwrite(buf.data(), 1, buf.size(), (FILE*)_fp); + return std::fwrite(buf.data(), 1, buf.size(), (FILE *)_fp); } int File::seek(int offset, int whence) { // seek file position use std::fseek - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_READY; } - return std::fseek((FILE*)_fp, offset, whence); + return std::fseek((FILE *)_fp, offset, whence); } int File::tell() { // get file position use std::ftell - if(_fp == nullptr) + if (_fp == nullptr) { return -err::ERR_NOT_READY; } - return std::ftell((FILE*)_fp); + return std::ftell((FILE *)_fp); } err::Err File::flush() { // flush file use std::fflush - if(_fp == nullptr) + if (_fp == nullptr) { return err::ERR_NOT_READY; } - std::fflush((FILE*)_fp); + std::fflush((FILE *)_fp); return err::ERR_NONE; } } // namespace maix::fs - diff --git a/components/basic/src/maix_i18n.cpp b/components/basic/src/maix_i18n.cpp index b40cc171..d66bc194 100644 --- a/components/basic/src/maix_i18n.cpp +++ b/components/basic/src/maix_i18n.cpp @@ -7,6 +7,8 @@ #include "maix_i18n.hpp" #include "maix_app.hpp" +#include +#include "maix_fs.hpp" namespace maix::i18n { @@ -33,4 +35,69 @@ namespace maix::i18n return "English"; } + const std::map> *load_trans_yaml(const std::string &locales_dir) + { + auto *translations = new std::map>(); + + // 遍历目录中的所有文件 + std::vector *files = fs::listdir(locales_dir, true, true); + for (std::string file : *files) + { + std::string filename = fs::basename(file); + std::vector splitname = fs::splitext(file); + if (splitname[1] == ".yaml") + { + YAML::Node node = YAML::LoadFile(file); + std::map content; + + // 遍历 YAML 文件中的所有键值对 + for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) + { + content[it->first.as()] = it->second.as(); + } + + (*translations)[splitname[0]] = content; + } + } + delete files; + return translations; + } + + err::Err load_trans_yaml(const std::string &locales_dir, std::map> &dict) + { + if(!fs::exists(locales_dir)) + { + log::error("dir [ %s ] not found", locales_dir.c_str()); + return err::ERR_ARGS; + } + + // 遍历目录中的所有文件 + std::vector *files = fs::listdir(locales_dir, true, true); + if(!files) + { + log::error("no trans yaml files"); + return err::ERR_ARGS; + } + for (std::string file : *files) + { + std::string filename = fs::basename(file); + std::vector splitname = fs::splitext(filename); + if (splitname[1] == ".yaml") + { + YAML::Node node = YAML::LoadFile(file); + std::map content; + + // 遍历 YAML 文件中的所有键值对 + for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) + { + content[it->first.as()] = it->second.as(); + } + + dict[splitname[0]] = content; + } + } + delete files; + return err::ERR_NONE; + } + } // namespace maix::i18n diff --git a/examples/i18n/README.md b/examples/i18n/README.md new file mode 100644 index 00000000..b4f75b5d --- /dev/null +++ b/examples/i18n/README.md @@ -0,0 +1,12 @@ +MaixCDK i18n demo +===== + +## Usage + +1. coding like [main.cpp](./main/src/main.cpp), use `tr` function to call the string you want to translate. +2. Execute `maixtool i18n -d . -r` in project dir to generate translation files, they will be in `locales` dir, all are `yaml` file. +3. Translate files. +4. Compile and you will get binary files in dist dir, copy them to board and run. + + + diff --git a/examples/i18n/app.yaml b/examples/i18n/app.yaml new file mode 100644 index 00000000..1c3fbe30 --- /dev/null +++ b/examples/i18n/app.yaml @@ -0,0 +1,9 @@ +id: i18n_demo +name: I18N demo +version: 1.0.0 +#icon: assets/hello.png +author: Sipeed Ltd +desc: i18n demo +files: + - locales + diff --git a/examples/i18n/locales/en.yaml b/examples/i18n/locales/en.yaml new file mode 100644 index 00000000..db0545a0 --- /dev/null +++ b/examples/i18n/locales/en.yaml @@ -0,0 +1,2 @@ +hello: hello +out: out diff --git a/examples/i18n/locales/zh.yaml b/examples/i18n/locales/zh.yaml new file mode 100644 index 00000000..254d3e3d --- /dev/null +++ b/examples/i18n/locales/zh.yaml @@ -0,0 +1,2 @@ +hello: 你好 +out: 出去 diff --git a/examples/i18n/main/src/main.cpp b/examples/i18n/main/src/main.cpp index ddb4fbc9..bd5d08da 100644 --- a/examples/i18n/main/src/main.cpp +++ b/examples/i18n/main/src/main.cpp @@ -4,37 +4,43 @@ using namespace maix; -const std::map locale_zh_dict = { - {"out", "输出"}, - {"hello", "你好"} -}; +// const std::map locale_zh_dict = { +// {"out", "输出"}, +// {"hello", "你好"} +// }; -const std::map locale_ja_dict = { - // {"out", "出力"}, - {"hello", "こんにちは"} -}; +// const std::map locale_ja_dict = { +// // {"out", "出力"}, +// {"hello", "こんにちは"} +// }; -const std::map> locales_dict = { - {"zh", locale_zh_dict}, - {"ja", locale_ja_dict} -}; +// const std::map> locales_dict = { +// {"zh", locale_zh_dict}, +// {"ja", locale_ja_dict} +// }; - -i18n::Trans trans(locales_dict); +// i18n::Trans trans(locales_dict); +i18n::Trans trans; int _main(int argc, char* argv[]) { + err::Err e = trans.load("locales"); + err::check_raise(e, "load translation yamls failed"); + log::info("system locale: %s\n", i18n::get_locale().c_str()); - log::info("%s: %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); + log::info("%s: %s, %s\n", i18n::get_locale().c_str(), trans.tr("out").c_str(), trans.tr("hello").c_str()); trans.set_locale("zh"); - log::info("%s: %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); + log::info("zh: %s, %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); trans.set_locale("en"); - log::info("%s: %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); + log::info("en: %s, %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); trans.set_locale("ja"); - log::info("%s: %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); + log::info("ja: %s, %s\n", trans.tr("out").c_str(), trans.tr("hello").c_str()); + + // after coding, you need to execute `maixtool i18n -d . -r` in project dir to generate translation files, and translate them + return 0; } diff --git a/tools/cmake/copy_assets.py b/tools/cmake/copy_assets.py new file mode 100644 index 00000000..30193588 --- /dev/null +++ b/tools/cmake/copy_assets.py @@ -0,0 +1,49 @@ +import os +import sys +import shutil +import yaml + +def copy_assets(project_path, dist_path): + app_yaml_path = os.path.join(project_path, 'app.yaml') + + # 检查 app.yaml 是否存在 + if not os.path.exists(app_yaml_path): + print(f"{app_yaml_path} not a app, skip copy files") + return + + # 读取 app.yaml 文件 + with open(app_yaml_path, 'r') as file: + config = yaml.safe_load(file) + + # 检查 assets 键是否存在 + if 'files' not in config: + print("No 'files' key found in app.yaml, skip copy files") + return + + assets = config.get('files', []) + + if isinstance(assets, list): + for item in assets: + src = os.path.join(project_path, item) + dest = os.path.join(dist_path, item) + if os.path.exists(src): + shutil.copytree(src, dest, dirs_exist_ok=True) + print(f"Copied {src} to {dest}") + else: + raise Exception(f"{src} does not exist.") + elif isinstance(assets, dict): + for src_key, dest_value in assets.items(): + src = os.path.join(project_path, src_key) + dest = os.path.join(dist_path, dest_value) + if os.path.exists(src): + shutil.copytree(src, dest, dirs_exist_ok=True) + print(f"Copied {src} to {dest}") + else: + raise Exception(f"{src} does not exist.") + else: + print("'files' key must be a list or a dictionary.") + +if __name__ == "__main__": + project_path = sys.argv[1] + dist_path = sys.argv[2] + copy_assets(project_path, dist_path) diff --git a/tools/cmake/gen_binary.cmake b/tools/cmake/gen_binary.cmake index bd653cf7..e8a100ed 100644 --- a/tools/cmake/gen_binary.cmake +++ b/tools/cmake/gen_binary.cmake @@ -25,12 +25,15 @@ if(${BUILD_TYPE} STREQUAL "Release") endif() endif() +set(cp_assets_cmd COMMAND python ${SDK_PATH}/tools/cmake/copy_assets.py ${PROJECT_PATH} ${PROJECT_DIST_DIR}/${PROJECT_ID}_${build_type}) + add_custom_command(TARGET ${PROJECT_ID} POST_BUILD ${strip_cmd} COMMAND mkdir -p ${CMAKE_BINARY_DIR}/dl_lib ${cp_command} ${cp_dist_cmd} ${cp_dl_to_dist_cmd} + ${cp_assets_cmd} DEPENDS ${PROJECT_ID} COMMENT "-- copy dynamic libs to build/dl_lib dir ...") diff --git a/tools/maixtool/maixtool/i18n.py b/tools/maixtool/maixtool/i18n.py new file mode 100644 index 00000000..7a47426a --- /dev/null +++ b/tools/maixtool/maixtool/i18n.py @@ -0,0 +1,101 @@ +import sys, os +import yaml +from collections import OrderedDict + + +def get_files(scan_dir, recursive, exts=[".c", ".cpp", ".h", ".hpp"]): + files = [] + if recursive: + for root, dirs, fs in os.walk(scan_dir): + for f in fs: + if os.path.splitext(f)[-1] in exts: + files.append(os.path.join(root, f)) + else: + for name in os.listdir(scan_dir): + if os.path.splitext(name)[1] in exts: + files.append(os.path.join(scan_dir, name)) + return files + + +def get_i18n_strs(file, keywords): + ''' + read content and find '_("xxx")', get all xxx and return + ''' + strs = [] + with open(file, "r") as f: + content = f.read() + for key in keywords: + idx = 0 + while True: + flag = False + idx = content.find(f'{key}("', idx) + if idx == -1: + idx = content.find(f"{key}('", idx) + if idx == -1: + break + flag = True + if not (idx == 0 or content[idx - 1] in [".", ",", " ", ">"]): # maybe like str etc. + idx += 1 + continue + idx += len(key) + 2 + if flag: + end = content.find("')", idx) + else: + end = content.find('")', idx) + if end == -1: + break + strs.append(content[idx:end]) + idx = end + return strs + +def main(search_dir, keywords, exts, out, recursive, locales): + print("seach dir: ", dir) + print("keywords: ", keywords) + print("extentions:", exts) + print("out dir: ", out) + print("") + files = get_files(search_dir, recursive) + print(f"{len(files)} files found") + keys_list = [] + for f in files: + strs = get_i18n_strs(f, keywords) + keys_list.extend(strs) + keys_list = list(set(keys_list)) + keys_list.sort() + keys_dict = {} + for i in keys_list: + keys_dict[i] = i + os.makedirs(out, exist_ok=True) + for locale in locales: + print(f"gen locale [ {locale} ]") + path = os.path.join(out, f"{locale}.yaml") + old = {} + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + old = yaml.safe_load(f) + if old is None: + old = {} + # update by old + for k in keys_dict: + if k in old: + keys_dict[k] = old[k] + with open(path, "w", encoding="utf-8") as f: + yaml.dump(keys_dict, f, allow_unicode=True) + + +if __name__ == "__main__": + import argparse + paser = argparse.ArgumentParser() + paser.add_argument("-k", "--keywords", nargs="+", type=str, default=["_", "tr"], help="translate function keywords to search") + paser.add_argument("-r", action="store_true", help="recursive search dir") + paser.add_argument("-e", "--exts", nargs="+", type=str, default=[".c", ".cpp", ".h", ".hpp", ".py"], help="file extention to search") + paser.add_argument("-l", "--locales", nargs="+", type=str, default=["en", "zh"], help="locals of region, like en zh ja etc.") + paser.add_argument("-o", "--out", type=str, default="locales", help="translation files output directory") + paser.add_argument("-d", "--dir", type=str, help="where to search", required=True) + args = paser.parse_args() + main(args.dir, args.keywords, args.exts, args.out, args.r, args.locales) + + + + + diff --git a/tools/maixtool/maixtool/maixtool.py b/tools/maixtool/maixtool/maixtool.py index adff6b55..790d75f1 100644 --- a/tools/maixtool/maixtool/maixtool.py +++ b/tools/maixtool/maixtool/maixtool.py @@ -3,6 +3,7 @@ from .version import __version__ import os from .app_deploy import serve from .app_release import pack +from .i18n import main as i18n_main def deploy(port, pkg_path): if not pkg_path: @@ -15,15 +16,29 @@ def deploy(port, pkg_path): serve(pkg_path, port) def main(): - cmds = ["release", "deploy"] parser = argparse.ArgumentParser(description='maixtool, tools for maix series development') parser.add_argument('-v', '--version', action='version', version=__version__) - parser.add_argument('-p', "--port", default=8888, type=int, help="deploy server port") - parser.add_argument("--pkg", default="", type=str, help="app package path") - parser.add_argument("command", help="command to run", choices=cmds) - args = parser.parse_args() - if args.command == "release": - pack(os.getcwd()) - elif args.command == "deploy": - deploy(args.port, args.pkg) + subparsers = parser.add_subparsers(help='command help', dest="cmd") + # release + parser_release = subparsers.add_parser("release", help="release APP") + # deploy + parser_deploy = subparsers.add_parser("deploy", help="release APP") + parser_deploy.add_argument('-p', "--port", default=8888, type=int, help="deploy server port") + parser_deploy.add_argument("--pkg", default="", type=str, help="app package path") + # i18n + parser_i18n = subparsers.add_parser("i18n", help="release APP") + parser_i18n.add_argument("-k", "--keywords", nargs="+", type=str, default=["_", "tr"], help="translate function keywords to search") + parser_i18n.add_argument("-r", action="store_true", help="recursive search dir") + parser_i18n.add_argument("-e", "--exts", nargs="+", type=str, default=[".c", ".cpp", ".h", ".hpp", ".py"], help="file extention to search") + parser_i18n.add_argument("-l", "--locales", nargs="+", type=str, default=["en", "zh"], help="locals of region, like en zh ja etc.") + parser_i18n.add_argument("-o", "--out", type=str, default="locales", help="translation files output directory") + parser_i18n.add_argument("-d", "--dir", type=str, help="where to search", required=True) + + args = parser.parse_args() + if args.cmd == "release": + pack(os.getcwd()) + elif args.cmd == "deploy": + deploy(args.port, args.pkg) + elif args.cmd == "i18n": + i18n_main(args.dir, args.keywords, args.exts, args.out, args.r, args.locales) diff --git a/tools/maixtool/maixtool/version.py b/tools/maixtool/maixtool/version.py index dd1a5a29..0999a0ff 100644 --- a/tools/maixtool/maixtool/version.py +++ b/tools/maixtool/maixtool/version.py @@ -1,4 +1,4 @@ -__version__ = "1.3.3" +__version__ = "1.3.4"