Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@ When building the firmware, the scripts/build_spiffs.sh script installs the web
## Reporting Issues

If you encounter a problem, open an issue describing the steps to reproduce and any relevant logs.

## Adding Translations

### Controller

### WebUI
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
56 changes: 56 additions & 0 deletions src/display/core/Translation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "Translation.h"
#include "TranslationStrings.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) {
int index = static_cast<int>(key);
if (index < 0 || index >= static_cast<int>(TranslationStrings::NUM_KEYS)) {
return "Unknown";
}

const char* const* strings = nullptr;
switch (currentLanguage) {
case Language::GERMAN:
strings = TranslationStrings::GERMAN;
break;
case Language::FRENCH:
strings = TranslationStrings::FRENCH;
break;
case Language::SPANISH:
strings = TranslationStrings::SPANISH;
break;
case Language::ENGLISH:
default:
strings = TranslationStrings::ENGLISH;
break;
}

const char* text = strings[index];
if (text != nullptr && text[0] != '\0') {
return text;
}

// Fallback to English
return TranslationStrings::ENGLISH[index];
}

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);
}


47 changes: 47 additions & 0 deletions src/display/core/Translation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#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;
};

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

#endif // TRANSLATION_H
91 changes: 91 additions & 0 deletions src/display/core/TranslationStrings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include "TranslationStrings.h"

namespace TranslationStrings {
// English
const char* const ENGLISH[] = {
"Brew", // BREW
"Steam", // STEAM
"Water", // WATER
"Grind", // GRIND
"Select profile", // SELECT_PROFILE
"Starting...", // STARTING
"Updating...", // UPDATING
"Temperature error, please restart", // TEMPERATURE_ERROR
"Autotuning...", // AUTOTUNING
"Finished", // FINISHED
"INFUSION", // INFUSION
"BREW", // BREW_PHASE
"Steps", // STEPS
"Phases", // PHASES
"step", // STEP
"phase", // PHASE
"Selected profile", // SELECTED_PROFILE
"Restart required" // RESTART_REQUIRED
};

// German
const char* const GERMAN[] = {
"Kaffee", // BREW
"Dampf", // STEAM
"Wasser", // WATER
"Mahlen", // GRIND
"Profil auswählen", // SELECT_PROFILE
"Starten...", // STARTING
"Aktualisieren...", // UPDATING
"Temperaturfehler, bitte neu starten", // TEMPERATURE_ERROR
"Autotune...", // AUTOTUNING
"Fertig", // FINISHED
"INFUSION", // INFUSION
"BEZUG", // BREW_PHASE
"Schritte", // STEPS
"Phasen", // PHASES
"Schritt", // STEP
"Phase", // PHASE
"Gewähltes Profil", // SELECTED_PROFILE
"Neustart benötigt" // RESTART_REQUIRED
};

// French
const char* const FRENCH[] = {
"Brew", // BREW
"Steam", // STEAM
"Water", // WATER
"Grind", // GRIND
"Select profile", // SELECT_PROFILE
"Starting...", // STARTING
"Updating...", // UPDATING
"Temperature error, please restart", // TEMPERATURE_ERROR
"Autotuning...", // AUTOTUNING
"Finished", // FINISHED
"INFUSION", // INFUSION
"BREW", // BREW_PHASE
"Steps", // STEPS
"Phases", // PHASES
"step", // STEP
"phase", // PHASE
"Selected profile", // SELECTED_PROFILE
"Restart required" // RESTART_REQUIRED
};

// Spanish
const char* const SPANISH[] = {
"Elaborar", // BREW
"Vapor", // STEAM
"Agua", // WATER
"Moler", // GRIND
"Seleccionar perfil", // SELECT_PROFILE
"Iniciando...", // STARTING
"Actualizando...", // UPDATING
"Error de temperatura, favor de reiniciar", // TEMPERATURE_ERROR
"Autocalibrando...", // AUTOTUNING
"Terminado", // FINISHED
"INFUSIÓN", // INFUSION
"ELABORAR", // BREW_PHASE
"Pasos", // STEPS
"Fases", // PHASES
"paso", // STEP
"fase", // PHASE
"Perfil seleccionado", // SELECTED_PROFILE
"Reinicio requerido" // RESTART_REQUIRED
};
}
15 changes: 15 additions & 0 deletions src/display/core/TranslationStrings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef TRANSLATION_STRINGS_H
#define TRANSLATION_STRINGS_H

enum class TranslationKey;

namespace TranslationStrings {
extern const char* const ENGLISH[];
extern const char* const GERMAN[];
extern const char* const FRENCH[];
extern const char* const SPANISH[];

static constexpr size_t NUM_KEYS = 18;
}

#endif // TRANSLATION_STRINGS_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.

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

class Controller;
class PluginManager;

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

void setup(Controller* controller, PluginManager* pluginManager) override;
void loop() override;

private:
void setLanguage(int language);
};

#endif // LANGUAGE_PLUGIN_H
Loading
Loading