Every country prints its prayer list, card deck and packet, from a records provider any map can use

The country wall passed no records, so each of its 175 cards offered only the poster and said the
prayer list was "coming soon", while the people groups were one request away. library/lists/
peopleGroupRecords.js is the records half of the print standard: one people-groups fetch per language
(normalised the way the prints read rows, list membership applied), then a SELECTOR picks the set — a
country by ISO3, explicit slugs, any field, or a host's own selector language. A selector nobody
understands selects nothing rather than every group on earth, and a failed fetch is not cached.

The country wall uses it by ISO3, so every country now prints the list, the deck (all five stocks) and
the full or lightweight packet. The packet's page 1 is, for now, the country's stored poster when it is
really there; the in-browser map generator will replace that.

check:people-group-records asserts the selection two-sided with an injected fetch; registered in
gates:pre.
This commit is contained in:
Doxa Maps 2026-09-29 04:13:53 +00:00
parent 58e113549c
commit f8744925e4
4 changed files with 181 additions and 3 deletions

View file

@ -0,0 +1,43 @@
#!/usr/bin/env bun
/**
* check-people-group-records — the records half of the print standard (library/lists/peopleGroupRecords.js)
* selects EXACTLY the set it is asked for, two-sided: a country selector returns that country's groups and
* not another's, an unknown selector returns nothing (never everything), one fetch serves a language, and
* a failed fetch is not cached. Headless, with an injected fetch. Runs under bun (lists.json is imported
* without an import attribute, as it is inside the bundles).
*/
const { createPeopleGroupRecords, selectRecords } = await import(new URL('../library/lists/peopleGroupRecords.js', import.meta.url).href)
const rows = [
{ slug: 'a', name: 'Zeta', country_code: { value: 'TCD', label: 'Chad' }, wagf_region: { value: 'africa', label: 'Africa' } },
{ slug: 'b', name: 'Alpha', country_code: { value: 'TCD', label: 'Chad' }, wagf_region: { value: 'africa', label: 'Africa' } },
{ slug: 'c', name: 'Mid', country_code: { value: 'IND', label: 'India' }, wagf_region: { value: 'asia', label: 'Asia' } },
]
let calls = 0, fail = false
const fetchImpl = async (url) => { calls++; if (fail) return { ok: false, status: 503 }; return { ok: true, json: async () => ({ posts: rows }) } }
const R = createPeopleGroupRecords({ apiBase: () => 'http://api', lang: () => 'es', fetchImpl })
const chad = await R({ type: 'country', values: ['tcd'] })
const bySlug = await R({ type: 'pg_slugs', values: ['c'] })
const byField = await R({ type: 'field', field: 'wagf_region', values: ['asia'] })
const unknown = await R({ type: 'rop3', values: ['x'] })
const r = {
country: chad.map((x) => x.slug).join(',') === 'b,a', // Chad only, sorted by name
countryExcludesIndia: !chad.some((x) => x.slug === 'c'),
normalized: chad[0].country_code === 'TCD' && chad[0].country_code_label === 'Chad',
slugs: bySlug.length === 1 && bySlug[0].slug === 'c',
field: byField.length === 1 && byField[0].slug === 'c',
unknownSelectsNothing: unknown.length === 0,
oneFetchPerLanguage: calls === 1,
evaluateHook: selectRecords(rows.map((x) => ({ ...x, country_code: 'X' })), { type: 'custom' }, () => new Set(['a'])).length === 1,
}
fail = true
const R2 = createPeopleGroupRecords({ apiBase: () => 'http://api', fetchImpl })
let threw = false; try { await R2({ type: 'country', values: ['TCD'] }) } catch { threw = true }
fail = false
const retried = (await R2({ type: 'country', values: ['TCD'] })).length === 2
r.failureNotCached = threw && retried
const R3 = createPeopleGroupRecords({ apiBase: () => '', fetchImpl })
let noApi = false; try { await R3({ type: 'country', values: ['TCD'] }) } catch { noApi = true }
r.noApiRejects = noApi
console.log(r)
const pass = Object.values(r).every(Boolean)
console.log(pass ? 'PG-RECORDS: PASS' : 'PG-RECORDS: FAIL'); process.exit(pass ? 0 : 1)

View file

@ -0,0 +1,109 @@
/**
* peopleGroupRecords.js — the RECORDS half of the print standard: the people groups of ANY set, in the
* shape usePosterPrints' `records(item)` wants, so any map can offer the prayer list, the card deck and
* the prayer packet for whatever its reader picked — a country, a region, a list, a selection.
*
* ★ WHY IT IS IN THE LIBRARY. Every wall that prints rows used to carry its own fetch: the UPG-100 wall
* had one, and the country walls had none, which is why a country card said "prayer list coming soon"
* while the data to print it was one request away. The fetch, the normalisation and the membership
* rules are the same for every set; only the SET differs, so the set is a parameter (a selector).
*
* A SELECTOR names the set:
* { type: 'country', values: ['TCD', …] } by ISO3 country code
* { type: 'pg_slugs', values: ['slug', …] } explicit people groups
* { type: 'field', field: 'wagf_region', values: […] } any field's value (after normalisation)
* Anything else is handed to the host's `evaluate(selector, records)` (the UPG-100 selector language),
* and a selector nobody understands selects NOTHING rather than everything: a packet of every people
* group on earth printed under one country's name would be worse than no packet.
*
* ONE FETCH PER LANGUAGE, shared by every item: the list endpoint returns every group, so a wall of 175
* countries costs one request, not 175. A failed fetch is not cached, so the next click retries.
*/
import { applyPeopleGroupList } from './peopleGroupList.js'
// What the prints read: the card's face and language, the profile's description (imb_gsec feeds its
// reachedness sentence), the sets page's joins (slug), and the country for the page-1 map and captions.
export const PRINT_FIELDS = 'slug,name,country_code,wagf_region,wagf_block,rop1,population,religion,'
+ 'image_url,primary_language,imb_gsec'
// Fields the API sends as { value, label } dicts. Normalised to the value, with the label kept beside
// it as <field>_label — the shape card-render and describeGroup read. Same rule as the UPG-100
// profile's normalize.js, so a row prints the same whichever surface fetched it.
const NESTED = ['country_code', 'wagf_region', 'wagf_block', 'primary_language', 'religion', 'imb_gsec']
export function normalizeRecord(raw) {
if (raw == null || typeof raw !== 'object') return raw
const out = { ...raw }
for (const f of NESTED) {
if (!(f in out)) continue
const v = out[f]
if (v && typeof v === 'object') {
out[f] = v.value ?? null
if (v.label != null) out[`${f}_label`] = v.label
}
}
return out
}
export function extractRecords(payload) {
if (Array.isArray(payload)) return payload
if (payload && typeof payload === 'object') {
return payload.posts || payload.data || payload.records || payload.results || payload.items || []
}
return []
}
/** The records a selector names, sorted by name. `evaluate` handles selector types this module does not. */
export function selectRecords(records, selector, evaluate = null) {
const rows = Array.isArray(records) ? records : []
const s = selector || {}
const vals = new Set((s.values || []).map(String))
let picked
if (s.type === 'country') {
const up = new Set([...vals].map((v) => v.toUpperCase()))
picked = rows.filter((r) => r && up.has(String(r.country_code || '').toUpperCase()))
} else if (s.type === 'pg_slugs') {
picked = rows.filter((r) => r && vals.has(String(r.slug)))
} else if (s.type === 'field' && s.field) {
picked = rows.filter((r) => r && vals.has(String(r[s.field])))
} else if (typeof evaluate === 'function') {
const slugs = evaluate(selector, rows)
picked = rows.filter((r) => r && slugs && slugs.has(r.slug))
} else {
picked = []
}
return picked.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
}
/**
* @param {object} o
* apiBase () => string the people-groups API origin ('' = none: every call rejects, so the
* print shows its error state rather than an empty packet)
* lang () => string the reader's language; names and labels come back in it
* profileConfig ref|object the host's config, for the universal `list` membership rule
* evaluate (selector, records) => Set<slug> for selector types beyond the built-in three
* fetchImpl fetch injectable, so the module is testable without a network
* @returns {(selector) => Promise<rows[]>}
*/
export function createPeopleGroupRecords({ apiBase, lang = () => 'en', profileConfig = null, evaluate = null, fetchImpl = null } = {}) {
const cache = new Map()
const doFetch = fetchImpl || ((...a) => fetch(...a))
function all() {
const lc = (typeof lang === 'function' ? lang() : lang) || 'en'
if (!cache.has(lc)) {
const p = (async () => {
const base = typeof apiBase === 'function' ? apiBase() : apiBase
if (!base) throw new Error('no people-groups API configured')
const resp = await doFetch(`${base}/api/people-groups/list?fields=${PRINT_FIELDS}&lang=${encodeURIComponent(lc)}`, { cache: 'no-store' })
if (!resp.ok) throw new Error(`people-groups fetch failed (${resp.status})`)
const rows = extractRecords(await resp.json())
const member = profileConfig ? await applyPeopleGroupList(rows, profileConfig) : rows
return member.map(normalizeRecord)
})()
p.catch(() => cache.delete(lc))
cache.set(lc, p)
}
return cache.get(lc)
}
return async (selector) => selectRecords(await all(), selector, evaluate)
}

View file

@ -321,7 +321,7 @@
"fix:sri": "node internal/fix-sri.mjs", "fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs", "gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard", "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs", "library-index": "node internal/generate-library-index.mjs",
@ -366,7 +366,8 @@
"verify:interop": "node internal/acceptance/interop-harness.mjs", "verify:interop": "node internal/acceptance/interop-harness.mjs",
"verify:interop:self-test": "node internal/acceptance/interop-harness.mjs --self-test", "verify:interop:self-test": "node internal/acceptance/interop-harness.mjs --self-test",
"verify:interop:self-test:camera-freeze": "node internal/acceptance/interop-harness.mjs --self-test --self-test-mode=camera-freeze", "verify:interop:self-test:camera-freeze": "node internal/acceptance/interop-harness.mjs --self-test --self-test-mode=camera-freeze",
"verify:map-paint": "node internal/verify-map-paint.mjs" "verify:map-paint": "node internal/verify-map-paint.mjs",
"check:people-group-records": "bun internal/check-people-group-records.mjs"
}, },
"//scripts": { "//scripts": {
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.", "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
@ -592,7 +593,8 @@
"verify:interop:self-test": "TWO regression modes, each required to fire ITS OWN combination-named failure: grid-prop-drop (default — freezes the grid child's filter props/attrs on the live dashboard-plus-grid page, reproducing the exact historical bug \"selecting anything in the dashboard above did not filter the grid below\") and `verify:interop:self-test:camera-freeze` (pins fitBounds/flyTo to no-ops on window.__rdashMap, reproducing \"the grid->map link stops moving the camera\"). A harness that has only ever reported PASS is unproven.", "verify:interop:self-test": "TWO regression modes, each required to fire ITS OWN combination-named failure: grid-prop-drop (default — freezes the grid child's filter props/attrs on the live dashboard-plus-grid page, reproducing the exact historical bug \"selecting anything in the dashboard above did not filter the grid below\") and `verify:interop:self-test:camera-freeze` (pins fitBounds/flyTo to no-ops on window.__rdashMap, reproducing \"the grid->map link stops moving the camera\"). A harness that has only ever reported PASS is unproven.",
"verify:interop:self-test:camera-freeze": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)", "verify:interop:self-test:camera-freeze": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
"check:gospel-set-fit": "the set charts FIT the canvas they are given. renderWorldCopies:false puts a hard floor under the zoom at log2(canvasWidth/512) — one world must fill the canvas WIDTH — and on a wide, short figure that floor is too close for the drawing's own height, so the outermost groups fall outside the frame (measured at 1440: 54 outside in ar, 97 in en, which is what proved it was the width and not RTL). Pure arithmetic against the components' own projection, so no browser and no drift. Negative control: the old 0.17 extentFraction still fails 796x249 at 271px of 169, so a pass cannot be vacuous. Also asserts both charts share one footprint, or the same population reads as a different size when a reader switches tabs.", "check:gospel-set-fit": "the set charts FIT the canvas they are given. renderWorldCopies:false puts a hard floor under the zoom at log2(canvasWidth/512) — one world must fill the canvas WIDTH — and on a wide, short figure that floor is too close for the drawing's own height, so the outermost groups fall outside the frame (measured at 1440: 54 outside in ar, 97 in en, which is what proved it was the width and not RTL). Pure arithmetic against the components' own projection, so no browser and no drift. Negative control: the old 0.17 extentFraction still fails 796x249 at 271px of 169, so a pass cannot be vacuous. Also asserts both charts share one footprint, or the same population reads as a different size when a reader switches tabs.",
"check:sentence-slots": "a translated SENTENCE that carries values stays ONE key, through library/composables/useSentence.js. Four properties that all fail silently: a translator can REORDER the clauses (the slots follow the message, not the call); a missing slot leaves its placeholder VISIBLE rather than rendering null or an empty gap, while 0 and '' are real values; slotted markup (a bold count, a dir=ltr island) goes in as a VALUE and is never translated around; and English renders as it did before the migration. Exists because the pattern it replaces built sentences by concatenating translated fragments around untranslated text — 'languages have', 'people.' — which in Arabic gives Arabic words in English clause order: grammatically broken, and worse than leaving it English because it looks translated. Sentinels are private-use (U+E000+n) so nothing in the i18n pipeline may rewrite them, and any sentinel already in a message is stripped before ours are placed — without that a stray swallowed a slot and the arm still passed, which is how this gate found its own weakness." "check:sentence-slots": "a translated SENTENCE that carries values stays ONE key, through library/composables/useSentence.js. Four properties that all fail silently: a translator can REORDER the clauses (the slots follow the message, not the call); a missing slot leaves its placeholder VISIBLE rather than rendering null or an empty gap, while 0 and '' are real values; slotted markup (a bold count, a dir=ltr island) goes in as a VALUE and is never translated around; and English renders as it did before the migration. Exists because the pattern it replaces built sentences by concatenating translated fragments around untranslated text — 'languages have', 'people.' — which in Arabic gives Arabic words in English clause order: grammatically broken, and worse than leaving it English because it looks translated. Sentinels are private-use (U+E000+n) so nothing in the i18n pipeline may rewrite them, and any sentinel already in a message is stripped before ours are placed — without that a stray swallowed a slot and the arm still passed, which is how this gate found its own weakness.",
"check:people-group-records": "the records half of the print standard selects EXACTLY the set it is asked for, two-sided: a country selector returns that country and not another, an unknown selector returns nothing rather than every group on earth, one fetch per language serves every item, and a failed fetch is not cached so the next click retries."
}, },
"devDependencies": { "devDependencies": {
"@capacitor/android": "^7.6.9", "@capacitor/android": "^7.6.9",

View file

@ -38,6 +38,9 @@
import { computed, inject, ref } from 'vue' import { computed, inject, ref } from 'vue'
import PosterWall from '@map/components/poster/PosterWall.vue' import PosterWall from '@map/components/poster/PosterWall.vue'
import { useOptionalT } from '@map/composables/useOptionalT.js' import { useOptionalT } from '@map/composables/useOptionalT.js'
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
// MOVED to the shared library 2026-09-24: the regions-dashboard poster dropdown is a second // MOVED to the shared library 2026-09-24: the regions-dashboard poster dropdown is a second
// consumer of this same dataset, and RULE 2 puts a dataset with a named second consumer in // consumer of this same dataset, and RULE 2 puts a dataset with a named second consumer in
// library/. Generated by library/scripts/posters/emit-countries.mjs, which now writes there. // library/. Generated by library/scripts/posters/emit-countries.mjs, which now writes there.
@ -52,6 +55,25 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
const items = computed(() => data.countries || []) const items = computed(() => data.countries || [])
// ★ EVERY COUNTRY PRINTS ITS PRAYER RESOURCES — the list, the card deck and the packet — through the
// library's print standard. The wall used to pass no records, so each card said "coming soon" while the
// people groups were one request away. Records come from the shared provider (one fetch per language
// for all 175 countries), selected by the country's ISO3.
const recordsFor = createPeopleGroupRecords({
apiBase: () => profileConfig.value?.apiBaseUrl || getApiBaseUrl(),
lang: () => getRuntimeLocale() || detectLocale(),
profileConfig,
})
const countryRecords = (c) => recordsFor({ type: 'country', values: [c.iso3] })
// The packet's page 1 is the country's own poster (posters/<id>.webp, 174 of them), absolute because
// the print frame is its own document, and only when it is really there: a missing image falls through
// to the packet's "map not included" line rather than printing a broken picture.
async function countryMap(c) {
const url = new URL(`${assetsBase.value}/posters/${c.id}.webp`, document.baseURI).href
const ok = await fetch(url, { method: 'HEAD', cache: 'force-cache' }).then((r) => r.ok).catch(() => false)
return ok ? url : null
}
// The region is already ON each record (emit-countries.mjs resolves it from // The region is already ON each record (emit-countries.mjs resolves it from
// wagf_region_label), so the taxonomy is not duplicated here the way the 100-list's // wagf_region_label), so the taxonomy is not duplicated here the way the 100-list's
// REGION_OF map has to be — the 100-list's zones carry no region field, countries do. // REGION_OF map has to be — the 100-list's zones carry no region field, countries do.
@ -99,6 +121,8 @@ const soonMessage = computed(() => t('messages.countryWallSoon'))
:intro="intro" :intro="intro"
unit="UPG" unit="UPG"
item-count-key="regions.countryCount" item-count-key="regions.countryCount"
:records="countryRecords"
:map-image="countryMap"
:collapsible="true" :collapsible="true"
:collapsed-by="collapsedBy" :collapsed-by="collapsedBy"
:unsupported="unsupported" :unsupported="unsupported"