Merge remote-tracking branch 'pablohc/refactor/pr-1258-cover-overlay' into mybuild
# Conflicts: # lib/I18n/translations/english.yaml # scripts/gen_i18n.py # src/CrossPointSettings.h # src/SettingsList.h # src/activities/boot_sleep/SleepActivity.cpp # src/activities/boot_sleep/SleepActivity.h
This commit is contained in:
@@ -0,0 +1,416 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Forward declaration for string arrays
|
||||||
|
namespace i18n_strings {
|
||||||
|
extern const char* const STRINGS_EN[];
|
||||||
|
extern const char* const STRINGS_ES[];
|
||||||
|
extern const char* const STRINGS_FR[];
|
||||||
|
extern const char* const STRINGS_DE[];
|
||||||
|
extern const char* const STRINGS_CS[];
|
||||||
|
extern const char* const STRINGS_PO[];
|
||||||
|
extern const char* const STRINGS_RU[];
|
||||||
|
extern const char* const STRINGS_SV[];
|
||||||
|
extern const char* const STRINGS_RO[];
|
||||||
|
extern const char* const STRINGS_CA[];
|
||||||
|
extern const char* const STRINGS_UK[];
|
||||||
|
extern const char* const STRINGS_BE[];
|
||||||
|
extern const char* const STRINGS_IT[];
|
||||||
|
extern const char* const STRINGS_PL[];
|
||||||
|
extern const char* const STRINGS_FI[];
|
||||||
|
extern const char* const STRINGS_DA[];
|
||||||
|
extern const char* const STRINGS_NL[];
|
||||||
|
extern const char* const STRINGS_TR[];
|
||||||
|
extern const char* const STRINGS_KK[];
|
||||||
|
} // namespace i18n_strings
|
||||||
|
|
||||||
|
// Language enum
|
||||||
|
enum class Language : uint8_t {
|
||||||
|
EN = 0,
|
||||||
|
ES = 1,
|
||||||
|
FR = 2,
|
||||||
|
DE = 3,
|
||||||
|
CS = 4,
|
||||||
|
PT = 5,
|
||||||
|
RU = 6,
|
||||||
|
SV = 7,
|
||||||
|
RO = 8,
|
||||||
|
CA = 9,
|
||||||
|
UK = 10,
|
||||||
|
BE = 11,
|
||||||
|
IT = 12,
|
||||||
|
PL = 13,
|
||||||
|
FI = 14,
|
||||||
|
DA = 15,
|
||||||
|
NL = 16,
|
||||||
|
TR = 17,
|
||||||
|
KK = 18,
|
||||||
|
_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
// Language display names (defined in I18nStrings.cpp)
|
||||||
|
extern const char* const LANGUAGE_NAMES[];
|
||||||
|
|
||||||
|
// Character sets for each language (defined in I18nStrings.cpp)
|
||||||
|
extern const char* const CHARACTER_SETS[];
|
||||||
|
|
||||||
|
// String IDs
|
||||||
|
enum class StrId : uint16_t {
|
||||||
|
STR_CROSSPOINT,
|
||||||
|
STR_BOOTING,
|
||||||
|
STR_SLEEPING,
|
||||||
|
STR_ENTERING_SLEEP,
|
||||||
|
STR_BROWSE_FILES,
|
||||||
|
STR_FILE_TRANSFER,
|
||||||
|
STR_SETTINGS_TITLE,
|
||||||
|
STR_CONTINUE_READING,
|
||||||
|
STR_NO_OPEN_BOOK,
|
||||||
|
STR_START_READING,
|
||||||
|
STR_NO_FILES_FOUND,
|
||||||
|
STR_SELECT_CHAPTER,
|
||||||
|
STR_NO_CHAPTERS,
|
||||||
|
STR_END_OF_BOOK,
|
||||||
|
STR_EMPTY_CHAPTER,
|
||||||
|
STR_INDEXING,
|
||||||
|
STR_MEMORY_ERROR,
|
||||||
|
STR_PAGE_LOAD_ERROR,
|
||||||
|
STR_EMPTY_FILE,
|
||||||
|
STR_OUT_OF_BOUNDS,
|
||||||
|
STR_LOADING,
|
||||||
|
STR_LOADING_POPUP,
|
||||||
|
STR_WIFI_NETWORKS,
|
||||||
|
STR_NO_NETWORKS,
|
||||||
|
STR_NETWORKS_FOUND,
|
||||||
|
STR_SCANNING,
|
||||||
|
STR_CONNECTING,
|
||||||
|
STR_CONNECTED,
|
||||||
|
STR_CONNECTION_FAILED,
|
||||||
|
STR_FORGET_NETWORK,
|
||||||
|
STR_SAVE_PASSWORD,
|
||||||
|
STR_PRESS_OK_SCAN,
|
||||||
|
STR_JOIN_NETWORK,
|
||||||
|
STR_CREATE_HOTSPOT,
|
||||||
|
STR_JOIN_DESC,
|
||||||
|
STR_HOTSPOT_DESC,
|
||||||
|
STR_STARTING_HOTSPOT,
|
||||||
|
STR_HOTSPOT_MODE,
|
||||||
|
STR_CONNECT_WIFI_HINT,
|
||||||
|
STR_OPEN_URL_HINT,
|
||||||
|
STR_OR_HTTP_PREFIX,
|
||||||
|
STR_SCAN_QR_HINT,
|
||||||
|
STR_CALIBRE_WIRELESS,
|
||||||
|
STR_CALIBRE_WEB_URL,
|
||||||
|
STR_NETWORK_LEGEND,
|
||||||
|
STR_MAC_ADDRESS,
|
||||||
|
STR_CHECKING_WIFI,
|
||||||
|
STR_ENTER_WIFI_PASSWORD,
|
||||||
|
STR_TO_PREFIX,
|
||||||
|
STR_CALIBRE_RECEIVING,
|
||||||
|
STR_CALIBRE_RECEIVED,
|
||||||
|
STR_CALIBRE_INSTRUCTION_1,
|
||||||
|
STR_CALIBRE_INSTRUCTION_2,
|
||||||
|
STR_CALIBRE_INSTRUCTION_3,
|
||||||
|
STR_CALIBRE_INSTRUCTION_4,
|
||||||
|
STR_CAT_DISPLAY,
|
||||||
|
STR_CAT_READER,
|
||||||
|
STR_CAT_CONTROLS,
|
||||||
|
STR_CAT_SYSTEM,
|
||||||
|
STR_SLEEP_SCREEN,
|
||||||
|
STR_SLEEP_COVER_MODE,
|
||||||
|
STR_HIDE_BATTERY,
|
||||||
|
STR_EXTRA_SPACING,
|
||||||
|
STR_TEXT_AA,
|
||||||
|
STR_IMAGES,
|
||||||
|
STR_IMAGES_DISPLAY,
|
||||||
|
STR_IMAGES_PLACEHOLDER,
|
||||||
|
STR_IMAGES_SUPPRESS,
|
||||||
|
STR_SHORT_PWR_BTN,
|
||||||
|
STR_ORIENTATION,
|
||||||
|
STR_SIDE_BTN_LAYOUT,
|
||||||
|
STR_LONG_PRESS_SKIP,
|
||||||
|
STR_FONT_FAMILY,
|
||||||
|
STR_FONT_SIZE,
|
||||||
|
STR_LINE_SPACING,
|
||||||
|
STR_SCREEN_MARGIN,
|
||||||
|
STR_PARA_ALIGNMENT,
|
||||||
|
STR_HYPHENATION,
|
||||||
|
STR_TIME_TO_SLEEP,
|
||||||
|
STR_SHOW_HIDDEN_FILES,
|
||||||
|
STR_REFRESH_FREQ,
|
||||||
|
STR_KOREADER_SYNC,
|
||||||
|
STR_CHECK_UPDATES,
|
||||||
|
STR_LANGUAGE,
|
||||||
|
STR_CLEAR_READING_CACHE,
|
||||||
|
STR_USERNAME,
|
||||||
|
STR_PASSWORD,
|
||||||
|
STR_SYNC_SERVER_URL,
|
||||||
|
STR_DOCUMENT_MATCHING,
|
||||||
|
STR_AUTHENTICATE,
|
||||||
|
STR_KOREADER_USERNAME,
|
||||||
|
STR_KOREADER_PASSWORD,
|
||||||
|
STR_FILENAME,
|
||||||
|
STR_BINARY,
|
||||||
|
STR_SET_CREDENTIALS_FIRST,
|
||||||
|
STR_WIFI_CONN_FAILED,
|
||||||
|
STR_AUTHENTICATING,
|
||||||
|
STR_AUTH_SUCCESS,
|
||||||
|
STR_KOREADER_AUTH,
|
||||||
|
STR_SYNC_READY,
|
||||||
|
STR_AUTH_FAILED,
|
||||||
|
STR_DONE,
|
||||||
|
STR_CLEAR_CACHE_WARNING_1,
|
||||||
|
STR_CLEAR_CACHE_WARNING_2,
|
||||||
|
STR_CLEAR_CACHE_WARNING_3,
|
||||||
|
STR_CLEAR_CACHE_WARNING_4,
|
||||||
|
STR_CLEARING_CACHE,
|
||||||
|
STR_CACHE_CLEARED,
|
||||||
|
STR_ITEMS_REMOVED,
|
||||||
|
STR_FAILED_LOWER,
|
||||||
|
STR_CLEAR_CACHE_FAILED,
|
||||||
|
STR_CHECK_SERIAL_OUTPUT,
|
||||||
|
STR_DARK,
|
||||||
|
STR_LIGHT,
|
||||||
|
STR_CUSTOM,
|
||||||
|
STR_COVER,
|
||||||
|
STR_NONE_OPT,
|
||||||
|
STR_FIT,
|
||||||
|
STR_CROP,
|
||||||
|
STR_NEVER,
|
||||||
|
STR_IN_READER,
|
||||||
|
STR_ALWAYS,
|
||||||
|
STR_IGNORE,
|
||||||
|
STR_SLEEP,
|
||||||
|
STR_PAGE_TURN,
|
||||||
|
STR_PORTRAIT,
|
||||||
|
STR_LANDSCAPE_CW,
|
||||||
|
STR_INVERTED,
|
||||||
|
STR_LANDSCAPE_CCW,
|
||||||
|
STR_PREV_NEXT,
|
||||||
|
STR_NEXT_PREV,
|
||||||
|
STR_BOOKERLY,
|
||||||
|
STR_NOTO_SANS,
|
||||||
|
STR_OPEN_DYSLEXIC,
|
||||||
|
STR_SMALL,
|
||||||
|
STR_MEDIUM,
|
||||||
|
STR_LARGE,
|
||||||
|
STR_X_LARGE,
|
||||||
|
STR_TIGHT,
|
||||||
|
STR_NORMAL,
|
||||||
|
STR_WIDE,
|
||||||
|
STR_JUSTIFY,
|
||||||
|
STR_ALIGN_LEFT,
|
||||||
|
STR_CENTER,
|
||||||
|
STR_ALIGN_RIGHT,
|
||||||
|
STR_MIN_1,
|
||||||
|
STR_MIN_5,
|
||||||
|
STR_MIN_10,
|
||||||
|
STR_MIN_15,
|
||||||
|
STR_MIN_30,
|
||||||
|
STR_PAGES_1,
|
||||||
|
STR_PAGES_5,
|
||||||
|
STR_PAGES_10,
|
||||||
|
STR_PAGES_15,
|
||||||
|
STR_PAGES_30,
|
||||||
|
STR_UPDATE,
|
||||||
|
STR_CHECKING_UPDATE,
|
||||||
|
STR_NEW_UPDATE,
|
||||||
|
STR_CURRENT_VERSION,
|
||||||
|
STR_NEW_VERSION,
|
||||||
|
STR_UPDATING,
|
||||||
|
STR_NO_UPDATE,
|
||||||
|
STR_UPDATE_FAILED,
|
||||||
|
STR_UPDATE_COMPLETE,
|
||||||
|
STR_POWER_ON_HINT,
|
||||||
|
STR_NO_ENTRIES,
|
||||||
|
STR_DOWNLOADING,
|
||||||
|
STR_DOWNLOAD_FAILED,
|
||||||
|
STR_ERROR_MSG,
|
||||||
|
STR_UNNAMED,
|
||||||
|
STR_NO_SERVER_URL,
|
||||||
|
STR_FETCH_FEED_FAILED,
|
||||||
|
STR_PARSE_FEED_FAILED,
|
||||||
|
STR_NETWORK_PREFIX,
|
||||||
|
STR_IP_ADDRESS_PREFIX,
|
||||||
|
STR_ERROR_GENERAL_FAILURE,
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND,
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT,
|
||||||
|
STR_SD_CARD,
|
||||||
|
STR_BACK,
|
||||||
|
STR_EXIT,
|
||||||
|
STR_HOME,
|
||||||
|
STR_SELECT,
|
||||||
|
STR_SELECTED,
|
||||||
|
STR_TOGGLE,
|
||||||
|
STR_CONFIRM,
|
||||||
|
STR_CANCEL,
|
||||||
|
STR_CONNECT,
|
||||||
|
STR_OPEN,
|
||||||
|
STR_DOWNLOAD,
|
||||||
|
STR_RETRY,
|
||||||
|
STR_YES,
|
||||||
|
STR_NO,
|
||||||
|
STR_SHOW,
|
||||||
|
STR_HIDE,
|
||||||
|
STR_STATE_ON,
|
||||||
|
STR_STATE_OFF,
|
||||||
|
STR_NOT_SET,
|
||||||
|
STR_DIR_LEFT,
|
||||||
|
STR_DIR_RIGHT,
|
||||||
|
STR_DIR_UP,
|
||||||
|
STR_DIR_DOWN,
|
||||||
|
STR_OK_BUTTON,
|
||||||
|
STR_SLEEP_COVER_FILTER,
|
||||||
|
STR_FILTER_CONTRAST,
|
||||||
|
STR_CUSTOMISE_STATUS_BAR,
|
||||||
|
STR_CHAPTER_PAGE_COUNT,
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE,
|
||||||
|
STR_PROGRESS_BAR,
|
||||||
|
STR_PROGRESS_BAR_THICKNESS,
|
||||||
|
STR_PROGRESS_BAR_THIN,
|
||||||
|
STR_PROGRESS_BAR_MEDIUM,
|
||||||
|
STR_PROGRESS_BAR_THICK,
|
||||||
|
STR_BOOK,
|
||||||
|
STR_CHAPTER,
|
||||||
|
STR_EXAMPLE_CHAPTER,
|
||||||
|
STR_EXAMPLE_BOOK,
|
||||||
|
STR_PREVIEW,
|
||||||
|
STR_TITLE,
|
||||||
|
STR_BATTERY,
|
||||||
|
STR_UI_THEME,
|
||||||
|
STR_THEME_CLASSIC,
|
||||||
|
STR_THEME_LYRA,
|
||||||
|
STR_THEME_LYRA_EXTENDED,
|
||||||
|
STR_SUNLIGHT_FADING_FIX,
|
||||||
|
STR_REMAP_FRONT_BUTTONS,
|
||||||
|
STR_OPDS_BROWSER,
|
||||||
|
STR_COVER_CUSTOM,
|
||||||
|
STR_MENU_RECENT_BOOKS,
|
||||||
|
STR_NO_RECENT_BOOKS,
|
||||||
|
STR_CALIBRE_DESC,
|
||||||
|
STR_FORGET_AND_REMOVE,
|
||||||
|
STR_FORGET_BUTTON,
|
||||||
|
STR_CALIBRE_STARTING,
|
||||||
|
STR_CALIBRE_SETUP,
|
||||||
|
STR_CALIBRE_STATUS,
|
||||||
|
STR_CLEAR_BUTTON,
|
||||||
|
STR_DEFAULT_VALUE,
|
||||||
|
STR_REMAP_PROMPT,
|
||||||
|
STR_UNASSIGNED,
|
||||||
|
STR_ALREADY_ASSIGNED,
|
||||||
|
STR_REMAP_RESET_HINT,
|
||||||
|
STR_REMAP_CANCEL_HINT,
|
||||||
|
STR_HW_BACK_LABEL,
|
||||||
|
STR_HW_CONFIRM_LABEL,
|
||||||
|
STR_HW_LEFT_LABEL,
|
||||||
|
STR_HW_RIGHT_LABEL,
|
||||||
|
STR_GO_TO_PERCENT,
|
||||||
|
STR_GO_HOME_BUTTON,
|
||||||
|
STR_SYNC_PROGRESS,
|
||||||
|
STR_DELETE_CACHE,
|
||||||
|
STR_DELETE,
|
||||||
|
STR_DISPLAY_QR,
|
||||||
|
STR_CHAPTER_PREFIX,
|
||||||
|
STR_PAGES_SEPARATOR,
|
||||||
|
STR_BOOK_PREFIX,
|
||||||
|
STR_CALIBRE_URL_HINT,
|
||||||
|
STR_PERCENT_STEP_HINT,
|
||||||
|
STR_SYNCING_TIME,
|
||||||
|
STR_CALC_HASH,
|
||||||
|
STR_HASH_FAILED,
|
||||||
|
STR_FETCH_PROGRESS,
|
||||||
|
STR_UPLOAD_PROGRESS,
|
||||||
|
STR_NO_CREDENTIALS_MSG,
|
||||||
|
STR_KOREADER_SETUP_HINT,
|
||||||
|
STR_PROGRESS_FOUND,
|
||||||
|
STR_REMOTE_LABEL,
|
||||||
|
STR_LOCAL_LABEL,
|
||||||
|
STR_PAGE_OVERALL_FORMAT,
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT,
|
||||||
|
STR_DEVICE_FROM_FORMAT,
|
||||||
|
STR_APPLY_REMOTE,
|
||||||
|
STR_UPLOAD_LOCAL,
|
||||||
|
STR_NO_REMOTE_MSG,
|
||||||
|
STR_UPLOAD_PROMPT,
|
||||||
|
STR_UPLOAD_SUCCESS,
|
||||||
|
STR_SYNC_FAILED_MSG,
|
||||||
|
STR_SECTION_PREFIX,
|
||||||
|
STR_UPLOAD,
|
||||||
|
STR_BOOK_S_STYLE,
|
||||||
|
STR_EMBEDDED_STYLE,
|
||||||
|
STR_OPDS_SERVER_URL,
|
||||||
|
STR_FOOTNOTES,
|
||||||
|
STR_NO_FOOTNOTES,
|
||||||
|
STR_LINK,
|
||||||
|
STR_SCREENSHOT_BUTTON,
|
||||||
|
STR_AUTO_TURN_ENABLED,
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN,
|
||||||
|
STR_SLEEP_COVER_OVERLAY,
|
||||||
|
STR_OVERLAY_WHITE,
|
||||||
|
STR_OVERLAY_GRAY,
|
||||||
|
STR_OVERLAY_BLACK,
|
||||||
|
STR_OVERLAY_OFF,
|
||||||
|
STR_OVERLAY_READING_PROGRESS,
|
||||||
|
STR_OVERLAY_READING_PROGRESS_NO_TOTAL,
|
||||||
|
STR_OVERLAY_CHAPTER_PAGE_SUFFIX,
|
||||||
|
// Sentinel - must be last
|
||||||
|
_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get string array for a language
|
||||||
|
inline const char* const* getStringArray(Language lang) {
|
||||||
|
switch (lang) {
|
||||||
|
case Language::EN:
|
||||||
|
return i18n_strings::STRINGS_EN;
|
||||||
|
case Language::ES:
|
||||||
|
return i18n_strings::STRINGS_ES;
|
||||||
|
case Language::FR:
|
||||||
|
return i18n_strings::STRINGS_FR;
|
||||||
|
case Language::DE:
|
||||||
|
return i18n_strings::STRINGS_DE;
|
||||||
|
case Language::CS:
|
||||||
|
return i18n_strings::STRINGS_CS;
|
||||||
|
case Language::PT:
|
||||||
|
return i18n_strings::STRINGS_PO;
|
||||||
|
case Language::RU:
|
||||||
|
return i18n_strings::STRINGS_RU;
|
||||||
|
case Language::SV:
|
||||||
|
return i18n_strings::STRINGS_SV;
|
||||||
|
case Language::RO:
|
||||||
|
return i18n_strings::STRINGS_RO;
|
||||||
|
case Language::CA:
|
||||||
|
return i18n_strings::STRINGS_CA;
|
||||||
|
case Language::UK:
|
||||||
|
return i18n_strings::STRINGS_UK;
|
||||||
|
case Language::BE:
|
||||||
|
return i18n_strings::STRINGS_BE;
|
||||||
|
case Language::IT:
|
||||||
|
return i18n_strings::STRINGS_IT;
|
||||||
|
case Language::PL:
|
||||||
|
return i18n_strings::STRINGS_PL;
|
||||||
|
case Language::FI:
|
||||||
|
return i18n_strings::STRINGS_FI;
|
||||||
|
case Language::DA:
|
||||||
|
return i18n_strings::STRINGS_DA;
|
||||||
|
case Language::NL:
|
||||||
|
return i18n_strings::STRINGS_NL;
|
||||||
|
case Language::TR:
|
||||||
|
return i18n_strings::STRINGS_TR;
|
||||||
|
case Language::KK:
|
||||||
|
return i18n_strings::STRINGS_KK;
|
||||||
|
default:
|
||||||
|
return i18n_strings::STRINGS_EN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get language count
|
||||||
|
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }
|
||||||
|
|
||||||
|
// Sorted language indices by code (auto-generated by gen_i18n.py)
|
||||||
|
// Order: English, Беларуская, Català, Čeština, Dansk, Deutsch, Español, Suomi, Français, Italiano
|
||||||
|
// Қазақша, Nederlands, Polski, Português (Brasil), Română, Русский, Svenska, Türkçe, Українська
|
||||||
|
constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {0, 11, 9, 4, 15, 3, 1, 14, 2, 12, 18, 16, 13, 5, 8, 6, 7, 17, 10};
|
||||||
|
|
||||||
|
static_assert(sizeof(SORTED_LANGUAGE_INDICES) / sizeof(SORTED_LANGUAGE_INDICES[0]) == getLanguageCount(),
|
||||||
|
"SORTED_LANGUAGE_INDICES size mismatch");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "I18nKeys.h"
|
||||||
|
|
||||||
|
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||||
|
|
||||||
|
namespace i18n_strings {
|
||||||
|
|
||||||
|
extern const char* const STRINGS_EN[];
|
||||||
|
extern const char* const STRINGS_ES[];
|
||||||
|
extern const char* const STRINGS_FR[];
|
||||||
|
extern const char* const STRINGS_DE[];
|
||||||
|
extern const char* const STRINGS_CS[];
|
||||||
|
extern const char* const STRINGS_PO[];
|
||||||
|
extern const char* const STRINGS_RU[];
|
||||||
|
extern const char* const STRINGS_SV[];
|
||||||
|
extern const char* const STRINGS_RO[];
|
||||||
|
extern const char* const STRINGS_CA[];
|
||||||
|
extern const char* const STRINGS_UK[];
|
||||||
|
extern const char* const STRINGS_BE[];
|
||||||
|
extern const char* const STRINGS_IT[];
|
||||||
|
extern const char* const STRINGS_PL[];
|
||||||
|
extern const char* const STRINGS_FI[];
|
||||||
|
extern const char* const STRINGS_DA[];
|
||||||
|
extern const char* const STRINGS_NL[];
|
||||||
|
extern const char* const STRINGS_TR[];
|
||||||
|
extern const char* const STRINGS_KK[];
|
||||||
|
|
||||||
|
} // namespace i18n_strings
|
||||||
@@ -343,6 +343,13 @@ STR_AUTHOR: "Author"
|
|||||||
STR_SERIES: "Series"
|
STR_SERIES: "Series"
|
||||||
STR_FILE_SIZE: "Size"
|
STR_FILE_SIZE: "Size"
|
||||||
STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
|
STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
|
||||||
|
STR_OVERLAY_WHITE: "White"
|
||||||
|
STR_OVERLAY_GRAY: "Gray"
|
||||||
|
STR_OVERLAY_BLACK: "Black"
|
||||||
|
STR_OVERLAY_OFF: "Off"
|
||||||
|
STR_OVERLAY_READING_PROGRESS: "Reading progress: Page %lu/%u - %.0f%%"
|
||||||
|
STR_OVERLAY_READING_PROGRESS_NO_TOTAL: "Reading progress: Page %lu"
|
||||||
|
STR_OVERLAY_CHAPTER_PAGE_SUFFIX: " - Page %d/%d - %.0f%% read"
|
||||||
STR_SYSTEM_INFO: "System Information"
|
STR_SYSTEM_INFO: "System Information"
|
||||||
STR_LOAD_XTC_FAILED: "Failed to load XTC file"
|
STR_LOAD_XTC_FAILED: "Failed to load XTC file"
|
||||||
STR_LOAD_EPUB_FAILED: "Failed to load EPUB file"
|
STR_LOAD_EPUB_FAILED: "Failed to load EPUB file"
|
||||||
|
|||||||
+114
-380
@@ -15,25 +15,18 @@ Each YAML file must contain:
|
|||||||
The English file is the reference. Missing keys in other languages are
|
The English file is the reference. Missing keys in other languages are
|
||||||
automatically filled from English, with a warning.
|
automatically filled from English, with a warning.
|
||||||
|
|
||||||
By default the script scans the src/ and lib/ trees for STR_* references and
|
|
||||||
reports any translation keys that are never used. Pass --strip-unused to
|
|
||||||
omit those keys from the generated output entirely.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python gen_i18n.py [translations_dir [output_dir]] [options]
|
python gen_i18n.py <translations_dir> <output_dir>
|
||||||
|
|
||||||
Examples:
|
Example:
|
||||||
python gen_i18n.py
|
|
||||||
python gen_i18n.py lib/I18n/translations lib/I18n/
|
python gen_i18n.py lib/I18n/translations lib/I18n/
|
||||||
python gen_i18n.py --strip-unused
|
|
||||||
python gen_i18n.py --strip-unused --src-dirs src lib/EpdFont
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Set, Tuple
|
from typing import List, Dict, Tuple
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -112,8 +105,7 @@ def parse_yaml_file(filepath: str) -> Dict[str, str]:
|
|||||||
|
|
||||||
def load_translations(
|
def load_translations(
|
||||||
translations_dir: str,
|
translations_dir: str,
|
||||||
verbose: bool = False,
|
) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]:
|
||||||
) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]], List[Set[str]]]:
|
|
||||||
"""
|
"""
|
||||||
Read every YAML file in *translations_dir* and return:
|
Read every YAML file in *translations_dir* and return:
|
||||||
language_codes e.g. ["EN", "ES", ...]
|
language_codes e.g. ["EN", "ES", ...]
|
||||||
@@ -146,29 +138,6 @@ def load_translations(
|
|||||||
if english_file is None:
|
if english_file is None:
|
||||||
raise ValueError("No YAML file with _language_code: EN found")
|
raise ValueError("No YAML file with _language_code: EN found")
|
||||||
|
|
||||||
duplicate_orders: Dict[str, List[str]] = {}
|
|
||||||
order_to_files: Dict[str, List[str]] = {}
|
|
||||||
for fname, data in parsed.items():
|
|
||||||
order = data.get("_order")
|
|
||||||
if not order:
|
|
||||||
continue
|
|
||||||
order_to_files.setdefault(order, []).append(fname)
|
|
||||||
|
|
||||||
for order, files in order_to_files.items():
|
|
||||||
if len(files) > 1:
|
|
||||||
duplicate_orders[order] = sorted(files)
|
|
||||||
|
|
||||||
if duplicate_orders:
|
|
||||||
duplicate_messages = [
|
|
||||||
f"_order {order}: {', '.join(files)}"
|
|
||||||
for order, files in sorted(
|
|
||||||
duplicate_orders.items(), key=lambda item: int(item[0])
|
|
||||||
)
|
|
||||||
]
|
|
||||||
raise ValueError(
|
|
||||||
"Duplicate _order values found:\n " + "\n ".join(duplicate_messages) + "\nEach _order value must be unique to ensure a deterministic language order."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Order: English first, then by _order metadata (falls back to filename)
|
# Order: English first, then by _order metadata (falls back to filename)
|
||||||
def sort_key(fname: str) -> Tuple[int, int, str]:
|
def sort_key(fname: str) -> Tuple[int, int, str]:
|
||||||
"""English always first (0), then by _order, then by filename."""
|
"""English always first (0), then by _order, then by filename."""
|
||||||
@@ -205,20 +174,16 @@ def load_translations(
|
|||||||
raise ValueError(f"Invalid C++ identifier in English file: '{key}'")
|
raise ValueError(f"Invalid C++ identifier in English file: '{key}'")
|
||||||
|
|
||||||
# Build translations dict, filling missing keys from English
|
# Build translations dict, filling missing keys from English
|
||||||
inherited_sets: List[Set[str]] = [set() for _ in ordered_files]
|
|
||||||
translations: Dict[str, List[str]] = {}
|
translations: Dict[str, List[str]] = {}
|
||||||
for key in string_keys:
|
for key in string_keys:
|
||||||
row: List[str] = []
|
row: List[str] = []
|
||||||
for lang_idx, fname in enumerate(ordered_files):
|
for fname in ordered_files:
|
||||||
data = parsed[fname]
|
data = parsed[fname]
|
||||||
value = data.get(key, "")
|
value = data.get(key, "")
|
||||||
if not value.strip() and fname != english_file:
|
if not value.strip() and fname != english_file:
|
||||||
value = english_data[key]
|
value = english_data[key]
|
||||||
inherited_sets[lang_idx].add(key)
|
lang_code = parsed[fname].get("_language_code", fname)
|
||||||
if verbose:
|
print(f" INFO: '{key}' missing in {lang_code}, using English fallback")
|
||||||
print(
|
|
||||||
f" INFO: '{key}' missing in {language_codes[lang_idx]}, using English fallback"
|
|
||||||
)
|
|
||||||
row.append(value)
|
row.append(value)
|
||||||
translations[key] = row
|
translations[key] = row
|
||||||
|
|
||||||
@@ -230,72 +195,60 @@ def load_translations(
|
|||||||
extra = [k for k in data if not k.startswith("_") and k not in english_data]
|
extra = [k for k in data if not k.startswith("_") and k not in english_data]
|
||||||
if extra:
|
if extra:
|
||||||
lang_code = data.get("_language_code", fname)
|
lang_code = data.get("_language_code", fname)
|
||||||
if verbose:
|
print(f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}")
|
||||||
print(
|
|
||||||
f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if verbose:
|
print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys")
|
||||||
print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys")
|
return language_codes, language_names, string_keys, translations
|
||||||
return language_codes, language_names, string_keys, translations, inherited_sets
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Unused-string detection
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
_GENERATED_FILENAMES: Set[str] = {"I18nKeys.h", "I18nStrings.h", "I18nStrings.cpp"}
|
|
||||||
|
|
||||||
|
|
||||||
def find_used_string_keys(
|
|
||||||
src_dirs: List[str],
|
|
||||||
skip_filenames: Optional[Set[str]] = None,
|
|
||||||
) -> Set[str]:
|
|
||||||
"""
|
|
||||||
Scan C/C++ source files under *src_dirs* for STR_* identifiers.
|
|
||||||
|
|
||||||
Files whose basename appears in *skip_filenames* are skipped so that
|
|
||||||
the generated I18n files don't count as "usage" of themselves.
|
|
||||||
|
|
||||||
Returns the set of all STR_KEY names that appear at least once.
|
|
||||||
"""
|
|
||||||
if skip_filenames is None:
|
|
||||||
skip_filenames = _GENERATED_FILENAMES
|
|
||||||
|
|
||||||
pattern = re.compile(r"\bSTR_[A-Za-z0-9_]+\b")
|
|
||||||
used: Set[str] = set()
|
|
||||||
|
|
||||||
for src_dir in src_dirs:
|
|
||||||
p = Path(src_dir)
|
|
||||||
if not p.is_dir():
|
|
||||||
continue
|
|
||||||
for f in p.rglob("*"):
|
|
||||||
if f.suffix not in {".cpp", ".h", ".c"}:
|
|
||||||
continue
|
|
||||||
if f.name in skip_filenames:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = f.read_text(encoding="utf-8", errors="replace")
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
for m in pattern.finditer(text):
|
|
||||||
used.add(m.group(0))
|
|
||||||
|
|
||||||
return used
|
|
||||||
|
|
||||||
|
|
||||||
def report_unused_keys(
|
|
||||||
string_keys: List[str],
|
|
||||||
used_keys: Set[str],
|
|
||||||
) -> List[str]:
|
|
||||||
"""Return a sorted list of keys from *string_keys* absent in *used_keys*."""
|
|
||||||
return [k for k in sorted(string_keys) if k not in used_keys]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# C++ string escaping
|
# C++ string escaping
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
LANG_ABBREVIATIONS = {
|
||||||
|
"english": "EN",
|
||||||
|
"español": "ES",
|
||||||
|
"espanol": "ES",
|
||||||
|
"italiano": "IT",
|
||||||
|
"svenska": "SV",
|
||||||
|
"français": "FR",
|
||||||
|
"francais": "FR",
|
||||||
|
"deutsch": "DE",
|
||||||
|
"german": "DE",
|
||||||
|
"polski": "PL",
|
||||||
|
"português": "PT",
|
||||||
|
"portugues": "PT",
|
||||||
|
"português (brasil)": "PO",
|
||||||
|
"中文": "ZH",
|
||||||
|
"chinese": "ZH",
|
||||||
|
"日本語": "JA",
|
||||||
|
"japanese": "JA",
|
||||||
|
"한국어": "KO",
|
||||||
|
"korean": "KO",
|
||||||
|
"русский": "RU",
|
||||||
|
"russian": "RU",
|
||||||
|
"العربية": "AR",
|
||||||
|
"arabic": "AR",
|
||||||
|
"עברית": "HE",
|
||||||
|
"hebrew": "HE",
|
||||||
|
"فارسی": "FA",
|
||||||
|
"persian": "FA",
|
||||||
|
"čeština": "CS",
|
||||||
|
"türkçe": "TR",
|
||||||
|
"turkish": "TR",
|
||||||
|
"Қазақша": "KK",
|
||||||
|
"kazakh": "KK",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_lang_abbreviation(lang_code: str, lang_name: str) -> str:
|
||||||
|
"""Return a 2-letter abbreviation for a language."""
|
||||||
|
lower = lang_name.lower()
|
||||||
|
if lower in LANG_ABBREVIATIONS:
|
||||||
|
return LANG_ABBREVIATIONS[lower]
|
||||||
|
return lang_code[:2].upper()
|
||||||
|
|
||||||
|
|
||||||
def escape_cpp_string(s: str) -> List[str]:
|
def escape_cpp_string(s: str) -> List[str]:
|
||||||
r"""
|
r"""
|
||||||
Convert *s* into one or more C++ string literal segments.
|
Convert *s* into one or more C++ string literal segments.
|
||||||
@@ -437,7 +390,6 @@ def generate_keys_header(
|
|||||||
language_names: List[str],
|
language_names: List[str],
|
||||||
string_keys: List[str],
|
string_keys: List[str],
|
||||||
output_path: str,
|
output_path: str,
|
||||||
verbose: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate I18nKeys.h."""
|
"""Generate I18nKeys.h."""
|
||||||
lines: List[str] = [
|
lines: List[str] = [
|
||||||
@@ -446,13 +398,13 @@ def generate_keys_header(
|
|||||||
"",
|
"",
|
||||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||||
"",
|
"",
|
||||||
"// Forward declarations for flat string data blobs and offset tables",
|
"// Forward declaration for string arrays",
|
||||||
"namespace i18n_strings {",
|
"namespace i18n_strings {",
|
||||||
]
|
]
|
||||||
|
|
||||||
for code in languages:
|
for code, name in zip(languages, language_names):
|
||||||
lines.append(f"extern const char STRINGS_{code}_DATA[];")
|
abbrev = get_lang_abbreviation(code, name)
|
||||||
lines.append(f"extern const uint16_t OFFSETS_{code}[];")
|
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
|
||||||
|
|
||||||
lines.append("} // namespace i18n_strings")
|
lines.append("} // namespace i18n_strings")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -484,28 +436,17 @@ def generate_keys_header(
|
|||||||
lines.append("};")
|
lines.append("};")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# LangStrings struct
|
# getStringArray helper
|
||||||
lines.append("// Holds a flat string blob and its offset table for one language")
|
lines.append("// Helper function to get string array for a language")
|
||||||
lines.append("struct LangStrings {")
|
lines.append("inline const char* const* getStringArray(Language lang) {")
|
||||||
lines.append(" const char* data;")
|
|
||||||
lines.append(" const uint16_t* offsets;")
|
|
||||||
lines.append("};")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# getLanguageStrings helper
|
|
||||||
lines.append("// Helper function to get string data for a language")
|
|
||||||
lines.append("inline LangStrings getLanguageStrings(Language lang) {")
|
|
||||||
lines.append(" switch (lang) {")
|
lines.append(" switch (lang) {")
|
||||||
for code in languages:
|
for code, name in zip(languages, language_names):
|
||||||
|
abbrev = get_lang_abbreviation(code, name)
|
||||||
lines.append(f" case Language::{code}:")
|
lines.append(f" case Language::{code}:")
|
||||||
lines.append(
|
lines.append(f" return i18n_strings::STRINGS_{abbrev};")
|
||||||
f" return {{i18n_strings::STRINGS_{code}_DATA, i18n_strings::OFFSETS_{code}}};"
|
first_abbrev = get_lang_abbreviation(languages[0], language_names[0])
|
||||||
)
|
|
||||||
first_code = languages[0]
|
|
||||||
lines.append(" default:")
|
lines.append(" default:")
|
||||||
lines.append(
|
lines.append(f" return i18n_strings::STRINGS_{first_abbrev};")
|
||||||
f" return {{i18n_strings::STRINGS_{first_code}_DATA, i18n_strings::OFFSETS_{first_code}}};"
|
|
||||||
)
|
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
lines.append("}")
|
lines.append("}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -526,9 +467,21 @@ def generate_keys_header(
|
|||||||
key=lambda i: languages[i],
|
key=lambda i: languages[i],
|
||||||
)
|
)
|
||||||
sorted_indices = [english_idx] + rest
|
sorted_indices = [english_idx] + rest
|
||||||
comment_names = ", ".join(language_names[i] for i in sorted_indices)
|
names = [language_names[i] for i in sorted_indices]
|
||||||
|
comment_names = ", ".join(names)
|
||||||
lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)")
|
lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)")
|
||||||
lines.append(f"// Order: {comment_names}")
|
if len(f"// Order: {comment_names}") <= 120:
|
||||||
|
lines.append(f"// Order: {comment_names}")
|
||||||
|
else:
|
||||||
|
current = "// Order: "
|
||||||
|
for name in names:
|
||||||
|
candidate = current + name + ", "
|
||||||
|
if len(candidate) > 100:
|
||||||
|
lines.append(current.rstrip(", "))
|
||||||
|
current = "// " + name + ", "
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
lines.append(current.rstrip(", "))
|
||||||
lines.append(
|
lines.append(
|
||||||
"constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {"
|
"constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {"
|
||||||
f"{', '.join(str(i) for i in sorted_indices)}"
|
f"{', '.join(str(i) for i in sorted_indices)}"
|
||||||
@@ -540,18 +493,19 @@ def generate_keys_header(
|
|||||||
)
|
)
|
||||||
lines.append(' "SORTED_LANGUAGE_INDICES size mismatch");')
|
lines.append(' "SORTED_LANGUAGE_INDICES size mismatch");')
|
||||||
|
|
||||||
_write_file(output_path, lines, verbose)
|
_write_file(output_path, lines)
|
||||||
|
|
||||||
|
|
||||||
def generate_strings_header(
|
def generate_strings_header(
|
||||||
languages: List[str],
|
languages: List[str],
|
||||||
language_names: List[str],
|
language_names: List[str],
|
||||||
output_path: str,
|
output_path: str,
|
||||||
verbose: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate I18nStrings.h."""
|
"""Generate I18nStrings.h."""
|
||||||
lines: List[str] = [
|
lines: List[str] = [
|
||||||
"#pragma once",
|
"#pragma once",
|
||||||
|
"#include <string>",
|
||||||
|
"",
|
||||||
'#include "I18nKeys.h"',
|
'#include "I18nKeys.h"',
|
||||||
"",
|
"",
|
||||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||||
@@ -560,13 +514,13 @@ def generate_strings_header(
|
|||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for code in languages:
|
for code, name in zip(languages, language_names):
|
||||||
lines.append(f"extern const char STRINGS_{code}_DATA[];")
|
abbrev = get_lang_abbreviation(code, name)
|
||||||
lines.append(f"extern const uint16_t OFFSETS_{code}[];")
|
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("} // namespace i18n_strings")
|
lines.append("} // namespace i18n_strings")
|
||||||
_write_file(output_path, lines, verbose)
|
_write_file(output_path, lines)
|
||||||
|
|
||||||
|
|
||||||
def generate_strings_cpp(
|
def generate_strings_cpp(
|
||||||
@@ -575,12 +529,10 @@ def generate_strings_cpp(
|
|||||||
string_keys: List[str],
|
string_keys: List[str],
|
||||||
translations: Dict[str, List[str]],
|
translations: Dict[str, List[str]],
|
||||||
output_path: str,
|
output_path: str,
|
||||||
verbose: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate I18nStrings.cpp."""
|
"""Generate I18nStrings.cpp."""
|
||||||
lines: List[str] = [
|
lines: List[str] = [
|
||||||
'#include "I18nStrings.h"',
|
'#include "I18nStrings.h"',
|
||||||
"#include <cstddef>",
|
|
||||||
"",
|
"",
|
||||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||||
"",
|
"",
|
||||||
@@ -603,41 +555,18 @@ def generate_strings_cpp(
|
|||||||
lines.append("};")
|
lines.append("};")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Per-language flat string blobs and offset tables
|
# Per-language string arrays
|
||||||
lines.append("namespace i18n_strings {")
|
lines.append("namespace i18n_strings {")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
for lang_idx, code in enumerate(languages):
|
for lang_idx, (code, name) in enumerate(zip(languages, language_names)):
|
||||||
lang_strings = [translations[key][lang_idx] for key in string_keys]
|
abbrev = get_lang_abbreviation(code, name)
|
||||||
|
lines.append(f"const char* const STRINGS_{abbrev}[] = {{")
|
||||||
|
|
||||||
# Precompute byte offsets (UTF-8 encoded, +1 per string for null terminator)
|
for key in string_keys:
|
||||||
offsets: List[int] = []
|
text = translations[key][lang_idx]
|
||||||
current_offset = 0
|
_append_string_entry(lines, text)
|
||||||
for s in lang_strings:
|
|
||||||
offsets.append(current_offset)
|
|
||||||
current_offset += len(s.encode("utf-8")) + 1
|
|
||||||
if current_offset > 65535:
|
|
||||||
raise ValueError(
|
|
||||||
f"Language {code}: total string data ({current_offset} bytes) "
|
|
||||||
"exceeds uint16_t offset range (65535)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Flat string data blob — all strings concatenated with \0 separators.
|
|
||||||
# clang-format off/on avoids reformatting of the adjacent string literals.
|
|
||||||
lines.append("// clang-format off")
|
|
||||||
lines.append(f"const char STRINGS_{code}_DATA[] =")
|
|
||||||
for text in lang_strings:
|
|
||||||
_append_string_data_entry(lines, text)
|
|
||||||
lines.append(";")
|
|
||||||
lines.append("// clang-format on")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Offset table — one uint16_t per StrId
|
|
||||||
lines.append(f"const uint16_t OFFSETS_{code}[] = {{")
|
|
||||||
chunk_size = 12
|
|
||||||
for i in range(0, len(offsets), chunk_size):
|
|
||||||
chunk = offsets[i : i + chunk_size]
|
|
||||||
lines.append(" " + ", ".join(str(o) for o in chunk) + ",")
|
|
||||||
lines.append("};")
|
lines.append("};")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
@@ -646,15 +575,16 @@ def generate_strings_cpp(
|
|||||||
|
|
||||||
# Compile-time size checks
|
# Compile-time size checks
|
||||||
lines.append("// Compile-time validation of array sizes")
|
lines.append("// Compile-time validation of array sizes")
|
||||||
for code in languages:
|
for code, name in zip(languages, language_names):
|
||||||
|
abbrev = get_lang_abbreviation(code, name)
|
||||||
lines.append(
|
lines.append(
|
||||||
f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) "
|
f"static_assert(sizeof(i18n_strings::STRINGS_{abbrev}) "
|
||||||
f"/ sizeof(i18n_strings::OFFSETS_{code}[0]) =="
|
f"/ sizeof(i18n_strings::STRINGS_{abbrev}[0]) =="
|
||||||
)
|
)
|
||||||
lines.append(" static_cast<size_t>(StrId::_COUNT),")
|
lines.append(" static_cast<size_t>(StrId::_COUNT),")
|
||||||
lines.append(f' "OFFSETS_{code} size mismatch");')
|
lines.append(f' "STRINGS_{abbrev} size mismatch");')
|
||||||
|
|
||||||
_write_file(output_path, lines, verbose)
|
_write_file(output_path, lines)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -662,91 +592,6 @@ def generate_strings_cpp(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _print_language_table(
|
|
||||||
language_codes: List[str],
|
|
||||||
language_names: List[str],
|
|
||||||
inherited_sets: List[Set[str]],
|
|
||||||
string_keys: List[str],
|
|
||||||
unused_keys: Set[str],
|
|
||||||
data_sizes: List[int],
|
|
||||||
) -> None:
|
|
||||||
"""Print a per-language summary table."""
|
|
||||||
total = len(string_keys)
|
|
||||||
headers = ("Language", "Code", "Own", "Fallback", "Unused", "Data (B)")
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
for code, name, inherited, size in zip(
|
|
||||||
language_codes, language_names, inherited_sets, data_sizes
|
|
||||||
):
|
|
||||||
own = total - len(inherited)
|
|
||||||
fallback = len(inherited)
|
|
||||||
# strings this language translated but the code never calls
|
|
||||||
unused = len(unused_keys - inherited)
|
|
||||||
rows.append((name, code, str(own), str(fallback), str(unused), str(size)))
|
|
||||||
|
|
||||||
# EN first, then alphabetically by ISO code
|
|
||||||
rows.sort(key=lambda r: (0 if r[1] == "EN" else 1, r[1]))
|
|
||||||
|
|
||||||
col_widths = [len(h) for h in headers]
|
|
||||||
for row in rows:
|
|
||||||
for i, cell in enumerate(row):
|
|
||||||
col_widths[i] = max(col_widths[i], len(cell))
|
|
||||||
|
|
||||||
fmt = " ".join(f"{{:<{w}}}" for w in col_widths)
|
|
||||||
sep = " ".join("-" * w for w in col_widths)
|
|
||||||
|
|
||||||
def _safe_print(line: str) -> None:
|
|
||||||
print(
|
|
||||||
line.encode(sys.stdout.encoding or "utf-8", errors="replace").decode(
|
|
||||||
sys.stdout.encoding or "utf-8", errors="replace"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
_safe_print(fmt.format(*headers))
|
|
||||||
_safe_print(sep)
|
|
||||||
for row in rows:
|
|
||||||
_safe_print(fmt.format(*row))
|
|
||||||
used = total - len(unused_keys)
|
|
||||||
total_size = sum(data_sizes)
|
|
||||||
n_lang = len(rows)
|
|
||||||
n_keys = len(string_keys)
|
|
||||||
# Current layout: uint16_t offset table (2 B per string per language)
|
|
||||||
offset_table_size = n_lang * n_keys * 2
|
|
||||||
current_total = total_size + offset_table_size
|
|
||||||
# Previous layout: const char* pointer array (4 B per string per language)
|
|
||||||
old_pointer_table_size = n_lang * n_keys * 4
|
|
||||||
old_total = total_size + old_pointer_table_size
|
|
||||||
saved = old_total - current_total
|
|
||||||
print(
|
|
||||||
f"\n Total: {total} | Used in code: {used} | Never used: {len(unused_keys)}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f" Flash (now): {total_size:>7,} B strings + {offset_table_size:>6,} B offset tables (uint16_t)"
|
|
||||||
f" = {current_total:>7,} B"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f" Flash (before): {total_size:>7,} B strings + {old_pointer_table_size:>6,} B pointer tables (ptr32)"
|
|
||||||
f" = {old_total:>7,} B"
|
|
||||||
)
|
|
||||||
print(f" Saved by offset tables: {saved:,} B")
|
|
||||||
|
|
||||||
|
|
||||||
def _append_string_data_entry(lines: List[str], text: str) -> None:
|
|
||||||
"""
|
|
||||||
Escape *text*, append a \\0 null separator, and format as indented C++
|
|
||||||
string literal lines for inclusion in a flat char data array blob.
|
|
||||||
"""
|
|
||||||
segments = escape_cpp_string(text)
|
|
||||||
# Append the null entry separator to the last segment
|
|
||||||
if segments and segments[-1] != "":
|
|
||||||
segments[-1] += "\\0"
|
|
||||||
elif segments:
|
|
||||||
segments[-1] = "\\0"
|
|
||||||
else:
|
|
||||||
segments = ["\\0"]
|
|
||||||
lines.extend(format_cpp_string_literal(segments))
|
|
||||||
|
|
||||||
|
|
||||||
def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None:
|
def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None:
|
||||||
"""Escape *text*, format as indented C++ lines, append comma (and optional comment)."""
|
"""Escape *text*, format as indented C++ lines, append comma (and optional comment)."""
|
||||||
segments = escape_cpp_string(text)
|
segments = escape_cpp_string(text)
|
||||||
@@ -756,12 +601,11 @@ def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None
|
|||||||
lines.extend(formatted)
|
lines.extend(formatted)
|
||||||
|
|
||||||
|
|
||||||
def _write_file(path: str, lines: List[str], verbose: bool = False) -> None:
|
def _write_file(path: str, lines: List[str]) -> None:
|
||||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
f.write("\n".join(lines))
|
f.write("\n".join(lines))
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
if verbose:
|
print(f"Generated: {path}")
|
||||||
print(f"Generated: {path}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -769,17 +613,10 @@ def _write_file(path: str, lines: List[str], verbose: bool = False) -> None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def main(
|
def main(translations_dir=None, output_dir=None) -> None:
|
||||||
translations_dir: Optional[str] = None,
|
|
||||||
output_dir: Optional[str] = None,
|
|
||||||
src_dirs: Optional[List[str]] = None,
|
|
||||||
strip_unused: bool = False,
|
|
||||||
verbose: bool = False,
|
|
||||||
) -> None:
|
|
||||||
# Default paths (relative to project root)
|
# Default paths (relative to project root)
|
||||||
default_translations_dir = "lib/I18n/translations"
|
default_translations_dir = "lib/I18n/translations"
|
||||||
default_output_dir = "lib/I18n/"
|
default_output_dir = "lib/I18n/"
|
||||||
default_src_dirs = ["src", "lib"]
|
|
||||||
|
|
||||||
if translations_dir is None or output_dir is None:
|
if translations_dir is None or output_dir is None:
|
||||||
if len(sys.argv) == 3:
|
if len(sys.argv) == 3:
|
||||||
@@ -790,9 +627,6 @@ def main(
|
|||||||
translations_dir = default_translations_dir
|
translations_dir = default_translations_dir
|
||||||
output_dir = default_output_dir
|
output_dir = default_output_dir
|
||||||
|
|
||||||
if src_dirs is None:
|
|
||||||
src_dirs = default_src_dirs
|
|
||||||
|
|
||||||
if not os.path.isdir(translations_dir):
|
if not os.path.isdir(translations_dir):
|
||||||
print(f"Error: Translations directory not found: {translations_dir}")
|
print(f"Error: Translations directory not found: {translations_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -801,91 +635,32 @@ def main(
|
|||||||
print(f"Error: Output directory not found: {output_dir}")
|
print(f"Error: Output directory not found: {output_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if verbose:
|
print(f"Reading translations from: {translations_dir}")
|
||||||
print(f"Reading translations from: {translations_dir}")
|
print(f"Output directory: {output_dir}")
|
||||||
print(f"Output directory: {output_dir}")
|
print()
|
||||||
print()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
languages, language_names, string_keys, translations, inherited_sets = (
|
languages, language_names, string_keys, translations = load_translations(
|
||||||
load_translations(translations_dir, verbose)
|
translations_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Unused-string detection ---
|
|
||||||
scan_dirs = [d for d in src_dirs if os.path.isdir(d)]
|
|
||||||
if scan_dirs:
|
|
||||||
used_keys = find_used_string_keys(scan_dirs)
|
|
||||||
unused_set = set(report_unused_keys(string_keys, used_keys))
|
|
||||||
else:
|
|
||||||
used_keys = set(string_keys)
|
|
||||||
unused_set = set()
|
|
||||||
|
|
||||||
# --- Missing-string detection (used in code but absent from English) ---
|
|
||||||
missing_keys = sorted(used_keys - set(string_keys))
|
|
||||||
if missing_keys:
|
|
||||||
print(
|
|
||||||
f"\n CRITICAL: {len(missing_keys)} string(s) used in source but missing from english.yaml:"
|
|
||||||
)
|
|
||||||
for key in missing_keys:
|
|
||||||
print(f" - {key}")
|
|
||||||
print()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Compute per-language data blob sizes:
|
|
||||||
# sum of UTF-8 byte length + 1 (null terminator) per string
|
|
||||||
data_sizes = [
|
|
||||||
sum(len(translations[k][i].encode("utf-8")) + 1 for k in string_keys)
|
|
||||||
for i in range(len(languages))
|
|
||||||
]
|
|
||||||
|
|
||||||
_print_language_table(
|
|
||||||
languages,
|
|
||||||
language_names,
|
|
||||||
inherited_sets,
|
|
||||||
string_keys,
|
|
||||||
unused_set,
|
|
||||||
data_sizes,
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
|
|
||||||
if verbose and unused_set:
|
|
||||||
print(f" Unused keys ({len(unused_set)}):")
|
|
||||||
for key in sorted(unused_set):
|
|
||||||
print(f" - {key}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
if unused_set and strip_unused:
|
|
||||||
string_keys = [k for k in string_keys if k not in unused_set]
|
|
||||||
translations = {
|
|
||||||
k: v for k, v in translations.items() if k not in unused_set
|
|
||||||
}
|
|
||||||
inherited_sets = [s - unused_set for s in inherited_sets]
|
|
||||||
print(f" Stripping {len(unused_set)} unused string(s) from output.")
|
|
||||||
|
|
||||||
out = Path(output_dir)
|
out = Path(output_dir)
|
||||||
generate_keys_header(
|
generate_keys_header(
|
||||||
languages, language_names, string_keys, str(out / "I18nKeys.h"), verbose
|
languages, language_names, string_keys, str(out / "I18nKeys.h")
|
||||||
)
|
|
||||||
generate_strings_header(
|
|
||||||
languages, language_names, str(out / "I18nStrings.h"), verbose
|
|
||||||
)
|
)
|
||||||
|
generate_strings_header(languages, language_names, str(out / "I18nStrings.h"))
|
||||||
generate_strings_cpp(
|
generate_strings_cpp(
|
||||||
languages,
|
languages,
|
||||||
language_names,
|
language_names,
|
||||||
string_keys,
|
string_keys,
|
||||||
translations,
|
translations,
|
||||||
str(out / "I18nStrings.cpp"),
|
str(out / "I18nStrings.cpp"),
|
||||||
verbose,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Code generation complete!")
|
print("✓ Code generation complete!")
|
||||||
print(f" Languages: {len(languages)}")
|
print(f" Languages: {len(languages)}")
|
||||||
print(f" String keys: {len(string_keys)}")
|
print(f" String keys: {len(string_keys)}")
|
||||||
if unused_set and not strip_unused:
|
|
||||||
print(
|
|
||||||
f" Unused keys: {len(unused_set)} (pass --strip-unused to remove them)"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\nError: {e}")
|
print(f"\nError: {e}")
|
||||||
@@ -893,52 +668,11 @@ def main(
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
main()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate I18n C++ files from per-language YAML translations."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"translations_dir",
|
|
||||||
nargs="?",
|
|
||||||
default=None,
|
|
||||||
help="Path to the translations directory (default: lib/I18n/translations)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"output_dir",
|
|
||||||
nargs="?",
|
|
||||||
default=None,
|
|
||||||
help="Path to the output directory (default: lib/I18n/)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--src-dirs",
|
|
||||||
nargs="+",
|
|
||||||
metavar="DIR",
|
|
||||||
default=None,
|
|
||||||
help="Source directories to scan for STR_* usage (default: src lib)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--strip-unused",
|
|
||||||
action="store_true",
|
|
||||||
help="Remove unused STR_* keys from the generated output",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--verbose",
|
|
||||||
"-v",
|
|
||||||
action="store_true",
|
|
||||||
help="Print per-key INFO/WARNING messages and file generation details",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
main(
|
|
||||||
args.translations_dir,
|
|
||||||
args.output_dir,
|
|
||||||
args.src_dirs,
|
|
||||||
args.strip_unused,
|
|
||||||
args.verbose,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
Import("env")
|
Import("env")
|
||||||
main(strip_unused=True)
|
print("Running i18n generation script from PlatformIO...")
|
||||||
|
main()
|
||||||
except NameError:
|
except NameError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class CrossPointSettings {
|
|||||||
// Sleep screen cover filter
|
// Sleep screen cover filter
|
||||||
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
||||||
// Apply information overlay with reading progress on sleep cover
|
// Apply information overlay with reading progress on sleep cover
|
||||||
uint8_t sleepCoverOverlay = 1;
|
uint8_t sleepCoverOverlay = 0;
|
||||||
// Status bar settings (statusBar retained for migration only)
|
// Status bar settings (statusBar retained for migration only)
|
||||||
uint8_t statusBar = FULL;
|
uint8_t statusBar = FULL;
|
||||||
uint8_t statusBarChapterPageCount = 1;
|
uint8_t statusBarChapterPageCount = 1;
|
||||||
|
|||||||
@@ -143,6 +143,19 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
|
|||||||
applyLegacyStatusBarSettings(s);
|
applyLegacyStatusBarSettings(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy migration: sleepCoverOverlay was a Toggle (0=off, 1=on). Now it's a 4-way Enum:
|
||||||
|
// {WHITE(0), GRAY(1), BLACK(2), OFF(3)}. Translate old values before the generic loop clamps.
|
||||||
|
if (!doc["sleepCoverOverlay"].isNull()) {
|
||||||
|
uint8_t legacyOverlay = doc["sleepCoverOverlay"] | (uint8_t)0;
|
||||||
|
if (legacyOverlay == 0) {
|
||||||
|
s.sleepCoverOverlay = 3; // OFF
|
||||||
|
doc["sleepCoverOverlay"] = 3;
|
||||||
|
if (needsResave) *needsResave = true;
|
||||||
|
} else if (legacyOverlay == 1) {
|
||||||
|
s.sleepCoverOverlay = 1; // GRAY (old "on" = gray dither)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& info : getSettingsList()) {
|
for (const auto& info : getSettingsList()) {
|
||||||
if (!info.key) continue;
|
if (!info.key) continue;
|
||||||
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
|
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
|
||||||
|
|||||||
+4
-2
@@ -23,8 +23,10 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
|||||||
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
|
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
|
||||||
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
|
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
|
||||||
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
|
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
|
||||||
SettingInfo::Toggle(StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, "sleepCoverOverlay",
|
SettingInfo::Enum(
|
||||||
StrId::STR_CAT_DISPLAY),
|
StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay,
|
||||||
|
{StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK, StrId::STR_OVERLAY_OFF},
|
||||||
|
"sleepCoverOverlay", StrId::STR_CAT_DISPLAY),
|
||||||
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
|
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
|
||||||
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
|
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
|
||||||
StrId::STR_CAT_DISPLAY),
|
StrId::STR_CAT_DISPLAY),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <PNGdec.h>
|
#include <PNGdec.h>
|
||||||
|
#include <Serialization.h>
|
||||||
#include <Txt.h>
|
#include <Txt.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
delay(100);
|
delay(100);
|
||||||
Bitmap bitmap(file, true);
|
Bitmap bitmap(file, true);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap, BookOverlayInfo{});
|
||||||
file.close();
|
file.close();
|
||||||
dir.close();
|
dir.close();
|
||||||
return;
|
return;
|
||||||
@@ -260,7 +261,7 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
Bitmap bitmap(file, true);
|
Bitmap bitmap(file, true);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
LOG_DBG("SLP", "Loading: /sleep.bmp");
|
LOG_DBG("SLP", "Loading: /sleep.bmp");
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap, BookOverlayInfo{});
|
||||||
file.close();
|
file.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -287,20 +288,15 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
|||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const {
|
BookOverlayInfo SleepActivity::getBookOverlayInfo(const std::string& bookPath) const {
|
||||||
std::string title;
|
BookOverlayInfo info;
|
||||||
std::string author;
|
|
||||||
std::string progressLine = "Reading..."; // Default progress text
|
|
||||||
|
|
||||||
// Extract metadata based on file type
|
|
||||||
if (FsHelpers::checkFileExtension(bookPath, ".xtc") || FsHelpers::checkFileExtension(bookPath, ".xtch")) {
|
if (FsHelpers::checkFileExtension(bookPath, ".xtc") || FsHelpers::checkFileExtension(bookPath, ".xtch")) {
|
||||||
// Handle XTC file
|
|
||||||
Xtc xtc(bookPath, "/.crosspoint");
|
Xtc xtc(bookPath, "/.crosspoint");
|
||||||
if (xtc.load()) {
|
if (xtc.load()) {
|
||||||
title = xtc.getTitle();
|
info.title = xtc.getTitle();
|
||||||
author = xtc.getAuthor();
|
info.author = xtc.getAuthor();
|
||||||
|
|
||||||
// Load progress - XTC stores current page
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
@@ -309,65 +305,62 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
|
|||||||
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
|
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
|
||||||
uint32_t totalPages = xtc.getPageCount();
|
uint32_t totalPages = xtc.getPageCount();
|
||||||
float progress = xtc.calculateProgress(currentPage) * 100.0f;
|
float progress = xtc.calculateProgress(currentPage) * 100.0f;
|
||||||
char progressStr[32];
|
char buf[64];
|
||||||
snprintf(progressStr, sizeof(progressStr), "%lu/%u %.0f%%", (unsigned long)currentPage + 1, totalPages,
|
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1, totalPages,
|
||||||
progress);
|
progress);
|
||||||
progressLine = progressStr;
|
info.progressText = buf;
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (FsHelpers::checkFileExtension(bookPath, ".txt")) {
|
} else if (FsHelpers::checkFileExtension(bookPath, ".txt")) {
|
||||||
// Handle TXT file
|
|
||||||
Txt txt(bookPath, "/.crosspoint");
|
Txt txt(bookPath, "/.crosspoint");
|
||||||
if (txt.load()) {
|
if (txt.load()) {
|
||||||
title = txt.getTitle();
|
info.title = txt.getTitle();
|
||||||
// TXT files don't have author metadata
|
|
||||||
|
|
||||||
// Load progress - TXT stores current page
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
if (f.read(data, 4) == 4) {
|
if (f.read(data, 4) == 4) {
|
||||||
uint32_t currentPage = data[0] + (data[1] << 8);
|
uint32_t currentPage = data[0] + (data[1] << 8);
|
||||||
|
|
||||||
// Load total pages from page index cache
|
|
||||||
uint32_t totalPages = 0;
|
uint32_t totalPages = 0;
|
||||||
FsFile indexFile;
|
FsFile indexFile;
|
||||||
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/index.bin", indexFile)) {
|
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/index.bin", indexFile)) {
|
||||||
// Read numPages directly (it's at a fixed offset after headers)
|
uint32_t magic;
|
||||||
// Skip: magic(4) + version(4) + fileSize(4) + cachedWidth(4) + cachedLines(4) + fontId(4) + margin(4) +
|
serialization::readPod(indexFile, magic);
|
||||||
// alignment(4) = 32 bytes
|
uint8_t version;
|
||||||
indexFile.seek(32);
|
serialization::readPod(indexFile, version);
|
||||||
if (indexFile.read(reinterpret_cast<uint8_t*>(&totalPages), sizeof(totalPages)) == sizeof(totalPages)) {
|
static constexpr uint32_t INDEX_CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||||
// Successfully read totalPages
|
static constexpr uint8_t INDEX_CACHE_VERSION = 2;
|
||||||
|
if (magic == INDEX_CACHE_MAGIC && version == INDEX_CACHE_VERSION) {
|
||||||
|
indexFile.seek(32);
|
||||||
|
serialization::readPod(indexFile, totalPages);
|
||||||
}
|
}
|
||||||
indexFile.close();
|
indexFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalPages > 0) {
|
if (totalPages > 0) {
|
||||||
float progress = (currentPage + 1) * 100.0f / totalPages;
|
float progress = (currentPage + 1) * 100.0f / totalPages;
|
||||||
char progressStr[32];
|
char buf[64];
|
||||||
snprintf(progressStr, sizeof(progressStr), "%lu/%u %.0f%%", (unsigned long)currentPage + 1, totalPages,
|
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1, totalPages,
|
||||||
progress);
|
progress);
|
||||||
progressLine = progressStr;
|
info.progressText = buf;
|
||||||
} else {
|
} else {
|
||||||
char progressStr[16];
|
char buf[64];
|
||||||
snprintf(progressStr, sizeof(progressStr), "Page %lu", (unsigned long)currentPage + 1);
|
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS_NO_TOTAL), (unsigned long)currentPage + 1);
|
||||||
progressLine = progressStr;
|
info.progressText = buf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (FsHelpers::checkFileExtension(bookPath, ".epub")) {
|
} else if (FsHelpers::checkFileExtension(bookPath, ".epub")) {
|
||||||
// Handle EPUB file
|
|
||||||
Epub epub(bookPath, "/.crosspoint");
|
Epub epub(bookPath, "/.crosspoint");
|
||||||
if (epub.load(true, true)) { // Skip CSS loading for metadata only
|
if (epub.load(true, true)) {
|
||||||
title = epub.getTitle();
|
info.title = epub.getTitle();
|
||||||
author = epub.getAuthor();
|
info.author = epub.getAuthor();
|
||||||
|
|
||||||
// Load progress and get chapter information
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (Storage.openFileForRead("SLP", epub.getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("SLP", epub.getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[6];
|
uint8_t data[6];
|
||||||
@@ -379,21 +372,21 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
|
|||||||
float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
|
float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
|
||||||
float bookProgress = epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
float bookProgress = epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||||
|
|
||||||
// Get chapter name from TOC
|
|
||||||
const int tocIndex = epub.getTocIndexForSpineIndex(currentSpineIndex);
|
const int tocIndex = epub.getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
std::string chapterName;
|
if (tocIndex != -1) {
|
||||||
if (tocIndex == -1) {
|
|
||||||
chapterName = "Unnamed Chapter";
|
|
||||||
} else {
|
|
||||||
const auto tocItem = epub.getTocItem(tocIndex);
|
const auto tocItem = epub.getTocItem(tocIndex);
|
||||||
chapterName = tocItem.title;
|
info.chapterName = tocItem.title;
|
||||||
|
char suffix[64];
|
||||||
|
snprintf(suffix, sizeof(suffix), tr(STR_OVERLAY_CHAPTER_PAGE_SUFFIX), currentPage + 1, pageCount,
|
||||||
|
bookProgress);
|
||||||
|
info.progressSuffix = suffix;
|
||||||
|
info.progressText = std::string(tr(STR_CHAPTER_PREFIX)) + info.chapterName + info.progressSuffix;
|
||||||
|
} else {
|
||||||
|
char buf[80];
|
||||||
|
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1,
|
||||||
|
(unsigned)pageCount, bookProgress);
|
||||||
|
info.progressText = buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create chapter info line with chapter name and progress
|
|
||||||
char chapterProgressStr[128];
|
|
||||||
snprintf(chapterProgressStr, sizeof(chapterProgressStr), "%s: %d/%d %.0f%%", chapterName.c_str(),
|
|
||||||
currentPage + 1, pageCount, bookProgress);
|
|
||||||
progressLine = chapterProgressStr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
@@ -401,21 +394,10 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format overlay text (two lines)
|
return info;
|
||||||
std::string overlayText;
|
|
||||||
if (!title.empty()) {
|
|
||||||
overlayText = title;
|
|
||||||
if (!author.empty()) {
|
|
||||||
overlayText += " - " + author;
|
|
||||||
}
|
|
||||||
overlayText += "\n"; // New line
|
|
||||||
overlayText += progressLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
return overlayText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const BookOverlayInfo& overlayInfo) const {
|
||||||
int x, y;
|
int x, y;
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
@@ -436,7 +418,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
}
|
}
|
||||||
x = 0;
|
x = 0;
|
||||||
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
y = 0;
|
||||||
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
|
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
|
||||||
} else {
|
} else {
|
||||||
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
||||||
@@ -452,7 +434,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
} else {
|
} else {
|
||||||
// center the image
|
// center the image
|
||||||
x = (pageWidth - bitmap.getWidth()) / 2;
|
x = (pageWidth - bitmap.getWidth()) / 2;
|
||||||
y = (pageHeight - bitmap.getHeight()) / 2;
|
y = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("SLP", "drawing to %d x %d", x, y);
|
LOG_DBG("SLP", "drawing to %d x %d", x, y);
|
||||||
@@ -467,43 +449,84 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
renderer.invertScreen();
|
renderer.invertScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw overlay text if provided
|
const uint8_t overlayMode = SETTINGS.sleepCoverOverlay;
|
||||||
const std::string overlayText = SETTINGS.sleepCoverOverlay ? getBookOverlayText(APP_STATE.openEpubPath) : "";
|
|
||||||
const auto drawOverlay = [&]() {
|
const auto drawOverlay = [&]() {
|
||||||
if (overlayText.empty()) {
|
const bool hasTitle = !overlayInfo.title.empty();
|
||||||
|
const bool hasProgress = !overlayInfo.progressText.empty();
|
||||||
|
const bool hasAuthor = !overlayInfo.author.empty();
|
||||||
|
if (!hasTitle && !hasAuthor && !hasProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split text into lines
|
const int lineHeight12 = renderer.getLineHeight(BOOKERLY_12_FONT_ID);
|
||||||
size_t newlinePos = overlayText.find('\n');
|
const int lineHeight10 = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
std::string line1 = (newlinePos != std::string::npos) ? overlayText.substr(0, newlinePos) : overlayText;
|
constexpr int lineSpacing = 3;
|
||||||
std::string line2 = (newlinePos != std::string::npos && newlinePos + 1 < overlayText.length())
|
constexpr int sectionSpacing = 10;
|
||||||
? overlayText.substr(newlinePos + 1)
|
const int availableWidth = pageWidth - 20;
|
||||||
: "";
|
|
||||||
|
|
||||||
// Truncate lines if too long for the screen
|
int textBlockHeight = 0;
|
||||||
line1 = renderer.truncatedText(UI_10_FONT_ID, line1.c_str(), pageWidth - 20);
|
if (hasTitle) {
|
||||||
line2 = renderer.truncatedText(UI_10_FONT_ID, line2.c_str(), pageWidth - 20);
|
textBlockHeight += lineHeight12;
|
||||||
|
if (hasAuthor) {
|
||||||
|
textBlockHeight += lineSpacing;
|
||||||
|
} else if (hasProgress) {
|
||||||
|
textBlockHeight += sectionSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasAuthor) {
|
||||||
|
textBlockHeight += lineHeight10;
|
||||||
|
if (hasProgress) {
|
||||||
|
textBlockHeight += sectionSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasProgress) {
|
||||||
|
textBlockHeight += lineHeight10;
|
||||||
|
}
|
||||||
|
|
||||||
// Compute overlay height to exactly fit the text lines with equal top/bottom padding
|
const bool textBlack = (overlayMode != 2);
|
||||||
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
const int topPadding = lineHeight12 / 3;
|
||||||
constexpr int lineSpacing = 4;
|
const int bottomPadding = lineHeight10 * 2 / 3;
|
||||||
constexpr int overlayPadding = 10;
|
const int overlayHeight = textBlockHeight + topPadding + bottomPadding;
|
||||||
const int numLines = line2.empty() ? 1 : 2;
|
const int overlayY = pageHeight - overlayHeight;
|
||||||
const int textBlockHeight = numLines * lineHeight + (numLines - 1) * lineSpacing;
|
|
||||||
const int overlayHeight = textBlockHeight + overlayPadding * 2;
|
|
||||||
const int overlayY = pageHeight - overlayHeight - 20;
|
|
||||||
renderer.fillRect(0, overlayY, pageWidth, overlayHeight, false); // White background
|
|
||||||
|
|
||||||
// Center text block vertically in the overlay rectangle
|
if (overlayMode == 1) {
|
||||||
const int textStartY = overlayY + (overlayHeight - textBlockHeight) / 2;
|
renderer.fillRectDither(0, overlayY, pageWidth, overlayHeight, Color::LightGray);
|
||||||
const int textX1 = (pageWidth - renderer.getTextWidth(UI_10_FONT_ID, line1.c_str())) / 2;
|
} else {
|
||||||
renderer.drawText(UI_10_FONT_ID, textX1, textStartY, line1.c_str(), true); // Black text
|
renderer.fillRect(0, overlayY, pageWidth, overlayHeight, overlayMode == 2);
|
||||||
|
}
|
||||||
|
|
||||||
if (!line2.empty()) {
|
int currentY = overlayY + topPadding;
|
||||||
const int line2Y = textStartY + lineHeight + lineSpacing;
|
|
||||||
const int textX2 = (pageWidth - renderer.getTextWidth(UI_10_FONT_ID, line2.c_str())) / 2;
|
if (hasTitle) {
|
||||||
renderer.drawText(UI_10_FONT_ID, textX2, line2Y, line2.c_str(), true); // Black text
|
const std::string titleStr =
|
||||||
|
renderer.truncatedText(BOOKERLY_12_FONT_ID, overlayInfo.title.c_str(), availableWidth, EpdFontFamily::BOLD);
|
||||||
|
renderer.drawCenteredText(BOOKERLY_12_FONT_ID, currentY, titleStr.c_str(), textBlack, EpdFontFamily::BOLD);
|
||||||
|
const int spacingAfterTitle = hasAuthor ? lineSpacing : (hasProgress ? sectionSpacing : lineSpacing);
|
||||||
|
currentY += lineHeight12 + spacingAfterTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuthor) {
|
||||||
|
const std::string authorStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.author.c_str(), availableWidth);
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, currentY, authorStr.c_str(), textBlack);
|
||||||
|
currentY += lineHeight10 + sectionSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasProgress) {
|
||||||
|
std::string progressStr;
|
||||||
|
if (!overlayInfo.chapterName.empty()) {
|
||||||
|
const std::string prefix = tr(STR_CHAPTER_PREFIX);
|
||||||
|
const int prefixWidth = renderer.getTextWidth(UI_10_FONT_ID, prefix.c_str());
|
||||||
|
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, overlayInfo.progressSuffix.c_str());
|
||||||
|
const int maxChapterWidth = availableWidth - prefixWidth - suffixWidth;
|
||||||
|
const std::string truncatedChapter =
|
||||||
|
maxChapterWidth > 0
|
||||||
|
? renderer.truncatedText(UI_10_FONT_ID, overlayInfo.chapterName.c_str(), maxChapterWidth)
|
||||||
|
: "";
|
||||||
|
progressStr = prefix + truncatedChapter + overlayInfo.progressSuffix;
|
||||||
|
} else {
|
||||||
|
progressStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.progressText.c_str(), availableWidth);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, currentY, progressStr.c_str(), textBlack);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -602,7 +625,10 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str());
|
LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str());
|
||||||
renderBitmapSleepScreen(bitmap);
|
const uint8_t overlayMode = SETTINGS.sleepCoverOverlay;
|
||||||
|
const BookOverlayInfo coverOverlayInfo =
|
||||||
|
overlayMode < 3 ? getBookOverlayInfo(APP_STATE.openEpubPath) : BookOverlayInfo{};
|
||||||
|
renderBitmapSleepScreen(bitmap, coverOverlayInfo);
|
||||||
file.close();
|
file.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
|
||||||
class Bitmap;
|
class Bitmap;
|
||||||
|
|
||||||
|
struct BookOverlayInfo {
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string progressText;
|
||||||
|
std::string chapterName;
|
||||||
|
std::string progressSuffix;
|
||||||
|
};
|
||||||
|
|
||||||
class SleepActivity final : public Activity {
|
class SleepActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
@@ -13,8 +24,8 @@ class SleepActivity final : public Activity {
|
|||||||
void renderDefaultSleepScreen() const;
|
void renderDefaultSleepScreen() const;
|
||||||
void renderCustomSleepScreen() const;
|
void renderCustomSleepScreen() const;
|
||||||
void renderCoverSleepScreen() const;
|
void renderCoverSleepScreen() const;
|
||||||
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
|
void renderBitmapSleepScreen(const Bitmap& bitmap, const BookOverlayInfo& overlayInfo) const;
|
||||||
void renderBlankSleepScreen() const;
|
void renderBlankSleepScreen() const;
|
||||||
void renderOverlaySleepScreen() const;
|
void renderOverlaySleepScreen() const;
|
||||||
std::string getBookOverlayText(const std::string& bookPath) const;
|
BookOverlayInfo getBookOverlayInfo(const std::string& bookPath) const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user