~aleteoryx/muditaos

f3566bcecc2747a79cfa75157caea8274298f5c0 — Krzysztof Móżdżyński 5 years ago 1521858
[EGD-4591] Change input language parser to load from files

Completely independent input language files and display language files.
Input language files are now loaded based on number of files in
"profiles" folder.
InputLanguageSettings window now shows input languages based on their
"filetype" value andfiles in "profiles" folder.
M changelog.md => changelog.md +1 -0
@@ 10,6 10,7 @@
### Changed

* Input keyboard language files parser from KBD to JSON.
* Input language files are now loaded based on files in "profiles" folder.

### Fixed


M doc/i18n.md => doc/i18n.md +5 -12
@@ 24,16 24,7 @@ The keys on the left side refer to the values on the right side. These values ar

### Keyboard input language

The first four values of JSON language files tell MuditaOS, which keyboard input language is the specific language setting using:
```c++
"common_kbd_lower": "English_lower",
"common_kbd_upper": "English_upper",
"common_kbd_numeric": "numeric",
"common_kbd_phone": "phone",
(...)
```

These values are names of JSON files, which are located in [the image/assets/profiles folder](../image/assets/profiles/).
Keyboard input language files have JSON extension and are located in [the image/assets/profiles folder](../image/assets/profiles/).

Every language has its own files for upper and lower letters. Here's an example of a working JSON file for `English_lower.json`:
```c++


@@ 57,9 48,11 @@ The first value declares the type of this file:
- `normal` - they are shown in input language settings, user can change it through GUI (e.g. from English to Polish).
- `special` - they won't show in input language settings, they can be modified only in code (e.g. numeric keyboard).

Normal-type files will be displayed in settings by their filename (e.g. English_lower will be shown as English). When you add a new input language you should always include files for lower and upper letters for it.
Normal-type files will be displayed in settings by their filename (e.g. `English_lower.json` and `English_upper.json` will be shown as `English`). When you add a new input language you should always include files for lower and upper letters for it.

Next key-value pairs includes code of the key (key) and characters available under this key (value).
Next key-value pairs includes code of the key (key) and characters available under this key (value). However, there are two buttons with hexadecimal number (under `61` and `63` buttons). Their value tells MuditaOS what they do:
- `0x0A` - after shortpress (explained below) button change input mode (from lower letters to upper, from upper to numbers etc.). After longpress they show special characters window.
- `0x08` - after shortpress button deletes character.

Files naming pattern should be: `<language>_<lower/upper>`, eg. correct implementation of Rodian input language should consist of two files: `Rodian_lower.json` and `Rodian_upper.json`.


M image/assets/lang/Deutsch.json => image/assets/lang/Deutsch.json +0 -3
@@ 1,7 1,4 @@
{
  "common_kbd_lower": "Deutsch_lower",
  "common_kbd_upper": "Deutsch_upper",
  "common_kbd_numeric": "numeric",
  "common_add": "ADD",
  "common_open": "OPEN",
  "common_call": "CALL",

M image/assets/lang/English.json => image/assets/lang/English.json +0 -4
@@ 1,8 1,4 @@
{
  "common_kbd_lower": "English_lower",
  "common_kbd_upper": "English_upper",
  "common_kbd_numeric": "numeric",
  "common_kbd_phone": "phone",
  "common_add": "ADD",
  "common_open": "OPEN",
  "common_call": "CALL",

M image/assets/lang/Espanol.json => image/assets/lang/Espanol.json +0 -3
@@ 1,7 1,4 @@
{
  "common_kbd_lower": "Espanol_lower",
  "common_kbd_upper": "Espanol_upper",
  "common_kbd_numeric": "numeric",
  "common_add": "ADD",
  "common_open": "ABIERTA",
  "common_call": "CALL",

M image/assets/lang/Francais.json => image/assets/lang/Francais.json +0 -3
@@ 1,7 1,4 @@
{
  "common_kbd_lower": "Francais_lower",
  "common_kbd_upper": "Francais_upper",
  "common_kbd_numeric": "numeric",
  "common_add": "ADD",
  "common_open": "OPEN",
  "common_call": "CALL",

M image/assets/lang/Polski.json => image/assets/lang/Polski.json +0 -3
@@ 1,7 1,4 @@
{
  "common_kbd_lower": "Polski_lower",
  "common_kbd_upper": "Polski_upper",
  "common_kbd_numeric": "numeric",
  "common_add": "DODAJ",
  "common_open": "OTWÓRZ",
  "common_call": "DZWOŃ",

M module-apps/application-settings-new/windows/InputLanguageWindow.cpp => module-apps/application-settings-new/windows/InputLanguageWindow.cpp +1 -1
@@ 20,7 20,7 @@ namespace gui
    auto InputLanguageWindow::buildOptionsList() -> std::list<gui::Option>
    {
        std::list<gui::Option> optionsList;
        const auto &langList = loader.getAvailableDisplayLanguages();
        const auto &langList = profiles.getAvailableInputLanguages();
        for (const auto &lang : langList) {
            optionsList.emplace_back(std::make_unique<gui::OptionSettings>(
                lang,

M module-apps/application-settings-new/windows/InputLanguageWindow.hpp => module-apps/application-settings-new/windows/InputLanguageWindow.hpp +2 -1
@@ 4,6 4,7 @@
#pragma once

#include "BaseSettingsWindow.hpp"
#include "Translator.hpp"

namespace gui
{


@@ 18,6 19,6 @@ namespace gui

      private:
        Language selectedLang;
        utils::LangLoader loader;
        Profiles profiles;
    };
} // namespace gui

M module-gui/gui/input/Profile.cpp => module-gui/gui/input/Profile.cpp +10 -2
@@ 10,14 10,22 @@
namespace gui
{

    Profile::Profile(const std::string &filepath) : name(filepath), inputChars(createJson(filepath))
    {}
    Profile::Profile(const std::filesystem::path &filepath)
    {
        name       = filepath.stem();
        inputChars = createJson(filepath);
    }

    const std::string &Profile::getName() noexcept
    {
        return name;
    }

    const json11::Json &Profile::getJson() const noexcept
    {
        return inputChars;
    }

    const json11::Json Profile::createJson(const std::string &filepath)
    {
        auto fd = std::fopen(filepath.c_str(), "r");

M module-gui/gui/input/Profile.hpp => module-gui/gui/input/Profile.hpp +3 -1
@@ 7,6 7,7 @@
#include <vector>
#include <map>
#include "json/json11.hpp"
#include <filesystem>

namespace gui
{


@@ 22,10 23,11 @@ namespace gui
      public:
        static constexpr uint32_t none_key = 0;
        Profile()                          = default;
        explicit Profile(const std::string &filepath);
        explicit Profile(const std::filesystem::path &filepath);

        [[nodiscard]] const std::string &getName() noexcept;
        [[nodiscard]] uint32_t getCharKey(bsp::KeyCodes code, uint32_t times);
        [[nodiscard]] const json11::Json &getJson() const noexcept;
    };

} /* namespace gui */

M module-gui/gui/input/Translator.cpp => module-gui/gui/input/Translator.cpp +42 -19
@@ 4,16 4,17 @@
#include "Translator.hpp"
#include "log/log.hpp"
#include <algorithm>
#include <log/log.hpp>
#include <filesystem>
#include "i18n/i18n.hpp"

namespace gui
{
    namespace
    namespace filetype
    {
        constexpr auto profilesFolder = "assets/profiles";
        constexpr auto extension      = ".json";
    } // namespace
        constexpr auto jsonKey = "filetype";
        constexpr auto normal  = "normal";
        constexpr auto special = "special";
    } // namespace filetype

    void recon_long_press(InputEvent &evt, const RawKey &key, const RawKey &prev_key_press, uint32_t time)
    {


@@ 202,24 203,46 @@ namespace gui
        }
    }

    std::vector<std::string> Profiles::getProfilesPaths()
    std::vector<std::string> Profiles::getProfilesNames()
    {
        std::vector<std::string> profileFiles;
        LOG_INFO("Scanning %s profiles folder: %s", extension, profilesFolder);
        std::vector<std::string> profilesNames;
        LOG_INFO("Scanning %s profiles folder: %s",
                 utils::files::jsonExtension,
                 utils::localize.InputLanguageDirPath.c_str());

        for (const auto &entry : std::filesystem::directory_iterator(profilesFolder)) {
            profileFiles.push_back(std::filesystem::path(entry.path()));
        for (const auto &entry : std::filesystem::directory_iterator(utils::localize.InputLanguageDirPath)) {
            profilesNames.push_back(std::filesystem::path(entry.path().stem()));
        }

        LOG_INFO("Total number of profiles: %u", static_cast<unsigned int>(profileFiles.size()));
        return profileFiles;
        LOG_INFO("Total number of profiles: %u", static_cast<unsigned int>(profilesNames.size()));
        return profilesNames;
    }

    std::vector<std::string> Profiles::getAvailableInputLanguages()
    {
        std::vector<std::string> profilesNames = getProfilesNames(), availableProfiles;

        for (auto &name : profilesNames) {
            auto profile = get().profilesList[name];
            if (profile.getJson()[filetype::jsonKey] == filetype::normal) {
                auto breakSignPosition            = name.find_last_of(utils::files::breakSign);
                std::string displayedLanguageName = name.substr(0, breakSignPosition);

                if (std::find(availableProfiles.begin(), availableProfiles.end(), displayedLanguageName) ==
                    availableProfiles.end()) {
                    availableProfiles.push_back(displayedLanguageName);
                }
            }
        }
        return availableProfiles;
    }

    void Profiles::init()
    {
        std::vector<std::string> profileFilesPaths = getProfilesPaths();
        for (std::string filePath : profileFilesPaths) {
            if (std::size(filePath)) {
        std::vector<std::string> profilesNames = getProfilesNames();
        for (const auto &profileName : profilesNames) {
            if (!profileName.empty()) {
                auto filePath = utils::localize.InputLanguageDirPath / (profileName + utils::files::jsonExtension);
                loadProfile(filePath);
            }
        }


@@ 240,16 263,16 @@ namespace gui

    Profile &Profiles::get(const std::string &name)
    {
        auto filepath = std::string(profilesFolder) + "/" + name + extension;
        std::filesystem::path filepath = utils::localize.InputLanguageDirPath / (name + utils::files::jsonExtension);
        // if profile not in profile map -> load
        if (std::size(filepath) == 0) {
        if (filepath.empty()) {
            LOG_ERROR("Request for nonexistent profile: %s", filepath.c_str());
            return get().empty;
        }
        if (get().profilesList.find(filepath) == get().profilesList.end()) {
        if (get().profilesList.find(name) == get().profilesList.end()) {
            get().loadProfile(filepath);
        }
        return get().profilesList[filepath];
        return get().profilesList[name];
    }

} /* namespace gui */

M module-gui/gui/input/Translator.hpp => module-gui/gui/input/Translator.hpp +2 -1
@@ 57,13 57,14 @@ namespace gui
        std::map<std::string, gui::Profile> profilesList = {};

        void loadProfile(const std::string &filepath);
        std::vector<std::string> getProfilesPaths();
        std::vector<std::string> getProfilesNames();
        void init();
        Profile empty;

        static Profiles &get();

      public:
        std::vector<std::string> getAvailableInputLanguages();
        Profile &get(const std::string &name);
    };


M module-gui/gui/widgets/InputMode.cpp => module-gui/gui/widgets/InputMode.cpp +10 -5
@@ 8,10 8,10 @@

/// input mode strings - as these are stored in json (in files...)
const std::map<InputMode::Mode, std::string> input_mode = {
    {InputMode::digit, "common_kbd_numeric"},
    {InputMode::ABC, "common_kbd_upper"},
    {InputMode::abc, "common_kbd_lower"},
    {InputMode::phone, "common_kbd_phone"},
    {InputMode::digit, "numeric"},
    {InputMode::ABC, "upper"},
    {InputMode::abc, "lower"},
    {InputMode::phone, "phone"},
};

static std::string getInputName(InputMode::Mode m)


@@ 61,7 61,12 @@ void InputMode::next()

const std::string &InputMode::get()
{
    return utils::localize.getInputLanguage(input_mode.at(modeNow()));
    auto actualInputMode = input_mode.at(modeNow());
    if (actualInputMode == input_mode.find(InputMode::digit)->second ||
        actualInputMode == input_mode.find(InputMode::phone)->second) {
        return input_mode.at(modeNow());
    }
    return utils::localize.getInputLanguage(actualInputMode);
}

void InputMode::show_input_type()

M module-utils/i18n/i18n.cpp => module-utils/i18n/i18n.cpp +21 -19
@@ 5,16 5,12 @@
#include "i18n.hpp"
#include "Utils.hpp"
#include <cstdio>
#include <filesystem>
#include <purefs/filesystem_paths.hpp>

namespace utils
{
    namespace
    {
        const auto LanguageDirPath = std::filesystem::path{"assets"} / "lang";
        constexpr auto extension   = ".json";

        auto returnNonEmptyString(const std::string &str, const std::string &ret) -> const std::string &
        {
            return str.empty() ? ret : str;


@@ 24,7 20,7 @@ namespace utils
    i18n localize;
    json11::Json LangLoader::createJson(const std::string &filename)
    {
        const auto path = LanguageDirPath / (filename + extension);
        const auto path = utils::localize.DisplayLanguageDirPath / (filename + utils::files::jsonExtension);
        auto fd         = std::fopen(path.c_str(), "r");
        if (fd == nullptr) {
            LOG_FATAL("Error during opening file %s", path.c_str());


@@ 55,7 51,16 @@ namespace utils
    std::vector<Language> LangLoader::getAvailableDisplayLanguages() const
    {
        std::vector<std::string> languageNames;
        for (const auto &entry : std::filesystem::directory_iterator(LanguageDirPath)) {
        for (const auto &entry : std::filesystem::directory_iterator(utils::localize.DisplayLanguageDirPath)) {
            languageNames.push_back(std::filesystem::path(entry.path()).stem());
        }
        return languageNames;
    }

    std::vector<Language> LangLoader::getAvailableInputLanguages() const
    {
        std::vector<std::string> languageNames;
        for (const auto &entry : std::filesystem::directory_iterator(utils::localize.InputLanguageDirPath)) {
            languageNames.push_back(std::filesystem::path(entry.path()).stem());
        }
        return languageNames;


@@ 63,25 68,22 @@ namespace utils

    void i18n::setInputLanguage(const Language &lang)
    {
        if (lang == currentInputLanguage) {
        if (lang == inputLanguage) {
            return;
        }
        currentInputLanguage = lang;
        if (lang == fallbackLanguageName) {
            inputLanguage = fallbackLanguage;
        }
        else {
            json11::Json pack = loader.createJson(lang);
            inputLanguage     = pack;
        }
        inputLanguage = lang;
    }
    const std::string &i18n::getInputLanguage(const std::string &str)

    const std::string &i18n::getInputLanguage(const std::string &inputMode)
    {
        // if language pack returned nothing then try default language
        if (inputLanguage[str].string_value().empty()) {
            return returnNonEmptyString(fallbackLanguage[str].string_value(), str);
        if (inputLanguage.empty()) {
            inputLanguageFilename = fallbackLanguageName + utils::files::breakSign + inputMode;
        }
        else {
            inputLanguageFilename = inputLanguage + utils::files::breakSign + inputMode;
        }
        return returnNonEmptyString(inputLanguage[str].string_value(), str);
        return inputLanguageFilename;
    }

    const std::string &i18n::get(const std::string &str)

M module-utils/i18n/i18n.hpp => module-utils/i18n/i18n.hpp +14 -4
@@ 5,16 5,24 @@
#include "json/json11.hpp"
#include <string>
#include <vfs.hpp>
#include <filesystem>

using Language = std::string;

namespace utils
{
    namespace files
    {
        constexpr auto jsonExtension = ".json";
        constexpr auto breakSign     = "_";
    } // namespace files

    class LangLoader
    {
      public:
        virtual ~LangLoader() = default;
        std::vector<Language> getAvailableDisplayLanguages() const;
        std::vector<Language> getAvailableInputLanguages() const;
        json11::Json createJson(const std::string &filename);
    };



@@ 22,20 30,22 @@ namespace utils
    {
      private:
        json11::Json displayLanguage;
        json11::Json inputLanguage;
        json11::Json fallbackLanguage; // backup language if item not found
        LangLoader loader;
        Language fallbackLanguageName;
        Language inputLanguage = fallbackLanguageName;
        Language inputLanguageFilename;
        Language currentDisplayLanguage = fallbackLanguageName;
        Language currentInputLanguage   = fallbackLanguageName;
        bool backupLanguageInitializer  = false;

      public:
        static constexpr auto DefaultLanguage = "English";
        static constexpr auto DefaultLanguage              = "English";
        const std::filesystem::path DisplayLanguageDirPath = "assets/lang";
        const std::filesystem::path InputLanguageDirPath   = "assets/profiles";

        virtual ~i18n() = default;
        void setInputLanguage(const Language &lang);
        const std::string &getInputLanguage(const std::string &str);
        const std::string &getInputLanguage(const std::string &inputMode);
        const std::string &get(const std::string &str);
        void setDisplayLanguage(const Language &lang);
        void setFallbackLanguage(const Language &lang);