**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` 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`. - [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. --- 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>
137 lines
5.6 KiB
C++
137 lines
5.6 KiB
C++
#include "EpubReaderMenuActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <I18n.h>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
void EpubReaderMenuActivity::onEnter() {
|
|
ActivityWithSubactivity::onEnter();
|
|
requestUpdate();
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); }
|
|
|
|
void EpubReaderMenuActivity::loop() {
|
|
if (subActivity) {
|
|
subActivity->loop();
|
|
return;
|
|
}
|
|
|
|
// Handle navigation
|
|
buttonNavigator.onNext([this] {
|
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPrevious([this] {
|
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
// Use local variables for items we need to check after potential deletion
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
const auto selectedAction = menuItems[selectedIndex].action;
|
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
|
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
|
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
if (selectedAction == MenuAction::LETTERBOX_FILL) {
|
|
// Cycle through: Default -> Dithered -> Solid -> None -> Default ...
|
|
int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT;
|
|
pendingLetterboxFill = indexToLetterboxFill(idx);
|
|
saveLetterboxFill();
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
// 1. Capture the callback and action locally
|
|
auto actionCallback = onAction;
|
|
|
|
// 2. Execute the callback
|
|
actionCallback(selectedAction);
|
|
|
|
// 3. CRITICAL: Return immediately. 'this' is likely deleted now.
|
|
return;
|
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
// Return the pending orientation to the parent so it can apply on exit.
|
|
onBack(pendingOrientation);
|
|
return; // Also return here just in case
|
|
}
|
|
}
|
|
|
|
void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
|
|
renderer.clearScreen();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto orientation = renderer.getOrientation();
|
|
// Landscape orientation: button hints are drawn along a vertical edge, so we
|
|
// reserve a horizontal gutter to prevent overlap with menu content.
|
|
const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise;
|
|
const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise;
|
|
// Inverted portrait: button hints appear near the logical top, so we reserve
|
|
// vertical space to keep the header and list clear.
|
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
|
const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0;
|
|
// Landscape CW places hints on the left edge; CCW keeps them on the right.
|
|
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
|
|
const int contentWidth = pageWidth - hintGutterWidth;
|
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
|
const int contentY = hintGutterHeight;
|
|
|
|
// Title
|
|
const std::string truncTitle =
|
|
renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD);
|
|
// Manual centering so we can respect the content gutter.
|
|
const int titleX =
|
|
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2;
|
|
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD);
|
|
|
|
// Progress summary
|
|
std::string progressLine;
|
|
if (totalPages > 0) {
|
|
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
|
|
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
|
|
}
|
|
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
|
|
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
|
|
|
|
// Menu Items
|
|
const int startY = 75 + contentY;
|
|
constexpr int lineHeight = 30;
|
|
|
|
for (size_t i = 0; i < menuItems.size(); ++i) {
|
|
const int displayY = startY + (i * lineHeight);
|
|
const bool isSelected = (static_cast<int>(i) == selectedIndex);
|
|
|
|
if (isSelected) {
|
|
// Highlight only the content area so we don't paint over hint gutters.
|
|
renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true);
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
|
|
// Render current letterbox fill value on the right edge of the content area.
|
|
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
|
|
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(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();
|
|
}
|