From 9c12b907377121b4a7f943559499eaced0fafb82 Mon Sep 17 00:00:00 2001 From: Lorenzo Santini <37672504+santinilor@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:45:41 +0200 Subject: [PATCH 01/14] feat: add power button short press for quick access to footnotes (#1658) --- USER_GUIDE.md | 2 ++ lib/I18n/translations/english.yaml | 1 + src/CrossPointSettings.h | 4 ++- src/SettingsList.h | 10 ++++--- src/activities/reader/EpubReaderActivity.cpp | 26 +++++++++++++++++++ .../reader/EpubReaderFootnotesActivity.cpp | 3 ++- src/activities/settings/SettingsActivity.cpp | 6 +++++ src/network/html/SettingsPage.html | 18 ++++++++++++- 8 files changed, 63 insertions(+), 7 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 225d5102..e2fd673b 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -247,7 +247,9 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Ignore" (default) - Require a long press to turn off the device - "Sleep" - A short press puts the device into sleep mode - "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off + - "Footnotes" - A short press in reading mode opens the footnotes submenu; if only one footnote is present on the page, the referenced page is opened directly. The short press on the power button can be used to select the footnote in the submenu, and to go back to the original page after finish reading the footnote (like the back button). - "Refresh" - A short press triggers a manual full-screen refresh, useful for clearing ghosting +- **Quick-return from footnotes**: Toggles on and off the quick return functionality from the footnotes. When the functionality it's active, a short press of the power button will act as the back button from the footnotes page. #### 3.6.4 System diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 67021f31..2ad915ba 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -318,6 +318,7 @@ STR_BOOK_S_STYLE: "Book's Style" STR_EMBEDDED_STYLE: "Embedded Style" STR_FOCUS_READING: "Focus Reading" STR_OPDS_SERVER_URL: "OPDS Server URL" +STR_PWR_BTN_FOOTNOTE_BACK: "Quick-return from footnotes" STR_SET_SLEEP_COVER: "Set Cover" STR_FOOTNOTES: "Footnotes" STR_NO_FOOTNOTES: "No footnotes on this page" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 7df8c8ed..07413595 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -134,7 +134,7 @@ class CrossPointSettings { }; // Short power button press actions - enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, SHORT_PWRBTN_COUNT }; + enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT }; // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; @@ -230,6 +230,8 @@ class CrossPointSettings { uint8_t uiTheme = LYRA; // Sunlight fading compensation uint8_t fadingFix = 0; + // Power button return from footnotes (1 = enabled, 0 = disabled) + uint8_t pwrBtnFootnoteBack = 1; // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) uint8_t embeddedStyle = 1; // Focus Reading - emphasizes the first part of words with bold diff --git a/src/SettingsList.h b/src/SettingsList.h index c7b223ea..acdba5bb 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -171,10 +171,12 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist {StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, "longPressButtonBehavior", StrId::STR_CAT_CONTROLS), - SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH}, - "shortPwrBtn", StrId::STR_CAT_CONTROLS), - + SettingInfo::Enum( + StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, + {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES}, + "shortPwrBtn", StrId::STR_CAT_CONTROLS), + SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack, + "pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS), // --- System --- SettingInfo::Value( StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeoutMinutes, diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index a5329290..08378ed3 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -312,6 +312,32 @@ void EpubReaderActivity::loop() { return; } + // auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); + + // Handle short power button press for footnotes + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FOOTNOTES && + mappedInput.wasReleased(MappedInputManager::Button::Power) && + !mappedInput.wasReleased(MappedInputManager::Button::Down)) { + if (footnoteDepth > 0) { + restoreSavedPosition(); + } else { + if (currentPageFootnotes.size() == 1) { + navigateToHref(currentPageFootnotes[0].href, true); + } else if (currentPageFootnotes.size() > 1) { + startActivityForResult( + std::make_unique(renderer, mappedInput, currentPageFootnotes), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& footnoteResult = std::get(result.data); + navigateToHref(footnoteResult.href, true); + } + requestUpdate(); + }); + } + } + return; + } + const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; diff --git a/src/activities/reader/EpubReaderFootnotesActivity.cpp b/src/activities/reader/EpubReaderFootnotesActivity.cpp index a50163ae..390dd3c5 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.cpp +++ b/src/activities/reader/EpubReaderFootnotesActivity.cpp @@ -26,7 +26,8 @@ void EpubReaderFootnotesActivity::loop() { return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || + mappedInput.wasReleased(MappedInputManager::Button::Power)) { if (selectedIndex >= 0 && selectedIndex < static_cast(footnotes.size())) { setResult(FootnoteResult{footnotes[selectedIndex].href}); finish(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 5e8b5773..8c0a7ef6 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -46,6 +46,10 @@ void SettingsActivity::rebuildSettingsLists() { } else if (setting.category == StrId::STR_CAT_READER) { readerSettings.push_back(setting); } else if (setting.category == StrId::STR_CAT_CONTROLS) { + if (setting.valuePtr == &CrossPointSettings::pwrBtnFootnoteBack && + SETTINGS.shortPwrBtn != CrossPointSettings::SHORT_PWRBTN::FOOTNOTES) { + continue; + } controlsSettings.push_back(setting); } else if (setting.category == StrId::STR_CAT_SYSTEM) { systemSettings.push_back(setting); @@ -271,6 +275,8 @@ void SettingsActivity::toggleCurrentSetting() { syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged); SETTINGS.saveToFile(); + rebuildSettingsLists(); + selectedSettingIndex = std::min(selectedSettingIndex, settingsCount); } void SettingsActivity::syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged) { diff --git a/src/network/html/SettingsPage.html b/src/network/html/SettingsPage.html index 4291a21b..8b3bee41 100644 --- a/src/network/html/SettingsPage.html +++ b/src/network/html/SettingsPage.html @@ -479,8 +479,23 @@ return changed; } + function updateSettingsVisibility() { + const shortPwrBtnVal = getControlValue('shortPwrBtn'); + const pwrBtnFootnoteBackRow = document.getElementById('row-setting-pwrBtnFootnoteBack'); + if (pwrBtnFootnoteBackRow) { + if (shortPwrBtnVal === 4) { + pwrBtnFootnoteBackRow.style.display = ''; + } else { + pwrBtnFootnoteBackRow.style.display = 'none'; + } + } + } + function handleSettingChanged(key) { syncQuickResumeTimeoutForSleepScreen(key === 'sleepScreen', key === 'quickResumeSleepScreen'); + if (key === 'shortPwrBtn') { + updateSettingsVisibility(); + } markChanged(); } @@ -511,7 +526,7 @@ for (const category in groups) { html += '

' + escapeHtml(category) + '

'; groups[category].forEach(function(s) { - html += '
' + + html += '
' + '' + escapeHtml(s.name) + '' + '' + renderControl(s) + '' + '
'; @@ -520,6 +535,7 @@ } container.innerHTML = html; + updateSettingsVisibility(); document.getElementById('save-container').style.display = ''; document.getElementById('saveBtn').disabled = true; preserveQuickResumeTimeoutOn = getControlValue('quickResumeSleepScreen') === 1; From 2ea042a68a54c1dfe13120feeabd67cfe862e711 Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Tue, 9 Jun 2026 15:58:21 +0300 Subject: [PATCH 02/14] fix: skip `` anchors (#2303) ## Summary * **What is the goal of this PR?** Prevent heap memory exhaustion caused by thousands of machine-generated anchor IDs injected by epub converters like Kobo KePub. * Fix #2292. * **What changes are included?** * **Element-type filter (Layer 1):** Introduced the `isNonNavigableInlineElement()` function in `ChapterHtmlSlimParser.cpp` to automatically skip recording IDs on `` elements, as they are purely inline wrappers used for tracking and lack navigable meaning. * **Hard cap (Layer 2):** Added the `MAX_ANCHORS_PER_CHAPTER = 1024` constant to act as a fallback safety net against unbounded heap growth from unknown future ID-injection patterns on non-span elements. * **TOC Safety net:** Ensured that IDs matching known Table of Contents (TOC) entries explicitly bypass both the `` filter and the 1024-anchor cap, guaranteeing that chapter page-break and core navigation logic are never compromised. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index d344ad73..2688f334 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -22,6 +22,13 @@ constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t PARSE_BUFFER_SIZE = 1024; +// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation +// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per +// chapter. A runaway count usually means a converter injected machine-generated IDs on +// every text fragment (e.g. Kobo KePub spans). The cap prevents unbounded heap growth +// on resource-constrained devices (~380KB heap). TOC anchors bypass this cap. +constexpr size_t MAX_ANCHORS_PER_CHAPTER = 1024; + constexpr const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr const char* BOLD_TAGS[] = {"b", "strong"}; @@ -49,6 +56,14 @@ const char* getAttribute(const XML_Char** atts, const char* attrName) { return nullptr; } +// Returns true if the HTML element is a purely inline, non-navigable wrapper. +// IDs on these elements are never meaningful navigation targets in epub content. +// Reading-system converters (Kobo KePub, Calibre, etc.) frequently inject thousands +// of such IDs for progress tracking or internal bookkeeping, and recording each one +// as a navigation anchor exhausts the heap on memory-constrained devices. +// Block-level, sectioning, and structural elements are always considered navigable. +bool isNonNavigableInlineElement(const char* name) { return strcmp(name, "span") == 0; } + bool isInternalEpubLink(const char* href) { if (!href || href[0] == '\0') return false; if (strncmp(href, "http://", 7) == 0 || strncmp(href, "https://", 8) == 0) return false; @@ -293,7 +308,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } else if (strcmp(atts[i], "id") == 0) { // Defer both anchor recording and TOC page breaks until startNewTextBlock, // after the previous block is flushed to pages via makePages(). - self->pendingAnchorId = atts[i + 1]; + // + // Skip IDs on non-navigable inline elements (e.g. ): these are never + // link targets in epub content, but reading-system converters can inject tens + // of thousands of them per chapter, exhausting the heap. TOC anchors are + // always recorded regardless of element type, since they drive page breaks. + const char* idValue = atts[i + 1]; + const bool isTocAnchor = + std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end(); + if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) { + self->pendingAnchorId = idValue; + } } else if (strcmp(atts[i], "dir") == 0) { dirAttr = atts[i + 1]; } From ca6ebc56fb7c7f2e165c768db8cd2be06e0847ea Mon Sep 17 00:00:00 2001 From: muhas Date: Wed, 10 Jun 2026 08:10:26 +0300 Subject: [PATCH 03/14] fix(lang): russian.yaml new string (#2309) --- lib/I18n/translations/russian.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 657385f4..ed167525 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -123,6 +123,7 @@ STR_CHECK_SERIAL_OUTPUT: "Проверьте вывод по UART для дет STR_DARK: "Тёмный" STR_LIGHT: "Светлый" STR_CUSTOM: "Свой" +STR_PWR_BTN_FOOTNOTE_BACK: "Быстрый возврат из примечаний" STR_COVER: "Обложка" STR_NONE_OPT: "Нет" STR_FIT: "Вписать" From b94b58756f3318035a4ee97739cfeaec898ffdb2 Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Wed, 10 Jun 2026 16:33:54 +0300 Subject: [PATCH 04/14] fix: german traslation for STR_INVERTED (#2315) --- lib/I18n/translations/german.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index d04dad9d..f289c13b 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -128,7 +128,7 @@ STR_PAGE_TURN: "Umblättern" STR_FORCE_REFRESH: "Bildschirm regenerieren" STR_PORTRAIT: "Hochformat" STR_LANDSCAPE_CW: "Querformat rechts" -STR_INVERTED: "Hochformat 180 Grad" +STR_INVERTED: "Hochformat 180°" STR_LANDSCAPE_CCW: "Querformat links" STR_PREV_NEXT: "Zurück/Weiter" STR_NEXT_PREV: "Weiter/Zurück" From eb9873511597c28eb5b3f9154de7908b2afb6a55 Mon Sep 17 00:00:00 2001 From: Martin <35097765+neresus@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:49:39 +0200 Subject: [PATCH 05/14] feat: Slovak translation add (#2251) Co-authored-by: Uri Tauber --- lib/I18n/translations/slovak.yaml | 378 ++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 lib/I18n/translations/slovak.yaml diff --git a/lib/I18n/translations/slovak.yaml b/lib/I18n/translations/slovak.yaml new file mode 100644 index 00000000..0d6eccbf --- /dev/null +++ b/lib/I18n/translations/slovak.yaml @@ -0,0 +1,378 @@ +_language_name: "Slovenčina" +_language_code: "SK" +_order: "24" + +STR_CROSSPOINT: "CrossPoint" +STR_BOOTING: "SPÚŠŤANIE" +STR_SLEEPING: "SPÁNOK" +STR_ENTERING_SLEEP: "Prechod do režimu spánku" +STR_BROWSE_FILES: "Prehliadať súbory" +STR_FILE_TRANSFER: "Prenos súborov" +STR_SETTINGS_TITLE: "Nastavenia" +STR_CONTINUE_READING: "Pokračovať v čítaní" +STR_NO_OPEN_BOOK: "Žiadna otvorená kniha" +STR_START_READING: "Začnite čítať nižšie" +STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory" +STR_SELECT_CHAPTER: "Vybrať kapitolu" +STR_NO_CHAPTERS: "Žiadne kapitoly" +STR_END_OF_BOOK: "Koniec knihy" +STR_EMPTY_CHAPTER: "Prázdna kapitola" +STR_INDEXING: "Indexovanie" +STR_MEMORY_ERROR: "Chyba pamäte" +STR_PAGE_LOAD_ERROR: "Chyba načítania stránky" +STR_EMPTY_FILE: "Prázdny súbor" +STR_OUT_OF_BOUNDS: "Mimo rozsahu" +STR_LOADING: "Načítava sa..." +STR_LOADING_POPUP: "Načítavanie" +STR_WIFI_NETWORKS: "Wi-Fi siete" +STR_NO_NETWORKS: "Nenašli sa žiadne siete" +STR_NETWORKS_FOUND: "Nájdených %zu sietí" +STR_SCANNING: "Skenovanie..." +STR_CONNECTING: "Pripájanie..." +STR_CONNECTED: "Pripojené!" +STR_CONNECTION_FAILED: "Pripojenie zlyhalo" +STR_FORGET_NETWORK: "Zabudnúť sieť?" +STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?" +STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie" +STR_JOIN_NETWORK: "Pripojiť sa k sieti" +STR_CREATE_HOTSPOT: "Vytvoriť hotspot" +STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti" +STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní" +STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..." +STR_HOTSPOT_MODE: "Režim hotspotu" +STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti" +STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači" +STR_OR_HTTP_PREFIX: "alebo http://" +STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:" +STR_CALIBRE_WIRELESS: "Calibre Wireless" +STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené" +STR_MAC_ADDRESS: "MAC adresa:" +STR_CHECKING_WIFI: "Kontrola Wi-Fi..." +STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi" +STR_TO_PREFIX: "pre" +STR_CALIBRE_RECEIVING: "Prijímanie:" +STR_CALIBRE_RECEIVED: "Prijaté:" +STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader" +STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti" +STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“" +STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“" +STR_CAT_DISPLAY: "Displej" +STR_CAT_READER: "Čítačka" +STR_CAT_CONTROLS: "Ovládanie" +STR_CAT_SYSTEM: "Systém" +STR_SLEEP_SCREEN: "Obrazovka spánku" +STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti" +STR_SLEEP_COVER_MODE: "Obrazovka spánku – režim krytu" +STR_HIDE_BATTERY: "Skryť % batérie" +STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi" +STR_TEXT_AA: "Vyhladzovanie textu" +STR_IMAGES: "Obrázky" +STR_IMAGES_DISPLAY: "Zobraz" +STR_IMAGES_PLACEHOLDER: "Rezervované miesto" +STR_IMAGES_SUPPRESS: "Potlačiť" +STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania" +STR_ORIENTATION: "Orientácia čítania" +STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii" +STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla" +STR_LONG_PRESS_BEHAVIOR_OFF: "VYP" +STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu" +STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu" +STR_FONT_FAMILY: "Rodina písiem čítačky" +STR_FONT_SIZE: "Veľkosť písma rozhrania" +STR_LINE_SPACING: "Riadkovanie čítačky" +STR_SCREEN_MARGIN: "Okraj obrazovky čítačky" +STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky" +STR_HYPHENATION: "Delenie slov" +STR_TIME_TO_SLEEP: "Čas do uspania" +STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory" +STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych" +STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read" +STR_REFRESH_FREQ: "Frekvencia obnovovania" +STR_KOREADER_SYNC: "KOReader Sync" +STR_CHECK_UPDATES: "Skontrolovať aktualizácie" +STR_LANGUAGE: "Jazyk" +STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania" +STR_USERNAME: "Používateľské meno" +STR_PASSWORD: "Heslo" +STR_SYNC_SERVER_URL: "URL synchronizačného servera" +STR_DOCUMENT_MATCHING: "Párovanie dokumentov" +STR_AUTHENTICATE: "Overiť" +STR_KOREADER_USERNAME: "Používateľské meno KOReader" +STR_KOREADER_PASSWORD: "Heslo KOReader" +STR_FILENAME: "Názov súboru" +STR_BINARY: "Binárny" +STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje" +STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo" +STR_AUTHENTICATING: "Overovanie..." +STR_AUTH_SUCCESS: "Overenie úspešné!" +STR_KOREADER_AUTH: "Overenie KOReader" +STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie" +STR_AUTH_FAILED: "Overenie zlyhalo" +STR_DONE: "Hotovo" +STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti." +STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!" +STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať" +STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení." +STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..." +STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná" +STR_ITEMS_REMOVED: "položiek odstránených" +STR_FAILED_LOWER: "zlyhalo" +STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo" +STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe" +STR_DARK: "Tmavý" +STR_LIGHT: "Svetlý" +STR_CUSTOM: "Vlastný" +STR_COVER: "Obálka" +STR_NONE_OPT: "Žiadny" +STR_FIT: "Prispôsobiť" +STR_CROP: "Orezať" +STR_NEVER: "Nikdy" +STR_IN_READER: "V čítačke" +STR_ALWAYS: "Vždy" +STR_IGNORE: "Ignorovať" +STR_SLEEP: "Spánok" +STR_PAGE_TURN: "Otáčanie stránok" +STR_FORCE_REFRESH: "Obnoviť obrazovku" +STR_PORTRAIT: "Na výšku" +STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" +STR_INVERTED: "Obrátený" +STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek" +STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" +STR_NEXT_PREV: "Nasledujúca/Predchádzajúca" +STR_DISABLED: "Vypnuté" +STR_NOTO_SERIF: "Noto Serif" +STR_NOTO_SANS: "Noto Sans" +STR_SMALL: "Malý" +STR_MEDIUM: "Stredný" +STR_LARGE: "Veľký" +STR_X_LARGE: "Obrovský" +STR_TIGHT: "Tesný" +STR_NORMAL: "Normálny" +STR_WIDE: "Široký" +STR_JUSTIFY: "Zarovnať do bloku" +STR_ALIGN_LEFT: "Vľavo" +STR_CENTER: "Na stred" +STR_ALIGN_RIGHT: "Vpravo" +STR_PAGES_1: "1 strana" +STR_PAGES_5: "5 strán" +STR_PAGES_10: "10 strán" +STR_PAGES_15: "15 strán" +STR_PAGES_30: "30 strán" +STR_UPDATE: "Aktualizácia" +STR_CHECKING_UPDATE: "Kontrola aktualizácií…" +STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!" +STR_CURRENT_VERSION: "Aktuálna verzia:" +STR_NEW_VERSION: "Nová verzia:" +STR_UPDATING: "Aktualizácia..." +STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia" +STR_UPDATE_FAILED: "Aktualizácia zlyhala" +STR_UPDATE_COMPLETE: "Aktualizácia dokončená" +STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie" +STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s." +STR_NO_ENTRIES: "Neboli nájdené žiadne položky" +STR_DOWNLOADING: "Sťahovanie..." +STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo" +STR_ERROR_MSG: "Chyba:" +STR_UNNAMED: "Nepomenované" +STR_HOLD_CONFIRM_TO_DELETE: "Podrž potvrdiť pre vymazanie" +STR_BOOKMARK_INSTRUCTIONS: "Podrž tlačidlo Potvrdiť pre vytvorenie záložky." +STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera" +STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo" +STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo" +STR_NEXT_PAGE: "Ďaľsia strana »" +STR_PREV_PAGE: "« Predchádzajúca str." +STR_NETWORK_PREFIX: "Sieť:" +STR_IP_ADDRESS_PREFIX: "IP adresa:" +STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba" +STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená" +STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia" +STR_SD_CARD: "SD karta" +STR_BACK: "« Späť" +STR_EXIT: "« Koniec" +STR_HOME: "« Domov" +STR_SELECT: "Vybrať" +STR_SELECTED: "Vybrané" +STR_TOGGLE: "Prepnúť" +STR_CONFIRM: "Potvrdiť" +STR_CANCEL: "Zrušiť" +STR_CONNECT: "Pripojiť" +STR_OPEN: "Otvoriť" +STR_DOWNLOAD: "Stiahnuť" +STR_RETRY: "Skúsiť znova" +STR_YES: "Áno" +STR_NO: "Nie" +STR_SHOW: "Zobraz" +STR_HIDE: "Skryť" +STR_STATE_ON: "ZAP" +STR_STATE_OFF: "VYP" +STR_NOT_SET: "Nenastavené" +STR_DIR_LEFT: "Vľavo" +STR_DIR_RIGHT: "Vpravo" +STR_DIR_UP: "Hore" +STR_DIR_DOWN: "Dole" +STR_OK_BUTTON: "OK" +STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku" +STR_FILTER_CONTRAST: "Kontrast" +STR_CUSTOMISE_STATUS_BAR: "Uprav status bar" +STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly" +STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent" +STR_PROGRESS_BAR: "Ukazovateľ čítania" +STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu" +STR_PROGRESS_BAR_THIN: "Tenký" +STR_PROGRESS_BAR_MEDIUM: "Stredný" +STR_PROGRESS_BAR_THICK: "Hrubý" +STR_BOOK: "Kniha" +STR_CHAPTER: "Kapitola" +STR_EXAMPLE_CHAPTER: "Kapitola 21" +STR_EXAMPLE_BOOK: "Názov knihy" +STR_PREVIEW: "Ukážka" +STR_TITLE: "Názov" +STR_BATTERY: "Batéria" +STR_XTC_STATUS_BAR: "Stavový panel XTC" +STR_BOTTOM: "Dole" +STR_TOP: "Hore" +STR_CLOCK: "Hodiny" +STR_CLOCK_UTC_OFFSET: "UTC posun hodín" +STR_CLOCK_FORMAT: "Formát času" +STR_CLOCK_FORMAT_24H: "24-hodinový" +STR_CLOCK_FORMAT_12H: "12-hodinový" +STR_CURRENT_TIME: "Aktuálny čas:" +STR_NEXT_FIELD: "Ďalej" +STR_CLOCK_SYNC: "Synchronizácia hodín" +STR_CLOCK_SYNC_NOW: "Synchronizovať teraz" +STR_CLOCK_SYNCING: "Synchronizácia cez NTP..." +STR_CLOCK_SYNC_OK: "Hodiny synchronizované" +STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala" +STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená" +STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova." +STR_CLOCK_SYNCED: "Hodiny synchronizované" +STR_UI_THEME: "Téma rozhrania" +STR_THEME_CLASSIC: "Klasická" +STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" +STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku" +STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá" +STR_BOOKMARKS: "Záložky" +STR_BOOKMARK_ADDED: "Záložka pridaná." +STR_OPDS_BROWSER: "Prehliadač OPDS" +STR_SEARCH: "Hľadať" +STR_COVER_CUSTOM: "Obálka + Vlastné" +STR_QUICK_RESUME: "Rýchle obnovenie" +STR_MENU_RECENT_BOOKS: "Nedávne knihy" +STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?" +STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy" +STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre" +STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?" +STR_FORGET_BUTTON: "Zabudnúť" +STR_CALIBRE_STARTING: "Spúšťanie Calibre..." +STR_CALIBRE_SETUP: "Nastavenie" +STR_CALIBRE_STATUS: "Stav" +STR_CLEAR_BUTTON: "Vymazať" +STR_DEFAULT_VALUE: "Predvolené" +STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu" +STR_UNASSIGNED: "Nepriradené" +STR_ALREADY_ASSIGNED: "Už priradené" +STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie" +STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie" +STR_HW_BACK_LABEL: "Späť (1. tlačidlo)" +STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)" +STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)" +STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)" +STR_GO_TO_PERCENT: "Prejsť na %" +STR_GO_HOME_BUTTON: "Prejsť na Domov" +STR_SYNC_PROGRESS: "Priebeh synchronizácie" +STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy" +STR_DELETE: "Vymazať" +STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?" +STR_DISPLAY_QR: "Zobraz stránku ako QR" +STR_CHAPTER_PREFIX: "Kapitola:" +STR_PAGES_SEPARATOR: "strán |" +STR_BOOK_PREFIX: "Kniha:" +STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy" +STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %" +STR_SYNCING_TIME: "Čas synchronizácie..." +STR_CALC_HASH: "Výpočet hashu dokumentu..." +STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu" +STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..." +STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..." +STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené" +STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach" +STR_PROGRESS_FOUND: "Priebeh nájdený!" +STR_REMOTE_LABEL: "Vzdialený:" +STR_LOCAL_LABEL: "Lokálny:" +STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%" +STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s" +STR_APPLY_REMOTE: "Použiť vzdialený priebeh" +STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh" +STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh" +STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?" +STR_UPLOAD_SUCCESS: "Priebeh nahraný!" +STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala" +STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh" +STR_SECTION_PREFIX: "Sekcia" +STR_UPLOAD: "Nahrať" +STR_BOOK_S_STYLE: "Štýl knihy" +STR_EMBEDDED_STYLE: "Vložený štýl" +STR_FOCUS_READING: "Sústredené čítanie" +STR_OPDS_SERVER_URL: "URL adresa OPDS servera" +STR_SET_SLEEP_COVER: "Nastav obal" +STR_FOOTNOTES: "Poznámky pod čiarou" +STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou" +STR_LINK: "[link]" +STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky" +STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" +STR_SLEEP_NEVER: "Nikdy" +STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min" +STR_ADD_SERVER: "Pridať server" +STR_SERVER_NAME: "Názov servera" +STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery" +STR_DELETE_SERVER: "Odstrániť server" +STR_OPDS_SERVERS: "OPDS servery" +STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: " +STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)" +STR_MANAGE_FONTS: "Správa písiem" +STR_FONT_BROWSER: "Prehliadač písiem" +STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..." +STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma" +STR_FONT_INSTALLED: "Písmo nainštalované!" +STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala" +STR_INSTALLED: "Nainštalované" +STR_DOWNLOAD_ALL: "Stiahnuť všetko" +STR_UPDATE_ALL: "Aktualizovať všetko" +STR_UPDATE_AVAILABLE: "Aktualizovať" +STR_CRASH_TITLE: "Zlyhanie systému" +STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby." +STR_CRASH_REASON: "Dôvod zlyhania:" +STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)" +STR_TILT_PAGE_TURN: "Otáčanie strán naklonením" +STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora" +STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora" +STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla" +STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla" +STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky" +STR_KB_TIPS: "Tipy:" +STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici" +STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL" +STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu" +STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak" +STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak" +STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak" +STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky" +STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty" +STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)" +STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin" +STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..." +STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru" +STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu" +STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý" +STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor" +STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!" +STR_RECOVERY_MODE: "Režim obnovenia" +STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho" From 68138fffdb39c83b6624f9c0552799bdbe072ce8 Mon Sep 17 00:00:00 2001 From: Hoang Manh Linh Date: Thu, 11 Jun 2026 19:10:45 +0700 Subject: [PATCH 06/14] feat(i18n): add Vietnamese translation (#2279) Co-authored-by: Claude Opus 4.8 (1M context) --- lib/I18n/translations/vietnamese.yaml | 378 ++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 lib/I18n/translations/vietnamese.yaml diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml new file mode 100644 index 00000000..157b987c --- /dev/null +++ b/lib/I18n/translations/vietnamese.yaml @@ -0,0 +1,378 @@ +_language_name: "Tiếng Việt" +_language_code: "VI" +_order: "24" + +STR_CROSSPOINT: "CrossPoint" +STR_BOOTING: "ĐANG KHỞI ĐỘNG" +STR_SLEEPING: "ĐANG NGỦ" +STR_ENTERING_SLEEP: "Đang vào chế độ ngủ" +STR_BROWSE_FILES: "Duyệt tập tin" +STR_FILE_TRANSFER: "Truyền tập tin" +STR_SETTINGS_TITLE: "Cài đặt" +STR_CONTINUE_READING: "Đọc tiếp" +STR_NO_OPEN_BOOK: "Chưa mở sách" +STR_START_READING: "Bắt đầu đọc bên dưới" +STR_NO_FILES_FOUND: "Không tìm thấy tập tin" +STR_SELECT_CHAPTER: "Chọn chương" +STR_NO_CHAPTERS: "Không có chương" +STR_END_OF_BOOK: "Hết sách" +STR_EMPTY_CHAPTER: "Chương trống" +STR_INDEXING: "Đang lập chỉ mục" +STR_MEMORY_ERROR: "Lỗi bộ nhớ" +STR_PAGE_LOAD_ERROR: "Lỗi tải trang" +STR_EMPTY_FILE: "Tập tin trống" +STR_OUT_OF_BOUNDS: "Vượt giới hạn" +STR_LOADING: "Đang tải..." +STR_LOADING_POPUP: "Đang tải" +STR_WIFI_NETWORKS: "Mạng Wi-Fi" +STR_NO_NETWORKS: "Không tìm thấy mạng" +STR_NETWORKS_FOUND: "Tìm thấy %zu mạng" +STR_SCANNING: "Đang quét..." +STR_CONNECTING: "Đang kết nối..." +STR_CONNECTED: "Đã kết nối!" +STR_CONNECTION_FAILED: "Kết nối thất bại" +STR_FORGET_NETWORK: "Quên mạng này?" +STR_SAVE_PASSWORD: "Lưu mật khẩu cho lần sau?" +STR_PRESS_OK_SCAN: "Nhấn OK để quét lại" +STR_JOIN_NETWORK: "Kết nối mạng" +STR_CREATE_HOTSPOT: "Tạo điểm phát" +STR_JOIN_DESC: "Kết nối tới mạng Wi-Fi có sẵn" +STR_HOTSPOT_DESC: "Tạo mạng Wi-Fi để máy khác kết nối" +STR_STARTING_HOTSPOT: "Đang tạo điểm phát..." +STR_HOTSPOT_MODE: "Chế độ điểm phát" +STR_CONNECT_WIFI_HINT: "Kết nối thiết bị của bạn vào mạng Wi-Fi này" +STR_OPEN_URL_HINT: "Mở URL này trong trình duyệt" +STR_OR_HTTP_PREFIX: "hoặc http://" +STR_SCAN_QR_HINT: "hoặc quét mã QR bằng điện thoại:" +STR_CALIBRE_WIRELESS: "Calibre không dây" +STR_NETWORK_LEGEND: "* = Mã hóa | + = Đã lưu" +STR_MAC_ADDRESS: "Địa chỉ MAC:" +STR_CHECKING_WIFI: "Đang kiểm tra Wi-Fi..." +STR_ENTER_WIFI_PASSWORD: "Nhập mật khẩu Wi-Fi" +STR_TO_PREFIX: "tới " +STR_CALIBRE_RECEIVING: "Đang nhận: " +STR_CALIBRE_RECEIVED: "Đã nhận: " +STR_CALIBRE_INSTRUCTION_1: "1) Cài plugin CrossPoint Reader" +STR_CALIBRE_INSTRUCTION_2: "2) Dùng chung một mạng Wi-Fi" +STR_CALIBRE_INSTRUCTION_3: "3) Trong Calibre: \"Send to device\"" +STR_CALIBRE_INSTRUCTION_4: "\"Giữ màn hình này khi đang gửi\"" +STR_CAT_DISPLAY: "Hiển thị" +STR_CAT_READER: "Trình đọc" +STR_CAT_CONTROLS: "Điều khiển" +STR_CAT_SYSTEM: "Hệ thống" +STR_SLEEP_SCREEN: "Màn hình ngủ" +STR_QUICK_RESUME_TIMEOUT: "Khôi phục nhanh khi hết giờ" +STR_SLEEP_COVER_MODE: "Kiểu ảnh bìa màn hình ngủ" +STR_HIDE_BATTERY: "Ẩn % pin" +STR_EXTRA_SPACING: "Giãn cách đoạn thêm" +STR_TEXT_AA: "Khử răng cưa chữ" +STR_IMAGES: "Hình ảnh" +STR_IMAGES_DISPLAY: "Hiển thị" +STR_IMAGES_PLACEHOLDER: "Khung thay thế" +STR_IMAGES_SUPPRESS: "Ẩn đi" +STR_SHORT_PWR_BTN: "Nhấn nhanh nút nguồn" +STR_ORIENTATION: "Hướng đọc" +STR_SIDE_BTN_LAYOUT: "Bố trí nút bên (trình đọc)" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Xoay nút trước theo hướng" +STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút" +STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT" +STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương" +STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng" +STR_FONT_FAMILY: "Phông chữ trình đọc" +STR_FONT_SIZE: "Cỡ chữ trình đọc" +STR_LINE_SPACING: "Giãn dòng trình đọc" +STR_SCREEN_MARGIN: "Lề màn hình trình đọc" +STR_PARA_ALIGNMENT: "Canh lề đoạn trình đọc" +STR_HYPHENATION: "Ngắt từ bằng gạch nối" +STR_TIME_TO_SLEEP: "Thời gian chờ ngủ" +STR_SHOW_HIDDEN_FILES: "Hiện tập tin ẩn" +STR_REMOVE_READ_FROM_RECENTS: "Xóa sách đã đọc khỏi danh sách gần đây" +STR_MOVE_FINISHED_TO_READ: "Chuyển sách đọc xong vào thư mục Đã đọc" +STR_REFRESH_FREQ: "Tần suất làm tươi" +STR_KOREADER_SYNC: "Đồng bộ KOReader" +STR_CHECK_UPDATES: "Kiểm tra cập nhật" +STR_LANGUAGE: "Ngôn ngữ" +STR_CLEAR_READING_CACHE: "Xóa bộ nhớ đệm đọc" +STR_USERNAME: "Tên đăng nhập" +STR_PASSWORD: "Mật khẩu" +STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ" +STR_DOCUMENT_MATCHING: "Khớp tài liệu" +STR_AUTHENTICATE: "Xác thực" +STR_KOREADER_USERNAME: "Tên đăng nhập KOReader" +STR_KOREADER_PASSWORD: "Mật khẩu KOReader" +STR_FILENAME: "Tên tập tin" +STR_BINARY: "Nhị phân" +STR_SET_CREDENTIALS_FIRST: "Hãy nhập thông tin đăng nhập trước" +STR_WIFI_CONN_FAILED: "Kết nối Wi-Fi thất bại" +STR_AUTHENTICATING: "Đang xác thực..." +STR_AUTH_SUCCESS: "Xác thực thành công!" +STR_KOREADER_AUTH: "Xác thực KOReader" +STR_SYNC_READY: "Đồng bộ KOReader đã sẵn sàng" +STR_AUTH_FAILED: "Xác thực thất bại" +STR_DONE: "Xong" +STR_CLEAR_CACHE_WARNING_1: "Thao tác này sẽ xóa toàn bộ dữ liệu sách đã lưu đệm." +STR_CLEAR_CACHE_WARNING_2: "Toàn bộ tiến độ đọc sẽ bị mất!" +STR_CLEAR_CACHE_WARNING_3: "Sách sẽ cần lập chỉ mục lại" +STR_CLEAR_CACHE_WARNING_4: "khi mở lại." +STR_CLEARING_CACHE: "Đang xóa bộ nhớ đệm..." +STR_CACHE_CLEARED: "Đã xóa bộ nhớ đệm" +STR_ITEMS_REMOVED: "mục đã xóa" +STR_FAILED_LOWER: "thất bại" +STR_CLEAR_CACHE_FAILED: "Không xóa được bộ nhớ đệm" +STR_CHECK_SERIAL_OUTPUT: "Xem chi tiết ở cổng serial" +STR_DARK: "Tối" +STR_LIGHT: "Sáng" +STR_CUSTOM: "Tùy chỉnh" +STR_COVER: "Ảnh bìa" +STR_NONE_OPT: "Không" +STR_FIT: "Vừa khung" +STR_CROP: "Cắt" +STR_NEVER: "Không bao giờ" +STR_IN_READER: "Trong trình đọc" +STR_ALWAYS: "Luôn luôn" +STR_IGNORE: "Bỏ qua" +STR_SLEEP: "Ngủ" +STR_PAGE_TURN: "Lật trang" +STR_FORCE_REFRESH: "Làm tươi màn hình" +STR_PORTRAIT: "Dọc" +STR_LANDSCAPE_CW: "Ngang (thuận)" +STR_INVERTED: "Lật ngược" +STR_LANDSCAPE_CCW: "Ngang (ngược)" +STR_PREV_NEXT: "Trước/Sau" +STR_NEXT_PREV: "Sau/Trước" +STR_DISABLED: "Tắt" +STR_NOTO_SERIF: "Noto Serif" +STR_NOTO_SANS: "Noto Sans" +STR_SMALL: "Nhỏ" +STR_MEDIUM: "Vừa" +STR_LARGE: "Lớn" +STR_X_LARGE: "Rất lớn" +STR_TIGHT: "Hẹp" +STR_NORMAL: "Bình thường" +STR_WIDE: "Rộng" +STR_JUSTIFY: "Đều hai bên" +STR_ALIGN_LEFT: "Trái" +STR_CENTER: "Giữa" +STR_ALIGN_RIGHT: "Phải" +STR_PAGES_1: "1 trang" +STR_PAGES_5: "5 trang" +STR_PAGES_10: "10 trang" +STR_PAGES_15: "15 trang" +STR_PAGES_30: "30 trang" +STR_UPDATE: "Cập nhật" +STR_CHECKING_UPDATE: "Đang kiểm tra cập nhật..." +STR_NEW_UPDATE: "Có bản cập nhật mới!" +STR_CURRENT_VERSION: "Phiên bản hiện tại: " +STR_NEW_VERSION: "Phiên bản mới: " +STR_UPDATING: "Đang cập nhật..." +STR_NO_UPDATE: "Không có bản cập nhật" +STR_UPDATE_FAILED: "Cập nhật thất bại" +STR_UPDATE_COMPLETE: "Cập nhật hoàn tất" +STR_POWER_ON_HINT: "Nhấn giữ nút nguồn để bật lại" +STR_RESTARTING_HINT: "Đang khởi động lại... Nếu máy không khởi động lại, hãy giữ nút nguồn vài giây." +STR_NO_ENTRIES: "Không tìm thấy mục nào" +STR_DOWNLOADING: "Đang tải về..." +STR_DOWNLOAD_FAILED: "Tải về thất bại" +STR_ERROR_MSG: "Lỗi:" +STR_UNNAMED: "Không tên" +STR_HOLD_CONFIRM_TO_DELETE: "Giữ Xác nhận để xóa" +STR_BOOKMARK_INSTRUCTIONS: "Giữ Xác nhận trong trình đọc để tạo dấu trang." +STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ" +STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp" +STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp" +STR_NEXT_PAGE: "Trang sau »" +STR_PREV_PAGE: "« Trang trước" +STR_NETWORK_PREFIX: "Mạng: " +STR_IP_ADDRESS_PREFIX: "Địa chỉ IP: " +STR_ERROR_GENERAL_FAILURE: "Lỗi: Sự cố chung" +STR_ERROR_NETWORK_NOT_FOUND: "Lỗi: Không tìm thấy mạng" +STR_ERROR_CONNECTION_TIMEOUT: "Lỗi: Kết nối quá thời gian" +STR_SD_CARD: "Thẻ SD" +STR_BACK: "« Quay lại" +STR_EXIT: "« Thoát" +STR_HOME: "« Thư viện" +STR_SELECT: "Chọn" +STR_SELECTED: "Đã chọn" +STR_TOGGLE: "Bật/Tắt" +STR_CONFIRM: "Xác nhận" +STR_CANCEL: "Hủy" +STR_CONNECT: "Kết nối" +STR_OPEN: "Mở" +STR_DOWNLOAD: "Tải về" +STR_RETRY: "Thử lại" +STR_YES: "Có" +STR_NO: "Không" +STR_SHOW: "Hiện" +STR_HIDE: "Ẩn" +STR_STATE_ON: "BẬT" +STR_STATE_OFF: "TẮT" +STR_NOT_SET: "Chưa đặt" +STR_DIR_LEFT: "Trái" +STR_DIR_RIGHT: "Phải" +STR_DIR_UP: "Lên" +STR_DIR_DOWN: "Xuống" +STR_OK_BUTTON: "OK" +STR_SLEEP_COVER_FILTER: "Bộ lọc ảnh bìa màn hình ngủ" +STR_FILTER_CONTRAST: "Tương phản" +STR_CUSTOMISE_STATUS_BAR: "Tùy chỉnh thanh trạng thái" +STR_CHAPTER_PAGE_COUNT: "Số trang trong chương" +STR_BOOK_PROGRESS_PERCENTAGE: "Phần trăm tiến độ sách" +STR_PROGRESS_BAR: "Thanh tiến độ" +STR_PROGRESS_BAR_THICKNESS: "Độ dày thanh tiến độ" +STR_PROGRESS_BAR_THIN: "Mỏng" +STR_PROGRESS_BAR_MEDIUM: "Vừa" +STR_PROGRESS_BAR_THICK: "Dày" +STR_BOOK: "Sách" +STR_CHAPTER: "Chương" +STR_EXAMPLE_CHAPTER: "Chương 21" +STR_EXAMPLE_BOOK: "Tên sách" +STR_PREVIEW: "Xem trước" +STR_TITLE: "Tiêu đề" +STR_BATTERY: "Pin" +STR_XTC_STATUS_BAR: "Thanh trạng thái XTC" +STR_BOTTOM: "Dưới" +STR_TOP: "Trên" +STR_CLOCK: "Đồng hồ" +STR_CLOCK_UTC_OFFSET: "Chênh lệch UTC" +STR_CLOCK_FORMAT: "Định dạng giờ" +STR_CLOCK_FORMAT_24H: "24 giờ" +STR_CLOCK_FORMAT_12H: "12 giờ" +STR_CURRENT_TIME: "Giờ hiện tại:" +STR_NEXT_FIELD: "Tiếp" +STR_CLOCK_SYNC: "Đồng bộ đồng hồ" +STR_CLOCK_SYNC_NOW: "Đồng bộ đồng hồ ngay" +STR_CLOCK_SYNCING: "Đang đồng bộ từ NTP..." +STR_CLOCK_SYNC_OK: "Đã đồng bộ đồng hồ" +STR_CLOCK_SYNC_FAIL: "Đồng bộ thất bại" +STR_CLOCK_SYNC_NO_WIFI: "Chưa kết nối Wi-Fi" +STR_CLOCK_SYNC_NO_WIFI_HINT: "Hãy kết nối Wi-Fi trước, rồi thử lại." +STR_CLOCK_SYNCED: "Đã đồng bộ đồng hồ" +STR_UI_THEME: "Giao diện" +STR_THEME_CLASSIC: "Cổ điển" +STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra mở rộng" +STR_SUNLIGHT_FADING_FIX: "Khắc phục mờ dưới nắng" +STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước" +STR_BOOKMARKS: "Dấu trang" +STR_BOOKMARK_ADDED: "Đã thêm dấu trang." +STR_OPDS_BROWSER: "Trình duyệt OPDS" +STR_SEARCH: "Tìm kiếm" +STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh" +STR_QUICK_RESUME: "Khôi phục nhanh" +STR_MENU_RECENT_BOOKS: "Sách gần đây" +STR_REMOVE_FROM_RECENTS: "Xóa khỏi sách gần đây?" +STR_NO_RECENT_BOOKS: "Không có sách gần đây" +STR_CALIBRE_DESC: "Dùng truyền không dây của Calibre" +STR_FORGET_AND_REMOVE: "Quên mạng và xóa mật khẩu đã lưu?" +STR_FORGET_BUTTON: "Quên" +STR_CALIBRE_STARTING: "Đang khởi động Calibre..." +STR_CALIBRE_SETUP: "Thiết lập" +STR_CALIBRE_STATUS: "Trạng thái" +STR_CLEAR_BUTTON: "Xóa" +STR_DEFAULT_VALUE: "Mặc định" +STR_REMAP_PROMPT: "Nhấn một nút mặt trước cho mỗi vai trò" +STR_UNASSIGNED: "Chưa gán" +STR_ALREADY_ASSIGNED: "Đã được gán" +STR_REMAP_RESET_HINT: "Nút bên Lên: Đặt lại bố trí mặc định" +STR_REMAP_CANCEL_HINT: "Nút bên Xuống: Hủy gán lại" +STR_HW_BACK_LABEL: "Quay lại (nút 1)" +STR_HW_CONFIRM_LABEL: "Xác nhận (nút 2)" +STR_HW_LEFT_LABEL: "Trái (nút 3)" +STR_HW_RIGHT_LABEL: "Phải (nút 4)" +STR_GO_TO_PERCENT: "Đến vị trí %" +STR_GO_HOME_BUTTON: "Về trang chủ" +STR_SYNC_PROGRESS: "Đồng bộ tiến độ" +STR_DELETE_CACHE: "Xóa bộ nhớ đệm sách" +STR_DELETE: "Xóa" +STR_CONFIRM_DELETE_BOOKMARK: "Xóa dấu trang này?" +STR_DISPLAY_QR: "Hiện trang dạng QR" +STR_CHAPTER_PREFIX: "Chương: " +STR_PAGES_SEPARATOR: " trang | " +STR_BOOK_PREFIX: "Sách: " +STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL" +STR_PERCENT_STEP_HINT: "Trái/Phải: 1% Lên/Xuống: 10%" +STR_SYNCING_TIME: "Đang đồng bộ giờ..." +STR_CALC_HASH: "Đang tính mã băm tài liệu..." +STR_HASH_FAILED: "Không tính được mã băm tài liệu" +STR_FETCH_PROGRESS: "Đang lấy tiến độ từ máy chủ..." +STR_UPLOAD_PROGRESS: "Đang tải tiến độ lên..." +STR_NO_CREDENTIALS_MSG: "Chưa cấu hình thông tin đăng nhập" +STR_KOREADER_SETUP_HINT: "Thiết lập tài khoản KOReader trong Cài đặt" +STR_PROGRESS_FOUND: "Đã tìm thấy tiến độ!" +STR_REMOTE_LABEL: "Máy chủ:" +STR_LOCAL_LABEL: "Cục bộ:" +STR_PAGE_OVERALL_FORMAT: "Trang %d, %.2f%% toàn bộ" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Trang %d/%d, %.2f%% toàn bộ" +STR_DEVICE_FROM_FORMAT: " Từ: %s" +STR_APPLY_REMOTE: "Áp dụng tiến độ máy chủ" +STR_UPLOAD_LOCAL: "Tải tiến độ cục bộ lên" +STR_NO_REMOTE_MSG: "Không có tiến độ trên máy chủ" +STR_UPLOAD_PROMPT: "Tải vị trí hiện tại lên?" +STR_UPLOAD_SUCCESS: "Đã tải tiến độ lên!" +STR_SYNC_FAILED_MSG: "Đồng bộ thất bại" +STR_SAVE_PROGRESS_FAILED: "Không lưu được tiến độ" +STR_SECTION_PREFIX: "Phần " +STR_UPLOAD: "Tải lên" +STR_BOOK_S_STYLE: "Kiểu của sách" +STR_EMBEDDED_STYLE: "Kiểu nhúng sẵn" +STR_FOCUS_READING: "Đọc tập trung" +STR_OPDS_SERVER_URL: "URL máy chủ OPDS" +STR_SET_SLEEP_COVER: "Đặt ảnh bìa" +STR_FOOTNOTES: "Chú thích" +STR_NO_FOOTNOTES: "Trang này không có chú thích" +STR_LINK: "[liên kết]" +STR_SCREENSHOT_BUTTON: "Chụp màn hình" +STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút" +STR_SLEEP_NEVER: "Không bao giờ" +STR_SLEEP_TIMER_STEP_HINT: "Trái/Phải: 1 phút Lên/Xuống: 5 phút" +STR_ADD_SERVER: "Thêm máy chủ" +STR_SERVER_NAME: "Tên máy chủ" +STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS" +STR_DELETE_SERVER: "Xóa máy chủ" +STR_OPDS_SERVERS: "Máy chủ OPDS" +STR_AUTO_TURN_ENABLED: "Tự lật trang: " +STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)" +STR_MANAGE_FONTS: "Quản lý phông chữ" +STR_FONT_BROWSER: "Trình duyệt phông chữ" +STR_LOADING_FONT_LIST: "Đang tải danh sách phông..." +STR_NO_FONTS_AVAILABLE: "Không có phông chữ" +STR_FONT_INSTALLED: "Đã cài phông chữ!" +STR_FONT_INSTALL_FAILED: "Cài phông chữ thất bại" +STR_INSTALLED: "Đã cài" +STR_DOWNLOAD_ALL: "Tải tất cả" +STR_UPDATE_ALL: "Cập nhật tất cả" +STR_UPDATE_AVAILABLE: "Cập nhật" +STR_CRASH_TITLE: "Lỗi hệ thống" +STR_CRASH_DESCRIPTION: "Báo cáo chi tiết đã được lưu vào crash_report.txt. Vui lòng đính kèm tập tin này khi báo lỗi." +STR_CRASH_REASON: "Lý do lỗi:" +STR_CRASH_NO_REASON: "(Không ghi nhận lý do)" +STR_TILT_PAGE_TURN: "Lật trang khi nghiêng" +STR_KB_HINT_MOVE_CURSOR: "Nhấn TRÁI hoặc PHẢI để di chuyển con trỏ" +STR_KB_HINT_RETURN_CURSOR: "Nhấn TRÁI để quay lại vị trí con trỏ" +STR_KB_HINT_HIDE_PASSWORD: "Giữ PHẢI rồi nhấn [***] để ẩn mật khẩu" +STR_KB_HINT_SHOW_PASSWORD: "Giữ PHẢI rồi nhấn [abc] để hiện mật khẩu" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Nhấn [***] để ẩn mật khẩu" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Nhấn [abc] để hiện mật khẩu" +STR_KB_HINT_EDIT_ENTRY: "Giữ LÊN để sửa mục" +STR_KB_TIPS: "Mẹo:" +STR_KB_HINT_RETURN_KEYBOARD: "Nhấn XUỐNG để quay lại bàn phím" +STR_KB_HINT_EXIT_URL_MODE: "Nhấn ABC để thoát chế độ URL" +STR_KB_HINT_CLEAR_TEXT: "Giữ DEL để xóa toàn bộ chữ" +STR_KB_HINT_SECONDARY_CHAR: "Giữ CHỌN để nhập ký tự phụ" +STR_KB_HINT_UPPER_SECONDARY: "Giữ CHỌN để viết HOA hoặc ký tự phụ" +STR_KB_HINT_LOWER_SECONDARY: "Giữ CHỌN để viết thường hoặc ký tự phụ" +STR_KB_HINT_URL_SNIPPETS: "Nhấn URL để chèn mẫu" +STR_SD_FIRMWARE_UPDATE: "Cập nhật firmware từ thẻ SD" +STR_SELECT_FIRMWARE_FILE: "Chọn tập tin firmware (.bin)" +STR_NO_BIN_FILES: "Không tìm thấy tập tin .bin" +STR_VALIDATING_FIRMWARE: "Đang kiểm tra firmware..." +STR_INVALID_FIRMWARE: "Tập tin firmware không hợp lệ" +STR_FIRMWARE_TOO_LARGE: "Firmware quá lớn so với phân vùng" +STR_FIRMWARE_TOO_SMALL: "Tập tin firmware quá nhỏ" +STR_FIRMWARE_UPDATE_PROMPT: "Cập nhật firmware?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Không mở được tập tin" +STR_FIRMWARE_WRITE_FAILED: "Ghi firmware thất bại" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!" +STR_RECOVERY_MODE: "Chế độ phục hồi" +STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn" From 1f83087aa5e522a486918cad13dbfa5f9edaac28 Mon Sep 17 00:00:00 2001 From: Hoang Manh Linh Date: Thu, 11 Jun 2026 19:11:50 +0700 Subject: [PATCH 07/14] feat(fonts): add Vietnamese glyph coverage to the built-in UI font (#2280) Co-authored-by: Claude Opus 4.8 (1M context) --- .../source/Ubuntu/Ubuntu-Vietnamese-Bold.ttf | Bin 0 -> 7960 bytes .../Ubuntu/Ubuntu-Vietnamese-Regular.ttf | Bin 0 -> 7888 bytes lib/EpdFont/builtinFonts/ubuntu_10_bold.h | 595 +++++++++++---- lib/EpdFont/builtinFonts/ubuntu_10_regular.h | 458 ++++++++--- lib/EpdFont/builtinFonts/ubuntu_12_bold.h | 716 +++++++++++++----- lib/EpdFont/builtinFonts/ubuntu_12_regular.h | 673 +++++++++++----- lib/EpdFont/scripts/convert-builtin-fonts.sh | 7 +- 7 files changed, 1823 insertions(+), 626 deletions(-) create mode 100644 lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Bold.ttf create mode 100644 lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Regular.ttf diff --git a/lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Bold.ttf b/lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2fcab561c7b7eb283ea7879a519b42a2a1c8e5a3 GIT binary patch literal 7960 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 I$p~8Xzge;z*#H0l literal 0 HcmV?d00001 diff --git a/lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Regular.ttf b/lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1a5e4a25b60d6bada9f96cbe4e9233ce2228c507 GIT binary patch literal 7888 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 $output_path echo "Generated $output_path" done From 301f1d1a38aec2bb052b8e390c0d54218dfa3771 Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Thu, 11 Jun 2026 15:18:53 +0300 Subject: [PATCH 08/14] fix: Compile Error: Duplicate _order values found (#2323) --- lib/I18n/translations/vietnamese.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 157b987c..b4ab0578 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -1,6 +1,6 @@ _language_name: "Tiếng Việt" _language_code: "VI" -_order: "24" +_order: "25" STR_CROSSPOINT: "CrossPoint" STR_BOOTING: "ĐANG KHỞI ĐỘNG" From 9202522a399ded8d125f0f1dea178868b7cac79b Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 11 Jun 2026 09:38:00 -0500 Subject: [PATCH 09/14] refactor: Drop redundant self-class prefix on applyDirectionToEntry (#2325) ## Summary Non-functional cleanup: `ChapterHtmlSlimParser::` prefix is redundant when calling `applyDirectionToEntry` within `ChapterHtmlSlimParser` methods. --- ### AI Usage Did you use AI tools to help write this code? _**NO**_ --- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 2688f334..c4b0ccd5 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -758,7 +758,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.depth = self->depth; entry.hasUnderline = true; entry.underline = true; - ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); @@ -843,7 +843,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasItalic = true; entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; } - ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); } else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) { @@ -866,7 +866,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasUnderline = true; entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; } - ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); } else if (matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS))) { @@ -889,7 +889,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasUnderline = true; entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; } - ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); } else if (strcmp(name, "sup") == 0 || strcmp(name, "sub") == 0) { @@ -931,7 +931,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasUnderline = true; entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; } - ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + applyDirectionToEntry(entry, cssStyle); if (cssStyle.hasVerticalAlign()) { if (cssStyle.verticalAlign == CssVerticalAlign::Super) { entry.hasSup = true; From f2e3d117dcf90c4fa2a8ade244cd848a5d9aeba5 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 11 Jun 2026 10:23:49 -0500 Subject: [PATCH 10/14] fix: Hanging indent causes overlapping words (#2324) --- lib/Epub/Epub/ParsedText.cpp | 10 +++++----- lib/Epub/Epub/Section.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 74a29d1a..d4d66144 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -799,7 +799,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const } for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) { - lineXPos.push_back(static_cast(xpos < 0 ? 0 : xpos)); + lineXPos.push_back(static_cast(xpos)); xpos += reorderedWidthsScratch[wordIdx]; const bool nextIsContinuation = @@ -830,7 +830,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const // Standard LTR/RTL positioning loop when no visual reordering is needed if (blockStyle.isRtl) { // RTL: position words from right to left - auto xpos = static_cast(effectivePageWidth); + int xpos = effectivePageWidth; if (effectiveAlignment == CssTextAlign::Left) { // Explicit left alignment in RTL context xpos = lineWordWidthSum + totalNaturalGaps; @@ -841,7 +841,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { xpos -= wordWidths[lastBreakAt + wordIdx]; - lineXPos.push_back(static_cast(xpos < 0 ? 0 : xpos)); + lineXPos.push_back(static_cast(xpos)); const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1]; if (nextIsContinuation) { @@ -867,7 +867,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const } } else { // LTR: position words from left to right - auto xpos = static_cast(firstLineIndent); + int xpos = firstLineIndent; if (effectiveAlignment == CssTextAlign::Right) { xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; } else if (effectiveAlignment == CssTextAlign::Center) { @@ -875,7 +875,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const } for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { - lineXPos.push_back(static_cast(xpos < 0 ? 0 : xpos)); + lineXPos.push_back(static_cast(xpos)); const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1]; if (nextIsContinuation) { diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 4ccfac0f..c13da526 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -10,7 +10,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 25; +constexpr uint8_t SECTION_FILE_VERSION = 26; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + From 6f05e401a1d64e21926db5628f2135537ea79420 Mon Sep 17 00:00:00 2001 From: Matteo Scopel Date: Thu, 11 Jun 2026 18:10:41 +0200 Subject: [PATCH 11/14] feat: add the Libre Baskerville font family (#2088) ## Summary This PR adds the [Libre Baskerville ](https://github.com/impallari/Libre-Baskerville) font family to the fonts available for download in CrossPoint. ## Additional Context Baskerville is a classic font family, widely available in some form or the other in most ereaders and widely used in publishing and in the academy. Libre Baskerville is a libre (SIL Open Font License v1) implementation of it, which makes it possible for CrossPoint to ship it without issues. ![Vita-e-destino_ch30_p1_13pct_99220.bmp](https://github.com/user-attachments/files/28070672/Vita-e-destino_ch30_p1_13pct_99220.bmp) --- ### AI Usage Did you use AI tools to help write this code? _**NO**_ --- lib/EpdFont/scripts/sd-fonts.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/EpdFont/scripts/sd-fonts.yaml b/lib/EpdFont/scripts/sd-fonts.yaml index c40441a9..8cb7f420 100644 --- a/lib/EpdFont/scripts/sd-fonts.yaml +++ b/lib/EpdFont/scripts/sd-fonts.yaml @@ -122,6 +122,16 @@ families: bold: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Bold.otf"} italic: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Italic.otf"} bolditalic: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-BoldItalic.otf"} + + - name: LibreBaskerville + description: "A serif reimplementing the classic Baskerville (Latin)" + intervals: latin-ext + sizes: [12, 14, 16, 18] + styles: + regular: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Regular.ttf"} + bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"} + italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"} + bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"} # ── Sans-serif ───────────────────────────────────────────────────────── From 21caebd6e43248621287a2d833dd1d5d49510491 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 12 Jun 2026 11:54:34 -0400 Subject: [PATCH 12/14] fix: use Noto Sans as punctuation fallback when generating downloadable SD fonts (#2331) --- lib/EpdFont/scripts/build-sd-fonts.py | 12 ++++++ lib/EpdFont/scripts/fontconvert_sdcard.py | 45 ++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/EpdFont/scripts/build-sd-fonts.py b/lib/EpdFont/scripts/build-sd-fonts.py index 8f981fbe..406cbb88 100755 --- a/lib/EpdFont/scripts/build-sd-fonts.py +++ b/lib/EpdFont/scripts/build-sd-fonts.py @@ -46,6 +46,7 @@ DEFAULT_CONFIG = SCRIPT_DIR / "sd-fonts.yaml" DEFAULT_OUTPUT = SCRIPT_DIR / "output" DOWNLOAD_DIR = SCRIPT_DIR / "downloaded_fonts" INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts" +DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf" def download_font(url: str, dest: Path) -> Path: @@ -186,12 +187,14 @@ def build_family( # Multi-style mode for style_name, font_path in resolved_styles.items(): cmd.extend([f"--{style_name}", str(font_path)]) + cmd.extend([f"--fallback-{style_name}", str(DEFAULT_FALLBACK_FONT)]) else: # Single-style mode style_name = next(iter(resolved_styles)) font_path = resolved_styles[style_name] cmd.append(str(font_path)) cmd.extend(["--style", style_name]) + cmd.extend([f"--fallback-{style_name}", str(DEFAULT_FALLBACK_FONT)]) cmd.extend(["--intervals", intervals]) cmd.extend(["--sizes", sizes]) @@ -330,6 +333,15 @@ def main(): print("ERROR: No families defined in config", file=sys.stderr) sys.exit(1) + if not DEFAULT_FALLBACK_FONT.exists() or not DEFAULT_FALLBACK_FONT.is_file(): + print( + "ERROR: Missing default fallback font: " + f"{DEFAULT_FALLBACK_FONT}\n" + "This font is required for fallback glyphs in SD font builds.", + file=sys.stderr, + ) + sys.exit(1) + # Filter if --only specified if args.only: only_names = set(args.only.split(",")) diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py index a6d9db5e..e2125be4 100755 --- a/lib/EpdFont/scripts/fontconvert_sdcard.py +++ b/lib/EpdFont/scripts/fontconvert_sdcard.py @@ -518,7 +518,8 @@ def extract_ligatures_fonttools(font_path, codepoints): return pairs -def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False): +def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False, + fallback_fontfile=None): """Rasterize all glyphs for one font style. Returns StyleRasterData.""" import freetype @@ -531,6 +532,10 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F # it before set_char_size() would waste work at the default size and risk # Invalid_Size_Handle on some fonts. face.set_char_size(size << 6, size << 6, 150, 150) + fallback_face = None + if fallback_fontfile: + fallback_face = freetype.Face(fallback_fontfile) + fallback_face.set_char_size(size << 6, size << 6, 150, 150) load_flags = freetype.FT_LOAD_RENDER if force_autohint: @@ -541,6 +546,11 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F if glyph_index > 0: face.load_glyph(glyph_index, load_flags) return face + if fallback_face: + fallback_glyph_index = fallback_face.get_char_index(code_point) + if fallback_glyph_index > 0: + fallback_face.load_glyph(fallback_glyph_index, load_flags) + return fallback_face return None # Validate intervals: remove codepoints not present in the font. @@ -552,7 +562,9 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F for i_start, i_end in intervals: start = i_start for code_point in range(i_start, i_end + 1): - if face.get_char_index(code_point) == 0: + has_primary = face.get_char_index(code_point) != 0 + has_fallback = fallback_face and fallback_face.get_char_index(code_point) != 0 + if not has_primary and not has_fallback: if start < code_point: validated_intervals.append((start, code_point - 1)) start = code_point + 1 @@ -763,10 +775,11 @@ def style_sections_total_size(sections): # --- File writers --- def generate_cpfont_multistyle(style_fonts, size, intervals, output_path, - force_autohint=False): + force_autohint=False, fallback_style_fonts=None): """Generate a multi-style v4 .cpfont file. style_fonts: dict of {style_id: fontfile_path} e.g. {0: "Regular.ttf", 2: "Italic.ttf"} + fallback_style_fonts: optional dict of {style_id: fallback_fontfile_path} """ MAGIC = b"CPFONT\x00\x00" HEADER_SIZE = 32 @@ -776,12 +789,15 @@ def generate_cpfont_multistyle(style_fonts, size, intervals, output_path, # Rasterize each style raster_data = {} # style_id -> StyleRasterData + fallback_style_fonts = fallback_style_fonts or {} for style_id in sorted(style_fonts.keys()): fontfile = style_fonts[style_id] + fallback_fontfile = fallback_style_fonts.get(style_id) print(f" Rasterizing style {style_id}...", file=sys.stderr) raster_data[style_id] = rasterize_font_style( fontfile, size, intervals, style_id=style_id, - force_autohint=force_autohint) + force_autohint=force_autohint, + fallback_fontfile=fallback_fontfile) # Pack binary sections for each style packed_sections = {} # style_id -> tuple of section bytearrays @@ -892,6 +908,14 @@ def main(): help="Font file for italic style.") parser.add_argument("--bolditalic", dest="font_bolditalic", help="Font file for bold-italic style.") + parser.add_argument("--fallback-regular", dest="fallback_regular", + help="Fallback font file for regular style.") + parser.add_argument("--fallback-bold", dest="fallback_bold", + help="Fallback font file for bold style.") + parser.add_argument("--fallback-italic", dest="fallback_italic", + help="Fallback font file for italic style.") + parser.add_argument("--fallback-bolditalic", dest="fallback_bolditalic", + help="Fallback font file for bold-italic style.") args = parser.parse_args() @@ -913,6 +937,16 @@ def main(): if args.font_bolditalic: style_fonts[3] = args.font_bolditalic + fallback_style_fonts = {} + if args.fallback_regular: + fallback_style_fonts[0] = args.fallback_regular + if args.fallback_bold: + fallback_style_fonts[1] = args.fallback_bold + if args.fallback_italic: + fallback_style_fonts[2] = args.fallback_italic + if args.fallback_bolditalic: + fallback_style_fonts[3] = args.fallback_bolditalic + is_multistyle = len(style_fonts) > 0 fontfile = args.fontfile @@ -980,7 +1014,8 @@ def main(): print(f"Generating {output_path} (size {sz}, {len(style_fonts)} style(s), v4)...", file=sys.stderr) total_size += generate_cpfont_multistyle( style_fonts, sz, intervals, output_path, - force_autohint=args.force_autohint) + force_autohint=args.force_autohint, + fallback_style_fonts=fallback_style_fonts) print(f"\nTotal: {len(sizes)} files, {total_size / 1024 / 1024:.2f} MB", file=sys.stderr) From 8634cb8ad351c5dc067803ae1110b0de9ad7caaf Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Fri, 12 Jun 2026 18:55:00 +0300 Subject: [PATCH 13/14] fix: Restore first-line paragraph indentation (#2320) --- lib/Epub/Epub/ParsedText.cpp | 39 ++++++++++++------------------ lib/Epub/Epub/ParsedText.h | 3 +-- lib/Epub/Epub/blocks/BlockStyle.h | 2 +- lib/Epub/Epub/blocks/TextBlock.cpp | 21 ++-------------- 4 files changed, 19 insertions(+), 46 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index d4d66144..b66b36ee 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -261,10 +261,18 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, } } -int ParsedText::resolveFirstLineIndent(const bool isFirstLine) const { - if (isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && - isNaturalAlign) { - return blockStyle.textIndent; +int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer& renderer, const int fontId) const { + if (!isFirstLine || !isNaturalAlign) { + return 0; + } + if (blockStyle.textIndentDefined) { + if (blockStyle.textIndent < 0 || !extraParagraphSpacing) { + return blockStyle.textIndent; + } + return 0; + } + if (!extraParagraphSpacing) { + return renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR) * 3; } return 0; } @@ -293,9 +301,6 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo blockStyle.alignment == CssTextAlign::Justify || (blockStyle.isRtl ? blockStyle.alignment == CssTextAlign::Right : blockStyle.alignment == CssTextAlign::Left); - // Apply fixed transforms before any per-line layout work. - applyParagraphIndent(); - // Ensure SD card font glyph metrics are loaded before measuring word widths. // For flash-based fonts isSdCardFont() returns false and this block is skipped // entirely — no heap allocation. For SD card fonts this reads glyph metadata @@ -356,7 +361,7 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c return {}; } - const int firstLineIndent = resolveFirstLineIndent(true); + const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId); // Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation. for (size_t i = 0; i < wordWidths.size(); ++i) { @@ -462,25 +467,11 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c return lineBreakIndices; } -void ParsedText::applyParagraphIndent() { - if (extraParagraphSpacing || words.empty()) { - return; - } - - if (blockStyle.textIndentDefined) { - // CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace - // The actual indent positioning is handled in extractLine() - } else if (isNaturalAlign) { - // No CSS text-indent defined - use EmSpace fallback for visual indent - words.front().insert(0, "\xe2\x80\x83"); - } -} - // Builds break indices while opportunistically splitting the word that would overflow the current line. std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector& wordWidths, std::vector& continuesVec) { - const int firstLineIndent = resolveFirstLineIndent(true); + const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId); std::vector lineBreakIndices; size_t currentIndex = 0; @@ -643,7 +634,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; const size_t lineWordCount = lineBreak - lastBreakAt; - const int firstLineIndent = resolveFirstLineIndent(breakIndex == 0); + const int firstLineIndent = resolveFirstLineIndent(breakIndex == 0, renderer, fontId); // Build line data by moving from the original vectors using index range std::vector lineWords; diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index df5dd3f1..9c3af7cb 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -30,8 +30,7 @@ class ParsedText { std::vector reorderedFocusSuffixScratch; std::vector visualOrderScratch; - void applyParagraphIndent(); - int resolveFirstLineIndent(bool isFirstLine) const; + int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const; std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector& wordWidths, std::vector& continuesVec); std::vector computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index 23ed790b..fbc18d42 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -115,7 +115,7 @@ struct BlockStyle { blockStyle.paddingRight = std::min(cssStyle.paddingRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx); // For textIndent: if it's a percentage we can't resolve (no viewport width), - // leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used + // leave textIndentDefined=false so the space-width fallback in resolveFirstLineIndent() is used if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) { blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw); blockStyle.textIndentDefined = true; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 96c96f51..9260fa32 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -61,31 +61,14 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) { const std::string& w = words[i]; - const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir); - // y is the top of the text line; add ascender to reach baseline, then offset 2px below + int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir); const int underlineY = wordY + ascender + 2; - int startX = wordX; - int underlineWidth = fullWordWidth; - - // if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line - if (w.size() >= 3 && static_cast(w[0]) == 0xE2 && static_cast(w[1]) == 0x80 && - static_cast(w[2]) == 0x83) { - const char* visiblePtr = w.c_str() + 3; - const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle); - const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle, baseDir); - startX = wordX + prefixWidth; - underlineWidth = visibleWidth; - } - - // SUP/SUB words are rendered at 50% glyph scale (see the baseline comment - // above and drawText), but getTextWidth reports the full-size width, so the - // underline would be drawn ~2x too long. Halve it to match the scaled glyphs. if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { underlineWidth = (underlineWidth + 1) / 2; } - renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true); + renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true); } } } From 55a56914d7ac5a1a75700d5fc03e7fd6d5ef8fd2 Mon Sep 17 00:00:00 2001 From: Mauvis Ledford Date: Mon, 15 Jun 2026 02:40:05 +0800 Subject: [PATCH 14/14] fix(i18n): localize home empty-state strings (#2342) --- src/components/themes/BaseTheme.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 87d5af23..caa7e6cc 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -635,8 +635,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // No book to continue reading const int y = bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book"); - renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); + renderer.drawCenteredText(UI_12_FONT_ID, y, tr(STR_NO_OPEN_BOOK)); + renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), tr(STR_START_READING)); } }