diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 60a3df5db..081ff9824 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -27,8 +27,8 @@ // Default path is overridable with PACKET_HTML so this is not machine-specific. import { chromium } from 'playwright' -import { existsSync } from 'node:fs' -import { resolve } from 'node:path' +import { existsSync, readFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' import { pathToFileURL } from 'node:url' const target = process.argv[2] || process.env.PACKET_HTML @@ -50,11 +50,45 @@ const errs = [] page.on('pageerror', (e) => errs.push(String(e).slice(0, 120))) await page.goto(pathToFileURL(file).href, { waitUntil: 'networkidle' }) +// Resolve the rhythm words for THIS packet's language from the shared catalog. +// Per-key fallback to English, the same contract the renderer honours. +const lang = await page.evaluate(() => document.documentElement.lang || 'en') +// The catalog sits beside the generator that produced this packet, so derive it +// from the packet's own path rather than hardcoding a machine-specific location. +const catalogPath = process.env.PACKET_STRINGS + || resolve(dirname(file), '..', '..', 'packet-strings.json') +if (!existsSync(catalogPath)) { + console.error(`PACKET-RENDER: FAIL — no strings catalog at ${catalogPath}`) + console.error(' Set PACKET_STRINGS if the packet lives outside the generator tree.') + process.exit(1) +} +const catalog = JSON.parse(readFileSync(catalogPath, 'utf8')) +const enR = catalog.en.rhythms +const langR = (catalog[lang] || {}).rhythms || {} +const expected = [...new Set(Object.keys(enR).map((k) => langR[k] ?? enR[k]))] +// ★ A NON-ENGLISH PACKET MUST ACTUALLY DIFFER FROM ENGLISH. The failure this +// catches is a language block that exists but does nothing — a typo'd key, a +// block under the wrong ISO code, or a renderer that ignored the lang argument. +// Every assertion below would still pass in that case, because an all-English +// packet is a perfectly valid packet; only the comparison reveals it. +const translated = lang !== 'en' + ? Object.keys(catalog.en).filter((k) => typeof catalog.en[k] === 'string' + && (catalog[lang] || {})[k] && catalog[lang][k] !== catalog.en[k]).length + : null +await page.evaluate((words) => { window.__EXPECTED_RHYTHMS = words }, expected) + const r = await page.evaluate(() => { const body = document.body.innerText const imgs = [...document.querySelectorAll('.card img')] - const RHYTHMS = ['PRAISE', 'WAIT', 'CONFESS', 'READ THE WORD', 'ASK', 'INTERCESSION', - 'PRAY THE WORD', 'THANK', 'SING', 'MEDITATE', 'LISTEN'] + // ⚠ The expected rhythm words come from the STRINGS CATALOG FOR THIS PACKET'S + // LANGUAGE, never a hardcoded English list. A fixed list tests the opposite of + // what i18n means: it passed a packet whose translation silently did nothing and + // FAILED every correctly-translated one (measured — the Spanish packet reported + // 0/11 rhythms while rendering all twelve correctly as ALABANZA/ESPERAR/...). + // Read the packet's own and resolve per-key, exactly as the renderer + // does, so this asserts the PROPERTY (all rhythms present, in this language) + // rather than a vocabulary. + const RHYTHMS = window.__EXPECTED_RHYTHMS || [] // Whole-map property, NOT one sampled element — see header note 2. const mapFaults = (() => { @@ -106,6 +140,10 @@ if (r.rhythms !== r.rhythmsTotal) fails.push(`only ${r.rhythms}/${r.rhythmsTotal if (!r.hasMark5) fails.push('Mark 5:1-20 is NOT printed in full (Driver requirement for point 4)') if (r.metacamp !== 2) fails.push(`${r.metacamp} metacamp.org attributions, expected exactly 2`) if (r.mapFaults.length) fails.push(`map: ${r.mapFaults.join('; ')}`) +if (translated === 0) { + fails.push(`lang="${lang}" but no string differs from English — ` + + 'the language block is missing, mis-keyed, or the renderer ignored it') +} if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`) if (landscape) fails.push(`${landscape} landscape page(s) — the packet must be portrait`) if (pages !== r.sheets) fails.push(`${pages} PDF pages for ${r.sheets} sheets — one sheet must be exactly one page`) @@ -117,6 +155,8 @@ console.log(` Mark 5 in full : ${r.hasMark5}`) console.log(` metacamp.org : ${r.metacamp}`) console.log(` map : ${r.mapFaults.length ? r.mapFaults.join('; ') : 'palette resolves, not black'}`) console.log(` page size : ${boxes[0] ? boxes[0].join(' x ') : '?'} pt`) +console.log(` language : ${lang} (${r.rhythms}/${r.rhythmsTotal} rhythms in ${lang}` + + `${translated === null ? '' : `, ${translated} strings translated`})`) if (fails.length) { console.error(`\nPACKET-RENDER: FAIL — ${fails.length} problem(s)`)