diff --git a/internal/check-zone-title-tier-a.mjs b/internal/check-zone-title-tier-a.mjs index dee38b142..c6c36176d 100644 --- a/internal/check-zone-title-tier-a.mjs +++ b/internal/check-zone-title-tier-a.mjs @@ -68,8 +68,12 @@ if (probe !== 'Sudan') { process.exit(75) } -const strings = { zone_join: '{a} + {b}', zone_join_more: '{a} + {n} more' } -const titleFor = (zone, locale) => zoneTitle(zone, strings, { countryName, locale }) +// ★ THE REAL PACKET STRINGS, per language with English per key — what the print paths pass. They carry +// the multi-country joiner, so a broken one-country guard would GENERATE "A + B + n more" here and the +// negative side below would catch it; a hand-made stub is weaker than the product's own catalogue. +const PS = JSON.parse(readFileSync(join(REPO, 'library/scripts/posters/packet-strings.json'), 'utf8')) +const block = (lc) => ({ ...(PS.en || {}), ...(PS[lc] || {}) }) +const titleFor = (zone, locale) => zoneTitle(zone, block(locale), { countryName, locale }) const fails = [] const notes = [] @@ -116,6 +120,28 @@ for (const z of multi) { notes.push(` ok ${String(multi.length).padEnd(22)} cluster entries decline in all ${LOCALES.length} locales, keeping their labels byte-identical`) notes.push(` ${''.padEnd(22)} (= ${multi.length - 1} leaf zones; the 2 deaf entries collapse to 1 leaf and both decline)`) +// ── (3) THE UNIFIED DEAF ZONE: named from the strings, never by its internal label ───────────────── +// It is not in clusters.json's entries (the legend builds it from the two deaf lists), so it gets its +// own check. Two-sided: with the strings it takes its translated name; with none it declines and keeps +// its label, rather than inventing one. +{ + const { DEAF_SELECTOR } = await import(join(PLUGIN, 'data/legend-tree.js')) + const deafZone = { id: 'deaf', label: 'Deaf (affinity bloc A017)', selector: DEAF_SELECTOR } + const got = {} + for (const lc of LOCALES) { + const t = titleFor(deafZone, lc) + const want = block(lc).zone_affinity_A017 + got[lc] = t + if (!want) fails.push(`the packet strings for ${lc} carry no zone_affinity_A017, so the Deaf zone cannot be named`) + else if (t !== want) fails.push(`the Deaf zone is titled ${JSON.stringify(t)} in ${lc}; its translated name is ${JSON.stringify(want)}`) + if (t && /affinity|A017/i.test(t)) fails.push(`the Deaf zone's ${lc} title prints internal text: "${t}"`) + } + if (zoneTitle(deafZone, null, { countryName, locale: 'en' }) !== null) { + fails.push('the Deaf zone generated a title with NO strings to name it — it must decline and keep its label') + } + notes.push(` ok ${'deaf'.padEnd(22)} ${LOCALES.map((lc) => `${lc}=${got[lc]}`).join(' ')} (and declines with no strings)`) +} + // ── report ─────────────────────────────────────────────────────────────────────────────────────── for (const n of notes) console.log(n) if (fails.length) { diff --git a/library/scripts/posters/packet-strings.json b/library/scripts/posters/packet-strings.json index 38529f509..580666f24 100644 --- a/library/scripts/posters/packet-strings.json +++ b/library/scripts/posters/packet-strings.json @@ -12,6 +12,7 @@ "map_page_sub": "A prayer list of {count} unreached people groups", "map_unavailable": "The map for this prayer list is not included in this printing.", "map_page_series": "One of {k} UPG 100 prayer lists", + "zone_affinity_A017": "Deaf people groups", "list_header": "{from}–{to} of {count} · pray for them by name", "people_suffix": "people", "no_photo": "no photo available", @@ -239,6 +240,7 @@ "map_page_sub": "Una lista de oración de {count} grupos étnicos no alcanzados", "map_unavailable": "El mapa de esta lista de oración no se incluye en esta impresión.", "map_page_series": "Una de las {k} listas de oración UPG 100", + "zone_affinity_A017": "Grupos de personas sordas", "list_header": "{from}–{to} de {count} · ora por ellos por nombre", "people_suffix": "personas", "no_photo": "sin fotografía disponible", @@ -466,6 +468,7 @@ "map_page_sub": "Une liste de prière de {count} groupes ethniques non atteints", "map_unavailable": "La carte de cette liste de prière n’est pas incluse dans cette impression.", "map_page_series": "Une des {k} listes de prière UPG 100", + "zone_affinity_A017": "Groupes de personnes sourdes", "list_header": "{from}–{to} sur {count} · priez pour eux par leur nom", "people_suffix": "personnes", "no_photo": "aucune photo disponible", @@ -693,6 +696,7 @@ "map_page_sub": "Uma lista de oração de {count} grupos étnicos não alcançados", "map_unavailable": "O mapa desta lista de oração não está incluído nesta impressão.", "map_page_series": "Uma das {k} listas de oração UPG 100", + "zone_affinity_A017": "Grupos de pessoas surdas", "list_header": "{from}–{to} de {count} · ore por eles pelo nome", "people_suffix": "pessoas", "no_photo": "sem fotografia disponível", @@ -918,6 +922,7 @@ "map_page_sub": "Eine Gebetsliste von {count} unerreichten Volksgruppen", "map_unavailable": "Die Karte zu dieser Gebetsliste ist in diesem Ausdruck nicht enthalten.", "map_page_series": "Eine von {k} Gebetslisten von UPG 100", + "zone_affinity_A017": "Gruppen gehörloser Menschen", "list_header": "{from}–{to} von {count} · betet namentlich für sie", "people_suffix": "Menschen", "no_photo": "kein Foto verfügbar", @@ -1145,6 +1150,7 @@ "map_page_sub": "Una lista di preghiera di {count} gruppi etnici non raggiunti", "map_unavailable": "La mappa di questa lista di preghiera non è inclusa in questa stampa.", "map_page_series": "Una delle {k} liste di preghiera UPG 100", + "zone_affinity_A017": "Gruppi di persone sorde", "list_header": "{from}–{to} di {count} · pregate per loro per nome", "people_suffix": "persone", "no_photo": "nessuna foto disponibile", @@ -1370,6 +1376,7 @@ "map_page_sub": "O listă de rugăciune pentru {count} grupuri etnice neatinse", "map_unavailable": "Harta acestei liste de rugăciune nu este inclusă în această tipărire.", "map_page_series": "Una dintre cele {k} liste de rugăciune UPG 100", + "zone_affinity_A017": "Grupuri de persoane surde", "list_header": "{from}–{to} din {count} · rugați-vă pentru ei pe nume", "people_suffix": "persoane", "no_photo": "nicio fotografie disponibilă", @@ -1595,6 +1602,7 @@ "map_page_sub": "Молитвенный список из {count} неохваченных народов", "map_unavailable": "Карта этого молитвенного списка не включена в эту распечатку.", "map_page_series": "Один из {k} молитвенных списков UPG 100", + "zone_affinity_A017": "Группы глухих людей", "list_header": "{from}–{to} из {count} · молитесь за них поимённо", "people_suffix": "человек", "no_photo": "фотография недоступна", @@ -1822,6 +1830,7 @@ "map_page_sub": "قائمة صلاة من أجل {count} شعبًا غير مبلَّغ", "map_unavailable": "خريطة قائمة الصلاة هذه غير مضمَّنة في هذه النسخة المطبوعة.", "map_page_series": "واحدة من {k} قائمة صلاة في UPG 100", + "zone_affinity_A017": "مجموعات الصمّ", "list_header": "{from}–{to} من {count} · صلّوا لأجلهم بالاسم", "people_suffix": "نسمة", "no_photo": "لا توجد صورة متاحة", @@ -2049,6 +2058,7 @@ "map_page_sub": "{count} अनछुए जनसमूहों की प्रार्थना सूची", "map_unavailable": "इस प्रार्थना सूची का नक्शा इस प्रिंट में शामिल नहीं है।", "map_page_series": "UPG 100 की {k} प्रार्थना सूचियों में से एक", + "zone_affinity_A017": "बधिर लोगों के समूह", "list_header": "{count} में से {from}–{to} · उनके नाम लेकर प्रार्थना करें", "people_suffix": "लोग", "no_photo": "कोई चित्र उपलब्ध नहीं", @@ -2274,6 +2284,7 @@ "map_page_sub": "为 {count} 个未得之民的祷告名单", "map_unavailable": "此祷告名单的地图未包含在本次打印中。", "map_page_series": "UPG 100 的 {k} 份祷告名单之一", + "zone_affinity_A017": "聋人群体", "list_header": "{count} 之中的 {from}–{to} · 按名为他们祷告", "people_suffix": "人", "no_photo": "没有照片", diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index 5bda0a1d5..8f918a93b 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -557,7 +557,7 @@ for (const z of zones) { // The title in the packet's language where it can be generated (one-country zones), else the // hand-written label — the same rule the browser print applies. - const title = ZONE_TITLE ? ZONE_TITLE(z, null, { alpha2: (c) => ALPHA2[c], locale: LANG }) : null; + const title = ZONE_TITLE ? ZONE_TITLE(z, { ...STRINGS.en, ...(STRINGS[LANG] || {}) }, { alpha2: (c) => ALPHA2[c], locale: LANG }) : null; const { html, pageCount, withPhoto } = await buildPacket(title ? { ...z, label: title } : z, groups, mapSvg); // ⚠ THE LIST IS PART OF THE FILENAME, and leaving it out was a real hazard I created while arguing // against exactly this. PACKET_LIST fails closed because "a packet that showed the wrong list is diff --git a/plugin-designs/my-upg-100-list/data/zone-title.js b/plugin-designs/my-upg-100-list/data/zone-title.js index d5d894f2e..93e344c0e 100644 --- a/plugin-designs/my-upg-100-list/data/zone-title.js +++ b/plugin-designs/my-upg-100-list/data/zone-title.js @@ -134,6 +134,16 @@ export function countryLabel(iso3, opts = {}) { * @returns {string|null} */ export function zoneTitle(zone, strings, opts = {}) { + // ★ AN AFFINITY-BLOC ZONE HAS NO COUNTRY TO NAME IT, BUT IT HAS A NAME OF ITS OWN. The Deaf row + // (affinity bloc A017) spans every region, so the country path below declines it — and the packet + // then printed the legend's internal label, "Deaf (affinity bloc A017)", as its title. Its name is + // translated once per language in the packet strings, keyed by bloc: zone_affinity_. Any other + // affinity bloc a list adds is named the same way, with one string and no code. + const sel = zone?.selector + if (sel && sel.type === 'affinity_bloc' && sel.rop1) { + const name = strings && strings[`zone_affinity_${sel.rop1}`] + return typeof name === 'string' && name.trim() ? name : null + } const codes = zoneCountryCodes(zone?.selector) if (!codes.length) return null // tier C — declined, caller keeps its label diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue index c8d09ec79..d3217bf81 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -1486,11 +1486,15 @@ async function printPrayerPacket(zoneId, variant = 'full') { // the zone unchanged. Loaded lazily, like the packet renderer, so the map pays nothing until a print. async function localizedZone(zone) { try { - const [{ zoneTitle }, { countryName }] = await Promise.all([ + const [{ zoneTitle }, { countryName }, strings] = await Promise.all([ import('./data/zone-title.js'), import('@map/utils/countryName.js'), + import('@map/scripts/posters/packet-strings.json').then((m) => m.default || m), ]); - const title = zoneTitle(zone, null, { countryName, locale: packetLang() }); + // the packet's own strings for this language, English per key where a block lacks one — the same + // fallback the renderer applies (an affinity-bloc zone such as Deaf is named from them) + const block = { ...(strings.en || {}), ...(strings[packetLang()] || {}) }; + const title = zoneTitle(zone, block, { countryName, locale: packetLang() }); return title ? { ...zone, label: title } : zone; } catch { return zone; } // a title that cannot be generated keeps the hand-written one }