From f28c76eb53ea0439761724b881201df660fddce9 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 24 Aug 2025 00:48:37 +0200 Subject: [PATCH] i18n implementation basics --- Data/data/i18n/core/en-GB.i18n | 7 ++ Data/data/i18n/core/en-US.i18n | 7 ++ Data/data/i18n/core/fr-FR.i18n | 7 ++ Data/data/i18n/core/nl-BE.i18n | 7 ++ Data/data/i18n/core/nl-NL.i18n | 7 ++ Data/data/i18n/launcher/en-GB.i18n | 3 + Data/data/i18n/launcher/en-US.i18n | 3 + Data/data/i18n/launcher/fr-FR.i18n | 3 + Data/data/i18n/launcher/nl-BE.i18n | 3 + Data/data/i18n/launcher/nl-NL.i18n | 3 + Tactility/Include/Tactility/i18n/Core.h | 17 ++++ Tactility/Include/Tactility/i18n/I18n.h | 24 ++++++ Tactility/Include/Tactility/i18n/Launcher.h | 13 +++ Tactility/Source/app/launcher/Launcher.cpp | 21 +++-- Tactility/Source/i18n/I18n.cpp | 85 ++++++++++++++++++ Translations/.gitignore | 2 + Translations/README.md | 25 ++++++ Translations/Translations.ods | Bin 0 -> 13439 bytes Translations/generate-all.py | 12 +++ Translations/generate.py | 90 ++++++++++++++++++++ 20 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 Data/data/i18n/core/en-GB.i18n create mode 100644 Data/data/i18n/core/en-US.i18n create mode 100644 Data/data/i18n/core/fr-FR.i18n create mode 100644 Data/data/i18n/core/nl-BE.i18n create mode 100644 Data/data/i18n/core/nl-NL.i18n create mode 100644 Data/data/i18n/launcher/en-GB.i18n create mode 100644 Data/data/i18n/launcher/en-US.i18n create mode 100644 Data/data/i18n/launcher/fr-FR.i18n create mode 100644 Data/data/i18n/launcher/nl-BE.i18n create mode 100644 Data/data/i18n/launcher/nl-NL.i18n create mode 100644 Tactility/Include/Tactility/i18n/Core.h create mode 100644 Tactility/Include/Tactility/i18n/I18n.h create mode 100644 Tactility/Include/Tactility/i18n/Launcher.h create mode 100644 Tactility/Source/i18n/I18n.cpp create mode 100644 Translations/.gitignore create mode 100644 Translations/README.md create mode 100644 Translations/Translations.ods create mode 100644 Translations/generate-all.py create mode 100644 Translations/generate.py diff --git a/Data/data/i18n/core/en-GB.i18n b/Data/data/i18n/core/en-GB.i18n new file mode 100644 index 00000000..a04e3142 --- /dev/null +++ b/Data/data/i18n/core/en-GB.i18n @@ -0,0 +1,7 @@ +OK +Yes +No +Cancel +Retry +Close +Open diff --git a/Data/data/i18n/core/en-US.i18n b/Data/data/i18n/core/en-US.i18n new file mode 100644 index 00000000..a04e3142 --- /dev/null +++ b/Data/data/i18n/core/en-US.i18n @@ -0,0 +1,7 @@ +OK +Yes +No +Cancel +Retry +Close +Open diff --git a/Data/data/i18n/core/fr-FR.i18n b/Data/data/i18n/core/fr-FR.i18n new file mode 100644 index 00000000..2f035013 --- /dev/null +++ b/Data/data/i18n/core/fr-FR.i18n @@ -0,0 +1,7 @@ +OK +Oui +Non +Annuler +Réessayer +Fermer +Ouvrir diff --git a/Data/data/i18n/core/nl-BE.i18n b/Data/data/i18n/core/nl-BE.i18n new file mode 100644 index 00000000..1a5002f4 --- /dev/null +++ b/Data/data/i18n/core/nl-BE.i18n @@ -0,0 +1,7 @@ +OK +Ja +Nee +Annuleren +Opnieuw +Sluiten +Openen diff --git a/Data/data/i18n/core/nl-NL.i18n b/Data/data/i18n/core/nl-NL.i18n new file mode 100644 index 00000000..1a5002f4 --- /dev/null +++ b/Data/data/i18n/core/nl-NL.i18n @@ -0,0 +1,7 @@ +OK +Ja +Nee +Annuleren +Opnieuw +Sluiten +Openen diff --git a/Data/data/i18n/launcher/en-GB.i18n b/Data/data/i18n/launcher/en-GB.i18n new file mode 100644 index 00000000..5226a6e5 --- /dev/null +++ b/Data/data/i18n/launcher/en-GB.i18n @@ -0,0 +1,3 @@ +Apps +Files +Settings diff --git a/Data/data/i18n/launcher/en-US.i18n b/Data/data/i18n/launcher/en-US.i18n new file mode 100644 index 00000000..5226a6e5 --- /dev/null +++ b/Data/data/i18n/launcher/en-US.i18n @@ -0,0 +1,3 @@ +Apps +Files +Settings diff --git a/Data/data/i18n/launcher/fr-FR.i18n b/Data/data/i18n/launcher/fr-FR.i18n new file mode 100644 index 00000000..663a29ea --- /dev/null +++ b/Data/data/i18n/launcher/fr-FR.i18n @@ -0,0 +1,3 @@ +Appli +Fichiers +Réglages diff --git a/Data/data/i18n/launcher/nl-BE.i18n b/Data/data/i18n/launcher/nl-BE.i18n new file mode 100644 index 00000000..89a1e21f --- /dev/null +++ b/Data/data/i18n/launcher/nl-BE.i18n @@ -0,0 +1,3 @@ +Apps +Bestanden +Instellingen diff --git a/Data/data/i18n/launcher/nl-NL.i18n b/Data/data/i18n/launcher/nl-NL.i18n new file mode 100644 index 00000000..89a1e21f --- /dev/null +++ b/Data/data/i18n/launcher/nl-NL.i18n @@ -0,0 +1,3 @@ +Apps +Bestanden +Instellingen diff --git a/Tactility/Include/Tactility/i18n/Core.h b/Tactility/Include/Tactility/i18n/Core.h new file mode 100644 index 00000000..591664cb --- /dev/null +++ b/Tactility/Include/Tactility/i18n/Core.h @@ -0,0 +1,17 @@ +#pragma once + +// WARNING: This file is auto-generated. Do not edit manually. + +namespace tt::i18n::core { + +enum class Text { + OK = 0, + YES = 1, + NO = 2, + CANCEL = 3, + RETRY = 4, + CLOSE = 5, + OPEN = 6, +}; + +} diff --git a/Tactility/Include/Tactility/i18n/I18n.h b/Tactility/Include/Tactility/i18n/I18n.h new file mode 100644 index 00000000..9e091045 --- /dev/null +++ b/Tactility/Include/Tactility/i18n/I18n.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace tt::i18n { + +class IndexedText { + +public: + + virtual ~IndexedText() = default; + + virtual const std::string& get(int index) const = 0; + + template + const std::string& get(EnumType value) const { return get(static_cast(value)); } + + const std::string& operator[](const int index) const { return get(index); } +}; + +std::shared_ptr loadIndexedText(const std::string& path); + +} \ No newline at end of file diff --git a/Tactility/Include/Tactility/i18n/Launcher.h b/Tactility/Include/Tactility/i18n/Launcher.h new file mode 100644 index 00000000..2a316726 --- /dev/null +++ b/Tactility/Include/Tactility/i18n/Launcher.h @@ -0,0 +1,13 @@ +#pragma once + +// WARNING: This file is auto-generated. Do not edit manually. + +namespace tt::i18n::launcher { + +enum class Text { + APPS = 0, + FILES = 1, + SETTINGS = 2, +}; + +} diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index af2e8ff5..82ec55af 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -2,15 +2,18 @@ #include "Tactility/app/AppRegistration.h" #include "Tactility/service/loader/Loader.h" +#include "Tactility/i18n/Launcher.h" + #include #include #include - -#define TAG "launcher" +#include namespace tt::app::launcher { +constexpr auto* TAG = "Launcher"; + static void onAppPressed(TT_UNUSED lv_event_t* e) { auto* appId = (const char*)lv_event_get_user_data(e); service::loader::startApp(appId); @@ -52,8 +55,11 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char } class LauncherApp : public App { + std::shared_ptr launcherText = i18n::loadIndexedText("/data/i18n/launcher"); void onCreate(TT_UNUSED AppContext& app) override { + assert(launcherText != nullptr); + BootProperties boot_properties; if (loadBootProperties(boot_properties) && !boot_properties.autoStartAppId.empty()) { TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str()); @@ -87,9 +93,14 @@ class LauncherApp : public App { auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png"); auto files_icon_path = paths->getSystemPathLvgl("icon_files.png"); auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png"); - createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0); - createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding); - createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding); + + const auto& apps_title = launcherText->get(i18n::launcher::Text::APPS); + const auto& files_title = launcherText->get(i18n::launcher::Text::FILES); + const auto& settings_title = launcherText->get(i18n::launcher::Text::SETTINGS); + + createAppButton(wrapper, apps_title.c_str(), apps_icon_path.c_str(), "AppList", 0); + createAppButton(wrapper, files_title.c_str(), files_icon_path.c_str(), "Files", padding); + createAppButton(wrapper, settings_title.c_str(), settings_icon_path.c_str(), "Settings", padding); } }; diff --git a/Tactility/Source/i18n/I18n.cpp b/Tactility/Source/i18n/I18n.cpp new file mode 100644 index 00000000..8ddc8fa3 --- /dev/null +++ b/Tactility/Source/i18n/I18n.cpp @@ -0,0 +1,85 @@ +#include "Tactility/i18n/I18n.h" +#include "Tactility/file/FileLock.h" + +#include +#include + +namespace tt::i18n { + +constexpr auto* TAG = "I18n"; +static std::string ERROR_RESULT = "TRANSLATION_ERROR"; + +class IndexedTextImplementation : IndexedText { + + std::vector data; + +public: + + explicit IndexedTextImplementation(std::vector data) : data(std::move(data)) {} + + const std::string& get(const int index) const override { + if (index < data.size()) { + return data[index]; + } else { + return ERROR_RESULT; + } + } +}; + +static std::string getDesiredLocale() { + // TODO: Implement locale settings + return "nl-NL"; +} + +static std::string getFallbackLocale() { + // TODO: Implement locale settings + return "en-GB"; +} + +static FILE* openI18nFile(const std::string& path) { + auto locale = getDesiredLocale(); + auto desired_file_path = std::format("{}/{}.i18n", path, locale); + auto* file = fopen(desired_file_path.c_str(), "r"); + if (file == nullptr) { + auto fallback_locale = getFallbackLocale(); + TT_LOG_W(TAG, "Translations not found for %s at %s", locale.c_str(), desired_file_path.c_str()); + auto fallback_file_path = std::format("{}/{}.i18n", path, getFallbackLocale()); + file = fopen(fallback_file_path.c_str(), "r"); + if (file == nullptr) { + TT_LOG_W(TAG, "Fallback translations not found for %s at %s", fallback_locale.c_str(), fallback_file_path.c_str()); + } + } + return file; +} + +std::shared_ptr loadIndexedText(const std::string& path) { + std::vector data; + + // We lock on folder level, because file is TBD + file::withLock(path, [&path, &data] { + auto* file = openI18nFile(path); + if (file != nullptr) { + char line[1024]; + // TODO: move to file::readLines(filePath, skipEndline, callback) + while (fgets(line, sizeof(line), file) != nullptr) { + // Strip newline + size_t line_length = strlen(line); + if (line_length > 0 && line[line_length - 1] == '\n') { + line[line_length - 1] = '\0'; + } + // Publish + data.push_back(line); + } + fclose(file); + } + }); + + if (data.empty()) { + return nullptr; + } + + auto result = std::make_shared(data); + return std::reinterpret_pointer_cast(result); +} + +} diff --git a/Translations/.gitignore b/Translations/.gitignore new file mode 100644 index 00000000..f54462c7 --- /dev/null +++ b/Translations/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.ods# \ No newline at end of file diff --git a/Translations/README.md b/Translations/README.md new file mode 100644 index 00000000..535a2d52 --- /dev/null +++ b/Translations/README.md @@ -0,0 +1,25 @@ +# Translations + +## Requirements + +- Install [LibreOffice Calc](https://libreoffice.org/) +- Ensure you have Python 3 installed (Python 2 is not supported) + +## Steps + +To add new translations or edit existing ones, please follow these steps: + +1. Edit `Translations.ods` (see chapter below) +2. In LibreOffice Calc, select the relevant tab that you want to export +3. Click on `File` -> `Save a copy...` and save the file as `[tabname].csv` (without the "[]") +4. Repeat step 2 and 3 for all tabs that you updated +5. Run `python generate-all.py` + +## Notes + +- Do not commit the CSV files +- When editing the ODS file, make sure you don't paste in formatted data (use CTRL+Shift+V instead of CTRL+V) +- ODS export settings: + - Field delimiter: `,` + - String delimiter: `"` + - Encoding: `UTF-8` \ No newline at end of file diff --git a/Translations/Translations.ods b/Translations/Translations.ods new file mode 100644 index 0000000000000000000000000000000000000000..4728ca6d1f0657fa1a101f542f2ae556dcb8d8d1 GIT binary patch literal 13439 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 F_FwtBqmcjr literal 0 HcmV?d00001 diff --git a/Translations/generate-all.py b/Translations/generate-all.py new file mode 100644 index 00000000..7cf778cb --- /dev/null +++ b/Translations/generate-all.py @@ -0,0 +1,12 @@ +import os + +def generate(csvFile, headerFile, headerNamespace, dataPath): + if os.path.isfile(csvFile): + print(f"Generating {headerFile}") + os.system(f"python generate.py {csvFile} {headerFile} {headerNamespace} {dataPath}") + else: + print(f"Skipping {headerFile} (not found)") + +if __name__ == "__main__": + generate("system.csv", "../Tactility/Include/Tactility/i18n/Core.h", "tt::i18n::core", "../Data/data/i18n/core") + generate("launcher.csv", "../Tactility/Include/Tactility/i18n/Launcher.h", "tt::i18n::launcher", "../Data/data/i18n/launcher") diff --git a/Translations/generate.py b/Translations/generate.py new file mode 100644 index 00000000..166348a8 --- /dev/null +++ b/Translations/generate.py @@ -0,0 +1,90 @@ +import csv +import sys +from typing import List + +def load_csv(path: str, delimiter: str = ",", quotechar: str = '"', encoding: str = "utf-8", skip_header: bool = False) -> List[List[str]]: + """ + Load a CSV file into a list of rows, where each row is a list of strings. + + Args: + path: Path to the CSV file. + delimiter: Field delimiter character. + quotechar: Quote character. + encoding: File encoding. + skip_header: If True, skip the first row (header). + + Returns: + List of rows (list of lists of strings). + """ + rows: List[List[str]] = [] + with open(path, "r", encoding=encoding, newline="") as f: + reader = csv.reader(f, delimiter=delimiter, quotechar=quotechar) + if skip_header: + next(reader, None) + for row in reader: + rows.append(row) + return rows + +def print_help(): + print("Usage: python generate.py [csv_file] [header_file_path] [header_namespace] [i18n_directory]\n\n") + print("\t[csv_file] the CSV file containing the translations, exported from the .ods file") + print("\t[header_file_path] the path to the header file to be generated") + print("\t[header_namespace] the C++ namespace to use for the generated header file") + print("\t[i18n_directory] the directory where the .i18n files will be generated") + +def open_i18n_files(row, i18n_path): + result = [] + for i in range(1, len(row)): + filepath = f"{i18n_path}/{row[i]}.i18n" + print(f"Opening {filepath}") + file = open(filepath, "w") + result.append(file) + return result + +def close_i18n_files(files): + for file in files: + file.close() + +def generate_header(filepath, rows, namespace): + file = open(filepath, "w") + file.write("#pragma once\n\n") + file.write("// WARNING: This file is auto-generated. Do not edit manually.\n\n") + file.write(f"namespace {namespace}") + file.write(" {\n\n") + file.write("enum class Text {\n") + for i in range(1, len(rows)): + key = rows[i][0].upper() + file.write(f" {key} = {i - 1},\n") + file.write("};\n") + file.write("\n}\n") + file.close() + +def translate(rows, language_index, file): + for i in range(1, len(rows)): + value = rows[i][language_index] + if value == "": + value = f"{rows[i][0]}_untranslated" + file.write(value) + file.write("\n") + +if __name__ == "__main__": + if "--help" in sys.argv: + print_help() + sys.exit() + if len(sys.argv) != 5: + print_help() + sys.exit() + csv_file = sys.argv[1] + header_path = sys.argv[2] + header_namespace = sys.argv[3] + i18n_path = sys.argv[4] + rows = load_csv(csv_file) + if len(rows) == 0: + print("Error: CSV file is empty.") + sys.exit(1) + generate_header(header_path, rows, header_namespace) + i18n_files = open_i18n_files(rows[0], i18n_path) + for i in range(0, len(i18n_files)): + i18n_file = i18n_files[i] + translate(rows, i + 1, i18n_file) + close_i18n_files(i18n_files)