From 81fd6dc7d5099c44c24aa9e07ce7c4a387775a4e Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Thu, 1 Oct 2026 10:52:55 +0000 Subject: [PATCH 1/5] Landing precondition: a library change's freshness scope includes shipped bundles the manifest does not list (template-bundle) --- internal/landing-preconditions.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/landing-preconditions.mjs b/internal/landing-preconditions.mjs index b6d13ca2d..0bac5b302 100644 --- a/internal/landing-preconditions.mjs +++ b/internal/landing-preconditions.mjs @@ -36,7 +36,7 @@ process.stdout.write(`LANDING-PRECONDITIONS: start — pid ${process.pid}, node ${process.version}\n`) import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' @@ -181,7 +181,14 @@ if (process.env.DOXA_SKIP_FRESHNESS === '1') { const libSet = new Set(libChanged) const areas = new Set(libChanged.map((f) => (f.match(/^library\/i18n\/locales\/[^/]+\/([^/]+)\.json$/) || [])[1]).filter(Boolean)) const manifest = JSON.parse(readFileSync(resolve(ROOT, 'public', TREE_NAME, 'manifest.json'), 'utf8')) - for (const { bundle } of manifest.bundles) { + // ★ EVERY SHIPPED BUNDLE, NOT ONLY THE MANIFEST'S. A bundle the tree ships but does not list (template-bundle) + // was never in scope, so a library change it imports left it stale with no landing to rebuild it; a final + // all-bundle freshness run found it weeks of library changes behind. Shipped = a plugin with an index.js + // whose built /.js is in the tree. + const shipped = readdirSync(resolve(ROOT, 'plugin-designs')).filter((d) => existsSync(resolve(ROOT, 'plugin-designs', d, 'index.js')) + && existsSync(resolve(ROOT, 'public', TREE_NAME, d, d + '.js'))) + const candidates = [...new Set([...manifest.bundles.map((b) => b.bundle), ...shipped])] + for (const bundle of candidates) { if (!existsSync(resolve(ROOT, 'plugin-designs', bundle, 'index.js'))) continue const graph = bundleGraph(ROOT, bundle).map((f) => f.slice(ROOT.length + 1)) if (graph.some((f) => libSet.has(f)) || (areas.size && areasForBundle(ROOT, bundle).areas.some((a) => areas.has(a)))) own.add(bundle) From c626a1fc027b9d6f80a93f67caf8efec7b0bbd0f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Thu, 1 Oct 2026 10:52:55 +0000 Subject: [PATCH 2/5] Packet fixtures build in their own directory, so the real Tibet zone packet survives check:packet-offline --- internal/build-packet-fixtures.mjs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/build-packet-fixtures.mjs b/internal/build-packet-fixtures.mjs index dd15a2ead..b9c344c2d 100644 --- a/internal/build-packet-fixtures.mjs +++ b/internal/build-packet-fixtures.mjs @@ -26,7 +26,7 @@ * * Usage: node internal/build-packet-fixtures.mjs */ -import { existsSync, renameSync, readdirSync } from 'node:fs' +import { existsSync, renameSync, readdirSync, mkdirSync, rmSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { spawnSync } from 'node:child_process' @@ -38,6 +38,11 @@ const OUT = resolve(REPO, 'library/scripts/posters/out/packets') // Every fixture is built against this zone: the records come from the fixture, and the zone only // supplies page 1's map. Keeping it constant means a shape difference is the ONLY variable. const ZONE = 'asia-china-tibet-autonomous-region' +// ★ BUILT IN THEIR OWN DIRECTORY. packet.mjs writes .html into its output dir, and every fixture is built +// against the REAL Tibet zone; built straight into out/packets, each run overwrote the real Tibet packet and the +// rename then removed it, so check:packets-all reported a generated zone as missing. PACKET_OUT sends each build +// to a private directory; only the renamed fixture-.html reaches out/packets. +const BUILD = join(OUT, '.fixture-build') if (!existsSync(FIXTURES)) { console.log(`PACKET-FIXTURES: SKIP — no fixtures directory at ${FIXTURES}`) @@ -54,14 +59,16 @@ console.log(`PACKET-FIXTURES: building ${fixtures.length} fixture(s)`) const built = [] const failed = [] +mkdirSync(OUT, { recursive: true }) +mkdirSync(BUILD, { recursive: true }) for (const fx of fixtures) { const name = fx.replace(/^records-/, '').replace(/\.json$/, '') const res = spawnSync(process.execPath, [join(REPO, 'library/scripts/posters/packet.mjs')], { cwd: REPO, encoding: 'utf8', - env: { ...process.env, PACKET_RECORDS: join(FIXTURES, fx), ONLY: ZONE }, + env: { ...process.env, PACKET_RECORDS: join(FIXTURES, fx), ONLY: ZONE, PACKET_OUT: BUILD }, }) - const produced = join(OUT, `${ZONE}.html`) + const produced = join(BUILD, `${ZONE}.html`) if (res.status !== 0 || !existsSync(produced)) { // The ERROR line, not the last line: Node ends a crash with its version banner, so the last line // of a TDZ ReferenceError read "Node.js v20.20.2" and named nothing. @@ -77,6 +84,8 @@ for (const fx of fixtures) { console.log(` ✓ ${fx} -> fixture-${name}.html`) } +try { rmSync(BUILD, { recursive: true, force: true }) } catch { /* best effort */ } + if (failed.length) { console.error(`\nPACKET-FIXTURES: FAIL — ${failed.length} fixture(s) did not build`) for (const f of failed) console.error(' - ' + f) From a63a873ef4a2bcb4bc489809faa495858d20e501 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Wed, 30 Sep 2026 23:26:04 +0000 Subject: [PATCH 3/5] Country Posters: India's state and union territory cards named in the reader's language; the state table retried after a failure countryPosters.stateNames x11, keyed by ISO 3166-2, for the India section's CARD labels (the wall, the print HUD, the packet and list titles). The printed sheet stays English by recorded decision, like every country poster. Generated, not typed: scripts/emit-india-state-names.mjs reads Wikidata labels by ISO 3166-2 (P300), dissolved items excluded; Wikidata is CC0 (source, licence, query, date and every fallback in data/india-state-names.meta.json). - Four codes ISO has renamed (IN-CT/OR/TG/UT -> CG/OD/TS/UK) are queried under the current code. - English is the table's own name. Where the table is briefer than its item ("Delhi" vs "National Capital Territory of Delhi"), the same-named item's labels win, so hi/ru match the English. - Reviewed by eye (hi, ar, zh): two explicit overrides with their evidence (ar Odisha = the ar.wikipedia title, not the pre-2011 label; zh Dadra and Nagar Haveli and Daman and Diu = Simplified, the only label being Traditional). A zh name from a plain "zh" label with no reviewed override FAILS the generator (poisoned: it did). The India section retries a failed table fetch at 5 s, 20 s and 60 s (bounded, cancelled on unmount), instead of staying hidden until the filter changes. check:country-sheet-one-path ARM D: every locale's stateNames == the table's 36 codes (a runtime-built key the i18n call-site gates cannot see), English == the table, and the licence recorded; its two poisons join the self-test. --- library/i18n/locales/ar/countryPosters.json | 40 +++++- library/i18n/locales/de/countryPosters.json | 40 +++++- library/i18n/locales/en/countryPosters.json | 40 +++++- library/i18n/locales/es/countryPosters.json | 40 +++++- library/i18n/locales/fr/countryPosters.json | 40 +++++- library/i18n/locales/hi/countryPosters.json | 40 +++++- library/i18n/locales/it/countryPosters.json | 40 +++++- library/i18n/locales/pt/countryPosters.json | 40 +++++- library/i18n/locales/ro/countryPosters.json | 40 +++++- library/i18n/locales/ru/countryPosters.json | 40 +++++- library/i18n/locales/zh/countryPosters.json | 40 +++++- .../country-posters/country-poster-wall.vue | 34 +++-- .../data/india-state-names.meta.json | 44 ++++++ .../scripts/check-country-sheet-one-path.mjs | 36 ++++- .../scripts/emit-india-state-names.mjs | 134 ++++++++++++++++++ 15 files changed, 661 insertions(+), 27 deletions(-) create mode 100644 plugin-designs/country-posters/data/india-state-names.meta.json create mode 100644 plugin-designs/country-posters/scripts/emit-india-state-names.mjs diff --git a/library/i18n/locales/ar/countryPosters.json b/library/i18n/locales/ar/countryPosters.json index 425618c10..b8f3634d2 100644 --- a/library/i18n/locales/ar/countryPosters.json +++ b/library/i18n/locales/ar/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "للهند قسمها الخاص في المقدّمة: عدد الشعوب غير المُبشَّرة فيها {count}، وهو أكثر بكثير من أي بلد آخر، لذا لكل ولاية وإقليم اتحادي فيها ملصقه الخاص.", "indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة.", - "indiaItemCount": "لا ولايات ولا أقاليم | ولاية أو إقليم اتحادي واحد | ولايتان أو إقليمان | {n} ولايات وأقاليم اتحادية | {n} ولاية وإقليمًا اتحاديًا | {n} ولاية وإقليم اتحادي" + "indiaItemCount": "لا ولايات ولا أقاليم | ولاية أو إقليم اتحادي واحد | ولايتان أو إقليمان | {n} ولايات وأقاليم اتحادية | {n} ولاية وإقليمًا اتحاديًا | {n} ولاية وإقليم اتحادي", + "stateNames": { + "IN-AN": "جزر أندمان وَ نِكُبَار", + "IN-AP": "أندرا برديش", + "IN-AR": "أروناجل برديش", + "IN-AS": "آسام", + "IN-BR": "بهار", + "IN-CH": "شَنديغار", + "IN-CT": "تشاتيسغار", + "IN-DH": "دادرا وناغار هافيلي ودامان وديو", + "IN-DL": "دلهي", + "IN-GA": "غوا", + "IN-GJ": "كجرات", + "IN-HP": "هماجل برديش", + "IN-HR": "هريانة", + "IN-JH": "جاركند", + "IN-JK": "إقليم جمون وكشمير", + "IN-KA": "كرناتك", + "IN-KL": "كيرلا", + "IN-LA": "لداخ", + "IN-LD": "لكشديب", + "IN-MH": "مهاراشترة", + "IN-ML": "ميكالية", + "IN-MN": "منيبور", + "IN-MP": "مدية برديش", + "IN-MZ": "مِزُورَم", + "IN-NL": "ناغالند", + "IN-OR": "أديشة", + "IN-PB": "بنجاب", + "IN-PY": "بُدُجيري", + "IN-RJ": "راجستان", + "IN-SK": "سِكِم", + "IN-TG": "تلنكانة", + "IN-TN": "تاميل نادو", + "IN-TR": "تريبورة", + "IN-UP": "أتر برديش", + "IN-UT": "أوتاراخند", + "IN-WB": "بنغال الغربية" + } } diff --git a/library/i18n/locales/de/countryPosters.json b/library/i18n/locales/de/countryPosters.json index 66822bd88..37548a896 100644 --- a/library/i18n/locales/de/countryPosters.json +++ b/library/i18n/locales/de/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "Indien hat einen eigenen Abschnitt, ganz oben: Mit {count} unerreichten Volksgruppen, weit mehr als jedes andere Land, hat jeder seiner Bundesstaaten und Unionsterritorien ein eigenes Plakat.", "indiaStateNoGroups": "Hier sind keine unerreichten Volksgruppen verzeichnet.", - "indiaItemCount": "{n} Bundesstaat oder Unionsterritorium | {n} Bundesstaaten und Unionsterritorien" + "indiaItemCount": "{n} Bundesstaat oder Unionsterritorium | {n} Bundesstaaten und Unionsterritorien", + "stateNames": { + "IN-AN": "Andamanen und Nikobaren", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra und Nagar Haveli und Daman und Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu und Kashmir", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Lakshadweep", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Odisha", + "IN-PB": "Punjab", + "IN-PY": "Puducherry", + "IN-RJ": "Rajasthan", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "Westbengalen" + } } diff --git a/library/i18n/locales/en/countryPosters.json b/library/i18n/locales/en/countryPosters.json index acf3da70a..2b0108393 100644 --- a/library/i18n/locales/en/countryPosters.json +++ b/library/i18n/locales/en/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "India has its own section, first: it has {count} unreached people groups, far more than any other country, so each of its states and union territories has its own poster.", "indiaStateNoGroups": "No unreached people groups are listed here.", - "indiaItemCount": "{n} states and union territories | {n} state or union territory | {n} states and union territories" + "indiaItemCount": "{n} states and union territories | {n} state or union territory | {n} states and union territories", + "stateNames": { + "IN-AN": "Andaman and Nicobar Islands", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra and Nagar Haveli and Daman and Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu and Kashmir", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Lakshadweep", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Odisha", + "IN-PB": "Punjab", + "IN-PY": "Puducherry", + "IN-RJ": "Rajasthan", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "West Bengal" + } } diff --git a/library/i18n/locales/es/countryPosters.json b/library/i18n/locales/es/countryPosters.json index 793861c75..934fcfe66 100644 --- a/library/i18n/locales/es/countryPosters.json +++ b/library/i18n/locales/es/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "India tiene su propia sección, la primera: tiene {count} pueblos no alcanzados, muchos más que cualquier otro país, así que cada uno de sus estados y territorios de la unión tiene su propio cartel.", "indiaStateNoGroups": "Aquí no figura ningún pueblo no alcanzado.", - "indiaItemCount": "{n} estado o territorio de la unión | {n} de estados y territorios de la unión | {n} estados y territorios de la unión" + "indiaItemCount": "{n} estado o territorio de la unión | {n} de estados y territorios de la unión | {n} estados y territorios de la unión", + "stateNames": { + "IN-AN": "Islas Andamán y Nicobar", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra y Nagar Haveli y Damán y Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Guyarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu y Cachemira", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Laquedivas", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Orissa", + "IN-PB": "Punyab", + "IN-PY": "Puducherry", + "IN-RJ": "Rajastán", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "Bengala Occidental" + } } diff --git a/library/i18n/locales/fr/countryPosters.json b/library/i18n/locales/fr/countryPosters.json index e03e089ca..d09cf6943 100644 --- a/library/i18n/locales/fr/countryPosters.json +++ b/library/i18n/locales/fr/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "L’Inde a sa propre section, en premier : elle compte {count} peuples non atteints, bien plus que tout autre pays ; chacun de ses États et territoires de l’Union a donc sa propre affiche.", "indiaStateNoGroups": "Aucun peuple non atteint n’est répertorié ici.", - "indiaItemCount": "{n} État ou territoire de l’Union | {n} d’États et territoires de l’Union | {n} États et territoires de l’Union" + "indiaItemCount": "{n} État ou territoire de l’Union | {n} d’États et territoires de l’Union | {n} États et territoires de l’Union", + "stateNames": { + "IN-AN": "Îles Andaman-et-Nicobar", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra et Nagar Haveli et Daman et Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu-et-Cachemire", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Lakshadweep", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Odisha", + "IN-PB": "Pendjab", + "IN-PY": "Territoire de Pondichéry", + "IN-RJ": "Rajasthan", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "Bengale-Occidental" + } } diff --git a/library/i18n/locales/hi/countryPosters.json b/library/i18n/locales/hi/countryPosters.json index baa73d419..913eed77e 100644 --- a/library/i18n/locales/hi/countryPosters.json +++ b/library/i18n/locales/hi/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "भारत का अपना अलग खंड है, सबसे पहले: इसमें {count} अपहुँचे जनसमूह हैं, किसी भी अन्य देश से कहीं अधिक, इसलिए इसके हर राज्य और केंद्र शासित प्रदेश का अपना पोस्टर है।", "indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।", - "indiaItemCount": "{n} राज्य या केंद्र शासित प्रदेश | {n} राज्य और केंद्र शासित प्रदेश" + "indiaItemCount": "{n} राज्य या केंद्र शासित प्रदेश | {n} राज्य और केंद्र शासित प्रदेश", + "stateNames": { + "IN-AN": "अण्डमान और निकोबार द्वीपसमूह", + "IN-AP": "आन्ध्र प्रदेश", + "IN-AR": "अरुणाचल प्रदेश", + "IN-AS": "असम", + "IN-BR": "बिहार", + "IN-CH": "चण्डीगढ़", + "IN-CT": "छत्तीसगढ़", + "IN-DH": "दादरा और नगर हवेली और दमन और दीव", + "IN-DL": "दिल्ली", + "IN-GA": "गोवा", + "IN-GJ": "गुजरात", + "IN-HP": "हिमाचल प्रदेश", + "IN-HR": "हरियाणा", + "IN-JH": "झारखण्ड", + "IN-JK": "जम्मू और कश्मीर", + "IN-KA": "कर्नाटक", + "IN-KL": "केरल", + "IN-LA": "लद्दाख़", + "IN-LD": "लक्षद्वीप", + "IN-MH": "महाराष्ट्र", + "IN-ML": "मेघालय", + "IN-MN": "मणिपुर", + "IN-MP": "मध्य प्रदेश", + "IN-MZ": "मिज़ोरम", + "IN-NL": "नागालैण्ड", + "IN-OR": "ओडिशा", + "IN-PB": "पंजाब", + "IN-PY": "पुदुचेरी", + "IN-RJ": "राजस्थान", + "IN-SK": "सिक्किम", + "IN-TG": "तेलंगाना", + "IN-TN": "तमिल नाडु", + "IN-TR": "त्रिपुरा", + "IN-UP": "उत्तर प्रदेश", + "IN-UT": "उत्तराखण्ड", + "IN-WB": "पश्चिम बंगाल" + } } diff --git a/library/i18n/locales/it/countryPosters.json b/library/i18n/locales/it/countryPosters.json index a21b06082..090389bc5 100644 --- a/library/i18n/locales/it/countryPosters.json +++ b/library/i18n/locales/it/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "L’India ha una sezione tutta sua, la prima: conta {count} popoli non raggiunti, molti più di qualsiasi altro paese, quindi ognuno dei suoi stati e territori dell’Unione ha il proprio manifesto.", "indiaStateNoGroups": "Qui non è elencato alcun popolo non raggiunto.", - "indiaItemCount": "{n} stato o territorio dell’Unione | {n} di stati e territori dell’Unione | {n} stati e territori dell’Unione" + "indiaItemCount": "{n} stato o territorio dell’Unione | {n} di stati e territori dell’Unione | {n} stati e territori dell’Unione", + "stateNames": { + "IN-AN": "isole Andamane e Nicobare", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra e Nagar Haveli e Daman e Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu e Kashmir", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Laccadive", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Orissa", + "IN-PB": "Punjab", + "IN-PY": "Pondicherry", + "IN-RJ": "Rajasthan", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "Bengala Occidentale" + } } diff --git a/library/i18n/locales/pt/countryPosters.json b/library/i18n/locales/pt/countryPosters.json index e43b71685..66501cd1c 100644 --- a/library/i18n/locales/pt/countryPosters.json +++ b/library/i18n/locales/pt/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "A Índia tem a sua própria seção, a primeira: tem {count} povos não alcançados, muito mais do que qualquer outro país, por isso cada um dos seus estados e territórios da união tem o seu próprio cartaz.", "indiaStateNoGroups": "Nenhum povo não alcançado está listado aqui.", - "indiaItemCount": "{n} estado ou território da união | {n} de estados e territórios da união | {n} estados e territórios da união" + "indiaItemCount": "{n} estado ou território da união | {n} de estados e territórios da união | {n} estados e territórios da união", + "stateNames": { + "IN-AN": "Andamão e Nicobar", + "IN-AP": "Andra Pradexe", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadrá e Nagar Aveli e Damão e Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarate", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu e Caxemira", + "IN-KA": "Carnataca", + "IN-KL": "Kerala", + "IN-LA": "Ladaque", + "IN-LD": "Laquedivas", + "IN-MH": "Maarastra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Orissa", + "IN-PB": "Punjab", + "IN-PY": "Puducherry", + "IN-RJ": "Rajastão", + "IN-SK": "Siquim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Utar Pradexe", + "IN-UT": "Utaracanda", + "IN-WB": "Bengala Ocidental" + } } diff --git a/library/i18n/locales/ro/countryPosters.json b/library/i18n/locales/ro/countryPosters.json index 5ccf50fe2..11b0d912f 100644 --- a/library/i18n/locales/ro/countryPosters.json +++ b/library/i18n/locales/ro/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "India are propria secțiune, prima: are {count} popoare neatinse, mult mai multe decât orice altă țară, așa că fiecare dintre statele și teritoriile sale ale Uniunii are propriul afiș.", "indiaStateNoGroups": "Aici nu este listat niciun popor neatins.", - "indiaItemCount": "un stat sau teritoriu al Uniunii | {n} state și teritorii ale Uniunii | {n} de state și teritorii ale Uniunii" + "indiaItemCount": "un stat sau teritoriu al Uniunii | {n} state și teritorii ale Uniunii | {n} de state și teritorii ale Uniunii", + "stateNames": { + "IN-AN": "Insulele Andaman și Nicobar", + "IN-AP": "Andhra Pradesh", + "IN-AR": "Arunachal Pradesh", + "IN-AS": "Assam", + "IN-BR": "Bihar", + "IN-CH": "Teritoriul Unional Chandigarh", + "IN-CT": "Chhattisgarh", + "IN-DH": "Dadra și Nagar Haveli și Daman și Diu", + "IN-DL": "Delhi", + "IN-GA": "Goa", + "IN-GJ": "Gujarat", + "IN-HP": "Himachal Pradesh", + "IN-HR": "Haryana", + "IN-JH": "Jharkhand", + "IN-JK": "Jammu și Cașmir", + "IN-KA": "Karnataka", + "IN-KL": "Kerala", + "IN-LA": "Ladakh", + "IN-LD": "Lakshadweep", + "IN-MH": "Maharashtra", + "IN-ML": "Meghalaya", + "IN-MN": "Manipur", + "IN-MP": "Madhya Pradesh", + "IN-MZ": "Mizoram", + "IN-NL": "Nagaland", + "IN-OR": "Odisha", + "IN-PB": "Punjab", + "IN-PY": "Puducherry", + "IN-RJ": "Rajasthan", + "IN-SK": "Sikkim", + "IN-TG": "Telangana", + "IN-TN": "Tamil Nadu", + "IN-TR": "Tripura", + "IN-UP": "Uttar Pradesh", + "IN-UT": "Uttarakhand", + "IN-WB": "Bengalul de Vest" + } } diff --git a/library/i18n/locales/ru/countryPosters.json b/library/i18n/locales/ru/countryPosters.json index 7ff8cb749..6a944e0ef 100644 --- a/library/i18n/locales/ru/countryPosters.json +++ b/library/i18n/locales/ru/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "У Индии свой раздел, первый: число недостигнутых народов в ней — {count}, намного больше, чем в любой другой стране, поэтому у каждого её штата и союзной территории есть свой плакат.", "indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа.", - "indiaItemCount": "{n} штат или союзная территория | {n} штата и союзные территории | {n} штатов и союзных территорий | {n} штата и союзной территории" + "indiaItemCount": "{n} штат или союзная территория | {n} штата и союзные территории | {n} штатов и союзных территорий | {n} штата и союзной территории", + "stateNames": { + "IN-AN": "Андаманские и Никобарские острова", + "IN-AP": "Андхра-Прадеш", + "IN-AR": "Аруначал-Прадеш", + "IN-AS": "Ассам", + "IN-BR": "Бихар", + "IN-CH": "Чандигарх", + "IN-CT": "Чхаттисгарх", + "IN-DH": "Дадра и Нагар-Хавели и Даман и Диу", + "IN-DL": "Дели", + "IN-GA": "Гоа", + "IN-GJ": "Гуджарат", + "IN-HP": "Химачал-Прадеш", + "IN-HR": "Харьяна", + "IN-JH": "Джаркханд", + "IN-JK": "Джамму и Кашмир", + "IN-KA": "Карнатака", + "IN-KL": "Керала", + "IN-LA": "Ладакх", + "IN-LD": "Лакшадвип", + "IN-MH": "Махараштра", + "IN-ML": "Мегхалая", + "IN-MN": "Манипур", + "IN-MP": "Мадхья-Прадеш", + "IN-MZ": "Мизорам", + "IN-NL": "Нагаленд", + "IN-OR": "Одиша", + "IN-PB": "Пенджаб", + "IN-PY": "Пондичерри", + "IN-RJ": "Раджастхан", + "IN-SK": "Сикким", + "IN-TG": "Телингана", + "IN-TN": "Тамилнад", + "IN-TR": "Трипура", + "IN-UP": "Уттар-Прадеш", + "IN-UT": "Уттаракханд", + "IN-WB": "Западная Бенгалия" + } } diff --git a/library/i18n/locales/zh/countryPosters.json b/library/i18n/locales/zh/countryPosters.json index 5572a850d..083663880 100644 --- a/library/i18n/locales/zh/countryPosters.json +++ b/library/i18n/locales/zh/countryPosters.json @@ -1,5 +1,43 @@ { "indiaNote": "印度有单独的一栏,排在最前:这里有 {count} 个未得之民族群,远多于任何其他国家,因此它的每个邦和中央直辖区都有自己的海报。", "indiaStateNoGroups": "此处未列出任何未得之民族群。", - "indiaItemCount": "{n} 个邦和中央直辖区" + "indiaItemCount": "{n} 个邦和中央直辖区", + "stateNames": { + "IN-AN": "安达曼-尼科巴群岛", + "IN-AP": "安得拉邦", + "IN-AR": "阿鲁纳恰尔邦", + "IN-AS": "阿萨姆邦", + "IN-BR": "比哈尔邦", + "IN-CH": "昌迪加尔", + "IN-CT": "切蒂斯格尔邦", + "IN-DH": "达德拉-纳加尔哈维利和达曼-第乌", + "IN-DL": "德里", + "IN-GA": "果阿邦", + "IN-GJ": "古吉拉特邦", + "IN-HP": "喜马偕尔邦", + "IN-HR": "哈里亚纳邦", + "IN-JH": "贾坎德邦", + "IN-JK": "查谟和克什米尔", + "IN-KA": "卡纳塔克邦", + "IN-KL": "喀拉拉邦", + "IN-LA": "拉达克", + "IN-LD": "拉克沙群岛", + "IN-MH": "马哈拉施特拉邦", + "IN-ML": "梅加拉亚邦", + "IN-MN": "曼尼普尔邦", + "IN-MP": "中央邦", + "IN-MZ": "米佐拉姆邦", + "IN-NL": "那加兰邦", + "IN-OR": "奥迪沙邦", + "IN-PB": "旁遮普邦", + "IN-PY": "本地治里", + "IN-RJ": "拉贾斯坦邦", + "IN-SK": "锡金邦", + "IN-TG": "特伦甘纳邦", + "IN-TN": "泰米尔纳德邦", + "IN-TR": "特里普拉邦", + "IN-UP": "北方邦", + "IN-UT": "北阿坎德邦", + "IN-WB": "西孟加拉邦" + } } diff --git a/plugin-designs/country-posters/country-poster-wall.vue b/plugin-designs/country-posters/country-poster-wall.vue index e1a53c8af..6de2c4b8f 100644 --- a/plugin-designs/country-posters/country-poster-wall.vue +++ b/plugin-designs/country-posters/country-poster-wall.vue @@ -31,14 +31,14 @@ * 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, watch } from 'vue' +import { computed, inject, onBeforeUnmount, ref, watch } from 'vue' import PosterWall from '@map/components/poster/PosterWall.vue' import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js' // A country's name in the reader's language (Intl, joined from our ISO-3 codes). import { countryName } from '@map/utils/countryName.js' import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js' import { getApiBaseUrl } from '@map/api/apiBaseUrl.js' -import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js' +import { getRuntimeLocale, detectLocale, tStatic } 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. @@ -108,21 +108,30 @@ try { tRegions = useRegionsI18n().t } catch (_) { /* a host with no i18n reads t // ★ INDIA'S STATES AND UNION TERRITORIES, one poster each, like a country one level down. The table is // fetched (and sha-checked) only when the unfiltered wall is shown; a host's filtered wall never asks for it. -// A failure hides the section rather than showing cards that cannot print. States with no people groups -// listed (Nagaland, Mizoram) are NAMED with a card that says so, never silently left out. +// A failure hides the section rather than showing cards that cannot print, and is retried a bounded number of +// times (the loader drops a failed fetch from its cache, so a retry really fetches again). States with no people +// groups listed (Nagaland, Mizoram) are NAMED with a card that says so, never silently left out. const indiaStates = ref(null) +const STATE_RETRY_MS = [5000, 20000, 60000] +let stateRetry = null +function loadStates(attempt = 0) { + loadIndiaStates(assetsBase.value).then((table) => { indiaStates.value = table.states }).catch(() => { + if (attempt < STATE_RETRY_MS.length && !filtered.value) stateRetry = setTimeout(() => loadStates(attempt + 1), STATE_RETRY_MS[attempt]) + }) +} watch(filtered, (f) => { - if (f || indiaStates.value) return - loadIndiaStates(assetsBase.value).then((table) => { indiaStates.value = table.states }).catch(() => { indiaStates.value = null }) + if (f || indiaStates.value || stateRetry) return + loadStates() }, { immediate: true }) +onBeforeUnmount(() => { clearTimeout(stateRetry) }) const INDIA_REGION = (byIso3.get('IND') || {}).region || 'Asia' const stateItems = computed(() => { const all = indiaStates.value if (filtered.value || !all) return [] const withGroups = statesWithPosters(all).map((st) => ({ - id: st.id, label: st.name, count: st.count, sheet: '3x3', region: INDIA_REGION, indiaState: true, slugs: st.slugs, + id: st.id, iso: st.iso, label: st.name, count: st.count, sheet: '3x3', region: INDIA_REGION, indiaState: true, slugs: st.slugs, })) - const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] })) + const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, iso: st.iso, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] })) return [...withGroups, ...none] }) @@ -130,12 +139,15 @@ const items = computed(() => { const l = wallLocale && (wallLocale.value ?? wallLocale) const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale() || 'en' const countries = selected.value - // State names stay as the table has them (place names from the data); only country names are localized. - if (/^en\b/i.test(locale)) return [...countries, ...stateItems.value] + // A state's card is named in the reader's language from the catalogue (countryPosters.stateNames, generated from + // Wikidata by scripts/emit-india-state-names.mjs); its printed sheet stays English, like a country's. + const states = /^en\b/i.test(locale) ? stateItems.value + : stateItems.value.map((st) => ({ ...st, label: tStatic(`countryPosters.stateNames.${st.iso}`, locale) || st.label })) + if (/^en\b/i.test(locale)) return [...countries, ...states] return [...countries.map((c) => { const name = countryName(c.iso3, locale) return name && name !== c.iso3 ? { ...c, label: name } : c - }), ...stateItems.value] + }), ...states] }) // ★ EVERY COUNTRY PRINTS ITS PRAYER RESOURCES — the list, the card deck and the packet — through the diff --git a/plugin-designs/country-posters/data/india-state-names.meta.json b/plugin-designs/country-posters/data/india-state-names.meta.json new file mode 100644 index 000000000..bd8998d08 --- /dev/null +++ b/plugin-designs/country-posters/data/india-state-names.meta.json @@ -0,0 +1,44 @@ +{ + "what": "India's 36 states and union territories, named in the 11 UI locales, for the Country Posters wall's card labels.", + "source": "Wikidata (https://www.wikidata.org): labels of the items whose ISO 3166-2 code (P300) matches data/people-groups/india-states.json; dissolved items (P576) excluded.", + "licence": "CC0 1.0: Wikidata structured data is released under CC0 (https://creativecommons.org/publicdomain/zero/1.0/).", + "english": "the state table's own name (the English wall is unchanged); Wikidata's English is only a cross-check", + "retrieved": "2026-09-30", + "regenerate": "node plugin-designs/country-posters/scripts/emit-india-state-names.mjs", + "query": "SELECT ?code ?lang ?label WHERE {\n VALUES ?code { \"IN-AN\" \"IN-AP\" \"IN-AR\" \"IN-AS\" \"IN-BR\" \"IN-CG\" \"IN-CH\" \"IN-DH\" \"IN-DL\" \"IN-GA\" \"IN-GJ\" \"IN-HP\" \"IN-HR\" \"IN-JH\" \"IN-JK\" \"IN-KA\" \"IN-KL\" \"IN-LA\" \"IN-LD\" \"IN-MH\" \"IN-ML\" \"IN-MN\" \"IN-MP\" \"IN-MZ\" \"IN-NL\" \"IN-OD\" \"IN-PB\" \"IN-PY\" \"IN-RJ\" \"IN-SK\" \"IN-TN\" \"IN-TR\" \"IN-TS\" \"IN-UK\" \"IN-UP\" \"IN-WB\" }\n ?item wdt:P300 ?code .\n FILTER NOT EXISTS { ?item wdt:P576 ?dissolved }\n ?item rdfs:label ?label . BIND(LANG(?label) AS ?lang)\n FILTER(?lang IN (\"en\", \"es\", \"fr\", \"pt\", \"de\", \"it\", \"ro\", \"ru\", \"ar\", \"hi\", \"zh-hans\", \"zh-cn\", \"zh\"))\n}", + "codeAliases": { + "IN-CT": "IN-CG", + "IN-OR": "IN-OD", + "IN-TG": "IN-TS", + "IN-UT": "IN-UK" + }, + "reviewedOverrides": [ + "IN-DH zh: \"达德拉-纳加尔哈维利和达曼-第乌\" (Wikidata has only a Traditional-script zh label; converted character by character to the Simplified catalogue script)", + "IN-OR ar: \"أديشة\" (the label is the pre-2011 name Orissa (أريسة); ar.wikipedia's title for the item is أديشة (Odisha))" + ], + "gaps": [], + "ambiguous": [], + "fallbacks": [ + "IN-CH es: \"Chandigarh\" from the same-named item (1 of 1)", + "IN-CH fr: \"Chandigarh\" from the same-named item (1 of 1)", + "IN-CH pt: \"Chandigarh\" from the same-named item (1 of 1)", + "IN-CH de: \"Chandigarh\" from the same-named item (1 of 1)", + "IN-CH it: \"Chandigarh\" from the same-named item (1 of 1)", + "IN-CH ar: \"شَنديغار\" from the same-named item (1 of 1)", + "IN-CH hi: \"चण्डीगढ़\" from the same-named item (1 of 1)", + "IN-CH zh: \"昌迪加尔\" from the same-named item (1 of 1)", + "IN-DL es: \"Delhi\" from the same-named item (preferred)", + "IN-DL fr: \"Delhi\" from the same-named item (preferred)", + "IN-DL pt: \"Delhi\" from the same-named item (preferred)", + "IN-DL de: \"Delhi\" from the same-named item (preferred)", + "IN-DL it: \"Delhi\" from the same-named item (preferred)", + "IN-DL ro: \"Delhi\" from the same-named item (preferred)", + "IN-DL ru: \"Дели\" from the same-named item (preferred)", + "IN-DL ar: \"دلهي\" from the same-named item (preferred)", + "IN-DL hi: \"दिल्ली\" from the same-named item (preferred)", + "IN-DL zh: \"德里\" from the same-named item (preferred)" + ], + "englishMismatches": [ + "IN-DL: table \"Delhi\" vs Wikidata \"National Capital Territory of Delhi\"; same-named labels preferred" + ] +} diff --git a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs index 7201dd09b..bcda80cca 100644 --- a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs +++ b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs @@ -141,9 +141,27 @@ export function armC(H, states, adm1Ind) { return fails } -// ARM E: India is its OWN section, FIRST, counted ONCE. The India country card, the state cards and the none-listed -// cards group together; the section's total is India's own count (not the card sum, which counts 855 twice); on that -// true total it outranks every regional section. PosterWall's default (no sectionTotal) must still be the card sum. +// ARM D: the state cards' localized names cover EXACTLY the table's divisions in every locale (a runtime-built key, +// `countryPosters.stateNames.${iso}`, that the i18n call-site gates cannot see), English is the table's own name, and +// the generated names carry their source licence. +const NAME_LOCALES = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh'] +export function armD(states, areas, meta) { + const fails = [] + const want = new Set((states || []).map((st) => st.iso)) + for (const l of NAME_LOCALES) { + const names = (areas[l] && areas[l].stateNames) || {} + const have = new Set(Object.keys(names)) + const missing = [...want].filter((x) => !have.has(x)), extra = [...have].filter((x) => !want.has(x)) + const empty = Object.entries(names).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k) + if (missing.length || extra.length || empty.length) fails.push(`${l} countryPosters.stateNames: missing [${missing}] extra [${extra}] empty [${empty}]`) + } + const en = (areas.en && areas.en.stateNames) || {} + const drift = (states || []).filter((st) => en[st.iso] !== st.name).map((st) => st.iso) + if (drift.length) fails.push(`English state names differ from the table's: ${drift.join(', ')} (re-run emit-india-state-names.mjs)`) + if (!meta || !/CC0/.test(String(meta.licence || ''))) fails.push('india-state-names.meta.json must record the source licence (CC0)') + return fails +} + // ARM F — A DESKTOP-ONLY CARD ON A PHONE (India): in EACH caller (the wall, the Regions list) its rows REJECT with // poster.phoneLimitAll before any record is fetched, never resolve [] (the print path reads [] as "that list has no // people groups to print", on a card that has 855), and the card shows the same full-block notice. @@ -158,6 +176,9 @@ export function armF(srcs) { return fails } +// ARM E: India is its OWN section, FIRST, counted ONCE. The India country card, the state cards and the none-listed +// cards group together; the section's total is India's own count (not the card sum, which counts 855 twice); on that +// true total it outranks every regional section. PosterWall's default (no sectionTotal) must still be the card sum. export function armE(countries, states, wallSrc, posterWallSrc, H) { const fails = [] if (!/:section-total=/.test(wallSrc)) fails.push('country-poster-wall.vue does not pass :section-total, so India is counted twice') @@ -189,6 +210,9 @@ async function main() { const adm1Ind = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson'), 'utf8')) if (states.length < 30 || (adm1Ind.features || []).length < 30) { console.log(`NOT MEASURED: state table ${states.length} / ADM1 ${(adm1Ind.features || []).length} features (floor 30)`); process.exitCode = 75; return } + const areas = Object.fromEntries(NAME_LOCALES.map((l) => [l, JSON.parse(readFileSync(join(ROOT, `library/i18n/locales/${l}/countryPosters.json`), 'utf8'))])) + const meta = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/country-posters/data/india-state-names.meta.json'), 'utf8')) + const files = {} for (const p of listSources(PD)) files[relative(ROOT, p)] = readFileSync(p, 'utf8') @@ -210,6 +234,8 @@ async function main() { ['a packet page becomes a large sheet', () => armB(seriesFor, countries, (it, f) => (it?.iso3 === 'IND' ? '60x96' : f))], ['the wall resolves [] for a phone-blocked card', () => armF({ wall: files[wall].replace("const desktopOnly = () => Promise.reject(keyedError('poster.phoneLimitAll'", "const desktopOnly = () => Promise.resolve([]) || (keyedError('poster.phoneLimitAll'"), regions: REG })], ['the Regions list resolves [] for a phone-blocked card', () => armF({ wall: files[wall], regions: REG.replace("phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll'", "phoneBlocked(item) ? Promise.resolve([]) || (keyedError('poster.phoneLimitAll'") })], + ['a locale loses a state name', () => armD(states, { ...areas, ro: { ...areas.ro, stateNames: Object.fromEntries(Object.entries(areas.ro.stateNames).filter(([k]) => k !== 'IN-GJ')) } }, meta)], + ['the names lose their licence line', () => armD(states, areas, { ...meta, licence: '' })], ['a helper that mutates the shared ADM1', () => armC({ ...H, stateGeometry: (z, a, n) => { (a.features || []).forEach((f) => { if (f.properties.shapeName === n) f.properties.listId = z.id }); return H.stateGeometry(z, a, n) } }, states, JSON.parse(JSON.stringify(adm1Ind)))], ] let bad = 0 @@ -228,12 +254,12 @@ async function main() { const callers = [] const E = armE(countries, states, files['plugin-designs/country-posters/country-poster-wall.vue'], readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H) - const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...E.fails, ...armF({ wall: files['plugin-designs/country-posters/country-poster-wall.vue'], regions: REG })] + const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...E.fails, ...armD(states, areas, meta), ...armF({ wall: files['plugin-designs/country-posters/country-poster-wall.vue'], regions: REG })] const missing = [...KNOWN_CALLERS, `${STATE_CALLER}#state`].filter((k) => !callers.includes(k)) if (missing.length) fails.push(`known callers not found calling makeCountrySheet: ${missing.join(', ')}`) if (fails.length) { fails.forEach((f) => console.log(`FAIL ${f}`)); process.exitCode = 1; return } const ind = seriesFor(countries, countries.find((c) => c.iso3 === 'IND')) - console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists}); ${H.statesWithPosters(states).length} state posters, series 1..${H.statesWithPosters(states).length}, every outline found; sections ${E.order.slice(0, 3).map(([g, t]) => `${g} ${t}`).join(' > ')}`) + console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists}); ${H.statesWithPosters(states).length} state posters, series 1..${H.statesWithPosters(states).length}, every outline found; sections ${E.order.slice(0, 3).map(([g, t]) => `${g} ${t}`).join(' > ')}; state names in ${NAME_LOCALES.length} locales x ${states.length}`) } main().catch((e) => { console.error(`FAIL ${e.stack || e}`); process.exitCode = 1 }) diff --git a/plugin-designs/country-posters/scripts/emit-india-state-names.mjs b/plugin-designs/country-posters/scripts/emit-india-state-names.mjs new file mode 100644 index 000000000..ab99a64c2 --- /dev/null +++ b/plugin-designs/country-posters/scripts/emit-india-state-names.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +// emit-india-state-names.mjs — India's 36 states and union territories named in the 11 UI locales, for the Country +// Posters wall's India section: the CARD labels, the print HUD and the packet/list titles. The printed sheet stays +// English by recorded decision (series.json english_only_note), like every country poster. +// +// SOURCE: Wikidata labels, matched on ISO 3166-2 (P300) against data/people-groups/india-states.json. Wikidata's +// structured data is CC0. The labels are human-edited, not machine translation. Dissolved items (P576) are excluded, +// so a retired code such as the pre-2020 Dadra and Nagar Haveli cannot win. +// English is the TABLE's own name, so the English wall does not change; Wikidata's English is only a cross-check. +// A locale Wikidata has no label for falls back to the English name AND is recorded in the meta file, never silently. +// +// Writes: library/i18n/locales//countryPosters.json -> "stateNames": { "IN-GJ": "…", … } (other keys kept) +// plugin-designs/country-posters/data/india-state-names.meta.json (source, licence, query, date, gaps) +// Regenerate: node plugin-designs/country-posters/scripts/emit-india-state-names.mjs +import { readFileSync, writeFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..') +const LOCALES = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh'] +const WD_LANG = { zh: ['zh-hans', 'zh-cn', 'zh'] } // the zh catalogue is Simplified +const wdLangs = (l) => WD_LANG[l] || [l] + +// The state table keeps geoBoundaries' ISO codes; ISO 3166-2:IN has since renamed four, and Wikidata's P300 holds +// the CURRENT code. Queried under the current code, keyed back to the table's. +const CODE_ALIAS = { 'IN-CT': 'IN-CG', 'IN-OR': 'IN-OD', 'IN-TG': 'IN-TS', 'IN-UT': 'IN-UK' } +const wdCode = (iso) => CODE_ALIAS[iso] || iso + +// REVIEWED OVERRIDES (hi, ar, zh read by eye, 2026-09-30), each with its evidence. Kept small and explicit: neither +// Wikidata's labels nor its Wikipedia titles are uniformly current (zh's Odisha TITLE is the old Orissa while its +// label is current; ar's Odisha LABEL is the old Orissa while its title is current), so no blanket rule is safe. +const OVERRIDES = { + 'IN-OR': { ar: ['أديشة', "the label is the pre-2011 name Orissa (أريسة); ar.wikipedia's title for the item is أديشة (Odisha)"] }, + 'IN-DH': { zh: ['达德拉-纳加尔哈维利和达曼-第乌', 'Wikidata has only a Traditional-script zh label; converted character by character to the Simplified catalogue script'] }, +} + +const states = JSON.parse(readFileSync(join(ROOT, 'data/people-groups/india-states.json'), 'utf8')).states +const isos = states.map((s) => wdCode(s.iso)).sort() +const langs = [...new Set(LOCALES.flatMap(wdLangs))] +const query = `SELECT ?code ?lang ?label WHERE { + VALUES ?code { ${isos.map((c) => `"${c}"`).join(' ')} } + ?item wdt:P300 ?code . + FILTER NOT EXISTS { ?item wdt:P576 ?dissolved } + ?item rdfs:label ?label . BIND(LANG(?label) AS ?lang) + FILTER(?lang IN (${langs.map((l) => `"${l}"`).join(', ')})) +}` +const res = await fetch('https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent(query), { + headers: { accept: 'application/sparql-results+json', 'user-agent': 'dx-maps country-posters emit-india-state-names' }, + signal: AbortSignal.timeout(60000), +}) +if (!res.ok) { console.error(`Wikidata ${res.status}`); process.exit(1) } +const rows = (await res.json()).results.bindings +const got = {} // iso -> Wikidata language -> Set(labels) +for (const r of rows) ((got[r.code.value] ||= {})[r.lang.value] ||= new Set()).add(r.label.value) + +// FALLBACK for a language the matched item has no label in (e.g. the "National Capital Territory of Delhi" item): +// the labels of items in India (P17 = Q668) whose English label IS the table's name (the city of Delhi, +// Chandigarh). Used only for the missing languages, and recorded. +async function sameNameLabels(name) { + const q = `SELECT ?lang ?label WHERE { ?item rdfs:label ${JSON.stringify(name)}@en ; wdt:P17 wd:Q668 . + ?item rdfs:label ?label . BIND(LANG(?label) AS ?lang) FILTER(?lang IN (${langs.map((l) => `"${l}"`).join(', ')})) }` + const r = await fetch('https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent(q), { + headers: { accept: 'application/sparql-results+json', 'user-agent': 'dx-maps country-posters emit-india-state-names' }, + signal: AbortSignal.timeout(60000), + }) + if (!r.ok) throw new Error(`Wikidata ${r.status}`) + const by = {} + for (const b of (await r.json()).results.bindings) { + const m = (by[b.lang.value] ||= new Map()); m.set(b.label.value, (m.get(b.label.value) || 0) + 1) + } + return by // lang -> Map(label -> how many same-named items carry it) +} +const fallbacks = [], overridden = [], unreviewedZh = [] + +const gaps = [], ambiguous = [], englishMismatches = [] +const names = Object.fromEntries(LOCALES.map((l) => [l, {}])) +for (const st of [...states].sort((a, b) => a.iso.localeCompare(b.iso))) { + const wd = got[wdCode(st.iso)] || {} + if (!Object.keys(wd).length) gaps.push(`${st.iso}: no Wikidata item (queried as ${wdCode(st.iso)})`) + let same = null + const wdEn = wd.en && [...wd.en][0] + // The table names the division more briefly than its item ("Delhi" vs "National Capital Territory of Delhi"): then + // the SAME-NAMED item's labels (the city of Delhi) are preferred in every language, so hi and ru match the English. + const preferSame = Boolean(wdEn && wdEn !== st.name) + if (preferSame) englishMismatches.push(`${st.iso}: table "${st.name}" vs Wikidata "${wdEn}"; same-named labels preferred`) + for (const l of LOCALES) { + if (l === 'en') { names.en[st.iso] = st.name; continue } + const ov = OVERRIDES[st.iso] && OVERRIDES[st.iso][l] + if (ov) { names[l][st.iso] = ov[0]; overridden.push(`${st.iso} ${l}: "${ov[0]}" (${ov[1]})`); continue } + if (preferSame) { + same ||= await sameNameLabels(st.name) + const m = wdLangs(l).map((w) => same[w]).find((x) => x && x.size) + if (m) { const [label] = [...m].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]; names[l][st.iso] = label; fallbacks.push(`${st.iso} ${l}: "${label}" from the same-named item (preferred)`); continue } + } + const src = wdLangs(l).find((w) => wd[w] && wd[w].size) + if (l === 'zh' && src === 'zh') unreviewedZh.push(`${st.iso}: only a plain "zh" label (script unknown: may be Traditional); add a reviewed OVERRIDE`) + const hit = src && wd[src] + if (!hit) { + same ||= await sameNameLabels(st.name) + const m = wdLangs(l).map((w) => same[w]).find((x) => x && x.size) + if (m) { + const [label, n] = [...m].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0] + names[l][st.iso] = label; fallbacks.push(`${st.iso} ${l}: "${label}" from the same-named item (${n} of ${[...m.values()].reduce((a, x) => a + x, 0)})`) + continue + } + names[l][st.iso] = st.name; gaps.push(`${st.iso} ${l}: no label, English used`); continue + } + if (hit.size > 1) ambiguous.push(`${st.iso} ${l}: ${[...hit].join(' | ')}`) + names[l][st.iso] = [...hit].sort()[0] + } +} +for (const l of LOCALES) { + const p = join(ROOT, `library/i18n/locales/${l}/countryPosters.json`) + const area = JSON.parse(readFileSync(p, 'utf8')) + area.stateNames = names[l] + writeFileSync(p, JSON.stringify(area, null, 2) + '\n') +} +const meta = { + what: "India's 36 states and union territories, named in the 11 UI locales, for the Country Posters wall's card labels.", + source: 'Wikidata (https://www.wikidata.org): labels of the items whose ISO 3166-2 code (P300) matches data/people-groups/india-states.json; dissolved items (P576) excluded.', + licence: 'CC0 1.0: Wikidata structured data is released under CC0 (https://creativecommons.org/publicdomain/zero/1.0/).', + english: "the state table's own name (the English wall is unchanged); Wikidata's English is only a cross-check", + retrieved: new Date().toISOString().slice(0, 10), + regenerate: 'node plugin-designs/country-posters/scripts/emit-india-state-names.mjs', + query, + codeAliases: CODE_ALIAS, + reviewedOverrides: overridden, + gaps, ambiguous, fallbacks, englishMismatches, +} +writeFileSync(join(ROOT, 'plugin-designs/country-posters/data/india-state-names.meta.json'), JSON.stringify(meta, null, 2) + '\n') +console.log(`states ${states.length}, Wikidata rows ${rows.length}; gaps ${gaps.length}, ambiguous ${ambiguous.length}, fallbacks ${fallbacks.length}, English mismatches ${englishMismatches.length}`) +for (const x of [...gaps, ...ambiguous, ...fallbacks, ...overridden, ...englishMismatches]) console.log(' ' + x) +// A zh name from a plain "zh" label may be Traditional script in a Simplified catalogue: refuse rather than ship it. +if (unreviewedZh.length) { unreviewedZh.forEach((x) => console.error('FAIL ' + x)); process.exitCode = 1 } From 9eb31ad10c011422c15c2dea8de0c0244509d984 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Thu, 1 Oct 2026 11:45:35 +0000 Subject: [PATCH 4/5] Rebuild country-posters, my-upg-100-list, poster-design and regions-dashboard (and the served poster-render copy) for the whole-India sheet --- .../country-posters/countries-map/index.html | 2 +- .../country-poster-wall/index.html | 2 +- .../country-posters-mfe/index.html | 2 +- .../country-posters/country-posters.js | 342 +++++++++--------- public/dx-maps/doxa-maps-spa/manifest.json | 12 +- public/dx-maps/manifest.json | 12 +- 6 files changed, 186 insertions(+), 186 deletions(-) diff --git a/public/dx-maps/country-posters/countries-map/index.html b/public/dx-maps/country-posters/countries-map/index.html index b183a9257..c729183f8 100644 --- a/public/dx-maps/country-posters/countries-map/index.html +++ b/public/dx-maps/country-posters/countries-map/index.html @@ -76,7 +76,7 @@ - + - + - + \n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -272,7 +272,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -288,8 +288,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index 200e12ca4..7842b1e1c 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -232,7 +232,7 @@ "bundle": "country-posters", "scriptUrl": "./country-posters/country-posters.js", "output": "dx-maps/country-posters/country-posters.js", - "integrity": "sha384-JqSefnts831RzaweJfarusgMAjiNHfTc0/PblNFpxfS35w81p/ajCCZe64nOqaVa", + "integrity": "sha384-bSqI3EcCVRKcElgNWMyXtbwVN5eivvXffyFHR3oyjxRxbFKt2Z5sSwn2EFhYvJ7r", "elements": [ "country-posters-map" ], @@ -255,8 +255,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -272,7 +272,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -288,8 +288,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] From 9ed868052cde8176475255daa6b3162c6b2aa8f0 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Thu, 1 Oct 2026 12:08:35 +0000 Subject: [PATCH 5/5] Rebuild country-posters, my-upg-100-list, poster-design and regions-dashboard on the merged tree --- .../country-posters/countries-map/index.html | 2 +- .../country-poster-wall/index.html | 2 +- .../country-posters-mfe/index.html | 2 +- .../country-posters/country-posters.js | 346 +++++++++--------- public/dx-maps/doxa-maps-spa/manifest.json | 12 +- public/dx-maps/manifest.json | 12 +- 6 files changed, 188 insertions(+), 188 deletions(-) diff --git a/public/dx-maps/country-posters/countries-map/index.html b/public/dx-maps/country-posters/countries-map/index.html index aa669153e..8c14eaa1d 100644 --- a/public/dx-maps/country-posters/countries-map/index.html +++ b/public/dx-maps/country-posters/countries-map/index.html @@ -76,7 +76,7 @@ - + - + - + \n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -272,7 +272,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -288,8 +288,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index e23fd2291..31c8794f1 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -232,7 +232,7 @@ "bundle": "country-posters", "scriptUrl": "./country-posters/country-posters.js", "output": "dx-maps/country-posters/country-posters.js", - "integrity": "sha384-hBvhBzd8aj06KvGJ4sAAawHJ6EDyJvyLaeJn8xNj70L20Go9QUpZihm9gKHWJDGm", + "integrity": "sha384-h2oLxe0tebx5EhTvdA3ImH7HWCsdsuzslh73eeatGLLpUuHN8cUIvairSNaXk9x2", "elements": [ "country-posters-map" ], @@ -255,8 +255,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -272,7 +272,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -288,8 +288,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ]