Skip to content

Commit 554a635

Browse files
committed
packaging and sorted-lookup changes
1 parent a65f8cb commit 554a635

7 files changed

Lines changed: 281 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [Unreleased]
6+
### Added
7+
- Optional per-table sorted-key lookup via `LangLookupMode` and `makeSortedLangTable(...)`.
8+
- Reusable root CMake target `esp_lang` with alias target `ESPLang::esp_lang`.
9+
10+
### Changed
11+
- Standalone CMake now consistently requires C++17 and builds tests against the root library target.
12+
- README now documents the `format(...)` format-string contract and standalone CMake consumption.
13+
514
## [0.1.0] - 2026-04-09
615
### Added
716
- Initial ESPLang release.

CMakeLists.txt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,28 @@
55
cmake_minimum_required(VERSION 3.12)
66
project(ESPLang)
77

8-
enable_testing()
8+
include(CTest)
99

10-
set(CMAKE_CXX_STANDARD 11)
10+
set(CMAKE_CXX_STANDARD 17)
1111
set(CMAKE_CXX_STANDARD_REQUIRED ON)
1212

1313
if(${COVERAGE})
1414
set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -g -O0")
1515
endif()
1616

17-
include_directories(${CMAKE_CURRENT_LIST_DIR}/src)
18-
add_subdirectory(test)
17+
add_library(esp_lang STATIC
18+
src/esp_lang/lang.cpp
19+
)
20+
21+
add_library(ESPLang::esp_lang ALIAS esp_lang)
22+
23+
target_include_directories(esp_lang
24+
PUBLIC
25+
${CMAKE_CURRENT_LIST_DIR}/src
26+
)
27+
28+
target_compile_features(esp_lang PUBLIC cxx_std_17)
29+
30+
if(BUILD_TESTING)
31+
add_subdirectory(test)
32+
endif()

README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ESPLang is a small translation table helper for ESP32 firmware. The library owns
1010
## Features
1111
- No hardcoded languages or translation keys in the library API.
1212
- Pointer-only registration of user-owned translation tables.
13-
- Linear, key-based lookup with no order-sensitive table requirements.
13+
- Linear lookup by default, with optional sorted-key tables for binary-search key lookup.
1414
- Fallback-language lookup for missing translations.
1515
- No heap allocation required by the library.
1616
- Caller-buffer formatting through `format(...)`.
@@ -91,13 +91,15 @@ void setup() {
9191
Public types:
9292
- `using LangLanguageId = uint16_t`
9393
- `using LangKeyId = uint16_t`
94+
- `enum class LangLookupMode : uint8_t { Linear, SortedByKey }` selects per-table key lookup strategy.
9495
- `LangTranslationEntry { key, text }` maps one user-defined key to one translation string.
95-
- `LangTranslationTable { language, entries, entryCount }` groups one language with its translation entries.
96+
- `LangTranslationTable { language, entries, entryCount, lookupMode }` groups one language with its translation entries and lookup mode.
9697
- `ESPLangConfig { tables, tableCount, defaultLanguage, useDefaultFallback, fallbackLanguage, missingText }` configures the pointer-owned table set, startup language, fallback behavior, and missing-text string.
9798

9899
Helpers:
99100
- `ESP_LANG_ENTRY(key, text)` creates a `LangTranslationEntry` from an enum key.
100-
- `makeLangTable(language, entries)` builds a `LangTranslationTable` with the correct entry count.
101+
- `makeLangTable(language, entries)` builds a linear `LangTranslationTable` with the correct entry count.
102+
- `makeSortedLangTable(language, entries)` builds a sorted-key `LangTranslationTable` that uses binary search for key lookup.
101103

102104
Main API:
103105
- `bool init(const ESPLangConfig& config)` / `void deinit()` / `bool isInitialized() const`
@@ -113,23 +115,44 @@ Main API:
113115

114116
Validation and lookup behavior:
115117
- `init(...)` returns `false` for null tables, zero table count, missing default language, missing explicit fallback language, invalid table storage, duplicate language tables, duplicate keys within the same language, or null text pointers.
118+
- `makeSortedLangTable(...)` requires entries to already be sorted by ascending key; `init(...)` rejects unsorted or duplicate keys in sorted tables.
116119
- `translate(key)` checks the selected language first, then the fallback language if it differs, then returns the configured missing text.
117120
- `translateFrom(language, key)` only searches the requested language.
118121
- `missingText` defaults to `"?"` when omitted or reset with `setMissingText(nullptr)`.
122+
- Language-table resolution remains linear even when individual tables use sorted-key lookup.
119123

120124
## Gotchas
121125
- `translateFrom(...)` does not use fallback lookup.
126+
- `format(...)` treats the resolved translation text as a `vsnprintf` format string. Placeholder mistakes in translation data, or mismatched argument types at the call site, become runtime formatting bugs.
122127
- `format(...)` returns `false` when the formatted output does not fully fit, even though the buffer may still contain truncated output.
123128
- `useDefaultFallback = true` resolves the effective fallback language to `defaultLanguage`.
124129
- Duplicate keys are only invalid within the same language table; reusing the same key across different languages is expected.
130+
- Sorted-key lookup is opt-in per table. The library never sorts caller data for you.
125131

126132
## Restrictions
127133
- Packaged as an ESP32 / Arduino-targeted library for ESPToolKit.
128134
- C++17 is required.
129135
- Translation tables and strings are caller-owned and must remain valid for the lifetime of the `ESPLang` instance.
130136

137+
## Standalone CMake
138+
```cmake
139+
include(FetchContent)
140+
141+
FetchContent_Declare(
142+
esp_lang
143+
GIT_REPOSITORY https://github.com/ESPToolKit/esp-lang.git
144+
GIT_TAG main
145+
)
146+
147+
FetchContent_MakeAvailable(esp_lang)
148+
149+
target_link_libraries(your_target PRIVATE ESPLang::esp_lang)
150+
```
151+
152+
If the source is vendored locally, `add_subdirectory(path/to/esp-lang)` exposes the same `ESPLang::esp_lang` target.
153+
131154
## Tests
132-
- Host-side tests in `test/test_esplang` cover config validation, lifecycle, lookup rules, fallback behavior, missing-text handling, and formatting behavior.
155+
- Host-side tests in `test/test_esplang` cover config validation, lifecycle, linear and sorted lookup rules, mixed fallback behavior, missing-text handling, and formatting behavior.
133156
- CI also builds the Arduino examples through both PlatformIO and Arduino CLI on the standard ESP32 board matrix.
134157

135158
## Formatting Baseline

src/esp_lang/lang.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ struct ResolvedConfig {
1616
const char *missingText = kDefaultMissingText;
1717
};
1818

19+
bool hasStrictlyAscendingKeys(const LangTranslationTable &table) {
20+
if (table.entryCount < 2) {
21+
return true;
22+
}
23+
24+
for (size_t entryIndex = 1; entryIndex < table.entryCount; ++entryIndex) {
25+
if (table.entries[entryIndex - 1].key >= table.entries[entryIndex].key) {
26+
return false;
27+
}
28+
}
29+
30+
return true;
31+
}
32+
1933
bool isConfigShapeValid(const ESPLangConfig &config) {
2034
if (config.tables == nullptr || config.tableCount == 0) {
2135
return false;
@@ -39,12 +53,20 @@ bool isConfigShapeValid(const ESPLangConfig &config) {
3953
return false;
4054
}
4155

56+
if (table.lookupMode == LangLookupMode::SortedByKey) {
57+
continue;
58+
}
59+
4260
for (size_t otherIndex = entryIndex + 1; otherIndex < table.entryCount; ++otherIndex) {
4361
if (table.entries[otherIndex].key == entry.key) {
4462
return false;
4563
}
4664
}
4765
}
66+
67+
if (table.lookupMode == LangLookupMode::SortedByKey && !hasStrictlyAscendingKeys(table)) {
68+
return false;
69+
}
4870
}
4971

5072
return true;
@@ -226,6 +248,26 @@ const char *ESPLang::findInTable(const LangTranslationTable *table, LangKeyId ke
226248
return nullptr;
227249
}
228250

251+
if (table->lookupMode == LangLookupMode::SortedByKey) {
252+
size_t left = 0;
253+
size_t right = table->entryCount;
254+
while (left < right) {
255+
const size_t middle = left + ((right - left) / 2);
256+
const LangTranslationEntry &entry = table->entries[middle];
257+
if (entry.key == key) {
258+
return entry.text;
259+
}
260+
261+
if (entry.key < key) {
262+
left = middle + 1;
263+
} else {
264+
right = middle;
265+
}
266+
}
267+
268+
return nullptr;
269+
}
270+
229271
for (size_t index = 0; index < table->entryCount; ++index) {
230272
if (table->entries[index].key == key) {
231273
return table->entries[index].text;

src/esp_lang/lang.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
using LangLanguageId = uint16_t;
99
using LangKeyId = uint16_t;
1010

11+
enum class LangLookupMode : uint8_t {
12+
Linear = 0,
13+
SortedByKey = 1,
14+
};
15+
1116
struct LangTranslationEntry {
1217
LangKeyId key = 0;
1318
const char *text = nullptr;
@@ -17,6 +22,7 @@ struct LangTranslationTable {
1722
LangLanguageId language = 0;
1823
const LangTranslationEntry *entries = nullptr;
1924
size_t entryCount = 0;
25+
LangLookupMode lookupMode = LangLookupMode::Linear;
2026
};
2127

2228
struct ESPLangConfig {
@@ -35,7 +41,13 @@ struct ESPLangConfig {
3541
template <size_t N>
3642
constexpr LangTranslationTable
3743
makeLangTable(LangLanguageId language, const LangTranslationEntry (&entries)[N]) {
38-
return LangTranslationTable{language, entries, N};
44+
return LangTranslationTable{language, entries, N, LangLookupMode::Linear};
45+
}
46+
47+
template <size_t N>
48+
constexpr LangTranslationTable
49+
makeSortedLangTable(LangLanguageId language, const LangTranslationEntry (&entries)[N]) {
50+
return LangTranslationTable{language, entries, N, LangLookupMode::SortedByKey};
3951
}
4052

4153
class ESPLang {

test/CMakeLists.txt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
add_library(esp_lang_core STATIC
2-
${PROJECT_SOURCE_DIR}/src/esp_lang/lang.cpp
3-
)
4-
5-
target_include_directories(esp_lang_core
6-
PUBLIC
7-
${PROJECT_SOURCE_DIR}/src
8-
)
9-
10-
target_compile_features(esp_lang_core PUBLIC cxx_std_17)
11-
121
add_executable(esp_lang_tests
132
test_esplang/test_esplang.cpp
143
)
154

165
target_link_libraries(esp_lang_tests
176
PRIVATE
18-
esp_lang_core
7+
ESPLang::esp_lang
198
)
209

2110
target_compile_features(esp_lang_tests PRIVATE cxx_std_17)

0 commit comments

Comments
 (0)