Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Translation support (I18n) #240

Merged
merged 18 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a698461
Add internationalization (i18n) support with language files
flattermann Feb 21, 2025
a9d6029
Enhance embedding logic to support wildcard file matching
flattermann Feb 24, 2025
081af79
Refactor I18n handling and enhance WidgetSet functionality
flattermann Feb 24, 2025
df0c972
Refactor I18N_DIR definition placement.
flattermann Feb 24, 2025
1da802a
Simplify widget translation loading logic.
flattermann Feb 24, 2025
ca82d19
Set default language fallback and refine translation loading.
flattermann Feb 24, 2025
2e9534b
Add multi-language support with language selection
flattermann Feb 24, 2025
1526a82
Fix navigation logic in WidgetSet::prev()
flattermann Feb 24, 2025
0e94df2
Refactor loading text to use i18n translations.
flattermann Feb 24, 2025
b39efac
Refactor I18n to optimize memory handling and translation loading.
flattermann Feb 24, 2025
419bd59
Added comments clarifying return type decisions for better memory man…
flattermann Feb 24, 2025
5b66d99
Refactor i18n usage in ParqetWidget for improved localization
flattermann Feb 24, 2025
a460e45
Refactor i18n strings and enhance logging for translations.
flattermann Feb 24, 2025
754e97a
Update i18n keys and strings for ParqetWidget configuration
flattermann Feb 25, 2025
7a9d912
**Refactor i18n implementation and restructure translations**
flattermann Feb 25, 2025
e3afb33
Update translations for orb rotation labels
flattermann Feb 25, 2025
845d80e
Removed old .txt i18n files from copy_files_to_littlefs.py
flattermann Feb 25, 2025
2a5198f
Remove unused methods and streamline includes.
flattermann Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions firmware/src/core/i18n/I18n.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
63 changes: 63 additions & 0 deletions firmware/src/core/i18n/I18n.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#ifndef I18N_H
#define I18N_H

#include "config_helper.h"
#include <Arduino.h>

#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 <size_t N>
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 <size_t X, size_t Y>
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 <size_t N>
static const char *i18n(const char *const (&translations)[N]) {
return I18n::get(translations);
}

// I18n helper (with index)
template <size_t X, size_t Y>
static const char *i18n(const char *const (&translations)[X][Y], size_t index) {
return I18n::get(translations, index);
}

#endif // I18N_H
133 changes: 133 additions & 0 deletions firmware/src/core/i18n/Translations.cpp
Original file line number Diff line number Diff line change
@@ -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 <a href='https://timezonedb.com/time-zones' target='blank'>this list</a>", // EN
"Zeitzone, verwenden Sie eine aus <a href='https://timezonedb.com/time-zones' target='blank'>dieser Liste</a>", // DE
"Fuseau horaire, utilisez-en un de <a href='https://timezonedb.com/time-zones' target='blank'>cette liste</a>" // 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-Drehung", // DE
"Rotation des Orbes" // FR
};

constexpr const char *const t_orbRot[4][LANG_NUM] = {
{
"No rotation", // EN
"Keine Drehung", // DE
"Pas de rotation" // FR
},
{
"Rotate 90°", // EN
"90° gedreht", // DE
"Tourné de 90°" // FR
},
{
"Rotate 180°", // EN
"180° gedreht", // DE
"Tourné de 180°" // FR
},
{
"Rotate 270°", // EN
"270° gedreht", // 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
};
30 changes: 30 additions & 0 deletions firmware/src/core/i18n/Translations.h
Original file line number Diff line number Diff line change
@@ -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
41 changes: 24 additions & 17 deletions firmware/src/core/utils/MainHelper.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "MainHelper.h"
#include "LittleFSHelper.h"
#include "Translations.h"
#include "config_helper.h"
#include "icons.h"
#include <ArduinoLog.h>
Expand All @@ -23,6 +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 = DEFAULT_LANGUAGE;

void MainHelper::init(WiFiManager *wm, ConfigManager *cm, ScreenManager *sm, WidgetSet *ws) {
s_wifiManager = wm;
Expand Down Expand Up @@ -60,19 +62,22 @@ void MainHelper::setupButtons() {
}

void MainHelper::setupConfig() {
s_configManager->addConfigString("General", "timezoneLoc", &s_timezoneLocation, 30, "Timezone Location, use one from <a href='https://timezonedb.com/time-zones' target='blank'>this list</a>");
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);
// 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(t_timezoneLoc));
String *optLang = I18n::getAllLanguages();
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, "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);
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) {
Expand Down Expand Up @@ -419,7 +424,7 @@ void MainHelper::showWelcome() {
s_screenManager->setFontColor(TFT_WHITE);

s_screenManager->selectScreen(0);
s_screenManager->drawCentreString("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);
Expand All @@ -428,11 +433,13 @@ 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(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("version: 1.1.0", ScreenCenterX, ScreenCenterY + 65, 15);
// VERSION is defined in MainHelper.h
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);
Expand Down Expand Up @@ -503,4 +510,4 @@ void MainHelper::watchdogInit() {

void MainHelper::watchdogReset() {
esp_task_wdt_reset();
}
}
2 changes: 2 additions & 0 deletions firmware/src/core/utils/MainHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include "git_info.h"
#include <Arduino.h>

#define VERSION "1.2beta"

// Set defaults if not set in config.h
#ifndef TFT_BRIGHTNESS
#define TFT_BRIGHTNESS 255
Expand Down
1 change: 1 addition & 0 deletions firmware/src/core/widget/Widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "Button.h"
#include "ConfigManager.h"
#include "ScreenManager.h"
#include "Translations.h" // include for use by all Widgets
#include "config_helper.h"

class Widget {
Expand Down
9 changes: 5 additions & 4 deletions firmware/src/core/widget/WidgetSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,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()
Expand All @@ -82,11 +83,11 @@ void WidgetSet::showCenteredLine(int screen, const String &text) {
}

void WidgetSet::showLoading() {
showCenteredLine(3, "Loading data:");
showCenteredLine(3, I18n::get(t_loadingData));
}

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());
Expand Down
4 changes: 2 additions & 2 deletions firmware/src/core/widget/WidgetSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,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;

Expand Down
Loading