diff --git a/plugin-designs/bible-posters/bible-posters.vue b/plugin-designs/bible-posters/bible-posters.vue index e43eeae89..1f29d2fae 100644 --- a/plugin-designs/bible-posters/bible-posters.vue +++ b/plugin-designs/bible-posters/bible-posters.vue @@ -43,7 +43,7 @@ const offeredBooks = computed(() => booksForTranslation(availableBookIds.value)) const plan = computed(() => (book.value ? planBiblePoster(size.value, book.value) : null)) const screen = computed(() => { if (!book.value) return null - return screenBiblePoster(book.value, size.value, { credit: translation.value.credit, dir: translation.value.dir }) + return screenBiblePoster(book.value, size.value, { credit: translation.value.credit, dir: translation.value.dir, lang: translation.value.bcp47 }) }) // Which of the ladder's sizes fit this book, for the "which size" guidance. @@ -89,7 +89,7 @@ async function loadBook() { function print() { if (!book.value) return - const html = buildBiblePosterHtml(book.value, size.value, { credit: translation.value.credit, dir: translation.value.dir }) + const html = buildBiblePosterHtml(book.value, size.value, { credit: translation.value.credit, dir: translation.value.dir, lang: translation.value.bcp47 }) printDocument(html, { labels: { print: t('biblePosters.print'), close: t('biblePosters.close') } }) } @@ -160,7 +160,7 @@ onBeforeUnmount(() => { if (ro) ro.disconnect() }) diff --git a/plugin-designs/bible-posters/index.js b/plugin-designs/bible-posters/index.js index 7d74642b4..757a92145 100644 --- a/plugin-designs/bible-posters/index.js +++ b/plugin-designs/bible-posters/index.js @@ -19,8 +19,9 @@ const profileModules = import.meta.glob('./*.vue') const BiblePostersElement = defineMapElement({ profileModules, configureApp: (app, { pinia, i18n }) => { - // Install i18n AND hand the instance to ProfileLoader by reference; the element root cannot call - // useI18n() itself, so this is how a host sets the plugin's interface language. + // defineMapElement already installs Pinia + i18n and provides the i18n instance to ProfileLoader + // (that is how a host sets the interface language). This hook is only for extra app-level provides; + // Bible Posters needs none. }, }) diff --git a/plugin-designs/bible-posters/src/biblePoster.js b/plugin-designs/bible-posters/src/biblePoster.js index 32afc7af0..774aa6f5a 100644 --- a/plugin-designs/bible-posters/src/biblePoster.js +++ b/plugin-designs/bible-posters/src/biblePoster.js @@ -132,13 +132,14 @@ function posterCss(plan, dir, unit) { /** The print document: one page, the whole book. `texts` supplies the credit line and optional dir/lang. */ export function buildBiblePosterHtml(book, size, texts = {}) { const dir = texts.dir || book.dir || 'ltr' + const lang = texts.lang || '' // a BCP-47 tag so hyphens:auto works and the page declares its language const plan = planBiblePoster(size, book) const css = '@page{size:' + size.widthIn + 'in ' + size.heightIn + 'in;margin:0}html,body{margin:0;padding:0}' + posterCss(plan, dir, 1) const inner = '

' + esc(book.name) + '

' + '
' + bodyHtml(book) + '
' + '
' + esc(texts.credit || '') + '
' - return '' + return '' + '' + esc(book.name) + '' + '
' + inner + '
' } @@ -151,5 +152,5 @@ export function screenBiblePoster(book, size, texts = {}) { const html = '

' + esc(book.name) + '

' + '
' + bodyHtml(book) + '
' + '
' + esc(texts.credit || '') + '
' - return { css, html, dir, plan, worldW: size.widthIn * PX_IN, worldH: size.heightIn * PX_IN } + return { css, html, dir, lang: texts.lang || '', plan, worldW: size.widthIn * PX_IN, worldH: size.heightIn * PX_IN } } diff --git a/plugin-designs/bible-posters/src/bibleSource.js b/plugin-designs/bible-posters/src/bibleSource.js index dbfcd71de..62fd5560e 100644 --- a/plugin-designs/bible-posters/src/bibleSource.js +++ b/plugin-designs/bible-posters/src/bibleSource.js @@ -72,26 +72,35 @@ export async function fetchBook(translationId, bookId, { signal, onProgress, dir const chapterBlocks = new Array(total) // keep chapter order regardless of fetch order let done = 0 + // One book is one atomic fetch: a chapter failure ABORTS the siblings (never a half book), while still + // honouring the caller's own signal. An internal controller chains to it so we can abort from inside. + const internal = new AbortController() + const onExt = () => internal.abort() + if (signal) { if (signal.aborted) internal.abort(); else signal.addEventListener('abort', onExt, { once: true }) } + async function one(ch) { - const d = await getJson(`${API_BASE}/${translationId}/${bookId}/${ch}.json`, signal) - const content = (d.chapter || d).content || [] - const blocks = [] - for (const item of content) { - if (item.type === 'heading') { const t = itemText(item); if (t) blocks.push({ kind: 'heading', chapter: ch, text: t }) } - else if (item.type === 'verse') { const t = itemText(item); if (t) blocks.push({ kind: 'verse', chapter: ch, number: item.number, text: t }) } - } - chapterBlocks[ch - 1] = blocks - done += 1 - onProgress?.(done, total) + try { + const d = await getJson(`${API_BASE}/${translationId}/${bookId}/${ch}.json`, internal.signal) + const content = (d.chapter || d).content || [] + const blocks = [] + for (const item of content) { + if (item.type === 'heading') { const t = itemText(item); if (t) blocks.push({ kind: 'heading', chapter: ch, text: t }) } + else if (item.type === 'verse') { const t = itemText(item); if (t) blocks.push({ kind: 'verse', chapter: ch, number: item.number, text: t }) } + } + chapterBlocks[ch - 1] = blocks + done += 1 + onProgress?.(done, total) + } catch (e) { internal.abort(); throw e } // stop the other workers on the first failure } // bounded-concurrency pool over chapters 1..total const queue = [] for (let ch = 1; ch <= total; ch++) queue.push(ch) const workers = new Array(Math.min(concurrency, total)).fill(0).map(async () => { - while (queue.length) { const ch = queue.shift(); await one(ch) } + while (queue.length && !internal.signal.aborted) { const ch = queue.shift(); await one(ch) } }) - await Promise.all(workers) + try { await Promise.all(workers) } + finally { if (signal) signal.removeEventListener('abort', onExt) } const blocks = chapterBlocks.flat().filter(Boolean) let verseCount = 0, charCount = 0 diff --git a/plugin-designs/bible-posters/src/translations.js b/plugin-designs/bible-posters/src/translations.js index 8220b33e1..73024a880 100644 --- a/plugin-designs/bible-posters/src/translations.js +++ b/plugin-designs/bible-posters/src/translations.js @@ -34,29 +34,29 @@ /** v1 STRICT PUBLIC-DOMAIN seed — the packet's 6 + WEB. Every entry has all 66 books (OT+NT). */ export const TRANSLATIONS = [ - { id: 'eng_web', iso639_3: 'eng', language: 'English', endonym: 'English', name: 'World English Bible', + { id: 'eng_web', iso639_3: 'eng', bcp47: 'en', language: 'English', endonym: 'English', name: 'World English Bible', dir: 'ltr', licence: 'public-domain', - credit: 'World English Bible · public domain · ebible.org (via bible.helloao.org)', + credit: 'World English Bible · public domain · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=eng-web' }, - { id: 'spa_r09', iso639_3: 'spa', language: 'Spanish', endonym: 'Español', name: 'Reina Valera 1909', + { id: 'spa_r09', iso639_3: 'spa', bcp47: 'es', language: 'Spanish', endonym: 'Español', name: 'Reina Valera 1909', dir: 'ltr', licence: 'public-domain', - credit: 'Reina Valera 1909 · dominio público · ebible.org (via bible.helloao.org)', + credit: 'Reina Valera 1909 · dominio público · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=spaRV1909' }, - { id: 'fra_ost', iso639_3: 'fra', language: 'French', endonym: 'Français', name: 'French Ostervald Bible', + { id: 'fra_ost', iso639_3: 'fra', bcp47: 'fr', language: 'French', endonym: 'Français', name: 'French Ostervald Bible', dir: 'ltr', licence: 'public-domain', - credit: 'Bible Ostervald · domaine public · ebible.org (via bible.helloao.org)', + credit: 'Bible Ostervald · domaine public · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=fra_fob' }, - { id: 'deu_l12', iso639_3: 'deu', language: 'German', endonym: 'Deutsch', name: 'German Luther Bible 1912', + { id: 'deu_l12', iso639_3: 'deu', bcp47: 'de', language: 'German', endonym: 'Deutsch', name: 'German Luther Bible 1912', dir: 'ltr', licence: 'public-domain', - credit: 'Lutherbibel 1912 · gemeinfrei · ebible.org (via bible.helloao.org)', + credit: 'Lutherbibel 1912 · gemeinfrei · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=deu1912' }, - { id: 'rus_syn', iso639_3: 'rus', language: 'Russian', endonym: 'Русский', name: 'Russian Synodal Bible', + { id: 'rus_syn', iso639_3: 'rus', bcp47: 'ru', language: 'Russian', endonym: 'Русский', name: 'Russian Synodal Bible', dir: 'ltr', licence: 'public-domain', - credit: 'Синодальный перевод · общественное достояние · ebible.org (via bible.helloao.org)', + credit: 'Синодальный перевод · общественное достояние · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=russyn' }, - { id: 'arb_vdv', iso639_3: 'arb', language: 'Arabic', endonym: 'العربية', name: 'Arabic Van Dyck Bible', + { id: 'arb_vdv', iso639_3: 'arb', bcp47: 'ar', language: 'Arabic', endonym: 'العربية', name: 'Arabic Van Dyck Bible', dir: 'rtl', licence: 'public-domain', - credit: 'فان دايك · ملك عام · ebible.org (via bible.helloao.org)', + credit: 'فان دايك · ملك عام · ebible.org · bible.helloao.org', licenseUrl: 'https://ebible.org/Scriptures/details.php?id=arb-vd' }, ]