Stop claiming Scripture in a language recorded as "Undetermined"

Reading the list profile's own screenshot turned up a row for "Undetermined (und)" —
14 people groups — offering two Open Bible Stories links. ISO 639-3 reserves und, mul,
mis, zxx and qaa-qtz for exactly the case where the language is NOT known, so a resource
attached to one is attached to nothing. Telling 14 groups there is Scripture in their
language when the record says the language is undetermined is the single worst thing this
map can do, and it took looking at the page to see it. Door43 publishes 4 entries under
und; 16 pins sit on a special code. All are now excluded at the index.

COVERAGE GOES DOWN, 8,935 -> 8,921 pins with a real URL. That is the right direction: the
14 were false.

The same screenshot showed Dogri rendering "Open Bible Stories  Open Bible Stories" —
two links a reader cannot tell apart. 40 languages carry more than one entry under the
same subject, because a language can have more than one independent translation effort.
The owner now disambiguates, and ONLY where it is needed: Dogri reads "Open Bible Stories
(translationCore-Create-BCS)" and "(OBS-TLF)", while a language with one entry is
unchanged.

The list profile also gained the pool it predated. It bundles its own resources.json at
build time, so Door43 never reached it: 49 languages, 63 links. Checked before writing —
120 of its 862 languages already carry a scripture link and those are exactly the 120 the
enriched index has, so nothing there needed correcting, and its curated 862-language scope
is this profile's subject rather than staleness. The enrichment is idempotent: it drops
what a previous run added before re-adding, or a stale title would survive the index
learning to disambiguate.

NOT DONE, DELIBERATELY: the 506 zero-reach rows still never match a COUNTRY filter.
Arclight carries a primaryCountryId that could attribute them, but the only alpha-2 to
alpha-3 mapping in this repo disagrees with itself on 18 countries and cannot resolve 51
of the 168 codes. A wrong attribution would point a real link at the wrong field, so the
behaviour is documented in the table meta instead of guessed at.

Verified: und/mul/mis/zxx absent from the index, the list renders 49 Door43 anchors where
it rendered none, three sampled new links answer 200, and pins, table, deep-link, country
and sign-language probes all still pass.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 05:22:59 +00:00
parent 694f3859d6
commit 117467f7e7
23 changed files with 1551 additions and 33889 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -68,7 +68,7 @@
},
{
"name": "gospel-resource-links",
"rows": 3549
"rows": 3548
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -42,6 +42,19 @@ const D43 = resolve(REPO, 'data/tables/door43-index.json')
const EBIBLE = resolve(REPO, 'data/tables/ebible-index.json')
const ARC = resolve(REPO, 'data/tables/arclight-index.json')
// Two entries with the same subject rendered as "Open Bible Stories Open Bible Stories"
// — two links a reader cannot tell apart. 40 languages are in that state, because a
// language can carry more than one independent translation effort. The OWNER is what
// distinguishes them, so it is appended only where it is needed to disambiguate.
function labelD43(items) {
const bySubject = {}
for (const i of items) (bySubject[i.subject] ||= []).push(i)
return items.map((i) => ({
s: bySubject[i.subject].length > 1 && i.owner ? `${i.subject} (${i.owner})` : i.subject,
u: i.url,
}))
}
const rowsOf = (p) => {
const d = JSON.parse(readFileSync(p, 'utf8'))
if (Array.isArray(d)) return d
@ -81,7 +94,7 @@ for (const r of res) {
codes[c] = {
// Published Door43 resources, each a real browsable URL. `subject` travels with the
// link so the UI can say "Stories" rather than implying a Bible translation.
...(dd && dd.items.length ? { d: dd.items.map((i) => ({ s: i.subject, u: i.url })) } : {}),
...(dd && dd.items.length ? { d: labelD43(dd.items) } : {}),
l: r.language || '',
...((s0 || ebUrl) && { s: s0 || ebUrl }),
...(!s0 && ebUrl ? { sv: ee.t || '', sf: ee.full ? 1 : 0 } : {}),
@ -108,7 +121,7 @@ for (const [c, dd] of Object.entries(d43)) {
if (codes[c] || !dd.items?.length) continue
codes[c] = {
l: dd.title || '',
d: dd.items.map((i) => ({ s: i.subject, u: i.url })),
d: labelD43(dd.items),
ck: 'door43',
un: 'ebible, arclight, grn, api_bible, dbl, progress_bible',
}

View file

@ -101,6 +101,7 @@ const doc = {
meta: {
title: 'Gospel Resources — every link, and who it reaches',
grain: 'one row per language x resource; reach columns aggregate the people groups speaking that language',
country_filter_scope: 'A COUNTRY filter matches on the countries where a people group on this map speaks the language. The 506 zero-reach rows therefore never match one — no group here names those languages — so filter by language or ISO code to find them. They are not attributed to a country on the basis of a "primary country" field, because the only alpha-2/alpha-3 mapping available in this repo disagrees with itself on 18 countries and cannot resolve 51 of the 168 codes; a wrong country attribution would point a real link at the wrong field.',
zero_reach_rows: 'A row with 0 people groups is a REAL published resource in a language that no people group on this map names as its primary language. It is listed because the table is every link, not every link that happens to have a pin — someone searching for that language still needs to find it.',
the_finding: `${out.length} real, openable resources across ${langCount} languages, together reaching ${reachedGroups.toLocaleString()} people groups on the map. Every URL here was copied from a checked pool, never constructed.`,
pools_caveat: 'Pools checked: eBible.org, JESUS Film (Arclight), Door43/unfoldingWord. GRN audio, api.bible, DBL and Progress Bible are NOT checked — an absence here is absence from these pools, never proof that nothing exists.',

View file

@ -0,0 +1,57 @@
#!/usr/bin/env node
/**
* enrich-resources-list.mjs — add the pools resources.json predates to the list profile.
*
* resources-list.vue BUNDLES ./data/resources.json at build time, so it did not benefit
* from the Door43 and catalogue passes that reached the map. Checked before writing, and
* the gap is narrower than it looks: 120 of its 862 languages already carry a scripture
* link in resources[], and those are exactly the 120 the enriched index has — nothing to
* correct there. What is genuinely missing is Door43: 49 of these languages have a
* published Open Bible Stories or Bible repo that the list never offered.
*
* SCOPE IS DELIBERATE AND LEFT ALONE. resources.json covers the curated 862-language DX
* set, not all 2,684 languages the map now knows. That is this profile's subject, not a
* staleness bug, so this script ADDS links to existing rows and never adds rows.
*
* linksFor() in the profile reads resources[] and film_url and guards on the URL itself,
* so an entry appended here renders as a real anchor with no UI change. Titles say what
* the resource IS — "Open Bible Stories" is 50 illustrated narratives, never "Scripture".
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '../../..')
const LIST = resolve(HERE, '../data/resources.json')
const PINS = resolve(REPO, 'data/tables/people-group-pins.json')
const doc = JSON.parse(readFileSync(LIST, 'utf8'))
const { codes } = JSON.parse(readFileSync(PINS, 'utf8'))
let addedD43 = 0, addedFilm = 0, touched = 0
for (const l of doc.languages || []) {
const c = codes[l.iso639_3]
if (!c) continue
let changed = false
// Idempotent: drop what a previous run of THIS script added, then re-add from the
// current index. Appending only by URL would leave a stale title in place after the
// index learns to disambiguate two same-named entries.
if (Array.isArray(l.resources)) {
const before = l.resources.length
l.resources = l.resources.filter((r) => r.source !== 'Door43')
if (l.resources.length !== before) changed = true
}
for (const d of c.d || []) {
const list = (l.resources ||= [])
if (list.some((r) => r.url === d.u)) continue
list.push({ url: d.u, title: d.s, source: 'Door43' })
addedD43++; changed = true
}
// Guard on the URL, matching linksFor's own rule: a flag without a URL would render an
// anchor to nowhere.
if (!l.film_url && c.f) { l.film_url = c.f; addedFilm++; changed = true }
if (changed) touched++
}
doc.stats = { ...(doc.stats || {}), enriched: new Date().toISOString().slice(0, 10), door43_links_added: addedD43, film_links_added: addedFilm }
writeFileSync(LIST, JSON.stringify(doc))
console.log(`languages touched ${touched} door43 links +${addedD43} film links +${addedFilm}`)

View file

@ -34,6 +34,15 @@ const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Geck
const SUBJECTS = ['Open Bible Stories', 'Bible', 'Aligned Bible']
const API = 'https://git.door43.org/api/v1/catalog/search'
// ISO 639-3 RESERVES these: und = undetermined, mul = multiple, mis = uncoded, zxx =
// no linguistic content, and qaa-qtz are private-use. They are not languages, so a
// resource attached to one is attached to nothing. MEASURED: Door43 publishes 4 entries
// under `und`, and 16 pins sit on a special code — the list profile was showing 14
// people groups an "Open Bible Stories" link for a language literally recorded as
// undetermined. That is a false claim about what exists in someone's language.
const isSpecialCode = (c) => c === 'und' || c === 'mul' || c === 'mis' || c === 'zxx' ||
(c >= 'qaa' && c <= 'qtz')
const index = {}
const sources = []
for (const subject of SUBJECTS) {
@ -44,12 +53,15 @@ for (const subject of SUBJECTS) {
sources.push({ subject, url, entries: data.length })
for (const r of data) {
const code = String(r.language || '').split('-')[0]
if (code.length !== 3) continue
if (code.length !== 3 || isSpecialCode(code)) continue
const href = r.repo?.html_url
if (!href) continue
const cur = index[code] || (index[code] = { title: r.language_title || '', items: [] })
if (!cur.items.some((i) => i.url === href)) {
cur.items.push({ subject, url: href, name: r.title || subject })
// The owner disambiguates: 40 languages carry more than one entry with the SAME
// subject, which rendered as "Open Bible Stories Open Bible Stories" — two links a
// reader cannot tell apart. Different owners are different translation efforts.
cur.items.push({ subject, url: href, name: r.title || subject, owner: r.owner || '' })
}
}
console.log(`${subject}: ${data.length} entries`)

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -68,7 +68,7 @@
},
{
"name": "gospel-resource-links",
"rows": 3549
"rows": 3548
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -556,7 +556,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz",
"integrity": "sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU",
"elements": [
"gospel-resources-map"
],
@ -572,7 +572,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -586,7 +586,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -600,7 +600,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -614,7 +614,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-wufBa8EPt7kABmGAqYBLzKSyPWPDg2AkLBRW7EL+Y/l0SLEjSCRuKpnKEYmd2tKz\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-V9sbj60QHWmwyU/6vHQ+uhFjyWFv5wsb1pgfPpiMezRB1DxxrMH6H5JSD4AXWGpU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]