diff --git a/internal/check-packet-variants.mjs b/internal/check-packet-variants.mjs index d7dcf0d41..b42f64184 100644 --- a/internal/check-packet-variants.mjs +++ b/internal/check-packet-variants.mjs @@ -102,6 +102,32 @@ ok(count(full, /class="sheet/g) - count(light, /class="sheet/g) === 3, ok(/10/.test(caps.join(' ')), 'the bounded tally does not state its unknown count — an un-inked category reads as zero in print') } +// ── THE RING LEGEND: THE AGREED VOCABULARY, AND ONLY THE RINGS THAT HOLD SOMEONE ──────────────── +// ★ The names are COMPOSED from band x culture rather than a flat 1..10 table, because "same +// people" (provisional on the Driver's row 89) appears in rings 1, 4 AND 7 — three cells that a +// flat table would have to keep in step. This asserts the composition produces the vocabulary +// gospel and exp settled, so a printed sheet cannot disagree with the screen legend. +{ + const only = (rings) => { + const html = R.buildPacket(zone, groups, new Map(), '', funnel, christian, 'full', + { bounded: [{ id: 'x', reached: true }], centered: rings.map((r, i) => ({ id: `r${i}`, ring: r })) }) + return (html.match(/
  • [^<]*<\/b>([^<]*)<\/li>/g) || []).map((s) => s.replace(/<[^>]+>/g, '').trim()) + } + const all = only([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ok(all.length === 10, `all ten rings populated should list ten names, got ${all.length}`) + const want = ['Same dialect, same people', 'Same dialect, near culture', 'Same dialect, different culture', + 'Same language, same people', 'Same language, near culture', 'Same language, different culture', + 'Same family, same people', 'Same family, near culture', 'Same family, different culture', 'Nearby only'] + want.forEach((w, i) => ok((all[i] || '').endsWith(w), + `ring ${i + 1} should read ${JSON.stringify(w)}, got ${JSON.stringify(all[i])}`)) + // ⚠ ONLY THE POPULATED RINGS. Ten rows of vocabulary beside a figure is a glossary, not a legend, + // and a named ring with no dot invites the reader to hunt for something that is not there. + const sparse = only([1, 1, 3, 7, 7, 7]) + ok(sparse.length === 3, `only the populated rings should be listed, got ${sparse.length}: ${JSON.stringify(sparse)}`) + ok(/^2 /.test(sparse[0]) && /^1 /.test(sparse[1]) && /^3 /.test(sparse[2]), + `the legend should carry each ring's own count, got ${JSON.stringify(sparse)}`) +} + // An unknown variant must print the FULL packet. A typo that printed a SHORTER packet would look // like a successful print. ok(build('nonsense-typo') === full, 'an unknown variant did not fall back to the full packet') diff --git a/library/packet/packet-render.js b/library/packet/packet-render.js index abbe6a887..a6c850be2 100644 --- a/library/packet/packet-render.js +++ b/library/packet/packet-render.js @@ -207,6 +207,11 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de .set-unk{fill:#fff;stroke:#b9b2a0;stroke-width:1.5} .set-centre{fill:#2f6d4f} .set-cap{margin:10px 0 0;font:400 15px Georgia,serif;color:#413d35} + /* The ring legend: only the rings this zone actually fills, in ring order. Left-aligned inside + the centred column because a ragged-right list of names reads far better than centred prose. */ + .set-rings{margin:10px 0 0;padding:0;list-style:none;text-align:start; + font:400 13px Georgia,serif;color:#6f6a5c;line-height:1.5} + .set-rings b{color:#1b1a17;font-weight:700} /* ── THE THREE PRAYER COLUMNS, now a HORIZONTAL strip at the end of each row ────────── They were a vertical stack in the old 683x160 card. In a ${CW}x${CH.toFixed(0)} ROW there is no @@ -820,6 +825,46 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de // it. If the figure does not fit, SHRINK THE WHOLE FIGURE — never compress the gap. Measured on // the real module with 40 groups: inside max 85.3 of 92 allowed, outside min 119.8 of 112 // required. The scale below is applied to the viewBox, so it cannot touch the gap. + // ★ THE AGREED RING VOCABULARY, COMPOSED — not a flat 1..10 table. Settled between gospel and exp + // and derived from exp's ringFor(), so the numbering cannot drift from the code that produces it; + // the screen legend renders the same words, so a printed sheet cannot disagree with the screen. + // 1 same dialect, same people 6 same language, different culture + // 2 same dialect, near culture 7 same family, same people + // 3 same dialect, different culture 8 same family, near culture + // 4 same language, same people 9 same family, different culture + // 5 same language, near culture 10 nearby only + // ⚠ COMPOSED FROM band x culture BECAUSE ONE CELL IS PROVISIONAL. "same people" vs the brief's + // "same culture" in column 1 is open on the Driver's row 89, and it appears in rings 1, 4 AND 7 — + // so a flat ten-string table would need THREE edits kept in step, which is exactly how two of them + // end up saying different things. Composed, it is ONE string (ring_culture_same) and the ring + // numbering, the columns and the colours are untouched either way. + const BANDS = ['dialect', 'dialect', 'dialect', 'language', 'language', 'language', 'family', 'family', 'family']; + const CULTURES = ['same', 'near', 'different', 'same', 'near', 'different', 'same', 'near', 'different']; + function ringName(n) { + if (n === RING_MAX) return t('ring_band_geography'); // 10 stands alone + const i = n - 1; + if (i < 0 || i >= BANDS.length) return ''; + return fill(t('ring_join'), { band: t(`ring_band_${BANDS[i]}`), culture: t(`ring_culture_${CULTURES[i]}`) }); + } + + // ONLY THE RINGS THAT HOLD SOMEONE. Ten rows of vocabulary beside a figure is a glossary, not a + // legend, and nine of the ten are routinely empty for one zone. Printing a named ring with no + // group on it also invites the reader to look for a dot that is not there. `ringTally` gives the + // per-ring counts, so the legend states what this zone actually contains, in ring order. + function ringLegend(tally) { + const counts = tally && tally.counts; + if (!counts || typeof counts.get !== 'function') return ''; + const rows = []; + for (let n = 1; n <= RING_MAX; n++) { + const c = Number(counts.get(n) || 0); + if (!c) continue; + const name = ringName(n); + if (!name) continue; + rows.push(`
  • ${num(c)} ${esc(name)}
  • `); + } + return rows.length ? `` : ''; + } + function setsPage(zone, sets) { // '' WITHOUT DATA, exactly like funnelPage and christianPage — a page that cannot state true // figures must not appear at all. This is what keeps FULL valid before a host supplies sets. @@ -871,6 +916,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de

    ${esc(fill(t('sets_centered_tally'), { placed: num(T.placed), centre: num(T.centre), unplaced: num(T.unplaced) }))}

    + ${ringLegend(T)} `); } diff --git a/library/scripts/posters/packet-strings.json b/library/scripts/posters/packet-strings.json index cfaa4b56e..9415ddb76 100644 --- a/library/scripts/posters/packet-strings.json +++ b/library/scripts/posters/packet-strings.json @@ -218,7 +218,15 @@ "sets_bounded_title": "Reached, and not", "sets_centered_title": "Distance from a neighbour", "sets_bounded_tally": "{inside} inside · {outside} outside · {unknown} unknown", - "sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced" + "sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced", + "ring_band_dialect": "Same dialect", + "ring_band_language": "Same language", + "ring_band_family": "Same family", + "ring_band_geography": "Nearby only", + "ring_culture_same": "same people", + "ring_culture_near": "near culture", + "ring_culture_different": "different culture", + "ring_join": "{band}, {culture}" }, "es": { "map_page_kicker": "GRUPOS ÉTNICOS NO EVANGELIZADOS", @@ -431,7 +439,15 @@ "sets_bounded_title": "Alcanzados y no alcanzados", "sets_centered_title": "Distancia a un vecino", "sets_bounded_tally": "{inside} dentro · {outside} fuera · {unknown} desconocido", - "sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar" + "sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar", + "ring_band_dialect": "Mismo dialecto", + "ring_band_language": "Misma lengua", + "ring_band_family": "Misma familia", + "ring_band_geography": "Solo cercanía", + "ring_culture_same": "mismo pueblo", + "ring_culture_near": "cultura cercana", + "ring_culture_different": "cultura distinta", + "ring_join": "{band}, {culture}" }, "fr": { "map_page_kicker": "GROUPES ETHNIQUES NON ATTEINTS", @@ -644,7 +660,15 @@ "sets_bounded_title": "Atteints, ou non", "sets_centered_title": "Distance d’un voisin", "sets_bounded_tally": "{inside} à l’intérieur · {outside} à l’extérieur · {unknown} inconnu", - "sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés" + "sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés", + "ring_band_dialect": "Même dialecte", + "ring_band_language": "Même langue", + "ring_band_family": "Même famille", + "ring_band_geography": "Proximité seule", + "ring_culture_same": "même peuple", + "ring_culture_near": "culture proche", + "ring_culture_different": "culture différente", + "ring_join": "{band}, {culture}" }, "pt": { "map_page_kicker": "GRUPOS ÉTNICOS NÃO ALCANÇADOS", @@ -855,7 +879,15 @@ "sets_bounded_title": "Alcançados e não alcançados", "sets_centered_title": "Distância de um vizinho", "sets_bounded_tally": "{inside} dentro · {outside} fora · {unknown} desconhecido", - "sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição" + "sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição", + "ring_band_dialect": "Mesmo dialeto", + "ring_band_language": "Mesma língua", + "ring_band_family": "Mesma família", + "ring_band_geography": "Apenas proximidade", + "ring_culture_same": "mesmo povo", + "ring_culture_near": "cultura próxima", + "ring_culture_different": "cultura diferente", + "ring_join": "{band}, {culture}" }, "de": { "map_page_kicker": "UNERREICHTE VOLKSGRUPPEN", @@ -1068,7 +1100,15 @@ "sets_bounded_title": "Erreicht und nicht erreicht", "sets_centered_title": "Abstand zu einem Nachbarn", "sets_bounded_tally": "{inside} innen · {outside} außen · {unknown} unbekannt", - "sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz" + "sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz", + "ring_band_dialect": "Gleicher Dialekt", + "ring_band_language": "Gleiche Sprache", + "ring_band_family": "Gleiche Sprachfamilie", + "ring_band_geography": "Nur benachbart", + "ring_culture_same": "gleiches Volk", + "ring_culture_near": "verwandte Kultur", + "ring_culture_different": "andere Kultur", + "ring_join": "{band}, {culture}" }, "it": { "map_page_kicker": "GRUPPI ETNICI NON RAGGIUNTI", @@ -1279,7 +1319,15 @@ "sets_bounded_title": "Raggiunti e non raggiunti", "sets_centered_title": "Distanza da un vicino", "sets_bounded_tally": "{inside} dentro · {outside} fuori · {unknown} sconosciuto", - "sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati" + "sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati", + "ring_band_dialect": "Stesso dialetto", + "ring_band_language": "Stessa lingua", + "ring_band_family": "Stessa famiglia", + "ring_band_geography": "Solo vicinanza", + "ring_culture_same": "stesso popolo", + "ring_culture_near": "cultura vicina", + "ring_culture_different": "cultura diversa", + "ring_join": "{band}, {culture}" }, "ro": { "map_page_kicker": "GRUPURI ETNICE NEATINSE", @@ -1490,7 +1538,15 @@ "sets_bounded_title": "Atinse și neatinse", "sets_centered_title": "Distanța față de un vecin", "sets_bounded_tally": "{inside} înăuntru · {outside} în afară · {unknown} necunoscut", - "sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate" + "sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate", + "ring_band_dialect": "Același dialect", + "ring_band_language": "Aceeași limbă", + "ring_band_family": "Aceeași familie", + "ring_band_geography": "Doar în apropiere", + "ring_culture_same": "același popor", + "ring_culture_near": "cultură apropiată", + "ring_culture_different": "cultură diferită", + "ring_join": "{band}, {culture}" }, "ru": { "map_page_kicker": "НЕОХВАЧЕННЫЕ НАРОДЫ", @@ -1703,7 +1759,15 @@ "sets_bounded_title": "Достигнутые и нет", "sets_centered_title": "Расстояние до соседа", "sets_bounded_tally": "{inside} внутри · {outside} снаружи · {unknown} неизвестно", - "sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места" + "sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места", + "ring_band_dialect": "Тот же диалект", + "ring_band_language": "Тот же язык", + "ring_band_family": "Та же семья", + "ring_band_geography": "Только соседство", + "ring_culture_same": "тот же народ", + "ring_culture_near": "близкая культура", + "ring_culture_different": "другая культура", + "ring_join": "{band}, {culture}" }, "ar": { "map_page_kicker": "شعوب غير مبلَّغة", @@ -1916,7 +1980,15 @@ "sets_bounded_title": "وصلتهم البشارة ومن لم تصلهم", "sets_centered_title": "المسافة إلى جار", "sets_bounded_tally": "{inside} في الداخل · {outside} في الخارج · {unknown} غير معروف", - "sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع" + "sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع", + "ring_band_dialect": "اللهجة نفسها", + "ring_band_language": "اللغة نفسها", + "ring_band_family": "العائلة اللغوية نفسها", + "ring_band_geography": "الجوار فقط", + "ring_culture_same": "الشعب نفسه", + "ring_culture_near": "ثقافة قريبة", + "ring_culture_different": "ثقافة مختلفة", + "ring_join": "{band}، {culture}" }, "hi": { "map_page_kicker": "अनछुए जनसमूह", @@ -2127,7 +2199,15 @@ "sets_bounded_title": "पहुँचे और नहीं पहुँचे", "sets_centered_title": "पड़ोसी से दूरी", "sets_bounded_tally": "{inside} भीतर · {outside} बाहर · {unknown} अज्ञात", - "sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित" + "sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित", + "ring_band_dialect": "वही बोली", + "ring_band_language": "वही भाषा", + "ring_band_family": "वही भाषा-परिवार", + "ring_band_geography": "केवल निकटता", + "ring_culture_same": "वही जन", + "ring_culture_near": "निकट संस्कृति", + "ring_culture_different": "भिन्न संस्कृति", + "ring_join": "{band}, {culture}" }, "zh": { "map_page_kicker": "未得之民", @@ -2338,6 +2418,14 @@ "sets_bounded_title": "已触及与未触及", "sets_centered_title": "与邻近族群的距离", "sets_bounded_tally": "{inside} 圈内 · {outside} 圈外 · {unknown} 未知", - "sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位" + "sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位", + "ring_band_dialect": "同一方言", + "ring_band_language": "同一语言", + "ring_band_family": "同一语系", + "ring_band_geography": "仅地理相邻", + "ring_culture_same": "同一族群", + "ring_culture_near": "相近文化", + "ring_culture_different": "不同文化", + "ring_join": "{band},{culture}" } }