From b164e9a95ff16710bab19890c18aba59a7fb3036 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:21:16 +0200 Subject: [PATCH 01/25] Migrate Lingo config to Strata memory policy --- src/Lingo.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Lingo.h b/src/Lingo.h index 0cba137..6af29c0 100644 --- a/src/Lingo.h +++ b/src/Lingo.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -96,10 +98,20 @@ class LingoLanguage { }; struct LingoConfig { + Strata::MemoryPolicy memory{ + .allocation = Strata::Placement::PreferExternal, + .taskStack = Strata::Placement::Internal, + }; LingoLanguage defaultLanguage{}; size_t maxTables = 64; const char *missingTranslation = ""; - bool preferPsram = true; +}; + +struct LingoDiag { + size_t tableCount = 0; + size_t tableCapacity = 0; + Strata::Placement registryPlacement = Strata::Placement::PreferExternal; + Strata::Region registryRegion = Strata::Region::Unknown; }; template class LingoEntry { @@ -251,7 +263,7 @@ class Lingo { size_t tableCount() const; size_t tableCapacity() const; - bool preferPsram() const; + LingoDiag getDiagnostics() const; private: LingoResult addTableRaw( @@ -269,7 +281,7 @@ class Lingo { size_t _tableCount = 0; size_t _tableCapacity = 0; const char *_missingTranslation = ""; - bool _preferPsram = true; + Strata::Placement _registryPlacement = Strata::Placement::PreferExternal; bool _initialized = false; std::atomic _defaultLanguage{0}; }; From 1c86b71497616fe5539d8f0cc5de2fb069f9c8d5 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:21:43 +0200 Subject: [PATCH 02/25] Route Lingo registry ownership through Strata --- src/Lingo.cpp | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Lingo.cpp b/src/Lingo.cpp index 7880c50..bd2a2f5 100644 --- a/src/Lingo.cpp +++ b/src/Lingo.cpp @@ -1,9 +1,7 @@ #include "Lingo.h" -#include "internal/LingoMemory.h" - #include -#include +#include namespace lingo_internal { @@ -34,7 +32,10 @@ LingoResult Lingo::init(const LingoConfig &config) { ); } - if (config.maxTables == 0 || config.missingTranslation == nullptr) { + if ( + config.maxTables == 0 || + config.missingTranslation == nullptr || + !Strata::validMemoryPolicy(config.memory)) { return LingoResult::failure(LingoStatus::InvalidConfig, "invalid lingo configuration"); } @@ -46,24 +47,26 @@ LingoResult Lingo::init(const LingoConfig &config) { ); } - const size_t allocationSize = sizeof(lingo_internal::LingoRegisteredTable) * config.maxTables; - void *memory = lingo_internal::allocate(allocationSize, config.preferPsram); - if (memory == nullptr) { + auto *tables = Strata::allocateArray( + config.maxTables, + config.memory.allocation + ); + if (tables == nullptr) { return LingoResult::failure( LingoStatus::AllocationFailed, "translation table registry allocation failed" ); } - _tables = static_cast(memory); for (size_t index = 0; index < config.maxTables; ++index) { - new (&_tables[index]) lingo_internal::LingoRegisteredTable(); + std::construct_at(&tables[index]); } + _tables = tables; _tableCount = 0; _tableCapacity = config.maxTables; _missingTranslation = config.missingTranslation; - _preferPsram = config.preferPsram; + _registryPlacement = config.memory.allocation; _defaultLanguage.store(config.defaultLanguage.value(), std::memory_order_release); _initialized = true; @@ -76,15 +79,15 @@ LingoResult Lingo::end() { } for (size_t index = 0; index < _tableCapacity; ++index) { - _tables[index].~LingoRegisteredTable(); + std::destroy_at(&_tables[index]); } - lingo_internal::release(_tables); + Strata::free(_tables); _tables = nullptr; _tableCount = 0; _tableCapacity = 0; _missingTranslation = kEmptyTranslation; - _preferPsram = true; + _registryPlacement = Strata::Placement::PreferExternal; _defaultLanguage.store(0, std::memory_order_release); _initialized = false; @@ -218,6 +221,11 @@ size_t Lingo::tableCapacity() const { return _tableCapacity; } -bool Lingo::preferPsram() const { - return _preferPsram; +LingoDiag Lingo::getDiagnostics() const { + return LingoDiag{ + .tableCount = _tableCount, + .tableCapacity = _tableCapacity, + .registryPlacement = _registryPlacement, + .registryRegion = _tables == nullptr ? Strata::Region::Unknown : Strata::regionOf(_tables), + }; } From 231187854ab7a523ed0d46b365956799aff881cc Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:22:32 +0200 Subject: [PATCH 03/25] Update Lingo host tests for Strata diagnostics --- tests/host/lingo_host_tests.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/host/lingo_host_tests.cpp b/tests/host/lingo_host_tests.cpp index 9f6d8b5..b79b3e7 100644 --- a/tests/host/lingo_host_tests.cpp +++ b/tests/host/lingo_host_tests.cpp @@ -76,16 +76,20 @@ void testLifecycle() { config.defaultLanguage = Language::Hu; config.maxTables = 8; config.missingTranslation = ""; - config.preferPsram = true; + config.memory.allocation = Strata::Placement::PreferExternal; assert(lingo.init(config)); assert(lingo.initialized()); assert(lingo.tableCapacity() == 8); - assert(lingo.preferPsram()); + const LingoDiag diag = lingo.getDiagnostics(); + assert(diag.tableCount == 0); + assert(diag.tableCapacity == 8); + assert(diag.registryPlacement == Strata::Placement::PreferExternal); assert(!lingo.init(config)); assert(lingo.end()); assert(!lingo.initialized()); + assert(lingo.getDiagnostics().registryRegion == Strata::Region::Unknown); assert(lingo.end()); assert(lingo.init(config)); @@ -107,6 +111,7 @@ void testRegistrationAndDomainIsolation() { assert(lingo.addTable(Language::En, TIME_EN)); assert(lingo.tableCount() == 6); + assert(lingo.getDiagnostics().tableCount == 6); assert(std::strcmp(lingo.get(CommonKey::Save), "Mentés") == 0); assert(std::strcmp(lingo.get(SoftwareKey::Save), "Mentés") == 0); From ae7fd91ab1acd7fac9c553a3ed9b26d68fb2dbb0 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:22:52 +0200 Subject: [PATCH 04/25] Add Strata memory policy tests --- tests/host/memory_policy_tests.cpp | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/host/memory_policy_tests.cpp diff --git a/tests/host/memory_policy_tests.cpp b/tests/host/memory_policy_tests.cpp new file mode 100644 index 0000000..8e18250 --- /dev/null +++ b/tests/host/memory_policy_tests.cpp @@ -0,0 +1,99 @@ +#include + +#include +#include + +enum class Language : uint16_t { + En, +}; + +void testDefaultPreservesExternalPreference() { + LingoConfig config; + assert(config.memory.allocation == Strata::Placement::PreferExternal); + assert(config.memory.taskStack == Strata::Placement::Internal); + + Lingo lingo; + config.defaultLanguage = Language::En; + assert(lingo.init(config)); + + const LingoDiag diag = lingo.getDiagnostics(); + assert(diag.registryPlacement == Strata::Placement::PreferExternal); + assert(diag.registryRegion == Strata::Region::Unknown); +} + +void testExplicitInternalPlacement() { + Lingo lingo; + LingoConfig config; + config.defaultLanguage = Language::En; + config.memory.allocation = Strata::Placement::Internal; + + assert(lingo.init(config)); + assert(lingo.getDiagnostics().registryPlacement == Strata::Placement::Internal); +} + +void testExplicitBackendDefaultPlacement() { + Lingo lingo; + LingoConfig config; + config.defaultLanguage = Language::En; + config.memory.allocation = Strata::Placement::Default; + + assert(lingo.init(config)); + assert(lingo.getDiagnostics().registryPlacement == Strata::Placement::Default); +} + +void testRequiredExternalFailsWithoutExternalMemory() { + Lingo lingo; + LingoConfig config; + config.defaultLanguage = Language::En; + config.memory.allocation = Strata::Placement::RequireExternal; + + const LingoResult result = lingo.init(config); + assert(!result); + assert(result.status == LingoStatus::AllocationFailed); + assert(!lingo.initialized()); +} + +void testInvalidMemoryPolicyRejected() { + Lingo lingo; + LingoConfig config; + config.defaultLanguage = Language::En; + config.memory.allocation = static_cast(0xff); + + const LingoResult allocationResult = lingo.init(config); + assert(!allocationResult); + assert(allocationResult.status == LingoStatus::InvalidConfig); + + config.memory.allocation = Strata::Placement::PreferExternal; + config.memory.taskStack = static_cast(0xff); + const LingoResult stackResult = lingo.init(config); + assert(!stackResult); + assert(stackResult.status == LingoStatus::InvalidConfig); +} + +void testEndResetsRegistryDiagnostics() { + Lingo lingo; + LingoConfig config; + config.defaultLanguage = Language::En; + config.memory.allocation = Strata::Placement::Internal; + + assert(lingo.init(config)); + assert(lingo.end()); + + const LingoDiag diag = lingo.getDiagnostics(); + assert(diag.tableCount == 0); + assert(diag.tableCapacity == 0); + assert(diag.registryPlacement == Strata::Placement::PreferExternal); + assert(diag.registryRegion == Strata::Region::Unknown); +} + +int main() { + testDefaultPreservesExternalPreference(); + testExplicitInternalPlacement(); + testExplicitBackendDefaultPlacement(); + testRequiredExternalFailsWithoutExternalMemory(); + testInvalidMemoryPolicyRejected(); + testEndResetsRegistryDiagnostics(); + + std::cout << "Lingo Strata memory policy tests passed\n"; + return 0; +} From c6f6af79aa45aa76f162ce5ed42d0dc8c01a3593 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:23:01 +0200 Subject: [PATCH 05/25] Remove bespoke PSRAM allocation tests --- tests/host/psram_allocation_tests.cpp | 77 --------------------------- 1 file changed, 77 deletions(-) delete mode 100644 tests/host/psram_allocation_tests.cpp diff --git a/tests/host/psram_allocation_tests.cpp b/tests/host/psram_allocation_tests.cpp deleted file mode 100644 index c1f645d..0000000 --- a/tests/host/psram_allocation_tests.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include - -#include - -#include -#include - -enum class Language : uint16_t { - En, -}; - -void testPsramPreferredWhenAvailable() { - lingo_psram_test::reset(); - lingo_psram_test::spiramTotal = 1024 * 1024; - - Lingo lingo; - LingoConfig config; - config.defaultLanguage = Language::En; - config.preferPsram = true; - - assert(lingo.init(config)); - assert(lingo_psram_test::spiramAllocations == 1); - assert(lingo_psram_test::internalAllocations == 0); -} - -void testInternalHeapFallbackWhenPsramAllocationFails() { - lingo_psram_test::reset(); - lingo_psram_test::spiramTotal = 1024 * 1024; - lingo_psram_test::failSpiramAllocation = true; - - Lingo lingo; - LingoConfig config; - config.defaultLanguage = Language::En; - config.preferPsram = true; - - assert(lingo.init(config)); - assert(lingo_psram_test::spiramAllocations == 1); - assert(lingo_psram_test::internalAllocations == 1); -} - -void testInternalHeapWhenPsramUnavailable() { - lingo_psram_test::reset(); - lingo_psram_test::spiramTotal = 0; - - Lingo lingo; - LingoConfig config; - config.defaultLanguage = Language::En; - config.preferPsram = true; - - assert(lingo.init(config)); - assert(lingo_psram_test::spiramAllocations == 0); - assert(lingo_psram_test::internalAllocations == 1); -} - -void testInternalHeapWhenPsramPreferenceDisabled() { - lingo_psram_test::reset(); - lingo_psram_test::spiramTotal = 1024 * 1024; - - Lingo lingo; - LingoConfig config; - config.defaultLanguage = Language::En; - config.preferPsram = false; - - assert(lingo.init(config)); - assert(lingo_psram_test::spiramAllocations == 0); - assert(lingo_psram_test::internalAllocations == 1); -} - -int main() { - testPsramPreferredWhenAvailable(); - testInternalHeapFallbackWhenPsramAllocationFails(); - testInternalHeapWhenPsramUnavailable(); - testInternalHeapWhenPsramPreferenceDisabled(); - - std::cout << "Lingo PSRAM allocation tests passed\n"; - return 0; -} From 9de445183074f6a1f4c692761b2721dfe2cb8cd4 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:23:08 +0200 Subject: [PATCH 06/25] Remove Lingo ESP heap allocation stub --- tests/host/esp_stubs/esp_heap_caps.h | 47 ---------------------------- 1 file changed, 47 deletions(-) delete mode 100644 tests/host/esp_stubs/esp_heap_caps.h diff --git a/tests/host/esp_stubs/esp_heap_caps.h b/tests/host/esp_stubs/esp_heap_caps.h deleted file mode 100644 index 32f6d3c..0000000 --- a/tests/host/esp_stubs/esp_heap_caps.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include - -#define MALLOC_CAP_SPIRAM 0x1 -#define MALLOC_CAP_8BIT 0x2 - -namespace lingo_psram_test { - -inline std::size_t spiramTotal = 0; -inline bool failSpiramAllocation = false; -inline std::size_t spiramAllocations = 0; -inline std::size_t internalAllocations = 0; - -inline void reset() { - spiramTotal = 0; - failSpiramAllocation = false; - spiramAllocations = 0; - internalAllocations = 0; -} - -} // namespace lingo_psram_test - -inline std::size_t heap_caps_get_total_size(int caps) { - if ((caps & MALLOC_CAP_SPIRAM) != 0) { - return lingo_psram_test::spiramTotal; - } - return 0; -} - -inline void *heap_caps_malloc(std::size_t size, int caps) { - if ((caps & MALLOC_CAP_SPIRAM) != 0) { - ++lingo_psram_test::spiramAllocations; - if (lingo_psram_test::failSpiramAllocation) { - return nullptr; - } - return std::malloc(size); - } - - ++lingo_psram_test::internalAllocations; - return std::malloc(size); -} - -inline void heap_caps_free(void *ptr) { - std::free(ptr); -} From 4ea9c4974ab2d33c0f2df385fc78807765741c77 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:23:15 +0200 Subject: [PATCH 07/25] Remove bespoke Lingo allocator --- src/internal/LingoMemory.h | 44 -------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/internal/LingoMemory.h diff --git a/src/internal/LingoMemory.h b/src/internal/LingoMemory.h deleted file mode 100644 index 71f3b96..0000000 --- a/src/internal/LingoMemory.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include - -#if defined(ESP32) -#include -#endif - -namespace lingo_internal { - -inline void *allocate(size_t size, bool preferPsram) { - if (size == 0) { - return nullptr; - } - -#if defined(ESP32) - if (preferPsram && heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) { - void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); - if (ptr != nullptr) { - return ptr; - } - } - - return heap_caps_malloc(size, MALLOC_CAP_8BIT); -#else - (void)preferPsram; - return std::malloc(size); -#endif -} - -inline void release(void *ptr) { - if (ptr == nullptr) { - return; - } - -#if defined(ESP32) - heap_caps_free(ptr); -#else - std::free(ptr); -#endif -} - -} // namespace lingo_internal From 4d9aa0fadde134e12f40b49a4a9899f533d53bbd Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:23:52 +0200 Subject: [PATCH 08/25] Add Strata v0.1.2 dependency for Lingo v0.2.0 --- library.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library.json b/library.json index 8022ede..c2a46a9 100644 --- a/library.json +++ b/library.json @@ -1,12 +1,13 @@ { "name": "Lingo", - "version": "0.1.0", - "description": "Strongly typed, feature-oriented translations for ESP32 with PSRAM-first registry allocation.", + "version": "0.2.0", + "description": "Strongly typed, feature-oriented translations for ESP32 with Strata-backed registry allocation.", "keywords": [ "esp32", "translation", "localization", "i18n", + "strata", "psram", "lingo" ], @@ -23,6 +24,9 @@ "license": "MIT", "frameworks": "arduino", "platforms": "espressif32", + "dependencies": { + "Strata": "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/ZekStack/strata.git#v0.1.2" + }, "build": { "srcDir": "src", "includeDir": "src", From 88310252be856e89d761afb96541102d677c24e3 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:24:02 +0200 Subject: [PATCH 09/25] Bump Lingo metadata to v0.2.0 --- library.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index be87c7f..48690bd 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Lingo -version=0.1.0 +version=0.2.0 author=zekageri maintainer=zekageri sentence=Strongly typed, feature-oriented translations for ESP32. -paragraph=Provides isolated enum-key translation domains, multiple feature tables per language, allocation-free lookup, same-domain fallback, and PSRAM-first registry allocation. +paragraph=Provides isolated enum-key translation domains, multiple feature tables per language, allocation-free lookup, same-domain fallback, and Strata-backed registry allocation. category=Data Processing url=https://github.com/ZekStack/lingo repository=https://github.com/ZekStack/lingo.git From ccf0be55136a69345bbd31877c83ae9c975d4a43 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:24:49 +0200 Subject: [PATCH 10/25] Align Lingo CI with Strata memory ownership --- .github/workflows/ci.yml | 97 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32a6cb5..abde632 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,14 @@ concurrency: cancel-in-progress: false env: + ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: https://espressif.github.io/arduino-esp32/package_esp32_index.json + ARDUINO_CLI_VERSION: 1.5.0 + ARDUINO_NETWORK_CONNECTION_TIMEOUT: 240s + ESP32_CORE_VERSION: 3.3.9 PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip PIOARDUINO_PLATFORM_VERSION: 55.03.39 PIOARDUINO_VERSION: 6.1.19 + STRATA_VERSION: v0.1.2 jobs: metadata: @@ -42,6 +47,7 @@ jobs: - uses: actions/checkout@v4 - name: Audit embedded boundaries run: | + set -e if grep -RInE '#include[[:space:]]+[<"](Accord|Curier|Flow|Fresh|Link|Phase|Pulse|Seal|Signal|Tempo|Trace|Worker)\.h[>"]' src; then echo "Lingo must not depend on another ZekStack library" exit 1 @@ -62,32 +68,47 @@ jobs: echo "Lingo internals must not use Arduino String" exit 1 fi + if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|(^|[^[:alnum:]_:])malloc[[:space:]]*\(|(^|[^[:alnum:]_:])calloc[[:space:]]*\(|(^|[^[:alnum:]_:])realloc[[:space:]]*\(|(^|[^[:alnum:]_:])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then + echo "Lingo-owned allocations must route through Strata" + exit 1 + fi host-tests: runs-on: [self-hosted, zekstack-ci] needs: source-audit steps: - uses: actions/checkout@v4 + - name: Checkout Strata + run: | + rm -rf /tmp/lingo-strata + git clone --depth 1 --branch "${STRATA_VERSION}" \ + https://github.com/ZekStack/strata.git /tmp/lingo-strata - name: Build host behavior tests run: | g++ -std=c++20 -Wall -Wextra -pedantic \ -Isrc \ + -I/tmp/lingo-strata/src \ src/Lingo.cpp \ + /tmp/lingo-strata/src/strata/Allocation.cpp \ + /tmp/lingo-strata/src/strata/Diagnostics.cpp \ + /tmp/lingo-strata/src/strata/internal/PlatformGeneric.cpp \ tests/host/lingo_host_tests.cpp \ -o /tmp/lingo-host-tests - name: Run host behavior tests run: /tmp/lingo-host-tests - - name: Build PSRAM allocation tests + - name: Build memory policy tests run: | g++ -std=c++20 -Wall -Wextra -pedantic \ - -DESP32 \ - -Itests/host/esp_stubs \ -Isrc \ + -I/tmp/lingo-strata/src \ src/Lingo.cpp \ - tests/host/psram_allocation_tests.cpp \ - -o /tmp/lingo-psram-tests - - name: Run PSRAM allocation tests - run: /tmp/lingo-psram-tests + /tmp/lingo-strata/src/strata/Allocation.cpp \ + /tmp/lingo-strata/src/strata/Diagnostics.cpp \ + /tmp/lingo-strata/src/strata/internal/PlatformGeneric.cpp \ + tests/host/memory_policy_tests.cpp \ + -o /tmp/lingo-memory-policy-tests + - name: Run memory policy tests + run: /tmp/lingo-memory-policy-tests build-examples: runs-on: [self-hosted, zekstack-ci] @@ -112,6 +133,8 @@ jobs: ~/.platformio ~/.cache/pip key: ${{ runner.os }}-pioarduino-${{ env.PIOARDUINO_VERSION }}-${{ env.PIOARDUINO_PLATFORM_VERSION }}-${{ hashFiles('**/library.json') }} + restore-keys: | + ${{ runner.os }}-pioarduino-${{ env.PIOARDUINO_VERSION }}-${{ env.PIOARDUINO_PLATFORM_VERSION }}- - name: Install PIOArduino run: | python -m pip install --upgrade "pioarduino==${PIOARDUINO_VERSION}" @@ -124,13 +147,69 @@ jobs: --board "${{ matrix.board }}" \ --lib="." \ --project-option "platform=${PIOARDUINO_PLATFORM_URL}" \ + --project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \ --project-option "build_unflags=-std=gnu++11" \ --project-option "build_flags=-std=gnu++20" done + arduino-cli: + runs-on: [self-hosted, zekstack-ci] + needs: source-audit + strategy: + fail-fast: false + matrix: + board: + - fqbn: esp32:esp32:esp32 + name: esp32dev + - fqbn: esp32:esp32:esp32s3 + name: esp32-s3-devkitc-1 + - fqbn: esp32:esp32:esp32c3 + name: esp32-c3-devkitm-1 + - fqbn: esp32:esp32:esp32p4 + name: esp32-p4-evboard + steps: + - uses: actions/checkout@v4 + - uses: arduino/setup-arduino-cli@v2 + with: + version: ${{ env.ARDUINO_CLI_VERSION }} + - name: Cache Arduino CLI packages + uses: actions/cache@v4 + with: + path: | + ~/.arduino15 + ~/.cache/arduino + key: ${{ runner.os }}-arduino-cli-${{ env.ARDUINO_CLI_VERSION }}-esp32-${{ env.ESP32_CORE_VERSION }} + restore-keys: | + ${{ runner.os }}-arduino-cli-${{ env.ARDUINO_CLI_VERSION }}-esp32- + - name: Install ESP32 core + run: | + arduino-cli core update-index + arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}" + - name: Add local libraries to sketchbook + run: | + set -e + SKETCHBOOK_DIR="${HOME}/Arduino" + rm -rf "$SKETCHBOOK_DIR/libraries/Lingo" "$SKETCHBOOK_DIR/libraries/Strata" + mkdir -p "$SKETCHBOOK_DIR/libraries/Lingo" + rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Lingo/" + git clone --depth 1 --branch "${STRATA_VERSION}" \ + https://github.com/ZekStack/strata.git \ + "$SKETCHBOOK_DIR/libraries/Strata" + - name: Build examples (${{ matrix.board.name }}) + env: + FQBN: ${{ matrix.board.fqbn }} + run: | + set -e + for sketch in examples/*; do + arduino-cli compile \ + --fqbn "$FQBN" \ + --build-property "compiler.cpp.extra_flags=-std=gnu++20" \ + "$sketch" + done + release: if: startsWith(github.ref, 'refs/tags/v') - needs: [metadata, clang-format, source-audit, host-tests, build-examples] + needs: [metadata, clang-format, source-audit, host-tests, build-examples, arduino-cli] runs-on: [self-hosted, zekstack-ci] permissions: contents: write @@ -142,8 +221,8 @@ jobs: run: | VERSION="${GITHUB_REF_NAME#v}" awk -v version="$VERSION" ' + $0 == "## " {if (capture) exit} $0 == "## " version {capture=1; next} - capture && /^## / {exit} capture {print} ' CHANGELOG.md > /tmp/release-notes.md gh release create "$GITHUB_REF_NAME" \ From 61880c085058844f9a34604515f1f037868eb0c3 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:25:08 +0200 Subject: [PATCH 11/25] Document Lingo v0.2.0 Strata migration --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cad5d..f36f0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to Lingo are documented in this file. +## 0.2.0 + +- Add Strata v0.1.2 as Lingo's memory ownership dependency. +- Replace `LingoConfig::preferPsram` with the shared `Strata::MemoryPolicy` configuration contract. +- Preserve the v0.1.x default registry behavior with `memory.allocation = Strata::Placement::PreferExternal`. +- Route the bounded translation-table registry through Strata typed allocation and `Strata::free()`. +- Add explicit `Default`, `Internal`, `PreferExternal`, and `RequireExternal` placement support. +- Add `LingoDiag` with requested registry placement and observed registry region. +- Validate Strata memory policies during `init()` and continue returning `LingoStatus::AllocationFailed` for unsatisfied allocations. +- Remove the bespoke ESP-IDF heap allocator, PSRAM test stubs, and direct `heap_caps_*` dependency. +- Add CI source contracts preventing direct heap allocation paths from returning to Lingo-owned code. +- Add host memory-policy coverage and Arduino CLI builds alongside the existing PlatformIO ESP32 matrix. + +### Migration from 0.1.x + +```cpp +// 0.1.x +config.preferPsram = false; + +// 0.2.0 +config.memory.allocation = Strata::Placement::Internal; +``` + +The former default `preferPsram = true` maps to `Strata::Placement::PreferExternal` and remains the Lingo default in v0.2.0. + ## 0.1.0 - Add strongly typed enum-key translation domains without RTTI. From e95e7ee7e1b6c797c9d1da287e45f2d14dc2bf43 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:25:45 +0200 Subject: [PATCH 12/25] Document Lingo Strata API --- docs/api.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index a5f719e..9a3450a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,14 +4,26 @@ ```cpp struct LingoConfig { + Strata::MemoryPolicy memory{ + .allocation = Strata::Placement::PreferExternal, + .taskStack = Strata::Placement::Internal, + }; LingoLanguage defaultLanguage{}; size_t maxTables = 64; const char *missingTranslation = ""; - bool preferPsram = true; }; ``` -`preferPsram` controls the registry allocation. On ESP32, Lingo first attempts PSRAM and falls back to normal 8-bit capable heap. On host builds it uses normal heap allocation. +`memory.allocation` controls the one bounded registry allocation owned by Lingo. The default is `Strata::Placement::PreferExternal`, preserving the v0.1.x PSRAM-first behavior with internal-memory fallback. + +Lingo does not create tasks, so `memory.taskStack` currently has no runtime storage to control. It remains part of the config to use the same `Strata::MemoryPolicy` contract as other ZekStack libraries. The complete policy is validated by `init()`. + +Useful allocation placements are: + +- `Strata::Placement::Default` - use Strata's backend-default allocation policy. +- `Strata::Placement::Internal` - require internal memory. +- `Strata::Placement::PreferExternal` - prefer external memory and allow safe fallback. +- `Strata::Placement::RequireExternal` - require external memory; initialization fails if unavailable. ## `LingoResult` @@ -24,6 +36,8 @@ if (!result) { } ``` +Invalid memory policies return `LingoStatus::InvalidConfig`. An otherwise valid policy that cannot satisfy the registry allocation returns `LingoStatus::AllocationFailed`. + ## Lifecycle ```cpp @@ -32,6 +46,8 @@ LingoResult end(); bool initialized() const; ``` +`init()` performs Lingo's only owned dynamic allocation. `end()` releases it through Strata. Re-initialization after `end()` is supported. + ## Tables ```cpp @@ -42,7 +58,7 @@ LingoResult addTable( ); ``` -Only one table may be registered for a given `{language, key enum type}` pair. +Only one table may be registered for a given `{language, key enum type}` pair. Registration does not allocate. ## Language selection @@ -75,10 +91,25 @@ const char *find(TKey key, TLanguage language) const; `get(key, language)` first checks the requested language, then the default language, then returns `missingTranslation`. -## Registry information +Lookup remains allocation-free. + +## Registry information and diagnostics ```cpp size_t tableCount() const; size_t tableCapacity() const; -bool preferPsram() const; +LingoDiag getDiagnostics() const; ``` + +`LingoDiag` keeps requested policy separate from observed memory location: + +```cpp +struct LingoDiag { + size_t tableCount; + size_t tableCapacity; + Strata::Placement registryPlacement; + Strata::Region registryRegion; +}; +``` + +For example, `registryPlacement == Strata::Placement::PreferExternal` may legitimately result in `registryRegion == Strata::Region::Internal` when external memory is unavailable and fallback is allowed. From b864d09eb40fe5053da51b3646086c3453510589 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:26:03 +0200 Subject: [PATCH 13/25] Update Lingo getting started for Strata --- docs/getting-started.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index a2b9e9f..af6004c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting started -Lingo maps strongly typed enum keys to static translation strings. +Lingo maps strongly typed enum keys to static translation strings and uses Strata for its bounded registry allocation. ## 1. Define languages @@ -45,7 +45,7 @@ Lingo lingo; LingoConfig config; config.defaultLanguage = Language::Hu; -config.preferPsram = true; +config.memory.allocation = Strata::Placement::PreferExternal; if (!lingo.init(config)) { return; @@ -55,6 +55,8 @@ lingo.addTable(Language::Hu, SOFTWARE_HU); lingo.addTable(Language::En, SOFTWARE_EN); ``` +`PreferExternal` is already Lingo's default and preserves the v0.1.x PSRAM-first behavior. Set `Internal` when the registry must stay in internal RAM, or `RequireExternal` when initialization must fail instead of falling back. + ## 5. Translate ```cpp @@ -64,3 +66,17 @@ const char *english = lingo.get(SoftwareKey::Install, Language::En); ``` `get()` never returns `nullptr`. Use `find()` when the caller needs a strict nullable lookup. + +## 6. Inspect registry placement + +```cpp +const LingoDiag diag = lingo.getDiagnostics(); + +// Requested policy. +Strata::Placement requested = diag.registryPlacement; + +// Actual memory region, when the platform can identify it. +Strata::Region actual = diag.registryRegion; +``` + +Requested placement and observed region are intentionally separate because `PreferExternal` may fall back to internal memory. From 03207926486344e1cf49e6edffca1cbd13493dfc Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:26:16 +0200 Subject: [PATCH 14/25] Replace Lingo PSRAM config docs with Strata policy --- docs/configuration.md | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3edf270..a87d43b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,5 +1,27 @@ # Configuration +## `memory` + +Lingo uses the shared ZekStack `Strata::MemoryPolicy` configuration contract. + +```cpp +LingoConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +``` + +Lingo owns one bounded dynamic allocation: the translation-table registry created by `init()`. `memory.allocation` controls that registry. + +The default is `Strata::Placement::PreferExternal`, which preserves the v0.1.x behavior: prefer external memory and allow safe fallback to internal memory. + +Available placements are: + +- `Strata::Placement::Default` - use the Strata backend's default allocation policy. +- `Strata::Placement::Internal` - allocate the registry from internal memory. +- `Strata::Placement::PreferExternal` - prefer external memory and allow fallback. +- `Strata::Placement::RequireExternal` - require external memory; `init()` returns `LingoStatus::AllocationFailed` when it cannot be satisfied. + +Lingo does not own any tasks, so `memory.taskStack` currently controls no Lingo allocation. It remains part of the config for ecosystem consistency and the full memory policy is validated by `init()`. + ## `defaultLanguage` The default language is stored during `init()`. It does not need to have a table registered yet, which allows normal boot-time initialization before features register their translations. @@ -18,7 +40,7 @@ Use `setDefaultLanguage()` after registration to change the selected language at config.maxTables = 64; ``` -Lingo allocates the registry once during `init()`. Table registration and translation lookup do not allocate. +Lingo allocates the complete registry once during `init()`. Table registration and translation lookup do not allocate. For two languages, 64 slots allow 32 feature domains. @@ -32,14 +54,24 @@ config.missingTranslation = ""; The pointer must remain valid while Lingo is initialized. -## `preferPsram` +## Migrating from v0.1.x -PSRAM is preferred by default: +Replace the old PSRAM boolean with Strata placement: ```cpp +// v0.1.x config.preferPsram = true; + +// v0.2.0 +config.memory.allocation = Strata::Placement::PreferExternal; ``` -On ESP32, Lingo attempts `MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT` first when SPIRAM is available. If that allocation fails or PSRAM is unavailable, it falls back to normal `MALLOC_CAP_8BIT` heap. +and: -Set it to `false` to allocate the registry from normal 8-bit capable heap directly. +```cpp +// v0.1.x +config.preferPsram = false; + +// v0.2.0 +config.memory.allocation = Strata::Placement::Internal; +``` From 1b6930d3d7043a367433ac9f5954ed2bb122c0ef Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:26:30 +0200 Subject: [PATCH 15/25] Document Strata-backed Lingo memory ownership --- docs/memory.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/memory.md b/docs/memory.md index 5727073..ce5d873 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,6 +1,6 @@ # Memory -Lingo is designed around static translation data and one bounded runtime registry. +Lingo is designed around static translation data and one bounded runtime registry. All Lingo-owned dynamic memory routes through Strata. ## Static translation data @@ -12,19 +12,35 @@ constexpr LingoTable EN{ }; ``` -The string literal and table live in static storage. Lookup does not allocate or copy the translation. +The string literal and table live in static storage. Lingo does not copy them. ## Registry allocation -`init()` allocates `maxTables` registry slots once. +`init()` allocates `maxTables` registry slots once through Strata. -With `preferPsram = true`, ESP32 allocation follows the same PSRAM-first policy used across ZekStack libraries: +```cpp +LingoConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +``` + +The default is `PreferExternal`, preserving Lingo v0.1.x behavior: external memory is preferred and internal memory is a valid fallback. + +Use `Internal` to keep the registry in internal RAM, `Default` to use the backend default, or `RequireExternal` when internal fallback is not acceptable. + +If the requested placement cannot be satisfied, `init()` returns `LingoStatus::AllocationFailed` and Lingo remains uninitialized. + +## Requested placement and observed region -1. use `MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT` when SPIRAM exists, -2. fall back to `MALLOC_CAP_8BIT`, -3. return `LingoStatus::AllocationFailed` if neither allocation succeeds. +`getDiagnostics()` reports both the requested placement and the actual observed region: -Host builds use `malloc`. +```cpp +const LingoDiag diag = lingo.getDiagnostics(); + +Strata::Placement requested = diag.registryPlacement; +Strata::Region actual = diag.registryRegion; +``` + +These are intentionally separate. For example, `PreferExternal` may resolve to `Region::Internal` when external allocation fails and fallback is allowed. ## Runtime allocation behavior @@ -35,4 +51,8 @@ After successful `init()`: - `get()` does not allocate, - `setDefaultLanguage()` does not allocate. -Only `init()` allocates registry storage and `end()` releases it. +Only `init()` allocates registry storage and `end()` releases it through Strata. + +## Ownership boundary + +Lingo production sources must not call ESP-IDF heap capability APIs, `ps_malloc`, or raw C heap allocation directly. CI enforces this so future Lingo-owned allocations continue to use Strata. From a79dce4c2df489aa71cdbe33f7101ecf97f78cb3 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:26:40 +0200 Subject: [PATCH 16/25] Update Basic example for Strata memory policy --- examples/Basic/Basic.ino | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/Basic/Basic.ino b/examples/Basic/Basic.ino index 28abcb0..305b92c 100644 --- a/examples/Basic/Basic.ino +++ b/examples/Basic/Basic.ino @@ -28,7 +28,7 @@ void setup() { LingoConfig config; config.defaultLanguage = Language::Hu; - config.preferPsram = true; + config.memory.allocation = Strata::Placement::PreferExternal; if (!lingo.init(config)) { return; @@ -40,6 +40,13 @@ void setup() { Serial.println(lingo.get(CommonKey::Key)); Serial.println(lingo.get(CommonKey::Example, Language::En)); + const LingoDiag diag = lingo.getDiagnostics(); + Serial.printf( + "registry placement=%u region=%u\n", + static_cast(diag.registryPlacement), + static_cast(diag.registryRegion) + ); + lingo.setDefaultLanguage(Language::En); Serial.println(lingo.get(CommonKey::Key)); } From c58b46c6de19147b0abe854fae9ee75bc95e09a2 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:27:18 +0200 Subject: [PATCH 17/25] Update Lingo README for Strata v0.2.0 --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2043e74..333878c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Lingo is a strongly typed, feature-oriented translation library for Arduino ESP32. -It is designed for firmware where each feature owns a small translation table, lookup must stay allocation-free, and runtime registry storage should prefer PSRAM when available. +It is designed for firmware where each feature owns a small translation table, lookup must stay allocation-free, and the bounded runtime registry should follow the same Strata memory policy used across ZekStack libraries. [![CI](https://github.com/ZekStack/lingo/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/lingo/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/ZekStack/lingo?sort=semver)](https://github.com/ZekStack/lingo/releases) @@ -12,14 +12,17 @@ It is designed for firmware where each feature owns a small translation table, l * **Feature-owned tables** - register many small translation domains per language instead of one global dictionary. * **Strongly typed keys** - each enum type is an isolated translation domain, so identical numeric values cannot collide across features. -* **PSRAM-first registry** - `preferPsram` defaults to `true` and falls back to normal ESP32 heap when needed. +* **Strata memory policy** - registry allocation uses `Strata::MemoryPolicy` and defaults to external-preferred allocation with safe internal fallback. * **Allocation-free lookup** - translation tables and strings are referenced directly; `get()` and `find()` do not allocate. * **No RTTI** - enum domains use unique static type tokens without `typeid` or `std::type_index`. * **Safe C strings** - `get()` always returns a valid `const char *`, suitable for logging and embedded display APIs. * **Explicit fallback** - requested-language lookup can fall back to the configured default language within the same feature domain. +* **Placement diagnostics** - requested registry placement and the observed memory region are reported separately. ## Install +Lingo v0.2.0 depends on [Strata](https://github.com/ZekStack/strata) v0.1.2. + ### PlatformIO ```ini @@ -37,16 +40,19 @@ build_unflags = -std=gnu++11 ``` -### Arduino IDE +Lingo's `library.json` pins Strata v0.1.2. -Lingo is not published to Arduino Library Manager yet. +### Arduino IDE -Install it by downloading the repository ZIP or cloning it into: +Lingo is not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory: ```txt Arduino/libraries/Lingo +Arduino/libraries/Strata ``` +Use Strata v0.1.2 with Lingo v0.2.0. + ## Quick start ```cpp @@ -78,7 +84,7 @@ constexpr LingoTable SOFTWARE_EN{ void setup() { LingoConfig config; config.defaultLanguage = Language::Hu; - config.preferPsram = true; + config.memory.allocation = Strata::Placement::PreferExternal; if (!lingo.init(config)) { return; @@ -90,10 +96,16 @@ void setup() { const char *current = lingo.get(SoftwareKey::Install); const char *english = lingo.get(SoftwareKey::Install, Language::En); + const LingoDiag diag = lingo.getDiagnostics(); + Strata::Placement requested = diag.registryPlacement; + Strata::Region actual = diag.registryRegion; + lingo.setDefaultLanguage(Language::En); } ``` +`PreferExternal` is Lingo's default, so setting it explicitly is optional. It preserves the v0.1.x PSRAM-first behavior while allowing internal fallback. + A second feature can define its own enum starting at the same numeric values without collisions: ```cpp @@ -106,6 +118,21 @@ lingo.addTable(Language::Hu, TIME_HU); lingo.addTable(Language::En, TIME_EN); ``` +## Memory placement + +Lingo owns one dynamic allocation: the bounded translation-table registry created during `init()`. + +```cpp +config.memory.allocation = Strata::Placement::Default; +config.memory.allocation = Strata::Placement::Internal; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.allocation = Strata::Placement::RequireExternal; +``` + +`RequireExternal` never silently falls back. If Strata cannot satisfy the placement, `init()` returns `LingoStatus::AllocationFailed` and Lingo remains uninitialized. + +Lingo creates no tasks, so `config.memory.taskStack` currently has no Lingo-owned task stack to control. The field remains part of the shared `Strata::MemoryPolicy` contract. + ## Important notes > [!IMPORTANT] @@ -115,7 +142,8 @@ lingo.addTable(Language::En, TIME_EN); * Size `maxTables` for the total number of registered language/domain pairs; each feature table in each language consumes one slot. * `get()` never returns `nullptr`; `find()` is the strict nullable lookup API. * `get(key, language)` falls back to the default language only within the same enum domain. -* `init()` allocates a bounded registry once. With `preferPsram = true`, ESP32 PSRAM is attempted first and normal heap is the fallback. +* `init()` allocates the complete bounded registry once through Strata. +* `addTable()`, `get()`, `find()`, and `setDefaultLanguage()` do not allocate. * After startup registration, concurrent `get()`/`find()` calls are supported. Do not mutate the table registry concurrently with lookups. * Translation bytes are passed through unchanged, including UTF-8 text. @@ -123,7 +151,7 @@ lingo.addTable(Language::En, TIME_EN); | Example | Description | | --- | --- | -| `Basic` | Initialize Lingo, register HU/EN tables, and switch the default language. | +| `Basic` | Initialize Lingo with Strata placement, register HU/EN tables, inspect registry diagnostics, and switch the default language. | | `FeatureTables` | Register multiple feature domains per language with overlapping numeric key values. | | `Fallback` | Demonstrate strict lookup and same-domain default-language fallback. | @@ -137,12 +165,12 @@ examples/Basic | Document | Description | | --- | --- | -| [`docs/getting-started.md`](docs/getting-started.md) | First integration and table registration. | -| [`docs/api.md`](docs/api.md) | Public types and methods. | -| [`docs/configuration.md`](docs/configuration.md) | Registry sizing, fallback string, and PSRAM preference. | +| [`docs/getting-started.md`](docs/getting-started.md) | First integration, Strata memory placement, and table registration. | +| [`docs/api.md`](docs/api.md) | Public types, methods, and diagnostics. | +| [`docs/configuration.md`](docs/configuration.md) | Memory policy, registry sizing, and fallback string. | | [`docs/tables-and-domains.md`](docs/tables-and-domains.md) | Feature-domain identity and registration rules. | | [`docs/fallback.md`](docs/fallback.md) | Exact `get()` and `find()` resolution behavior. | -| [`docs/memory.md`](docs/memory.md) | Static translations, bounded allocation, and PSRAM behavior. | +| [`docs/memory.md`](docs/memory.md) | Static translations, bounded allocation, and Strata ownership. | | [`docs/thread-safety.md`](docs/thread-safety.md) | Supported runtime concurrency and lifecycle rules. | | [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common registration and lookup failures. | @@ -158,9 +186,30 @@ const char *value = lingo.get(SoftwareKey::Install); const char *hu = lingo.get(SoftwareKey::Install, Language::Hu); const char *strict = lingo.find(SoftwareKey::Install, Language::Hu); +LingoDiag diag = lingo.getDiagnostics(); lingo.setDefaultLanguage(Language::En); ``` +## Migrating from v0.1.x + +```cpp +// v0.1.x: prefer PSRAM with internal fallback +config.preferPsram = true; + +// v0.2.0 +config.memory.allocation = Strata::Placement::PreferExternal; +``` + +```cpp +// v0.1.x: internal heap +config.preferPsram = false; + +// v0.2.0 +config.memory.allocation = Strata::Placement::Internal; +``` + +`Lingo::preferPsram()` was removed. Use `getDiagnostics().registryPlacement` for requested policy and `getDiagnostics().registryRegion` for the observed location. + ## Compatibility | Item | Support | @@ -168,12 +217,12 @@ lingo.setDefaultLanguage(Language::En); | Framework | Arduino ESP32 | | Platform | `espressif32` | | Language | C++20 | -| Dependencies | none | +| Dependencies | Strata v0.1.2 | | RTTI | not required | | Exceptions | not used for public error handling | -| PSRAM | preferred for registry allocation by default | +| Registry default | `Strata::Placement::PreferExternal` | | Runtime lookup allocation | none | -| Status | initial `0.1.0` development | +| Status | v0.2.0 Strata memory-policy migration | ## License From efbc1e5216be767f9a6e64bc0230fc5af6ba8bdc Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:28:17 +0200 Subject: [PATCH 18/25] Document Strata allocation failures --- docs/troubleshooting.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2d5f827..990fa7e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,15 @@ # Troubleshooting +## `InvalidConfig` + +`init()` rejects zero `maxTables`, a null `missingTranslation`, an overflowing registry size, or an invalid `Strata::MemoryPolicy`. Use one of the defined `Strata::Placement` values for both policy fields. + +## `AllocationFailed` + +Strata could not allocate the bounded registry with the requested policy. + +If `config.memory.allocation == Strata::Placement::RequireExternal`, external memory is mandatory and Lingo does not fall back to internal RAM. Use `PreferExternal` when internal fallback is acceptable or `Internal` when the registry should stay in internal memory. + ## `TableAlreadyRegistered` Only one table may be registered for each `{language, key enum type}` pair. Merge that feature's translations into one table for the language. From 1380b4027005d4a2d9d304f4bb18df518919840c Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:28:57 +0200 Subject: [PATCH 19/25] Fix release notes extraction in Lingo CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abde632..2fcafa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,8 +221,8 @@ jobs: run: | VERSION="${GITHUB_REF_NAME#v}" awk -v version="$VERSION" ' - $0 == "## " {if (capture) exit} $0 == "## " version {capture=1; next} + capture && /^## / {exit} capture {print} ' CHANGELOG.md > /tmp/release-notes.md gh release create "$GITHUB_REF_NAME" \ From 1cf5720fafb45e99659b3fdfee2d56e613aa7ac7 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:31:53 +0200 Subject: [PATCH 20/25] Fix clang-format in Lingo init validation --- src/Lingo.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Lingo.cpp b/src/Lingo.cpp index bd2a2f5..3b1ba61 100644 --- a/src/Lingo.cpp +++ b/src/Lingo.cpp @@ -32,10 +32,8 @@ LingoResult Lingo::init(const LingoConfig &config) { ); } - if ( - config.maxTables == 0 || - config.missingTranslation == nullptr || - !Strata::validMemoryPolicy(config.memory)) { + if (config.maxTables == 0 || config.missingTranslation == nullptr || + !Strata::validMemoryPolicy(config.memory)) { return LingoResult::failure(LingoStatus::InvalidConfig, "invalid lingo configuration"); } From a9d200f50dfbf9b9d3e341ffa39877d63b2ad1c4 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:33:52 +0200 Subject: [PATCH 21/25] Migrate FeatureTables example to Strata memory policy --- examples/FeatureTables/FeatureTables.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/FeatureTables/FeatureTables.ino b/examples/FeatureTables/FeatureTables.ino index 58bd623..52b5a40 100644 --- a/examples/FeatureTables/FeatureTables.ino +++ b/examples/FeatureTables/FeatureTables.ino @@ -56,7 +56,7 @@ void setup() { LingoConfig config; config.defaultLanguage = Language::Hu; config.maxTables = 8; - config.preferPsram = true; + config.memory.allocation = Strata::Placement::PreferExternal; if (!lingo.init(config)) { return; From 02038afdcd43d8e1d08a3a6a6284b540006ceb09 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:30:53 +0200 Subject: [PATCH 22/25] Fix PlatformIO CI disk exhaustion --- .github/workflows/ci.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fcafa3..caeee77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,9 @@ jobs: build-examples: runs-on: [self-hosted, zekstack-ci] needs: [source-audit, host-tests] + env: + PLATFORMIO_CORE_DIR: ${{ runner.temp }}/lingo-platformio + PIP_CACHE_DIR: ${{ runner.temp }}/lingo-pip-cache strategy: fail-fast: false matrix: @@ -122,19 +125,15 @@ jobs: - esp32-c3-devkitm-1 - esp32-p4-evboard steps: + - name: Clean stale PIOArduino state + run: | + rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" + rm -rf "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" + mkdir -p "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.13' - - name: Cache PIOArduino - uses: actions/cache@v4 - with: - path: | - ~/.platformio - ~/.cache/pip - key: ${{ runner.os }}-pioarduino-${{ env.PIOARDUINO_VERSION }}-${{ env.PIOARDUINO_PLATFORM_VERSION }}-${{ hashFiles('**/library.json') }} - restore-keys: | - ${{ runner.os }}-pioarduino-${{ env.PIOARDUINO_VERSION }}-${{ env.PIOARDUINO_PLATFORM_VERSION }}- - name: Install PIOArduino run: | python -m pip install --upgrade "pioarduino==${PIOARDUINO_VERSION}" @@ -151,6 +150,9 @@ jobs: --project-option "build_unflags=-std=gnu++11" \ --project-option "build_flags=-std=gnu++20" done + - name: Clean PIOArduino temporary state + if: always() + run: rm -rf "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" arduino-cli: runs-on: [self-hosted, zekstack-ci] From daeb7d4a37635358b3f979ba2165ce0aa6bddfbd Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:33:30 +0200 Subject: [PATCH 23/25] Fix CI workflow validation --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caeee77..a5949df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,9 +113,6 @@ jobs: build-examples: runs-on: [self-hosted, zekstack-ci] needs: [source-audit, host-tests] - env: - PLATFORMIO_CORE_DIR: ${{ runner.temp }}/lingo-platformio - PIP_CACHE_DIR: ${{ runner.temp }}/lingo-pip-cache strategy: fail-fast: false matrix: @@ -127,9 +124,13 @@ jobs: steps: - name: Clean stale PIOArduino state run: | + PLATFORMIO_CORE_DIR="${RUNNER_TEMP}/lingo-platformio" + PIP_CACHE_DIR="${RUNNER_TEMP}/lingo-pip-cache" rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" rm -rf "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" mkdir -p "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" + echo "PLATFORMIO_CORE_DIR=${PLATFORMIO_CORE_DIR}" >> "${GITHUB_ENV}" + echo "PIP_CACHE_DIR=${PIP_CACHE_DIR}" >> "${GITHUB_ENV}" - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: From 59aec3ad45eabaeaea9b92204967d63face7bfea Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:37:38 +0200 Subject: [PATCH 24/25] Isolate embedded CI toolchains --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5949df..a787045 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ on: workflow_dispatch: concurrency: - group: lingo-${{ github.ref }} - cancel-in-progress: false + group: lingo-${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true env: ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: https://espressif.github.io/arduino-esp32/package_esp32_index.json @@ -122,11 +122,11 @@ jobs: - esp32-c3-devkitm-1 - esp32-p4-evboard steps: - - name: Clean stale PIOArduino state + - name: Prepare isolated PIOArduino state run: | PLATFORMIO_CORE_DIR="${RUNNER_TEMP}/lingo-platformio" PIP_CACHE_DIR="${RUNNER_TEMP}/lingo-pip-cache" - rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" + rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" "${HOME}/.arduino15" "${HOME}/.cache/arduino" rm -rf "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" mkdir -p "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" echo "PLATFORMIO_CORE_DIR=${PLATFORMIO_CORE_DIR}" >> "${GITHUB_ENV}" @@ -171,19 +171,21 @@ jobs: - fqbn: esp32:esp32:esp32p4 name: esp32-p4-evboard steps: + - name: Prepare isolated Arduino CLI state + run: | + ARDUINO_DATA_DIR="${RUNNER_TEMP}/lingo-arduino-data" + ARDUINO_DOWNLOADS_DIR="${RUNNER_TEMP}/lingo-arduino-downloads" + ARDUINO_USER_DIR="${RUNNER_TEMP}/lingo-arduino-user" + rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" "${HOME}/.arduino15" "${HOME}/.cache/arduino" + rm -rf "${ARDUINO_DATA_DIR}" "${ARDUINO_DOWNLOADS_DIR}" "${ARDUINO_USER_DIR}" + mkdir -p "${ARDUINO_DATA_DIR}" "${ARDUINO_DOWNLOADS_DIR}" "${ARDUINO_USER_DIR}" + echo "ARDUINO_DIRECTORIES_DATA=${ARDUINO_DATA_DIR}" >> "${GITHUB_ENV}" + echo "ARDUINO_DIRECTORIES_DOWNLOADS=${ARDUINO_DOWNLOADS_DIR}" >> "${GITHUB_ENV}" + echo "ARDUINO_DIRECTORIES_USER=${ARDUINO_USER_DIR}" >> "${GITHUB_ENV}" - uses: actions/checkout@v4 - uses: arduino/setup-arduino-cli@v2 with: version: ${{ env.ARDUINO_CLI_VERSION }} - - name: Cache Arduino CLI packages - uses: actions/cache@v4 - with: - path: | - ~/.arduino15 - ~/.cache/arduino - key: ${{ runner.os }}-arduino-cli-${{ env.ARDUINO_CLI_VERSION }}-esp32-${{ env.ESP32_CORE_VERSION }} - restore-keys: | - ${{ runner.os }}-arduino-cli-${{ env.ARDUINO_CLI_VERSION }}-esp32- - name: Install ESP32 core run: | arduino-cli core update-index @@ -191,8 +193,7 @@ jobs: - name: Add local libraries to sketchbook run: | set -e - SKETCHBOOK_DIR="${HOME}/Arduino" - rm -rf "$SKETCHBOOK_DIR/libraries/Lingo" "$SKETCHBOOK_DIR/libraries/Strata" + SKETCHBOOK_DIR="${ARDUINO_DIRECTORIES_USER}" mkdir -p "$SKETCHBOOK_DIR/libraries/Lingo" rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Lingo/" git clone --depth 1 --branch "${STRATA_VERSION}" \ @@ -209,6 +210,12 @@ jobs: --build-property "compiler.cpp.extra_flags=-std=gnu++20" \ "$sketch" done + - name: Clean Arduino CLI temporary state + if: always() + run: | + rm -rf "${ARDUINO_DIRECTORIES_DATA}" \ + "${ARDUINO_DIRECTORIES_DOWNLOADS}" \ + "${ARDUINO_DIRECTORIES_USER}" release: if: startsWith(github.ref, 'refs/tags/v') From 44b06986538e5eb0429eac623b92a917b994b5fe Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:15:13 +0200 Subject: [PATCH 25/25] Fix self-hosted runner cache cleanup --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a787045..b03814f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,6 @@ jobs: run: | PLATFORMIO_CORE_DIR="${RUNNER_TEMP}/lingo-platformio" PIP_CACHE_DIR="${RUNNER_TEMP}/lingo-pip-cache" - rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" "${HOME}/.arduino15" "${HOME}/.cache/arduino" rm -rf "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" mkdir -p "${PLATFORMIO_CORE_DIR}" "${PIP_CACHE_DIR}" echo "PLATFORMIO_CORE_DIR=${PLATFORMIO_CORE_DIR}" >> "${GITHUB_ENV}" @@ -176,7 +175,6 @@ jobs: ARDUINO_DATA_DIR="${RUNNER_TEMP}/lingo-arduino-data" ARDUINO_DOWNLOADS_DIR="${RUNNER_TEMP}/lingo-arduino-downloads" ARDUINO_USER_DIR="${RUNNER_TEMP}/lingo-arduino-user" - rm -rf "${HOME}/.platformio" "${HOME}/.cache/pip" "${HOME}/.arduino15" "${HOME}/.cache/arduino" rm -rf "${ARDUINO_DATA_DIR}" "${ARDUINO_DOWNLOADS_DIR}" "${ARDUINO_USER_DIR}" mkdir -p "${ARDUINO_DATA_DIR}" "${ARDUINO_DOWNLOADS_DIR}" "${ARDUINO_USER_DIR}" echo "ARDUINO_DIRECTORIES_DATA=${ARDUINO_DATA_DIR}" >> "${GITHUB_ENV}"