From a698461b7e682306395856807faa9ff4e2a47c6a Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Fri, 21 Feb 2025 15:40:37 +0100 Subject: [PATCH 01/18] Add internationalization (i18n) support with language files Introduced an i18n system for localizing app text using key-value language files. Added `I18n.h` and `I18n.cpp` to manage translations, integrated i18n into welcome screens, and updated `MainHelper` to facilitate setup. Created a sample English language file (`i18n/en.txt`). --- firmware/src/core/utils/I18n.cpp | 44 ++++++++++++++++++++++++++ firmware/src/core/utils/I18n.h | 35 ++++++++++++++++++++ firmware/src/core/utils/MainHelper.cpp | 17 +++++++--- firmware/src/core/utils/MainHelper.h | 2 ++ firmware/src/main.cpp | 1 + i18n/en.txt | 5 +++ scripts/copy_files_to_littlefs.py | 5 +++ 7 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 firmware/src/core/utils/I18n.cpp create mode 100644 firmware/src/core/utils/I18n.h create mode 100644 i18n/en.txt diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp new file mode 100644 index 00000000..370cb2fa --- /dev/null +++ b/firmware/src/core/utils/I18n.cpp @@ -0,0 +1,44 @@ +#include "I18n.h" +#include + +std::map I18n::translations; + +bool I18n::load(const char *filename) { + File file = LittleFS.open(filename, "r"); + if (!file) { + Serial.println("Failed to open language file"); + return false; + } + + translations.clear(); + while (file.available()) { + String line = file.readStringUntil('\n'); + line.trim(); + + if (line.length() == 0 || line.startsWith("#")) + continue; + + int sepIndex = line.indexOf('='); + if (sepIndex > 0) { + String key = line.substring(0, sepIndex); + String value = line.substring(sepIndex + 1); + key.trim(); + value.trim(); + translations[key] = value; + } + } + file.close(); + return true; +} + +String I18n::get(const String &key) { + if (translations.find(key) != translations.end()) { + return translations[key]; + } + return key; +} + +void I18n::replacePlaceholder(String &str, int index, const String &value) { + String placeholder = "\\" + String(index); // e.g., \1, \2 + str.replace(placeholder, value); +} \ No newline at end of file diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h new file mode 100644 index 00000000..09f47541 --- /dev/null +++ b/firmware/src/core/utils/I18n.h @@ -0,0 +1,35 @@ +#ifndef I18N_H +#define I18N_H + +#include +#include + +class I18n { +public: + static bool load(const char *filename); + + static String get(const String &key); + + template + static String get(const String &key, Args... args) { + String result = get(key); + replacePlaceholders(result, args...); + return result; + } + +private: + static std::map translations; + + static void replacePlaceholder(String &str, int index, const String &value); + + template + static void replacePlaceholders(String &str, T value, Args... args) { + static int index = 1; + replacePlaceholder(str, index++, String(value)); + replacePlaceholders(str, args...); + } + + static void replacePlaceholders(String &) {} +}; + +#endif // I18N_H diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index 9812cf2a..a6c77ce1 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -419,7 +419,7 @@ void MainHelper::showWelcome() { s_screenManager->setFontColor(TFT_WHITE); s_screenManager->selectScreen(0); - s_screenManager->drawCentreString("Welcome", ScreenCenterX, ScreenCenterY, 29); + s_screenManager->drawCentreString(I18n::get("welcome"), ScreenCenterX, ScreenCenterY, 29); if (GIT_BRANCH != "main" && GIT_BRANCH != "unknown" && GIT_BRANCH != "HEAD") { s_screenManager->setFontColor(TFT_RED); s_screenManager->drawCentreString(GIT_BRANCH, ScreenCenterX, ScreenCenterY - 40, 15); @@ -428,11 +428,11 @@ void MainHelper::showWelcome() { } s_screenManager->selectScreen(1); - s_screenManager->drawCentreString("Info Orbs", ScreenCenterX, ScreenCenterY - 50, 22); - s_screenManager->drawCentreString("by", ScreenCenterX, ScreenCenterY - 5, 22); - s_screenManager->drawCentreString("brett.tech", ScreenCenterX, ScreenCenterY + 30, 22); + s_screenManager->drawCentreString(I18n::get("infoorbs"), ScreenCenterX, ScreenCenterY - 50, 22); + s_screenManager->drawCentreString(I18n::get("by"), ScreenCenterX, ScreenCenterY - 5, 22); + s_screenManager->drawCentreString(I18n::get("brett.tech"), ScreenCenterX, ScreenCenterY + 30, 22); s_screenManager->setFontColor(TFT_RED); - s_screenManager->drawCentreString("version: 1.1.0", ScreenCenterX, ScreenCenterY + 65, 15); + s_screenManager->drawCentreString(I18n::get("version", "1.1.0"), ScreenCenterX, ScreenCenterY + 65, 15); s_screenManager->selectScreen(2); s_screenManager->drawJpg(0, 0, logo_start, logo_end - logo_start); @@ -504,3 +504,10 @@ void MainHelper::watchdogInit() { void MainHelper::watchdogReset() { esp_task_wdt_reset(); } + +void MainHelper::setupI18n() { + String langFile = "i18n/en.txt"; // TODO add to GUI + if (I18n::load(langFile.c_str())) { + Log.noticeln("Loaded language file: %s", langFile.c_str()); + } +} diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index 8fb89bc5..1e60c81e 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -3,6 +3,7 @@ #include "Button.h" #include "ConfigManager.h" +#include "I18n.h" #include "OrbsWiFiManager.h" #include "ScreenManager.h" #include "ShowMemoryUsage.h" @@ -79,6 +80,7 @@ class MainHelper { static void watchdogReset(); static void updateBrightnessByTime(uint8_t hour24); + static void setupI18n(); }; #endif \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 38cfe5c6..545ec640 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -60,6 +60,7 @@ void setup() { MainHelper::setupLittleFS(); MainHelper::setupConfig(); MainHelper::setupButtons(); + MainHelper::setupI18n(); MainHelper::showWelcome(); pinMode(BUSY_PIN, OUTPUT); diff --git a/i18n/en.txt b/i18n/en.txt new file mode 100644 index 00000000..f506ab2f --- /dev/null +++ b/i18n/en.txt @@ -0,0 +1,5 @@ +welcome=Welcome +infoorbs=InfoOrbs +by=by +brett.tech=brett.tech +version=Version \1 \ No newline at end of file diff --git a/scripts/copy_files_to_littlefs.py b/scripts/copy_files_to_littlefs.py index b893385e..d49f8c5e 100644 --- a/scripts/copy_files_to_littlefs.py +++ b/scripts/copy_files_to_littlefs.py @@ -18,6 +18,11 @@ # Map macros to directories and their respective files # The final filename is constructed as "dir + file" so the dir should end with a "/" unless you know what you are doing embed_map = { + "CONFIG_H != 0": [ # Always true + ["i18n/", # Source directory + ["en.txt"], # Files + 0], # Do not skip parent directories + ], "USE_CLOCK_CUSTOM > 0": [ # Condition ["images/clock/CustomClock0/", # Source directory ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], # Files From a9d602942d221785ddd34b2c0538f786ac9871c0 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 04:16:53 +0100 Subject: [PATCH 02/18] Enhance embedding logic to support wildcard file matching Updated the scripts to include wildcard file matching using `glob` for greater flexibility in file selection. Refactored and fixed formatting inconsistencies for improved readability. These changes streamline file embedding and copying processes in build workflows. --- scripts/copy_files_to_littlefs.py | 92 +++++++++++++++++-------------- scripts/embed_files.py | 35 ++++++++---- 2 files changed, 76 insertions(+), 51 deletions(-) diff --git a/scripts/copy_files_to_littlefs.py b/scripts/copy_files_to_littlefs.py index d49f8c5e..43243056 100644 --- a/scripts/copy_files_to_littlefs.py +++ b/scripts/copy_files_to_littlefs.py @@ -6,7 +6,7 @@ # You do NOT need to run it manually ########################################################################################################### -import re, shutil, os, os.path +import re, shutil, os, os.path, glob from SCons.Script import Import # Extract macros from the config header file @@ -18,60 +18,61 @@ # Map macros to directories and their respective files # The final filename is constructed as "dir + file" so the dir should end with a "/" unless you know what you are doing embed_map = { - "CONFIG_H != 0": [ # Always true - ["i18n/", # Source directory - ["en.txt"], # Files - 0], # Do not skip parent directories + "CONFIG_H != 0": [ # Always true + ["i18n/", # Source directory + ["*.txt"], # Files + 0], # Do not skip parent directories ], - "USE_CLOCK_CUSTOM > 0": [ # Condition - ["images/clock/CustomClock0/", # Source directory - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], # Files - 2], # Skip first two levels while copying ("images/clock/") + "USE_CLOCK_CUSTOM > 0": [ # Condition + ["images/clock/CustomClock0/", # Source directory + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + # Files + 2], # Skip first two levels while copying ("images/clock/") ], "USE_CLOCK_CUSTOM > 1": [ - ["images/clock/CustomClock1/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock1/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 2": [ - ["images/clock/CustomClock2/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock2/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 3": [ - ["images/clock/CustomClock3/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock3/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 4": [ - ["images/clock/CustomClock4/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock4/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 5": [ - ["images/clock/CustomClock5/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock5/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 6": [ - ["images/clock/CustomClock6/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock6/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 7": [ - ["images/clock/CustomClock7/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock7/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 8": [ - ["images/clock/CustomClock8/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock8/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], "USE_CLOCK_CUSTOM > 9": [ - ["images/clock/CustomClock9/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], - 2], + ["images/clock/CustomClock9/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], + 2], ], } @@ -79,6 +80,7 @@ board = env.BoardConfig() + # Function to extract macros and their values from a header file def extract_macros_with_values(file_path): macros = {} @@ -109,6 +111,7 @@ def extract_macros_with_values(file_path): print(f"Warning: Header file '{file_path}' not found.") return macros + def copy_files_to_builddir(files): for srcFile, dstFile in files: base = os.path.dirname(dstFile) @@ -116,6 +119,7 @@ def copy_files_to_builddir(files): os.makedirs(finalPath, exist_ok=True) shutil.copy(srcFile, finalPath) + def clear_directory(directory): if os.path.exists(directory) and os.path.isdir(directory): for item in os.listdir(directory): @@ -125,7 +129,8 @@ def clear_directory(directory): elif os.path.isdir(item_path): shutil.rmtree(item_path) # Remove directory and its contents else: - print(f"The path {directory} does not exist or is not a directory.") + print(f"The path {directory} does not exist or is not a directory.") + def action(): # Clear LittleFS directory first @@ -134,13 +139,15 @@ def action(): clear_directory(out_dir) # Extract -D flags from BUILD_FLAGS build_flags = env.get("BUILD_FLAGS", []) - cpp_defines = {flag[2:].split("=")[0].strip(): flag[2:].split("=")[1].strip() if "=" in flag else None for flag in build_flags if flag.startswith("-D")} + cpp_defines = {flag[2:].split("=")[0].strip(): flag[2:].split("=")[1].strip() if "=" in flag else None for flag in + build_flags if flag.startswith("-D")} # Extract macros from the config header file header_macros = extract_macros_with_values(config_header_path) # Combine all macros all_macros = {**header_macros, **cpp_defines} + # Uncomment for debugging # print("Extracted macros with values:", all_macros) @@ -166,8 +173,12 @@ def evaluate_condition(condition, macros): # Skip some levels if necessary parts = directory.split("/") final_directory = "/".join(parts[skip_level:]) + matched_files = [] + for file in files: + matched_files.extend(glob.glob(os.path.join(directory, file))) # Add [src, dst] to out list - to_copy.extend([[directory + file, final_directory + file] for file in files]) + to_copy.extend( + [[mfile, os.path.join(final_directory, os.path.basename(mfile))] for mfile in matched_files]) # Update the build environment with embedded files if len(to_copy) > 0: @@ -176,7 +187,8 @@ def evaluate_condition(condition, macros): else: print("No files selected for embedding.") + # I would prefer to use # env.AddPreAction("buildprog", action) # but that does not work :-( -action() \ No newline at end of file +action() diff --git a/scripts/embed_files.py b/scripts/embed_files.py index 378f891a..feddfb3b 100644 --- a/scripts/embed_files.py +++ b/scripts/embed_files.py @@ -6,7 +6,7 @@ # You do NOT need to run it manually ########################################################################################################### -import re +import re, os.path, glob from os.path import basename, join from SCons.Script import Import @@ -16,13 +16,15 @@ # Map macros to directories and their respective files # The final filename is constructed as "dir + file" so the dir should end with a "/" unless you know what you are doing embed_map = { - "USE_CLOCK_NIXIE == NIXIE_NOHOLES": [ # Condition - ["images/clock/nixie.no-holes/", # Source directory - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"]], # Files + "USE_CLOCK_NIXIE == NIXIE_NOHOLES": [ # Condition + ["images/clock/nixie.no-holes/", # Source directory + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", + "11.jpg"]], # Files ], "USE_CLOCK_NIXIE == NIXIE_HOLES": [ - ["images/clock/nixie.holes/", - ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"]], + ["images/clock/nixie.holes/", + ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", + "11.jpg"]], ], } @@ -30,6 +32,7 @@ board = env.BoardConfig() + #################################################### begin copy #################################################### # Copied from ~/.platformio/platforms/espressif32/builder/frameworks/_embed_files.py @@ -43,6 +46,7 @@ def embed_files(files, files_type): env.AddPostAction(file_target, revert_original_file) env.AppendUnique(PIOBUILDFILES=[env.File(join("$BUILD_DIR", filename))]) + mcu = board.get("build.mcu", "esp32") env.Append( BUILDERS=dict( @@ -56,9 +60,9 @@ def embed_files(files, files_type): "--input-target", "binary", "--output-target", - "elf32-littleriscv" if mcu in ("esp32c3","esp32c6") else "elf32-xtensa-le", + "elf32-littleriscv" if mcu in ("esp32c3", "esp32c6") else "elf32-xtensa-le", "--binary-architecture", - "riscv" if mcu in ("esp32c3","esp32c6") else "xtensa", + "riscv" if mcu in ("esp32c3", "esp32c6") else "xtensa", "--rename-section", ".data=.rodata.embedded", "$SOURCE", @@ -71,6 +75,8 @@ def embed_files(files, files_type): ) ) ) + + ##################################################### end copy ##################################################### # Function to extract macros and their values from a header file @@ -103,16 +109,19 @@ def extract_macros_with_values(file_path): print(f"Warning: Header file '{file_path}' not found.") return macros + def action(): # Extract -D flags from BUILD_FLAGS build_flags = env.get("BUILD_FLAGS", []) - cpp_defines = {flag[2:].split("=")[0].strip(): flag[2:].split("=")[1].strip() if "=" in flag else None for flag in build_flags if flag.startswith("-D")} + cpp_defines = {flag[2:].split("=")[0].strip(): flag[2:].split("=")[1].strip() if "=" in flag else None for flag in + build_flags if flag.startswith("-D")} # Extract macros from the config header file header_macros = extract_macros_with_values(config_header_path) # Combine all macros all_macros = {**header_macros, **cpp_defines} + # Uncomment for debugging # print("Extracted macros with values:", all_macros) @@ -135,7 +144,10 @@ def evaluate_condition(condition, macros): for condition, directories in embed_map.items(): if evaluate_condition(condition, all_macros): for directory, files in directories: - to_embed.extend(["../" + directory + file for file in files]) + matched_files = [] + for file in files: + matched_files.extend(glob.glob(os.path.join(directory, file))) + to_embed.extend(["../" + mfile for mfile in matched_files]) # Update the build environment with embedded files if len(to_embed) > 0: @@ -144,7 +156,8 @@ def evaluate_condition(condition, macros): else: print("No files selected for embedding.") + # I would prefer to use # env.AddPreAction("buildprog", action) # but that does not work :-( -action() \ No newline at end of file +action() From 081af7951c8ba95fa839817beb9e1b76710848f1 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 04:21:09 +0100 Subject: [PATCH 03/18] Refactor I18n handling and enhance WidgetSet functionality Introduced dynamic widget-specific translations and centralized language management in I18n. Added helper methods in WidgetSet for widget retrieval and improved iteration logic. This lays groundwork for better localization support and improves code consistency. --- firmware/src/core/utils/I18n.cpp | 40 ++++++++++++++++++++------ firmware/src/core/utils/I18n.h | 9 ++++-- firmware/src/core/utils/MainHelper.cpp | 5 +--- firmware/src/core/utils/MainHelper.h | 4 +++ firmware/src/core/widget/Widget.h | 1 + firmware/src/core/widget/WidgetSet.cpp | 13 ++++++++- firmware/src/core/widget/WidgetSet.h | 6 ++-- 7 files changed, 60 insertions(+), 18 deletions(-) diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index 370cb2fa..802e060f 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -1,16 +1,37 @@ #include "I18n.h" + +#include #include +#include + +std::map I18n::s_translations; +String I18n::s_language = "en"; -std::map I18n::translations; +void I18n::setLanguage(const String &lang) { + s_language = lang; + loadFile(lang, false); +} + +String I18n::getLanguage() { + return s_language; +} -bool I18n::load(const char *filename) { - File file = LittleFS.open(filename, "r"); +void I18n::loadExtraTranslations(const String &suffix) { + auto lower = suffix; + lower.toLowerCase(); + loadFile(s_language + "." + lower, true); +} + +bool I18n::loadFile(const String &filename, const bool append) { + const String fullName = I18N_DIR + filename + ".txt"; + File file = LittleFS.open(fullName, "r"); if (!file) { - Serial.println("Failed to open language file"); + Log.warningln("Failed to open language file %s", fullName.c_str()); return false; } - - translations.clear(); + if (!append) { + s_translations.clear(); + } while (file.available()) { String line = file.readStringUntil('\n'); line.trim(); @@ -24,16 +45,17 @@ bool I18n::load(const char *filename) { String value = line.substring(sepIndex + 1); key.trim(); value.trim(); - translations[key] = value; + s_translations[key] = value; } } file.close(); + Log.noticeln("Loaded language file %s", fullName.c_str()); return true; } String I18n::get(const String &key) { - if (translations.find(key) != translations.end()) { - return translations[key]; + if (s_translations.find(key) != s_translations.end()) { + return s_translations[key]; } return key; } diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 09f47541..6d2d35d0 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -6,8 +6,10 @@ class I18n { public: - static bool load(const char *filename); + static void setLanguage(const String &language); + static String getLanguage(); + static void loadExtraTranslations(const String &suffix); static String get(const String &key); template @@ -18,7 +20,10 @@ class I18n { } private: - static std::map translations; + static String s_language; + static std::map s_translations; + + static bool loadFile(const String &filename, bool append); static void replacePlaceholder(String &str, int index, const String &value); diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index a6c77ce1..1a71f76d 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -506,8 +506,5 @@ void MainHelper::watchdogReset() { } void MainHelper::setupI18n() { - String langFile = "i18n/en.txt"; // TODO add to GUI - if (I18n::load(langFile.c_str())) { - Log.noticeln("Loaded language file: %s", langFile.c_str()); - } + I18n::setLanguage("en"); // TODO add to GUI } diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index 1e60c81e..90b8d9b8 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -47,6 +47,10 @@ #define WDT_TIMEOUT 60 // Timeout in seconds #endif +#ifndef I18N_DIR + #define I18N_DIR "/i18n/" +#endif + class MainHelper { public: static void init(WiFiManager *wm, ConfigManager *cm, ScreenManager *sm, WidgetSet *ws); diff --git a/firmware/src/core/widget/Widget.h b/firmware/src/core/widget/Widget.h index 5f32bfb4..024eb9b7 100644 --- a/firmware/src/core/widget/Widget.h +++ b/firmware/src/core/widget/Widget.h @@ -3,6 +3,7 @@ #include "Button.h" #include "ConfigManager.h" +#include "I18n.h" #include "ScreenManager.h" #include "config_helper.h" diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index c9bb0b93..ece7a2c4 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -11,6 +11,17 @@ void WidgetSet::add(Widget *widget) { m_widgets[m_widgetCount] = widget; m_widgets[m_widgetCount]->setup(); m_widgetCount++; + if (!widget->getName().isEmpty()) { + I18n::loadExtraTranslations(widget->getName()); + } +} + +Widget **WidgetSet::getAllWidgets() { + return m_widgets; +} + +uint8_t WidgetSet::getWidgetCount() const { + return m_widgetCount; } void WidgetSet::drawCurrent(bool force) { @@ -86,7 +97,7 @@ void WidgetSet::showLoading() { } void WidgetSet::updateAll() { - for (int8_t i; i < m_widgetCount; i++) { + for (uint8_t i = 0; i < m_widgetCount; i++) { if (m_widgets[i]->isEnabled()) { Serial.printf("updating widget %s\n", m_widgets[i]->getName().c_str()); showCenteredLine(4, m_widgets[i]->getName()); diff --git a/firmware/src/core/widget/WidgetSet.h b/firmware/src/core/widget/WidgetSet.h index 13581f7f..616d80e5 100644 --- a/firmware/src/core/widget/WidgetSet.h +++ b/firmware/src/core/widget/WidgetSet.h @@ -11,6 +11,8 @@ class WidgetSet { public: WidgetSet(ScreenManager *sm); void add(Widget *widget); + Widget **getAllWidgets(); + uint8_t getWidgetCount() const; void drawCurrent(bool force = false); void updateCurrent(); Widget *getCurrent(); @@ -28,8 +30,8 @@ class WidgetSet { ScreenManager *m_screenManager; bool m_clearScreensOnDrawCurrent = true; Widget *m_widgets[MAX_WIDGETS]; - int8_t m_widgetCount = 0; - int8_t m_currentWidget = 0; + uint8_t m_widgetCount = 0; + uint8_t m_currentWidget = 0; bool m_initialized = false; From df0c972c42279d5cf5b0741dae11c8af56a804dd Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 04:25:48 +0100 Subject: [PATCH 04/18] Refactor I18N_DIR definition placement. Move the I18N_DIR macro from MainHelper.h to I18n.h for better encapsulation and logical grouping of localization-related definitions. Also, remove an unnecessary include of MainHelper.h in I18n.cpp. --- firmware/src/core/utils/I18n.cpp | 1 - firmware/src/core/utils/I18n.h | 4 ++++ firmware/src/core/utils/MainHelper.h | 4 ---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index 802e060f..a3190212 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -2,7 +2,6 @@ #include #include -#include std::map I18n::s_translations; String I18n::s_language = "en"; diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 6d2d35d0..03b36d43 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -4,6 +4,10 @@ #include #include +#ifndef I18N_DIR + #define I18N_DIR "/i18n/" +#endif + class I18n { public: static void setLanguage(const String &language); diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index 90b8d9b8..1e60c81e 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -47,10 +47,6 @@ #define WDT_TIMEOUT 60 // Timeout in seconds #endif -#ifndef I18N_DIR - #define I18N_DIR "/i18n/" -#endif - class MainHelper { public: static void init(WiFiManager *wm, ConfigManager *cm, ScreenManager *sm, WidgetSet *ws); From 1da802a5bbf7784b58f6ba9a5424bec9556222b2 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 10:50:42 +0100 Subject: [PATCH 05/18] Simplify widget translation loading logic. Removed redundant empty name check and clarified translation loading process in `WidgetSet`. Added localization entry for "Parqet" and improved robustness of `loadExtraTranslations` to handle empty suffixes. --- firmware/src/core/utils/I18n.cpp | 3 +++ firmware/src/core/widget/WidgetSet.cpp | 5 ++--- i18n/en.parqet.txt | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 i18n/en.parqet.txt diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index a3190212..8322b7b4 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -16,6 +16,9 @@ String I18n::getLanguage() { } void I18n::loadExtraTranslations(const String &suffix) { + if (suffix.isEmpty() == 0) { + return; + } auto lower = suffix; lower.toLowerCase(); loadFile(s_language + "." + lower, true); diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index ece7a2c4..0fbc8cef 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -11,9 +11,8 @@ void WidgetSet::add(Widget *widget) { m_widgets[m_widgetCount] = widget; m_widgets[m_widgetCount]->setup(); m_widgetCount++; - if (!widget->getName().isEmpty()) { - I18n::loadExtraTranslations(widget->getName()); - } + // Load widget specific translations + I18n::loadExtraTranslations(widget->getName()); } Widget **WidgetSet::getAllWidgets() { diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt new file mode 100644 index 00000000..fdb10187 --- /dev/null +++ b/i18n/en.parqet.txt @@ -0,0 +1 @@ +parqet.name=Parqet \ No newline at end of file From ca82d19f31d1b0320a25951f38db43cd8afcebc9 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 12:12:57 +0100 Subject: [PATCH 06/18] Set default language fallback and refine translation loading. Updated I18n system to handle a default language fallback when loading translations, ensuring robust behavior when specific language files are unavailable. Renamed parameters and improved clarity in translation loading logic for better maintainability. --- firmware/src/core/utils/I18n.cpp | 31 ++++++++++++++++++++++--------- firmware/src/core/utils/I18n.h | 9 +++++++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index 8322b7b4..77808193 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -4,34 +4,47 @@ #include std::map I18n::s_translations; -String I18n::s_language = "en"; +String I18n::s_language = DEFAULT_LANGUAGE; void I18n::setLanguage(const String &lang) { s_language = lang; - loadFile(lang, false); + if (s_language != DEFAULT_LANGUAGE) { + // Load default translations (fallback) + loadFile(DEFAULT_LANGUAGE); + } + // Load actual language file + loadFile(lang); } String I18n::getLanguage() { return s_language; } -void I18n::loadExtraTranslations(const String &suffix) { - if (suffix.isEmpty() == 0) { +void I18n::loadExtraTranslations(const String &extraName) { + if (extraName.isEmpty() == 0) { return; } - auto lower = suffix; - lower.toLowerCase(); - loadFile(s_language + "." + lower, true); + // Convert Widget name to lowercase for file name + auto lowercaseName = extraName; + lowercaseName.toLowerCase(); + if (s_language != DEFAULT_LANGUAGE) { + // Load default translations (fallback) + loadFile(String(DEFAULT_LANGUAGE) + "." + lowercaseName); + } + loadFile(s_language + "." + lowercaseName); } -bool I18n::loadFile(const String &filename, const bool append) { +bool I18n::loadFile(const String &filename, const bool clear) { + if (filename.isEmpty()) { + return false; + } const String fullName = I18N_DIR + filename + ".txt"; File file = LittleFS.open(fullName, "r"); if (!file) { Log.warningln("Failed to open language file %s", fullName.c_str()); return false; } - if (!append) { + if (clear) { s_translations.clear(); } while (file.available()) { diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 03b36d43..63262d94 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -1,6 +1,7 @@ #ifndef I18N_H #define I18N_H +#include "config_helper.h" #include #include @@ -8,12 +9,16 @@ #define I18N_DIR "/i18n/" #endif +#ifndef DEFAULT_LANGUAGE + #define DEFAULT_LANGUAGE "en" +#endif + class I18n { public: static void setLanguage(const String &language); static String getLanguage(); - static void loadExtraTranslations(const String &suffix); + static void loadExtraTranslations(const String &extraName); static String get(const String &key); template @@ -27,7 +32,7 @@ class I18n { static String s_language; static std::map s_translations; - static bool loadFile(const String &filename, bool append); + static bool loadFile(const String &filename, bool clear = false); static void replacePlaceholder(String &str, int index, const String &value); From 2e9534b4843871cdf6a402181c84ffdb3c902771 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 12:58:21 +0100 Subject: [PATCH 07/18] Add multi-language support with language selection Introduce language selection using a dropdown in the configuration settings, replacing hardcoded default handling. Added support for managing language IDs, retrieving available languages, and loading translations dynamically based on the selected language. --- firmware/src/core/utils/I18n.cpp | 39 ++++++++++++++++---------- firmware/src/core/utils/I18n.h | 24 +++++++++++----- firmware/src/core/utils/MainHelper.cpp | 12 ++++---- firmware/src/main.cpp | 1 - i18n/de.txt | 5 ++++ 5 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 i18n/de.txt diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index 77808193..32bcd427 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -3,38 +3,50 @@ #include #include +String I18n::s_allLanguages[] = ALL_LANGUAGES; std::map I18n::s_translations; -String I18n::s_language = DEFAULT_LANGUAGE; +int I18n::s_languageId = DEFAULT_LANGUAGE; -void I18n::setLanguage(const String &lang) { - s_language = lang; - if (s_language != DEFAULT_LANGUAGE) { +void I18n::setLanguageId(const int langId) { + s_languageId = langId; + if (s_languageId != DEFAULT_LANGUAGE) { // Load default translations (fallback) loadFile(DEFAULT_LANGUAGE); } // Load actual language file - loadFile(lang); + loadFile(langId); } -String I18n::getLanguage() { - return s_language; +String I18n::getLanguageString(const int langId) { + if (langId >= 0 && langId < LANG_NUM) { + return s_allLanguages[langId]; + } + return "invalid"; +} + +String *I18n::getAllLanguages() { + return s_allLanguages; } void I18n::loadExtraTranslations(const String &extraName) { - if (extraName.isEmpty() == 0) { + if (extraName.isEmpty()) { return; } // Convert Widget name to lowercase for file name auto lowercaseName = extraName; lowercaseName.toLowerCase(); - if (s_language != DEFAULT_LANGUAGE) { + if (s_languageId != DEFAULT_LANGUAGE) { // Load default translations (fallback) - loadFile(String(DEFAULT_LANGUAGE) + "." + lowercaseName); + loadFile(getLanguageString(DEFAULT_LANGUAGE) + "." + lowercaseName); } - loadFile(s_language + "." + lowercaseName); + loadFile(getLanguageString(s_languageId) + "." + lowercaseName); } -bool I18n::loadFile(const String &filename, const bool clear) { +bool I18n::loadFile(const int langId) { + return loadFile(getLanguageString(langId)); +} + +bool I18n::loadFile(const String &filename) { if (filename.isEmpty()) { return false; } @@ -44,9 +56,6 @@ bool I18n::loadFile(const String &filename, const bool clear) { Log.warningln("Failed to open language file %s", fullName.c_str()); return false; } - if (clear) { - s_translations.clear(); - } while (file.available()) { String line = file.readStringUntil('\n'); line.trim(); diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 63262d94..731802fd 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -9,14 +9,22 @@ #define I18N_DIR "/i18n/" #endif -#ifndef DEFAULT_LANGUAGE - #define DEFAULT_LANGUAGE "en" -#endif +#define ALL_LANGUAGES {"en", "de", "fr"} + +enum Language { + LANG_EN = 0, + LANG_DE, + LANG_FR, + LANG_NUM +}; + +#define DEFAULT_LANGUAGE LANG_EN class I18n { public: - static void setLanguage(const String &language); - static String getLanguage(); + static void setLanguageId(int langId); + static String getLanguageString(int langId); + static String *getAllLanguages(); static void loadExtraTranslations(const String &extraName); static String get(const String &key); @@ -29,10 +37,12 @@ class I18n { } private: - static String s_language; + static String s_allLanguages[]; + static int s_languageId; static std::map s_translations; - static bool loadFile(const String &filename, bool clear = false); + static bool loadFile(const String &filename); + static bool loadFile(int langId); static void replacePlaceholder(String &str, int index, const String &value); diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index 1a71f76d..def76711 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -23,6 +23,7 @@ static bool s_nightMode = DIM_ENABLED; static int s_dimStartHour = DIM_START_HOUR; static int s_dimEndHour = DIM_END_HOUR; static int s_dimBrightness = DIM_BRIGHTNESS; +static int s_languageId = LANG_EN; void MainHelper::init(WiFiManager *wm, ConfigManager *cm, ScreenManager *sm, WidgetSet *ws) { s_wifiManager = wm; @@ -61,9 +62,10 @@ void MainHelper::setupButtons() { void MainHelper::setupConfig() { s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, "Timezone Location, use one from this list"); + String *optLang = I18n::getAllLanguages(); + s_configManager->addConfigComboBox("General", "lang", &s_languageId, optLang, LANG_NUM, "Language"); s_configManager->addConfigInt("General", "widgetCycDelay", &s_widgetCycleDelay, "Automatically cycle widgets every X seconds, set to 0 to disable"); s_configManager->addConfigString("General", "ntpServer", &s_ntpServer, 30, "NTP server", true); - String optRotation[] = {"No rotation", "Rotate 90° clockwise", "Rotate 180°", "Rotate 270° clockwise"}; s_configManager->addConfigComboBox("TFT Settings", "orbRotation", &s_orbRotation, optRotation, 4, "Orb rotation"); s_configManager->addConfigBool("TFT Settings", "nightmode", &s_nightMode, "Enable Nighttime mode"); @@ -73,6 +75,8 @@ void MainHelper::setupConfig() { s_configManager->addConfigComboBox("TFT Settings", "dimStartHour", &s_dimStartHour, optHours, 24, "Nighttime Start [24h format]", true); s_configManager->addConfigComboBox("TFT Settings", "dimEndHour", &s_dimEndHour, optHours, 24, "Nighttime End [24h format]", true); s_configManager->addConfigInt("TFT Settings", "dimBrightness", &s_dimBrightness, "Nighttime Brightness [0-255]", true); + + I18n::setLanguageId(s_languageId); } void MainHelper::buttonPressed(uint8_t buttonId, ButtonState state) { @@ -503,8 +507,4 @@ void MainHelper::watchdogInit() { void MainHelper::watchdogReset() { esp_task_wdt_reset(); -} - -void MainHelper::setupI18n() { - I18n::setLanguage("en"); // TODO add to GUI -} +} \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 545ec640..38cfe5c6 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -60,7 +60,6 @@ void setup() { MainHelper::setupLittleFS(); MainHelper::setupConfig(); MainHelper::setupButtons(); - MainHelper::setupI18n(); MainHelper::showWelcome(); pinMode(BUSY_PIN, OUTPUT); diff --git a/i18n/de.txt b/i18n/de.txt new file mode 100644 index 00000000..3ba2982c --- /dev/null +++ b/i18n/de.txt @@ -0,0 +1,5 @@ +welcome=Willkommen +infoorbs=InfoOrbs +by=von +brett.tech=brett.tech +version=Version \1 \ No newline at end of file From 1526a82b837ff47aa27f50855162d965c580b335 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 13:04:32 +0100 Subject: [PATCH 08/18] Fix navigation logic in WidgetSet::prev() Adjusted the decrement logic in WidgetSet::prev() to prevent accessing invalid widget indices. Ensures proper wrapping behavior when navigating to the previous widget in the circular widget list. --- firmware/src/core/widget/WidgetSet.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index 0fbc8cef..2622d231 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -63,9 +63,10 @@ void WidgetSet::next() { } void WidgetSet::prev() { - m_currentWidget--; - if (m_currentWidget < 0) { + if (m_currentWidget == 0) { m_currentWidget = m_widgetCount - 1; + } else { + m_currentWidget--; } if (!getCurrent()->isEnabled()) { // Recursive call to next() From 0e94df21b4cf67aed27732dc9fcb90c916f128db Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 13:09:11 +0100 Subject: [PATCH 09/18] Refactor loading text to use i18n translations. Replaced hardcoded "Loading data:" text in WidgetSet with a translatable string using the i18n system. Added the "loading_data" key to English and German translation files to support localization. This improves maintainability and enables multi-language support. --- firmware/src/core/widget/WidgetSet.cpp | 2 +- i18n/de.txt | 3 ++- i18n/en.txt | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index 2622d231..a8db4083 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -93,7 +93,7 @@ void WidgetSet::showCenteredLine(int screen, const String &text) { } void WidgetSet::showLoading() { - showCenteredLine(3, "Loading data:"); + showCenteredLine(3, I18n::get("loading_data")); } void WidgetSet::updateAll() { diff --git a/i18n/de.txt b/i18n/de.txt index 3ba2982c..ca570c7b 100644 --- a/i18n/de.txt +++ b/i18n/de.txt @@ -2,4 +2,5 @@ welcome=Willkommen infoorbs=InfoOrbs by=von brett.tech=brett.tech -version=Version \1 \ No newline at end of file +version=Version \1 +loading_data=Lade Daten: \ No newline at end of file diff --git a/i18n/en.txt b/i18n/en.txt index f506ab2f..fccc8492 100644 --- a/i18n/en.txt +++ b/i18n/en.txt @@ -2,4 +2,5 @@ welcome=Welcome infoorbs=InfoOrbs by=by brett.tech=brett.tech -version=Version \1 \ No newline at end of file +version=Version \1 +loading_data=Loading data: \ No newline at end of file From b39efac9caed24dcb6dbc53586fc901492348e5f Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 21:07:15 +0100 Subject: [PATCH 10/18] Refactor I18n to optimize memory handling and translation loading. Updated I18n to use `const char*` for translations to reduce memory usage and added proper cleanup via `clearTranslations()`. Implemented dynamic loading of widget-specific translations and updated ParqetWidget to leverage these changes. Adjusted related files and language resources accordingly. --- firmware/src/core/utils/I18n.cpp | 31 +++++++++++++------ firmware/src/core/utils/I18n.h | 5 +-- firmware/src/core/widget/WidgetSet.cpp | 2 -- .../src/widgets/parqetwidget/ParqetWidget.cpp | 6 ++-- i18n/de.parqet.txt | 2 ++ i18n/de.txt | 3 +- i18n/en.parqet.txt | 3 +- i18n/en.txt | 3 +- 8 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 i18n/de.parqet.txt diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index 32bcd427..ae250443 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -2,19 +2,26 @@ #include #include +#include // For strdup and free String I18n::s_allLanguages[] = ALL_LANGUAGES; -std::map I18n::s_translations; +std::map I18n::s_translations; int I18n::s_languageId = DEFAULT_LANGUAGE; void I18n::setLanguageId(const int langId) { + clearTranslations(); // Clear previous translations s_languageId = langId; if (s_languageId != DEFAULT_LANGUAGE) { - // Load default translations (fallback) - loadFile(DEFAULT_LANGUAGE); + loadFile(DEFAULT_LANGUAGE); // Fallback translations } - // Load actual language file - loadFile(langId); + loadFile(langId); // Load selected language +} + +void I18n::clearTranslations() { + for (auto &entry : s_translations) { + free((void *) entry.second); // Free allocated memory + } + s_translations.clear(); } String I18n::getLanguageString(const int langId) { @@ -69,7 +76,13 @@ bool I18n::loadFile(const String &filename) { String value = line.substring(sepIndex + 1); key.trim(); value.trim(); - s_translations[key] = value; + // Free existing value if key already exists + if (s_translations.count(key)) { + free((void *) s_translations[key]); + } + + // Store as const char* + s_translations[key] = strdup(value.c_str()); } } file.close(); @@ -77,11 +90,11 @@ bool I18n::loadFile(const String &filename) { return true; } -String I18n::get(const String &key) { - if (s_translations.find(key) != s_translations.end()) { +const char *I18n::get(const String &key) { + if (s_translations.count(key)) { return s_translations[key]; } - return key; + return key.c_str(); } void I18n::replacePlaceholder(String &str, int index, const String &value) { diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 731802fd..1ebbe553 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -27,7 +27,7 @@ class I18n { static String *getAllLanguages(); static void loadExtraTranslations(const String &extraName); - static String get(const String &key); + static const char *get(const String &key); template static String get(const String &key, Args... args) { @@ -39,8 +39,9 @@ class I18n { private: static String s_allLanguages[]; static int s_languageId; - static std::map s_translations; + static std::map s_translations; + static void clearTranslations(); static bool loadFile(const String &filename); static bool loadFile(int langId); diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index a8db4083..a039381f 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -11,8 +11,6 @@ void WidgetSet::add(Widget *widget) { m_widgets[m_widgetCount] = widget; m_widgets[m_widgetCount]->setup(); m_widgetCount++; - // Load widget specific translations - I18n::loadExtraTranslations(widget->getName()); } Widget **WidgetSet::getAllWidgets() { diff --git a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp index 8d12d5f2..1dc2be05 100644 --- a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp +++ b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp @@ -9,8 +9,10 @@ ParqetWidget::ParqetWidget(ScreenManager &manager, ConfigManager &config) : Widget(manager, config) { Serial.printf("Constructing ParqetWidget, portfolioId=%s\n", m_portfolioId.c_str()); - m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, "Enable Widget"); - m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, "Portfolio ID (must be set to public!)"); + // Load widget specific translations + I18n::loadExtraTranslations(ParqetWidget::getName()); + m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, I18n::get("enable_widget")); + m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, I18n::get("parqet.enter_portfolio_id")); m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, "Default timeframe (you can change timeframes by medium pressing the middle button)", true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, "Performance measure", true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, "Chart measure", true); diff --git a/i18n/de.parqet.txt b/i18n/de.parqet.txt new file mode 100644 index 00000000..27c106cd --- /dev/null +++ b/i18n/de.parqet.txt @@ -0,0 +1,2 @@ +parqet.name=Parqet +parqet.enter_portfolio_id=Portfolio ID (muss auf öffentlich stehen!) \ No newline at end of file diff --git a/i18n/de.txt b/i18n/de.txt index ca570c7b..7813df60 100644 --- a/i18n/de.txt +++ b/i18n/de.txt @@ -3,4 +3,5 @@ infoorbs=InfoOrbs by=von brett.tech=brett.tech version=Version \1 -loading_data=Lade Daten: \ No newline at end of file +loading_data=Lade Daten: +enable_widget=Widget aktivieren \ No newline at end of file diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt index fdb10187..2b7fe5f1 100644 --- a/i18n/en.parqet.txt +++ b/i18n/en.parqet.txt @@ -1 +1,2 @@ -parqet.name=Parqet \ No newline at end of file +parqet.name=Parqet +parqet.enter_portfolio_id=Portfolio ID (must be set to public!) \ No newline at end of file diff --git a/i18n/en.txt b/i18n/en.txt index fccc8492..0bc975c5 100644 --- a/i18n/en.txt +++ b/i18n/en.txt @@ -3,4 +3,5 @@ infoorbs=InfoOrbs by=by brett.tech=brett.tech version=Version \1 -loading_data=Loading data: \ No newline at end of file +loading_data=Loading data: +enable_widget=Enable Widget \ No newline at end of file From 419bd5937c6fc757e1b13a45187c227e00c9cc33 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 21:14:21 +0100 Subject: [PATCH 11/18] Added comments clarifying return type decisions for better memory management and usage constraints. --- firmware/src/core/utils/I18n.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h index 1ebbe553..013a9c2a 100644 --- a/firmware/src/core/utils/I18n.h +++ b/firmware/src/core/utils/I18n.h @@ -27,8 +27,16 @@ class I18n { static String *getAllLanguages(); static void loadExtraTranslations(const String &extraName); + + // Returning const char* here allows us to use it in ConfigManager.addConfig*() + // because it will never go out of scope (as long as the translation is not removed) static const char *get(const String &key); + // Returning String here instead of const char* as it ensures the memory + // is managed properly and avoids the risk of accessing invalid memory + // from dynamic translations. However, this approach does not allow us to + // use it in ConfigManager.addConfig*(), because the String and its + // c_str might go out of scope template static String get(const String &key, Args... args) { String result = get(key); From 5b66d9940c31086604ed64497b9078c55e13d319 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 22:24:31 +0100 Subject: [PATCH 12/18] Refactor i18n usage in ParqetWidget for improved localization Replaced hardcoded strings with the newly implemented `i18n` function, centralizing translation handling and improving maintainability. Updated translation files (en/de) with additional keys for widget configuration options and labels. This ensures consistency and simplifies future localization efforts. --- firmware/src/core/widget/Widget.cpp | 6 +++++- firmware/src/core/widget/Widget.h | 1 + .../src/widgets/parqetwidget/ParqetWidget.cpp | 16 ++++++++-------- i18n/de.parqet.txt | 10 ++++++++-- i18n/en.parqet.txt | 10 ++++++++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/firmware/src/core/widget/Widget.cpp b/firmware/src/core/widget/Widget.cpp index 401539ce..c7ba2ef1 100644 --- a/firmware/src/core/widget/Widget.cpp +++ b/firmware/src/core/widget/Widget.cpp @@ -12,4 +12,8 @@ void Widget::setBusy(bool busy) { bool Widget::isEnabled() { return m_enabled; -} \ No newline at end of file +} + +const char *Widget::i18n(const String &key) { + return I18n::get(key); +} diff --git a/firmware/src/core/widget/Widget.h b/firmware/src/core/widget/Widget.h index 024eb9b7..553c3c60 100644 --- a/firmware/src/core/widget/Widget.h +++ b/firmware/src/core/widget/Widget.h @@ -18,6 +18,7 @@ class Widget { virtual String getName() = 0; void setBusy(bool busy); bool isEnabled(); + static const char *i18n(const String &key); protected: ScreenManager &m_manager; diff --git a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp index 1dc2be05..f7d3213d 100644 --- a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp +++ b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp @@ -11,16 +11,16 @@ ParqetWidget::ParqetWidget(ScreenManager &manager, ConfigManager &config) : Widg Serial.printf("Constructing ParqetWidget, portfolioId=%s\n", m_portfolioId.c_str()); // Load widget specific translations I18n::loadExtraTranslations(ParqetWidget::getName()); - m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, I18n::get("enable_widget")); - m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, I18n::get("parqet.enter_portfolio_id")); - m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, "Default timeframe (you can change timeframes by medium pressing the middle button)", true); + m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n("enable_widget")); + m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n("pq.cnf.portfolio_id")); + m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, i18n("pq.cnf.mode"), true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, "Performance measure", true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, "Chart measure", true); - m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, "Show clock on first screen", true); - m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, "Show totals screen", true); - m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, "Show total portfolio value", true); + m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, i18n("pq.cnf.clock"), true); + m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, i18n("pq.cnf.totals"), true); + m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n("pq.cnf.total_val"), true); String optPriceVal[] = {"Show current price", "Show current value"}; - m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, "Show price or value for stocks", true); + m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, i18n("pq.cnf.values"), true); m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, "ParqetProxy URL", true); m_curMode = m_defaultMode; m_curPerfMeasure = m_defaultPerfMeasure; @@ -292,7 +292,7 @@ void ParqetWidget::displayStock(int8_t displayIndex, ParqetHoldingDataModel &sto m_manager.drawString(stock.getCurrentPrice(2), ScreenCenterX, 58, 26, Align::MiddleCenter); } } else { - m_manager.drawString("Portfolio", ScreenCenterX, 58, 26, Align::MiddleCenter); + m_manager.drawString(i18n("pq.portfolio"), ScreenCenterX, 58, 26, Align::MiddleCenter); } if (m_showTotalChart && stock.getId() == "total" && m_portfolio.getChartDataCount() >= 7) { diff --git a/i18n/de.parqet.txt b/i18n/de.parqet.txt index 27c106cd..2cc3edcc 100644 --- a/i18n/de.parqet.txt +++ b/i18n/de.parqet.txt @@ -1,2 +1,8 @@ -parqet.name=Parqet -parqet.enter_portfolio_id=Portfolio ID (muss auf öffentlich stehen!) \ No newline at end of file +pq.portfolio=Portfolio +pq.total=G E S A M T +pq.cnf.portfolio_id=Portfolio-ID (muss auf öffentlich stehen!) +pq.cnf.mode=Standardzeitraum (Zeitraum kann durch mittellanges Drücken des mittleren Buttons geändert werden) +pq.cnf.clock=Uhr auf dem ersten Bildschirm anzeigen +pq.cnf.totals=Gesamtübersicht anzeigen +pq.cnf.total_val=Gesamtwert des Portfolios anzeigen +pq.cnf.values=Preis oder Wert der Aktien anzeigen \ No newline at end of file diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt index 2b7fe5f1..f89ebf38 100644 --- a/i18n/en.parqet.txt +++ b/i18n/en.parqet.txt @@ -1,2 +1,8 @@ -parqet.name=Parqet -parqet.enter_portfolio_id=Portfolio ID (must be set to public!) \ No newline at end of file +pq.portfolio=Portfolio +pq.total=T O T A L +pq.cnf.portfolio_id=Portfolio ID (must be set to public!) +pq.cnf.mode=Default timeframe (you can change timeframes by medium pressing the middle button) +pq.cnf.clock=Show clock on first screen +pq.cnf.totals=Show totals screen +pq.cnf.total_val=Show total portfolio value +pq.cnf.values=Show price or value for stocks \ No newline at end of file From a460e457ccbe23988015f0aa1d8fac615f9a148f Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Mon, 24 Feb 2025 23:03:34 +0100 Subject: [PATCH 13/18] Refactor i18n strings and enhance logging for translations. Updated i18n keys for consistency and improved translation handling. Added logging for missing and found translations in `I18n::get`. Simplified configuration translations using the `i18n` helper method for clarity and maintainability. --- firmware/src/core/utils/I18n.cpp | 7 ++-- firmware/src/core/utils/MainHelper.cpp | 34 +++++++++++-------- firmware/src/core/utils/MainHelper.h | 3 +- firmware/src/core/widget/WidgetSet.cpp | 2 +- .../src/widgets/parqetwidget/ParqetWidget.cpp | 6 ++-- i18n/de.parqet.txt | 4 +-- i18n/de.txt | 20 +++++++++-- i18n/en.parqet.txt | 4 +-- i18n/en.txt | 20 +++++++++-- 9 files changed, 68 insertions(+), 32 deletions(-) diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp index ae250443..3da7471f 100644 --- a/firmware/src/core/utils/I18n.cpp +++ b/firmware/src/core/utils/I18n.cpp @@ -92,9 +92,12 @@ bool I18n::loadFile(const String &filename) { const char *I18n::get(const String &key) { if (s_translations.count(key)) { - return s_translations[key]; + const auto val = s_translations[key]; + Log.traceln("Translation found: %s -> %s", key.c_str(), val); + return val; } - return key.c_str(); + Log.warningln("Translation not found: %s", key.c_str()); + return "@missingTranslation@"; } void I18n::replacePlaceholder(String &str, int index, const String &value) { diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index def76711..021f16fd 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -61,22 +61,22 @@ void MainHelper::setupButtons() { } void MainHelper::setupConfig() { - s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, "Timezone Location, use one from this list"); + // Set language here to get i18n strings for the configuration + I18n::setLanguageId(s_configManager->getConfigInt("lang", DEFAULT_LANGUAGE)); + s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, i18n("timezoneLoc")); String *optLang = I18n::getAllLanguages(); - s_configManager->addConfigComboBox("General", "lang", &s_languageId, optLang, LANG_NUM, "Language"); - s_configManager->addConfigInt("General", "widgetCycDelay", &s_widgetCycleDelay, "Automatically cycle widgets every X seconds, set to 0 to disable"); - s_configManager->addConfigString("General", "ntpServer", &s_ntpServer, 30, "NTP server", true); - String optRotation[] = {"No rotation", "Rotate 90° clockwise", "Rotate 180°", "Rotate 270° clockwise"}; - s_configManager->addConfigComboBox("TFT Settings", "orbRotation", &s_orbRotation, optRotation, 4, "Orb rotation"); - s_configManager->addConfigBool("TFT Settings", "nightmode", &s_nightMode, "Enable Nighttime mode"); - s_configManager->addConfigInt("TFT Settings", "tftBrightness", &s_tftBrightness, "TFT Brightness [0-255]", true); + s_configManager->addConfigComboBox("General", "lang", &s_languageId, optLang, LANG_NUM, i18n("language")); + s_configManager->addConfigInt("General", "widgetCycDelay", &s_widgetCycleDelay, i18n("widgetCycleDelay")); + s_configManager->addConfigString("General", "ntpServer", &s_ntpServer, 30, i18n("ntpServer"), true); + String optRotation[] = {i18n("orbRotation.0"), i18n("orbRotation.1"), i18n("orbRotation.2"), i18n("orbRotation.3")}; + s_configManager->addConfigComboBox("TFT Settings", "orbRotation", &s_orbRotation, optRotation, 4, i18n("orbRotation")); + s_configManager->addConfigBool("TFT Settings", "nightmode", &s_nightMode, i18n("nightmode")); + s_configManager->addConfigInt("TFT Settings", "tftBrightness", &s_tftBrightness, i18n("tftBrightness"), true); String optHours[] = {"0:00", "1:00", "2:00", "3:00", "4:00", "5:00", "6:00", "7:00", "8:00", "9:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00"}; - s_configManager->addConfigComboBox("TFT Settings", "dimStartHour", &s_dimStartHour, optHours, 24, "Nighttime Start [24h format]", true); - s_configManager->addConfigComboBox("TFT Settings", "dimEndHour", &s_dimEndHour, optHours, 24, "Nighttime End [24h format]", true); - s_configManager->addConfigInt("TFT Settings", "dimBrightness", &s_dimBrightness, "Nighttime Brightness [0-255]", true); - - I18n::setLanguageId(s_languageId); + s_configManager->addConfigComboBox("TFT Settings", "dimStartHour", &s_dimStartHour, optHours, 24, i18n("dimStartHour"), true); + s_configManager->addConfigComboBox("TFT Settings", "dimEndHour", &s_dimEndHour, optHours, 24, i18n("dimEndHour"), true); + s_configManager->addConfigInt("TFT Settings", "dimBrightness", &s_dimBrightness, i18n("dimBrightness"), true); } void MainHelper::buttonPressed(uint8_t buttonId, ButtonState state) { @@ -432,7 +432,7 @@ void MainHelper::showWelcome() { } s_screenManager->selectScreen(1); - s_screenManager->drawCentreString(I18n::get("infoorbs"), ScreenCenterX, ScreenCenterY - 50, 22); + s_screenManager->drawCentreString(I18n::get("infoOrbs"), ScreenCenterX, ScreenCenterY - 50, 22); s_screenManager->drawCentreString(I18n::get("by"), ScreenCenterX, ScreenCenterY - 5, 22); s_screenManager->drawCentreString(I18n::get("brett.tech"), ScreenCenterX, ScreenCenterY + 30, 22); s_screenManager->setFontColor(TFT_RED); @@ -507,4 +507,8 @@ void MainHelper::watchdogInit() { void MainHelper::watchdogReset() { esp_task_wdt_reset(); -} \ No newline at end of file +} + +const char *MainHelper::i18n(const String &key) { + return I18n::get(key); +} diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index 1e60c81e..b3f39b97 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -80,7 +80,8 @@ class MainHelper { static void watchdogReset(); static void updateBrightnessByTime(uint8_t hour24); - static void setupI18n(); + + static const char *i18n(const String &key); }; #endif \ No newline at end of file diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index a039381f..e44a0703 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -91,7 +91,7 @@ void WidgetSet::showCenteredLine(int screen, const String &text) { } void WidgetSet::showLoading() { - showCenteredLine(3, I18n::get("loading_data")); + showCenteredLine(3, I18n::get("loadingData")); } void WidgetSet::updateAll() { diff --git a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp index f7d3213d..092cc82e 100644 --- a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp +++ b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp @@ -11,14 +11,14 @@ ParqetWidget::ParqetWidget(ScreenManager &manager, ConfigManager &config) : Widg Serial.printf("Constructing ParqetWidget, portfolioId=%s\n", m_portfolioId.c_str()); // Load widget specific translations I18n::loadExtraTranslations(ParqetWidget::getName()); - m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n("enable_widget")); - m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n("pq.cnf.portfolio_id")); + m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n("enableWidget")); + m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n("pq.cnf.portfolioId")); m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, i18n("pq.cnf.mode"), true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, "Performance measure", true); m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, "Chart measure", true); m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, i18n("pq.cnf.clock"), true); m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, i18n("pq.cnf.totals"), true); - m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n("pq.cnf.total_val"), true); + m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n("pq.cnf.totalVal"), true); String optPriceVal[] = {"Show current price", "Show current value"}; m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, i18n("pq.cnf.values"), true); m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, "ParqetProxy URL", true); diff --git a/i18n/de.parqet.txt b/i18n/de.parqet.txt index 2cc3edcc..7d4414f1 100644 --- a/i18n/de.parqet.txt +++ b/i18n/de.parqet.txt @@ -1,8 +1,8 @@ pq.portfolio=Portfolio pq.total=G E S A M T -pq.cnf.portfolio_id=Portfolio-ID (muss auf öffentlich stehen!) +pq.cnf.portfolioId=Portfolio-ID (muss auf öffentlich stehen!) pq.cnf.mode=Standardzeitraum (Zeitraum kann durch mittellanges Drücken des mittleren Buttons geändert werden) pq.cnf.clock=Uhr auf dem ersten Bildschirm anzeigen pq.cnf.totals=Gesamtübersicht anzeigen -pq.cnf.total_val=Gesamtwert des Portfolios anzeigen +pq.cnf.totalVal=Gesamtwert des Portfolios anzeigen pq.cnf.values=Preis oder Wert der Aktien anzeigen \ No newline at end of file diff --git a/i18n/de.txt b/i18n/de.txt index 7813df60..4176079e 100644 --- a/i18n/de.txt +++ b/i18n/de.txt @@ -1,7 +1,21 @@ welcome=Willkommen -infoorbs=InfoOrbs +infoOrbs=InfoOrbs by=von brett.tech=brett.tech version=Version \1 -loading_data=Lade Daten: -enable_widget=Widget aktivieren \ No newline at end of file +loadingData=Lade Daten: +enableWidget=Widget aktivieren +timezoneLoc=Zeitzone, verwende eine aus dieser Liste +language=Sprache +widgetCycleDelay=Widgets automatisch alle X Sekunden wechseln, 0 zum Deaktivieren +ntpServer=NTP-Server +orbRotation=Orb-Drehung +orbRotation.0=Keine Drehung +orbRotation.1=90° im Uhrzeigersinn +orbRotation.2=180° drehen +orbRotation.3=270° im Uhrzeigersinn +nightmode=Nachtmodus aktivieren +tftBrightness=TFT-Helligkeit [0-255] +dimStartHour=Startzeit Nachtmodus [24h-Format] +dimEndHour=Endzeit Nachtmodus [24h-Format] +dimBrightness=Helligkeit im Nachtmodus [0-255] \ No newline at end of file diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt index f89ebf38..910aa2ff 100644 --- a/i18n/en.parqet.txt +++ b/i18n/en.parqet.txt @@ -1,8 +1,8 @@ pq.portfolio=Portfolio pq.total=T O T A L -pq.cnf.portfolio_id=Portfolio ID (must be set to public!) +pq.cnf.portfolioId=Portfolio ID (must be set to public!) pq.cnf.mode=Default timeframe (you can change timeframes by medium pressing the middle button) pq.cnf.clock=Show clock on first screen pq.cnf.totals=Show totals screen -pq.cnf.total_val=Show total portfolio value +pq.cnf.totalVal=Show total portfolio value pq.cnf.values=Show price or value for stocks \ No newline at end of file diff --git a/i18n/en.txt b/i18n/en.txt index 0bc975c5..b5e2d68f 100644 --- a/i18n/en.txt +++ b/i18n/en.txt @@ -1,7 +1,21 @@ welcome=Welcome -infoorbs=InfoOrbs +infoOrbs=InfoOrbs by=by brett.tech=brett.tech version=Version \1 -loading_data=Loading data: -enable_widget=Enable Widget \ No newline at end of file +loadingData=Loading data: +enableWidget=Enable Widget +timezoneLoc=Timezone Location, use one from this list +language=Language +widgetCycleDelay=Automatically cycle widgets every X seconds, set to 0 to disable +ntpServer=NTP server +orbRotation=Orb rotation +orbRotation.0=No rotation +orbRotation.1=Rotate 90° clockwise +orbRotation.2=Rotate 180° +orbRotation.3=Rotate 270° clockwise +nightmode=Enable Nighttime mode +tftBrightness=TFT Brightness [0-255] +dimStartHour=Nighttime Start [24h format] +dimEndHour=Nighttime End [24h format] +dimBrightness=Nighttime Brightness [0-255] From 754e97aa9cf5c876704e86609d2a8037be5989bb Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Tue, 25 Feb 2025 13:47:28 +0100 Subject: [PATCH 14/18] Update i18n keys and strings for ParqetWidget configuration Standardize and localize configuration text by replacing raw strings with i18n keys in `ParqetWidget.cpp`. Added missing translations for new i18n keys in German and English localization files. Improved consistency and clarity in user-facing configuration labels. --- firmware/src/widgets/parqetwidget/ParqetWidget.cpp | 6 +++--- i18n/de.parqet.txt | 5 ++++- i18n/en.parqet.txt | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp index 092cc82e..7494d6b6 100644 --- a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp +++ b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp @@ -14,14 +14,14 @@ ParqetWidget::ParqetWidget(ScreenManager &manager, ConfigManager &config) : Widg m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n("enableWidget")); m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n("pq.cnf.portfolioId")); m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, i18n("pq.cnf.mode"), true); - m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, "Performance measure", true); - m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, "Chart measure", true); + m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, i18n("pq.conf.perfMeasure"), true); + m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, i18n("pq.cnf.chartMeasure"), true); m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, i18n("pq.cnf.clock"), true); m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, i18n("pq.cnf.totals"), true); m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n("pq.cnf.totalVal"), true); String optPriceVal[] = {"Show current price", "Show current value"}; m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, i18n("pq.cnf.values"), true); - m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, "ParqetProxy URL", true); + m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, i18n("pq.cnf.proxyUrl"), true); m_curMode = m_defaultMode; m_curPerfMeasure = m_defaultPerfMeasure; m_curPerfChartMeasure = m_defaultPerfChartMeasure; diff --git a/i18n/de.parqet.txt b/i18n/de.parqet.txt index 7d4414f1..85a2342e 100644 --- a/i18n/de.parqet.txt +++ b/i18n/de.parqet.txt @@ -2,7 +2,10 @@ pq.portfolio=Portfolio pq.total=G E S A M T pq.cnf.portfolioId=Portfolio-ID (muss auf öffentlich stehen!) pq.cnf.mode=Standardzeitraum (Zeitraum kann durch mittellanges Drücken des mittleren Buttons geändert werden) +pq.cnf.perfMeasure=Performance Messwert +pq.cnf.chartMeasure=Chart Messwert pq.cnf.clock=Uhr auf dem ersten Bildschirm anzeigen pq.cnf.totals=Gesamtübersicht anzeigen pq.cnf.totalVal=Gesamtwert des Portfolios anzeigen -pq.cnf.values=Preis oder Wert der Aktien anzeigen \ No newline at end of file +pq.cnf.values=Preis oder Wert der Aktien anzeigen +pq.cnf.proxyUrl=ParqetProxy URL \ No newline at end of file diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt index 910aa2ff..88ea1f0d 100644 --- a/i18n/en.parqet.txt +++ b/i18n/en.parqet.txt @@ -2,7 +2,10 @@ pq.portfolio=Portfolio pq.total=T O T A L pq.cnf.portfolioId=Portfolio ID (must be set to public!) pq.cnf.mode=Default timeframe (you can change timeframes by medium pressing the middle button) +pq.cnf.perfMeasure=Performance measure +pq.cnf.chartMeasure=Chart measure pq.cnf.clock=Show clock on first screen pq.cnf.totals=Show totals screen pq.cnf.totalVal=Show total portfolio value -pq.cnf.values=Show price or value for stocks \ No newline at end of file +pq.cnf.values=Show price or value for stocks +pq.cnf.proxyUrl=ParqetProxy URL \ No newline at end of file From 7a9d912bbf6bc9d9174468f66f5b8849248eeae8 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Tue, 25 Feb 2025 17:07:24 +0100 Subject: [PATCH 15/18] **Refactor i18n implementation and restructure translations** Replaced hardcoded strings with i18n keys across various modules to improve translations consistency and maintainability. Introduced dedicated translation files for better organization, including new headers in specific widgets like `ParqetWidget`. Optimized i18n function usage in configuration settings and removed outdated translation management logic. --- firmware/src/core/i18n/I18n.cpp | 19 +++ firmware/src/core/i18n/I18n.h | 63 +++++++++ firmware/src/core/i18n/Translations.cpp | 133 ++++++++++++++++++ firmware/src/core/i18n/Translations.h | 30 ++++ firmware/src/core/utils/I18n.cpp | 106 -------------- firmware/src/core/utils/I18n.h | 68 --------- firmware/src/core/utils/MainHelper.cpp | 40 +++--- firmware/src/core/utils/MainHelper.h | 4 +- firmware/src/core/widget/Widget.cpp | 4 - firmware/src/core/widget/Widget.h | 3 +- firmware/src/core/widget/WidgetSet.cpp | 2 +- .../parqetwidget/ParqetTranslations.cpp | 72 ++++++++++ .../widgets/parqetwidget/ParqetTranslations.h | 20 +++ .../src/widgets/parqetwidget/ParqetWidget.cpp | 26 ++-- i18n/de.parqet.txt | 11 -- i18n/de.txt | 21 --- i18n/en.parqet.txt | 11 -- i18n/en.txt | 21 --- platformio.ini | 1 + 19 files changed, 373 insertions(+), 282 deletions(-) create mode 100644 firmware/src/core/i18n/I18n.cpp create mode 100644 firmware/src/core/i18n/I18n.h create mode 100644 firmware/src/core/i18n/Translations.cpp create mode 100644 firmware/src/core/i18n/Translations.h delete mode 100644 firmware/src/core/utils/I18n.cpp delete mode 100644 firmware/src/core/utils/I18n.h create mode 100644 firmware/src/widgets/parqetwidget/ParqetTranslations.cpp create mode 100644 firmware/src/widgets/parqetwidget/ParqetTranslations.h delete mode 100644 i18n/de.parqet.txt delete mode 100644 i18n/de.txt delete mode 100644 i18n/en.parqet.txt delete mode 100644 i18n/en.txt diff --git a/firmware/src/core/i18n/I18n.cpp b/firmware/src/core/i18n/I18n.cpp new file mode 100644 index 00000000..ccedcbdf --- /dev/null +++ b/firmware/src/core/i18n/I18n.cpp @@ -0,0 +1,19 @@ +#include "I18n.h" + +String I18n::s_allLanguages[] = ALL_LANGUAGES; +int I18n::s_languageId = DEFAULT_LANGUAGE; + +void I18n::setLanguageId(const int langId) { + s_languageId = langId; +} + +String I18n::getLanguageString(const int langId) { + if (langId >= 0 && langId < LANG_NUM) { + return s_allLanguages[langId]; + } + return "invalid"; +} + +String *I18n::getAllLanguages() { + return s_allLanguages; +} diff --git a/firmware/src/core/i18n/I18n.h b/firmware/src/core/i18n/I18n.h new file mode 100644 index 00000000..9a23cd65 --- /dev/null +++ b/firmware/src/core/i18n/I18n.h @@ -0,0 +1,63 @@ +#ifndef I18N_H +#define I18N_H + +#include "config_helper.h" +#include + +#define ALL_LANGUAGES {"en", "de", "fr"} + +enum Language { + LANG_EN = 0, + LANG_DE, + LANG_FR, + LANG_NUM // keep this as last item +}; + +#define DEFAULT_LANGUAGE LANG_EN + +class I18n { +public: + static void setLanguageId(int langId); + static String getLanguageString(int langId); + static String *getAllLanguages(); + + template + static const char *get(const char *const (&translations)[N]) { + const char *text = translations[s_languageId]; + if (text == nullptr) { + text = translations[DEFAULT_LANGUAGE]; + } + return text ? text : "@missingTranslation@"; + } + + template + static const char *get(const char *const (&translations)[X][Y], size_t index) { + if (index >= X) { + return "@invalidIndex@"; + } + + const char *text = translations[index][s_languageId]; + if (text == nullptr) { + text = translations[index][DEFAULT_LANGUAGE]; + } + return text ? text : "@missingTranslation@"; + } + +private: + static String s_allLanguages[]; + static int s_languageId; +}; + +// I18n helper +template +static const char *i18n(const char *const (&translations)[N]) { + return I18n::get(translations); +} + +// I18n helper (with index) +template +static const char *i18n(const char *const (&translations)[X][Y], size_t index) { + return I18n::get(translations, index); +} + +#endif // I18N_H diff --git a/firmware/src/core/i18n/Translations.cpp b/firmware/src/core/i18n/Translations.cpp new file mode 100644 index 00000000..1ca73cff --- /dev/null +++ b/firmware/src/core/i18n/Translations.cpp @@ -0,0 +1,133 @@ +#include "Translations.h" + +// Languages are defined in I18n.h: +// EN, DE, FR + +// THIS FILE IS ONLY FOR GENERAL TRANSLATIONS +// Widget translations should be placed in a separate .h/.cpp in the Widget directory + +// All translation variables should start with "t_" + +constexpr const char *const t_welcome[LANG_NUM] = { + "Welcome", // EN + "Willkommen", // DE + "Bienvenue" // FR +}; + +constexpr const char *const t_infoOrbs[LANG_NUM] = { + "InfoOrbs", // EN + "InfoOrbs", // DE + "InfoOrbs" // FR +}; + +constexpr const char *const t_by[LANG_NUM] = { + "by", // EN + "von", // DE + "par" // FR +}; + +constexpr const char *const t_brettTech[LANG_NUM] = { + "brett.tech", // EN + "brett.tech", // DE + "brett.tech" // FR +}; + +constexpr const char *const t_version[LANG_NUM] = { + "Version", // EN + "Version", // DE + "Version" // FR +}; + +constexpr const char *const t_loadingData[LANG_NUM] = { + "Loading data:", // EN + "Lade Daten:", // DE + "Chargement:" // FR +}; + +constexpr const char *const t_enableWidget[LANG_NUM] = { + "Enable Widget", // EN + "Widget aktivieren", // DE + "Activer le widget" // FR +}; + +constexpr const char *const t_timezoneLoc[LANG_NUM] = { + "Timezone, use one from this list", // EN + "Zeitzone, verwenden Sie eine aus dieser Liste", // DE + "Fuseau horaire, utilisez-en un de cette liste" // FR +}; + +constexpr const char *const t_language[LANG_NUM] = { + "Language", // EN + "Sprache", // DE + "Langue" // FR +}; + +constexpr const char *const t_widgetCycleDelay[LANG_NUM] = { + "Automatically cycle widgets every X seconds, set to 0 to disable", // EN + "Wechseln Sie die Widgets automatisch alle X Sekunden, auf 0 setzen, um zu deaktivieren", // DE + "Faites défiler les widgets automatiquement toutes les X secondes, définissez sur 0 pour désactiver" // FR +}; + +constexpr const char *const t_ntpServer[LANG_NUM] = { + "NTP server", // EN + "NTP-Server", // DE + "Serveur NTP" // FR +}; + +constexpr const char *const t_orbRotation[LANG_NUM] = { + "Orb rotation", // EN + "Orb-Rotation", // DE + "Rotation des Orbes" // FR +}; + +constexpr const char *const t_orbRot[4][LANG_NUM] = { + { + "No rotation", // EN + "Keine Rotation", // DE + "Pas de rotation" // FR + }, + { + "Rotate 90° clockwise", // EN + "90° im Uhrzeigersinn", // DE + "Tourné de 90°" // FR + }, + { + "Rotate 180°", // EN + "180° gedreht", // DE + "Tourné de 180°" // FR + }, + { + "Rotate 270° clockwise", // EN + "270° im Uhrzeigersinn", // DE + "Tourné de 270°" // FR + }}; + +constexpr const char *const t_nightmode[LANG_NUM] = { + "Enable Nighttime mode", // EN + "Nachtmodus aktivieren", // DE + "Activer le mode nuit" // FR +}; + +constexpr const char *const t_tftBrightness[LANG_NUM] = { + "TFT Brightness [0-255]", // EN + "TFT-Helligkeit [0-255]", // DE + "Luminosité TFT [0-255]" // FR +}; + +constexpr const char *const t_dimStartHour[LANG_NUM] = { + "Nighttime Start [24h format]", // EN + "Nachtmodus Start [24h-Format]", // DE + "Début de la nuit [format 24h]" // FR +}; + +constexpr const char *const t_dimEndHour[LANG_NUM] = { + "Nighttime End [24h format]", // EN + "Nachtmodus Ende [24h-Format]", // DE + "Fin de la nuit [format 24h]" // FR +}; + +constexpr const char *const t_dimBrightness[LANG_NUM] = { + "Nighttime Brightness [0-255]", // EN + "Nachtmodus Helligkeit [0-255]", // DE + "Luminosité nocturne [0-255]" // FR +}; diff --git a/firmware/src/core/i18n/Translations.h b/firmware/src/core/i18n/Translations.h new file mode 100644 index 00000000..b8e4e1ed --- /dev/null +++ b/firmware/src/core/i18n/Translations.h @@ -0,0 +1,30 @@ +#ifndef TRANSLATIONS_H +#define TRANSLATIONS_H + +#include "I18n.h" + +// THIS FILE IS ONLY FOR GENERAL TRANSLATIONS +// Widget translations should be placed in a separate .h/.cpp in the Widget directory + +// All translation variables should start with "t_" + +extern const char *const t_welcome[LANG_NUM]; +extern const char *const t_infoOrbs[LANG_NUM]; +extern const char *const t_by[LANG_NUM]; +extern const char *const t_brettTech[LANG_NUM]; +extern const char *const t_version[LANG_NUM]; +extern const char *const t_loadingData[LANG_NUM]; +extern const char *const t_enableWidget[LANG_NUM]; +extern const char *const t_timezoneLoc[LANG_NUM]; +extern const char *const t_language[LANG_NUM]; +extern const char *const t_widgetCycleDelay[LANG_NUM]; +extern const char *const t_ntpServer[LANG_NUM]; +extern const char *const t_orbRotation[LANG_NUM]; +extern const char *const t_orbRot[4][LANG_NUM]; +extern const char *const t_nightmode[LANG_NUM]; +extern const char *const t_tftBrightness[LANG_NUM]; +extern const char *const t_dimStartHour[LANG_NUM]; +extern const char *const t_dimEndHour[LANG_NUM]; +extern const char *const t_dimBrightness[LANG_NUM]; + +#endif // TRANSLATIONS_H diff --git a/firmware/src/core/utils/I18n.cpp b/firmware/src/core/utils/I18n.cpp deleted file mode 100644 index 3da7471f..00000000 --- a/firmware/src/core/utils/I18n.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "I18n.h" - -#include -#include -#include // For strdup and free - -String I18n::s_allLanguages[] = ALL_LANGUAGES; -std::map I18n::s_translations; -int I18n::s_languageId = DEFAULT_LANGUAGE; - -void I18n::setLanguageId(const int langId) { - clearTranslations(); // Clear previous translations - s_languageId = langId; - if (s_languageId != DEFAULT_LANGUAGE) { - loadFile(DEFAULT_LANGUAGE); // Fallback translations - } - loadFile(langId); // Load selected language -} - -void I18n::clearTranslations() { - for (auto &entry : s_translations) { - free((void *) entry.second); // Free allocated memory - } - s_translations.clear(); -} - -String I18n::getLanguageString(const int langId) { - if (langId >= 0 && langId < LANG_NUM) { - return s_allLanguages[langId]; - } - return "invalid"; -} - -String *I18n::getAllLanguages() { - return s_allLanguages; -} - -void I18n::loadExtraTranslations(const String &extraName) { - if (extraName.isEmpty()) { - return; - } - // Convert Widget name to lowercase for file name - auto lowercaseName = extraName; - lowercaseName.toLowerCase(); - if (s_languageId != DEFAULT_LANGUAGE) { - // Load default translations (fallback) - loadFile(getLanguageString(DEFAULT_LANGUAGE) + "." + lowercaseName); - } - loadFile(getLanguageString(s_languageId) + "." + lowercaseName); -} - -bool I18n::loadFile(const int langId) { - return loadFile(getLanguageString(langId)); -} - -bool I18n::loadFile(const String &filename) { - if (filename.isEmpty()) { - return false; - } - const String fullName = I18N_DIR + filename + ".txt"; - File file = LittleFS.open(fullName, "r"); - if (!file) { - Log.warningln("Failed to open language file %s", fullName.c_str()); - return false; - } - while (file.available()) { - String line = file.readStringUntil('\n'); - line.trim(); - - if (line.length() == 0 || line.startsWith("#")) - continue; - - int sepIndex = line.indexOf('='); - if (sepIndex > 0) { - String key = line.substring(0, sepIndex); - String value = line.substring(sepIndex + 1); - key.trim(); - value.trim(); - // Free existing value if key already exists - if (s_translations.count(key)) { - free((void *) s_translations[key]); - } - - // Store as const char* - s_translations[key] = strdup(value.c_str()); - } - } - file.close(); - Log.noticeln("Loaded language file %s", fullName.c_str()); - return true; -} - -const char *I18n::get(const String &key) { - if (s_translations.count(key)) { - const auto val = s_translations[key]; - Log.traceln("Translation found: %s -> %s", key.c_str(), val); - return val; - } - Log.warningln("Translation not found: %s", key.c_str()); - return "@missingTranslation@"; -} - -void I18n::replacePlaceholder(String &str, int index, const String &value) { - String placeholder = "\\" + String(index); // e.g., \1, \2 - str.replace(placeholder, value); -} \ No newline at end of file diff --git a/firmware/src/core/utils/I18n.h b/firmware/src/core/utils/I18n.h deleted file mode 100644 index 013a9c2a..00000000 --- a/firmware/src/core/utils/I18n.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef I18N_H -#define I18N_H - -#include "config_helper.h" -#include -#include - -#ifndef I18N_DIR - #define I18N_DIR "/i18n/" -#endif - -#define ALL_LANGUAGES {"en", "de", "fr"} - -enum Language { - LANG_EN = 0, - LANG_DE, - LANG_FR, - LANG_NUM -}; - -#define DEFAULT_LANGUAGE LANG_EN - -class I18n { -public: - static void setLanguageId(int langId); - static String getLanguageString(int langId); - static String *getAllLanguages(); - - static void loadExtraTranslations(const String &extraName); - - // Returning const char* here allows us to use it in ConfigManager.addConfig*() - // because it will never go out of scope (as long as the translation is not removed) - static const char *get(const String &key); - - // Returning String here instead of const char* as it ensures the memory - // is managed properly and avoids the risk of accessing invalid memory - // from dynamic translations. However, this approach does not allow us to - // use it in ConfigManager.addConfig*(), because the String and its - // c_str might go out of scope - template - static String get(const String &key, Args... args) { - String result = get(key); - replacePlaceholders(result, args...); - return result; - } - -private: - static String s_allLanguages[]; - static int s_languageId; - static std::map s_translations; - - static void clearTranslations(); - static bool loadFile(const String &filename); - static bool loadFile(int langId); - - static void replacePlaceholder(String &str, int index, const String &value); - - template - static void replacePlaceholders(String &str, T value, Args... args) { - static int index = 1; - replacePlaceholder(str, index++, String(value)); - replacePlaceholders(str, args...); - } - - static void replacePlaceholders(String &) {} -}; - -#endif // I18N_H diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index 021f16fd..c8728f7c 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -4,6 +4,7 @@ #include "icons.h" #include #include +#include #include static Button buttonLeft; @@ -63,20 +64,20 @@ void MainHelper::setupButtons() { void MainHelper::setupConfig() { // Set language here to get i18n strings for the configuration I18n::setLanguageId(s_configManager->getConfigInt("lang", DEFAULT_LANGUAGE)); - s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, i18n("timezoneLoc")); + s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, i18n(t_timezoneLoc)); String *optLang = I18n::getAllLanguages(); - s_configManager->addConfigComboBox("General", "lang", &s_languageId, optLang, LANG_NUM, i18n("language")); - s_configManager->addConfigInt("General", "widgetCycDelay", &s_widgetCycleDelay, i18n("widgetCycleDelay")); - s_configManager->addConfigString("General", "ntpServer", &s_ntpServer, 30, i18n("ntpServer"), true); - String optRotation[] = {i18n("orbRotation.0"), i18n("orbRotation.1"), i18n("orbRotation.2"), i18n("orbRotation.3")}; - s_configManager->addConfigComboBox("TFT Settings", "orbRotation", &s_orbRotation, optRotation, 4, i18n("orbRotation")); - s_configManager->addConfigBool("TFT Settings", "nightmode", &s_nightMode, i18n("nightmode")); - s_configManager->addConfigInt("TFT Settings", "tftBrightness", &s_tftBrightness, i18n("tftBrightness"), true); + s_configManager->addConfigComboBox("General", "lang", &s_languageId, optLang, LANG_NUM, i18n(t_language)); + s_configManager->addConfigInt("General", "widgetCycDelay", &s_widgetCycleDelay, i18n(t_widgetCycleDelay)); + s_configManager->addConfigString("General", "ntpServer", &s_ntpServer, 30, i18n(t_ntpServer), true); + String optRotation[] = {i18n(t_orbRot, 0), i18n(t_orbRot, 1), i18n(t_orbRot, 2), i18n(t_orbRot, 3)}; + s_configManager->addConfigComboBox("TFT Settings", "orbRotation", &s_orbRotation, optRotation, 4, i18n(t_orbRotation)); + s_configManager->addConfigBool("TFT Settings", "nightmode", &s_nightMode, i18n(t_nightmode)); + s_configManager->addConfigInt("TFT Settings", "tftBrightness", &s_tftBrightness, i18n(t_tftBrightness), true); String optHours[] = {"0:00", "1:00", "2:00", "3:00", "4:00", "5:00", "6:00", "7:00", "8:00", "9:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00"}; - s_configManager->addConfigComboBox("TFT Settings", "dimStartHour", &s_dimStartHour, optHours, 24, i18n("dimStartHour"), true); - s_configManager->addConfigComboBox("TFT Settings", "dimEndHour", &s_dimEndHour, optHours, 24, i18n("dimEndHour"), true); - s_configManager->addConfigInt("TFT Settings", "dimBrightness", &s_dimBrightness, i18n("dimBrightness"), true); + s_configManager->addConfigComboBox("TFT Settings", "dimStartHour", &s_dimStartHour, optHours, 24, i18n(t_dimStartHour), true); + s_configManager->addConfigComboBox("TFT Settings", "dimEndHour", &s_dimEndHour, optHours, 24, i18n(t_dimEndHour), true); + s_configManager->addConfigInt("TFT Settings", "dimBrightness", &s_dimBrightness, i18n(t_dimBrightness), true); } void MainHelper::buttonPressed(uint8_t buttonId, ButtonState state) { @@ -423,7 +424,7 @@ void MainHelper::showWelcome() { s_screenManager->setFontColor(TFT_WHITE); s_screenManager->selectScreen(0); - s_screenManager->drawCentreString(I18n::get("welcome"), ScreenCenterX, ScreenCenterY, 29); + s_screenManager->drawCentreString(i18n(t_welcome), ScreenCenterX, ScreenCenterY, 29); if (GIT_BRANCH != "main" && GIT_BRANCH != "unknown" && GIT_BRANCH != "HEAD") { s_screenManager->setFontColor(TFT_RED); s_screenManager->drawCentreString(GIT_BRANCH, ScreenCenterX, ScreenCenterY - 40, 15); @@ -432,11 +433,12 @@ void MainHelper::showWelcome() { } s_screenManager->selectScreen(1); - s_screenManager->drawCentreString(I18n::get("infoOrbs"), ScreenCenterX, ScreenCenterY - 50, 22); - s_screenManager->drawCentreString(I18n::get("by"), ScreenCenterX, ScreenCenterY - 5, 22); - s_screenManager->drawCentreString(I18n::get("brett.tech"), ScreenCenterX, ScreenCenterY + 30, 22); + s_screenManager->drawCentreString(i18n(t_infoOrbs), ScreenCenterX, ScreenCenterY - 50, 22); + s_screenManager->drawCentreString(i18n(t_by), ScreenCenterX, ScreenCenterY - 5, 22); + s_screenManager->drawCentreString(i18n(t_brettTech), ScreenCenterX, ScreenCenterY + 30, 22); s_screenManager->setFontColor(TFT_RED); - s_screenManager->drawCentreString(I18n::get("version", "1.1.0"), ScreenCenterX, ScreenCenterY + 65, 15); + const auto version = String(i18n(t_version)) + " " + String(VERSION); + s_screenManager->drawCentreString(version, ScreenCenterX, ScreenCenterY + 65, 15); s_screenManager->selectScreen(2); s_screenManager->drawJpg(0, 0, logo_start, logo_end - logo_start); @@ -507,8 +509,4 @@ void MainHelper::watchdogInit() { void MainHelper::watchdogReset() { esp_task_wdt_reset(); -} - -const char *MainHelper::i18n(const String &key) { - return I18n::get(key); -} +} \ No newline at end of file diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index b3f39b97..e77e0f32 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -12,6 +12,8 @@ #include "git_info.h" #include +#define VERSION "1.2beta" + // Set defaults if not set in config.h #ifndef TFT_BRIGHTNESS #define TFT_BRIGHTNESS 255 @@ -80,8 +82,6 @@ class MainHelper { static void watchdogReset(); static void updateBrightnessByTime(uint8_t hour24); - - static const char *i18n(const String &key); }; #endif \ No newline at end of file diff --git a/firmware/src/core/widget/Widget.cpp b/firmware/src/core/widget/Widget.cpp index c7ba2ef1..ef20637e 100644 --- a/firmware/src/core/widget/Widget.cpp +++ b/firmware/src/core/widget/Widget.cpp @@ -13,7 +13,3 @@ void Widget::setBusy(bool busy) { bool Widget::isEnabled() { return m_enabled; } - -const char *Widget::i18n(const String &key) { - return I18n::get(key); -} diff --git a/firmware/src/core/widget/Widget.h b/firmware/src/core/widget/Widget.h index 553c3c60..7170e402 100644 --- a/firmware/src/core/widget/Widget.h +++ b/firmware/src/core/widget/Widget.h @@ -3,8 +3,8 @@ #include "Button.h" #include "ConfigManager.h" -#include "I18n.h" #include "ScreenManager.h" +#include "Translations.h" // include for use by all Widgets #include "config_helper.h" class Widget { @@ -18,7 +18,6 @@ class Widget { virtual String getName() = 0; void setBusy(bool busy); bool isEnabled(); - static const char *i18n(const String &key); protected: ScreenManager &m_manager; diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index e44a0703..61aa4661 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -91,7 +91,7 @@ void WidgetSet::showCenteredLine(int screen, const String &text) { } void WidgetSet::showLoading() { - showCenteredLine(3, I18n::get("loadingData")); + showCenteredLine(3, I18n::get(t_loadingData)); } void WidgetSet::updateAll() { diff --git a/firmware/src/widgets/parqetwidget/ParqetTranslations.cpp b/firmware/src/widgets/parqetwidget/ParqetTranslations.cpp new file mode 100644 index 00000000..41c507cd --- /dev/null +++ b/firmware/src/widgets/parqetwidget/ParqetTranslations.cpp @@ -0,0 +1,72 @@ +#include "ParqetTranslations.h" + +// Languages are defined in I18n.h: +// EN, DE, FR + +// All translation variables should start with "t_" + +constexpr const char *const t_portfolio[LANG_NUM] = { + "Portfolio", // EN + "Portfolio", // DE + "Portefeuille" // FR +}; + +constexpr const char *const t_total[LANG_NUM] = { + "T O T A L", // EN + "G E S A M T", // DE + "T O T A L" // FR +}; + +constexpr const char *const t_cnfPortfolioId[LANG_NUM] = { + "Portfolio ID (must be set to public!)", // EN + "Portfolio-ID (muss auf öffentlich gesetzt werden!)", // DE + "ID du portefeuille (doit être défini sur public!)" // FR +}; + +constexpr const char *const t_cnfMode[LANG_NUM] = { + "Default timeframe (you can change timeframes by medium pressing the middle button)", // EN + "Standard-Zeitrahmen (Sie können die Zeitrahmen durch mittellanges Drücken der mittleren Taste ändern)", // DE + "Plage horaire par défaut (vous pouvez changer les plages horaires en appuyant sur le bouton central)" // FR +}; + +constexpr const char *const t_cnfPerfMeasure[LANG_NUM] = { + "Performance measure", // EN + "Performance-Messwert", // DE + "Mesure de performance" // FR +}; + +constexpr const char *const t_cnfChartMeasure[LANG_NUM] = { + "Chart measure", // EN + "Chart-Messwert", // DE + "Mesure du graphique" // FR +}; + +constexpr const char *const t_cnfClock[LANG_NUM] = { + "Show clock on first screen", // EN + "Uhr auf dem ersten Bildschirm anzeigen", // DE + "Afficher l'horloge sur le premier écran" // FR +}; + +constexpr const char *const t_cnfTotals[LANG_NUM] = { + "Show totals screen", // EN + "Gesamtbildschirm anzeigen", // DE + "Afficher l'écran total" // FR +}; + +constexpr const char *const t_cnfTotalVal[LANG_NUM] = { + "Show total portfolio value", // EN + "Gesamtportfolio-Wert anzeigen", // DE + "Afficher la valeur totale du portefeuille" // FR +}; + +constexpr const char *const t_cnfValues[LANG_NUM] = { + "Show price or value for stocks", // EN + "Preis oder Wert für Aktien anzeigen", // DE + "Afficher le prix ou la valeur des actions" // FR +}; + +constexpr const char *const t_cnfProxyUrl[LANG_NUM] = { + "ParqetProxy URL", // EN + "ParqetProxy-URL", // DE + "URL ParqetProxy" // FR +}; diff --git a/firmware/src/widgets/parqetwidget/ParqetTranslations.h b/firmware/src/widgets/parqetwidget/ParqetTranslations.h new file mode 100644 index 00000000..a5630949 --- /dev/null +++ b/firmware/src/widgets/parqetwidget/ParqetTranslations.h @@ -0,0 +1,20 @@ +#ifndef PARQETTRANSLATIONS_H +#define PARQETTRANSLATIONS_H + +#include "I18n.h" + +// All translation variables should start with "t_" + +extern const char *const t_portfolio[LANG_NUM]; +extern const char *const t_total[LANG_NUM]; +extern const char *const t_cnfPortfolioId[LANG_NUM]; +extern const char *const t_cnfMode[LANG_NUM]; +extern const char *const t_cnfPerfMeasure[LANG_NUM]; +extern const char *const t_cnfChartMeasure[LANG_NUM]; +extern const char *const t_cnfClock[LANG_NUM]; +extern const char *const t_cnfTotals[LANG_NUM]; +extern const char *const t_cnfTotalVal[LANG_NUM]; +extern const char *const t_cnfValues[LANG_NUM]; +extern const char *const t_cnfProxyUrl[LANG_NUM]; + +#endif // PARQETTRANSLATIONS_H diff --git a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp index 7494d6b6..8520d9af 100644 --- a/firmware/src/widgets/parqetwidget/ParqetWidget.cpp +++ b/firmware/src/widgets/parqetwidget/ParqetWidget.cpp @@ -1,27 +1,25 @@ #include "ParqetWidget.h" +#include "ParqetTranslations.h" #include #include #include #include - #include ParqetWidget::ParqetWidget(ScreenManager &manager, ConfigManager &config) : Widget(manager, config) { Serial.printf("Constructing ParqetWidget, portfolioId=%s\n", m_portfolioId.c_str()); - // Load widget specific translations - I18n::loadExtraTranslations(ParqetWidget::getName()); - m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n("enableWidget")); - m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n("pq.cnf.portfolioId")); - m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, i18n("pq.cnf.mode"), true); - m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, i18n("pq.conf.perfMeasure"), true); - m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, i18n("pq.cnf.chartMeasure"), true); - m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, i18n("pq.cnf.clock"), true); - m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, i18n("pq.cnf.totals"), true); - m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n("pq.cnf.totalVal"), true); + m_config.addConfigBool("ParqetWidget", "pqEnabled", &m_enabled, i18n(t_enableWidget)); + m_config.addConfigString("ParqetWidget", "pqportfoId", &m_portfolioId, 50, i18n(t_cnfPortfolioId)); + m_config.addConfigComboBox("ParqetWidget", "pqDefMode", &m_defaultMode, m_modes, PARQET_MODE_COUNT, i18n(t_cnfMode), true); + m_config.addConfigComboBox("ParqetWidget", "pqDefPerf", &m_defaultPerfMeasure, m_perfMeasures, PARQET_PERF_COUNT, i18n(t_cnfPerfMeasure), true); + m_config.addConfigComboBox("ParqetWidget", "pqDefPerfCh", &m_defaultPerfChartMeasure, m_perfChartMeasures, PARQET_PERF_CHART_COUNT, i18n(t_cnfChartMeasure), true); + m_config.addConfigBool("ParqetWidget", "pqShowClock", &m_showClock, i18n(t_cnfClock), true); + m_config.addConfigBool("ParqetWidget", "pqShowTotalScr", &m_showTotalScreen, i18n(t_cnfTotals), true); + m_config.addConfigBool("ParqetWidget", "pqShowTotalVal", &m_showTotalValue, i18n(t_cnfTotalVal), true); String optPriceVal[] = {"Show current price", "Show current value"}; - m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, i18n("pq.cnf.values"), true); - m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, i18n("pq.cnf.proxyUrl"), true); + m_config.addConfigComboBox("ParqetWidget", "pqShowValues", &m_showValues, optPriceVal, 2, i18n(t_cnfValues), true); + m_config.addConfigString("ParqetWidget", "pqProxyUrl", &m_proxyUrl, 75, i18n(t_cnfProxyUrl), true); m_curMode = m_defaultMode; m_curPerfMeasure = m_defaultPerfMeasure; m_curPerfChartMeasure = m_defaultPerfChartMeasure; @@ -292,7 +290,7 @@ void ParqetWidget::displayStock(int8_t displayIndex, ParqetHoldingDataModel &sto m_manager.drawString(stock.getCurrentPrice(2), ScreenCenterX, 58, 26, Align::MiddleCenter); } } else { - m_manager.drawString(i18n("pq.portfolio"), ScreenCenterX, 58, 26, Align::MiddleCenter); + m_manager.drawString(i18n(t_portfolio), ScreenCenterX, 58, 26, Align::MiddleCenter); } if (m_showTotalChart && stock.getId() == "total" && m_portfolio.getChartDataCount() >= 7) { diff --git a/i18n/de.parqet.txt b/i18n/de.parqet.txt deleted file mode 100644 index 85a2342e..00000000 --- a/i18n/de.parqet.txt +++ /dev/null @@ -1,11 +0,0 @@ -pq.portfolio=Portfolio -pq.total=G E S A M T -pq.cnf.portfolioId=Portfolio-ID (muss auf öffentlich stehen!) -pq.cnf.mode=Standardzeitraum (Zeitraum kann durch mittellanges Drücken des mittleren Buttons geändert werden) -pq.cnf.perfMeasure=Performance Messwert -pq.cnf.chartMeasure=Chart Messwert -pq.cnf.clock=Uhr auf dem ersten Bildschirm anzeigen -pq.cnf.totals=Gesamtübersicht anzeigen -pq.cnf.totalVal=Gesamtwert des Portfolios anzeigen -pq.cnf.values=Preis oder Wert der Aktien anzeigen -pq.cnf.proxyUrl=ParqetProxy URL \ No newline at end of file diff --git a/i18n/de.txt b/i18n/de.txt deleted file mode 100644 index 4176079e..00000000 --- a/i18n/de.txt +++ /dev/null @@ -1,21 +0,0 @@ -welcome=Willkommen -infoOrbs=InfoOrbs -by=von -brett.tech=brett.tech -version=Version \1 -loadingData=Lade Daten: -enableWidget=Widget aktivieren -timezoneLoc=Zeitzone, verwende eine aus dieser Liste -language=Sprache -widgetCycleDelay=Widgets automatisch alle X Sekunden wechseln, 0 zum Deaktivieren -ntpServer=NTP-Server -orbRotation=Orb-Drehung -orbRotation.0=Keine Drehung -orbRotation.1=90° im Uhrzeigersinn -orbRotation.2=180° drehen -orbRotation.3=270° im Uhrzeigersinn -nightmode=Nachtmodus aktivieren -tftBrightness=TFT-Helligkeit [0-255] -dimStartHour=Startzeit Nachtmodus [24h-Format] -dimEndHour=Endzeit Nachtmodus [24h-Format] -dimBrightness=Helligkeit im Nachtmodus [0-255] \ No newline at end of file diff --git a/i18n/en.parqet.txt b/i18n/en.parqet.txt deleted file mode 100644 index 88ea1f0d..00000000 --- a/i18n/en.parqet.txt +++ /dev/null @@ -1,11 +0,0 @@ -pq.portfolio=Portfolio -pq.total=T O T A L -pq.cnf.portfolioId=Portfolio ID (must be set to public!) -pq.cnf.mode=Default timeframe (you can change timeframes by medium pressing the middle button) -pq.cnf.perfMeasure=Performance measure -pq.cnf.chartMeasure=Chart measure -pq.cnf.clock=Show clock on first screen -pq.cnf.totals=Show totals screen -pq.cnf.totalVal=Show total portfolio value -pq.cnf.values=Show price or value for stocks -pq.cnf.proxyUrl=ParqetProxy URL \ No newline at end of file diff --git a/i18n/en.txt b/i18n/en.txt deleted file mode 100644 index b5e2d68f..00000000 --- a/i18n/en.txt +++ /dev/null @@ -1,21 +0,0 @@ -welcome=Welcome -infoOrbs=InfoOrbs -by=by -brett.tech=brett.tech -version=Version \1 -loadingData=Loading data: -enableWidget=Enable Widget -timezoneLoc=Timezone Location, use one from this list -language=Language -widgetCycleDelay=Automatically cycle widgets every X seconds, set to 0 to disable -ntpServer=NTP server -orbRotation=Orb rotation -orbRotation.0=No rotation -orbRotation.1=Rotate 90° clockwise -orbRotation.2=Rotate 180° -orbRotation.3=Rotate 270° clockwise -nightmode=Enable Nighttime mode -tftBrightness=TFT Brightness [0-255] -dimStartHour=Nighttime Start [24h format] -dimEndHour=Nighttime End [24h format] -dimBrightness=Nighttime Brightness [0-255] diff --git a/platformio.ini b/platformio.ini index dd4c256d..94fbadfc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -87,6 +87,7 @@ build_flags = -I firmware/src/core/button -I firmware/src/core/configmanager -I firmware/src/core/globaltime + -I firmware/src/core/i18n -I firmware/src/core/screenmanager -I firmware/src/core/utils -I firmware/src/core/widget From e3afb3321787750026a680bca3e49b97a4890fa5 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Tue, 25 Feb 2025 17:14:32 +0100 Subject: [PATCH 16/18] Update translations for orb rotation labels Updated German translations to improve clarity and consistency. Simplified English rotation descriptions to remove redundant "clockwise" term. French translations remain unchanged. --- firmware/src/core/i18n/Translations.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/firmware/src/core/i18n/Translations.cpp b/firmware/src/core/i18n/Translations.cpp index 1ca73cff..390ad32d 100644 --- a/firmware/src/core/i18n/Translations.cpp +++ b/firmware/src/core/i18n/Translations.cpp @@ -76,19 +76,19 @@ constexpr const char *const t_ntpServer[LANG_NUM] = { constexpr const char *const t_orbRotation[LANG_NUM] = { "Orb rotation", // EN - "Orb-Rotation", // DE + "Orb-Drehung", // DE "Rotation des Orbes" // FR }; constexpr const char *const t_orbRot[4][LANG_NUM] = { { "No rotation", // EN - "Keine Rotation", // DE + "Keine Drehung", // DE "Pas de rotation" // FR }, { - "Rotate 90° clockwise", // EN - "90° im Uhrzeigersinn", // DE + "Rotate 90°", // EN + "90° gedreht", // DE "Tourné de 90°" // FR }, { @@ -97,8 +97,8 @@ constexpr const char *const t_orbRot[4][LANG_NUM] = { "Tourné de 180°" // FR }, { - "Rotate 270° clockwise", // EN - "270° im Uhrzeigersinn", // DE + "Rotate 270°", // EN + "270° gedreht", // DE "Tourné de 270°" // FR }}; From 845d80ef39b1639d7d2dcedfd0640e060c8ac995 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Tue, 25 Feb 2025 17:20:12 +0100 Subject: [PATCH 17/18] Removed old .txt i18n files from copy_files_to_littlefs.py --- scripts/copy_files_to_littlefs.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/copy_files_to_littlefs.py b/scripts/copy_files_to_littlefs.py index 43243056..af926b93 100644 --- a/scripts/copy_files_to_littlefs.py +++ b/scripts/copy_files_to_littlefs.py @@ -18,11 +18,6 @@ # Map macros to directories and their respective files # The final filename is constructed as "dir + file" so the dir should end with a "/" unless you know what you are doing embed_map = { - "CONFIG_H != 0": [ # Always true - ["i18n/", # Source directory - ["*.txt"], # Files - 0], # Do not skip parent directories - ], "USE_CLOCK_CUSTOM > 0": [ # Condition ["images/clock/CustomClock0/", # Source directory ["0.jpg", "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg"], From 2a5198f5b6485a156bbc7bd167152ec819ad2ef0 Mon Sep 17 00:00:00 2001 From: Christian Erpelding Date: Tue, 25 Feb 2025 17:26:42 +0100 Subject: [PATCH 18/18] Remove unused methods and streamline includes. Removed unused `getAllWidgets` and `getWidgetCount` methods from `WidgetSet`. Cleaned up redundant includes in `MainHelper.h` and adjusted related files for consistency. Minor update to use `DEFAULT_LANGUAGE` instead of a hardcoded value. --- firmware/src/core/utils/MainHelper.cpp | 5 +++-- firmware/src/core/utils/MainHelper.h | 1 - firmware/src/core/widget/Widget.cpp | 2 +- firmware/src/core/widget/WidgetSet.cpp | 8 -------- firmware/src/core/widget/WidgetSet.h | 2 -- 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/firmware/src/core/utils/MainHelper.cpp b/firmware/src/core/utils/MainHelper.cpp index c8728f7c..d9186594 100644 --- a/firmware/src/core/utils/MainHelper.cpp +++ b/firmware/src/core/utils/MainHelper.cpp @@ -1,10 +1,10 @@ #include "MainHelper.h" #include "LittleFSHelper.h" +#include "Translations.h" #include "config_helper.h" #include "icons.h" #include #include -#include #include static Button buttonLeft; @@ -24,7 +24,7 @@ static bool s_nightMode = DIM_ENABLED; static int s_dimStartHour = DIM_START_HOUR; static int s_dimEndHour = DIM_END_HOUR; static int s_dimBrightness = DIM_BRIGHTNESS; -static int s_languageId = LANG_EN; +static int s_languageId = DEFAULT_LANGUAGE; void MainHelper::init(WiFiManager *wm, ConfigManager *cm, ScreenManager *sm, WidgetSet *ws) { s_wifiManager = wm; @@ -437,6 +437,7 @@ void MainHelper::showWelcome() { s_screenManager->drawCentreString(i18n(t_by), ScreenCenterX, ScreenCenterY - 5, 22); s_screenManager->drawCentreString(i18n(t_brettTech), ScreenCenterX, ScreenCenterY + 30, 22); s_screenManager->setFontColor(TFT_RED); + // VERSION is defined in MainHelper.h const auto version = String(i18n(t_version)) + " " + String(VERSION); s_screenManager->drawCentreString(version, ScreenCenterX, ScreenCenterY + 65, 15); diff --git a/firmware/src/core/utils/MainHelper.h b/firmware/src/core/utils/MainHelper.h index e77e0f32..c53d7cae 100644 --- a/firmware/src/core/utils/MainHelper.h +++ b/firmware/src/core/utils/MainHelper.h @@ -3,7 +3,6 @@ #include "Button.h" #include "ConfigManager.h" -#include "I18n.h" #include "OrbsWiFiManager.h" #include "ScreenManager.h" #include "ShowMemoryUsage.h" diff --git a/firmware/src/core/widget/Widget.cpp b/firmware/src/core/widget/Widget.cpp index ef20637e..401539ce 100644 --- a/firmware/src/core/widget/Widget.cpp +++ b/firmware/src/core/widget/Widget.cpp @@ -12,4 +12,4 @@ void Widget::setBusy(bool busy) { bool Widget::isEnabled() { return m_enabled; -} +} \ No newline at end of file diff --git a/firmware/src/core/widget/WidgetSet.cpp b/firmware/src/core/widget/WidgetSet.cpp index 61aa4661..241337f2 100644 --- a/firmware/src/core/widget/WidgetSet.cpp +++ b/firmware/src/core/widget/WidgetSet.cpp @@ -13,14 +13,6 @@ void WidgetSet::add(Widget *widget) { m_widgetCount++; } -Widget **WidgetSet::getAllWidgets() { - return m_widgets; -} - -uint8_t WidgetSet::getWidgetCount() const { - return m_widgetCount; -} - void WidgetSet::drawCurrent(bool force) { if (m_clearScreensOnDrawCurrent) { m_screenManager->clearAllScreens(); diff --git a/firmware/src/core/widget/WidgetSet.h b/firmware/src/core/widget/WidgetSet.h index 616d80e5..1f26c472 100644 --- a/firmware/src/core/widget/WidgetSet.h +++ b/firmware/src/core/widget/WidgetSet.h @@ -11,8 +11,6 @@ class WidgetSet { public: WidgetSet(ScreenManager *sm); void add(Widget *widget); - Widget **getAllWidgets(); - uint8_t getWidgetCount() const; void drawCurrent(bool force = false); void updateCurrent(); Widget *getCurrent();