Add API endpoint to retrieve custom languages and complete language pack
This commit is contained in:
parent
8257eb7aa2
commit
e29b86e4dc
27
include/I18n.h
Normal file
27
include/I18n.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <WString.h>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
struct LanguageInfo_t {
|
||||||
|
String code;
|
||||||
|
String name;
|
||||||
|
String filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
class I18nClass {
|
||||||
|
public:
|
||||||
|
I18nClass();
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
std::list<LanguageInfo_t> getAvailableLanguages();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void readLangPacks();
|
||||||
|
void readConfig(String file);
|
||||||
|
|
||||||
|
std::list<LanguageInfo_t> _availLanguages;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern I18nClass I18n;
|
||||||
@ -11,4 +11,5 @@ public:
|
|||||||
static int getTimezoneOffset();
|
static int getTimezoneOffset();
|
||||||
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
|
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
|
||||||
static void removeAllFiles();
|
static void removeAllFiles();
|
||||||
|
static String generateMd5FromFile(String file);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include "WebApi_file.h"
|
#include "WebApi_file.h"
|
||||||
#include "WebApi_firmware.h"
|
#include "WebApi_firmware.h"
|
||||||
#include "WebApi_gridprofile.h"
|
#include "WebApi_gridprofile.h"
|
||||||
|
#include "WebApi_i18n.h"
|
||||||
#include "WebApi_inverter.h"
|
#include "WebApi_inverter.h"
|
||||||
#include "WebApi_limit.h"
|
#include "WebApi_limit.h"
|
||||||
#include "WebApi_maintenance.h"
|
#include "WebApi_maintenance.h"
|
||||||
@ -53,6 +54,7 @@ private:
|
|||||||
WebApiFileClass _webApiFile;
|
WebApiFileClass _webApiFile;
|
||||||
WebApiFirmwareClass _webApiFirmware;
|
WebApiFirmwareClass _webApiFirmware;
|
||||||
WebApiGridProfileClass _webApiGridprofile;
|
WebApiGridProfileClass _webApiGridprofile;
|
||||||
|
WebApiI18nClass _webApiI18n;
|
||||||
WebApiInverterClass _webApiInverter;
|
WebApiInverterClass _webApiInverter;
|
||||||
WebApiLimitClass _webApiLimit;
|
WebApiLimitClass _webApiLimit;
|
||||||
WebApiMaintenanceClass _webApiMaintenance;
|
WebApiMaintenanceClass _webApiMaintenance;
|
||||||
|
|||||||
14
include/WebApi_i18n.h
Normal file
14
include/WebApi_i18n.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class WebApiI18nClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onI18nLanguages(AsyncWebServerRequest* request);
|
||||||
|
void onI18nLanguage(AsyncWebServerRequest* request);
|
||||||
|
};
|
||||||
@ -108,3 +108,5 @@
|
|||||||
#define LED_BRIGHTNESS 100U
|
#define LED_BRIGHTNESS 100U
|
||||||
|
|
||||||
#define MAX_INVERTER_LIMIT 2250
|
#define MAX_INVERTER_LIMIT 2250
|
||||||
|
|
||||||
|
#define LANG_PACK_SUFFIX ".lang.json"
|
||||||
|
|||||||
72
src/I18n.cpp
Normal file
72
src/I18n.cpp
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "I18n.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include "defaults.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
I18nClass I18n;
|
||||||
|
|
||||||
|
I18nClass::I18nClass()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18nClass::init(Scheduler& scheduler)
|
||||||
|
{
|
||||||
|
readLangPacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::list<LanguageInfo_t> I18nClass::getAvailableLanguages()
|
||||||
|
{
|
||||||
|
return _availLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18nClass::readLangPacks()
|
||||||
|
{
|
||||||
|
auto root = LittleFS.open("/");
|
||||||
|
auto file = root.getNextFileName();
|
||||||
|
|
||||||
|
while (file != "") {
|
||||||
|
if (file.endsWith(LANG_PACK_SUFFIX)) {
|
||||||
|
MessageOutput.printf("Read File %s\r\n", file.c_str());
|
||||||
|
readConfig(file);
|
||||||
|
}
|
||||||
|
file = root.getNextFileName();
|
||||||
|
}
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18nClass::readConfig(String file)
|
||||||
|
{
|
||||||
|
JsonDocument filter;
|
||||||
|
filter["meta"] = true;
|
||||||
|
|
||||||
|
File f = LittleFS.open(file, "r", false);
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
|
||||||
|
// Deserialize the JSON document
|
||||||
|
const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter));
|
||||||
|
if (error) {
|
||||||
|
MessageOutput.printf("Failed to read file %s\r\n", file.c_str());
|
||||||
|
f.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LanguageInfo_t lang;
|
||||||
|
lang.code = String(doc["meta"]["code"]);
|
||||||
|
lang.name = String(doc["meta"]["name"]);
|
||||||
|
lang.filename = file;
|
||||||
|
|
||||||
|
_availLanguages.push_back(lang);
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@
|
|||||||
#include "MessageOutput.h"
|
#include "MessageOutput.h"
|
||||||
#include "PinMapping.h"
|
#include "PinMapping.h"
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
#include <MD5Builder.h>
|
||||||
|
|
||||||
uint32_t Utils::getChipId()
|
uint32_t Utils::getChipId()
|
||||||
{
|
{
|
||||||
@ -80,3 +81,34 @@ void Utils::removeAllFiles()
|
|||||||
file = root.getNextFileName();
|
file = root.getNextFileName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String Utils::generateMd5FromFile(String file)
|
||||||
|
{
|
||||||
|
if (!LittleFS.exists(file)) {
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
File f = LittleFS.open(file, "r");
|
||||||
|
if (!file) {
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
MD5Builder md5;
|
||||||
|
md5.begin();
|
||||||
|
|
||||||
|
// Read the file in chunks to avoid using too much memory
|
||||||
|
const size_t bufferSize = 512;
|
||||||
|
uint8_t buffer[bufferSize];
|
||||||
|
|
||||||
|
while (f.available()) {
|
||||||
|
size_t bytesRead = f.read(buffer, bufferSize);
|
||||||
|
md5.add(buffer, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize and calculate the MD5 hash
|
||||||
|
md5.calculate();
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
return md5.toString();
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ void WebApiClass::init(Scheduler& scheduler)
|
|||||||
_webApiFile.init(_server, scheduler);
|
_webApiFile.init(_server, scheduler);
|
||||||
_webApiFirmware.init(_server, scheduler);
|
_webApiFirmware.init(_server, scheduler);
|
||||||
_webApiGridprofile.init(_server, scheduler);
|
_webApiGridprofile.init(_server, scheduler);
|
||||||
|
_webApiI18n.init(_server, scheduler);
|
||||||
_webApiInverter.init(_server, scheduler);
|
_webApiInverter.init(_server, scheduler);
|
||||||
_webApiLimit.init(_server, scheduler);
|
_webApiLimit.init(_server, scheduler);
|
||||||
_webApiMaintenance.init(_server, scheduler);
|
_webApiMaintenance.init(_server, scheduler);
|
||||||
|
|||||||
81
src/WebApi_i18n.cpp
Normal file
81
src/WebApi_i18n.cpp
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "WebApi_i18n.h"
|
||||||
|
#include "I18n.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include "WebApi.h"
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
|
||||||
|
void WebApiI18nClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||||
|
{
|
||||||
|
using std::placeholders::_1;
|
||||||
|
|
||||||
|
server.on("/api/i18n/languages", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguages, this, _1));
|
||||||
|
server.on("/api/i18n/language", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguage, this, _1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiI18nClass::onI18nLanguages(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse(true);
|
||||||
|
auto& root = response->getRoot();
|
||||||
|
const auto& languages = I18n.getAvailableLanguages();
|
||||||
|
|
||||||
|
for (auto& language : languages) {
|
||||||
|
auto jsonLang = root.add<JsonObject>();
|
||||||
|
|
||||||
|
jsonLang["code"] = language.code;
|
||||||
|
jsonLang["name"] = language.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiI18nClass::onI18nLanguage(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
if (request->hasParam("code")) {
|
||||||
|
String code = request->getParam("code")->value();
|
||||||
|
|
||||||
|
const auto& languages = I18n.getAvailableLanguages();
|
||||||
|
auto it = std::find_if(languages.begin(), languages.end(), [code](const LanguageInfo_t& elem) {
|
||||||
|
return elem.code == code;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it != languages.end()) {
|
||||||
|
String md5 = Utils::generateMd5FromFile(it->filename);
|
||||||
|
|
||||||
|
String expectedEtag;
|
||||||
|
expectedEtag = "\"";
|
||||||
|
expectedEtag += md5;
|
||||||
|
expectedEtag += "\"";
|
||||||
|
|
||||||
|
bool eTagMatch = false;
|
||||||
|
if (request->hasHeader("If-None-Match")) {
|
||||||
|
const AsyncWebHeader* h = request->getHeader("If-None-Match");
|
||||||
|
eTagMatch = h->value().equals(expectedEtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin response 200 or 304
|
||||||
|
AsyncWebServerResponse* response;
|
||||||
|
if (eTagMatch) {
|
||||||
|
response = request->beginResponse(304);
|
||||||
|
} else {
|
||||||
|
response = request->beginResponse(LittleFS, it->filename, asyncsrv::T_application_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP requires cache headers in 200 and 304 to be identical
|
||||||
|
response->addHeader("Cache-Control", "public, must-revalidate");
|
||||||
|
response->addHeader("ETag", expectedEtag);
|
||||||
|
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request->send(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "Datastore.h"
|
#include "Datastore.h"
|
||||||
#include "Display_Graphic.h"
|
#include "Display_Graphic.h"
|
||||||
|
#include "I18n.h"
|
||||||
#include "InverterSettings.h"
|
#include "InverterSettings.h"
|
||||||
#include "Led_Single.h"
|
#include "Led_Single.h"
|
||||||
#include "MessageOutput.h"
|
#include "MessageOutput.h"
|
||||||
@ -24,9 +25,9 @@
|
|||||||
#include "defaults.h"
|
#include "defaults.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
#include <SpiManager.h>
|
||||||
#include <TaskScheduler.h>
|
#include <TaskScheduler.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <SpiManager.h>
|
|
||||||
|
|
||||||
#include <driver/uart.h>
|
#include <driver/uart.h>
|
||||||
|
|
||||||
@ -83,6 +84,11 @@ void setup()
|
|||||||
auto& config = Configuration.get();
|
auto& config = Configuration.get();
|
||||||
MessageOutput.println("done");
|
MessageOutput.println("done");
|
||||||
|
|
||||||
|
// Read languate pack
|
||||||
|
MessageOutput.print("Reading language pack... ");
|
||||||
|
I18n.init(scheduler);
|
||||||
|
MessageOutput.println("done");
|
||||||
|
|
||||||
// Load PinMapping
|
// Load PinMapping
|
||||||
MessageOutput.print("Reading PinMapping... ");
|
MessageOutput.print("Reading PinMapping... ");
|
||||||
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
|
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user