doxa-maps/library/packet/packetStrings.js
Doxa Maps a3e58ac7ca Serve the packet text at print time instead of bundling all 11 languages
A single-file build inlines every import, dynamic ones included, so
packet-strings.json (~115 KB minified: every language's prayer points,
captions and headings) rode in every bundle that can print a packet, list
or card deck. With the wall's print messages translated, country-posters
crossed the 1.5 MB ceiling (1,502,226 bytes).

The table is now served at <assets>/lib/packet-strings.json and fetched at
the first print by library/packet/packetStrings.js, the same move as the
sheet generator and ne-places.json:

- Pinned like them: PACKET_STRINGS_SHA384 in poster-render.integrity.js
  (pin-poster-render.mjs); the text is hashed before it is parsed, and an
  unmatched table is not used. These strings are laid into the print
  document, so an unverified copy never reaches it.
- English is bundled as the fallback (a named import of the JSON, so only
  the en block ships): with no asset base, on file://, on a network error or
  an integrity mismatch the print comes out in English and the console says
  why. A print never fails for want of this file.
- generate-tree copies it into the pool AND the served allowlist; verified
  by regenerating the tree (the served copy is byte-identical to source).
- check:poster-render-pinned, two-sided: the pin matches source and served
  copy, the genuine copy loads all 11 languages, a tampered copy falls back
  to English (mutation-tested: disabling the hash check fails it), and an
  unserved one falls back rather than failing.
- sha384 is one shared helper (library/packet/sha384.js).

Sizes: country-posters 1,378,463 (was 1,484,053), my-upg-100-list 1,143,456
(was 1,243,509), regions-dashboard 973,802 (was 1,079,448).
2026-09-29 08:33:44 +00:00

49 lines
2.7 KiB
JavaScript

/**
* packetStrings — the text of every printed packet, list and card deck, in all 11 languages, FETCHED at print
* time from the served tree (<assets>/lib/packet-strings.json) instead of bundled.
*
* WHY. A single-file build inlines every import, dynamic ones included, so the whole table (~115 KB
* minified: every language's prayer points, captions and headings) rode in every bundle that can print —
* country-posters crossed the 1.5 MB ceiling once its wall's print messages were translated. A reader needs
* the table only when they print. Same move as the sheet generator and ne-places.json (posterSheets.js).
*
* PINNED like them (PACKET_STRINGS_SHA384): the text is hashed before it is parsed, and a table that is not
* the pinned one is not used. These strings are laid into the print document, so an unverified copy must
* never reach it.
*
* ★ ENGLISH IS BUNDLED AS THE FALLBACK, so a print never fails for want of this file: without an asset base
* (a host that sets none), on file:// where a page cannot fetch, on a network error, or on an integrity
* mismatch, the packet prints in English and the console says why. `{ en }` is a named import of the JSON, so
* only the English block is bundled; the other ten languages stay on the server.
*/
import { en as EN } from '../scripts/posters/packet-strings.json'
import { PACKET_STRINGS_SHA384 } from './poster-render.integrity.js'
import { sha384 } from './sha384.js'
// Captured when the module evaluates, as in posterSheets.js: a relative asset base resolves against the page
// that loaded the map, even if a single-page host changes its URL before the click.
const PAGE_BASE = globalThis.document?.baseURI || undefined
const ENGLISH_ONLY = Object.freeze({ en: EN })
const cache = new Map()
/** The full table ({ en, es, …, zh }), or `{ en }` when the served copy cannot be used. Never rejects. */
export function loadPacketStrings(assetsBase) {
if (!assetsBase) return Promise.resolve(ENGLISH_ONLY)
let url
try { url = new URL(`${assetsBase}/lib/packet-strings.json`, PAGE_BASE).href } catch { return Promise.resolve(ENGLISH_ONLY) }
if (!cache.has(url)) {
const p = (async () => {
const res = await fetch(url, { cache: 'force-cache' })
if (!res.ok) throw new Error(`not served (${res.status})`)
const text = await res.text()
if ((await sha384(text)) !== PACKET_STRINGS_SHA384) throw new Error('failed its integrity check')
return JSON.parse(text)
})().catch((e) => {
console.warn(`[packetStrings] ${url}: ${e?.message || e} — printing in English`)
cache.delete(url) // a later print tries again rather than inheriting one failure
return ENGLISH_ONLY
})
cache.set(url, p)
}
return cache.get(url)
}