Bible Posters: language-tagged columns, atomic book fetch, cleaner credits
Address studio's 5B nits (non-blocking, folded in before the first render): - Tag each translation with a BCP-47 code and set lang on the poster (html + body) so hyphens:auto works in narrow justified columns — removes rivers on Latin text. - Fetch a whole book atomically: a single chapter failure now aborts the sibling requests and surfaces the error, instead of leaving a half book. - Drop the English word "via" from the per-poster credit lines (a bare separator). - Correct the entry comment: defineMapElement installs i18n; the hook only adds provides.
This commit is contained in:
parent
5c44b3cdcd
commit
cdf0df268d
5 changed files with 42 additions and 31 deletions
|
|
@ -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() })
|
|||
<template v-else-if="screen">
|
||||
<div class="bpx-scaler" :style="{ width: screen.worldW + 'px', height: screen.worldH + 'px', transform: 'scale(' + previewScale + ')' }">
|
||||
<component :is="'style'">{{ screen.css }}</component>
|
||||
<div class="bp" v-html="screen.html"></div>
|
||||
<div class="bp" :lang="screen.lang || null" :dir="screen.dir" v-html="screen.html"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = '<div class="bp-head"><h1 class="bp-title" dir="auto">' + esc(book.name) + '</h1></div>'
|
||||
+ '<div class="bp-body" dir="auto">' + bodyHtml(book) + '</div>'
|
||||
+ '<div class="bp-credit">' + esc(texts.credit || '') + '</div>'
|
||||
return '<!doctype html><html' + (dir === 'rtl' ? ' dir="rtl"' : '') + '><head><meta charset="utf-8">'
|
||||
return '<!doctype html><html' + (lang ? ' lang="' + esc(lang) + '"' : '') + (dir === 'rtl' ? ' dir="rtl"' : '') + '><head><meta charset="utf-8">'
|
||||
+ '<title>' + esc(book.name) + '</title><style>' + css + '</style></head>'
|
||||
+ '<body><div class="bp">' + inner + '</div></body></html>'
|
||||
}
|
||||
|
|
@ -151,5 +152,5 @@ export function screenBiblePoster(book, size, texts = {}) {
|
|||
const html = '<div class="bp-head"><h1 class="bp-title" dir="auto">' + esc(book.name) + '</h1></div>'
|
||||
+ '<div class="bp-body" dir="auto">' + bodyHtml(book) + '</div>'
|
||||
+ '<div class="bp-credit">' + esc(texts.credit || '') + '</div>'
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue