The legend's region sections, its Deaf row and its one-country zones, the wall's card names, the print progress and error messages, the map's status badge and the Population column all rendered English in every locale. None of them was visible to check:hardcoded-english: they were single words, .js modules, Error() arguments, library components or number formatting. - Legend: region rows through one REGION_KEY table shared with the wall (poster.region_*, new region_deaf); zones through zoneTitle (tier A by country, the Deaf row by its bloc name); node ids stay English-derived. - Wall: card labels through zoneTitle, so the Show-on-map name, alt text, tooltip, HUD title and printed title agree with the legend and packet. - Errors a reader sees are keyedError(key, English, params); errorText(e, t) shows them translated, and an unkeyed error still shows its own message. Developer detail for a failed data load goes to the console; the badge says upgMap.mapDataFailed. - Population: Intl compact notation in the app's locale, same precision. - Country Posters: the unsupported card's reason is messages.countryPosterTooMany, not the data file's English note. - Strings only the UPG-100 map shows move to a new upgMap area, so other bundles do not carry them (country-posters 1,490,907 bytes). - check:hardcoded-english judges keyedError's English as a fallback (quiet when the key resolves everywhere, a finding when it does not); check:label-key-coverage follows REGION_KEY to legend-tree.js and covers the legend's table and its regionFor producer. Both self-tests two-sided.
154 lines
8.5 KiB
Vue
154 lines
8.5 KiB
Vue
<script setup>
|
|
/**
|
|
* country-poster-wall.vue — the COUNTRY poster wall (pane 2 of country-posters).
|
|
*
|
|
* @element country-posters-map
|
|
* @description One printable card per country, grouped into COLLAPSIBLE regional sections so the
|
|
* wall stays navigable at real data volume (Asia alone is 28 countries / 1,309 groups). Rendering
|
|
* lives in the shared PosterWall component; this profile supplies only its data and taxonomy.
|
|
*
|
|
* See the note in countries-map.vue: the generator reads the `@description` TAG, not the prose
|
|
* below it, and an absent tag makes the cap-5 SPA fall back to labelling the tab "experimental
|
|
* bundle".
|
|
*
|
|
* One card per country, grouped into COLLAPSIBLE regional sections. All of the
|
|
* rendering lives in the shared @map/components/poster/PosterWall.vue — this
|
|
* profile supplies only its data and its taxonomy.
|
|
*
|
|
* > design note 2026-08-01: "the poster wall should be semantically organized by the
|
|
* > regions… there's going to be way too many countries, so we'll need to be able
|
|
* > to collapse the regions and see how many countries there are in each of the
|
|
* > regions, because people don't want to just look at, like, scroll through
|
|
* > hundreds of countries."
|
|
*
|
|
* WHY COLLAPSE IS NOT A NICETY — measured from the live data:
|
|
* Asia 28 countries / 1309 groups
|
|
* Africa 41 / 314 Middle East 21 / 223
|
|
* LatAm 9 / 151 Europe 27 / 55
|
|
* Oceania 17 / 17 N.America 15 / 16 Other 18 / 21
|
|
* 92 of 175 countries have exactly ONE people group and 15 have two. Without
|
|
* collapsing, the wall is a scroll of mostly single-entry cards.
|
|
*
|
|
* SORTING IS THE ANSWER TO SPARSE SHEETS, not a template variant.
|
|
* > Brief: "if it has a one-people-group on it, we still want a one-people-group
|
|
* > map generated… those get sorted to the bottom of the list."
|
|
* The shared wall always sorts sections AND cards by descending count, so the
|
|
* dense countries lead and the long tail sinks. There is no floor.
|
|
*/
|
|
import { computed, inject, ref } from 'vue'
|
|
import PosterWall from '@map/components/poster/PosterWall.vue'
|
|
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
|
|
// 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.
|
|
import data from '@map/data/country-posters.json'
|
|
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
|
|
|
const profileConfig = inject('profileConfig', ref(null))
|
|
|
|
const instanceId = computed(() => profileConfig.value?.instanceId || 'country-posters-wall')
|
|
const isDark = computed(() => profileConfig.value?.isDark === true)
|
|
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
|
|
|
|
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] })
|
|
// ★ GENERATED AT THE CLICK: the country's sheet is drawn in the browser from its people groups and the
|
|
// world boundaries (packet/posterSheets.js — the same generator the Node script runs), for the poster
|
|
// print (letter, vector) and the packet's page 1. No stored sheet exists for a country; this is the path.
|
|
// The series facts are the ones generate.mjs states in country mode: map N of M over the countries on
|
|
// the same sheet size, and their total groups.
|
|
async function countrySheet(c, format) {
|
|
const [{ generateCountrySheet }, { ISO3_NAME }, records] = await Promise.all([
|
|
import('@map/packet/posterSheets.js'), import('@map/data/iso3-name.js'), countryRecords(c),
|
|
])
|
|
const tier = items.value.filter((x) => x.sheet === c.sheet)
|
|
const series = { totalGroups: tier.reduce((a, x) => a + (x.count || 0), 0), totalLists: tier.length, index: tier.indexOf(c) + 1 }
|
|
return generateCountrySheet({ country: c, records, assetsBase: assetsBase.value, ISO3_NAME, format, series })
|
|
}
|
|
// The name table the generated sheet was labelled from, so a packet in another language relabels page 1's
|
|
// country names (the same seam the UPG-100 wall passes).
|
|
const iso3Names = () => import('@map/data/iso3-name.js').then((m) => m.ISO3_NAME)
|
|
// A STORED fallback behind the generator: the country's preview (previews/<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}/previews/${c.id}.webp`, document.baseURI).href // where the tree serves them
|
|
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
|
|
// 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.
|
|
// The data carries the WAGF region as its ENGLISH label; the section heading is that region in the
|
|
// page's language, from the dashboard's own WAGF table (dashboard.wagfRegion.<label as a slug>), so the
|
|
// wall and the Regions dashboard cannot name one region two ways. A label with no key stays as drawn.
|
|
const t = useOptionalT('')
|
|
const regionSlug = (label) => String(label).toLowerCase().replace(/\s+/g, '_')
|
|
const groupBy = (c) => {
|
|
if (!c.region || c.region === 'Other') return t('dashboard.other')
|
|
const key = `dashboard.wagfRegion.${regionSlug(c.region)}`
|
|
const v = t(key)
|
|
return v && v !== key ? v : c.region
|
|
}
|
|
|
|
// Low-density regions open COLLAPSED. Threshold is on the region's total, not its
|
|
// country count: Europe is 27 countries but only 55 groups, so it is long and thin —
|
|
// exactly what the design does not want to scroll through first.
|
|
const collapsedBy = (sec) => sec.total < 60
|
|
|
|
// The not-yet reason travels WITH the record, so the exact number the wall prints
|
|
// can never drift from the data. Brief: "it should say the exact number in our data."
|
|
// The reason is shown on the card and in the print HUD, so it speaks the reader's language: the data's
|
|
// unsupportedReason is an English note (and a flag), not display text. The card already states the
|
|
// count above it, so the sentence needs no number and no plural agreement.
|
|
const unsupported = (c) => (c.unsupportedReason ? t('messages.countryPosterTooMany') : null)
|
|
|
|
// ★ THROUGH THE CATALOGUE, like the UPG-100 wall: the heading, the tag, the intro and the counts read
|
|
// in the page's language. The intro keeps its one emphasis as three plain keys (A, the emphasis, B)
|
|
// so no translation carries markup; the emphasis is the same phrase the UPG-100 wall uses.
|
|
const escHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
const heading = computed(() => t('messages.countryWallHeading'))
|
|
const wallTag = computed(() => t('messages.upg100WallTag'))
|
|
const intro = computed(() => `${escHtml(t('messages.countryWallIntroA'))}<strong>${escHtml(t('messages.upg100WallIntroEmph'))}</strong>${escHtml(t('messages.countryWallIntroB'))}`)
|
|
const soonMessage = computed(() => t('messages.countryWallSoon'))
|
|
</script>
|
|
|
|
<template>
|
|
<PosterWall
|
|
:items="items"
|
|
:group-by="groupBy"
|
|
:instance-id="instanceId"
|
|
:is-dark="isDark"
|
|
:assets-base="assetsBase"
|
|
event-name="doxa-country-posters:show-on-map"
|
|
:heading="heading"
|
|
:tag="wallTag"
|
|
:intro="intro"
|
|
unit="UPG"
|
|
item-count-key="regions.countryCount"
|
|
:records="countryRecords"
|
|
:card-stocks="true"
|
|
:sheet="countrySheet"
|
|
:iso3-names="iso3Names"
|
|
:map-image="countryMap"
|
|
:collapsible="true"
|
|
:collapsed-by="collapsedBy"
|
|
:unsupported="unsupported"
|
|
:prayer-list-soon-message="soonMessage"
|
|
download-prefix="doxa-country"
|
|
/>
|
|
</template>
|