Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
7 changes: 7 additions & 0 deletions src/display/core/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Settings::Settings() {
sunriseExtBrightness = preferences.getInt("sr_exb", 255);
emptyTankDistance = preferences.getInt("sr_ed", 200);
fullTankDistance = preferences.getInt("sr_fd", 50);
language = preferences.getInt("lang", DEFAULT_LANGUAGE);

preferences.end();

Expand Down Expand Up @@ -379,6 +380,11 @@ void Settings::setFullTankDistance(int full_tank_distance) {
save();
}

void Settings::setLanguage(int language) {
this->language = language;
save();
}

void Settings::doSave() {
if (!dirty) {
return;
Expand Down Expand Up @@ -448,6 +454,7 @@ void Settings::doSave() {
preferences.putInt("sr_exb", sunriseExtBrightness);
preferences.putInt("sr_ed", emptyTankDistance);
preferences.putInt("sr_fd", fullTankDistance);
preferences.putInt("lang", language);

preferences.end();
}
Expand Down
3 changes: 3 additions & 0 deletions src/display/core/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class Settings {
int getSunriseExtBrightness() const { return sunriseExtBrightness; }
int getEmptyTankDistance() const { return emptyTankDistance; }
int getFullTankDistance() const { return fullTankDistance; }
int getLanguage() const { return language; }
void setLanguage(int language);
void setTargetBrewTemp(int target_brew_temp);
void setTargetSteamTemp(int target_steam_temp);
void setTargetWaterTemp(int target_water_temp);
Expand Down Expand Up @@ -205,6 +207,7 @@ class Settings {
int sunriseExtBrightness = 255;
int emptyTankDistance = 200;
int fullTankDistance = 50;
int language = DEFAULT_LANGUAGE;

void doSave();
xTaskHandle taskHandle;
Expand Down
140 changes: 140 additions & 0 deletions src/display/core/Translation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#include "Translation.h"
#include <stdarg.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Include cstdio for vsnprintf declaration

vsnprintf is declared in /<stdio.h>. Without it, some toolchains will error.

 #include <stdarg.h>
+#include <cstdio>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <stdarg.h>
#include <stdarg.h>
#include <cstdio>
🤖 Prompt for AI Agents
In src/display/core/Translation.cpp around line 3, add the standard C header
that declares vsnprintf (e.g., include <cstdio> or <stdio.h>) because the
current file only includes <stdarg.h> and some toolchains require the proper
declaration for vsnprintf; update the includes to #include <cstdio> (or #include
<stdio.h>) alongside <stdarg.h> so the vsnprintf prototype is available.


Language Translation::currentLanguage = Language::ENGLISH;

void Translation::setLanguage(Language lang) {
currentLanguage = lang;
}

Language Translation::getLanguage() {
return currentLanguage;
}

const char* Translation::get(TranslationKey key) {
const char *text = nullptr;
switch (currentLanguage) {
case Language::GERMAN:
text = getGerman(key);
break;
case Language::FRENCH:
text = getFrench(key);
break;
case Language::SPANISH:
text = getSpanish(key);
break;
case Language::ENGLISH:
default:
text = getEnglish(key);
break;
}
if (text == nullptr || text[0] == '\0') {
return getEnglish(key);
}
return text;
}

String Translation::format(const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
return String(buffer);
}

const char* Translation::getEnglish(TranslationKey key) {
switch (key) {
case TranslationKey::BREW: return "Brew";
case TranslationKey::STEAM: return "Steam";
case TranslationKey::WATER: return "Water";
case TranslationKey::GRIND: return "Grind";
case TranslationKey::SELECT_PROFILE: return "Select profile";
case TranslationKey::STARTING: return "Starting...";
case TranslationKey::UPDATING: return "Updating...";
case TranslationKey::TEMPERATURE_ERROR: return "Temperature error, please restart";
case TranslationKey::AUTOTUNING: return "Autotuning...";
case TranslationKey::FINISHED: return "Finished";
case TranslationKey::INFUSION: return "INFUSION";
case TranslationKey::BREW_PHASE: return "BREW";
case TranslationKey::STEPS: return "Steps";
case TranslationKey::PHASES: return "Phases";
case TranslationKey::STEP: return "step";
case TranslationKey::PHASE: return "phase";
case TranslationKey::SELECTED_PROFILE: return "Selected profile";
case TranslationKey::RESTART_REQUIRED: return "Restart required";
default: return "";
}
}

const char* Translation::getGerman(TranslationKey key) {
switch (key) {
case TranslationKey::BREW: return "Kaffee";
case TranslationKey::STEAM: return "Dampf";
case TranslationKey::WATER: return "Wasser";
case TranslationKey::GRIND: return "Mahlen";
case TranslationKey::SELECT_PROFILE: return "Profil auswählen";
case TranslationKey::STARTING: return "Starten...";
case TranslationKey::UPDATING: return "Aktualisieren...";
case TranslationKey::TEMPERATURE_ERROR: return "Temperaturfehler, bitte neu starten";
case TranslationKey::AUTOTUNING: return "Autotune...";
case TranslationKey::FINISHED: return "Fertig";
case TranslationKey::INFUSION: return "INFUSION";
case TranslationKey::BREW_PHASE: return "BEZUG";
case TranslationKey::STEPS: return "Schritte";
case TranslationKey::PHASES: return "Phasen";
case TranslationKey::STEP: return "Schritt";
case TranslationKey::PHASE: return "Phase";
case TranslationKey::SELECTED_PROFILE: return "Gewähltes Profil";
case TranslationKey::RESTART_REQUIRED: return "Neustart benötigt";
default: return "";
}
}

const char* Translation::getFrench(TranslationKey key) {
switch (key) {
case TranslationKey::BREW: return "Brew";
case TranslationKey::STEAM: return "Steam";
case TranslationKey::WATER: return "Water";
case TranslationKey::GRIND: return "Grind";
case TranslationKey::SELECT_PROFILE: return "Select profile";
case TranslationKey::STARTING: return "Starting...";
case TranslationKey::UPDATING: return "Updating...";
case TranslationKey::TEMPERATURE_ERROR: return "Temperature error, please restart";
case TranslationKey::AUTOTUNING: return "Autotuning...";
case TranslationKey::FINISHED: return "Finished";
case TranslationKey::INFUSION: return "INFUSION";
case TranslationKey::BREW_PHASE: return "BREW";
case TranslationKey::STEPS: return "Steps";
case TranslationKey::PHASES: return "Phases";
case TranslationKey::STEP: return "step";
case TranslationKey::PHASE: return "phase";
case TranslationKey::SELECTED_PROFILE: return "Selected profile";
case TranslationKey::RESTART_REQUIRED: return "Restart required";
default: return "";
}
}

const char* Translation::getSpanish(TranslationKey key) {
switch (key) {
case TranslationKey::BREW: return "Brew";
case TranslationKey::STEAM: return "Steam";
case TranslationKey::WATER: return "Water";
case TranslationKey::GRIND: return "Grind";
case TranslationKey::SELECT_PROFILE: return "Select profile";
case TranslationKey::STARTING: return "Starting...";
case TranslationKey::UPDATING: return "Updating...";
case TranslationKey::TEMPERATURE_ERROR: return "Temperature error, please restart";
case TranslationKey::AUTOTUNING: return "Autotuning...";
case TranslationKey::FINISHED: return "Finished";
case TranslationKey::INFUSION: return "INFUSION";
case TranslationKey::BREW_PHASE: return "BREW";
case TranslationKey::STEPS: return "Steps";
case TranslationKey::PHASES: return "Phases";
case TranslationKey::STEP: return "step";
case TranslationKey::PHASE: return "phase";
case TranslationKey::SELECTED_PROFILE: return "Selected profile";
case TranslationKey::RESTART_REQUIRED: return "Restart required";
default: return "";
}
}
51 changes: 51 additions & 0 deletions src/display/core/Translation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef TRANSLATION_H
#define TRANSLATION_H

#include <Arduino.h>

enum class Language {
ENGLISH,
GERMAN,
FRENCH,
SPANISH
};

enum class TranslationKey {
BREW,
STEAM,
WATER,
GRIND,
SELECT_PROFILE,
STARTING,
UPDATING,
TEMPERATURE_ERROR,
AUTOTUNING,
FINISHED,
INFUSION,
BREW_PHASE,
STEPS,
PHASES,
STEP,
PHASE,
SELECTED_PROFILE,
RESTART_REQUIRED
};

class Translation {
public:
static void setLanguage(Language lang);
static Language getLanguage();
static const char* get(TranslationKey key);
static String format(const char* format, ...);

private:
static Language currentLanguage;
static const char* getEnglish(TranslationKey key);
static const char* getGerman(TranslationKey key);
static const char* getFrench(TranslationKey key);
static const char* getSpanish(TranslationKey key);
};

#define TR(key) Translation::get(TranslationKey::key)

#endif // TRANSLATION_H
2 changes: 1 addition & 1 deletion src/display/core/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#define DEFAULT_TIMEZONE "Europe/Rome"
#define DEFAULT_STEAM_PUMP_PERCENTAGE 4.f
#define WIFI_CONNECT_ATTEMPTS 20

#define DEFAULT_LANGUAGE 0
#define MODE_STANDBY 0
#define MODE_BREW 1
#define MODE_STEAM 2
Expand Down
51 changes: 51 additions & 0 deletions src/display/plugins/LanguagePlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "LanguagePlugin.h"
#include <display/core/Translation.h>

LanguagePlugin::LanguagePlugin(Controller *controller) : Plugin(controller) {
controller->getPluginManager()->registerPlugin(this);
}

void LanguagePlugin::init() {
controller->getPluginManager()->on("language:change", [this](Event const &event) {
int language = event.getInt("language");
setLanguage(language);
});
}

void LanguagePlugin::setLanguage(int language) {
Translation::setLanguage(static_cast<Language>(language));
controller->getSettings().setLanguage(language);
controller->getPluginManager()->emit("ui:refresh", {});
}
Comment on lines +15 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate and short-circuit language changes to prevent invalid state and unnecessary writes

Currently, any integer is cast to Language and persisted. Add range checking and skip if unchanged.

 void LanguagePlugin::setLanguage(int language) {
-    Translation::setLanguage(static_cast<Language>(language));
-    controller->getSettings().setLanguage(language);
-    controller->getPluginManager()->emit("ui:refresh", {});
+    auto newLang = static_cast<Language>(language);
+    // Clamp to known enum values
+    if (newLang != Language::ENGLISH && newLang != Language::GERMAN &&
+        newLang != Language::FRENCH && newLang != Language::SPANISH) {
+        // Ignore invalid values
+        return;
+    }
+    if (Translation::getLanguage() == newLang) {
+        return; // no-op if same language
+    }
+    Translation::setLanguage(newLang);
+    controller->getSettings().setLanguage(static_cast<int>(newLang));
+    controller->getPluginManager()->emit("ui:refresh", {});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void LanguagePlugin::setLanguage(int language) {
Translation::setLanguage(static_cast<Language>(language));
controller->getSettings().setLanguage(language);
controller->getPluginManager()->emit("ui:refresh", {});
}
void LanguagePlugin::setLanguage(int language) {
auto newLang = static_cast<Language>(language);
// Clamp to known enum values
if (newLang != Language::ENGLISH && newLang != Language::GERMAN &&
newLang != Language::FRENCH && newLang != Language::SPANISH) {
// Ignore invalid values
return;
}
if (Translation::getLanguage() == newLang) {
return; // no-op if same language
}
Translation::setLanguage(newLang);
controller->getSettings().setLanguage(static_cast<int>(newLang));
controller->getPluginManager()->emit("ui:refresh", {});
}
🤖 Prompt for AI Agents
In src/display/plugins/LanguagePlugin.cpp around lines 15-19, add validation and
a short-circuit so only valid enum values are applied and persisted: first check
that the passed int is within the Language enum range (reject out-of-range
values), then if the target Language equals the current language (from
Translation::getLanguage() or controller->getSettings().getLanguage()) return
early; otherwise call Translation::setLanguage(...) and persist via
controller->getSettings().setLanguage(...) and emit "ui:refresh" — perform the
checks before mutating state to avoid invalid state and unnecessary writes.


const char* LanguagePlugin::getName() const {
return "Language";
}

const char* LanguagePlugin::getDescription() const {
return "Language settings for the display";
}

bool LanguagePlugin::isEnabled() const {
return true;
}

void LanguagePlugin::setEnabled(bool enabled) {
}

String LanguagePlugin::getConfig() const {
return "{\"language\":" + String(controller->getSettings().getLanguage()) + "}";
}

void LanguagePlugin::setConfig(const String &config) {
if (config.indexOf("\"language\":") != -1) {
int start = config.indexOf("\"language\":") + 11;
int end = config.indexOf(",", start);
if (end == -1) end = config.indexOf("}", start);
if (end != -1) {
String langStr = config.substring(start, end);
int language = langStr.toInt();
setLanguage(language);
}
}
}
Comment on lines +40 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make config parsing tolerant to whitespace and validate bounds

Parsing by fixed offset fails with spaces and will silently accept invalid values. Trim and bound-check before applying.

 void LanguagePlugin::setConfig(const String &config) {
-    if (config.indexOf("\"language\":") != -1) {
-        int start = config.indexOf("\"language\":") + 11;
-        int end = config.indexOf(",", start);
-        if (end == -1) end = config.indexOf("}", start);
-        if (end != -1) {
-            String langStr = config.substring(start, end);
-            int language = langStr.toInt();
-            setLanguage(language);
-        }
-    }
+    int keyPos = config.indexOf("\"language\"");
+    if (keyPos == -1) return;
+    int colon = config.indexOf(":", keyPos);
+    if (colon == -1) return;
+    int end = config.indexOf(",", colon);
+    if (end == -1) end = config.indexOf("}", colon);
+    if (end == -1) return;
+    String langStr = config.substring(colon + 1, end);
+    langStr.trim();
+    if (langStr.length() == 0) return;
+    int language = langStr.toInt(); // tolerates leading '+'/'-' and spaces
+    setLanguage(language);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void LanguagePlugin::setConfig(const String &config) {
if (config.indexOf("\"language\":") != -1) {
int start = config.indexOf("\"language\":") + 11;
int end = config.indexOf(",", start);
if (end == -1) end = config.indexOf("}", start);
if (end != -1) {
String langStr = config.substring(start, end);
int language = langStr.toInt();
setLanguage(language);
}
}
}
void LanguagePlugin::setConfig(const String &config) {
int keyPos = config.indexOf("\"language\"");
if (keyPos == -1) return;
int colon = config.indexOf(":", keyPos);
if (colon == -1) return;
int end = config.indexOf(",", colon);
if (end == -1) end = config.indexOf("}", colon);
if (end == -1) return;
String langStr = config.substring(colon + 1, end);
langStr.trim();
if (langStr.length() == 0) return;
int language = langStr.toInt(); // tolerates leading '+'/'-' and spaces
setLanguage(language);
}
🤖 Prompt for AI Agents
In src/display/plugins/LanguagePlugin.cpp around lines 40 to 51, the config
parsing uses a fixed offset after "\"language\":" which breaks when there is
whitespace and then silently accepts invalid values; change the logic to locate
the "\"language\"" token, find the next ':' position dynamically, set start =
colonIndex + 1, extract substring from start to the following ',' or '}', then
trim surrounding whitespace and optional quotes from that substring, parse it to
an integer only after trimming, and validate the parsed value against the
allowed language bounds (e.g. non-negative and <= your MAX_LANGUAGE or check
against the Language enum range) before calling setLanguage; if parsing fails or
the value is out of bounds, do not call setLanguage and handle the error (log or
ignore) accordingly.

22 changes: 22 additions & 0 deletions src/display/plugins/LanguagePlugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef LANGUAGE_PLUGIN_H
#define LANGUAGE_PLUGIN_H

#include <display/core/Plugin.h>

class LanguagePlugin : public Plugin {
public:
explicit LanguagePlugin(Controller *controller);

void init() override;
const char* getName() const override;
const char* getDescription() const override;
bool isEnabled() const override;
void setEnabled(bool enabled) override;
String getConfig() const override;
void setConfig(const String &config) override;

private:
void setLanguage(int language);
};

#endif // LANGUAGE_PLUGIN_H
Loading
Loading