Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/ebooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ jobs:
strategy:
fail-fast: false
matrix:
lang: [en, ru, zh, es, pt]
# CI matrix may be a subset of translate/langs.json; keep in sync when adding ebook locales.
lang: [en, ru, zh, es, pt, ar]
steps:
- uses: actions/checkout@v4

Expand All @@ -56,10 +57,16 @@ jobs:
- name: Manifest tests
run: node --test forge/ebook/book.test.mjs

- name: Install pandoc, typst, and CJK fonts
- name: Install pandoc, typst, and CJK + Arabic fonts
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends fonts-noto-cjk
mkdir -p ~/.fonts
for face in Regular Bold Italic BoldItalic; do
curl -sSL -o ~/.fonts/Amiri-$face.ttf \
"https://github.com/google/fonts/raw/main/ofl/amiri/Amiri-$face.ttf"
done
fc-cache -f ~/.fonts
curl -sSL -o pandoc.tgz \
"https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz"
tar xzf pandoc.tgz
Expand All @@ -75,6 +82,7 @@ jobs:
- name: PDF
run: |
typst fonts | grep -i -E 'noto (serif|sans) cjk'
typst fonts | grep -i 'amiri'
node forge/ebook/pdf/build.mjs --lang ${{ matrix.lang }}

- uses: actions/setup-java@v4
Expand Down Expand Up @@ -127,12 +135,12 @@ jobs:
set -euo pipefail
mapfile -t files < <(find dist -maxdepth 1 -type f \
\( -name 'HowToLiveBetter-*.epub' -o -name 'HowToLiveBetter-*.pdf' \) | sort)
if [ "${#files[@]}" -ne 10 ]; then
echo "expected 10 assets, got ${#files[@]}"
if [ "${#files[@]}" -ne 12 ]; then
echo "expected 12 assets, got ${#files[@]}"
ls -l dist
exit 1
fi
notes="Built from ${GITHUB_SHA::7}. EPUB and PDF for en, ru, zh, es, pt."
notes="Built from ${GITHUB_SHA::7}. EPUB and PDF for en, ru, zh, es, pt, ar."
notes="$notes Assets are replaced on each main build; download URLs stay fixed."
if gh release view ebooks-latest >/dev/null 2>&1; then
gh release edit ebooks-latest --title "Ebooks (EPUB / PDF)" --notes "$notes"
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ YAMLLINT = .venv/bin/yamllint
.PHONY: help sync-upstream digest assemble verify verify-all wave repair status lint format test test-integration ci og og-html update-readme hooks check-commit-msg pages-artifact serve web-build quality style lt check-content check-links ebook-deps ebook-test ebook-epub ebook-pdf ebooks

# Locale list must stay in sync with translate/langs.json (registry).
OG_HTML = forge/og/en.html forge/og/ru.html forge/og/es.html forge/og/zh.html forge/og/pt.html
OG_HTML = forge/og/en.html forge/og/ru.html forge/og/es.html forge/og/zh.html forge/og/pt.html forge/og/ar.html

check-content: ## CJK-leak, parity, readme-badge checks
$(PY) forge/ops/check_content.py
Expand Down Expand Up @@ -148,7 +148,7 @@ og-html: ## Render forge/og/{en,ru,es,zh,pt}.html from _template.html

og: og-html ## Regenerate OG PNGs from forge/og/*.html → site/assets/og/
@mkdir -p site/assets/og
@for lang in en ru es zh pt; do \
@for lang in en ru es zh pt ar; do \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--headless --disable-gpu --hide-scrollbars \
--force-device-scale-factor=1 --window-size=1200,630 \
Expand Down
2 changes: 1 addition & 1 deletion README.ar.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

| اللغة | الموقع | README | PDF | EPUB |
| --- | --- | --- | --- | --- |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |
| 🇬🇧 English | [Read on the site](https://dlgrv.github.io/HowToLiveBetter/en/) | [README.md](README.md) | [Download PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.pdf) | [Download EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.epub) |
| 🇷🇺 Русский | [Читать на сайте](https://dlgrv.github.io/HowToLiveBetter/ru/) | [README.ru.md](README.ru.md) | [Скачать PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.pdf) | [Скачать EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.epub) |
| 🇨🇳 中文 | [在网站阅读](https://dlgrv.github.io/HowToLiveBetter/zh/) | [README.zh.md](README.zh.md) | [下载 PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.pdf) | [下载 EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.epub) |
Expand Down
2 changes: 1 addition & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You don't have to do it all: this is a ranked shortlist, not a task list — pic
| --- | --- | --- | --- | --- |
| 🇪🇸 Español | [Leer en el sitio](https://dlgrv.github.io/HowToLiveBetter/es/) | [README.es.md](README.es.md) | [Descargar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.pdf) | [Descargar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.epub) |
| 🇧🇷 Português | [Ler no site](https://dlgrv.github.io/HowToLiveBetter/pt/) | [README.pt.md](README.pt.md) | [Baixar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.pdf) | [Baixar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.epub) |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |
| 🇬🇧 English | [Read on the site](https://dlgrv.github.io/HowToLiveBetter/en/) | [README.md](README.md) | [Download PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.pdf) | [Download EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.epub) |
| 🇷🇺 Русский | [Читать на сайте](https://dlgrv.github.io/HowToLiveBetter/ru/) | [README.ru.md](README.ru.md) | [Скачать PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.pdf) | [Скачать EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.epub) |
| 🇨🇳 中文 | [在网站阅读](https://dlgrv.github.io/HowToLiveBetter/zh/) | [README.zh.md](README.zh.md) | [下载 PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.pdf) | [下载 EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.epub) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Covers longevity and disease prevention, accidents and first aid, saving money a
| 🇨🇳 中文 | [在网站阅读](https://dlgrv.github.io/HowToLiveBetter/zh/) | [README.zh.md](README.zh.md) | [下载 PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.pdf) | [下载 EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.epub) |
| 🇪🇸 Español | [Leer en el sitio](https://dlgrv.github.io/HowToLiveBetter/es/) | [README.es.md](README.es.md) | [Descargar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.pdf) | [Descargar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.epub) |
| 🇧🇷 Português | [Ler no site](https://dlgrv.github.io/HowToLiveBetter/pt/) | [README.pt.md](README.pt.md) | [Baixar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.pdf) | [Baixar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.epub) |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |

---

Expand Down
2 changes: 1 addition & 1 deletion README.pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Você não precisa fazer tudo: é uma lista ordenada por retorno, não uma lista
| Idioma | Site | README | PDF | EPUB |
| --- | --- | --- | --- | --- |
| 🇧🇷 Português | [Ler no site](https://dlgrv.github.io/HowToLiveBetter/pt/) | [README.pt.md](README.pt.md) | [Baixar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.pdf) | [Baixar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.epub) |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |
| 🇬🇧 English | [Read on the site](https://dlgrv.github.io/HowToLiveBetter/en/) | [README.md](README.md) | [Download PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.pdf) | [Download EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-en.epub) |
| 🇷🇺 Русский | [Читать на сайте](https://dlgrv.github.io/HowToLiveBetter/ru/) | [README.ru.md](README.ru.md) | [Скачать PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.pdf) | [Скачать EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.epub) |
| 🇨🇳 中文 | [在网站阅读](https://dlgrv.github.io/HowToLiveBetter/zh/) | [README.zh.md](README.zh.md) | [下载 PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.pdf) | [下载 EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.epub) |
Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
| 🇨🇳 中文 | [在网站阅读](https://dlgrv.github.io/HowToLiveBetter/zh/) | [README.zh.md](README.zh.md) | [下载 PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.pdf) | [下载 EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-zh.epub) |
| 🇪🇸 Español | [Leer en el sitio](https://dlgrv.github.io/HowToLiveBetter/es/) | [README.es.md](README.es.md) | [Descargar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.pdf) | [Descargar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.epub) |
| 🇧🇷 Português | [Ler no site](https://dlgrv.github.io/HowToLiveBetter/pt/) | [README.pt.md](README.pt.md) | [Baixar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.pdf) | [Baixar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.epub) |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |

---

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ AI 助手 skill 支持 Claude Code 和 Codex。装上后直接问「替朋友担
| 🇷🇺 Русский | [Читать на сайте](https://dlgrv.github.io/HowToLiveBetter/ru/) | [README.ru.md](README.ru.md) | [Скачать PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.pdf) | [Скачать EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ru.epub) |
| 🇪🇸 Español | [Leer en el sitio](https://dlgrv.github.io/HowToLiveBetter/es/) | [README.es.md](README.es.md) | [Descargar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.pdf) | [Descargar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-es.epub) |
| 🇧🇷 Português | [Ler no site](https://dlgrv.github.io/HowToLiveBetter/pt/) | [README.pt.md](README.pt.md) | [Baixar PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.pdf) | [Baixar EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-pt.epub) |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | — | — |
| 🇸🇦 العربية | [اقرأ على الموقع](https://dlgrv.github.io/HowToLiveBetter/ar/) | [README.ar.md](README.ar.md) | [تنزيل PDF](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.pdf) | [تنزيل EPUB](https://github.com/dlgrv/HowToLiveBetter/releases/download/ebooks-latest/HowToLiveBetter-ar.epub) |

---

Expand Down
18 changes: 18 additions & 0 deletions forge/ebook/book.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ export const read = (rel) => readFileSync(resolveRepoFile(rel), 'utf8').replace(

export const unique = (arr) => [...new Set(arr)];

// Right-to-left locales. EPUB spine page-progression-direction, XHTML dir
// attributes, and Typst running-head mirroring key off this (see epub/build.mjs
// and pdf/template.typ). Typst itself infers paragraph direction from
// `#set text(lang: "ar")`, so no per-paragraph tagging is needed.
export const RTL_LANGS = new Set(['ar']);
export const isRtl = (code) => RTL_LANGS.has(code);

// Stable per-locale EPUB identifiers. Every code in translate/langs.json needs
// one (loadLangs throws otherwise); keep values unique across locales.
export const EBOOK_IDS = {
en: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000001',
ru: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000002',
zh: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000003',
es: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000004',
pt: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000005',
ar: 'urn:uuid:6f0e2a10-7b21-4c3a-9d11-000000000006',
};

export function loadLangs() {
const raw = JSON.parse(read('translate/langs.json'));
return raw.languages.map((row) => {
Expand Down
22 changes: 21 additions & 1 deletion forge/ebook/book.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { ensureH1, fitEpubTableColumns, fitTypstTableColumns, formatEntryFields, headingText, isChapterPath, localeFor, navLabel, prepareSection, promoteItemHeadings, read, readBook, renderEpubHtmlToken, requireRepoFile, stripBackLink, stripSourceLines, tableColumnFr, twoColumnTableFr } from './book.mjs';
import { ensureH1, fitEpubTableColumns, fitTypstTableColumns, formatEntryFields, headingText, isChapterPath, localeFor, loadLangs, navLabel, prepareSection, promoteItemHeadings, read, readBook, renderEpubHtmlToken, requireRepoFile, stripBackLink, stripSourceLines, tableColumnFr, twoColumnTableFr, isRtl, EBOOK_IDS } from './book.mjs';

const EXPECTED_CHAPTERS = 34;

Expand Down Expand Up @@ -169,6 +169,7 @@ test('pdf and epub keep field gaps above body leading', () => {
assert.match(css, /p \{ margin: 0 0 0\.75em; \}/);
assert.match(css, /dl\.entry dt \{[^}]*margin:\s*0\.85em/);
assert.match(css, /nav#toc > ol > li \{[^}]*margin:\s*0\.55em/);
assert.match(css, /padding-inline-start:\s*1\.4em/);
});

test('nav labels decode entities before escaping', () => {
Expand Down Expand Up @@ -280,3 +281,22 @@ test('missing chapter file fails', () => {
assert.throws(() => requireRepoFile('book/99-missing.md'), /missing book\/99-missing\.md/);
assert.throws(() => readBook('nope'), /unknown --lang/);
});

test('every locale has a unique ebook id and RTL is ar-only', () => {
const codes = loadLangs().map((row) => row.code);
for (const code of codes) {
assert.match(EBOOK_IDS[code] ?? '', /^urn:uuid:[0-9a-f-]+$/, code);
}
assert.equal(new Set(Object.values(EBOOK_IDS)).size, Object.keys(EBOOK_IDS).length);
assert.equal(isRtl('ar'), true);
for (const code of codes.filter((c) => c !== 'ar')) {
assert.equal(isRtl(code), false);
}
});

test('PDF template carries Arabic fonts and an RTL branch', () => {
const typ = read('forge/ebook/pdf/template.typ');
assert.match(typ, /"Amiri", "Noto Naskh Arabic"/);
assert.match(typ, /\$if\(rtl\)\$/);
assert.match(typ, /align\(start, it\)/);
});
Loading
Loading