Gospel x Internet: translation readiness, from open data

For an AI or Bible-translation team choosing where a first New Testament
draft is feasible: per language, five named open signals, each cited and
licensed: Glottolog's most extensive description (CC BY 4.0), Scripture
books found (eBible), a language Wikipedia (CC BY-SA 4.0), Common Voice
validated speech hours (CC0) and FLORES+ (CC BY-SA 4.0). A dated snapshot
the build reads; no live fetch. The board shows 'Open data (of 5)', a
sortable column (with the signals in the CSV), a drawer section with each
signal and its source, and a line on the tree card. It is a count of named
facts, not a score. Codes that could not be mapped are listed (6 Wikipedia
codes), and the 31 languages Glottolog lacks read as unknown, never none.
Of the 736 languages with no New Testament found: 20 have 3+ signals.
This commit is contained in:
Doxa Maps 2026-09-27 17:51:54 +00:00
parent 4e818b7442
commit 71f524cd52
22 changed files with 5130 additions and 39 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
#!/usr/bin/env node
/**
* build-translation-readiness.mjs — per roster language, the OPEN data a translation team could start
* from, read from the committed snapshot (fetch-translation-readiness.mjs) and eBible's index. No live
* fetch, no invented score: five named signals, each with its value and source, and the COUNT of the
* five that are present (a count of named facts, not a weighting).
*
* grammar Glottolog MED: the most extensive description (grammar / sketch / text-or-dictionary);
* present when the level is better than "word list or less"
* scripture eBible: canonical books found (partial Scripture counts; a full NT is the board's green)
* wikipedia a language Wikipedia exists (open written text)
* speech Common Voice validated hours > 0 (open speech)
* benchmark the language is in FLORES+ (machine-translation evaluation)
*
* Mapping is explicit (ISO 639-3 <-> Glottocode via the tree snapshot; 639-1 via SIL in the fetcher), and
* the report counts, for the languages with no New Testament, how many could not be mapped to Glottolog
* (their grammar signal is then UNKNOWN, not "none").
* Outputs data/tables/gospel-internet-readiness.json (+ the public copy). `--check` compares.
*/
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 T = (f) => JSON.parse(readFileSync(resolve(REPO, 'data/tables', f), 'utf8'))
const CHECK = process.argv.includes('--check')
const snap = JSON.parse(readFileSync(resolve(REPO, 'data/translation-readiness/snapshot.json'), 'utf8'))
const tree = T('gospel-internet-tree.json')
const gi = T('gospel-internet.json')
const ebible = T('ebible-index.json').codes || {}
const glotto = JSON.parse(readFileSync(resolve(REPO, 'data/glottolog/glottolog-subset.json'), 'utf8'))
const inGlottolog = new Set(glotto.languoids.filter((l) => l.iso).map((l) => l.iso))
const byIso = new Map()
for (const r of gi.rows) {
const e = byIso.get(r.iso639_3) || { iso: r.iso639_3, language: r.language, groups: 0, people: 0 }
e.groups++; e.people += Number(r.population) || 0
byIso.set(r.iso639_3, e)
}
const stOf = new Map(tree.nodes.filter((n) => n.iso && n.st).map((n) => [n.iso, n.st]))
const rows = []
for (const e of byIso.values()) {
const m = snap.med[e.iso]
const eb = ebible[e.iso]
const books = eb ? Number(eb.books) || 0 : 0
const row = {
iso: e.iso, language: e.language, groups: e.groups, people: e.people,
nt: stOf.get(e.iso) === 'g',
grammar_level: m ? m.level : null, // 0 long grammar .. 4 word list or less
grammar: m ? snap.med_codes[m.level] : (inGlottolog.has(e.iso) ? '' : null), // null = not in Glottolog: unknown
scripture_books: books,
wikipedia: snap.wikipedia[e.iso] || '',
speech_hours: snap.commonVoice[e.iso] || 0,
benchmark: (snap.flores[e.iso] || []).join(' '),
}
row.signals = [row.grammar_level !== null && row.grammar_level < 4, books > 0, !!row.wikipedia, row.speech_hours > 0, !!row.benchmark].filter(Boolean).length
rows.push(row)
}
rows.sort((a, b) => a.iso.localeCompare(b.iso))
const noNt = rows.filter((r) => !r.nt)
const count = (f) => noNt.filter(f).length
const coverage = {
languages: rows.length, without_new_testament: noNt.length,
not_in_glottolog: count((r) => r.grammar === null),
signal: { grammar: count((r) => r.grammar_level !== null && r.grammar_level < 4), scripture: count((r) => r.scripture_books > 0),
wikipedia: count((r) => !!r.wikipedia), speech: count((r) => r.speech_hours > 0), benchmark: count((r) => !!r.benchmark) },
by_signal_count: noNt.reduce((m, r) => { m[r.signals] = (m[r.signals] || 0) + 1; return m }, {}),
source_codes_unmapped: snap.unmapped,
}
const doc = {
meta: {
title: 'Translation readiness: the open data each language has, for a team starting a translation',
generated_by: 'plugin-designs/gospel-internet/scripts/build-translation-readiness.mjs',
snapshot: snap.fetched,
sources: { ...snap.sources, ebible: { name: 'eBible.org translation catalogue (books per language)', licence: 'per translation, as eBible lists it' } },
signals: {
grammar: 'Glottolog MED better than "word list or less" (a grammar, sketch, text or dictionary)',
scripture: 'eBible: at least one canonical book found (partial Scripture)',
wikipedia: 'an open language Wikipedia exists',
speech: 'Mozilla Common Voice: validated hours > 0',
benchmark: 'the language is in FLORES+ (MT evaluation)',
},
no_score: 'signals = how many of the five named facts are present. It is a count, not a weighting or a prediction.',
unknown: 'grammar is null where the language is not in Glottolog (IMB private codes): unknown, never "none"',
coverage,
},
rows,
}
const body = JSON.stringify(doc) + '\n'
const OUTS = ['data/tables', 'public/dx-maps/assets/tables'].map((d) => resolve(REPO, d, 'gospel-internet-readiness.json'))
if (CHECK) {
let same = true
for (const o of OUTS) { try { if (readFileSync(o, 'utf8') !== body) same = false } catch { same = false } }
console.log(same ? 'ok readiness table is current' : 'FAIL readiness table is stale: re-run the builder'); if (!same) process.exitCode = 1
} else {
for (const o of OUTS) writeFileSync(o, body)
console.log('ok wrote gospel-internet-readiness.json:', rows.length, 'languages')
}
console.log(JSON.stringify(coverage))

View file

@ -48,7 +48,7 @@ const get = (lc, key) => {
// SETTLED, not deferred: the correct word in that language IS the English spelling. Each is a decision,
// listed by locale and key so a new identical string is still caught.
const SAME_ON_PURPOSE = new Set(['fr:res.source', 'fr:res.type', 'ro:tree.dialect', 'fr:facet.religion',
'de:facet.region', 'de:facet.religion', 'de:tree.poster', 'it:tree.poster', 'ro:tree.poster'])
'de:facet.region', 'de:facet.religion', 'de:tree.poster', 'it:tree.poster', 'ro:tree.poster', 'de:col.openWiki', 'es:col.openWiki', 'it:col.openWiki', 'ro:col.openWiki'])
let fails = 0
if (keys.size < 100) { console.log(`NOT MEASURED: read only ${keys.size} keys (expected 100+); the parser is broken, not the bundle`); process.exitCode = 75 }

View file

@ -141,6 +141,19 @@ for (const n of ix.nodes) if (!n.p) { const x = scZero.get(n.id); gy += x.g + x.
ok(gy === 0, `with only zero-access groups in scope, no family counts a green or yellow language (${gy})`)
bus.clearFilters()
// 11. translation readiness: current against its snapshot, every roster language present, the no-NT set is
// the tree's, the 31 codes Glottolog lacks read as UNKNOWN (null) not "none", and the signal count is a
// recount of the five named facts (no hidden weighting)
let rcur = true
try { execFileSync('node', [resolve(HERE, 'build-translation-readiness.mjs'), '--check'], { stdio: 'pipe' }) } catch { rcur = false }
ok(rcur, 'the readiness table is what its builder makes from the committed snapshot')
const rd = JSON.parse(readFileSync(resolve(REPO, 'data/tables/gospel-internet-readiness.json'), 'utf8'))
ok(rd.rows.length === langsOnBoard.size, `readiness covers every roster language (${rd.rows.length})`)
ok(rd.rows.filter((r) => !r.nt).length === tot.y + tot.r, `its no-New-Testament set is the tree's (${rd.rows.filter((r) => !r.nt).length} == ${tot.y + tot.r})`)
ok(rd.meta.coverage.not_in_glottolog === 31 && rd.rows.filter((r) => r.grammar === null).length === 31, 'the 31 codes Glottolog lacks read as unknown (null), never "none"')
const recount = (r) => [r.grammar_level !== null && r.grammar_level < 4, r.scripture_books > 0, !!r.wikipedia, r.speech_hours > 0, !!r.benchmark].filter(Boolean).length
ok(rd.rows.every((r) => r.signals === recount(r)) && !POISON, 'every signal count is a recount of the five named facts')
// 8. safety: no engagement / who-works-where field in the shipped data
const cols = doc.meta.columns || []
const banned = cols.filter((c) => /engage|worker|team|agenc|missionar/i.test(c))

View file

@ -0,0 +1,102 @@
#!/usr/bin/env node
/**
* fetch-translation-readiness.mjs — a DATED SNAPSHOT of the open language data an AI or Bible-translation
* team could start from, for the languages our people groups speak. Run by hand; the board and its
* builder read ONLY the committed snapshot (no live fetch at runtime). Card BUILD-TRANSLATION-READINESS-VIEW.
*
* Four open sources, each stored with its URL, licence and fetch date:
* Glottolog 5.x "most extensive description" (MED) per languoid CC BY 4.0
* Wikimedia the language Wikipedias (sitematrix), open editions CC BY-SA 4.0 (the text)
* Common Voice validated speech hours per locale (Mozilla) CC0 1.0
* FLORES+ the MT benchmark's languages (OLDI, Hugging Face) CC BY-SA 4.0
* plus SIL's ISO 639-3 code table, used ONLY to map 2-letter codes (ISO 639-1) to ISO 639-3.
* (Partial Scripture comes from eBible's index, already in data/tables, and is read by the builder.)
*
* Codes are mapped EXPLICITLY and every code that cannot be mapped is COUNTED and listed, never dropped
* silently (dx-shepherd's guard 3).
* Writes data/translation-readiness/snapshot.json. Refuses a much smaller snapshot than the last one.
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } 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 OUT = resolve(REPO, 'data/translation-readiness/snapshot.json')
const today = new Date().toISOString().slice(0, 10)
const SRC = {
glottolog: { url: 'https://raw.githubusercontent.com/glottolog/glottolog-cldf/master/cldf/values.csv', licence: 'CC BY 4.0', name: 'Glottolog (CLDF): most extensive description (MED)' },
sil: { url: 'https://iso639-3.sil.org/sites/iso639-3/files/downloads/iso-639-3.tab', licence: 'SIL ISO 639-3 code tables (free to use)', name: 'SIL ISO 639-3 code table' },
wikipedia: { url: 'https://meta.wikimedia.org/w/api.php?action=sitematrix&format=json&smtype=language&smlangprop=code|site', licence: 'CC BY-SA 4.0 (Wikipedia text)', name: 'Wikimedia sitematrix: language Wikipedias' },
commonVoice: { url: 'https://commonvoice.mozilla.org/api/v1/stats/languages', licence: 'CC0 1.0', name: 'Mozilla Common Voice: validated hours per locale' },
flores: { url: 'https://huggingface.co/api/datasets/openlanguagedata/flores_plus/tree/main/dev', licence: 'CC BY-SA 4.0', name: 'FLORES+ (OLDI) MT benchmark languages' },
}
const text = async (u) => { const r = await fetch(u, { headers: { 'User-Agent': 'dx-maps readiness snapshot (open data)' } }); if (!r.ok) throw new Error(`${u} HTTP ${r.status}`); return r.text() }
// our languoids: every roster ISO code and its Glottolog id (from the committed tree snapshot)
const glotto = JSON.parse(readFileSync(resolve(REPO, 'data/glottolog/glottolog-subset.json'), 'utf8'))
const ourIds = new Map(glotto.languoids.filter((l) => l.iso).map((l) => [l.id, l.iso]))
// 1. MED for our languoids only (values.csv is ~20 MB; the snapshot keeps a few hundred rows)
const med = {}
for (const line of (await text(SRC.glottolog.url)).split('\n')) {
if (!line.includes(',med,')) continue
const [, lang, param, value, code] = line.split(',')
if (param === 'med' && ourIds.has(lang)) med[ourIds.get(lang)] = { level: Number(value), code }
}
// 2. SIL: ISO 639-1 -> ISO 639-3, and the set of valid 639-3 codes
const part1 = {}
const valid = new Set()
for (const row of (await text(SRC.sil.url)).split('\n').slice(1)) {
const c = row.split('\t')
if (!c[0]) continue
valid.add(c[0])
if (c[3]) part1[c[3]] = c[0]
}
const toIso3 = (code) => { const b = String(code || '').toLowerCase().split(/[-_]/)[0]; return valid.has(b) ? b : part1[b] || '' }
// 3. Wikipedia editions (open ones), mapped to ISO 639-3
const sm = JSON.parse(await text(SRC.wikipedia.url)).sitematrix || {}
const wikipedia = {}
const wikiUnmapped = []
for (const [k, v] of Object.entries(sm)) {
if (k === 'count' || !v || !v.site) continue
const wp = v.site.find((s) => s.code === 'wiki' && !('closed' in s))
if (!wp) continue
const iso = toIso3(v.code)
if (iso) wikipedia[iso] = v.code; else wikiUnmapped.push(v.code)
}
// 4. Common Voice validated hours, mapped to ISO 639-3
const commonVoice = {}
const cvUnmapped = []
for (const l of JSON.parse(await text(SRC.commonVoice.url))) {
if (!(l.validatedHours > 0)) continue
const iso = toIso3(l.locale)
if (iso) commonVoice[iso] = Math.max(commonVoice[iso] || 0, l.validatedHours); else cvUnmapped.push(l.locale)
}
// 5. FLORES+ languages (lang_Script), ISO 639-3 is the first part
const flores = {}
for (const m of (await text(SRC.flores.url)).matchAll(/dev\/([a-z]{3})_([A-Z][a-z]{3})\.jsonl/g)) (flores[m[1]] ||= []).push(m[2])
const doc = {
fetched: today,
sources: Object.fromEntries(Object.entries(SRC).map(([k, v]) => [k, { ...v, fetched: today }])),
med_codes: { 0: 'long grammar (300+ pages)', 1: 'grammar', 2: 'grammar sketch', 3: 'text, phonology, dictionary or a New Testament', 4: 'word list or less' },
med, wikipedia, commonVoice, flores,
unmapped: { wikipedia: wikiUnmapped.sort(), commonVoice: cvUnmapped.sort() },
counts: { med: Object.keys(med).length, wikipedia: Object.keys(wikipedia).length, commonVoice: Object.keys(commonVoice).length, flores: Object.keys(flores).length,
wikipedia_unmapped: wikiUnmapped.length, commonVoice_unmapped: cvUnmapped.length },
}
if (existsSync(OUT)) {
const prev = JSON.parse(readFileSync(OUT, 'utf8'))
for (const k of ['med', 'wikipedia', 'commonVoice', 'flores']) {
if (doc.counts[k] < 0.9 * (prev.counts?.[k] || 0)) { console.error(`REFUSING: ${k} dropped from ${prev.counts[k]} to ${doc.counts[k]} (a bad fetch?)`); process.exit(1) }
}
}
mkdirSync(dirname(OUT), { recursive: true })
writeFileSync(OUT, JSON.stringify(doc, null, 1) + '\n')
console.log('ok', OUT.replace(REPO + '/', ''), JSON.stringify(doc.counts))

View file

@ -34,6 +34,14 @@ export function buildColumns(T) {
{ key: 'internet_band', header: 'internet_band', label: T('gospelInternet.col.band', 'Internet band'), w: 115, show: (r) => band(r.internet_band), swatch: true },
{ key: 'state_internet_women', header: 'state_internet_women_pct', label: T('gospelInternet.col.stateWomen', 'State online: women'), w: 130, num: true, hidden: true, show: (r) => pct(r.state_internet_women) },
{ key: 'state_internet_men', header: 'state_internet_men_pct', label: T('gospelInternet.col.stateMen', 'State online: men'), w: 130, num: true, hidden: true, show: (r) => pct(r.state_internet_men) },
// translation readiness: how many of five named OPEN data signals the language has (a count, not a score)
{ key: 'open_data', header: 'open_data_signals_of_5', label: T('gospelInternet.col.openData', 'Open data (of 5)'), w: 120, num: true,
show: (r) => (r.open_data == null ? '' : r.open_data.toLocaleString()) },
{ key: 'open_grammar', header: 'grammar_glottolog_med', label: T('gospelInternet.col.openGrammar', 'Grammar (Glottolog)'), w: 170, hidden: true },
{ key: 'open_scripture_books', header: 'scripture_books_ebible', label: T('gospelInternet.col.openBooks', 'Scripture books (eBible)'), w: 130, num: true, hidden: true },
{ key: 'open_wikipedia', header: 'wikipedia_edition', label: T('gospelInternet.col.openWiki', 'Wikipedia'), w: 100, hidden: true },
{ key: 'open_speech_hours', header: 'speech_hours_common_voice', label: T('gospelInternet.col.openSpeech', 'Speech hours (Common Voice)'), w: 150, num: true, hidden: true },
{ key: 'open_benchmark', header: 'mt_benchmark_flores', label: T('gospelInternet.col.openBench', 'MT benchmark (FLORES+)'), w: 150, hidden: true },
{ key: 'top_messaging_app', header: 'top_messaging_app_country_ios', label: T('gospelInternet.col.messaging', 'Top messaging app (country, iOS)'), w: 170 },
{ key: 'top_platform', header: 'top_social_platform_country', label: T('gospelInternet.col.platform', 'Top platform (country)'), w: 140 },
{ key: 'mobile_per_100', header: 'mobile_subscriptions_per_100', label: T('gospelInternet.col.mobile', 'Mobile per 100'), w: 110, num: true, hidden: true },

View file

@ -69,6 +69,24 @@ const sections = computed(() => {
plat.push({ label: T('gospelInternet.drawer.messaging', 'Messaging apps (iOS downloads)'), text: T('gospelInternet.charts.notMeasured', 'Not measured'), note: T('gospelInternet.drawer.noFeed', 'This country is not in Apple\'s feed: its messaging apps are unknown here, not absent.') })
}
if (pc && pc.social.length) plat.push({ label: T('gospelInternet.drawer.social', 'Social platforms (web referrals)'), text: pc.social.map((x) => `${x.platform} ${n(x.pct)}%`).join(' · '), note: T('gospelInternet.drawer.platformNote', 'Share of web referrals among tracked social platforms in the country (StatCounter). Messaging apps such as WhatsApp are not measured.') })
// Open data to start a translation from: five named, cited signals (no score). The last line is the
// honesty the data needs: strong open data and no New Testament FOUND often means one exists that the
// sources checked missed.
const rd = board.readiness.value.byIso.get(g.iso639_3)
const openLines = []
const openLinks = []
if (rd) {
openLines.push({ label: T('gospelInternet.drawer.openCount', 'Open data'), text: T('gospelInternet.drawer.openOf', '{n} of 5 signals', { n: n(rd.signals) }) })
openLines.push({ label: T('gospelInternet.col.openGrammar', 'Grammar (Glottolog)'), text: rd.grammar === null ? T('gospelInternet.drawer.notInSource', 'not in this source') : (rd.grammar || '—') })
openLines.push({ label: T('gospelInternet.col.openBooks', 'Scripture books (eBible)'), text: n(rd.scripture_books) || '0' })
openLines.push({ label: T('gospelInternet.col.openSpeech', 'Speech hours (Common Voice)'), text: rd.speech_hours ? n(rd.speech_hours) : '0' })
openLines.push({ label: T('gospelInternet.col.openBench', 'MT benchmark (FLORES+)'), text: rd.benchmark || '—' })
if (rd.wikipedia) openLinks.push({ label: T('gospelInternet.col.openWiki', 'Wikipedia') + ' (' + rd.wikipedia + ')', href: safeHref(`https://${rd.wikipedia}.wikipedia.org/`) })
if (rd.grammar !== null && g.iso639_3) openLinks.push({ label: 'Glottolog', href: safeHref(`https://glottolog.org/glottolog?iso=${g.iso639_3}`) })
}
const openNote = rd && !rd.nt && rd.signals >= 3
? T('gospelInternet.drawer.openCheck', 'Strong open data but no New Testament found: check first, one may exist that the sources checked missed.')
: T('gospelInternet.drawer.openNote', 'Sources: Glottolog (CC BY 4.0), eBible, Wikipedia (CC BY-SA 4.0), Common Voice (CC0), FLORES+ (CC BY-SA 4.0). A count of named facts, not a score.')
return [
{ id: 'own', title: T('gospelInternet.drawer.own', 'In their own language'),
lead: { dot: g.access_detail, text: label(T, 'detail', g.access_detail) },
@ -79,6 +97,7 @@ const sections = computed(() => {
note: wayLines.length ? T('gospelInternet.drawer.wayNote', 'A neighbouring language, not a recommendation: whether a group speaks it is a question for someone on the ground.') : '' },
{ id: 'conn', title: T('gospelInternet.drawer.conn', 'Connectivity'), lines: conn, links: [] },
{ id: 'platforms', title: T('gospelInternet.drawer.platforms', 'Platforms in the country'), lines: plat, links: [] },
{ id: 'open', title: T('gospelInternet.drawer.openTitle', 'Open data to start a translation from'), lines: openLines, links: openLinks.filter((l) => l.href), note: rd ? openNote : '' },
].filter((s) => s.lines.length || s.links.length || s.lead)
})

View file

@ -359,6 +359,7 @@ useShadowStyles(
<span>{{ T('gospelInternet.tree.languagesHere', 'languages here') }}</span>
</div>
<div v-else-if="card.n.st" class="gtr-c"><span class="gtr-dot" :style="{ background: TREE_COLOR[card.n.st] }"></span>{{ T('gospelInternet.tree.st.' + card.n.st, ST_EN[card.n.st]) }}</div>
<div v-if="card.n.iso && board.readiness.value.byIso.get(card.n.iso)" class="gtr-via" data-tree-open>{{ T('gospelInternet.drawer.openCount', 'Open data') }}: {{ T('gospelInternet.drawer.openOf', '{n} of 5 signals', { n: fmt(board.readiness.value.byIso.get(card.n.iso).signals) }) }}</div>
<div v-if="card.n.inh" class="gtr-via">{{ T('gospelInternet.tree.inherited', 'Colour of its language: no source catalogues resources by dialect.') }}</div>
<div v-if="card.n.via" class="gtr-via">{{ T('gospelInternet.tree.via', 'Also on this path:') }} {{ card.n.via.join(' › ') }}</div>
<button v-if="card.selected" type="button" @click="board.setTreeNode('')">{{ T('controls.clear', 'Clear') }}</button>

View file

@ -269,7 +269,7 @@ export function accessByBand(rows, measure = 'groups') {
export function sortRows(rows, { key, dir = 1 } = {}) {
if (!key) return rows
const num = (v) => (v === null || v === '' || v === undefined ? null : Number(v))
const isNum = ['population', 'internet_pct', 'mobile_per_100', 'people_praying', 'state_internet_women', 'state_internet_men'].includes(key)
const isNum = ['population', 'internet_pct', 'mobile_per_100', 'people_praying', 'state_internet_women', 'state_internet_men', 'open_data'].includes(key)
return [...rows].sort((a, b) => {
const x = a[key], y = b[key]
if (isNum) {

View file

@ -27,6 +27,7 @@ export function useBoard() {
const bySlug = shallowRef(new Map())
const meta = shallowRef({})
const countries = shallowRef({}) // iso3 -> { messaging, top_messaging, in_app_feed, social, social_month }
const readiness = shallowRef({ meta: {}, byIso: new Map() }) // translation readiness, per language
const loading = ref(true)
const error = ref('')
const listId = computed(() => resolvePeopleGroupList(profileConfig.value))
@ -56,13 +57,27 @@ export function useBoard() {
loading.value = true
error.value = ''
try {
const url = `${resolveAssetsBase(cfg())}/tables/gospel-internet.json`
const res = await fetch(url)
const base = resolveAssetsBase(cfg())
const [res, rres] = await Promise.all([fetch(`${base}/tables/gospel-internet.json`), fetch(`${base}/tables/gospel-internet-readiness.json`).catch(() => null)])
if (!res.ok) throw new Error(`gospel-internet.json HTTP ${res.status}`)
const doc = await res.json()
// translation readiness is an ADDITION: if it fails to load, the board still works (no column values)
if (rres && rres.ok) {
const rd = await rres.json()
readiness.value = markRaw({ meta: rd.meta || {}, byIso: new Map((rd.rows || []).map((r) => [r.iso, r])) })
}
// The universal `list` prop: the rows of the page's people-group list, and none if it cannot be
// loaded (applyPeopleGroupList fails CLOSED — never DOXA's whole list under another list's name).
const rows = await applyPeopleGroupList(M.normalizeRows(doc), profileConfig.value)
for (const r of rows) {
const x = readiness.value.byIso.get(r.iso639_3)
r.open_data = x ? x.signals : null
r.open_grammar = x && x.grammar ? x.grammar : ''
r.open_scripture_books = x ? x.scripture_books : null
r.open_wikipedia = x ? x.wikipedia : ''
r.open_speech_hours = x ? x.speech_hours : null
r.open_benchmark = x ? x.benchmark : ''
}
meta.value = markRaw(doc.meta || {})
countries.value = markRaw(doc.countries || {})
all.value = markRaw(rows)
@ -188,7 +203,7 @@ export function useBoard() {
}
const board = {
bus, all, bySlug, filtered, sorted, meta, countries, loading, error, listId, listIsDefault,
bus, all, bySlug, filtered, sorted, meta, countries, readiness, loading, error, listId, listIsDefault,
view, sort, openSlug, openRow, profileConfig,
centre, tree, resources, treeNode, setCentre, setTreeNode, loadTree,
load, setView, openGroup, closeGroup, readUrl, groupLink,

File diff suppressed because one or more lines are too long

View file

@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK",
"integrity": "sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV" 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

@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK",
"integrity": "sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-/5R1QCmIz5vgIpbnYsItuTQE5KCYezYAEesD36ijiZaqKmM7AAAR7G5DN7P8k2IK\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-qAcTOFyxfCu60sdF8d6Rbxlnn2RfoKmPiKl0WrqZRJjs+sW7iQXxgtXghS6uDhhV\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{