CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
I18n.cpp
Go to the documentation of this file.
1
6
7#include "cdc_ui/I18n.h"
8
9#include "cdc_core/Raii.h"
10#include "cdc_core/Cp437.h"
11#include "cdc_ui/PsramCjson.h"
12#include "cdc_log.h"
13
14#include "cJSON.h"
15#include "esp_err.h"
16#include "nvs.h"
17#include "nvs_flash.h"
18
19#include <dirent.h>
20
21#include <algorithm>
22#include <cstdint>
23#include <cstdio>
24#include <cstdlib>
25#include <cstring>
26#include <string>
27
28namespace cdc::ui {
29
30static const char* TAG = "I18n";
31
32namespace {
33
34constexpr const char* NVS_NAMESPACE = "i18n";
35constexpr const char* NVS_KEY_LANG_CODE = "langc";
36
39constexpr I18nEntry kCoreStrings[] = {
40 {"core.main_menu", "Main Menu"},
41 {"core.settings", "Settings"},
42 {"core.hardware", "Hardware"},
43 {"core.tools", "Tools"},
44 {"core.hardware_info", "Hardware Info"},
45 {"core.name", "Name"},
46 {"core.info", "Info"},
47 {"core.info2", "Info 2"},
48 {"core.default_name", "CDC Badge"},
49 {"core.default_info", "v" APP_VERSION},
50 {"core.back", "Back"},
51 {"core.ok", "OK"},
52 {"core.cancel", "Cancel"},
53 {"core.save", "Save"},
54 {"core.delete", "Delete"},
55 {"core.edit", "Edit"},
56 {"core.view", "View"},
57 {"core.select", "Select"},
58 {"core.open", "Open"},
59 {"core.add", "Add"},
60 {"core.new", "New"},
61 {"core.new_folder", "New folder"},
62 {"core.new_file", "New file"},
63 {"core.exists", "Already exists"},
64 {"core.not_empty", "Not empty"},
65 {"core.sure", "Are you sure?"},
66 {"core.vfat", "vFAT"},
67 {"core.yes", "Yes"},
68 {"core.no", "No"},
69 {"core.on", "On"},
70 {"core.off", "Off"},
71 {"core.saved", "Saved"},
72 {"core.deleted", "Deleted"},
73 {"core.failed", "Failed"},
74 {"core.timeout", "Timeout"},
75 {"core.empty", "empty"},
76
77 {"core.lock", "Lock"},
78 {"core.unlock", "Unlock"},
79 {"core.enter_pin", "Enter PIN"},
80 {"core.press_any_key", "Any key: unlock [3]: menu"},
81 {"core.deep_sleep", "Deep Sleep"},
82 {"core.wrong_pin", "Wrong PIN"},
83 {"core.locked_out", "Locked out"},
84 {"core.too_many_attempts", "Too many attempts"},
85
86 {"core.change_pin", "Change PIN"},
87 {"core.set_duress_pin", "Set Duress PIN"},
88 {"core.current_pin", "Current PIN"},
89 {"core.new_pin", "New PIN"},
90 {"core.confirm_pin", "Confirm PIN"},
91 {"core.pin_changed", "PIN changed"},
92 {"core.pins_dont_match", "PINs don't match"},
93 {"core.pin_too_short", "PIN too short"},
94 {"core.pin_mismatch", "PINs don't match"},
95 {"core.retries", "Retries"},
96 {"core.error_generic", "Error"},
97
98 {"core.brightness", "Brightness"},
99 {"core.language", "Language"},
100 {"core.lang_name", "English"},
101 {"core.timezone", "Timezone"},
102 {"core.summer_time", "Daylight Saving"},
103 {"core.badge_text", "Badge Text"},
104 {"core.auto_sleep", "Sleep Interval"},
105 {"core.set_date", "Set Date"},
106 {"core.set_time", "Set Time"},
107 {"core.date", "Date"},
108 {"core.time", "Time"},
109 {"core.date_saved", "Date saved"},
110 {"core.time_saved", "Time saved"},
111 {"core.modules", "Modules"},
112 {"core.never", "Never"},
113 {"core.minutes", "min"},
114
115 {"core.wifi_menu", "WiFi"},
116 {"core.wifi_setup", "WiFi Setup"},
117 {"core.wifi_connect", "Connect"},
118 {"core.wifi_details", "Details"},
119 {"core.wifi_disconnect", "Disconnect"},
120 {"core.wifi_on", "WiFi ON"},
121 {"core.wifi_off", "WiFi OFF"},
122 {"core.wifi_scanning", "Scanning..."},
123 {"core.wifi_no_networks", "No networks"},
124 {"core.wifi_connecting", "Connecting..."},
125 {"core.wifi_connected", "Connected!"},
126 {"core.wifi_disconnected", "Disconnected"},
127 {"core.wifi_failed", "Connection failed"},
128 {"core.wifi_no_config", "No WiFi configured"},
129 {"core.wifi_password", "Password"},
130 {"core.wifi_add_manual", "Add Manual"},
131 {"core.wifi_ssid", "SSID"},
132 {"core.wifi_encryption", "Encryption"},
133 {"core.wifi_ip_mode", "IP Mode"},
134 {"core.wifi_dhcp", "DHCP (Auto)"},
135 {"core.wifi_static", "Static IP"},
136 {"core.wifi_gateway", "Gateway"},
137 {"core.wifi_netmask", "Netmask"},
138 {"core.wifi_dns", "DNS"},
139 {"core.wifi_saved_config", "Saved Config"},
140 {"core.wifi_signal", "Signal"},
141 {"core.ntp_sync", "Sync Time"},
142 {"core.ntp_syncing", "Syncing time..."},
143 {"core.ntp_success", "Time synced!"},
144 {"core.ntp_failed", "Sync failed"},
145 {"core.ntp_timeout", "Sync timeout"},
146 {"core.bluetooth", "Bluetooth"},
147 {"core.bluetooth_on", "Bluetooth ON"},
148 {"core.bluetooth_off", "Bluetooth OFF"},
149 {"core.ble_status", "BLE Status"},
150 {"core.ble_scan", "Scan Devices"},
151 {"core.ble_scanning", "Scanning..."},
152 {"core.ble_no_devices", "No devices found"},
153 {"core.ble_connected_to", "Connected to"},
154 {"core.ble_not_connected", "Not connected"},
155 {"core.ble_mac_address", "MAC"},
156 {"core.ble_signal", "Signal"},
157 {"core.ble_paired_devices", "Paired devices"},
158 {"core.ble_no_paired", "No paired devices"},
159 {"core.ble_forget_one", "Forget this device?"},
160 {"core.ble_forget_all", "Forget all bonds"},
161 {"core.ble_forget_all_confirm", "Forget all paired devices?"},
162 {"core.ble_bonds_cleared", "Bonds cleared"},
163 {"core.ble_pairing_menu", "Pair device"},
164 {"core.ble_pairing_title", "Pairing Mode"},
165 {"core.ble_pairing_instr", "On your device, select:"},
166 {"core.ble_pairing_waiting", "Waiting for device..."},
167 {"core.ble_pairing_connected", "Connected"},
168 {"core.ble_pairing_exit", "[N] Exit"},
169 {"core.msg_beacon", "Beacon"},
170 {"core.msg_beacon_on", "Beacon: on"},
171 {"core.msg_beacon_off", "Beacon: off"},
172 {"core.msg_beacon_name", "Beacon name"},
173 {"core.msg_beacon_scan", "Beacon scan"},
174 {"core.msg_pick_peer", "Select badge"},
175 {"core.msg_searching", "Searching for badges..."},
176 {"core.msg_offer_title", "Incoming transfer"},
177 {"core.msg_offer_from", "%s wants to send you:"},
178 {"core.msg_text", "Text message"},
179 {"core.msg_data", "Data"},
180 {"core.msg_sending", "Sending..."},
181 {"core.msg_receiving", "Receiving..."},
182 {"core.msg_transfer_ok", "Transfer complete"},
183 {"core.msg_transfer_fail", "Transfer failed"},
184 {"core.msg_declined", "Declined"},
185 {"core.msg_unsupported", "Type not supported"},
186 {"core.system_test", "System Test"},
187 {"core.tr01_cache_rebuild", "TR01 Cache Rebuild"},
188 {"core.tr01_cache_cleanup", "TR01 Cache Cleanup"},
189 {"core.expert", "Expert"},
190 {"core.expert_warning", "Caution"},
191 {"core.bootloader", "Bootloader"},
192 {"core.shipping_mode", "Shipping Mode"},
193 {"core.shipping_mode_q", "Enter shipping mode? Disconnects the battery."},
194 {"core.plugins", "Plugins"},
195 {"core.task_working", "Please wait"},
196 {"core.sleep", "Sleep"},
197 {"core.usb_replug_required","USB replug may be needed"},
198 {"core.usb_no_free_slot", "No free USB slot - disable a USB module (e.g. GPG) first"},
199 {"core.module_error_generic","Module error"},
200 {"core.module_retry_prompt","Reload module?"},
201
202 {"core.backup", "Backup"},
203 {"core.backup_export", "Export"},
204 {"core.backup_import", "Import"},
205 {"core.backup_delete", "Delete"},
206 {"core.backup_passphrase", "Passphrase"},
207 {"core.backup_confirm_passphrase", "Confirm passphrase"},
208 {"core.backup_pass_empty", "Passphrase required"},
209 {"core.backup_export_ok", "Backup saved"},
210 {"core.backup_none", "No backup present"},
211 {"core.backup_import_fail", "Wrong passphrase or corrupt file"},
212 {"core.backup_delete_q", "Delete backup?"},
213 {"core.backup_summary", "Restore Summary"},
214 {"core.backup_imported", "Imported"},
215 {"core.backup_failed", "Failed"},
216 {"core.backup_modules", "Modules"},
217 {"core.backup_skipped", "Skipped"},
218 {"core.backup_system", "System Settings"},
219 {"core.backup_scope_info", "No keys are backed up (FIDO2/GPG); only data."},
220
221 {"core.hw_section_memory", "Memory"},
222 {"core.hw_section_runtime", "Runtime"},
223 {"core.hw_i2c_bus", "I2C Bus"},
224 {"core.hw_bq25895", "BQ25895"},
225 {"core.hw_tca9535", "TCA9535"},
226 {"core.hw_display", "Display"},
227 {"core.hw_tropic01", "TROPIC01"},
228 {"core.hw_tr01_session", "TR01 Session"},
229 {"core.hw_tr01_riscv_fw", "TR01 RISC-V FW"},
230 {"core.hw_tr01_spect_fw", "TR01 SPECT FW"},
231 {"core.hw_tr01_rmem_slot", "TR01 R-Mem slot"},
232 {"core.hw_wifi", "WiFi"},
233 {"core.hw_ble", "BLE"},
234 {"core.hw_heap", "Heap"},
235 {"core.hw_psram", "PSRAM"},
236 {"core.hw_nvs", "NVS"},
237 {"core.hw_entries", "entries"},
238 {"core.hw_battery", "Battery"},
239 {"core.hw_temp", "Temp"},
240 {"core.hw_uptime", "Uptime"},
241 {"core.hw_cpu_load", "CPU Load"},
242 {"core.hw_charging_suffix", " (chg)"},
243 {"core.hw_not_available", "n/a"},
244
245 {"core.actions", "Actions"},
246 {"core.light", "Light"},
247
248 {"core.hint_back", "[N] Back"},
249 {"core.hint_select", "[Y] Select"},
250 {"core.hint_ok_back", "[Y] OK [N] Back"},
251 {"core.hint_approve_deny", "[Y] Approve [N] Deny"},
252 {"core.hint_brightness", "<4 6> Adjust [Y] Save"},
253 {"core.hint_pin_input", "[0-9] Input [Y] OK"},
254 {"core.hint_t9_input", "[0-9] T9 [Y] OK"},
255 {"core.t9_full", "Full"},
256 {"core.hint_list_menu", "[3] Menu"},
257 {"core.hint_scroll_back", "[2/8] Scroll [N] Back"},
258 {"core.hint_field_nav", "[4] < [6] >"},
259 {"core.hint_date_input", "[0-9] [Y] OK [N] Clear"},
260 {"core.hint_time_input", "[0-9] [Y] OK [N] Clear"},
261
262 {"core.wifi_connecting", "Connecting to"},
263 {"core.plugin_loading", "Loading"},
264 {"core.stop", "Stop"},
265 {"core.start", "Start"},
266 {"core.enable", "Enable"},
267 {"core.disable", "Disable"},
268 {"core.plugin_enabled", "Plugin enabled"},
269 {"core.plugin_disabled", "Plugin disabled"},
270 {"core.plugin_bg_running", "Runs in background"},
271 {"core.plugin_not_running", "Not running"},
272 {"core.hint_plugin_list", "[Y] Start [3] Menu [N] Back"},
273 {"core.hint_password_hidden", "[hold Y] Show [Y] Save"},
274 {"core.hint_password_revealed", "[hold Y] Hide [Y] Save"},
275
276 {"core.qr_error", "QR Error"},
277 {"core.no_data", "No data"},
278};
279
280constexpr std::size_t kCoreCount =
281 sizeof(kCoreStrings) / sizeof(kCoreStrings[0]);
282
283} // namespace
284
285I18n::I18n() = default;
286
288{
289 static I18n s;
290 return s;
291}
292
293void I18n::registerCoreEnglishTable()
294{
295 registerEnglishTable(kCoreStrings, kCoreCount);
296}
297
299{
300 registerCoreEnglishTable();
301 loadLanguageFromNvs();
302 LOG_I(TAG, "I18n initialized, lang=%s, core=%u entries",
303 currentLang_.c_str(), static_cast<unsigned>(kCoreCount));
304 return true;
305}
306
307void I18n::registerEnglishTable(const I18nEntry* entries, std::size_t count)
308{
309 if (!entries || count == 0) return;
310 en_.reserve(en_.size() + count);
311 en_.insert(en_.end(), entries, entries + count);
312 enSorted_ = false;
313}
314
315bool I18n::sortIfNeeded() const
316{
317 if (enSorted_) return true;
318 std::sort(en_.begin(), en_.end(),
319 [](const I18nEntry& a, const I18nEntry& b) {
320 return std::strcmp(a.key, b.key) < 0;
321 });
322 enSorted_ = true;
323 return true;
324}
325
326const char* I18n::enLookup(const char* key) const
327{
328 sortIfNeeded();
329 I18nEntry probe{key, nullptr};
330 auto it = std::lower_bound(en_.begin(), en_.end(), probe,
331 [](const I18nEntry& a, const I18nEntry& b) {
332 return std::strcmp(a.key, b.key) < 0;
333 });
334 if (it != en_.end() && std::strcmp(it->key, key) == 0) return it->en;
335 return nullptr;
336}
337
338const char* I18n::overlayLookup(const char* key) const
339{
340 if (overlayCount_ == 0 || !overlayRefs_) return nullptr;
341 const OverlayRef* base = overlayRefs_.get();
342 std::size_t lo = 0, hi = overlayCount_;
343 while (lo < hi) {
344 std::size_t mid = lo + (hi - lo) / 2;
345 int c = std::strcmp(base[mid].key, key);
346 if (c == 0) return base[mid].value;
347 if (c < 0) lo = mid + 1; else hi = mid;
348 }
349 return nullptr;
350}
351
352const char* I18n::overlayTr(const char* key) const
353{
354 if (!key || currentLang_ == "en") return nullptr;
355 return overlayLookup(key);
356}
357
358const char* I18n::tr(const char* key) const
359{
360 if (!key) return "";
361 if (currentLang_ != "en") {
362 if (const char* s = overlayLookup(key)) return s;
363 }
364 if (const char* s = enLookup(key)) return s;
365 static thread_local char missing[64];
366 std::snprintf(missing, sizeof(missing), "?%s", key);
367 return missing;
368}
369
370bool I18n::setLanguageCode(const char* code)
371{
372 std::string newLang = (code && *code) ? code : "en";
373 if (newLang == currentLang_) return true;
374 currentLang_ = std::move(newLang);
375
376 overlayBlob_.reset();
377 overlayRefs_.reset();
378 overlayCount_ = 0;
379 if (currentLang_ != "en") loadActiveOverlayFile();
380 if (onChanged_) onChanged_();
381
382 saveLanguageToNvs();
383 LOG_I(TAG, "Language changed to %s", currentLang_.c_str());
384 return true;
385}
386
387void I18n::scanAvailableLanguages()
388{
389 overlayLangs_.clear();
390 DIR* dir = opendir(OVERLAY_DIR);
391 if (!dir) return;
392
393 constexpr const char* kPrefix = "lang_";
394 constexpr size_t kPrefixLen = 5; // strlen("lang_")
395 constexpr size_t kSuffixLen = 5; // strlen(".json")
396
397 struct dirent* ent = nullptr;
398 while ((ent = readdir(dir)) != nullptr) {
399 const char* n = ent->d_name;
400 const size_t len = std::strlen(n);
401 if (len <= kPrefixLen + kSuffixLen) continue;
402 if (std::strncmp(n, kPrefix, kPrefixLen) != 0) continue;
403 if (std::strcmp(n + len - kSuffixLen, ".json") != 0) continue;
404
405 std::string code(n + kPrefixLen, len - kPrefixLen - kSuffixLen);
406 if (code.empty() || code == "en") continue; // English is in-code
407
408 // Default the display name to the code, then try to read the file's
409 // own `core.lang_name` endonym.
410 OverlayLanguage lang{code, code};
411 const std::string path = std::string(OVERLAY_DIR) + "/" + n;
412 if (auto fp = cdc::core::openFile(path.c_str(), "rb")) {
413 std::fseek(fp.get(), 0, SEEK_END);
414 const long size = std::ftell(fp.get());
415 std::fseek(fp.get(), 0, SEEK_SET);
416 if (size > 0 && size <= 1024 * 1024) {
417 auto buf = cdc::core::psramAlloc<char>(static_cast<std::size_t>(size) + 1);
418 if (buf && std::fread(buf.get(), 1, size, fp.get()) == static_cast<size_t>(size)) {
419 buf.get()[size] = '\0';
420 PsramCjsonScope cjson_psram;
421 if (cJSON* root = cJSON_Parse(buf.get())) {
422 cJSON* nm = cJSON_GetObjectItemCaseSensitive(root, "core.lang_name");
423 if (cJSON_IsString(nm) && nm->valuestring && *nm->valuestring) {
424 lang.name = cdc::core::cp437::fromUtf8(nm->valuestring);
425 }
426 cJSON_Delete(root);
427 }
428 }
429 }
430 }
431 overlayLangs_.push_back(std::move(lang));
432 }
433 closedir(dir);
434
435 std::sort(overlayLangs_.begin(), overlayLangs_.end(),
436 [](const OverlayLanguage& a, const OverlayLanguage& b) {
437 return a.code < b.code;
438 });
439}
440
441bool I18n::loadActiveOverlayFile()
442{
443 overlayBlob_.reset();
444 overlayRefs_.reset();
445 overlayCount_ = 0;
446
447 const std::string path =
448 std::string(OVERLAY_DIR) + "/lang_" + currentLang_ + ".json";
449
450 auto fp = cdc::core::openFile(path.c_str(), "rb");
451 if (!fp) {
452 LOG_W(TAG, "Overlay file not found: %s", path.c_str());
453 return false;
454 }
455
456 std::fseek(fp.get(), 0, SEEK_END);
457 long size = std::ftell(fp.get());
458 std::fseek(fp.get(), 0, SEEK_SET);
459 if (size <= 0 || size > 1024 * 1024) {
460 LOG_W(TAG, "Overlay file size invalid: %ld", size);
461 return false;
462 }
463
464 auto buf = cdc::core::psramAlloc<char>(static_cast<std::size_t>(size) + 1);
465 if (!buf) {
466 LOG_E(TAG, "Overlay PSRAM allocation failed (%ld bytes)", size);
467 return false;
468 }
469 if (std::fread(buf.get(), 1, size, fp.get()) != static_cast<size_t>(size)) {
470 LOG_E(TAG, "Overlay file read failed");
471 return false;
472 }
473 buf.get()[size] = '\0';
474
475 // Parse tree + final storage both live in PSRAM.
476 PsramCjsonScope cjson_psram;
477 cJSON* root = cJSON_Parse(buf.get());
478 if (!root || !cJSON_IsObject(root)) {
479 LOG_E(TAG, "Overlay JSON parse failed near: %s",
480 cJSON_GetErrorPtr() ? cJSON_GetErrorPtr() : "<unknown>");
481 if (root) cJSON_Delete(root);
482 return false;
483 }
484
485 // Pass 1: count string entries and the bytes needed for the packed blob
486 // (key + CP437-converted value, each null-terminated).
487 std::size_t count = 0;
488 std::size_t bytes = 0;
489 for (cJSON* e = root->child; e; e = e->next) {
490 if (!cJSON_IsString(e) || !e->string || !e->valuestring) continue;
491 ++count;
492 bytes += std::strlen(e->string) + 1;
493 bytes += cdc::core::cp437::fromUtf8(e->valuestring).size() + 1;
494 }
495 if (count == 0) { cJSON_Delete(root); return true; }
496
497 auto blob = cdc::core::psramAlloc<char>(bytes);
498 auto refs = cdc::core::psramAlloc<OverlayRef>(count);
499 if (!blob || !refs) {
500 LOG_E(TAG, "Overlay storage PSRAM allocation failed");
501 cJSON_Delete(root);
502 return false;
503 }
504
505 // Pass 2: pack into the PSRAM blob and record key/value pointers.
506 char* w = blob.get();
507 std::size_t idx = 0;
508 for (cJSON* e = root->child; e; e = e->next) {
509 if (!cJSON_IsString(e) || !e->string || !e->valuestring) continue;
510 const std::string val = cdc::core::cp437::fromUtf8(e->valuestring);
511 const std::size_t kl = std::strlen(e->string);
512 refs.get()[idx].key = w;
513 std::memcpy(w, e->string, kl + 1);
514 w += kl + 1;
515 refs.get()[idx].value = w;
516 std::memcpy(w, val.c_str(), val.size() + 1);
517 w += val.size() + 1;
518 ++idx;
519 }
520 cJSON_Delete(root);
521
522 std::sort(refs.get(), refs.get() + count,
523 [](const OverlayRef& a, const OverlayRef& b) {
524 return std::strcmp(a.key, b.key) < 0;
525 });
526
527 overlayBlob_ = std::move(blob);
528 overlayRefs_ = std::move(refs);
529 overlayCount_ = count;
530
531 LOG_I(TAG, "Overlay '%s' loaded: %u entries (PSRAM)",
532 currentLang_.c_str(), static_cast<unsigned>(count));
533 return true;
534}
535
537{
538 scanAvailableLanguages();
539
540 bool ok = true;
541 if (currentLang_ != "en") {
542 ok = loadActiveOverlayFile();
543 } else {
544 overlayBlob_.reset();
545 overlayRefs_.reset();
546 overlayCount_ = 0;
547 }
548
549 LOG_I(TAG, "i18n overlay: %u languages available, active='%s'",
550 static_cast<unsigned>(overlayLangs_.size()),
551 currentLang_.c_str());
552
553 if (onChanged_) onChanged_();
554 return ok;
555}
556
557const char* I18n::languageName(const char* code) const
558{
559 if (!code || !*code || std::strcmp(code, "en") == 0) {
560 const char* en = enLookup("core.lang_name");
561 return en ? en : "English";
562 }
563 for (const auto& l : overlayLangs_) {
564 if (l.code == code) return l.name.c_str();
565 }
566 return code;
567}
568
569void I18n::loadLanguageFromNvs()
570{
571 cdc::core::NvsScope nvs(NVS_NAMESPACE, NVS_READONLY);
572 if (!nvs) return;
573 size_t len = 0;
574 if (nvs_get_str(nvs, NVS_KEY_LANG_CODE, nullptr, &len) != ESP_OK || len == 0) return;
575 if (len > 8) len = 8;
576 char buf[9] = {};
577 if (nvs_get_str(nvs, NVS_KEY_LANG_CODE, buf, &len) == ESP_OK) {
578 currentLang_ = buf;
579 }
580}
581
582void I18n::saveLanguageToNvs()
583{
584 cdc::core::NvsScope nvs(NVS_NAMESPACE, NVS_READWRITE);
585 if (!nvs) return;
586 nvs_set_str(nvs, NVS_KEY_LANG_CODE, currentLang_.c_str());
587 nvs.commit();
588}
589
590} // namespace cdc::ui
Canonical CP437 <-> Unicode/UTF-8 codec.
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
RAII scope that routes cJSON allocations to PSRAM.
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
RAII wrapper for an NVS handle.
Definition Raii.h:106
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
const char * overlayTr(const char *key) const
Overlay-only lookup with no English fallback.
Definition I18n.cpp:352
bool setLanguageCode(const char *code)
Set the active language by code.
Definition I18n.cpp:370
bool loadOverlay()
Rescan available languages and (re)load the active overlay.
Definition I18n.cpp:536
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
Definition I18n.cpp:307
const char * languageName(const char *code) const
Display name (endonym) for a language code, for the picker.
Definition I18n.cpp:557
static constexpr const char * OVERLAY_DIR
Directory on the plugins FAT holding the per-language files.
Definition I18n.h:69
bool init()
Initialize and load persisted language code from NVS.
Definition I18n.cpp:298
const char * tr(const char *key) const
Look up a translation by key.
Definition I18n.cpp:358
#define NVS_NAMESPACE
#define APP_VERSION
std::string fromUtf8(const char *s)
Convert a UTF-8 string to CP437 bytes (unmapped chars dropped).
Definition Cp437.cpp:58
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
Definition Raii.h:51
Centralized key-code constants for cdc_views.
Definition IModule.h:8
static const char * TAG
Single English translation entry.
Definition I18n.h:44
Internationalization singleton.
Definition I18n.h:61
std::string name
Endonym for the picker (CP437-encoded), e.g. "Deutsch".
Definition I18n.h:63