refactor: Move language setting into JSON settings (#1796)

## Summary

Another follow up to #1408. That PR revised the language.bin settings
file to version 2. After merging, it occurred to me that it would make
more sense to have the language inside the overall settings.json file,
easy for users to see and change directly.

This change adds migration from language.bin to settings.json. It only
migrates from the pre-#1408 v1 language.bin format, because v2 has only
been in for a couple of commits and I don't think we need long-term
maintenance code to handle it.

#1408 had a potential long-term maintenance issue for migration. It
assumed that the enum order of languages never changes, only grows. So,
e.g. "Portuguese (Portugal)" could never be added next to "Portuguese
(Brazil)", only appended. This change includes a hardcoded frozen
ordering of the language indices as they existed at 2f969a9, and
migrates to storing a language code string in the settings.json instead
for future reordering flexibility.

Similar to the settings.bin to settings.json migration, which renames
settings.bin to settings.bin.bak, this change renames language.bin to
language.bin.bak after adding the language setting to settings.json.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
This commit is contained in:
Zach Nelson
2026-05-01 09:39:28 -05:00
committed by GitHub
parent ae865f6d08
commit b25389b43e
8 changed files with 75 additions and 77 deletions
+30 -1
View File
@@ -8,6 +8,7 @@
#include <cstring>
#include <string>
#include "I18nKeys.h"
#include "fontIds.h"
// Initialize the static instance
@@ -26,6 +27,8 @@ constexpr uint8_t SETTINGS_FILE_VERSION = 1;
constexpr char SETTINGS_FILE_BIN[] = "/.crosspoint/settings.bin";
constexpr char SETTINGS_FILE_JSON[] = "/.crosspoint/settings.json";
constexpr char SETTINGS_FILE_BAK[] = "/.crosspoint/settings.bin.bak";
constexpr char LANG_FILE_BIN[] = "/.crosspoint/language.bin";
constexpr char LANG_FILE_BAK[] = "/.crosspoint/language.bin.bak";
// Convert legacy front button layout into explicit logical->hardware mapping.
void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
@@ -95,6 +98,7 @@ bool CrossPointSettings::loadFromFile() {
LOG_ERR("CPS", "Failed to resave settings after format update");
}
}
migrateLanguageBinaryFile();
return result;
}
}
@@ -102,6 +106,7 @@ bool CrossPointSettings::loadFromFile() {
// Fall back to binary migration
if (Storage.exists(SETTINGS_FILE_BIN)) {
if (loadFromBinaryFile()) {
migrateLanguageBinaryFile();
if (saveToFile()) {
Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK);
LOG_DBG("CPS", "Migrated settings.bin to settings.json");
@@ -113,7 +118,31 @@ bool CrossPointSettings::loadFromFile() {
}
}
return false;
// No settings files at all -- check for standalone language.bin
return migrateLanguageBinaryFile();
}
bool CrossPointSettings::migrateLanguageBinaryFile() {
// V1_LANGUAGES / V1_LANGUAGE_COUNT are emitted by gen_i18n.py with the
// frozen enum order from 2f969a9.
if (!Storage.exists(LANG_FILE_BIN)) return false;
FsFile f;
if (Storage.openFileForRead("CPS", LANG_FILE_BIN, f)) {
uint8_t version;
serialization::readPod(f, version);
if (version == 1) {
uint8_t oldIndex;
serialization::readPod(f, oldIndex);
if (oldIndex < V1_LANGUAGE_COUNT) {
language = static_cast<uint8_t>(V1_LANGUAGES[oldIndex]);
}
}
}
Storage.rename(LANG_FILE_BIN, LANG_FILE_BAK);
saveToFile();
LOG_DBG("CPS", "Migrated language.bin into settings.json");
return true;
}
bool CrossPointSettings::loadFromBinaryFile() {
+3
View File
@@ -203,6 +203,8 @@ class CrossPointSettings {
uint8_t imageRendering = IMAGES_DISPLAY;
// Tilt-based page turning (X3 only — requires QMI8658 IMU)
uint8_t tiltPageTurn = TILT_OFF;
// Language setting (Language enum index, default 0 = EN)
uint8_t language = 0;
~CrossPointSettings() = default;
@@ -224,6 +226,7 @@ class CrossPointSettings {
private:
bool loadFromBinaryFile();
bool migrateLanguageBinaryFile();
public:
float getReaderLineCompression() const;
+9
View File
@@ -141,6 +141,10 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
doc["frontButtonLeft"] = s.frontButtonLeft;
doc["frontButtonRight"] = s.frontButtonRight;
// Language -- managed by LanguageSelectActivity, not in SettingsList.
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN";
String json;
serializeJson(doc, json);
return Storage.writeFile(path, json);
@@ -220,6 +224,11 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
CrossPointSettings::validateFrontButtonMapping(s);
// Language -- stored as code string for stability across enum reorders.
if (doc["language"].is<const char*>()) {
s.language = static_cast<uint8_t>(I18n::languageFromCode(doc["language"].as<const char*>()));
}
LOG_DBG("CPS", "Settings loaded from file");
return true;
@@ -6,6 +6,7 @@
#include <algorithm>
#include <iterator>
#include "CrossPointSettings.h"
#include "I18nKeys.h"
#include "MappedInputManager.h"
#include "fontIds.h"
@@ -49,11 +50,16 @@ void LanguageSelectActivity::loop() {
}
void LanguageSelectActivity::handleSelection() {
const uint8_t langIndex = SORTED_LANGUAGE_INDICES[selectedIndex];
{
RenderLock lock(*this);
I18N.setLanguage(static_cast<Language>(SORTED_LANGUAGE_INDICES[selectedIndex]));
I18N.setLanguage(static_cast<Language>(langIndex));
}
SETTINGS.language = langIndex;
SETTINGS.saveToFile();
// Return to previous page
onBack();
}
+1 -1
View File
@@ -259,7 +259,7 @@ void setup() {
HalSystem::checkPanic();
SETTINGS.loadFromFile();
I18N.loadSettings();
I18N.setLanguage(static_cast<Language>(SETTINGS.language));
KOREADER_STORE.loadFromFile();
OPDS_STORE.loadFromFile();
UITheme::getInstance().reload();