feat: User-Interface I18n System (#728)

## Summary

**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.

**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.

- Data Structures:

- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
  - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
  - `lib/I18n/translations.csv`: single source of truth. 

- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.

- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`

## Additional Context

This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.

The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.

The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.

To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.

### Next Steps

- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.

Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!

**Status:** EDIT: ready to be merged.

As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
This commit is contained in:
Uri Tauber
2026-02-16 15:28:42 +02:00
committed by GitHub
parent 3d47c081f2
commit 7ba5978848
52 changed files with 4516 additions and 379 deletions

View File

@@ -4,6 +4,7 @@
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include "CrossPointSettings.h"
@@ -501,7 +502,7 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
// Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
@@ -540,7 +541,7 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
LOG_DBG("ERS", "Cache not found, building...");
const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); };
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
@@ -585,7 +586,7 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
if (section->pageCount == 0) {
LOG_DBG("ERS", "No pages to render");
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
@@ -593,7 +594,7 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
@@ -762,8 +763,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
std::string title;
int titleWidth;
if (tocIndex == -1) {
title = "Unnamed";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
title = tr(STR_UNNAMED);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} else {
const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title;

View File

@@ -1,6 +1,7 @@
#include "EpubReaderChapterSelectionActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "components/UITheme.h"
@@ -104,8 +105,8 @@ void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
// Manual centering to honor content gutters.
const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Chapter", EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Chapter", true, EpdFontFamily::BOLD);
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Highlight only the content area, not the hint gutters.
@@ -127,7 +128,7 @@ void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
}
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();

View File

@@ -1,6 +1,7 @@
#include "EpubReaderMenuActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "components/UITheme.h"
@@ -84,9 +85,10 @@ void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
// Progress summary
std::string progressLine;
if (totalPages > 0) {
progressLine = "Chapter: " + std::to_string(currentPage) + "/" + std::to_string(totalPages) + " pages | ";
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
}
progressLine += "Book: " + std::to_string(bookProgressPercent) + "%";
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
// Menu Items
@@ -102,18 +104,18 @@ void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true);
}
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, menuItems[i].label.c_str(), !isSelected);
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected);
if (menuItems[i].action == MenuAction::ROTATE_SCREEN) {
// Render current orientation value on the right edge of the content area.
const auto value = orientationLabels[pendingOrientation];
const char* value = I18N.get(orientationLabels[pendingOrientation]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
}
// Footer / Hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();

View File

@@ -1,5 +1,6 @@
#pragma once
#include <Epub.h>
#include <I18n.h>
#include <functional>
#include <string>
@@ -34,21 +35,24 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
private:
struct MenuItem {
MenuAction action;
std::string label;
StrId labelId;
};
// Fixed menu layout (order matters for up/down navigation).
const std::vector<MenuItem> menuItems = {
{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, {MenuAction::ROTATE_SCREEN, "Reading Orientation"},
{MenuAction::GO_TO_PERCENT, "Go to %"}, {MenuAction::GO_HOME, "Go Home"},
{MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}};
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER},
{MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT},
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON},
{MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
int selectedIndex = 0;
ButtonNavigator buttonNavigator;
std::string title = "Reader Menu";
uint8_t pendingOrientation = 0;
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
StrId::STR_LANDSCAPE_CCW};
int currentPage = 0;
int totalPages = 0;
int bookProgressPercent = 0;

View File

@@ -1,6 +1,7 @@
#include "EpubReaderPercentSelectionActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "components/UITheme.h"
@@ -59,7 +60,7 @@ void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
// Title and numeric percent value.
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Position", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_GO_TO_PERCENT), true, EpdFontFamily::BOLD);
const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_12_FONT_ID, 90, percentText.c_str(), true, EpdFontFamily::BOLD);
@@ -84,10 +85,10 @@ void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) {
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
// Hint text for step sizes.
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, "Left/Right: 1% Up/Down: 10%", true);
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, tr(STR_PERCENT_STEP_HINT), true);
// Button hints follow the current front button layout.
const auto labels = mappedInput.mapLabels("« Back", "Select", "-", "+");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();

View File

@@ -1,6 +1,7 @@
#include "KOReaderSyncActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <esp_sntp.h>
@@ -54,7 +55,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
{
RenderLock lock(*this);
state = SYNCING;
statusMessage = "Syncing time...";
statusMessage = tr(STR_SYNCING_TIME);
}
requestUpdate();
@@ -63,7 +64,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
{
RenderLock lock(*this);
statusMessage = "Calculating document hash...";
statusMessage = tr(STR_CALC_HASH);
}
requestUpdate();
@@ -81,7 +82,7 @@ void KOReaderSyncActivity::performSync() {
{
RenderLock lock(*this);
state = SYNC_FAILED;
statusMessage = "Failed to calculate document hash";
statusMessage = tr(STR_HASH_FAILED);
}
requestUpdate();
return;
@@ -91,7 +92,7 @@ void KOReaderSyncActivity::performSync() {
{
RenderLock lock(*this);
statusMessage = "Fetching remote progress...";
statusMessage = tr(STR_FETCH_PROGRESS);
}
requestUpdateAndWait();
@@ -140,7 +141,7 @@ void KOReaderSyncActivity::performUpload() {
{
RenderLock lock(*this);
state = UPLOADING;
statusMessage = "Uploading progress...";
statusMessage = tr(STR_UPLOAD_PROGRESS);
}
requestUpdate();
requestUpdateAndWait();
@@ -191,7 +192,7 @@ void KOReaderSyncActivity::onEnter() {
if (WiFi.status() == WL_CONNECTED) {
LOG_DBG("KOSync", "Already connected to WiFi");
state = SYNCING;
statusMessage = "Syncing time...";
statusMessage = tr(STR_SYNCING_TIME);
requestUpdate();
// Perform sync directly (will be handled in loop)
@@ -202,7 +203,7 @@ void KOReaderSyncActivity::onEnter() {
syncTimeWithNTP();
{
RenderLock lock(*self);
self->statusMessage = "Calculating document hash...";
self->statusMessage = tr(STR_CALC_HASH);
}
self->requestUpdate();
self->performSync();
@@ -236,13 +237,13 @@ void KOReaderSyncActivity::render(Activity::RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
if (state == NO_CREDENTIALS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No credentials configured", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Set up KOReader account in Settings");
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_CREDENTIALS_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_KOREADER_SETUP_HINT));
const auto labels = mappedInput.mapLabels("Back", "", "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
@@ -256,40 +257,41 @@ void KOReaderSyncActivity::render(Activity::RenderLock&&) {
if (state == SHOWING_RESULT) {
// Show comparison
renderer.drawCenteredText(UI_10_FONT_ID, 120, "Progress found!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD);
// Get chapter names from TOC
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
const std::string remoteChapter = (remoteTocIndex >= 0)
? epub->getTocItem(remoteTocIndex).title
: ("Section " + std::to_string(remotePosition.spineIndex + 1));
const std::string localChapter = (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
: ("Section " + std::to_string(currentSpineIndex + 1));
const std::string remoteChapter =
(remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
const std::string localChapter =
(localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
// Remote progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 160, "Remote:", true);
renderer.drawText(UI_10_FONT_ID, 20, 160, tr(STR_REMOTE_LABEL), true);
char remoteChapterStr[128];
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr);
char remotePageStr[64];
snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1,
snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1,
remoteProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr);
if (!remoteProgress.device.empty()) {
char deviceStr[64];
snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str());
snprintf(deviceStr, sizeof(deviceStr), tr(STR_DEVICE_FROM_FORMAT), remoteProgress.device.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr);
}
// Local progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 270, "Local:", true);
renderer.drawText(UI_10_FONT_ID, 20, 270, tr(STR_LOCAL_LABEL), true);
char localChapterStr[128];
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr);
char localPageStr[64];
snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine,
snprintf(localPageStr, sizeof(localPageStr), tr(STR_PAGE_TOTAL_OVERALL_FORMAT), currentPage + 1, totalPagesInSpine,
localProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
@@ -300,45 +302,45 @@ void KOReaderSyncActivity::render(Activity::RenderLock&&) {
if (selectedOption == 0) {
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
}
renderer.drawText(UI_10_FONT_ID, 20, optionY, "Apply remote progress", selectedOption != 0);
renderer.drawText(UI_10_FONT_ID, 20, optionY, tr(STR_APPLY_REMOTE), selectedOption != 0);
// Upload option
if (selectedOption == 1) {
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
}
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1);
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1);
// Bottom button hints: show Back and Select
const auto labels = mappedInput.mapLabels("Back", "Select", "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == NO_REMOTE_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?");
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_REMOTE_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_UPLOAD_PROMPT));
const auto labels = mappedInput.mapLabels("Back", "Upload", "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_UPLOAD), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == UPLOAD_COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Progress uploaded!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels("Back", "", "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == SYNC_FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Sync failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;

View File

@@ -2,6 +2,7 @@
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Serialization.h>
#include <Utf8.h>
@@ -182,7 +183,7 @@ void TxtReaderActivity::buildPageIndex() {
LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize);
GUI.drawPopup(renderer, "Indexing...");
GUI.drawPopup(renderer, tr(STR_INDEXING));
while (offset < fileSize) {
std::vector<std::string> tempLines;
@@ -350,7 +351,7 @@ void TxtReaderActivity::render(Activity::RenderLock&&) {
if (pageOffsets.empty()) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty file", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_FILE), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}

View File

@@ -10,6 +10,7 @@
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
@@ -143,7 +144,7 @@ void XtcReaderActivity::render(Activity::RenderLock&&) {
if (currentPage >= xtc->getPageCount()) {
// Show end of book screen
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
@@ -172,7 +173,7 @@ void XtcReaderActivity::renderPage() {
if (!pageBuffer) {
LOG_ERR("XTR", "Failed to allocate page buffer (%lu bytes)", pageBufferSize);
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_MEMORY_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
@@ -183,7 +184,7 @@ void XtcReaderActivity::renderPage() {
LOG_ERR("XTR", "Failed to load page %lu", currentPage);
free(pageBuffer);
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}

View File

@@ -1,6 +1,7 @@
#include "XtcReaderChapterSelectionActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
@@ -104,14 +105,14 @@ void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
const int pageItems = getPageItems();
// Manual centering to honor content gutters.
const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Select Chapter", EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Select Chapter", true, EpdFontFamily::BOLD);
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD);
const auto& chapters = xtc->getChapters();
if (chapters.empty()) {
// Center the empty state within the gutter-safe content region.
const int emptyX = contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, "No chapters")) / 2;
renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, "No chapters");
const int emptyX = contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, tr(STR_NO_CHAPTERS))) / 2;
renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, tr(STR_NO_CHAPTERS));
renderer.displayBuffer();
return;
}
@@ -121,13 +122,13 @@ void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30);
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
const auto& chapter = chapters[i];
const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str();
const char* title = chapter.name.empty() ? tr(STR_UNNAMED) : chapter.name.c_str();
renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex);
}
// Skip button hints in landscape CW mode (they overlap content)
if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) {
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}