Merge main (studio landing 10) into go-int/work
The Research Gallery keeps main's source and bundle: per its owner (rega, relaying the Driver) the board appears under Experimental, which the gallery discovers from the manifest with no gallery change.
This commit is contained in:
commit
2b4457e157
148 changed files with 6709 additions and 1237 deletions
|
|
@ -26,6 +26,12 @@ and a change here can break other maps.
|
|||
`import … from '@map/components/ViewSelect.vue'`
|
||||
- **BarChart** — BarChart.vue — THE SHARED HORIZONTAL BAR CHART (hand-rolled SVG, zero dependencies).
|
||||
`import … from '@map/components/charts/BarChart.vue'`
|
||||
- **ProgressBar** — ProgressBar.vue — the DX-life progress bar: a single-value track, or a segmented one with a legend.
|
||||
`import … from '@map/components/charts/ProgressBar.vue'`
|
||||
- **DashboardFacetRail** — DashboardFacetRail (was Sidebar.vue) — RDASH redesign phase 2: the floating @map SemanticTreeLegend is DROPPED
|
||||
`import … from '@map/components/dashboards/DashboardFacetRail.vue'`
|
||||
- **DashboardFilterChips** — DashboardFilterChips — the live filterSet rendered as removable AND-stacked chips
|
||||
`import … from '@map/components/dashboards/DashboardFilterChips.vue'`
|
||||
- **PinHint** — PinHint.vue — the conversion coach-mark (bottom-center of any map).
|
||||
`import … from '@map/components/hints/PinHint.vue'`
|
||||
- **ApproxPinNote** — ApproxPinNote.vue — the ONE wording every map with people-group pins uses (APPROXPIN, 2026-09-26).
|
||||
|
|
@ -84,8 +90,12 @@ and a change here can break other maps.
|
|||
`import … from '@map/components/poster/slots/TitleSlot.vue'`
|
||||
- **QaCorner** — QaCorner.vue — the embeddable QA corner widget (bottom-right of any map).
|
||||
`import … from '@map/components/qa/QaCorner.vue'`
|
||||
- **GoalCard** — GoalCard.vue — a headline figure with a goal line pinned to the bottom of the card.
|
||||
`import … from '@map/components/regions/GoalCard.vue'`
|
||||
- **RegionSectionCard** — RegionSectionCard.vue — one region as a card: a heading, a summary line, a grid of its countries,
|
||||
`import … from '@map/components/regions/RegionSectionCard.vue'`
|
||||
- **StatCard** — StatCard.vue — one figure with a caption, on the DX-life card surface.
|
||||
`import … from '@map/components/regions/StatCard.vue'`
|
||||
- **FilterDropdown** — FilterDropdown.vue — SHARED (@map/components/search/FilterDropdown.vue).
|
||||
`import … from '@map/components/search/FilterDropdown.vue'`
|
||||
- **SearchBar** — SearchBar.vue — SHARED (@map/components/search/SearchBar.vue).
|
||||
|
|
@ -117,6 +127,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/composables/retryableAsyncComponent.js'`
|
||||
- **useAvatarGenerator** — Avatar Generator Composable
|
||||
`import … from '@map/composables/useAvatarGenerator.js'`
|
||||
- **useCardSurface** — useCardSurface.js — the DX-life card surface, as ONE rule set shared by every card component.
|
||||
`import … from '@map/composables/useCardSurface.js'`
|
||||
- **useCountryOutline** — useCountryOutline.js — country boundary OUTLINE overlay on country search.
|
||||
`import … from '@map/composables/useCountryOutline.js'`
|
||||
- **useCsvExport** — useCsvExport.js — ★ SHARED LIBRARY CODE (dashboard-kit landing 3). Two parameters make it
|
||||
|
|
@ -129,6 +141,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/composables/useDocumentFont.js'`
|
||||
- **useEdgeScrollAffordance** — useEdgeScrollAffordance — the gray "you can scroll past this" edge strips.
|
||||
`import … from '@map/composables/useEdgeScrollAffordance.js'`
|
||||
- **useFacetAggregation** — useFacetAggregation.js — count rows by a facet accessor, and roll the tail into one bucket.
|
||||
`import … from '@map/composables/useFacetAggregation.js'`
|
||||
- **useGeocoderSearch** — Shared geocoder→map search logic (kind-switch + Mapbox filter builder + the
|
||||
`import … from '@map/composables/useGeocoderSearch.js'`
|
||||
- **useMapAttribution** — useMapAttribution.js — the data-credit line every map MUST carry.
|
||||
|
|
@ -194,6 +208,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/utils/colocation.js'`
|
||||
- **countries-meta** — countries-meta.js — a PORT of DX-life's config/countries-meta.ts (326 lines), the rollup
|
||||
`import … from '@map/utils/countries-meta.js'`
|
||||
- **countryName** — countryName.js — a country's name in the reader's language.
|
||||
`import … from '@map/utils/countryName.js'`
|
||||
- **embedBase** — embedBase.js — Where THIS bundle's built tree is served from, resolved at runtime.
|
||||
`import … from '@map/utils/embedBase.js'`
|
||||
- **geoUtils** — geoUtils.js — Geographic / GeoJSON Utility Functions
|
||||
|
|
@ -235,6 +251,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/data/country-posters.json'`
|
||||
- **iso-numeric-to-alpha2** — "4":"AF","8":"AL","12":"DZ","24":"AO","32":"AR","36":"AU","40":"AT","50":"BD",
|
||||
`import … from '@map/data/iso-numeric-to-alpha2.json'`
|
||||
- **iso3-to-alpha2** — "_meta": {
|
||||
`import … from '@map/data/iso3-to-alpha2.json'`
|
||||
- **langFamilyByIso** — "aar": "Afro-Asiatic",
|
||||
`import … from '@map/data/langFamilyByIso.json'`
|
||||
- **langFamilyByLanguage** — {"Chamacoco":"Zamucoan","Ninam":"Yanomaman","Yagua":"Yaguan","Muinane":"Witotoan","Finnish":"Uralic","Hill Mar
|
||||
|
|
|
|||
|
|
@ -127,6 +127,10 @@ const CONSUMERS = [
|
|||
]
|
||||
const SOURCES = [
|
||||
'plugin-designs/research-dashboard/src/composables/useDashboardAggregations.js',
|
||||
// kit landing 4c-1 split the generic counting half out of the file above. Its palettes STAYED, so
|
||||
// this gate's subject did not move — but the new module is scanned too, so a private palette
|
||||
// added to kit code in future is caught on the same rule rather than escaping into the library.
|
||||
'library/composables/useFacetAggregation.js',
|
||||
'plugin-designs/research-dashboard/src/composables/useTreeLegendColors.js',
|
||||
]
|
||||
|
||||
|
|
|
|||
72
internal/check-package-json-unique-keys.mjs
Normal file
72
internal/check-package-json-unique-keys.mjs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-package-json-unique-keys.mjs — no object in package.json may carry the same key twice.
|
||||
*
|
||||
* WHY: JSON.parse keeps the LAST of two equal keys and says nothing, so a duplicate silently kills the
|
||||
* first entry. "check:religion-label-not-value" sat twice in //scripts, one description dead, while
|
||||
* check:doc-scripts passed (it reads the parsed object, where only one survives). It was a merge artifact:
|
||||
* the landing tooling unions both sides' package.json additions. Found by regions-plugin; only Bun's parser
|
||||
* warned, while running something unrelated.
|
||||
*
|
||||
* HOW: a small JSON scanner that tracks every object's keys by position, so it sees what JSON.parse
|
||||
* discards. The self-test runs on every invocation (a poisoned sample must fail), so the gate cannot go blind.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = process.env.DOXA_REPO || join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
/** Every duplicated key, as "<path>: <key>". Assumes text that JSON.parse accepts. */
|
||||
export function duplicateKeys(text) {
|
||||
const out = []
|
||||
const stack = [] // { keys:Set, path, isObj, expectKey }
|
||||
let i = 0
|
||||
const readString = () => {
|
||||
let s = ''; i++
|
||||
while (text[i] !== '"') { if (text[i] === '\\') { s += text[i] + text[i + 1]; i += 2 } else s += text[i++] }
|
||||
i++
|
||||
return s
|
||||
}
|
||||
let lastKey = null
|
||||
while (i < text.length) {
|
||||
const c = text[i]
|
||||
const top = stack[stack.length - 1]
|
||||
if (c === '{' || c === '[') {
|
||||
const path = top ? `${top.path}${top.isObj ? '.' + lastKey : '[]'}` : '$'
|
||||
stack.push({ keys: new Set(), path, isObj: c === '{', expectKey: c === '{' })
|
||||
i++
|
||||
} else if (c === '}' || c === ']') { stack.pop(); i++ }
|
||||
else if (c === '"') {
|
||||
const s = readString()
|
||||
if (top && top.isObj && top.expectKey) {
|
||||
if (top.keys.has(s)) out.push(`${top.path}: ${s}`)
|
||||
top.keys.add(s); lastKey = s; top.expectKey = false
|
||||
}
|
||||
} else if (c === ',') { if (top && top.isObj) top.expectKey = true; i++ }
|
||||
else i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Self-test, every run: a duplicate at depth must be caught, and a clean sample must pass.
|
||||
const poisoned = '{"scripts":{"a":"x","b":"y"},"//scripts":{"a":"1","c":"2","a":"3"},"v":["a","a"]}'
|
||||
const selfBad = duplicateKeys(poisoned)
|
||||
if (selfBad.length !== 1 || !selfBad[0].includes('a')) {
|
||||
console.error(`PACKAGE-JSON-UNIQUE-KEYS: self-test FAIL — expected exactly one duplicate in the poisoned sample, got ${JSON.stringify(selfBad)}`)
|
||||
process.exit(3)
|
||||
}
|
||||
if (duplicateKeys('{"a":{"b":1},"c":{"b":2},"d":"\\"b\\""}').length) {
|
||||
console.error('PACKAGE-JSON-UNIQUE-KEYS: self-test FAIL — a clean sample (same key in SIBLING objects, a quoted key in a value) was reported')
|
||||
process.exit(3)
|
||||
}
|
||||
|
||||
const text = readFileSync(join(ROOT, 'package.json'), 'utf8')
|
||||
JSON.parse(text)
|
||||
const dups = duplicateKeys(text)
|
||||
if (dups.length) {
|
||||
console.error(`PACKAGE-JSON-UNIQUE-KEYS: FAIL — ${dups.length} duplicated key(s); JSON.parse silently keeps only the last:`)
|
||||
for (const d of dups) console.error(' ✗ ' + d)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('PACKAGE-JSON-UNIQUE-KEYS: PASS — every object in package.json has unique keys (self-test: a poisoned duplicate is caught).')
|
||||
356
internal/check-setup-order.mjs
Normal file
356
internal/check-setup-order.mjs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-setup-order.mjs — code that RUNS during setup must not be written below what it reads.
|
||||
*
|
||||
* Two arms, one root cause: a line's POSITION in a setup block is not the same as the MOMENT it
|
||||
* runs, and both defects below shipped because the author read the file top-to-bottom and assumed
|
||||
* the runtime would too. Both were found in the Driver's own console, in a gallery tab, on code
|
||||
* that built clean, parsed clean and passed every other gate in this tree.
|
||||
*
|
||||
* ── ARM A: IMMEDIATE-TDZ ──────────────────────────────────────────────────────────────────────
|
||||
* research-map.vue:2427 — `ReferenceError: Cannot access '_lastFitKey' before initialization`,
|
||||
* and the whole map blanked, because setup threw.
|
||||
*
|
||||
* `watch(src, cb, { immediate: true })` runs cb DURING setup, at the line the watch() sits on.
|
||||
* That watcher called applyFitSlugs(), a `function` declaration 350 lines further down — which is
|
||||
* hoisted, so the CALL resolved fine. Its constants were not: `let _lastFitKey` and seven
|
||||
* `const FIT_*` above it are lexical, still in their temporal dead zone, and the first read threw.
|
||||
*
|
||||
* THE HOISTING ASYMMETRY IS THE WHOLE TRAP. A hoisted function is a promise that the NAME
|
||||
* resolves early, not that its BODY can run early. So the dangerous edge is not "does the watcher
|
||||
* read something below it" — that is easy to see — it is "does the watcher CALL something that
|
||||
* reads something below it", one level of indirection away, and invisible at both ends: the
|
||||
* watcher looks like it only touches the store, and the function looks like it only touches
|
||||
* constants that are obviously right there beside it.
|
||||
*
|
||||
* ── ARM B: HOOK-AFTER-AWAIT ───────────────────────────────────────────────────────────────────
|
||||
* DashboardMapPanel.vue:1233 — `onBeforeUnmount is called when there is no active component
|
||||
* instance` (a warning, which is why it survived), and the cleanup silently never ran.
|
||||
*
|
||||
* Every lifecycle registrar closes over Vue's CURRENT INSTANCE, which exists only while setup is
|
||||
* on the stack. `onMounted(async () => { await initializeMap(); ...; onBeforeUnmount(teardown) })`
|
||||
* resumes after a microtask, with no current instance — so the registration is dropped and a
|
||||
* ResizeObserver on document.body plus a window 'resize' listener outlive the component. The
|
||||
* failure mode is the worst kind: nothing throws, the feature works, and the leak only shows up
|
||||
* as a studio that gets slower the more tabs the reader has visited.
|
||||
*
|
||||
* FIX SHAPE, not just "move the line": the teardown cannot live next to the code that creates the
|
||||
* thing, because that code is past the await. Declare the handle at setup scope and tear it down
|
||||
* in one hook registered during setup — which is the pattern the same file already used for
|
||||
* containerRO and attribRO, three lines away from the one that got it wrong.
|
||||
*
|
||||
* Both arms STRIP COMMENTS FIRST. This tree has been bitten five times by a check satisfied by a
|
||||
* comment quoting the code it was looking for, and this file's own header names both symbols.
|
||||
*
|
||||
* USAGE: node internal/check-setup-order.mjs [--poison]
|
||||
* EXIT: 0 = clean, 1 = a defect, 3 = the self-test failed (the gate cannot be trusted).
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { resolve, dirname, relative, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
const POISON = process.argv.includes('--poison')
|
||||
|
||||
const HOOKS = [
|
||||
'onMounted', 'onBeforeMount', 'onBeforeUnmount', 'onUnmounted', 'onUpdated', 'onBeforeUpdate',
|
||||
'onActivated', 'onDeactivated', 'onErrorCaptured', 'onRenderTracked', 'onRenderTriggered',
|
||||
'onServerPrefetch',
|
||||
]
|
||||
const HOOK_RE = new RegExp('\\b(' + HOOKS.join('|') + ')\\s*\\(', 'g')
|
||||
|
||||
/* Blank out comments and string bodies in place, so every index stays where it was. */
|
||||
const blank = (m) => m.replace(/[^\n]/g, ' ')
|
||||
const stripComments = (src) => src
|
||||
.replace(/<!--[\s\S]*?-->/g, blank)
|
||||
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
||||
.replace(/(^|[^:\\'"`/])\/\/[^\n]*/g, (m, p) => p + ' '.repeat(m.length - p.length))
|
||||
/* Blank the TEXT of every literal while keeping `${...}` interpolations, which are code.
|
||||
* One state machine rather than three regexes, because the three quote styles nest: version 2 of
|
||||
* this gate flagged four data-tables.vue files for "ensure() reads `table`" when the only `table`
|
||||
* in ensure() is the word in `new Error(`table not found (${r.status})`)`. Prose in a template
|
||||
* literal is exactly as inert as prose in a comment, and a regex cannot tell them apart. */
|
||||
const stripStrings = (src) => {
|
||||
const out = src.split('')
|
||||
const stack = []
|
||||
let i = 0
|
||||
const blankAt = (n) => { if (src[n] !== '\n') out[n] = ' ' }
|
||||
while (i < src.length) {
|
||||
const c = src[i]
|
||||
const top = stack[stack.length - 1]
|
||||
if (top && (top.kind === 'sq' || top.kind === 'dq')) {
|
||||
if (c === '\\') { blankAt(i); if (i + 1 < src.length) blankAt(i + 1); i += 2; continue }
|
||||
if ((c === "'" && top.kind === 'sq') || (c === '"' && top.kind === 'dq') || c === '\n') { stack.pop(); i++; continue }
|
||||
blankAt(i); i++; continue
|
||||
}
|
||||
if (top && top.kind === 'tpl') {
|
||||
if (c === '\\') { blankAt(i); if (i + 1 < src.length) blankAt(i + 1); i += 2; continue }
|
||||
if (c === '`') { stack.pop(); i++; continue }
|
||||
if (c === '$' && src[i + 1] === '{') { stack.push({ kind: 'expr', depth: 0 }); i += 2; continue }
|
||||
blankAt(i); i++; continue
|
||||
}
|
||||
if (c === "'") { stack.push({ kind: 'sq' }); i++; continue }
|
||||
if (c === '"') { stack.push({ kind: 'dq' }); i++; continue }
|
||||
if (c === '`') { stack.push({ kind: 'tpl' }); i++; continue }
|
||||
if (top && top.kind === 'expr') {
|
||||
if (c === '{') top.depth++
|
||||
else if (c === '}') { if (top.depth === 0) { stack.pop(); i++; continue } top.depth-- }
|
||||
}
|
||||
i++
|
||||
}
|
||||
return out.join('')
|
||||
}
|
||||
|
||||
/* Only the <script> half of an SFC can run at setup time; the template is a different phase. */
|
||||
function scriptOf(src, file) {
|
||||
if (!file.endsWith('.vue')) return src
|
||||
const out = src.replace(/[^\n]/g, ' ').split('')
|
||||
const re = /<script\b[^>]*>([\s\S]*?)<\/script>/g
|
||||
let m
|
||||
while ((m = re.exec(src))) {
|
||||
for (let i = m.index + m[0].indexOf('>') + 1; i < m.index + m[0].length - 9; i++) out[i] = src[i]
|
||||
}
|
||||
return out.join('')
|
||||
}
|
||||
|
||||
/* Brace-match forward from the first `{` at or after `from`. Returns [open, close] or null. */
|
||||
function braceSpan(src, from) {
|
||||
const open = src.indexOf('{', from)
|
||||
if (open < 0) return null
|
||||
let d = 0
|
||||
for (let i = open; i < src.length; i++) {
|
||||
if (src[i] === '{') d++
|
||||
else if (src[i] === '}' && --d === 0) return [open, i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const lineOf = (src, i) => src.slice(0, i).split('\n').length
|
||||
|
||||
const namesInPattern = (pat) => String(pat)
|
||||
.replace(/[{}[\]]/g, ' ')
|
||||
.split(',')
|
||||
.map((s) => s.replace(/:[\s\S]*$/, '').replace(/=[\s\S]*$/, '').replace(/\.\.\./, '').trim())
|
||||
.filter((n) => /^[A-Za-z_$][\w$]*$/.test(n))
|
||||
|
||||
/* Names a span DECLARES for itself: any of these resolves locally, so reading one says nothing
|
||||
* about a binding further down the file. Version 3 of this gate flagged charts.vue for "load()
|
||||
* reads byCountry, declared at line 354" when load() destructures its OWN `byCountry` on its
|
||||
* second line — a read of a shadowing local, 220 lines away from the const it appears to name.
|
||||
* Shadowing is the normal case in this tree (short names, small helpers), not an exotic one. */
|
||||
function localNames(span) {
|
||||
const out = new Set()
|
||||
const add = (pat) => { for (const n of namesInPattern(pat)) out.add(n) }
|
||||
let m
|
||||
const decl = /\b(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\]|[A-Za-z_$][\w$]*)/g
|
||||
while ((m = decl.exec(span))) add(m[1])
|
||||
const params = /\bfunction\s*[A-Za-z_$]?[\w$]*\s*\(([^)]*)\)|\(([^)]*)\)\s*=>|\bcatch\s*\(([^)]*)\)/g
|
||||
while ((m = params.exec(span))) add(m[1] ?? m[2] ?? m[3])
|
||||
const bare = /\b([A-Za-z_$][\w$]*)\s*=>/g
|
||||
while ((m = bare.exec(span))) out.add(m[1])
|
||||
return out
|
||||
}
|
||||
|
||||
/* Identifiers a span READS FROM AN OUTER SCOPE: member names, object keys and anything the span
|
||||
* declares itself are all removed, because none of them is a binding this scope has to resolve. */
|
||||
function identsIn(span) {
|
||||
const local = localNames(span)
|
||||
const read = stripStrings(span)
|
||||
.replace(/\.\s*[A-Za-z_$][\w$]*/g, ' ') // r.slug — a property, not a binding
|
||||
.replace(/\b[A-Za-z_$][\w$]*\s*:/g, ' ') // { padding: … } — a key, not a binding
|
||||
.match(/\b[A-Za-z_$][\w$]*\b/g) || []
|
||||
return new Set(read.filter((n) => !local.has(n)))
|
||||
}
|
||||
|
||||
/* Top-level (column-0) lexical declarations, which are the ones a setup block resolves. `var`
|
||||
* hoists to undefined rather than throwing, so it is a different bug and not this gate's. */
|
||||
function topLevelLexical(src) {
|
||||
const out = []
|
||||
const re = /^(?:const|let)\s+(?:\{([^}]*)\}|\[([^\]]*)\]|([A-Za-z_$][\w$]*))/gm
|
||||
let m
|
||||
while ((m = re.exec(src))) {
|
||||
const names = m[3] ? [m[3]]
|
||||
: String(m[1] || m[2]).split(',').map((s) => s.replace(/:.*$/, '').replace(/=.*$/, '').replace(/\.\.\./, '').trim())
|
||||
for (const n of names) if (/^[A-Za-z_$][\w$]*$/.test(n)) out.push({ name: n, at: m.index })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/* Top-level `function NAME(...) {}` declarations — hoisted, so reachable from an immediate watcher. */
|
||||
function topLevelFunctions(src) {
|
||||
const out = new Map()
|
||||
const re = /^(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/gm
|
||||
let m
|
||||
while ((m = re.exec(src))) {
|
||||
const span = braceSpan(src, m.index)
|
||||
if (span) out.set(m[1], { at: m.index, body: src.slice(span[0], span[1] + 1) })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── ARM A ────────────────────────────────────────────────────────────────────────────────────────
|
||||
function armA(code) {
|
||||
const hits = []
|
||||
const lexical = topLevelLexical(code)
|
||||
const fns = topLevelFunctions(code)
|
||||
const re = /\bimmediate\s*:\s*true\b/g
|
||||
let m
|
||||
while ((m = re.exec(code))) {
|
||||
// Walk back to the watch()/watchEffect() this option belongs to: the nearest one before it.
|
||||
const before = code.slice(0, m.index)
|
||||
const start = Math.max(before.lastIndexOf('watch('), before.lastIndexOf('watchEffect('))
|
||||
if (start < 0) continue
|
||||
const span = code.slice(start, m.index)
|
||||
const reads = identsIn(span)
|
||||
// Declared BELOW the watcher = in its temporal dead zone while the immediate run is on the stack.
|
||||
for (const d of lexical) {
|
||||
if (d.at > start && reads.has(d.name)) {
|
||||
hits.push({ line: lineOf(code, start), why: `immediate watcher reads '${d.name}', declared at line ${lineOf(code, d.at)}` })
|
||||
}
|
||||
}
|
||||
// One level of indirection: a hoisted function it calls, whose own body reads something below.
|
||||
for (const [name, fn] of fns) {
|
||||
if (!reads.has(name)) continue
|
||||
const inner = identsIn(fn.body)
|
||||
for (const d of lexical) {
|
||||
if (d.at > start && inner.has(d.name)) {
|
||||
hits.push({ line: lineOf(code, start), why: `immediate watcher calls ${name}(), which reads '${d.name}', declared at line ${lineOf(code, d.at)}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// ── ARM B ────────────────────────────────────────────────────────────────────────────────────────
|
||||
function armB(code) {
|
||||
const hits = []
|
||||
const re = /\basync\b/g
|
||||
let m
|
||||
while ((m = re.exec(code))) {
|
||||
const span = braceSpan(code, m.index)
|
||||
if (!span) continue
|
||||
const body = code.slice(span[0], span[1] + 1)
|
||||
const firstAwait = body.search(/\bawait\b/)
|
||||
if (firstAwait < 0) continue
|
||||
let h
|
||||
const hre = new RegExp(HOOK_RE.source, 'g')
|
||||
while ((h = hre.exec(body))) {
|
||||
if (h.index > firstAwait) {
|
||||
hits.push({ line: lineOf(code, span[0] + h.index), why: `${h[1]}() is registered after an await, so it never attaches` })
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
const run = (src, file) => {
|
||||
/* Strings are blanked HERE, for both arms. Arm B's first version flagged useMapInstance.js:430 —
|
||||
* where the only `onBeforeUnmount` on the line is inside the text of a console.error advising a
|
||||
* caller to use one. A gate that reads prose as code cries wolf, and a crying gate gets ignored. */
|
||||
const code = stripStrings(stripComments(scriptOf(src, file)))
|
||||
return [...armA(code).map((h) => ({ ...h, arm: 'IMMEDIATE-TDZ' })), ...armB(code).map((h) => ({ ...h, arm: 'HOOK-AFTER-AWAIT' }))]
|
||||
}
|
||||
|
||||
// ── self-test: both defects as they actually shipped, and the fixes, which must stay quiet ───────
|
||||
const PLANTED_A = `<script setup>
|
||||
const mapStore = inject('mapStore')
|
||||
watch(() => cfg?.value?.externalFilters, (raw) => {
|
||||
if ('fitSlugs' in raw) applyFitSlugs(raw.fitSlugs)
|
||||
}, { deep: true, immediate: true })
|
||||
const FIT_MAX_ZOOM = 5.5
|
||||
let _lastFitKey = ''
|
||||
function applyFitSlugs(slugs) {
|
||||
const key = String(slugs)
|
||||
if (key === _lastFitKey) return
|
||||
_lastFitKey = key
|
||||
fit({ maxZoom: FIT_MAX_ZOOM })
|
||||
}
|
||||
</script>`
|
||||
const PLANTED_B = `<script setup>
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
const resync = () => sync(map.value)
|
||||
window.addEventListener('resize', resync)
|
||||
onBeforeUnmount(() => { window.removeEventListener('resize', resync) })
|
||||
})
|
||||
</script>`
|
||||
/* The same two files after the fix, plus the shapes most likely to be mistaken for the defects:
|
||||
* a NON-immediate watcher above its constants (legal — it cannot run until they exist), a hook
|
||||
* registered BEFORE an await, and a comment that names the symbol the arms search for. */
|
||||
const CLEAN = `<script setup>
|
||||
console.error('containerRef.value is null - call initializeMap() inside onMounted()')
|
||||
async function ensure(file) { if (!ok) throw new Error(\`table not found (\${file})\`) }
|
||||
async function load() { const [pins, byCountry] = await all(); tables.value = { pins, byCountry } }
|
||||
watch(profileConfig, load, { immediate: true })
|
||||
const byCountry = computed(() => series(tables.value))
|
||||
watch(current, (t) => t && ensure(t.file), { immediate: true })
|
||||
const table = computed(() => cache.value[current.value?.file] || null)
|
||||
const FIT_MAX_ZOOM = 5.5
|
||||
let _lastFitKey = ''
|
||||
watch(() => cfg?.value?.externalFilters, (raw) => {
|
||||
if ('fitSlugs' in raw) applyFitSlugs(raw.fitSlugs)
|
||||
}, { deep: true, immediate: true })
|
||||
watch(() => store.other, () => { later({ maxZoom: LATE_ZOOM }) })
|
||||
const LATE_ZOOM = 3
|
||||
function applyFitSlugs(slugs) {
|
||||
if (String(slugs) === _lastFitKey) return
|
||||
_lastFitKey = String(slugs)
|
||||
fit({ maxZoom: FIT_MAX_ZOOM, padding: 70 })
|
||||
}
|
||||
let scrollRO = null
|
||||
onBeforeUnmount(() => { if (scrollRO) scrollRO.disconnect() })
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
scrollRO = new ResizeObserver(() => sync(map.value))
|
||||
})
|
||||
</script>`
|
||||
const blindA = run(PLANTED_A, 'x.vue').filter((h) => h.arm === 'IMMEDIATE-TDZ').length === 0
|
||||
const blindB = run(PLANTED_B, 'x.vue').filter((h) => h.arm === 'HOOK-AFTER-AWAIT').length === 0
|
||||
const noisy = run(CLEAN, 'x.vue')
|
||||
if (blindA || blindB || noisy.length) {
|
||||
console.error('SETUP-ORDER: SELF-TEST FAILED — '
|
||||
+ (blindA ? 'arm A is blind to the shipped TDZ. ' : '')
|
||||
+ (blindB ? 'arm B is blind to the shipped late hook. ' : '')
|
||||
+ (noisy.length ? 'false positives on the fixed code: ' + noisy.map((h) => `${h.arm} ${h.why}`).join('; ') : ''))
|
||||
process.exit(3)
|
||||
}
|
||||
if (POISON) { console.log('SETUP-ORDER: self-test passed (both defects caught, both fixes quiet).'); process.exit(0) }
|
||||
|
||||
// ── scan ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
const ROOTS = ['plugin-designs', 'library']
|
||||
const SKIP = /(^|\/)(node_modules|dist|public|\.git|fixtures)(\/|$)/
|
||||
function files(dir, acc = []) {
|
||||
let entries
|
||||
try { entries = readdirSync(dir) } catch (_) { return acc }
|
||||
for (const e of entries) {
|
||||
const p = join(dir, e)
|
||||
if (SKIP.test(relative(REPO, p))) continue
|
||||
let st
|
||||
try { st = statSync(p) } catch (_) { continue }
|
||||
if (st.isDirectory()) files(p, acc)
|
||||
else if (/\.(vue|js|mjs)$/.test(e)) acc.push(p)
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
const found = []
|
||||
for (const root of ROOTS) {
|
||||
for (const f of files(resolve(REPO, root))) {
|
||||
let src
|
||||
try { src = readFileSync(f, 'utf8') } catch (_) { continue }
|
||||
if (!/\bimmediate\s*:\s*true\b|\basync\b/.test(src)) continue
|
||||
for (const h of run(src, f)) found.push({ file: relative(REPO, f), ...h })
|
||||
}
|
||||
}
|
||||
|
||||
if (!found.length) {
|
||||
console.log('SETUP-ORDER: OK — no immediate watcher reads below itself, and no lifecycle hook is registered after an await.')
|
||||
process.exit(0)
|
||||
}
|
||||
console.error(`SETUP-ORDER: ${found.length} defect(s) —`)
|
||||
for (const h of found) console.error(` ${h.arm} ${h.file}:${h.line} ${h.why}`)
|
||||
console.error('\nARM A: move the declaration above the watcher (a hoisted function resolves its NAME early, not its BODY).')
|
||||
console.error('ARM B: declare the handle at setup scope and tear it down in a hook registered BEFORE the first await.')
|
||||
process.exit(1)
|
||||
203
internal/check-shadow-css-parses.mjs
Normal file
203
internal/check-shadow-css-parses.mjs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
#!/usr/bin/env node
|
||||
// check-shadow-css-parses.mjs — `bun run check:shadow-css-parses`. Source-only, headless, no network.
|
||||
//
|
||||
// LINE COMMENTS, NOT A BLOCK COMMENT, and that is not a style preference: this file's own header
|
||||
// quotes the CSS comment delimiters it exists to reason about, so wrapping it in /* */ ended the
|
||||
// comment early and the gate would not even parse. A file about a reader getting confused by comment
|
||||
// delimiters is an apt place to be bitten by one.
|
||||
//
|
||||
// Does the CSS we assemble as JS strings actually PARSE as CSS?
|
||||
//
|
||||
// THE GAP THIS CLOSES WAS MEASURED, not imagined. In REGLIB batch 1 a line-filtered deletion left 9
|
||||
// orphaned declarations and braces at 12 open / 21 close in regions-list.vue -- and check:sfc-parse
|
||||
// PASSED it, because that gate only proves the JS parses, and a broken stylesheet inside a valid
|
||||
// string literal is still a valid string literal. studio confirmed independently that nothing in the
|
||||
// tree validates CSS syntax: "check:legend-logical-insets reads declarations and would not notice
|
||||
// unbalanced braces."
|
||||
//
|
||||
// ★ IT USES THE REAL PARSERS, AND THE THREE BUGS THAT FORCED THAT ARE THE POINT OF THIS COMMENT.
|
||||
// The first version hand-rolled a JS lexer to pull the string literals out, and it was wrong three
|
||||
// times in a row, each time producing a confident FALSE RED against a file that was fine:
|
||||
// 1. it read /* CSS comments */ inside the CSS as stranded declarations -- the reader must speak
|
||||
// the language of the text it reads, and this text is CSS, not JS;
|
||||
// 2. it ran over a whole .vue file, so the apostrophe in GoalCard's prose header ("one caller's
|
||||
// feature") opened a phantom string literal and it reported `</div>` from the TEMPLATE as CSS;
|
||||
// 3. PosterWall.vue:518 has `.replace(/"/g, '"')` -- a REGEX LITERAL CONTAINING A QUOTE, which
|
||||
// no hand-rolled scanner can get right without resolving the regex-vs-division ambiguity.
|
||||
// Three false reds from one shortcut. So the literals now come out of the Babel AST that the build
|
||||
// itself parses: comments, template literals, regexes and apostrophes all stop being my problem.
|
||||
// Only the CSS scan below is hand-written, because CSS is the thing actually being checked.
|
||||
import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parse as sfcParse } from '@vue/compiler-sfc'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
|
||||
// Each copy of this repo grades ITS OWN tree: a hardcoded path once made one copy silently grade
|
||||
// another worktree's files (check:no-abs-paths, 2026-09-24).
|
||||
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
let fails = 0
|
||||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
|
||||
// ── the script block, via the SFC parser the build uses ─────────────────────────────────────────
|
||||
function scriptsOf(path, src) {
|
||||
if (!path.endsWith('.vue')) return [src]
|
||||
const { descriptor } = sfcParse(src, { filename: path })
|
||||
return [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean)
|
||||
}
|
||||
|
||||
// ── every useShadowStyles(<css>, label) argument, folded to a string ────────────────────────────
|
||||
// Returns { css, complete } -- complete:false means part of the argument was an identifier (an
|
||||
// imported constant), so the text is PARTIAL and must not be scanned as if it were the whole sheet.
|
||||
function shadowCssOf(script, filename) {
|
||||
const ast = babelParse(script, { sourceType: 'module', plugins: ['jsx'], errorRecovery: true })
|
||||
const found = []
|
||||
|
||||
// Fold a `'a' + 'b' + IDENT` expression into its literal parts, noting whether any part was not one.
|
||||
function fold(node, acc) {
|
||||
if (!node) return
|
||||
if (node.type === 'StringLiteral') { acc.css += node.value; return }
|
||||
if (node.type === 'TemplateLiteral') {
|
||||
if (node.expressions.length) acc.complete = false
|
||||
acc.css += node.quasis.map((q) => q.value.cooked ?? '').join('')
|
||||
return
|
||||
}
|
||||
if (node.type === 'BinaryExpression' && node.operator === '+') { fold(node.left, acc); fold(node.right, acc); return }
|
||||
acc.complete = false // an identifier, a call, a conditional -- not text we can read here
|
||||
}
|
||||
|
||||
;(function walk(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (Array.isArray(node)) { node.forEach(walk); return }
|
||||
if (node.type === 'CallExpression' && node.callee && node.callee.name === 'useShadowStyles') {
|
||||
const acc = { css: '', complete: true }
|
||||
fold(node.arguments[0], acc)
|
||||
found.push(acc)
|
||||
}
|
||||
for (const k of Object.keys(node)) {
|
||||
if (k === 'loc' || k === 'leadingComments' || k === 'trailingComments') continue
|
||||
walk(node[k])
|
||||
}
|
||||
})(ast.program)
|
||||
return found
|
||||
}
|
||||
|
||||
// ── scan a CSS string ───────────────────────────────────────────────────────────────────────────
|
||||
// CSS comments and quoted values are consumed as their own token types: a /* */ is not a declaration
|
||||
// and a brace inside content:'a{b' does not open a rule.
|
||||
function scanCss(cssRaw) {
|
||||
let depth = 0, chunk = '', open = 0, close = 0, i = 0
|
||||
const orphans = [], emptySelectors = []
|
||||
const n = cssRaw.length
|
||||
while (i < n) {
|
||||
const ch = cssRaw[i]
|
||||
if (ch === '/' && cssRaw[i + 1] === '*') {
|
||||
i += 2
|
||||
while (i < n && !(cssRaw[i] === '*' && cssRaw[i + 1] === '/')) i++
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (ch === "'" || ch === '"') {
|
||||
const q = ch
|
||||
i++
|
||||
while (i < n && cssRaw[i] !== q) { if (cssRaw[i] === '\\') i++; i++ }
|
||||
i++
|
||||
chunk += "''"
|
||||
continue
|
||||
}
|
||||
if (ch === '{') {
|
||||
open++
|
||||
if (depth === 0 && !chunk.trim()) emptySelectors.push('(empty selector before "{")')
|
||||
if (depth === 0 && /;/.test(chunk) && /[A-Za-z-]+\s*:/.test(chunk)) orphans.push(chunk.trim().slice(0, 90))
|
||||
depth++; chunk = ''; i++
|
||||
continue
|
||||
}
|
||||
if (ch === '}') { close++; depth = Math.max(0, depth - 1); chunk = ''; i++; continue }
|
||||
chunk += ch
|
||||
if (depth === 0 && ch === ';' && /[A-Za-z-]+\s*:/.test(chunk)) { orphans.push(chunk.trim().slice(0, 90)); chunk = '' }
|
||||
i++
|
||||
}
|
||||
return { open, close, orphans, emptySelectors }
|
||||
}
|
||||
|
||||
// ── SELF-TESTS: the scanner must be shown to FAIL on the damage it is meant to catch ────────────
|
||||
console.log('SELF-TESTS (a scanner that cannot fail proves nothing)')
|
||||
let r = scanCss('.a{ color:red; } @media (x){ .b{ color:blue; } }')
|
||||
ok(r.open === r.close && !r.orphans.length, `valid CSS scans clean (${r.open}/${r.close} braces, 0 orphans)`)
|
||||
r = scanCss('.a{ color:red; } padding:4px; margin:0; .b{ color:blue; }')
|
||||
ok(r.orphans.length === 2, `NEG CONTROL: two stranded declarations are caught (found ${r.orphans.length}) -- the exact batch-1 damage`)
|
||||
r = scanCss('.a{ color:red; } } }')
|
||||
ok(r.close - r.open === 2, `NEG CONTROL: unbalanced braces are caught (${r.open} open / ${r.close} close)`)
|
||||
r = scanCss(".a{ content:'a{b'; }")
|
||||
ok(r.open === 1 && r.close === 1, `NEG CONTROL: a brace inside a quoted CSS value does not unbalance the count (${r.open}/${r.close})`)
|
||||
r = scanCss('/* padding:4px; a comment that looks like a declaration */ .a{ color:red; }')
|
||||
ok(!r.orphans.length, 'NEG CONTROL: a CSS comment containing "padding:4px;" is not read as a stranded declaration')
|
||||
r = scanCss('.a{ background:url(data:image/svg+xml;base64,AA==); }')
|
||||
ok(r.open === 1 && !r.orphans.length, 'a data: URL with a ; inside url() does not read as an orphan')
|
||||
|
||||
// the three extractor bugs, as regression tests against the files that exposed them
|
||||
{
|
||||
const apostrophe = "<!-- one caller's feature -->\n<script setup>\nuseShadowStyles('.a{ color:red; }', 'x')\n</script>\n<template><div class=\"b\"/></template>"
|
||||
const got = scriptsOf('x.vue', apostrophe).flatMap((s) => shadowCssOf(s, 'x.vue')).map((a) => a.css).join('')
|
||||
ok(got === '.a{ color:red; }', 'NEG CONTROL: an apostrophe in a .vue prose header cannot leak template text into the CSS')
|
||||
}
|
||||
{
|
||||
const regexy = "const esc = (v) => String(v).replace(/\"/g, '"')\nuseShadowStyles('.a{ color:red; }', 'x')"
|
||||
const got = shadowCssOf(regexy, 'x.js').map((a) => a.css).join('')
|
||||
ok(got === '.a{ color:red; }', 'NEG CONTROL: a regex literal containing a quote (PosterWall.vue:518) no longer desynchronises the read')
|
||||
}
|
||||
{
|
||||
const partial = "import { A } from './a.js'\nuseShadowStyles(A + '.b{ color:red; }', 'x')"
|
||||
const got = shadowCssOf(partial, 'x.js')
|
||||
ok(got.length === 1 && got[0].complete === false,
|
||||
'an argument built partly from an imported constant is reported PARTIAL, not scanned as a whole sheet')
|
||||
}
|
||||
|
||||
// ── the tree ────────────────────────────────────────────────────────────────────────────────────
|
||||
const walk = (d) => readdirSync(d).flatMap((f) => {
|
||||
const p = join(d, f)
|
||||
if (statSync(p).isDirectory()) return f === 'node_modules' || f.startsWith('.') ? [] : walk(p)
|
||||
return /\.(vue|js)$/.test(f) ? [p] : []
|
||||
})
|
||||
const rel = (p) => p.slice(REPO.length + 1)
|
||||
const files = [...walk(join(REPO, 'library')), ...walk(join(REPO, 'plugin-designs'))].sort()
|
||||
|
||||
console.log('\nSHADOW CSS, file by file')
|
||||
let measured = 0
|
||||
const partials = []
|
||||
for (const f of files) {
|
||||
const src = readFileSync(f, 'utf8')
|
||||
if (!src.includes('useShadowStyles(')) continue
|
||||
let blocks
|
||||
try { blocks = scriptsOf(f, src).flatMap((s) => shadowCssOf(s, f)) }
|
||||
catch (e) { console.log(` FAIL - ${rel(f)}: could not be parsed (${e.message.slice(0, 70)})`); fails++; continue }
|
||||
for (const { css, complete } of blocks) {
|
||||
if (!/[{}]/.test(css)) { partials.push(f); continue }
|
||||
// ★ A PARTIAL ARGUMENT IS STILL SCANNED, and skipping it was a false green waiting to happen.
|
||||
// region-dashboards.vue builds its sheet as DXLIFE_TOKENS + SHADOW_BOX_SIZING_RESET + literals,
|
||||
// so the first cut of this check classified it PARTIAL and skipped it -- excluding the very file
|
||||
// whose CSS I had just cut rules out of, which is the whole reason the check exists. The imported
|
||||
// constants are complete rule sets, so the literal remainder must balance on its own; if one ever
|
||||
// straddled a rule this would go red and the label below says where to look.
|
||||
measured++
|
||||
const { open, close, orphans, emptySelectors } = scanCss(css)
|
||||
const note = complete ? '' : ' (literal segments only; imported constants scanned in their own module)'
|
||||
if (open !== close || orphans.length || emptySelectors.length) {
|
||||
console.log(` FAIL - ${rel(f)}: ${open} open / ${close} close brace(s)${note}`)
|
||||
for (const o of orphans) console.log(` orphan declaration: ${o}`)
|
||||
for (const e of emptySelectors) console.log(` ${e}`)
|
||||
fails++
|
||||
} else {
|
||||
console.log(` ok - ${rel(f)}: ${open} rule(s), braces balanced, 0 orphans${note}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(measured > 0, `${measured} stylesheet(s) were actually scanned (0 would mean this check measured nothing)`)
|
||||
if (partials.length) {
|
||||
console.log('\n NO CSS LITERALS AT ALL (the argument is entirely an imported constant):')
|
||||
for (const f of [...new Set(partials)]) console.log(` ${rel(f)}`)
|
||||
}
|
||||
|
||||
console.log(fails ? `\nSHADOW-CSS: ${fails} FAIL(s)` : `\nSHADOW-CSS: ${measured} stylesheet(s) parse as CSS -- braces balanced, no stranded declarations`)
|
||||
process.exit(fails ? 1 : 0)
|
||||
133
library/components/charts/ProgressBar.vue
Normal file
133
library/components/charts/ProgressBar.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<!--
|
||||
ProgressBar.vue — the DX-life progress bar: a single-value track, or a segmented one with a legend.
|
||||
|
||||
Ported from blocks/_progress-bar.scss plus RegionDetail.vue's scoped .prayer-coverage-segments
|
||||
override. Three instances in the regions dashboard used it: one segmented bar with a three-key
|
||||
legend, and two plain adopted/engaged bars.
|
||||
|
||||
★ ONE NUMBER FEEDS BOTH THE WIDTH AND THE LEGEND, which is the actual reason this is a component
|
||||
and not two copies of markup. Before extraction the segment widths were computed from raw counts in
|
||||
the template while the legend printed those counts separately, so a bar and the number beside it
|
||||
could disagree and nothing would say so. Here a segment is { value, color, label } and the width
|
||||
IS value/max -- they cannot drift apart, because there is only one figure.
|
||||
|
||||
★ SEGMENT COLOUR IS A VALUE, NOT A CLASS, and this is the ONE substitution in the port. Their
|
||||
markup (and ours, before this) selected colours with .rd-seg--full / --partial / --none, three
|
||||
prayer-specific class names. A shared component cannot know what "full prayer coverage" is, and
|
||||
baking those three names in is exactly the monolith this extraction exists to undo. So the caller
|
||||
passes a CSS colour -- the regions plugin passes the same var(--dx-accent) / var(--dx-partial) /
|
||||
var(--dx-track) tokens those classes set, so every rendered colour is unchanged; only the mechanism
|
||||
moved from a stylesheet rule to an inline background. The three --full/--partial/--none rules are
|
||||
deleted rather than left behind, because a rule no selector can reach is not a fallback.
|
||||
|
||||
THE THREE TEMPLATE BRANCHES, and why there are no comments between them: a comment sitting between
|
||||
v-if branches is a RENDERED NODE, which turns the component's root into a FRAGMENT
|
||||
(<!--[-->...<!--]-->) instead of a single element. Measured by server-rendering it. So the branches
|
||||
are bare and the explanation lives here:
|
||||
• showLegend -> root is the ported .stack.stack--xs wrapper, 10px above the legend
|
||||
• segments only -> root is the segmented .rd-bar
|
||||
• neither -> root IS the plain .rd-bar, exactly as their cards 2 and 3 render it
|
||||
|
||||
DOM IS UNCHANGED FOR ALL THREE INSTANCES, deliberately, because the clone is measured against the
|
||||
live site. With a legend the root is the ported .stack.stack--xs wrapper (10px between bar and
|
||||
legend); without one the root IS the .rd-bar element, exactly as their cards 2 and 3 render it --
|
||||
hence the three template branches rather than one uniform wrapper, which would have added a box
|
||||
DX-life does not have to two of the three cards.
|
||||
|
||||
NOT ADDED HERE, ON PURPOSE: role="progressbar" / aria-valuenow. Neither their bar nor our port has
|
||||
it, and inventing an accessibility tree the clone target does not have is a change to request, not
|
||||
to slip in. Carded for a ruling instead.
|
||||
|
||||
CONTRACT: the containing shadow root carries the DX-life token layer (--dx-track, --dx-accent,
|
||||
--spacing-*). No i18n is installed or required -- every string arrives pre-localized.
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** The filled amount, in the same units as `max`. Ignored when `segments` is non-empty. */
|
||||
value: { type: Number, default: 0 },
|
||||
/** The denominator. A zero or missing max renders an empty bar rather than dividing by zero. */
|
||||
max: { type: Number, default: 100 },
|
||||
/** The single-value fill colour. Any CSS colour; defaults to the DX-life accent. */
|
||||
color: { type: String, default: 'var(--dx-accent)' },
|
||||
/**
|
||||
* [{ value, color, label }] — when non-empty the bar is segmented. `value` is a RAW count over the
|
||||
* same `max`, not a percentage: the component divides, so the legend and the width agree.
|
||||
*/
|
||||
segments: { type: Array, default: () => [] },
|
||||
/** Render the legend built from `segments`. Has no effect without segments. */
|
||||
legend: { type: Boolean, default: false },
|
||||
/** aria-label for the track. Omitted entirely when empty, as their plain bars have none. */
|
||||
label: { type: String, default: '' },
|
||||
/** Formats a segment's value for the legend. The caller owns locale-dependent formatting. */
|
||||
format: { type: Function, default: (n) => String(n) },
|
||||
/** Between a legend key's label and its number. A prop because ':' is not universal punctuation. */
|
||||
separator: { type: String, default: ': ' },
|
||||
})
|
||||
|
||||
// RegionDetail.vue:28-30. Capped at 100 for the same reason theirs is: a bar wider than its
|
||||
// track is a rendering bug, not a data story. Ported verbatim from the plugin's percentOfSubject,
|
||||
// which existed only to serve these bars and is deleted there.
|
||||
const percentOf = (count) => (props.max ? Math.min(100, ((count || 0) / props.max) * 100) : 0)
|
||||
|
||||
const isSegmented = computed(() => props.segments.length > 0)
|
||||
const showLegend = computed(() => props.legend && isSegmented.value)
|
||||
const pct = computed(() => percentOf(props.value))
|
||||
const segStyles = computed(() => props.segments.map((s) => ({
|
||||
width: percentOf(s.value) + '%',
|
||||
background: s.color,
|
||||
})))
|
||||
|
||||
useShadowStyles(
|
||||
// ── .progress-bar: blocks/_progress-bar.scss. The HEIGHT comes from padding-top, not height
|
||||
// -- clamp(15px, 6%, 25px) at [data-size="md"] -- so the track scales with its own width.
|
||||
'.rd-bar{ position:relative; overflow:hidden; width:100%; border-radius:100px;' +
|
||||
' background:var(--dx-track); border:none; padding-top:clamp(15px, 6%, 25px); }' +
|
||||
'.rd-bar__slider{ position:absolute; top:0; height:100%; width:0; border-radius:100px;' +
|
||||
' background:var(--dx-accent); transition:width 1s ease-out; }' +
|
||||
// .prayer-coverage-segments overrides that padding with a real height (their own scoped rule)
|
||||
'.rd-bar--segments{ display:flex; height:clamp(15px, 6cqw, 25px); padding-top:0; }' +
|
||||
'.rd-seg{ height:100%; transition:width 1s ease-out; }' +
|
||||
// The ported .stack.stack--xs that separates the bar from its legend by 10px. It is the root only
|
||||
// when a legend is rendered, so a plain bar keeps their exact single-element shape.
|
||||
'.rd-bar-stack{ display:flex; flex-direction:column; }' +
|
||||
'.rd-bar-stack > * + *{ margin-top:10px; }' +
|
||||
'.rd-legend{ display:grid; justify-content:center; gap:3.5px; font-size:var(--font-size-sm);' +
|
||||
' font-weight:600; text-align:center; }' +
|
||||
'.rd-legend span{ display:inline-flex; align-items:center; justify-content:center; gap:4px; }' +
|
||||
'.rd-key{ display:inline-block; flex:0 0 auto; width:7.5px; height:7.5px; border-radius:100px; }' +
|
||||
// ★ THE COMPONENT CARRIES ITS OWN REDUCED-MOTION GUARD, and it is not redundant. base/_reset.scss
|
||||
// does this with a DOCUMENT-level universal selector, which styles nothing inside a shadow root, so
|
||||
// the regions plugin re-ported it scoped to its own `.rd-root *`. That covers these bars only while
|
||||
// they are inside .rd-root. A shared component used anywhere else would carry the 1s width
|
||||
// transition with nothing suppressing it -- reintroducing the exact regression that was measured
|
||||
// and fixed here (theirs 1s -> 1e-05s under emulated reduce, ours stayed 1s: a vestibular visitor
|
||||
// got a full one-second sliding bar their own site suppresses). A component that ships an animation
|
||||
// owes the guard for it; relying on an ancestor's reset is what makes a component non-portable.
|
||||
'@media (prefers-reduced-motion: reduce){' +
|
||||
' .rd-bar__slider, .rd-seg{ transition-duration:0.01ms !important; } }'
|
||||
, 'progress-bar')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showLegend" class="rd-bar-stack">
|
||||
<div class="rd-bar rd-bar--segments" :aria-label="label || undefined">
|
||||
<div v-for="(s, i) in segStyles" :key="i" class="rd-seg" :style="s" />
|
||||
</div>
|
||||
<div class="rd-legend">
|
||||
<span v-for="(s, i) in segments" :key="i">
|
||||
<i class="rd-key" :style="{ background: s.color }" />{{ s.label }}{{ separator }}{{ format(s.value) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isSegmented" class="rd-bar rd-bar--segments" :aria-label="label || undefined">
|
||||
<div v-for="(s, i) in segStyles" :key="i" class="rd-seg" :style="s" />
|
||||
</div>
|
||||
|
||||
<div v-else class="rd-bar" :aria-label="label || undefined">
|
||||
<div class="rd-bar__slider" :style="{ width: pct + '%', background: color }" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -15,20 +15,41 @@ const L = (key, en) => tStatic('dashboard.' + key) || en
|
|||
* --rd-accent left rail. Reuses aggregate()/lbl() from @map/composables/useDashboardAggregations and
|
||||
* the same per-row facet → bus.setFilter wiring.
|
||||
*/
|
||||
/**
|
||||
* ── KIT LANDING 4c-3: MOVED HERE FROM plugin-designs/research-dashboard/ ──────────────────────
|
||||
* The facet rail follows the chip bar (4c-2). Rendering is unchanged; what changed is where its two
|
||||
* remaining plugin imports point. 4b had already removed the research COLOUR and LABEL tables (they
|
||||
* stay in plugin-designs and arrive on dashboardConfig.legend), which left exactly two:
|
||||
* useDashboardAggregations -> @map/composables/useFacetAggregation.js (extracted by 4c-1)
|
||||
* useDashboardShell -> @map/composables/useDashboardSearch.js (extracted by 4a)
|
||||
* Both repoint at the MODULE that owns the function rather than at a re-export: a library component
|
||||
* reading a plugin's shim is the dependency this move exists to remove, and check:filter-multivalue
|
||||
* already records why it follows the file — left at the plugin path it read the shim, found no
|
||||
* predicate to object to, and PASSED VACUOUSLY.
|
||||
*
|
||||
* Unlike the chip bar, this component IS mounted and IS on screen (research-dashboard.vue), so the
|
||||
* move is verifiable by eyes as well as by gate — and it needs them: it is 770 lines of tree, search
|
||||
* and drag behaviour, and nothing about "the imports resolve" says the rail still opens four trees
|
||||
* by default or that its search still narrows.
|
||||
*/
|
||||
import { inject, ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { aggregate, lbl } from '../../composables/useDashboardAggregations.js'
|
||||
import { buildSearchAccessors, makeSearchMatcher, filterRowsBySearch } from '../../composables/useDashboardShell.js'
|
||||
import { useTreeLegendColors } from '../../composables/useTreeLegendColors.js'
|
||||
import { useTreeLegendLabels } from '../../composables/useTreeLegendLabels.js'
|
||||
import { aggregate, lbl, otherLabel } from '@map/composables/useFacetAggregation.js'
|
||||
import { buildSearchAccessors, makeSearchMatcher, filterRowsBySearch } from '@map/composables/useDashboardSearch.js'
|
||||
// ★ NO RESEARCH COLOUR IMPORTS (kit landing 4b). This component used to import
|
||||
// useTreeLegendColors and useTreeLegendLabels, which are the DOXA palettes and the People code
|
||||
// tables — research-specific, and they stay in plugin-designs. Importing them is what stopped this
|
||||
// file from being able to move into the kit at all. It now declares what it needs and the shell
|
||||
// supplies it on dashboardConfig.legend; see the note beside that provide().
|
||||
|
||||
const dash = inject('dashboardData', null)
|
||||
const bus = inject('selectionBus', null)
|
||||
const cfg = inject('dashboardConfig', {})
|
||||
// DataSourceManager-normalized features (same store the geocoder bars use) — the shape
|
||||
// useAffinityBlockLegendData resolves labels from (affinityBlockLabel / clusterLabel / peopleGroupLabel).
|
||||
const dataStore = inject('dataStore', null)
|
||||
const searchSourceId = inject('searchSourceId', null)
|
||||
// `dataStore` and `searchSourceId` were injected here ONLY to construct useTreeLegendColors and
|
||||
// useTreeLegendLabels, and landing 4b removed both constructions. They are gone rather than left
|
||||
// harmlessly in place: an unused inject is a claim about the host, and a kit component that appears
|
||||
// to need a research dataStore is one a new dashboard will dutifully provide before finding out it
|
||||
// was never read. The chip bar shed the same pair in 4c-2, for the same reason.
|
||||
const filterSet = bus?.filterSet || {}
|
||||
|
||||
// facet-key -> row accessor (sourced from config) — drives both the count tally and setFilter.
|
||||
|
|
@ -134,7 +155,12 @@ function isExpandedRaw(facet, value) {
|
|||
function toRows(agg, facet, level) {
|
||||
const max = agg.reduce((m, d) => Math.max(m, d.value), 0) || 1
|
||||
return agg.map((d) => ({
|
||||
facet, value: d.key, label: d.key, count: d.value, frac: d.value / max, level,
|
||||
// ★ THE BUCKET'S KEY IS A SENTINEL, ITS LABEL IS A TRANSLATION (kit landing 4c-1).
|
||||
// `value` stays d.key — including the English 'Other' — because the row's inertness is tested
|
||||
// against it (`row.other` is set from it below, and the template refuses to make such a row
|
||||
// clickable). Translating the value would make the bucket filterable in Arabic, on a value no
|
||||
// row carries. Only the LABEL is projected.
|
||||
facet, value: d.key, label: d.other ? otherLabel() : d.key, count: d.value, frac: d.value / max, level,
|
||||
other: !!d.other, active: !d.other && bus.isFilterActive(facet, d.key),
|
||||
// Carried per-ROW rather than looked up in the template, so the zero-row baseline snapshot
|
||||
// (which spreads a row object) keeps the mark when a row is re-admitted.
|
||||
|
|
@ -328,13 +354,24 @@ function addTrunks(rows) {
|
|||
// The maps were built inline here, which made this rail the ONLY surface that projected them —
|
||||
// the filter chip bar rendered the raw code. They now live in useTreeLegendLabels, next to
|
||||
// useTreeLegendColors, and every People-facing surface reads that one copy.
|
||||
const { affinityLabelMaps } = useTreeLegendLabels(cfg, dataStore, searchSourceId)
|
||||
// ★ THE KIT'S OWN NEUTRAL, NOT the research palette's. useTreeLegendColors exports a NEUTRAL with
|
||||
// the same value today, and importing it would re-create the dependency this landing removes. The
|
||||
// two are deliberately NOT coupled: a consumer with no palette gets a plainly uncoloured rail, and
|
||||
// the research palette remains free to change its own neutral without reaching in here.
|
||||
const KIT_NEUTRAL_SWATCH = '#c4c8c0'
|
||||
// A dashboard that supplies no `legend` gets an honest degraded rail — uncoloured swatches and raw
|
||||
// values — rather than a crash. That is the state go-int sees before it wires its own palette, and
|
||||
// a half-configured dashboard should look plain, not broken.
|
||||
const legend = computed(() => (cfg && cfg.legend) || null)
|
||||
const colorFor = (...a) => (legend.value?.colorFor ? legend.value.colorFor(...a) : KIT_NEUTRAL_SWATCH)
|
||||
const legendLabelFor = (facet, value) => (legend.value?.labelFor ? legend.value.labelFor(facet, value) : value)
|
||||
|
||||
// ── COLOURS: sourced from the cap-3 research map's colour strategies ────────────────────────
|
||||
// Every hex in this rail comes from the SAME strategy modules the map's pins paint with, via
|
||||
// useTreeLegendColors — so the legend can never drift from the map. Colour is applied AFTER the
|
||||
// rows exist and touches only `swatch`; `facet`/`value` (what the bus filters on) are untouched.
|
||||
const { colorFor } = useTreeLegendColors()
|
||||
// ── COLOURS: still the research map's strategies, now HANDED IN rather than imported ─────────
|
||||
// Every hex in this rail comes from the SAME strategy modules the map's pins paint with, so the
|
||||
// legend cannot drift from the map — but the rail no longer reaches for them itself. The shell
|
||||
// supplies them as dashboardConfig.legend.colorFor (kit landing 4b), which is what lets this file
|
||||
// become kit code while the DOXA palettes stay in plugin-designs. Colour is applied AFTER the rows
|
||||
// exist and touches only `swatch`; `facet`/`value` (what the bus filters on) are untouched.
|
||||
|
||||
// A row inherits its ANCESTOR's colour, mirroring the map's semantic tree where one hue owns a
|
||||
// whole subtree (useAffinityBlockLegendData.js:169 assigns one colour per bloc and reuses it for
|
||||
|
|
@ -379,8 +416,14 @@ const sections = computed(() => (cfg.facetRail || []).map((s) => {
|
|||
// For a tree-backed section (People), swap the aggregated CODE keys for the composable's human
|
||||
// names — display only; facet/value/filter identity stays the code so filtering matches the map.
|
||||
if (s.tree === 'affinity') {
|
||||
const maps = affinityLabelMaps.value
|
||||
rows = rows.map((r) => (r.other ? r : { ...r, label: (maps[r.facet] && maps[r.facet].get(r.value)) || r.label }))
|
||||
// labelFor returns the VALUE unchanged when it has no mapping, so "did it map?" is
|
||||
// `mapped !== r.value` — and an unmapped row keeps the label it already had, exactly as the
|
||||
// affinityLabelMaps lookup did with its `|| r.label`.
|
||||
rows = rows.map((r) => {
|
||||
if (r.other) return r
|
||||
const mapped = legendLabelFor(r.facet, r.value)
|
||||
return mapped !== r.value ? { ...r, label: mapped } : r
|
||||
})
|
||||
}
|
||||
rows = addColors(rows, s.axis, religionFamKeys.value)
|
||||
// Trunks are computed LAST, over the FINAL row list, so the ├/└ glyphs reflect exactly what
|
||||
|
|
@ -1,8 +1,26 @@
|
|||
<script setup>
|
||||
/**
|
||||
* FilterChips — RDASH phase 5: the live filterSet rendered as removable AND-stacked chips
|
||||
* (Islam ✕ | South Asia ✕ | …) + Clear all. The chip bar IS the live query state (plan §INTERACTION)
|
||||
* and is what CSV export reads later. Click delegation on the bar (defineCustomElement-safe).
|
||||
* DashboardFilterChips — the live filterSet rendered as removable AND-stacked chips
|
||||
* (Islam ✕ | South Asia ✕ | …) + Clear all. The chip bar IS the live query state and is what CSV
|
||||
* export reads later. Click delegation on the bar, which is what makes it defineCustomElement-safe.
|
||||
*
|
||||
* ── KIT LANDING 4c-2: MOVED HERE FROM plugin-designs/research-dashboard/ ──────────────────────
|
||||
* Nothing about the rendering changed. What made the move possible was landing 4b, which inverted
|
||||
* the one dependency that blocked it: this file used to import useTreeLegendLabels — the People
|
||||
* CODE → NAME tables, which are research-specific and STAY in plugin-designs — and a library
|
||||
* component importing a plugin composable is exactly what check:no-cross-plugin-imports refuses.
|
||||
* Now the resolver arrives on dashboardConfig.legend and this file needs no research anything.
|
||||
*
|
||||
* ★ AN EXPLICIT EXCEPTION TO library/components/WARNING.md, recorded so the next reader does not
|
||||
* think the rule was missed. That file says a component graduates into the library only once MORE
|
||||
* THAN ONE map uses it. This one is used by ZERO: the topbar dedup pass unmounted it when
|
||||
* SearchFilterHeader became the single source of truth for showing and clearing filters, and it has
|
||||
* stayed unmounted deliberately. It is here because the kit's promise runs the other way round — a
|
||||
* NEW dashboard mounts kit components, so waiting for a second consumer before providing the first
|
||||
* one is a deadlock. The honest consequence: its passing gate means "it mounts and degrades", not
|
||||
* "it looks right", and nobody has SEEN it since the dedup. Anyone wiring it into a dashboard should
|
||||
* expect to be its first real reader, and must not mount it alongside a SearchFilterHeader that is
|
||||
* already showing chips — two chip rows over one filterSet is the bug the dedup removed.
|
||||
*/
|
||||
import { inject, computed } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
|
@ -12,14 +30,14 @@ import { tStatic } from '@map/i18n/index.js'
|
|||
// takes the subtree with it.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { useTreeLegendLabels } from '../../composables/useTreeLegendLabels.js'
|
||||
// No research label import (kit landing 4b) — the shell supplies it on dashboardConfig.legend.
|
||||
|
||||
const bus = inject('selectionBus', null)
|
||||
const cfg = inject('dashboardConfig', {})
|
||||
// Same store the facet rail reads — the source useAffinityBlockLegendData resolves People
|
||||
// codes to human names from.
|
||||
const dataStore = inject('dataStore', null)
|
||||
const searchSourceId = inject('searchSourceId', null)
|
||||
// `dataStore` and `searchSourceId` were injected here ONLY to construct useTreeLegendLabels, and
|
||||
// landing 4b removed that construction. They are gone rather than left harmlessly in place: an
|
||||
// unused inject is a claim about the host, and a kit component that appears to need a research
|
||||
// dataStore is one a new dashboard will dutifully provide before finding out it was never read.
|
||||
const filterSet = bus?.filterSet || {}
|
||||
|
||||
// ── People codes must not reach this chip ────────────────────────────────────────────────────
|
||||
|
|
@ -29,7 +47,14 @@ const filterSet = bus?.filterSet || {}
|
|||
// DISPLAY ONLY: `k` (the facet key) and the stored filterSet value are never rewritten — the bus
|
||||
// matches the accessor's output byte-for-byte, and a chip that merely looked right while the
|
||||
// table stopped responding would be a worse bug than the one being fixed.
|
||||
const { labelFor } = useTreeLegendLabels(cfg, dataStore, searchSourceId)
|
||||
// ★ THE UNCONFIGURED DEFAULT JOINS A LIST, because a filterSet value is an ARRAY once two values
|
||||
// are selected (spec D1) and 4b's fallback returned it untouched — so `String(value)` printed
|
||||
// "A012,A013", comma-jammed, while a dashboard that DID supply a resolver got "South Asian Peoples,
|
||||
// Bengali Peoples". Configured and unconfigured should differ in whether CODES RESOLVE, never in
|
||||
// punctuation: the second difference reads as a rendering bug rather than as missing configuration.
|
||||
// Kept as one liftable expression on purpose — check:legend-mirrors pulls it out of this source and
|
||||
// drives it, so "no resolver yields the raw value" is measured instead of asserted.
|
||||
const labelFor = (facet, value) => (cfg?.legend?.labelFor ? cfg.legend.labelFor(facet, value) : (Array.isArray(value) ? value.join(', ') : value))
|
||||
const chips = computed(() => Object.entries(filterSet).map(([k, v]) => ({
|
||||
k,
|
||||
text: String(labelFor(k, v)),
|
||||
156
library/components/regions/GoalCard.vue
Normal file
156
library/components/regions/GoalCard.vue
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<!--
|
||||
GoalCard.vue — a headline figure with a goal line pinned to the bottom of the card.
|
||||
|
||||
The Driver named these directly: "the banners are the cards that tell you the prayer progress".
|
||||
Their RegionDetail.vue renders three of them in a row -- Prayer Coverage, Adoption Progress,
|
||||
Engagement Progress -- identical in structure and different only in their numbers, their bar, and
|
||||
one dark variant. That is one component with props, which is why it is here and not three copies.
|
||||
|
||||
★ NAMED GoalCard, NOT PrayerGoalCard, and the deviation from the name in the order is deliberate:
|
||||
nothing in this component's props or CSS is about prayer. The shepherd's name described the first
|
||||
instance; a shared component named after one caller mis-describes it for every later one. Flagged
|
||||
in the handoff rather than decided silently.
|
||||
|
||||
★ THE MIDDLE IS A SLOT. What sits between the figure and the goal line differs per instance -- a
|
||||
segmented bar with a legend in one, a plain bar in the other two -- so the card does not import a
|
||||
bar at all. It knows about a title, a figure, and a goal; the caller supplies the rest. This is the
|
||||
same rule RegionSectionCard follows for its poster wall: one caller's feature must not become every
|
||||
caller's API.
|
||||
|
||||
★ margin-top:auto IS THE WHOLE POINT OF THE LAYOUT, not a detail. It is what makes three cards
|
||||
holding different amounts of copy align their goal lines flush along the bottom edge. The full
|
||||
measurement history is on the rules below; it survived a probe that measured the pair as "same".
|
||||
|
||||
TWO TEMPLATE DETAILS, documented here rather than in the template, because a comment between nodes
|
||||
is a rendered node and this card's DOM is compared against the markup it replaced:
|
||||
• the figure and its caption sit in an UNCLASSED <div>. That grouping is what makes the card's
|
||||
stack margin fall between the groups rather than between a number and its own label.
|
||||
• the space after </strong> in the goal line is INTERPOLATED, not written as template whitespace,
|
||||
so it cannot depend on the compiler's whitespace:condense behaviour.
|
||||
|
||||
CONTRACT, two things the host root must provide, exactly as for RegionSectionCard:
|
||||
• the DX-life token layer (--dx-radius, --dx-elevated, --dx-dark, --spacing-*, --font-size-*).
|
||||
• for the Cyrillic heading switch, a `lang` attribute on an ancestor INSIDE THE SAME shadow root.
|
||||
A [lang] selector cannot see the host document's attribute across a shadow boundary, so a
|
||||
consumer that does not set lang on an ancestor gets the default face and nothing else changes.
|
||||
No i18n is installed or required: every string arrives pre-localized, including the goal label.
|
||||
-->
|
||||
<script setup>
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { useCardSurface } from '../../composables/useCardSurface.js'
|
||||
|
||||
defineProps({
|
||||
/** The card heading, already localized. */
|
||||
title: { type: String, default: '' },
|
||||
/** The headline figure, pre-formatted by the caller, e.g. "21 / 2,107". */
|
||||
value: { type: [String, Number], default: '' },
|
||||
/** The caption under the figure, already localized. */
|
||||
valueLabel: { type: String, default: '' },
|
||||
/** The goal sentence, already localized. Pinned to the bottom of the card. */
|
||||
goal: { type: String, default: '' },
|
||||
/** The bold lead-in to the goal sentence, e.g. "Goal". Omitted entirely when empty. */
|
||||
goalLabel: { type: String, default: '' },
|
||||
/** Follows goalLabel inside the <strong>. A prop because ':' is not universal punctuation. */
|
||||
labelSeparator: { type: String, default: ':' },
|
||||
/** 'secondary' is their dark-on-light variant, used by the first of the three cards. */
|
||||
variant: { type: String, default: 'default' },
|
||||
})
|
||||
|
||||
// .rd-card / --secondary, shared with StatCard under one stable label.
|
||||
useCardSurface()
|
||||
|
||||
useShadowStyles(
|
||||
// ★ THEIR .stack IS margin-top, AND THAT IS LOAD-BEARING — not a stylistic detail.
|
||||
// composition/_stack.scss: `.stack > * + * { margin-top: var(--stack-spacing) }`, with
|
||||
// `.stack--lg` setting that to --spacing-lg. RegionDetail.vue's scoped
|
||||
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
|
||||
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
|
||||
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
|
||||
// so every goal card carried ~23.5px of space their cards do not have. The DRIVER saw it
|
||||
// ('spcing is nto ths ame', naming these three cards). Ported as their mechanism.
|
||||
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
|
||||
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
|
||||
|
||||
// ── the heading. h2: _globals.scss clamp(--font-size-2xl, 6vw, --font-size-4xl).
|
||||
// ★ THIS IS A SECOND COPY OF THE PLUGIN'S .rd-h2 RULE, under this card's own class name, and the
|
||||
// duplication is deliberate rather than overlooked. Using .rd-h2 itself would put two identical
|
||||
// selectors in one shadow root from two different files -- the shape that drifts silently the first
|
||||
// time one is tuned. The plugin keeps .rd-h2 for the one heading it still owns (its table title).
|
||||
// The real fix is a shared ported-globals module beside the token layer, which is a batch of its
|
||||
// own; carded, not smuggled in here.
|
||||
'.rd-goal-card__title{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl)); line-height:1;' +
|
||||
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' font-weight:400; }' +
|
||||
// ★ CYRILLIC HEADINGS SWITCH FACE, as theirs do. base/_globals.scss:35-44:
|
||||
// h1,h2,h3 { [lang="ru-RU"] & { font-family: var(--font-family-cyrillic); letter-spacing: -0.02em } }
|
||||
// and --font-family-cyrillic is Oswald. The switch is not decoration: BEBAS NEUE HAS NO CYRILLIC
|
||||
// GLYPHS, so a Russian heading in it leaves the family entirely and lands on whatever condensed
|
||||
// system face the browser picks.
|
||||
// BOTH TAG FORMS, deliberately: their site tags the document ru-RU while our i18n locale is `ru`,
|
||||
// and a host page embedding this element may set either. A selector that matched only one would
|
||||
// work on their site and fail on ours, or the reverse, depending on who set the attribute.
|
||||
'[lang="ru"] .rd-goal-card__title, [lang="ru-RU"] .rd-goal-card__title{' +
|
||||
' font-family:"Oswald","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' letter-spacing:-0.02em; }' +
|
||||
|
||||
// ── the figure. .font-size-4xl .font-weight-medium / .font-size-lg
|
||||
// ★ NO line-height HERE. Theirs is `<p class="font-size-4xl font-weight-medium">` and the
|
||||
// .font-size-4xl utility sets ONLY a font-size — so their number inherits body's 1.5. Ours forced
|
||||
// 1.1, which was compensating for the base line-height we never ported (now on :host in
|
||||
// dxlife-tokens.js). rega measured the cost: 24px of card height, their number ~12px lower and its
|
||||
// label ~24px lower. Inheriting is the faithful port.
|
||||
// ★ A CLAMP, LIKE THEIRS, AND IT RESOLVES THE NOWRAP WORRY RATHER THAN FIGHTING IT.
|
||||
// utilities/_typography.scss gives .font-size-4xl
|
||||
// font-size: clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))
|
||||
// and ours was a flat var(--font-size-4xl). Identical at 1440 and above (both sit on the ceiling),
|
||||
// but below that ours never shrank -- MEASURED against their resolved values:
|
||||
// 390px viewport theirs 38.15 ours 59.60 ours 21.46 too large
|
||||
// 768px viewport theirs 46.08 ours 59.60 ours 13.52 too large
|
||||
// 1024px and up theirs 59.60 ours 59.60 identical
|
||||
// I had reported this as unchangeable because a figure must never split across lines in a 372px
|
||||
// card. That was backwards: the clamp makes the figure SMALLER exactly where the wrap was observed.
|
||||
// In a 372px container 6cqw is 22.3px, below the 2xl floor, so it resolves to 38.15px against the
|
||||
// 59.60px that was breaking. The clamp helps the very problem the flat value was chosen for, and
|
||||
// the nowrap stays as a belt.
|
||||
// cqw not vw, consistent with every other clamp in this port (the container-vs-viewport
|
||||
// substitution the Driver is still ruling on); at 768 that lands 42.4 against their 46.08.
|
||||
'.rd-big{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl));' +
|
||||
' font-weight:500; }' +
|
||||
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
|
||||
'.rd-big-label{ margin:0; font-size:var(--font-size-lg); }' +
|
||||
|
||||
// ── the goal line. .region-goal — margin-top:auto, spelled at higher specificity than the stack
|
||||
// rule above so the override is unambiguous rather than depending on source order.
|
||||
// ★ margin-top:auto, AND IT IS LOAD-BEARING. Theirs is RegionDetail.vue:24 `.region-goal {
|
||||
// margin-top: auto }` inside `<div class="stack stack--lg | card | text-center">`, and
|
||||
// composition/_stack.scss makes .stack a flex COLUMN with `> * + * { margin-top:
|
||||
// var(--stack-spacing) }`. So the auto OVERRIDES that child margin and pushes the goal line to the
|
||||
// BOTTOM of the card. Ours was margin:0, which left it directly under the text above it.
|
||||
// The visible consequence, and why it matters for a clone: the three goal cards hold different
|
||||
// amounts of copy, so theirs align their goal lines flush along the cards' bottom edge while ours
|
||||
// floated at three different heights.
|
||||
// FOUND BY READING THEIR MARKUP after rega's spacing probe measured this pair as "same" -- which
|
||||
// it is, at 1440, because with no free space in the card the auto resolves to 0. An equal computed
|
||||
// margin hid a real difference, so the probe was right and incomplete at the same time.
|
||||
'.rd-goal{ margin:auto 0 0; font-size:var(--font-size-sm); }' +
|
||||
'.rd-goal-card > .rd-goal{ margin-top:auto; }' +
|
||||
// base/_globals.scss:200-202 — <strong> is --font-weight-medium (500) on their site, not the UA's
|
||||
// bold. The plugin's own `.rd-root strong` STAYS where it is: UupgsList renders a <strong> in the
|
||||
// same shadow root and depends on it, so this is a second rule for this card's own subtree, not a
|
||||
// move. Checked by reading the whole root rather than this card's markup -- a file-scoped grep
|
||||
// would have called the plugin's copy dead and deleted a rule another component needs.
|
||||
'.rd-goal strong{ font-weight:500; }'
|
||||
, 'goal-card')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rd-card rd-goal-card" :class="{ 'rd-card--secondary': variant === 'secondary' }">
|
||||
<h2 class="rd-goal-card__title">{{ title }}</h2>
|
||||
<div>
|
||||
<p class="rd-big">{{ value }}</p>
|
||||
<p class="rd-big-label">{{ valueLabel }}</p>
|
||||
</div>
|
||||
<slot />
|
||||
<p class="rd-goal"><strong v-if="goalLabel">{{ goalLabel }}{{ labelSeparator }}</strong>{{ goalLabel ? ' ' : '' }}{{ goal }}</p>
|
||||
</div>
|
||||
</template>
|
||||
63
library/components/regions/StatCard.vue
Normal file
63
library/components/regions/StatCard.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
StatCard.vue — one figure with a caption, on the DX-life card surface.
|
||||
|
||||
Their RegionDetail.vue renders a row of these above the map: a big number and a small grey label,
|
||||
centred on a small-padding card. That is the whole component, and it is the smallest genuinely
|
||||
reusable piece of the regions dashboard.
|
||||
|
||||
★ THE GRID IS NOT IN HERE, AND THAT IS THE POINT. The plugin's `.rd-stats` container
|
||||
(grid-template-columns: repeat(auto-fit, minmax(180px, 1fr))) stays with the CONSUMER. How many
|
||||
stats there are and how they wrap is a decision belonging to the page, not to one stat; baking a
|
||||
grid into a single card would make it impossible to place one on its own, or two side by side, or
|
||||
in a column. So a consumer supplies its own container and puts as many StatCards in it as it has.
|
||||
|
||||
VALUES ARE PRE-FORMATTED BY THE CALLER. `value` is a string or number and is rendered verbatim,
|
||||
because number formatting is locale-dependent and the caller already knows the locale -- the
|
||||
regions plugin passes formatNumber(n) through its own i18n. This component installs no i18n and
|
||||
therefore works on a host that has none.
|
||||
|
||||
CONTRACT: the containing shadow root must carry the DX-life token layer (DXLIFE_TOKENS in the
|
||||
regions plugin) and, for the figure to render in the intended face, the Bebas Neue @font-face --
|
||||
which is DOCUMENT-scoped and so must be registered on the host document, not in the shadow root.
|
||||
See useDocumentFont.js. Without the face the declared fallbacks apply; nothing breaks.
|
||||
-->
|
||||
<script setup>
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { useCardSurface } from '../../composables/useCardSurface.js'
|
||||
|
||||
defineProps({
|
||||
/** The figure. Already formatted for the locale by the caller, e.g. "43,045,275". */
|
||||
value: { type: [String, Number], default: '' },
|
||||
/** The caption under the figure. Already localized by the caller. */
|
||||
label: { type: String, default: '' },
|
||||
})
|
||||
|
||||
// .rd-card / --small, shared with GoalCard under one stable label so only one copy is injected.
|
||||
useCardSurface()
|
||||
|
||||
useShadowStyles(
|
||||
// ── .region-stat: RegionDetail.vue's scoped block ──
|
||||
'.rd-stat{ text-align:center; }' +
|
||||
// ★ THE data-len SHRINK IS GONE, AND ITS OWN COMMENT SHOWED WHY. Their .region-stat__value is
|
||||
// font-family-heading + font-size-3xl + line-height:1 and nothing else -- no such attribute exists
|
||||
// in their markup. Ours was a shrink-to-fit workaround whose stated premise was "our condensed
|
||||
// fallback is wider" than Bebas. That is STALE: Bebas is vendored now (assets/fonts/bebas-neue,
|
||||
// glyph oracle 279 = 279 SAME), so there is no wider fallback to compensate for.
|
||||
// Its other claim -- "every value DX-life itself renders keeps their size" -- was FALSIFIED by
|
||||
// measurement: rega found "43,045,275" (10 chars, so it hit the 'long' bucket) rendered at
|
||||
// 38.15px against their 47.68px on the same-width card, while the other three stats matched
|
||||
// exactly. A rule whose own invariant is violated in practice is not protecting anything.
|
||||
// Deleted WITH its mechanism -- the binding and lenBucket() too, not just the rules.
|
||||
'.rd-stat__value{ margin:0; font-size:var(--font-size-3xl); line-height:1;' +
|
||||
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' font-weight:400; }' +
|
||||
'.rd-stat__label{ margin:var(--spacing-2xs) 0 0; font-size:var(--font-size-sm); color:var(--dx-muted); }'
|
||||
, 'stat-card')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rd-card rd-card--small rd-stat">
|
||||
<p class="rd-stat__value">{{ value }}</p>
|
||||
<p class="rd-stat__label">{{ label }}</p>
|
||||
</div>
|
||||
</template>
|
||||
44
library/composables/useCardSurface.js
Normal file
44
library/composables/useCardSurface.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* useCardSurface.js — the DX-life card surface, as ONE rule set shared by every card component.
|
||||
*
|
||||
* WHY THIS IS A MODULE AND NOT A COPY IN EACH CARD. Vue only injects the ROOT component's <style>
|
||||
* into a shadow root, so every library component that renders inside one carries its own CSS and
|
||||
* injects it with useShadowStyles (see that file). StatCard and GoalCard are two components that
|
||||
* need the SAME surface -- radius, elevation, padding, and the two variants -- and a copy in each is
|
||||
* a rule set that will drift the first time one of them is tuned.
|
||||
*
|
||||
* THE STABLE LABEL IS THE WHOLE TRICK. useShadowStyles dedups on its label, so both cards (and any
|
||||
* consumer that renders .rd-card markup of its own) can call useCardSurface() and exactly ONE
|
||||
* <style data-shadow-styles="dxlife-card-surface"> lands per shadow root, no matter how many card
|
||||
* components mount inside it. Calling it from N components is not N stylesheets.
|
||||
*
|
||||
* ★ THE CLASS NAMES KEEP THEIR `rd-` PREFIX, DELIBERATELY. It came from the regions-dashboard port
|
||||
* and means nothing outside it. Renaming would change no pixel and would cost the traceability from
|
||||
* each declaration back to the DX-life rule and the measurement that fixed it, which is the only
|
||||
* reason these values can be defended. The names are internal to the components; a consumer writes
|
||||
* <StatCard> and never types them. A rename is a safe later cleanup, not a prerequisite for reuse.
|
||||
*
|
||||
* CONTRACT: the shadow root must already carry the DX-life token layer (--dx-radius, --dx-elevated,
|
||||
* --dx-text, --dx-dark, --dx-on-dark, --spacing-*). The regions plugin injects it as DXLIFE_TOKENS.
|
||||
* Without it these rules resolve to nothing -- they are a skin over that scale, not a replacement.
|
||||
*/
|
||||
import { useShadowStyles } from './useShadowStyles.js'
|
||||
|
||||
export const CARD_SURFACE_CSS =
|
||||
// ── .card: blocks/_cards.scss:9-21. radius --border-radius-xlg (35px);
|
||||
// padding clamp(--spacing-lg, 6vw, --spacing-2xl) clamp(--spacing-lg, 6vw, --spacing-3xl).
|
||||
'.rd-card{ position:relative; border-radius:var(--dx-radius);' +
|
||||
' background:var(--dx-elevated); color:var(--dx-text);' +
|
||||
' padding:clamp(var(--spacing-lg), 6cqw, var(--spacing-2xl)) clamp(var(--spacing-lg), 6cqw, var(--spacing-3xl)); }' +
|
||||
// .card[padding-small] -> --spacing-lg flat
|
||||
'.rd-card--small{ padding:var(--spacing-lg); }' +
|
||||
// .card[data-variant="secondary"] -> surface-brand-dark on text-on-brand-light
|
||||
'.rd-card--secondary{ background:var(--dx-dark); color:var(--dx-on-dark); }'
|
||||
|
||||
/**
|
||||
* Injects the card surface into the containing shadow root, once, under a stable label.
|
||||
* Call it at setup() time from any component that renders .rd-card markup.
|
||||
*/
|
||||
export function useCardSurface() {
|
||||
useShadowStyles(CARD_SURFACE_CSS, 'dxlife-card-surface')
|
||||
}
|
||||
58
library/composables/useFacetAggregation.js
Normal file
58
library/composables/useFacetAggregation.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* useFacetAggregation.js — count rows by a facet accessor, and roll the tail into one bucket.
|
||||
*
|
||||
* ★ EXTRACTED FROM useDashboardAggregations (dashboard-kit landing 4c-1). That module has a split
|
||||
* personality: these two functions are pure counting, while the rest of it — colorFor with its
|
||||
* hardcoded religion / wagf_region / affinity_bloc branches, the religion palettes, the donut and
|
||||
* bar builders — is research-specific and STAYS in plugin-designs. Only the generic half could
|
||||
* move, and the facet rail needs only the generic half, which is what made this the piece blocking
|
||||
* kit landing 4c.
|
||||
*
|
||||
* Bodies are verbatim. Nothing here knows a facet name: the caller supplies the accessor.
|
||||
*/
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
/**
|
||||
* A deliberately NEUTRAL tone for a value this facet has no palette for — matching the tree
|
||||
* legend's NEUTRAL exactly. An uncoloured slice must read as "no colour strategy for this key",
|
||||
* never as a confident member of some other family. Critically it is STABLE: the same key gets
|
||||
* the same neutral every time, so nothing changes colour when a filter reorders the list.
|
||||
*/
|
||||
export const NEUTRAL = '#c4c8c0'
|
||||
|
||||
export const lbl = (v) => (v === null || v === undefined ? '' : (typeof v === 'object' ? (v.label ?? v.value ?? '') : v))
|
||||
|
||||
/** Count rows by a key accessor; collapse the long tail into 'Other'. */
|
||||
export function aggregate(rows, keyFn, topN) {
|
||||
const m = new Map()
|
||||
for (const r of rows) { const k = keyFn(r); if (k === null || k === undefined || k === '') continue; m.set(k, (m.get(k) || 0) + 1) }
|
||||
let arr = [...m.entries()].map(([key, value]) => ({ key, value })).sort((a, b) => b.value - a.value)
|
||||
if (topN && arr.length > topN) {
|
||||
const top = arr.slice(0, topN)
|
||||
const other = arr.slice(topN).reduce((s, x) => s + x.value, 0)
|
||||
if (other > 0) top.push({ key: 'Other', value: other, other: true })
|
||||
arr = top
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ★ THE ROLLUP BUCKET'S KEY IS A SENTINEL; ITS LABEL IS A TRANSLATION. Keep them apart.
|
||||
*
|
||||
* aggregate() pushes `{ key: OTHER_KEY, other: true }` for the tail, and 'Other' was rendered
|
||||
* directly — so every locale showed the English word, in the facet rail and the charts rail both.
|
||||
* check:i18n-keys-exist could not see it, because there is no T() call to inspect: the string was
|
||||
* never a lookup, it was a key being used as a label.
|
||||
*
|
||||
* The KEY must not be translated. The rail tests `key === 'Other'` to make the row inert and
|
||||
* unfilterable, and the charts rail refuses to filter on it — a localized key would make the bucket
|
||||
* clickable in Arabic and silently filter on a value no row carries. That is the same value/label
|
||||
* trap as the religion families, arriving in generic code this time, which is worse: unfixed it
|
||||
* would have been exported to every future dashboard the moment this module became kit code.
|
||||
*
|
||||
* So the sentinel stays English and OTHER_LABEL() projects it for a reader, next to the function
|
||||
* that produces it, so the two cannot drift apart.
|
||||
*/
|
||||
export const OTHER_KEY = 'Other'
|
||||
export const otherLabel = () => tStatic('dashboard.other') || OTHER_KEY
|
||||
256
library/data/iso3-to-alpha2.json
Normal file
256
library/data/iso3-to-alpha2.json
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
{
|
||||
"_meta": {
|
||||
"generated_by": "library/scripts/generate-iso3-to-alpha2.mjs (bun run gen:iso3-to-alpha2)",
|
||||
"purpose": "alpha-3 -> alpha-2 so Intl.DisplayNames can produce a LOCALIZED country name. Drives no data fetch.",
|
||||
"not_to_be_confused_with": "library/scripts/research/out/iso3-to-iso2.json — the 176-country research spine that DOES drive fetches",
|
||||
"source": "CLDR cldr-core 48.2.2, vendored at library/vendor/cldr/codeMappings.json (Unicode-3.0)",
|
||||
"countries": 242,
|
||||
"exceptions": {
|
||||
"KOS": "XK — Kosovo has no ISO 3166-1 assignment; XK is the user-assigned code CLDR itself uses in locale ids (sr_Cyrl_XK)."
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"AFG": "AF",
|
||||
"ALB": "AL",
|
||||
"DZA": "DZ",
|
||||
"ASM": "AS",
|
||||
"AND": "AD",
|
||||
"AGO": "AO",
|
||||
"AIA": "AI",
|
||||
"ATG": "AG",
|
||||
"ARG": "AR",
|
||||
"ARM": "AM",
|
||||
"ABW": "AW",
|
||||
"AUS": "AU",
|
||||
"AUT": "AT",
|
||||
"AZE": "AZ",
|
||||
"BHR": "BH",
|
||||
"BGD": "BD",
|
||||
"BRB": "BB",
|
||||
"BLR": "BY",
|
||||
"BEL": "BE",
|
||||
"BLZ": "BZ",
|
||||
"BEN": "BJ",
|
||||
"BMU": "BM",
|
||||
"BTN": "BT",
|
||||
"BOL": "BO",
|
||||
"BES": "BQ",
|
||||
"BIH": "BA",
|
||||
"BWA": "BW",
|
||||
"BRA": "BR",
|
||||
"BRN": "BN",
|
||||
"BGR": "BG",
|
||||
"BFA": "BF",
|
||||
"BDI": "BI",
|
||||
"CPV": "CV",
|
||||
"KHM": "KH",
|
||||
"CMR": "CM",
|
||||
"CAN": "CA",
|
||||
"CYM": "KY",
|
||||
"CAF": "CF",
|
||||
"TCD": "TD",
|
||||
"CHL": "CL",
|
||||
"CHN": "CN",
|
||||
"CXR": "CX",
|
||||
"CCK": "CC",
|
||||
"COL": "CO",
|
||||
"COM": "KM",
|
||||
"COK": "CK",
|
||||
"CRI": "CR",
|
||||
"CIV": "CI",
|
||||
"HRV": "HR",
|
||||
"CUB": "CU",
|
||||
"CUW": "CW",
|
||||
"CYP": "CY",
|
||||
"DNK": "DK",
|
||||
"DJI": "DJ",
|
||||
"DMA": "DM",
|
||||
"DOM": "DO",
|
||||
"ECU": "EC",
|
||||
"EGY": "EG",
|
||||
"SLV": "SV",
|
||||
"GNQ": "GQ",
|
||||
"ERI": "ER",
|
||||
"EST": "EE",
|
||||
"SWZ": "SZ",
|
||||
"ETH": "ET",
|
||||
"FLK": "FK",
|
||||
"FRO": "FO",
|
||||
"FJI": "FJ",
|
||||
"FIN": "FI",
|
||||
"FRA": "FR",
|
||||
"GUF": "GF",
|
||||
"PYF": "PF",
|
||||
"GAB": "GA",
|
||||
"GEO": "GE",
|
||||
"DEU": "DE",
|
||||
"GHA": "GH",
|
||||
"GIB": "GI",
|
||||
"GRC": "GR",
|
||||
"GRL": "GL",
|
||||
"GRD": "GD",
|
||||
"GLP": "GP",
|
||||
"GUM": "GU",
|
||||
"GTM": "GT",
|
||||
"GGY": "GG",
|
||||
"GIN": "GN",
|
||||
"GNB": "GW",
|
||||
"GUY": "GY",
|
||||
"HTI": "HT",
|
||||
"HND": "HN",
|
||||
"HKG": "HK",
|
||||
"HUN": "HU",
|
||||
"ISL": "IS",
|
||||
"IND": "IN",
|
||||
"IDN": "ID",
|
||||
"IRN": "IR",
|
||||
"IRQ": "IQ",
|
||||
"IRL": "IE",
|
||||
"IMN": "IM",
|
||||
"ISR": "IL",
|
||||
"ITA": "IT",
|
||||
"JAM": "JM",
|
||||
"JPN": "JP",
|
||||
"JEY": "JE",
|
||||
"JOR": "JO",
|
||||
"KAZ": "KZ",
|
||||
"KEN": "KE",
|
||||
"KIR": "KI",
|
||||
"KOS": "XK",
|
||||
"KWT": "KW",
|
||||
"KGZ": "KG",
|
||||
"LAO": "LA",
|
||||
"LVA": "LV",
|
||||
"LBN": "LB",
|
||||
"LSO": "LS",
|
||||
"LBR": "LR",
|
||||
"LBY": "LY",
|
||||
"LIE": "LI",
|
||||
"LTU": "LT",
|
||||
"LUX": "LU",
|
||||
"MAC": "MO",
|
||||
"MDG": "MG",
|
||||
"MWI": "MW",
|
||||
"MYS": "MY",
|
||||
"MLI": "ML",
|
||||
"MLT": "MT",
|
||||
"MTQ": "MQ",
|
||||
"MRT": "MR",
|
||||
"MUS": "MU",
|
||||
"MYT": "YT",
|
||||
"MEX": "MX",
|
||||
"MDA": "MD",
|
||||
"MCO": "MC",
|
||||
"MNG": "MN",
|
||||
"MNE": "ME",
|
||||
"MSR": "MS",
|
||||
"MAR": "MA",
|
||||
"MOZ": "MZ",
|
||||
"MMR": "MM",
|
||||
"NAM": "NA",
|
||||
"NRU": "NR",
|
||||
"NPL": "NP",
|
||||
"NCL": "NC",
|
||||
"NZL": "NZ",
|
||||
"NIC": "NI",
|
||||
"NER": "NE",
|
||||
"NGA": "NG",
|
||||
"NIU": "NU",
|
||||
"NFK": "NF",
|
||||
"PRK": "KP",
|
||||
"MKD": "MK",
|
||||
"NOR": "NO",
|
||||
"OMN": "OM",
|
||||
"PAK": "PK",
|
||||
"PLW": "PW",
|
||||
"PSE": "PS",
|
||||
"PAN": "PA",
|
||||
"PNG": "PG",
|
||||
"PRY": "PY",
|
||||
"PER": "PE",
|
||||
"POL": "PL",
|
||||
"PRT": "PT",
|
||||
"PRI": "PR",
|
||||
"QAT": "QA",
|
||||
"REU": "RE",
|
||||
"ROU": "RO",
|
||||
"RWA": "RW",
|
||||
"BLM": "BL",
|
||||
"SHN": "SH",
|
||||
"KNA": "KN",
|
||||
"LCA": "LC",
|
||||
"MAF": "MF",
|
||||
"SPM": "PM",
|
||||
"VCT": "VC",
|
||||
"WSM": "WS",
|
||||
"SMR": "SM",
|
||||
"STP": "ST",
|
||||
"SAU": "SA",
|
||||
"SEN": "SN",
|
||||
"SRB": "RS",
|
||||
"SYC": "SC",
|
||||
"SLE": "SL",
|
||||
"SGP": "SG",
|
||||
"SXM": "SX",
|
||||
"SVK": "SK",
|
||||
"SVN": "SI",
|
||||
"SOM": "SO",
|
||||
"ZAF": "ZA",
|
||||
"KOR": "KR",
|
||||
"SSD": "SS",
|
||||
"ESP": "ES",
|
||||
"LKA": "LK",
|
||||
"SDN": "SD",
|
||||
"SUR": "SR",
|
||||
"SJM": "SJ",
|
||||
"SWE": "SE",
|
||||
"CHE": "CH",
|
||||
"SYR": "SY",
|
||||
"TWN": "TW",
|
||||
"TJK": "TJ",
|
||||
"TZA": "TZ",
|
||||
"THA": "TH",
|
||||
"BHS": "BS",
|
||||
"VGB": "VG",
|
||||
"CZE": "CZ",
|
||||
"COD": "CD",
|
||||
"FSM": "FM",
|
||||
"GMB": "GM",
|
||||
"MDV": "MV",
|
||||
"MHL": "MH",
|
||||
"NLD": "NL",
|
||||
"MNP": "MP",
|
||||
"PHL": "PH",
|
||||
"PCN": "PN",
|
||||
"COG": "CG",
|
||||
"RUS": "RU",
|
||||
"SLB": "SB",
|
||||
"TCA": "TC",
|
||||
"ARE": "AE",
|
||||
"GBR": "GB",
|
||||
"USA": "US",
|
||||
"VIR": "VI",
|
||||
"TLS": "TL",
|
||||
"TGO": "TG",
|
||||
"TKL": "TK",
|
||||
"TON": "TO",
|
||||
"TTO": "TT",
|
||||
"TUN": "TN",
|
||||
"TUR": "TR",
|
||||
"TKM": "TM",
|
||||
"TUV": "TV",
|
||||
"UGA": "UG",
|
||||
"UKR": "UA",
|
||||
"URY": "UY",
|
||||
"UZB": "UZ",
|
||||
"VUT": "VU",
|
||||
"VAT": "VA",
|
||||
"VEN": "VE",
|
||||
"VNM": "VN",
|
||||
"WLF": "WF",
|
||||
"ESH": "EH",
|
||||
"YEM": "YE",
|
||||
"ZMB": "ZM",
|
||||
"ZWE": "ZW"
|
||||
}
|
||||
}
|
||||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "الأشخاص المصلّون",
|
||||
"selected_first": "المحدَّدة أولاً",
|
||||
"selected_first_hint": "رفع الشعوب المحدَّدة إلى الأعلى مع الإبقاء على ترتيب الأعمدة",
|
||||
"n_shown": "{n} معروض"
|
||||
"n_shown": "{n} معروض",
|
||||
"other": "أخرى"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "فيديو كتابي",
|
||||
"Gospel presentation": "عرض للإنجيل",
|
||||
"Countries": "البلدان",
|
||||
"People groups": "الشعوب"
|
||||
"People groups": "الشعوب",
|
||||
"People Group": "الشعب",
|
||||
"Basis": "مستوى البيانات",
|
||||
"Bridge ISO": "ISO اللغة الجسرية",
|
||||
"Bridge language": "اللغة الجسرية",
|
||||
"Film": "فيلم",
|
||||
"Groups with nothing": "شعوب بلا شيء",
|
||||
"People with nothing": "أشخاص بلا شيء",
|
||||
"Pools checked": "المصادر المفحوصة",
|
||||
"Audio": "صوتي",
|
||||
"Bridge candidate": "لغة جسرية مرشحة",
|
||||
"Bridge film": "فيلم باللغة الجسرية",
|
||||
"Bridge scripture": "الكتاب المقدس باللغة الجسرية",
|
||||
"Bridge strength": "قوة الجسر",
|
||||
"Est. deaths/day": "الوفيات المقدَّرة يوميًا",
|
||||
"Film link": "رابط الفيلم",
|
||||
"Full Bible": "الكتاب المقدس كاملًا",
|
||||
"NO RESOURCE": "لا يوجد أي مورد",
|
||||
"Nobody praying": "لا أحد يصلّي",
|
||||
"People": "عدد الأشخاص",
|
||||
"Pools NOT checked": "المصادر التي لم تُفحص",
|
||||
"Scripture link": "رابط الكتاب المقدس",
|
||||
"Sign language": "لغة الإشارة",
|
||||
"Status": "الحالة",
|
||||
"Training": "تدريب على التلمذة",
|
||||
"% with nothing": "% بلا شيء",
|
||||
"ALIGNED": "الشروط مستوفاة",
|
||||
"Audio link": "رابط الصوت",
|
||||
"Band": "الفئة",
|
||||
"Bible stories": "قصص الكتاب المقدس",
|
||||
"Can already hear": "يسمعون بالفعل",
|
||||
"Candidate % of country": "نسبة المرشحة في البلد %",
|
||||
"Candidate ISO": "ISO المرشحة",
|
||||
"Candidate is official": "المرشحة لغة رسمية",
|
||||
"Carries a resource": "تحمل موردًا",
|
||||
"Channel": "القناة",
|
||||
"Channel source": "مصدر القناة",
|
||||
"Code": "الرمز",
|
||||
"Code note": "ملاحظة الرمز",
|
||||
"Code unlookupable": "الرمز غير مفهرس في أي مصدر",
|
||||
"Code unmatchable": "رمز يتعذر مطابقته",
|
||||
"Deaf bridge": "جسر الصم",
|
||||
"Deaf bridge link": "رابط جسر الصم",
|
||||
"Deaf bridge publisher": "ناشر جسر الصم",
|
||||
"Deaf programme in country": "برنامج للصم في البلد",
|
||||
"Death rate /1000": "معدل الوفيات /1000",
|
||||
"Est. deaths/yr": "الوفيات المقدَّرة سنويًا",
|
||||
"Example group": "شعب على سبيل المثال",
|
||||
"Film in this language": "فيلم بهذه اللغة",
|
||||
"GAP": "فجوة",
|
||||
"GROWING + UNREACHED": "متزايدون + غير مبلَّغين",
|
||||
"Gospel audio": "صوتيات الإنجيل",
|
||||
"Groups": "الشعوب",
|
||||
"Groups sharing bridge": "شعوب تشترك في الجسر",
|
||||
"Groups speaking": "الشعوب الناطقة",
|
||||
"Groups speaking it": "شعوب تتحدثها",
|
||||
"Groups the map link shows": "الشعوب التي يعرضها رابط الخريطة",
|
||||
"Groups with Scripture": "شعوب لديها الكتاب المقدس",
|
||||
"Groups with film": "شعوب لديها الفيلم",
|
||||
"Growing & unreached": "متزايدون وغير مبلَّغين",
|
||||
"Growth %/yr": "النمو %/سنة",
|
||||
"Has anything": "لديه أي شيء",
|
||||
"Heart film": "فيلم باللغة الأم",
|
||||
"Heart language": "اللغة الأم",
|
||||
"Heart scripture": "الكتاب المقدس باللغة الأم",
|
||||
"Internet": "الإنترنت",
|
||||
"Internet %": "الإنترنت %",
|
||||
"Its population": "عدد سكانه",
|
||||
"JESUS Film": "فيلم يسوع",
|
||||
"Kind of gap": "نوع الفجوة",
|
||||
"Kind of way in": "نوع المدخل",
|
||||
"Languages": "اللغات",
|
||||
"Languages here": "اللغات هنا",
|
||||
"Languages they speak": "اللغات التي يتحدثونها",
|
||||
"Largest group": "أكبر شعب",
|
||||
"Largest groups": "أكبر الشعوب",
|
||||
"Largest with nothing": "الأكبر بلا شيء",
|
||||
"Lead status": "حالة الدليل الأولي",
|
||||
"Link": "الرابط",
|
||||
"Method": "المنهجية",
|
||||
"NT": "العهد الجديد",
|
||||
"Note": "ملاحظة",
|
||||
"OPPORTUNITY": "فرصة",
|
||||
"Of those with a prayer page": "من بين من لديهم صفحة صلاة",
|
||||
"On the map": "على الخريطة",
|
||||
"Open": "فتح",
|
||||
"People groups here": "الشعوب هنا",
|
||||
"People nobody is praying for": "أشخاص لا يصلّي أحد لأجلهم",
|
||||
"People praying": "المصلّون",
|
||||
"Place": "المكان",
|
||||
"Placement approximate": "الموقع تقريبي",
|
||||
"Pools not checked": "المصادر غير المفحوصة",
|
||||
"Pray": "صلِّ",
|
||||
"Primary religion": "الديانة الرئيسية",
|
||||
"Province": "المقاطعة",
|
||||
"Rank": "الترتيب",
|
||||
"Resource": "المورد",
|
||||
"Scope": "مدى التغطية",
|
||||
"Scripture in this language": "الكتاب المقدس بهذه اللغة",
|
||||
"Signed film": "فيلم بلغة الإشارة",
|
||||
"Signed film link": "رابط فيلم لغة الإشارة",
|
||||
"Source": "المصدر",
|
||||
"Speakers": "المتحدثون",
|
||||
"Their language": "لغتهم",
|
||||
"These peoples on the map": "هذه الشعوب على الخريطة",
|
||||
"Training in the bridge": "تدريب باللغة الجسرية",
|
||||
"Translated by": "ترجمة",
|
||||
"Type": "النوع",
|
||||
"UPG population": "سكان الشعوب غير المبلَّغة",
|
||||
"Urban %": "الحضر %",
|
||||
"Ways in": "المداخل",
|
||||
"Ways in (country-level)": "المداخل (على مستوى البلد)",
|
||||
"Year": "السنة"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "شعوب دوكسا — ما المتوفر من الإنجيل، وما الذي ينقص",
|
||||
"links": "موارد الإنجيل — كل رابط، ومن يصل إليه",
|
||||
"nothing": "حيث لا يوجد شيء",
|
||||
"countrygap": "الفجوة حسب البلد",
|
||||
"translation": "أولوية الترجمة — يسمعون، ولم يُعثر على نص",
|
||||
"translationcountry": "أولوية الترجمة حسب البلد — يسمعون، ولم يُعثر على نص",
|
||||
"gaps": "الفجوات والفرص",
|
||||
"bridges": "فرص جسور المدن",
|
||||
"deaf": "فجوة الصم",
|
||||
"resources": "الشعوب × موارد الإنجيل",
|
||||
"languages": "اللغات حسب البلد (مرتبة) × الموارد",
|
||||
"growth": "الوصول إلى الإنجيل مقابل نمو السكان",
|
||||
"province": "الوصول إلى الإنجيل حسب المقاطعة — الهند والصين",
|
||||
"bridgereach": "لغة واحدة، كم شعبًا — المدخل، مرتبًا حسب مدى الانتشار"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "الكتاب المقدس (أي نوع)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
|
||||
"upg100WallTag": "تعبئة للصلاة",
|
||||
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>.",
|
||||
"upg100WallIntroA": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم ",
|
||||
"upg100WallIntroEmph": "باسم شعبهم",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "جارٍ تحميل {name}…",
|
||||
"couldNotLoadNamed": "تعذّر تحميل {name}.",
|
||||
"engineMissing": "لم يتم تحميل محرك الخرائط {engine}.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Betende Personen",
|
||||
"selected_first": "Ausgewählte zuerst",
|
||||
"selected_first_hint": "Die ausgewählten Volksgruppen nach oben holen, die Spaltensortierung bleibt erhalten",
|
||||
"n_shown": "{n} angezeigt"
|
||||
"n_shown": "{n} angezeigt",
|
||||
"other": "Andere"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Bibelvideo",
|
||||
"Gospel presentation": "Evangeliums-Darstellung",
|
||||
"Countries": "Länder",
|
||||
"People groups": "Volksgruppen"
|
||||
"People groups": "Volksgruppen",
|
||||
"People Group": "Volksgruppe",
|
||||
"Basis": "Bezugsebene",
|
||||
"Bridge ISO": "Brücken-ISO",
|
||||
"Bridge language": "Brückensprache",
|
||||
"Film": "Film",
|
||||
"Groups with nothing": "Gruppen ohne etwas",
|
||||
"People with nothing": "Menschen ohne etwas",
|
||||
"Pools checked": "Geprüfte Quellen",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Brückenkandidat",
|
||||
"Bridge film": "Brücken-Film",
|
||||
"Bridge scripture": "Brücken-Bibeltext",
|
||||
"Bridge strength": "Brückenstärke",
|
||||
"Est. deaths/day": "Geschätzte Todesfälle/Tag",
|
||||
"Film link": "Film-Link",
|
||||
"Full Bible": "Ganze Bibel",
|
||||
"NO RESOURCE": "KEINE RESSOURCE",
|
||||
"Nobody praying": "Niemand betet",
|
||||
"People": "Menschen",
|
||||
"Pools NOT checked": "NICHT geprüfte Quellen",
|
||||
"Scripture link": "Bibeltext-Link",
|
||||
"Sign language": "Gebärdensprache",
|
||||
"Status": "Status",
|
||||
"Training": "Jüngerschaftstraining",
|
||||
"% with nothing": "% ohne etwas",
|
||||
"ALIGNED": "ALLE BEDINGUNGEN ERFÜLLT",
|
||||
"Audio link": "Audio-Link",
|
||||
"Band": "Stufe",
|
||||
"Bible stories": "Bibelgeschichten",
|
||||
"Can already hear": "Können schon hören",
|
||||
"Candidate % of country": "Kandidat % des Landes",
|
||||
"Candidate ISO": "Kandidaten-ISO",
|
||||
"Candidate is official": "Kandidat ist Amtssprache",
|
||||
"Carries a resource": "Hat eine Ressource",
|
||||
"Channel": "Kanal",
|
||||
"Channel source": "Kanalquelle",
|
||||
"Code": "Code",
|
||||
"Code note": "Code-Hinweis",
|
||||
"Code unlookupable": "Code in keiner Quelle indexiert",
|
||||
"Code unmatchable": "Code nicht zuordenbar",
|
||||
"Deaf bridge": "Gehörlosen-Brücke",
|
||||
"Deaf bridge link": "Link Gehörlosen-Brücke",
|
||||
"Deaf bridge publisher": "Herausgeber Gehörlosen-Brücke",
|
||||
"Deaf programme in country": "Gehörlosenprogramm im Land",
|
||||
"Death rate /1000": "Sterberate /1000",
|
||||
"Est. deaths/yr": "Geschätzte Todesfälle/Jahr",
|
||||
"Example group": "Beispielgruppe",
|
||||
"Film in this language": "Film in dieser Sprache",
|
||||
"GAP": "LÜCKE",
|
||||
"GROWING + UNREACHED": "WACHSEND + UNERREICHT",
|
||||
"Gospel audio": "Evangeliums-Audio",
|
||||
"Groups": "Gruppen",
|
||||
"Groups sharing bridge": "Gruppen mit gleicher Brücke",
|
||||
"Groups speaking": "Sprechende Gruppen",
|
||||
"Groups speaking it": "Gruppen, die sie sprechen",
|
||||
"Groups the map link shows": "Gruppen im Karten-Link",
|
||||
"Groups with Scripture": "Gruppen mit Bibeltext",
|
||||
"Groups with film": "Gruppen mit Film",
|
||||
"Growing & unreached": "Wachsend & unerreicht",
|
||||
"Growth %/yr": "Wachstum %/Jahr",
|
||||
"Has anything": "Hat etwas",
|
||||
"Heart film": "Film in eigener Sprache",
|
||||
"Heart language": "Eigene Sprache",
|
||||
"Heart scripture": "Bibeltext in eigener Sprache",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Ihre Bevölkerung",
|
||||
"JESUS Film": "JESUS Film",
|
||||
"Kind of gap": "Art der Lücke",
|
||||
"Kind of way in": "Art des Zugangs",
|
||||
"Languages": "Sprachen",
|
||||
"Languages here": "Sprachen hier",
|
||||
"Languages they speak": "Ihre Sprachen",
|
||||
"Largest group": "Größte Gruppe",
|
||||
"Largest groups": "Größte Gruppen",
|
||||
"Largest with nothing": "Größte ohne etwas",
|
||||
"Lead status": "Status des Hinweises",
|
||||
"Link": "Link",
|
||||
"Method": "Methode",
|
||||
"NT": "NT",
|
||||
"Note": "Hinweis",
|
||||
"OPPORTUNITY": "CHANCE",
|
||||
"Of those with a prayer page": "Davon mit Gebetsseite",
|
||||
"On the map": "Auf der Karte",
|
||||
"Open": "Öffnen",
|
||||
"People groups here": "Volksgruppen hier",
|
||||
"People nobody is praying for": "Menschen, für die niemand betet",
|
||||
"People praying": "Betende",
|
||||
"Place": "Ort",
|
||||
"Placement approximate": "Verortung ungefähr",
|
||||
"Pools not checked": "Nicht geprüfte Quellen",
|
||||
"Pray": "Beten",
|
||||
"Primary religion": "Hauptreligion",
|
||||
"Province": "Provinz",
|
||||
"Rank": "Rang",
|
||||
"Resource": "Ressource",
|
||||
"Scope": "Umfang",
|
||||
"Scripture in this language": "Bibeltext in dieser Sprache",
|
||||
"Signed film": "Film in Gebärdensprache",
|
||||
"Signed film link": "Link Film in Gebärdensprache",
|
||||
"Source": "Quelle",
|
||||
"Speakers": "Sprecher",
|
||||
"Their language": "Ihre Sprache",
|
||||
"These peoples on the map": "Diese Völker auf der Karte",
|
||||
"Training in the bridge": "Training in der Brückensprache",
|
||||
"Translated by": "Übersetzt von",
|
||||
"Type": "Typ",
|
||||
"UPG population": "Bevölkerung unerreichter Volksgruppen",
|
||||
"Urban %": "Städtisch %",
|
||||
"Ways in": "Zugänge",
|
||||
"Ways in (country-level)": "Zugänge (Landesebene)",
|
||||
"Year": "Jahr"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Doxa-Volksgruppen — worin das Evangelium verfügbar ist und was fehlt",
|
||||
"links": "Evangeliums-Ressourcen — jeder Link und wen er erreicht",
|
||||
"nothing": "Wo nichts gefunden wurde",
|
||||
"countrygap": "Die Lücke nach Land",
|
||||
"translation": "Übersetzungspriorität — können hören, kein Text gefunden",
|
||||
"translationcountry": "Übersetzungspriorität nach Land — können hören, kein Text gefunden",
|
||||
"gaps": "Lücken und Chancen",
|
||||
"bridges": "Chancen für Stadt-Brücken",
|
||||
"deaf": "Die Gehörlosen-Lücke",
|
||||
"resources": "Volksgruppen × Evangeliums-Ressourcen",
|
||||
"languages": "Sprachen nach Land (nach Rang) x Ressourcen",
|
||||
"growth": "Zugang zum Evangelium vs. Bevölkerungswachstum",
|
||||
"province": "Zugang zum Evangelium nach Provinz — Indien und China",
|
||||
"bridgereach": "Eine Sprache, wie viele Völker — der Zugang, nach Reichweite geordnet"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Bibeltext (beliebig)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "Meine UPG-100-Liste — Gebetslisten und Poster",
|
||||
"upg100WallTag": "Gebetsmobilisierung",
|
||||
"upg100WallIntro": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie <strong>mit dem Namen ihrer Volksgruppe</strong> beten.",
|
||||
"upg100WallIntroA": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie ",
|
||||
"upg100WallIntroEmph": "mit dem Namen ihrer Volksgruppe",
|
||||
"upg100WallIntroB": " beten.",
|
||||
"loadingNamed": "{name} wird geladen…",
|
||||
"couldNotLoadNamed": "{name} konnte nicht geladen werden.",
|
||||
"engineMissing": "Die Karten-Engine {engine} wurde nicht geladen.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "People praying",
|
||||
"selected_first": "Selected first",
|
||||
"selected_first_hint": "Float the selected people groups to the top, keeping your column sort",
|
||||
"n_shown": "{n} shown"
|
||||
"n_shown": "{n} shown",
|
||||
"other": "Other"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Bible video",
|
||||
"Gospel presentation": "Gospel presentation",
|
||||
"Countries": "Countries",
|
||||
"People groups": "People groups"
|
||||
"People groups": "People groups",
|
||||
"People Group": "People Group",
|
||||
"Basis": "Basis",
|
||||
"Bridge ISO": "Bridge ISO",
|
||||
"Bridge language": "Bridge language",
|
||||
"Film": "Film",
|
||||
"Groups with nothing": "Groups with nothing",
|
||||
"People with nothing": "People with nothing",
|
||||
"Pools checked": "Pools checked",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Bridge candidate",
|
||||
"Bridge film": "Bridge film",
|
||||
"Bridge scripture": "Bridge scripture",
|
||||
"Bridge strength": "Bridge strength",
|
||||
"Est. deaths/day": "Est. deaths/day",
|
||||
"Film link": "Film link",
|
||||
"Full Bible": "Full Bible",
|
||||
"NO RESOURCE": "NO RESOURCE",
|
||||
"Nobody praying": "Nobody praying",
|
||||
"People": "People",
|
||||
"Pools NOT checked": "Pools NOT checked",
|
||||
"Scripture link": "Scripture link",
|
||||
"Sign language": "Sign language",
|
||||
"Status": "Status",
|
||||
"Training": "Training",
|
||||
"% with nothing": "% with nothing",
|
||||
"ALIGNED": "ALIGNED",
|
||||
"Audio link": "Audio link",
|
||||
"Band": "Band",
|
||||
"Bible stories": "Bible stories",
|
||||
"Can already hear": "Can already hear",
|
||||
"Candidate % of country": "Candidate % of country",
|
||||
"Candidate ISO": "Candidate ISO",
|
||||
"Candidate is official": "Candidate is official",
|
||||
"Carries a resource": "Carries a resource",
|
||||
"Channel": "Channel",
|
||||
"Channel source": "Channel source",
|
||||
"Code": "Code",
|
||||
"Code note": "Code note",
|
||||
"Code unlookupable": "Code unlookupable",
|
||||
"Code unmatchable": "Code unmatchable",
|
||||
"Deaf bridge": "Deaf bridge",
|
||||
"Deaf bridge link": "Deaf bridge link",
|
||||
"Deaf bridge publisher": "Deaf bridge publisher",
|
||||
"Deaf programme in country": "Deaf programme in country",
|
||||
"Death rate /1000": "Death rate /1000",
|
||||
"Est. deaths/yr": "Est. deaths/yr",
|
||||
"Example group": "Example group",
|
||||
"Film in this language": "Film in this language",
|
||||
"GAP": "GAP",
|
||||
"GROWING + UNREACHED": "GROWING + UNREACHED",
|
||||
"Gospel audio": "Gospel audio",
|
||||
"Groups": "Groups",
|
||||
"Groups sharing bridge": "Groups sharing bridge",
|
||||
"Groups speaking": "Groups speaking",
|
||||
"Groups speaking it": "Groups speaking it",
|
||||
"Groups the map link shows": "Groups the map link shows",
|
||||
"Groups with Scripture": "Groups with Scripture",
|
||||
"Groups with film": "Groups with film",
|
||||
"Growing & unreached": "Growing & unreached",
|
||||
"Growth %/yr": "Growth %/yr",
|
||||
"Has anything": "Has anything",
|
||||
"Heart film": "Heart film",
|
||||
"Heart language": "Heart language",
|
||||
"Heart scripture": "Heart scripture",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Its population",
|
||||
"JESUS Film": "JESUS Film",
|
||||
"Kind of gap": "Kind of gap",
|
||||
"Kind of way in": "Kind of way in",
|
||||
"Languages": "Languages",
|
||||
"Languages here": "Languages here",
|
||||
"Languages they speak": "Languages they speak",
|
||||
"Largest group": "Largest group",
|
||||
"Largest groups": "Largest groups",
|
||||
"Largest with nothing": "Largest with nothing",
|
||||
"Lead status": "Lead status",
|
||||
"Link": "Link",
|
||||
"Method": "Method",
|
||||
"NT": "NT",
|
||||
"Note": "Note",
|
||||
"OPPORTUNITY": "OPPORTUNITY",
|
||||
"Of those with a prayer page": "Of those with a prayer page",
|
||||
"On the map": "On the map",
|
||||
"Open": "Open",
|
||||
"People groups here": "People groups here",
|
||||
"People nobody is praying for": "People nobody is praying for",
|
||||
"People praying": "People praying",
|
||||
"Place": "Place",
|
||||
"Placement approximate": "Placement approximate",
|
||||
"Pools not checked": "Pools not checked",
|
||||
"Pray": "Pray",
|
||||
"Primary religion": "Primary religion",
|
||||
"Province": "Province",
|
||||
"Rank": "Rank",
|
||||
"Resource": "Resource",
|
||||
"Scope": "Scope",
|
||||
"Scripture in this language": "Scripture in this language",
|
||||
"Signed film": "Signed film",
|
||||
"Signed film link": "Signed film link",
|
||||
"Source": "Source",
|
||||
"Speakers": "Speakers",
|
||||
"Their language": "Their language",
|
||||
"These peoples on the map": "These peoples on the map",
|
||||
"Training in the bridge": "Training in the bridge",
|
||||
"Translated by": "Translated by",
|
||||
"Type": "Type",
|
||||
"UPG population": "UPG population",
|
||||
"Urban %": "Urban %",
|
||||
"Ways in": "Ways in",
|
||||
"Ways in (country-level)": "Ways in (country-level)",
|
||||
"Year": "Year"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Doxa people groups — what the gospel is available in, and what is missing",
|
||||
"links": "Gospel Resources — every link, and who it reaches",
|
||||
"nothing": "Where Nothing Was Found",
|
||||
"countrygap": "The gap by country",
|
||||
"translation": "Translation priority — can hear it, no text found",
|
||||
"translationcountry": "Translation priority by country — can hear it, no text found",
|
||||
"gaps": "Gaps and Opportunities",
|
||||
"bridges": "City Bridge Opportunities",
|
||||
"deaf": "The Deaf Gap",
|
||||
"resources": "People Groups × Gospel Resources",
|
||||
"languages": "Languages by Country (ranked) x Resources",
|
||||
"growth": "Gospel Access vs Population Growth",
|
||||
"province": "Gospel access by province — India and China",
|
||||
"bridgereach": "One language, how many peoples — the way in, ranked by reach"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Scripture (any)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "My UPG 100 List — Prayer Lists & Posters",
|
||||
"upg100WallTag": "Prayer Mobilization",
|
||||
"upg100WallIntro": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them <strong>by their people group name</strong>.",
|
||||
"upg100WallIntroA": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them ",
|
||||
"upg100WallIntroEmph": "by their people group name",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Loading {name}…",
|
||||
"couldNotLoadNamed": "Could not load {name}.",
|
||||
"engineMissing": "The {engine} map engine did not load.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Personas que oran",
|
||||
"selected_first": "Los seleccionados primero",
|
||||
"selected_first_hint": "Llevar arriba los grupos de personas seleccionados, manteniendo el orden de las columnas",
|
||||
"n_shown": "{n} mostrados"
|
||||
"n_shown": "{n} mostrados",
|
||||
"other": "Otros"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Video bíblico",
|
||||
"Gospel presentation": "Presentación del evangelio",
|
||||
"Countries": "Países",
|
||||
"People groups": "Pueblos"
|
||||
"People groups": "Pueblos",
|
||||
"People Group": "Pueblo",
|
||||
"Basis": "Basado en",
|
||||
"Bridge ISO": "ISO puente",
|
||||
"Bridge language": "Lengua puente",
|
||||
"Film": "Película",
|
||||
"Groups with nothing": "Pueblos sin nada",
|
||||
"People with nothing": "Personas sin nada",
|
||||
"Pools checked": "Fuentes revisadas",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Candidata a puente",
|
||||
"Bridge film": "Película en lengua puente",
|
||||
"Bridge scripture": "Escrituras en lengua puente",
|
||||
"Bridge strength": "Fuerza del puente",
|
||||
"Est. deaths/day": "Muertes estimadas/día",
|
||||
"Film link": "Enlace de película",
|
||||
"Full Bible": "Biblia completa",
|
||||
"NO RESOURCE": "NINGÚN RECURSO",
|
||||
"Nobody praying": "Nadie ora",
|
||||
"People": "Personas",
|
||||
"Pools NOT checked": "Fuentes NO revisadas",
|
||||
"Scripture link": "Enlace de Escrituras",
|
||||
"Sign language": "Lengua de señas",
|
||||
"Status": "Estado",
|
||||
"Training": "Formación de discipulado",
|
||||
"% with nothing": "% sin nada",
|
||||
"ALIGNED": "CONDICIONES CUMPLIDAS",
|
||||
"Audio link": "Enlace de audio",
|
||||
"Band": "Nivel",
|
||||
"Bible stories": "Historias bíblicas",
|
||||
"Can already hear": "Ya pueden oír",
|
||||
"Candidate % of country": "Candidata % del país",
|
||||
"Candidate ISO": "ISO candidata",
|
||||
"Candidate is official": "Candidata es oficial",
|
||||
"Carries a resource": "Tiene un recurso",
|
||||
"Channel": "Canal",
|
||||
"Channel source": "Fuente del canal",
|
||||
"Code": "Código",
|
||||
"Code note": "Nota del código",
|
||||
"Code unlookupable": "Código sin índice en ninguna fuente",
|
||||
"Code unmatchable": "Código sin correspondencia",
|
||||
"Deaf bridge": "Puente para sordos",
|
||||
"Deaf bridge link": "Enlace del puente para sordos",
|
||||
"Deaf bridge publisher": "Editor del puente para sordos",
|
||||
"Deaf programme in country": "Programa para sordos en el país",
|
||||
"Death rate /1000": "Tasa de mortalidad /1000",
|
||||
"Est. deaths/yr": "Muertes estimadas/año",
|
||||
"Example group": "Pueblo de ejemplo",
|
||||
"Film in this language": "Película en este idioma",
|
||||
"GAP": "BRECHA",
|
||||
"GROWING + UNREACHED": "EN CRECIMIENTO + NO ALCANZADO",
|
||||
"Gospel audio": "Audio del evangelio",
|
||||
"Groups": "Pueblos",
|
||||
"Groups sharing bridge": "Pueblos que comparten puente",
|
||||
"Groups speaking": "Pueblos hablantes",
|
||||
"Groups speaking it": "Pueblos que la hablan",
|
||||
"Groups the map link shows": "Pueblos que muestra el enlace del mapa",
|
||||
"Groups with Scripture": "Pueblos con Escrituras",
|
||||
"Groups with film": "Pueblos con película",
|
||||
"Growing & unreached": "En crecimiento y no alcanzados",
|
||||
"Growth %/yr": "Crecimiento %/año",
|
||||
"Has anything": "Tiene algo",
|
||||
"Heart film": "Película en idioma propio",
|
||||
"Heart language": "Idioma propio",
|
||||
"Heart scripture": "Escrituras en idioma propio",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Su población",
|
||||
"JESUS Film": "Película JESÚS",
|
||||
"Kind of gap": "Tipo de brecha",
|
||||
"Kind of way in": "Tipo de vía de acceso",
|
||||
"Languages": "Idiomas",
|
||||
"Languages here": "Idiomas aquí",
|
||||
"Languages they speak": "Idiomas que hablan",
|
||||
"Largest group": "Pueblo más grande",
|
||||
"Largest groups": "Pueblos más grandes",
|
||||
"Largest with nothing": "Más grande sin nada",
|
||||
"Lead status": "Estado de la pista",
|
||||
"Link": "Enlace",
|
||||
"Method": "Método",
|
||||
"NT": "NT",
|
||||
"Note": "Nota",
|
||||
"OPPORTUNITY": "OPORTUNIDAD",
|
||||
"Of those with a prayer page": "De los que tienen página de oración",
|
||||
"On the map": "En el mapa",
|
||||
"Open": "Abrir",
|
||||
"People groups here": "Pueblos aquí",
|
||||
"People nobody is praying for": "Personas por las que nadie ora",
|
||||
"People praying": "Personas orando",
|
||||
"Place": "Lugar",
|
||||
"Placement approximate": "Ubicación aproximada",
|
||||
"Pools not checked": "Fuentes no revisadas",
|
||||
"Pray": "Orar",
|
||||
"Primary religion": "Religión principal",
|
||||
"Province": "Provincia",
|
||||
"Rank": "Posición",
|
||||
"Resource": "Recurso",
|
||||
"Scope": "Extensión",
|
||||
"Scripture in this language": "Escrituras en este idioma",
|
||||
"Signed film": "Película en lengua de señas",
|
||||
"Signed film link": "Enlace de película en lengua de señas",
|
||||
"Source": "Fuente",
|
||||
"Speakers": "Hablantes",
|
||||
"Their language": "Su idioma",
|
||||
"These peoples on the map": "Estos pueblos en el mapa",
|
||||
"Training in the bridge": "Formación en la lengua puente",
|
||||
"Translated by": "Traducido por",
|
||||
"Type": "Tipo",
|
||||
"UPG population": "Población de pueblos no alcanzados",
|
||||
"Urban %": "Urbano %",
|
||||
"Ways in": "Vías de acceso",
|
||||
"Ways in (country-level)": "Vías de acceso (nivel país)",
|
||||
"Year": "Año"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Pueblos de Doxa — en qué está disponible el evangelio y qué falta",
|
||||
"links": "Recursos del evangelio — cada enlace y a quién alcanza",
|
||||
"nothing": "Donde no se encontró nada",
|
||||
"countrygap": "La brecha por país",
|
||||
"translation": "Prioridad de traducción — pueden oírlo, no se encontró texto",
|
||||
"translationcountry": "Prioridad de traducción por país — pueden oírlo, no se encontró texto",
|
||||
"gaps": "Brechas y oportunidades",
|
||||
"bridges": "Oportunidades de puentes urbanos",
|
||||
"deaf": "La brecha sorda",
|
||||
"resources": "Pueblos × recursos del evangelio",
|
||||
"languages": "Idiomas por país (clasificados) x recursos",
|
||||
"growth": "Acceso al evangelio vs crecimiento de la población",
|
||||
"province": "Acceso al Evangelio por provincia — India y China",
|
||||
"bridgereach": "Una lengua, cuántos pueblos — la vía de acceso, ordenada por alcance"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Escrituras (cualquiera)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "Mi lista UPG 100 — Listas de oración y carteles",
|
||||
"upg100WallTag": "Movilización de oración",
|
||||
"upg100WallIntro": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos <strong>por el nombre de su pueblo</strong>.",
|
||||
"upg100WallIntroA": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos ",
|
||||
"upg100WallIntroEmph": "por el nombre de su pueblo",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Cargando {name}…",
|
||||
"couldNotLoadNamed": "No se pudo cargar {name}.",
|
||||
"engineMissing": "El motor de mapas {engine} no se cargó.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Personnes qui prient",
|
||||
"selected_first": "La sélection en premier",
|
||||
"selected_first_hint": "Faire remonter les groupes de personnes sélectionnés, en conservant le tri des colonnes",
|
||||
"n_shown": "{n} affichés"
|
||||
"n_shown": "{n} affichés",
|
||||
"other": "Autres"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Vidéo biblique",
|
||||
"Gospel presentation": "Présentation de l'Évangile",
|
||||
"Countries": "Pays",
|
||||
"People groups": "Peuples"
|
||||
"People groups": "Peuples",
|
||||
"People Group": "Peuple",
|
||||
"Basis": "Basé sur",
|
||||
"Bridge ISO": "ISO passerelle",
|
||||
"Bridge language": "Langue passerelle",
|
||||
"Film": "Film",
|
||||
"Groups with nothing": "Peuples sans rien",
|
||||
"People with nothing": "Personnes sans rien",
|
||||
"Pools checked": "Sources consultées",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Candidate passerelle",
|
||||
"Bridge film": "Film en langue passerelle",
|
||||
"Bridge scripture": "Écritures en langue passerelle",
|
||||
"Bridge strength": "Force de la passerelle",
|
||||
"Est. deaths/day": "Décès estimés/jour",
|
||||
"Film link": "Lien du film",
|
||||
"Full Bible": "Bible complète",
|
||||
"NO RESOURCE": "AUCUNE RESSOURCE",
|
||||
"Nobody praying": "Personne ne prie",
|
||||
"People": "Personnes",
|
||||
"Pools NOT checked": "Sources NON consultées",
|
||||
"Scripture link": "Lien des Écritures",
|
||||
"Sign language": "Langue des signes",
|
||||
"Status": "Statut",
|
||||
"Training": "Formation au discipulat",
|
||||
"% with nothing": "% sans rien",
|
||||
"ALIGNED": "CONDITIONS RÉUNIES",
|
||||
"Audio link": "Lien audio",
|
||||
"Band": "Niveau",
|
||||
"Bible stories": "Histoires bibliques",
|
||||
"Can already hear": "Entendent déjà",
|
||||
"Candidate % of country": "Candidate % du pays",
|
||||
"Candidate ISO": "ISO candidate",
|
||||
"Candidate is official": "Candidate officielle",
|
||||
"Carries a resource": "A une ressource",
|
||||
"Channel": "Canal",
|
||||
"Channel source": "Source du canal",
|
||||
"Code": "Code",
|
||||
"Code note": "Note sur le code",
|
||||
"Code unlookupable": "Code absent de toute source",
|
||||
"Code unmatchable": "Code sans correspondance",
|
||||
"Deaf bridge": "Passerelle pour sourds",
|
||||
"Deaf bridge link": "Lien passerelle pour sourds",
|
||||
"Deaf bridge publisher": "Éditeur passerelle pour sourds",
|
||||
"Deaf programme in country": "Programme pour sourds dans le pays",
|
||||
"Death rate /1000": "Taux de mortalité /1000",
|
||||
"Est. deaths/yr": "Décès estimés/an",
|
||||
"Example group": "Peuple exemple",
|
||||
"Film in this language": "Film dans cette langue",
|
||||
"GAP": "ÉCART",
|
||||
"GROWING + UNREACHED": "EN CROISSANCE + NON ATTEINT",
|
||||
"Gospel audio": "Audio de l'Évangile",
|
||||
"Groups": "Peuples",
|
||||
"Groups sharing bridge": "Peuples partageant la passerelle",
|
||||
"Groups speaking": "Peuples locuteurs",
|
||||
"Groups speaking it": "Peuples qui la parlent",
|
||||
"Groups the map link shows": "Peuples affichés par le lien de carte",
|
||||
"Groups with Scripture": "Peuples avec Écritures",
|
||||
"Groups with film": "Peuples avec film",
|
||||
"Growing & unreached": "En croissance et non atteints",
|
||||
"Growth %/yr": "Croissance %/an",
|
||||
"Has anything": "A quelque chose",
|
||||
"Heart film": "Film en langue propre",
|
||||
"Heart language": "Langue propre",
|
||||
"Heart scripture": "Écritures en langue propre",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Sa population",
|
||||
"JESUS Film": "Film JÉSUS",
|
||||
"Kind of gap": "Type d'écart",
|
||||
"Kind of way in": "Type de porte d'entrée",
|
||||
"Languages": "Langues",
|
||||
"Languages here": "Langues ici",
|
||||
"Languages they speak": "Langues parlées",
|
||||
"Largest group": "Plus grand peuple",
|
||||
"Largest groups": "Plus grands peuples",
|
||||
"Largest with nothing": "Plus grand sans rien",
|
||||
"Lead status": "Statut de la piste",
|
||||
"Link": "Lien",
|
||||
"Method": "Méthode",
|
||||
"NT": "NT",
|
||||
"Note": "Note",
|
||||
"OPPORTUNITY": "OPPORTUNITÉ",
|
||||
"Of those with a prayer page": "Parmi ceux avec une page de prière",
|
||||
"On the map": "Sur la carte",
|
||||
"Open": "Ouvrir",
|
||||
"People groups here": "Peuples ici",
|
||||
"People nobody is praying for": "Personnes pour qui personne ne prie",
|
||||
"People praying": "Personnes qui prient",
|
||||
"Place": "Lieu",
|
||||
"Placement approximate": "Emplacement approximatif",
|
||||
"Pools not checked": "Sources non consultées",
|
||||
"Pray": "Prier",
|
||||
"Primary religion": "Religion principale",
|
||||
"Province": "Province",
|
||||
"Rank": "Rang",
|
||||
"Resource": "Ressource",
|
||||
"Scope": "Étendue",
|
||||
"Scripture in this language": "Écritures dans cette langue",
|
||||
"Signed film": "Film en langue des signes",
|
||||
"Signed film link": "Lien du film en langue des signes",
|
||||
"Source": "Source",
|
||||
"Speakers": "Locuteurs",
|
||||
"Their language": "Leur langue",
|
||||
"These peoples on the map": "Ces peuples sur la carte",
|
||||
"Training in the bridge": "Formation dans la langue passerelle",
|
||||
"Translated by": "Traduit par",
|
||||
"Type": "Type",
|
||||
"UPG population": "Population des peuples non atteints",
|
||||
"Urban %": "Urbain %",
|
||||
"Ways in": "Portes d'entrée",
|
||||
"Ways in (country-level)": "Portes d'entrée (niveau pays)",
|
||||
"Year": "Année"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Peuples Doxa — ce qui existe de l'Évangile, et ce qui manque",
|
||||
"links": "Ressources de l'Évangile — chaque lien, et qui il atteint",
|
||||
"nothing": "Où rien n'a été trouvé",
|
||||
"countrygap": "L'écart par pays",
|
||||
"translation": "Priorité de traduction — peuvent l'entendre, aucun texte trouvé",
|
||||
"translationcountry": "Priorité de traduction par pays — peuvent l'entendre, aucun texte trouvé",
|
||||
"gaps": "Écarts et opportunités",
|
||||
"bridges": "Opportunités de ponts urbains",
|
||||
"deaf": "L'écart sourd",
|
||||
"resources": "Peuples × ressources de l'Évangile",
|
||||
"languages": "Langues par pays (classées) x ressources",
|
||||
"growth": "Accès à l'Évangile vs croissance démographique",
|
||||
"province": "Accès à l'Évangile par province — Inde et Chine",
|
||||
"bridgereach": "Une langue, combien de peuples — la porte d'entrée, classée par portée"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Écritures (toutes)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "Ma liste UPG 100 — Listes de prière et affiches",
|
||||
"upg100WallTag": "Mobilisation pour la prière",
|
||||
"upg100WallIntro": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux <strong>par le nom de leur peuple</strong>.",
|
||||
"upg100WallIntroA": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux ",
|
||||
"upg100WallIntroEmph": "par le nom de leur peuple",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Chargement de {name}…",
|
||||
"couldNotLoadNamed": "Impossible de charger {name}.",
|
||||
"engineMissing": "Le moteur de carte {engine} ne s’est pas chargé.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "प्रार्थना करने वाले लोग",
|
||||
"selected_first": "चयनित पहले",
|
||||
"selected_first_hint": "चयनित जनसमूहों को ऊपर लाएँ, आपका कॉलम क्रम बनाए रखते हुए",
|
||||
"n_shown": "{n} दिखाए गए"
|
||||
"n_shown": "{n} दिखाए गए",
|
||||
"other": "अन्य"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "बाइबल वीडियो",
|
||||
"Gospel presentation": "सुसमाचार प्रस्तुति",
|
||||
"Countries": "देश",
|
||||
"People groups": "जनसमूह"
|
||||
"People groups": "जनसमूह",
|
||||
"People Group": "जनसमूह",
|
||||
"Basis": "आधार स्तर",
|
||||
"Bridge ISO": "सेतु ISO",
|
||||
"Bridge language": "सेतु भाषा",
|
||||
"Film": "फ़िल्म",
|
||||
"Groups with nothing": "कुछ नहीं वाले जनसमूह",
|
||||
"People with nothing": "कुछ नहीं वाले लोग",
|
||||
"Pools checked": "जाँचे गए स्रोत",
|
||||
"Audio": "ऑडियो",
|
||||
"Bridge candidate": "संभावित सेतु",
|
||||
"Bridge film": "सेतु फ़िल्म",
|
||||
"Bridge scripture": "सेतु पवित्र शास्त्र",
|
||||
"Bridge strength": "सेतु की मज़बूती",
|
||||
"Est. deaths/day": "अनुमानित मृत्यु/दिन",
|
||||
"Film link": "फ़िल्म लिंक",
|
||||
"Full Bible": "पूरी बाइबल",
|
||||
"NO RESOURCE": "कोई संसाधन नहीं",
|
||||
"Nobody praying": "कोई प्रार्थना नहीं कर रहा",
|
||||
"People": "लोग",
|
||||
"Pools NOT checked": "नहीं जाँचे गए स्रोत",
|
||||
"Scripture link": "पवित्र शास्त्र लिंक",
|
||||
"Sign language": "सांकेतिक भाषा",
|
||||
"Status": "स्थिति",
|
||||
"Training": "शिष्यत्व प्रशिक्षण",
|
||||
"% with nothing": "% के पास कुछ नहीं",
|
||||
"ALIGNED": "सभी शर्तें पूरी",
|
||||
"Audio link": "ऑडियो लिंक",
|
||||
"Band": "श्रेणी",
|
||||
"Bible stories": "बाइबल की कहानियाँ",
|
||||
"Can already hear": "पहले से सुन सकते हैं",
|
||||
"Candidate % of country": "संभावित भाषा देश का %",
|
||||
"Candidate ISO": "संभावित ISO",
|
||||
"Candidate is official": "संभावित आधिकारिक है",
|
||||
"Carries a resource": "संसाधन उपलब्ध",
|
||||
"Channel": "चैनल",
|
||||
"Channel source": "चैनल स्रोत",
|
||||
"Code": "कोड",
|
||||
"Code note": "कोड टिप्पणी",
|
||||
"Code unlookupable": "कोड किसी स्रोत में नहीं",
|
||||
"Code unmatchable": "कोड मेल नहीं खाता",
|
||||
"Deaf bridge": "बधिर सेतु",
|
||||
"Deaf bridge link": "बधिर सेतु लिंक",
|
||||
"Deaf bridge publisher": "बधिर सेतु प्रकाशक",
|
||||
"Deaf programme in country": "देश में बधिर कार्यक्रम",
|
||||
"Death rate /1000": "मृत्यु दर /1000",
|
||||
"Est. deaths/yr": "अनुमानित मृत्यु/वर्ष",
|
||||
"Example group": "उदाहरण जनसमूह",
|
||||
"Film in this language": "इस भाषा में फ़िल्म",
|
||||
"GAP": "कमी",
|
||||
"GROWING + UNREACHED": "बढ़ते + अनपहुँचे",
|
||||
"Gospel audio": "सुसमाचार ऑडियो",
|
||||
"Groups": "जनसमूह",
|
||||
"Groups sharing bridge": "सेतु साझा करने वाले जनसमूह",
|
||||
"Groups speaking": "बोलने वाले जनसमूह",
|
||||
"Groups speaking it": "इसे बोलने वाले जनसमूह",
|
||||
"Groups the map link shows": "मानचित्र लिंक में दिखने वाले जनसमूह",
|
||||
"Groups with Scripture": "पवित्र शास्त्र वाले जनसमूह",
|
||||
"Groups with film": "फ़िल्म वाले जनसमूह",
|
||||
"Growing & unreached": "बढ़ते और अनपहुँचे",
|
||||
"Growth %/yr": "वृद्धि %/वर्ष",
|
||||
"Has anything": "कुछ उपलब्ध",
|
||||
"Heart film": "अपनी भाषा में फ़िल्म",
|
||||
"Heart language": "अपनी भाषा",
|
||||
"Heart scripture": "अपनी भाषा में पवित्र शास्त्र",
|
||||
"Internet": "इंटरनेट",
|
||||
"Internet %": "इंटरनेट %",
|
||||
"Its population": "उसकी जनसंख्या",
|
||||
"JESUS Film": "यीशु फ़िल्म",
|
||||
"Kind of gap": "कमी का प्रकार",
|
||||
"Kind of way in": "रास्ते का प्रकार",
|
||||
"Languages": "भाषाएँ",
|
||||
"Languages here": "यहाँ की भाषाएँ",
|
||||
"Languages they speak": "उनकी भाषाएँ",
|
||||
"Largest group": "सबसे बड़ा जनसमूह",
|
||||
"Largest groups": "सबसे बड़े जनसमूह",
|
||||
"Largest with nothing": "कुछ नहीं वाला सबसे बड़ा",
|
||||
"Lead status": "सुराग की स्थिति",
|
||||
"Link": "लिंक",
|
||||
"Method": "विधि",
|
||||
"NT": "नया नियम",
|
||||
"Note": "टिप्पणी",
|
||||
"OPPORTUNITY": "अवसर",
|
||||
"Of those with a prayer page": "प्रार्थना पेज वालों में से",
|
||||
"On the map": "मानचित्र पर",
|
||||
"Open": "खोलें",
|
||||
"People groups here": "यहाँ के जनसमूह",
|
||||
"People nobody is praying for": "जिनके लिए कोई प्रार्थना नहीं कर रहा",
|
||||
"People praying": "प्रार्थना करने वाले",
|
||||
"Place": "स्थान",
|
||||
"Placement approximate": "स्थान अनुमानित",
|
||||
"Pools not checked": "नहीं जाँचे गए स्रोत",
|
||||
"Pray": "प्रार्थना करें",
|
||||
"Primary religion": "मुख्य धर्म",
|
||||
"Province": "प्रांत",
|
||||
"Rank": "क्रम",
|
||||
"Resource": "संसाधन",
|
||||
"Scope": "विस्तार",
|
||||
"Scripture in this language": "इस भाषा में पवित्र शास्त्र",
|
||||
"Signed film": "सांकेतिक फ़िल्म",
|
||||
"Signed film link": "सांकेतिक फ़िल्म लिंक",
|
||||
"Source": "स्रोत",
|
||||
"Speakers": "बोलने वाले",
|
||||
"Their language": "उनकी भाषा",
|
||||
"These peoples on the map": "मानचित्र पर ये जनसमूह",
|
||||
"Training in the bridge": "सेतु भाषा में प्रशिक्षण",
|
||||
"Translated by": "अनुवादक",
|
||||
"Type": "प्रकार",
|
||||
"UPG population": "अनपहुँचे जनसमूहों की जनसंख्या",
|
||||
"Urban %": "शहरी %",
|
||||
"Ways in": "रास्ते",
|
||||
"Ways in (country-level)": "रास्ते (देश स्तर)",
|
||||
"Year": "वर्ष"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "दोक्सा जनसमूह — सुसमाचार किसमें उपलब्ध है, और क्या कमी है",
|
||||
"links": "सुसमाचार संसाधन — हर लिंक, और वह किस तक पहुँचता है",
|
||||
"nothing": "जहाँ कुछ नहीं मिला",
|
||||
"countrygap": "देश अनुसार कमी",
|
||||
"translation": "अनुवाद प्राथमिकता — सुन सकते हैं, कोई पाठ नहीं मिला",
|
||||
"translationcountry": "देश अनुसार अनुवाद प्राथमिकता — सुन सकते हैं, कोई पाठ नहीं मिला",
|
||||
"gaps": "कमियाँ और अवसर",
|
||||
"bridges": "नगर सेतु अवसर",
|
||||
"deaf": "बधिर कमी",
|
||||
"resources": "जनसमूह × सुसमाचार संसाधन",
|
||||
"languages": "देश अनुसार भाषाएँ (क्रमबद्ध) x संसाधन",
|
||||
"growth": "सुसमाचार पहुँच बनाम जनसंख्या वृद्धि",
|
||||
"province": "प्रांत के अनुसार सुसमाचार तक पहुँच — भारत और चीन",
|
||||
"bridgereach": "एक भाषा, कितने जनसमूह — पहुँचने का रास्ता, पहुँच के क्रम में"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "पवित्र शास्त्र (कोई भी)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
|
||||
"upg100WallTag": "प्रार्थना लामबंदी",
|
||||
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।",
|
||||
"upg100WallIntroA": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए ",
|
||||
"upg100WallIntroEmph": "उनके जनसमूह के नाम से",
|
||||
"upg100WallIntroB": " प्रार्थना करते हुए।",
|
||||
"loadingNamed": "{name} लोड हो रहा है…",
|
||||
"couldNotLoadNamed": "{name} लोड नहीं हो सका।",
|
||||
"engineMissing": "{engine} नक्शा इंजन लोड नहीं हुआ।",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Persone che pregano",
|
||||
"selected_first": "Prima i selezionati",
|
||||
"selected_first_hint": "Porta in alto i gruppi di persone selezionati, mantenendo l’ordinamento delle colonne",
|
||||
"n_shown": "{n} mostrati"
|
||||
"n_shown": "{n} mostrati",
|
||||
"other": "Altri"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Video biblico",
|
||||
"Gospel presentation": "Presentazione del Vangelo",
|
||||
"Countries": "Paesi",
|
||||
"People groups": "Popoli"
|
||||
"People groups": "Popoli",
|
||||
"People Group": "Popolo",
|
||||
"Basis": "Basato su",
|
||||
"Bridge ISO": "ISO ponte",
|
||||
"Bridge language": "Lingua ponte",
|
||||
"Film": "Film",
|
||||
"Groups with nothing": "Popoli senza nulla",
|
||||
"People with nothing": "Persone senza nulla",
|
||||
"Pools checked": "Fonti controllate",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Candidata ponte",
|
||||
"Bridge film": "Film in lingua ponte",
|
||||
"Bridge scripture": "Scritture in lingua ponte",
|
||||
"Bridge strength": "Forza del ponte",
|
||||
"Est. deaths/day": "Decessi stimati/giorno",
|
||||
"Film link": "Link al film",
|
||||
"Full Bible": "Bibbia completa",
|
||||
"NO RESOURCE": "NESSUNA RISORSA",
|
||||
"Nobody praying": "Nessuno prega",
|
||||
"People": "Persone",
|
||||
"Pools NOT checked": "Fonti NON controllate",
|
||||
"Scripture link": "Link alle Scritture",
|
||||
"Sign language": "Lingua dei segni",
|
||||
"Status": "Stato",
|
||||
"Training": "Formazione al discepolato",
|
||||
"% with nothing": "% senza nulla",
|
||||
"ALIGNED": "CONDIZIONI SODDISFATTE",
|
||||
"Audio link": "Link audio",
|
||||
"Band": "Livello",
|
||||
"Bible stories": "Storie bibliche",
|
||||
"Can already hear": "Già possono ascoltare",
|
||||
"Candidate % of country": "Candidata % del paese",
|
||||
"Candidate ISO": "ISO candidata",
|
||||
"Candidate is official": "Candidata ufficiale",
|
||||
"Carries a resource": "Ha una risorsa",
|
||||
"Channel": "Canale",
|
||||
"Channel source": "Fonte del canale",
|
||||
"Code": "Codice",
|
||||
"Code note": "Nota sul codice",
|
||||
"Code unlookupable": "Codice assente da ogni fonte",
|
||||
"Code unmatchable": "Codice senza corrispondenza",
|
||||
"Deaf bridge": "Ponte per sordi",
|
||||
"Deaf bridge link": "Link ponte per sordi",
|
||||
"Deaf bridge publisher": "Editore ponte per sordi",
|
||||
"Deaf programme in country": "Programma per sordi nel paese",
|
||||
"Death rate /1000": "Tasso di mortalità /1000",
|
||||
"Est. deaths/yr": "Decessi stimati/anno",
|
||||
"Example group": "Popolo di esempio",
|
||||
"Film in this language": "Film in questa lingua",
|
||||
"GAP": "DIVARIO",
|
||||
"GROWING + UNREACHED": "IN CRESCITA + NON RAGGIUNTO",
|
||||
"Gospel audio": "Audio del Vangelo",
|
||||
"Groups": "Popoli",
|
||||
"Groups sharing bridge": "Popoli che condividono il ponte",
|
||||
"Groups speaking": "Popoli parlanti",
|
||||
"Groups speaking it": "Popoli che la parlano",
|
||||
"Groups the map link shows": "Popoli mostrati dal link alla mappa",
|
||||
"Groups with Scripture": "Popoli con Scritture",
|
||||
"Groups with film": "Popoli con film",
|
||||
"Growing & unreached": "In crescita e non raggiunti",
|
||||
"Growth %/yr": "Crescita %/anno",
|
||||
"Has anything": "Ha qualcosa",
|
||||
"Heart film": "Film nella lingua propria",
|
||||
"Heart language": "Lingua propria",
|
||||
"Heart scripture": "Scritture nella lingua propria",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Sua popolazione",
|
||||
"JESUS Film": "Film GESÙ",
|
||||
"Kind of gap": "Tipo di divario",
|
||||
"Kind of way in": "Tipo di via d'accesso",
|
||||
"Languages": "Lingue",
|
||||
"Languages here": "Lingue qui",
|
||||
"Languages they speak": "Lingue parlate",
|
||||
"Largest group": "Popolo più grande",
|
||||
"Largest groups": "Popoli più grandi",
|
||||
"Largest with nothing": "Più grande senza nulla",
|
||||
"Lead status": "Stato della pista",
|
||||
"Link": "Link",
|
||||
"Method": "Metodo",
|
||||
"NT": "NT",
|
||||
"Note": "Nota",
|
||||
"OPPORTUNITY": "OPPORTUNITÀ",
|
||||
"Of those with a prayer page": "Di quelli con una pagina di preghiera",
|
||||
"On the map": "Sulla mappa",
|
||||
"Open": "Apri",
|
||||
"People groups here": "Popoli qui",
|
||||
"People nobody is praying for": "Persone per cui nessuno prega",
|
||||
"People praying": "Persone che pregano",
|
||||
"Place": "Luogo",
|
||||
"Placement approximate": "Posizione approssimativa",
|
||||
"Pools not checked": "Fonti non controllate",
|
||||
"Pray": "Prega",
|
||||
"Primary religion": "Religione principale",
|
||||
"Province": "Provincia",
|
||||
"Rank": "Classifica",
|
||||
"Resource": "Risorsa",
|
||||
"Scope": "Estensione",
|
||||
"Scripture in this language": "Scritture in questa lingua",
|
||||
"Signed film": "Film in lingua dei segni",
|
||||
"Signed film link": "Link film in lingua dei segni",
|
||||
"Source": "Fonte",
|
||||
"Speakers": "Parlanti",
|
||||
"Their language": "La loro lingua",
|
||||
"These peoples on the map": "Questi popoli sulla mappa",
|
||||
"Training in the bridge": "Formazione nella lingua ponte",
|
||||
"Translated by": "Tradotto da",
|
||||
"Type": "Tipo",
|
||||
"UPG population": "Popolazione dei popoli non raggiunti",
|
||||
"Urban %": "Urbano %",
|
||||
"Ways in": "Vie d'accesso",
|
||||
"Ways in (country-level)": "Vie d'accesso (livello paese)",
|
||||
"Year": "Anno"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Popoli Doxa — in cosa è disponibile il Vangelo, e cosa manca",
|
||||
"links": "Risorse del Vangelo — ogni link, e chi raggiunge",
|
||||
"nothing": "Dove non si è trovato nulla",
|
||||
"countrygap": "Il divario per paese",
|
||||
"translation": "Priorità di traduzione — possono ascoltarlo, nessun testo trovato",
|
||||
"translationcountry": "Priorità di traduzione per paese — possono ascoltarlo, nessun testo trovato",
|
||||
"gaps": "Divari e opportunità",
|
||||
"bridges": "Opportunità di ponti urbani",
|
||||
"deaf": "Il divario sordo",
|
||||
"resources": "Popoli × risorse del Vangelo",
|
||||
"languages": "Lingue per paese (in classifica) x risorse",
|
||||
"growth": "Accesso al Vangelo vs crescita della popolazione",
|
||||
"province": "Accesso al Vangelo per provincia — India e Cina",
|
||||
"bridgereach": "Una lingua, quanti popoli — la via d'accesso, ordinata per portata"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Scritture (qualsiasi)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "La mia lista UPG 100 — Liste di preghiera e poster",
|
||||
"upg100WallTag": "Mobilitazione alla preghiera",
|
||||
"upg100WallIntro": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro <strong>con il nome del loro popolo</strong>.",
|
||||
"upg100WallIntroA": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro ",
|
||||
"upg100WallIntroEmph": "con il nome del loro popolo",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Caricamento di {name}…",
|
||||
"couldNotLoadNamed": "Impossibile caricare {name}.",
|
||||
"engineMissing": "Il motore di mappa {engine} non è stato caricato.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Pessoas a orar",
|
||||
"selected_first": "Os selecionados primeiro",
|
||||
"selected_first_hint": "Trazer para cima os grupos de pessoas selecionados, mantendo a ordenação das colunas",
|
||||
"n_shown": "{n} apresentados"
|
||||
"n_shown": "{n} apresentados",
|
||||
"other": "Outros"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Vídeo bíblico",
|
||||
"Gospel presentation": "Apresentação do evangelho",
|
||||
"Countries": "Países",
|
||||
"People groups": "Povos"
|
||||
"People groups": "Povos",
|
||||
"People Group": "Povo",
|
||||
"Basis": "Com base em",
|
||||
"Bridge ISO": "ISO da ponte",
|
||||
"Bridge language": "Língua-ponte",
|
||||
"Film": "Filme",
|
||||
"Groups with nothing": "Povos sem nada",
|
||||
"People with nothing": "Pessoas sem nada",
|
||||
"Pools checked": "Fontes verificadas",
|
||||
"Audio": "Áudio",
|
||||
"Bridge candidate": "Candidata a ponte",
|
||||
"Bridge film": "Filme na língua-ponte",
|
||||
"Bridge scripture": "Escrituras na língua-ponte",
|
||||
"Bridge strength": "Força da ponte",
|
||||
"Est. deaths/day": "Mortes estimadas/dia",
|
||||
"Film link": "Link do filme",
|
||||
"Full Bible": "Bíblia completa",
|
||||
"NO RESOURCE": "NENHUM RECURSO",
|
||||
"Nobody praying": "Ninguém orando",
|
||||
"People": "Pessoas",
|
||||
"Pools NOT checked": "Fontes NÃO verificadas",
|
||||
"Scripture link": "Link das Escrituras",
|
||||
"Sign language": "Língua de sinais",
|
||||
"Status": "Status",
|
||||
"Training": "Treinamento de discipulado",
|
||||
"% with nothing": "% sem nada",
|
||||
"ALIGNED": "CONDIÇÕES ATENDIDAS",
|
||||
"Audio link": "Link do áudio",
|
||||
"Band": "Nível",
|
||||
"Bible stories": "Histórias bíblicas",
|
||||
"Can already hear": "Já podem ouvir",
|
||||
"Candidate % of country": "Candidata % do país",
|
||||
"Candidate ISO": "ISO da candidata",
|
||||
"Candidate is official": "Candidata é oficial",
|
||||
"Carries a resource": "Tem um recurso",
|
||||
"Channel": "Canal",
|
||||
"Channel source": "Fonte do canal",
|
||||
"Code": "Código",
|
||||
"Code note": "Nota do código",
|
||||
"Code unlookupable": "Código em nenhuma fonte",
|
||||
"Code unmatchable": "Código sem correspondência",
|
||||
"Deaf bridge": "Ponte para surdos",
|
||||
"Deaf bridge link": "Link da ponte para surdos",
|
||||
"Deaf bridge publisher": "Editora da ponte para surdos",
|
||||
"Deaf programme in country": "Programa para surdos no país",
|
||||
"Death rate /1000": "Taxa de mortalidade /1000",
|
||||
"Est. deaths/yr": "Mortes estimadas/ano",
|
||||
"Example group": "Povo de exemplo",
|
||||
"Film in this language": "Filme neste idioma",
|
||||
"GAP": "LACUNA",
|
||||
"GROWING + UNREACHED": "EM CRESCIMENTO + NÃO ALCANÇADO",
|
||||
"Gospel audio": "Áudio do evangelho",
|
||||
"Groups": "Povos",
|
||||
"Groups sharing bridge": "Povos que compartilham a ponte",
|
||||
"Groups speaking": "Povos falantes",
|
||||
"Groups speaking it": "Povos que a falam",
|
||||
"Groups the map link shows": "Povos mostrados pelo link do mapa",
|
||||
"Groups with Scripture": "Povos com Escrituras",
|
||||
"Groups with film": "Povos com filme",
|
||||
"Growing & unreached": "Em crescimento e não alcançados",
|
||||
"Growth %/yr": "Crescimento %/ano",
|
||||
"Has anything": "Tem algo",
|
||||
"Heart film": "Filme no idioma próprio",
|
||||
"Heart language": "Idioma próprio",
|
||||
"Heart scripture": "Escrituras no idioma próprio",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Sua população",
|
||||
"JESUS Film": "Filme JESUS",
|
||||
"Kind of gap": "Tipo de lacuna",
|
||||
"Kind of way in": "Tipo de porta de entrada",
|
||||
"Languages": "Idiomas",
|
||||
"Languages here": "Idiomas aqui",
|
||||
"Languages they speak": "Idiomas que falam",
|
||||
"Largest group": "Maior povo",
|
||||
"Largest groups": "Maiores povos",
|
||||
"Largest with nothing": "Maior sem nada",
|
||||
"Lead status": "Status da pista",
|
||||
"Link": "Link",
|
||||
"Method": "Método",
|
||||
"NT": "NT",
|
||||
"Note": "Nota",
|
||||
"OPPORTUNITY": "OPORTUNIDADE",
|
||||
"Of those with a prayer page": "Dos que têm página de oração",
|
||||
"On the map": "No mapa",
|
||||
"Open": "Abrir",
|
||||
"People groups here": "Povos aqui",
|
||||
"People nobody is praying for": "Pessoas por quem ninguém ora",
|
||||
"People praying": "Pessoas orando",
|
||||
"Place": "Local",
|
||||
"Placement approximate": "Localização aproximada",
|
||||
"Pools not checked": "Fontes não verificadas",
|
||||
"Pray": "Orar",
|
||||
"Primary religion": "Religião principal",
|
||||
"Province": "Província",
|
||||
"Rank": "Posição",
|
||||
"Resource": "Recurso",
|
||||
"Scope": "Abrangência",
|
||||
"Scripture in this language": "Escrituras neste idioma",
|
||||
"Signed film": "Filme em língua de sinais",
|
||||
"Signed film link": "Link do filme em língua de sinais",
|
||||
"Source": "Fonte",
|
||||
"Speakers": "Falantes",
|
||||
"Their language": "Seu idioma",
|
||||
"These peoples on the map": "Estes povos no mapa",
|
||||
"Training in the bridge": "Treinamento na língua-ponte",
|
||||
"Translated by": "Traduzido por",
|
||||
"Type": "Tipo",
|
||||
"UPG population": "População dos povos não alcançados",
|
||||
"Urban %": "Urbano %",
|
||||
"Ways in": "Portas de entrada",
|
||||
"Ways in (country-level)": "Portas de entrada (nível país)",
|
||||
"Year": "Ano"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Povos Doxa — em que o evangelho está disponível, e o que falta",
|
||||
"links": "Recursos do evangelho — cada link, e quem ele alcança",
|
||||
"nothing": "Onde nada foi encontrado",
|
||||
"countrygap": "A lacuna por país",
|
||||
"translation": "Prioridade de tradução — podem ouvir, nenhum texto encontrado",
|
||||
"translationcountry": "Prioridade de tradução por país — podem ouvir, nenhum texto encontrado",
|
||||
"gaps": "Lacunas e oportunidades",
|
||||
"bridges": "Oportunidades de pontes urbanas",
|
||||
"deaf": "A lacuna surda",
|
||||
"resources": "Povos × recursos do evangelho",
|
||||
"languages": "Idiomas por país (classificados) x recursos",
|
||||
"growth": "Acesso ao evangelho vs crescimento populacional",
|
||||
"province": "Acesso ao Evangelho por província — Índia e China",
|
||||
"bridgereach": "Uma língua, quantos povos — a porta de entrada, ordenada por alcance"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Escrituras (qualquer)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "A minha lista UPG 100 — Listas de oração e cartazes",
|
||||
"upg100WallTag": "Mobilização de oração",
|
||||
"upg100WallIntro": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles <strong>pelo nome do seu povo</strong>.",
|
||||
"upg100WallIntroA": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles ",
|
||||
"upg100WallIntroEmph": "pelo nome do seu povo",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Carregando {name}…",
|
||||
"couldNotLoadNamed": "Não foi possível carregar {name}.",
|
||||
"engineMissing": "O motor de mapa {engine} não foi carregado.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Persoane care se roagă",
|
||||
"selected_first": "Cele selectate primele",
|
||||
"selected_first_hint": "Ridică grupurile de oameni selectate în partea de sus, păstrând sortarea coloanelor",
|
||||
"n_shown": "{n} afișate"
|
||||
"n_shown": "{n} afișate",
|
||||
"other": "Altele"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Video biblic",
|
||||
"Gospel presentation": "Prezentarea Evangheliei",
|
||||
"Countries": "Țări",
|
||||
"People groups": "Popoare"
|
||||
"People groups": "Popoare",
|
||||
"People Group": "Popor",
|
||||
"Basis": "Pe baza",
|
||||
"Bridge ISO": "ISO punte",
|
||||
"Bridge language": "Limbă-punte",
|
||||
"Film": "Film",
|
||||
"Groups with nothing": "Popoare fără nimic",
|
||||
"People with nothing": "Persoane fără nimic",
|
||||
"Pools checked": "Surse verificate",
|
||||
"Audio": "Audio",
|
||||
"Bridge candidate": "Candidată punte",
|
||||
"Bridge film": "Film în limba-punte",
|
||||
"Bridge scripture": "Scripturi în limba-punte",
|
||||
"Bridge strength": "Puterea punții",
|
||||
"Est. deaths/day": "Decese estimate/zi",
|
||||
"Film link": "Link film",
|
||||
"Full Bible": "Biblia completă",
|
||||
"NO RESOURCE": "NICIO RESURSĂ",
|
||||
"Nobody praying": "Nimeni nu se roagă",
|
||||
"People": "Persoane",
|
||||
"Pools NOT checked": "Surse NEVERIFICATE",
|
||||
"Scripture link": "Link Scripturi",
|
||||
"Sign language": "Limba semnelor",
|
||||
"Status": "Stare",
|
||||
"Training": "Instruire în ucenicie",
|
||||
"% with nothing": "% fără nimic",
|
||||
"ALIGNED": "CONDIȚII ÎNDEPLINITE",
|
||||
"Audio link": "Link audio",
|
||||
"Band": "Nivel",
|
||||
"Bible stories": "Povestiri biblice",
|
||||
"Can already hear": "Pot deja auzi",
|
||||
"Candidate % of country": "Candidată % din țară",
|
||||
"Candidate ISO": "ISO candidată",
|
||||
"Candidate is official": "Candidată e oficială",
|
||||
"Carries a resource": "Are o resursă",
|
||||
"Channel": "Canal",
|
||||
"Channel source": "Sursa canalului",
|
||||
"Code": "Cod",
|
||||
"Code note": "Notă cod",
|
||||
"Code unlookupable": "Cod absent din orice sursă",
|
||||
"Code unmatchable": "Cod fără corespondență",
|
||||
"Deaf bridge": "Punte pentru surzi",
|
||||
"Deaf bridge link": "Link punte pentru surzi",
|
||||
"Deaf bridge publisher": "Editor punte pentru surzi",
|
||||
"Deaf programme in country": "Program pentru surzi în țară",
|
||||
"Death rate /1000": "Rata mortalității /1000",
|
||||
"Est. deaths/yr": "Decese estimate/an",
|
||||
"Example group": "Popor exemplu",
|
||||
"Film in this language": "Film în această limbă",
|
||||
"GAP": "GOL",
|
||||
"GROWING + UNREACHED": "ÎN CREȘTERE + NEATINS",
|
||||
"Gospel audio": "Audio cu Evanghelia",
|
||||
"Groups": "Popoare",
|
||||
"Groups sharing bridge": "Popoare cu aceeași punte",
|
||||
"Groups speaking": "Popoare vorbitoare",
|
||||
"Groups speaking it": "Popoare care o vorbesc",
|
||||
"Groups the map link shows": "Popoare afișate de linkul hărții",
|
||||
"Groups with Scripture": "Popoare cu Scripturi",
|
||||
"Groups with film": "Popoare cu film",
|
||||
"Growing & unreached": "În creștere și neatinse",
|
||||
"Growth %/yr": "Creștere %/an",
|
||||
"Has anything": "Are ceva",
|
||||
"Heart film": "Film în limba proprie",
|
||||
"Heart language": "Limba proprie",
|
||||
"Heart scripture": "Scripturi în limba proprie",
|
||||
"Internet": "Internet",
|
||||
"Internet %": "Internet %",
|
||||
"Its population": "Populația sa",
|
||||
"JESUS Film": "Filmul ISUS",
|
||||
"Kind of gap": "Tipul golului",
|
||||
"Kind of way in": "Tipul căii de acces",
|
||||
"Languages": "Limbi",
|
||||
"Languages here": "Limbi aici",
|
||||
"Languages they speak": "Limbile vorbite",
|
||||
"Largest group": "Cel mai mare popor",
|
||||
"Largest groups": "Cele mai mari popoare",
|
||||
"Largest with nothing": "Cel mai mare fără nimic",
|
||||
"Lead status": "Stadiul pistei",
|
||||
"Link": "Link",
|
||||
"Method": "Metodă",
|
||||
"NT": "NT",
|
||||
"Note": "Notă",
|
||||
"OPPORTUNITY": "OPORTUNITATE",
|
||||
"Of those with a prayer page": "Dintre cele cu pagină de rugăciune",
|
||||
"On the map": "Pe hartă",
|
||||
"Open": "Deschide",
|
||||
"People groups here": "Popoare aici",
|
||||
"People nobody is praying for": "Persoane pentru care nu se roagă nimeni",
|
||||
"People praying": "Persoane care se roagă",
|
||||
"Place": "Loc",
|
||||
"Placement approximate": "Amplasare aproximativă",
|
||||
"Pools not checked": "Surse neverificate",
|
||||
"Pray": "Roagă-te",
|
||||
"Primary religion": "Religia principală",
|
||||
"Province": "Provincie",
|
||||
"Rank": "Rang",
|
||||
"Resource": "Resursă",
|
||||
"Scope": "Acoperire",
|
||||
"Scripture in this language": "Scripturi în această limbă",
|
||||
"Signed film": "Film în limba semnelor",
|
||||
"Signed film link": "Link film în limba semnelor",
|
||||
"Source": "Sursă",
|
||||
"Speakers": "Vorbitori",
|
||||
"Their language": "Limba lor",
|
||||
"These peoples on the map": "Aceste popoare pe hartă",
|
||||
"Training in the bridge": "Instruire în limba-punte",
|
||||
"Translated by": "Tradus de",
|
||||
"Type": "Tip",
|
||||
"UPG population": "Populația popoarelor neatinse",
|
||||
"Urban %": "Urban %",
|
||||
"Ways in": "Căi de acces",
|
||||
"Ways in (country-level)": "Căi de acces (la nivel de țară)",
|
||||
"Year": "An"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Popoare Doxa — în ce este disponibilă Evanghelia și ce lipsește",
|
||||
"links": "Resurse ale Evangheliei — fiecare link și pe cine atinge",
|
||||
"nothing": "Unde nu s-a găsit nimic",
|
||||
"countrygap": "Golul pe țări",
|
||||
"translation": "Prioritate de traducere — pot auzi, niciun text găsit",
|
||||
"translationcountry": "Prioritate de traducere pe țări — pot auzi, niciun text găsit",
|
||||
"gaps": "Goluri și oportunități",
|
||||
"bridges": "Oportunități de punți urbane",
|
||||
"deaf": "Golul surd",
|
||||
"resources": "Popoare × resurse ale Evangheliei",
|
||||
"languages": "Limbi pe țări (clasate) x resurse",
|
||||
"growth": "Accesul la Evanghelie vs creșterea populației",
|
||||
"province": "Accesul la Evanghelie pe provincii — India și China",
|
||||
"bridgereach": "O limbă, câte popoare — calea de acces, ordonată după acoperire"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Scripturi (oricare)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "Lista mea UPG 100 — Liste de rugăciune și afișe",
|
||||
"upg100WallTag": "Mobilizare pentru rugăciune",
|
||||
"upg100WallIntro": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei <strong>pe numele poporului lor</strong>.",
|
||||
"upg100WallIntroA": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei ",
|
||||
"upg100WallIntroEmph": "pe numele poporului lor",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Se încarcă {name}…",
|
||||
"couldNotLoadNamed": "Nu s-a putut încărca {name}.",
|
||||
"engineMissing": "Motorul de hartă {engine} nu s-a încărcat.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Молящиеся",
|
||||
"selected_first": "Выбранные сначала",
|
||||
"selected_first_hint": "Поднять выбранные народы наверх, сохранив сортировку столбцов",
|
||||
"n_shown": "{n} показано"
|
||||
"n_shown": "{n} показано",
|
||||
"other": "Другое"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "Библейское видео",
|
||||
"Gospel presentation": "Изложение Евангелия",
|
||||
"Countries": "Страны",
|
||||
"People groups": "Народы"
|
||||
"People groups": "Народы",
|
||||
"People Group": "Народ",
|
||||
"Basis": "Уровень данных",
|
||||
"Bridge ISO": "ISO посредника",
|
||||
"Bridge language": "Язык-посредник",
|
||||
"Film": "Фильм",
|
||||
"Groups with nothing": "Народов без ресурсов",
|
||||
"People with nothing": "Людей без ресурсов",
|
||||
"Pools checked": "Проверенные источники",
|
||||
"Audio": "Аудио",
|
||||
"Bridge candidate": "Кандидат в посредники",
|
||||
"Bridge film": "Фильм на языке-посреднике",
|
||||
"Bridge scripture": "Писание на языке-посреднике",
|
||||
"Bridge strength": "Сила посредника",
|
||||
"Est. deaths/day": "Оценка смертей в день",
|
||||
"Film link": "Ссылка на фильм",
|
||||
"Full Bible": "Полная Библия",
|
||||
"NO RESOURCE": "НЕТ РЕСУРСОВ",
|
||||
"Nobody praying": "Никто не молится",
|
||||
"People": "Людей",
|
||||
"Pools NOT checked": "НЕпроверенные источники",
|
||||
"Scripture link": "Ссылка на Писание",
|
||||
"Sign language": "Жестовый язык",
|
||||
"Status": "Статус",
|
||||
"Training": "Обучение ученичеству",
|
||||
"% with nothing": "% без ресурсов",
|
||||
"ALIGNED": "УСЛОВИЯ ВЫПОЛНЕНЫ",
|
||||
"Audio link": "Ссылка на аудио",
|
||||
"Band": "Уровень",
|
||||
"Bible stories": "Библейские истории",
|
||||
"Can already hear": "Уже могут услышать",
|
||||
"Candidate % of country": "Кандидат, % страны",
|
||||
"Candidate ISO": "ISO кандидата",
|
||||
"Candidate is official": "Кандидат официальный",
|
||||
"Carries a resource": "Есть ресурс",
|
||||
"Channel": "Канал",
|
||||
"Channel source": "Источник канала",
|
||||
"Code": "Код",
|
||||
"Code note": "Примечание к коду",
|
||||
"Code unlookupable": "Кода нет ни в одном источнике",
|
||||
"Code unmatchable": "Код без соответствия",
|
||||
"Deaf bridge": "Посредник для глухих",
|
||||
"Deaf bridge link": "Ссылка: посредник для глухих",
|
||||
"Deaf bridge publisher": "Издатель: посредник для глухих",
|
||||
"Deaf programme in country": "Программа для глухих в стране",
|
||||
"Death rate /1000": "Смертность /1000",
|
||||
"Est. deaths/yr": "Оценка смертей в год",
|
||||
"Example group": "Пример народа",
|
||||
"Film in this language": "Фильм на этом языке",
|
||||
"GAP": "ПРОБЕЛ",
|
||||
"GROWING + UNREACHED": "РАСТУЩИЕ + НЕДОСТИГНУТЫЕ",
|
||||
"Gospel audio": "Аудио с Евангелием",
|
||||
"Groups": "Народов",
|
||||
"Groups sharing bridge": "Народов с тем же посредником",
|
||||
"Groups speaking": "Говорящих народов",
|
||||
"Groups speaking it": "Народов, говорящих на нём",
|
||||
"Groups the map link shows": "Народы по ссылке на карту",
|
||||
"Groups with Scripture": "Народов с Писанием",
|
||||
"Groups with film": "Народов с фильмом",
|
||||
"Growing & unreached": "Растущие и недостигнутые",
|
||||
"Growth %/yr": "Рост %/год",
|
||||
"Has anything": "Есть что-то",
|
||||
"Heart film": "Фильм на родном языке",
|
||||
"Heart language": "Родной язык",
|
||||
"Heart scripture": "Писание на родном языке",
|
||||
"Internet": "Интернет",
|
||||
"Internet %": "Интернет %",
|
||||
"Its population": "Его численность",
|
||||
"JESUS Film": "Фильм «Иисус»",
|
||||
"Kind of gap": "Вид пробела",
|
||||
"Kind of way in": "Вид пути",
|
||||
"Languages": "Языки",
|
||||
"Languages here": "Языки здесь",
|
||||
"Languages they speak": "Их языки",
|
||||
"Largest group": "Крупнейший народ",
|
||||
"Largest groups": "Крупнейшие народы",
|
||||
"Largest with nothing": "Крупнейший без ресурсов",
|
||||
"Lead status": "Статус наводки",
|
||||
"Link": "Ссылка",
|
||||
"Method": "Метод",
|
||||
"NT": "НЗ",
|
||||
"Note": "Примечание",
|
||||
"OPPORTUNITY": "ВОЗМОЖНОСТЬ",
|
||||
"Of those with a prayer page": "Из тех, у кого есть страница молитвы",
|
||||
"On the map": "На карте",
|
||||
"Open": "Открыть",
|
||||
"People groups here": "Народы здесь",
|
||||
"People nobody is praying for": "Люди, за которых никто не молится",
|
||||
"People praying": "Молящиеся",
|
||||
"Place": "Место",
|
||||
"Placement approximate": "Расположение приблизительное",
|
||||
"Pools not checked": "Непроверенные источники",
|
||||
"Pray": "Молиться",
|
||||
"Primary religion": "Основная религия",
|
||||
"Province": "Провинция",
|
||||
"Rank": "Ранг",
|
||||
"Resource": "Ресурс",
|
||||
"Scope": "Охват",
|
||||
"Scripture in this language": "Писание на этом языке",
|
||||
"Signed film": "Фильм на жестовом языке",
|
||||
"Signed film link": "Ссылка на фильм на жестовом языке",
|
||||
"Source": "Источник",
|
||||
"Speakers": "Носители",
|
||||
"Their language": "Их язык",
|
||||
"These peoples on the map": "Эти народы на карте",
|
||||
"Training in the bridge": "Обучение на языке-посреднике",
|
||||
"Translated by": "Переводчик",
|
||||
"Type": "Тип",
|
||||
"UPG population": "Численность недостигнутых народов",
|
||||
"Urban %": "Городское население %",
|
||||
"Ways in": "Пути",
|
||||
"Ways in (country-level)": "Пути (на уровне страны)",
|
||||
"Year": "Год"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Народы Doxa — что из Евангелия доступно и чего не хватает",
|
||||
"links": "Ресурсы Евангелия — каждая ссылка и кого она достигает",
|
||||
"nothing": "Где ничего не найдено",
|
||||
"countrygap": "Пробел по странам",
|
||||
"translation": "Приоритет перевода — могут услышать, текст не найден",
|
||||
"translationcountry": "Приоритет перевода по странам — могут услышать, текст не найден",
|
||||
"gaps": "Пробелы и возможности",
|
||||
"bridges": "Возможности городских мостов",
|
||||
"deaf": "Пробел у глухих",
|
||||
"resources": "Народы × ресурсы Евангелия",
|
||||
"languages": "Языки по странам (по рангу) x ресурсы",
|
||||
"growth": "Доступ к Евангелию и рост населения",
|
||||
"province": "Доступ к Евангелию по провинциям — Индия и Китай",
|
||||
"bridgereach": "Один язык — сколько народов: путь, по охвату"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "Писание (любое)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "Мой список UPG 100 — молитвенные списки и плакаты",
|
||||
"upg100WallTag": "Молитвенная мобилизация",
|
||||
"upg100WallIntro": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них <strong>по имени их народа</strong>.",
|
||||
"upg100WallIntroA": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них ",
|
||||
"upg100WallIntroEmph": "по имени их народа",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "Загрузка {name}…",
|
||||
"couldNotLoadNamed": "Не удалось загрузить {name}.",
|
||||
"engineMissing": "Картографический движок {engine} не загрузился.",
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "祷告的人数",
|
||||
"selected_first": "所选优先",
|
||||
"selected_first_hint": "将所选民族置顶,同时保留您的列排序",
|
||||
"n_shown": "显示 {n} 个"
|
||||
"n_shown": "显示 {n} 个",
|
||||
"other": "其他"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,135 @@
|
|||
"Bible video": "圣经视频",
|
||||
"Gospel presentation": "福音简介",
|
||||
"Countries": "国家",
|
||||
"People groups": "族群"
|
||||
"People groups": "族群",
|
||||
"People Group": "族群",
|
||||
"Basis": "数据层级",
|
||||
"Bridge ISO": "桥梁语言 ISO",
|
||||
"Bridge language": "桥梁语言",
|
||||
"Film": "影片",
|
||||
"Groups with nothing": "一无所有的族群",
|
||||
"People with nothing": "一无所有的人数",
|
||||
"Pools checked": "已查资源库",
|
||||
"Audio": "音频",
|
||||
"Bridge candidate": "候选桥梁语言",
|
||||
"Bridge film": "桥梁语言影片",
|
||||
"Bridge scripture": "桥梁语言圣经",
|
||||
"Bridge strength": "桥梁强度",
|
||||
"Est. deaths/day": "每日估计死亡人数",
|
||||
"Film link": "影片链接",
|
||||
"Full Bible": "全本圣经",
|
||||
"NO RESOURCE": "无资源",
|
||||
"Nobody praying": "无人代祷",
|
||||
"People": "人数",
|
||||
"Pools NOT checked": "未查的资源库",
|
||||
"Scripture link": "圣经链接",
|
||||
"Sign language": "手语",
|
||||
"Status": "状态",
|
||||
"Training": "门徒训练",
|
||||
"% with nothing": "% 一无所有",
|
||||
"ALIGNED": "条件齐备",
|
||||
"Audio link": "音频链接",
|
||||
"Band": "等级",
|
||||
"Bible stories": "圣经故事",
|
||||
"Can already hear": "已能听到",
|
||||
"Candidate % of country": "候选语言占全国 %",
|
||||
"Candidate ISO": "候选 ISO",
|
||||
"Candidate is official": "候选为官方语言",
|
||||
"Carries a resource": "有资源",
|
||||
"Channel": "渠道",
|
||||
"Channel source": "渠道来源",
|
||||
"Code": "代码",
|
||||
"Code note": "代码说明",
|
||||
"Code unlookupable": "代码未被任何资源库收录",
|
||||
"Code unmatchable": "代码无法匹配",
|
||||
"Deaf bridge": "聋人桥梁",
|
||||
"Deaf bridge link": "聋人桥梁链接",
|
||||
"Deaf bridge publisher": "聋人桥梁发布方",
|
||||
"Deaf programme in country": "国内聋人事工",
|
||||
"Death rate /1000": "死亡率 /1000",
|
||||
"Est. deaths/yr": "每年估计死亡人数",
|
||||
"Example group": "示例族群",
|
||||
"Film in this language": "该语言影片",
|
||||
"GAP": "缺口",
|
||||
"GROWING + UNREACHED": "增长中 + 未接触",
|
||||
"Gospel audio": "福音音频",
|
||||
"Groups": "族群数",
|
||||
"Groups sharing bridge": "共用桥梁的族群",
|
||||
"Groups speaking": "使用族群",
|
||||
"Groups speaking it": "使用该语言的族群",
|
||||
"Groups the map link shows": "地图链接显示的族群",
|
||||
"Groups with Scripture": "有圣经的族群",
|
||||
"Groups with film": "有影片的族群",
|
||||
"Growing & unreached": "增长中且未接触",
|
||||
"Growth %/yr": "增长率 %/年",
|
||||
"Has anything": "有任何资源",
|
||||
"Heart film": "自己语言的影片",
|
||||
"Heart language": "自己的语言",
|
||||
"Heart scripture": "自己语言的圣经",
|
||||
"Internet": "互联网",
|
||||
"Internet %": "互联网 %",
|
||||
"Its population": "其人口",
|
||||
"JESUS Film": "《耶稣传》",
|
||||
"Kind of gap": "缺口类型",
|
||||
"Kind of way in": "切入点类型",
|
||||
"Languages": "语言",
|
||||
"Languages here": "本地语言",
|
||||
"Languages they speak": "所用语言",
|
||||
"Largest group": "最大族群",
|
||||
"Largest groups": "最大的族群",
|
||||
"Largest with nothing": "一无所有的最大族群",
|
||||
"Lead status": "线索状态",
|
||||
"Link": "链接",
|
||||
"Method": "方法",
|
||||
"NT": "新约",
|
||||
"Note": "备注",
|
||||
"OPPORTUNITY": "机会",
|
||||
"Of those with a prayer page": "其中有祷告页面的",
|
||||
"On the map": "在地图上",
|
||||
"Open": "打开",
|
||||
"People groups here": "本地族群",
|
||||
"People nobody is praying for": "无人代祷的人数",
|
||||
"People praying": "代祷人数",
|
||||
"Place": "地点",
|
||||
"Placement approximate": "位置为近似",
|
||||
"Pools not checked": "未查资源库",
|
||||
"Pray": "代祷",
|
||||
"Primary religion": "主要宗教",
|
||||
"Province": "省份",
|
||||
"Rank": "排名",
|
||||
"Resource": "资源",
|
||||
"Scope": "涵盖程度",
|
||||
"Scripture in this language": "该语言圣经",
|
||||
"Signed film": "手语影片",
|
||||
"Signed film link": "手语影片链接",
|
||||
"Source": "来源",
|
||||
"Speakers": "使用人数",
|
||||
"Their language": "他们的语言",
|
||||
"These peoples on the map": "地图上的这些民族",
|
||||
"Training in the bridge": "桥梁语言培训",
|
||||
"Translated by": "译者",
|
||||
"Type": "类型",
|
||||
"UPG population": "未接触族群人口",
|
||||
"Urban %": "城镇 %",
|
||||
"Ways in": "切入点",
|
||||
"Ways in (country-level)": "切入点(国家层面)",
|
||||
"Year": "年份"
|
||||
},
|
||||
"tableTitles": {
|
||||
"doxa": "Doxa 族群 — 福音有哪些形式可用,还缺什么",
|
||||
"links": "福音资源 — 每个链接及其覆盖的人群",
|
||||
"nothing": "一无所获之处",
|
||||
"countrygap": "各国缺口",
|
||||
"translation": "翻译优先级 — 能听,未找到文字",
|
||||
"translationcountry": "各国翻译优先级 — 能听,未找到文字",
|
||||
"gaps": "缺口与机会",
|
||||
"bridges": "城市桥梁机会",
|
||||
"deaf": "聋人缺口",
|
||||
"resources": "族群 × 福音资源",
|
||||
"languages": "各国语言(排名)x 资源",
|
||||
"growth": "福音可及性与人口增长",
|
||||
"province": "按省份划分的福音可及性 — 印度和中国",
|
||||
"bridgereach": "一种语言,覆盖多少民族 — 切入点,按覆盖面排序"
|
||||
},
|
||||
"charts": {
|
||||
"scriptureAny": "圣经(任意)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "我的 UPG 100 名單 — 禱告名單與海報",
|
||||
"upg100WallTag": "禱告動員",
|
||||
"upg100WallIntro": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — <strong>按著他們族群的名字</strong>為他們禱告。",
|
||||
"upg100WallIntroA": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — ",
|
||||
"upg100WallIntroEmph": "按著他們族群的名字",
|
||||
"upg100WallIntroB": "為他們禱告。",
|
||||
"loadingNamed": "正在加载{name}…",
|
||||
"couldNotLoadNamed": "无法加载{name}。",
|
||||
"engineMissing": "{engine} 地图引擎未能加载。",
|
||||
|
|
|
|||
122
library/scripts/generate-iso3-to-alpha2.mjs
Normal file
122
library/scripts/generate-iso3-to-alpha2.mjs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env node
|
||||
// generate-iso3-to-alpha2.mjs — `bun run gen:iso3-to-alpha2`. Offline, no network, no dependency.
|
||||
//
|
||||
// Writes library/data/iso3-to-alpha2.json: the alpha-2 code for each of the 242 alpha-3 codes in
|
||||
// library/data/countries.json, so a country NAME can be localized. Intl.DisplayNames('region') takes
|
||||
// alpha-2 or UN M49 only -- it does NOT accept alpha-3 -- and our data speaks alpha-3 throughout, so
|
||||
// without this join every country name is stuck in English.
|
||||
//
|
||||
// ★ THIS IS NOT THE RESEARCH MAP. library/scripts/research/out/iso3-to-iso2.json is a different file
|
||||
// with a different job: the 176-country spine that drives the data FETCHES. Expanding that one changes
|
||||
// what gets fetched, which is a data-scope decision for the Driver (dx-shepherd, REG242B) and must not
|
||||
// happen as a side effect of adding country names. This map drives no fetch. The two are deliberately
|
||||
// not interchangeable, which is why they have different names, different shapes and this paragraph.
|
||||
//
|
||||
// ★ IT GRADES ITS OWN OUTPUT AND EXITS NON-ZERO. A generator that writes whatever it derived is a
|
||||
// generator that will one day write a hole. Coverage, injectivity and the single documented exception
|
||||
// are asserted here, so a bad refresh of the vendored CLDR cannot land quietly.
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const read = (p) => JSON.parse(readFileSync(join(REPO, p), 'utf8'))
|
||||
|
||||
const VENDOR = 'library/vendor/cldr/codeMappings.json'
|
||||
const OUT = 'library/data/iso3-to-alpha2.json'
|
||||
|
||||
// ── the one exception, and why it is not a fudge ─────────────────────────────────────────────────
|
||||
// Kosovo has NO ISO 3166-1 assignment, so CLDR's codeMappings cannot carry it and no amount of
|
||||
// re-downloading will add it. XK is the USER-ASSIGNED code the EU, the IMF, Unicode's own CLDR
|
||||
// locale data (sr_Cyrl_XK) and every major platform settled on. It is recorded here as a named
|
||||
// exception with its reason rather than patched into the vendored file, so the vendored bytes stay
|
||||
// exactly upstream's and the deviation stays visible to the next reader.
|
||||
const EXCEPTIONS = {
|
||||
KOS: { alpha2: 'XK', why: 'Kosovo has no ISO 3166-1 assignment; XK is the user-assigned code CLDR itself uses in locale ids (sr_Cyrl_XK).' },
|
||||
}
|
||||
|
||||
const cldr = read(VENDOR).supplemental.codeMappings
|
||||
const countries = read('library/data/countries.json')
|
||||
|
||||
// CLDR is keyed by alpha-2 with an _alpha3 inside; we need the inverse.
|
||||
const byAlpha3 = new Map()
|
||||
const cldrCollisions = []
|
||||
for (const [alpha2, v] of Object.entries(cldr)) {
|
||||
const a3 = v && v._alpha3
|
||||
if (!a3) continue
|
||||
if (byAlpha3.has(a3)) cldrCollisions.push(`${a3} claimed by ${byAlpha3.get(a3)} and ${alpha2}`)
|
||||
else byAlpha3.set(a3, alpha2)
|
||||
}
|
||||
|
||||
const map = {}
|
||||
const missing = []
|
||||
for (const { value: iso3, label } of countries) {
|
||||
const fromCldr = byAlpha3.get(iso3)
|
||||
const exc = EXCEPTIONS[iso3]
|
||||
if (fromCldr) map[iso3] = fromCldr
|
||||
else if (exc) map[iso3] = exc.alpha2
|
||||
else missing.push(`${iso3} (${label})`)
|
||||
}
|
||||
|
||||
// ── assertions: the generator refuses to ship a map it cannot defend ────────────────────────────
|
||||
let fails = 0
|
||||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
|
||||
ok(countries.length === 242, `the country list holds 242 codes (found ${countries.length})`)
|
||||
ok(missing.length === 0, missing.length
|
||||
? `${missing.length} code(s) have NO alpha-2 and no documented exception: ${missing.join(', ')}`
|
||||
: 'every code resolved, via CLDR or a documented exception')
|
||||
|
||||
const seen = new Map()
|
||||
const dupes = []
|
||||
for (const [iso3, a2] of Object.entries(map)) {
|
||||
if (seen.has(a2)) dupes.push(`${a2} claimed by ${seen.get(a2)} and ${iso3}`)
|
||||
else seen.set(a2, iso3)
|
||||
}
|
||||
ok(dupes.length === 0, dupes.length
|
||||
? `alpha-2 collisions -- at least one country would take another's name: ${dupes.join('; ')}`
|
||||
: `all ${Object.keys(map).length} codes map to a DISTINCT alpha-2`)
|
||||
|
||||
ok(Object.entries(map).every(([, a2]) => /^[A-Z]{2}$/.test(a2)), 'every alpha-2 is two uppercase letters')
|
||||
|
||||
// Every exception must still BE one. If CLDR ever assigns KOS, this fires and the entry should go.
|
||||
for (const [iso3, e] of Object.entries(EXCEPTIONS)) {
|
||||
ok(!byAlpha3.has(iso3),
|
||||
`the ${iso3} exception is still needed -- CLDR ${byAlpha3.has(iso3) ? `now assigns it ${byAlpha3.get(iso3)}, so DELETE the exception` : 'still has no assignment for it'}`)
|
||||
}
|
||||
|
||||
// ── cross-check the research map rather than touching it ────────────────────────────────────────
|
||||
// CLDR is an independent source for the 176 values that map already ships. Disagreements are reported,
|
||||
// never written: that file drives fetches and is not this generator's to edit.
|
||||
let disagreements = 0
|
||||
try {
|
||||
const research = read('library/scripts/research/out/iso3-to-iso2.json')
|
||||
for (const [iso3, a2] of Object.entries(research)) {
|
||||
const c = byAlpha3.get(iso3)
|
||||
if (c && c !== a2) { console.log(` FAIL - research map says ${iso3} -> ${a2}, CLDR says ${c}`); disagreements++ }
|
||||
}
|
||||
ok(disagreements === 0,
|
||||
`CLDR agrees with all ${Object.keys(research).length} values in the research spine map (reported, never rewritten -- that file drives fetches)`)
|
||||
} catch (e) {
|
||||
console.log(` note - research map not readable here (${e.code || e.message}); cross-check skipped`)
|
||||
}
|
||||
|
||||
if (fails) {
|
||||
console.log(`\nISO3-TO-ALPHA2: ${fails} FAIL(s) — nothing written`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const cldrVersion = '48.2.2' // keep in step with library/vendor/cldr/README.md
|
||||
writeFileSync(join(REPO, OUT), JSON.stringify({
|
||||
_meta: {
|
||||
generated_by: 'library/scripts/generate-iso3-to-alpha2.mjs (bun run gen:iso3-to-alpha2)',
|
||||
purpose: 'alpha-3 -> alpha-2 so Intl.DisplayNames can produce a LOCALIZED country name. Drives no data fetch.',
|
||||
not_to_be_confused_with: 'library/scripts/research/out/iso3-to-iso2.json — the 176-country research spine that DOES drive fetches',
|
||||
source: `CLDR cldr-core ${cldrVersion}, vendored at ${VENDOR} (Unicode-3.0)`,
|
||||
countries: Object.keys(map).length,
|
||||
exceptions: Object.fromEntries(Object.entries(EXCEPTIONS).map(([k, v]) => [k, `${v.alpha2} — ${v.why}`])),
|
||||
},
|
||||
map,
|
||||
}, null, 2) + '\n')
|
||||
|
||||
console.log(`\nISO3-TO-ALPHA2: wrote ${OUT} — ${Object.keys(map).length} countries, ${Object.keys(EXCEPTIONS).length} documented exception(s)`)
|
||||
|
|
@ -118,6 +118,7 @@
|
|||
"NFK": "NF",
|
||||
"NGA": "NG",
|
||||
"NIU": "NU",
|
||||
"NOR": "NO",
|
||||
"NPL": "NP",
|
||||
"NZL": "NZ",
|
||||
"OMN": "OM",
|
||||
|
|
@ -137,7 +138,7 @@
|
|||
"SDN": "SD",
|
||||
"SEN": "SN",
|
||||
"SHN": "SH",
|
||||
"SJM": "NO",
|
||||
"SJM": "SJ",
|
||||
"SLB": "SB",
|
||||
"SLE": "SL",
|
||||
"SMR": "SM",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,17 @@ else ok('every value is a well-formed 2-letter uppercase code');
|
|||
// 3. SPOT-CHECK the codes this corpus actually got wrong, so a regression is named
|
||||
// rather than merely counted. These are the sovereign states whose alpha-2 was
|
||||
// previously their neighbour's or their former administrator's.
|
||||
const MUST = { NER: 'NE', NGA: 'NG', PSE: 'PS', ISR: 'IL', HKG: 'HK', MAC: 'MO', ABW: 'AW' };
|
||||
// SJM belongs here for the same reason PSE and HKG do: it was mapped to its administrator's code.
|
||||
// SJM -> NO served NORWAY's data under the name "Svalbard and Jan Mayen", and INJECTIVITY COULD NOT
|
||||
// SEE IT -- the check passes as long as no two codes share an ISO2, and NOR is absent from this map
|
||||
// entirely, so "NO" had exactly one claimant and looked clean. A uniqueness test cannot tell a right
|
||||
// code from a wrong one that nobody else wanted; only a named expectation can.
|
||||
// NOR belongs here as the OTHER HALF of the SJM defect. Norway was absent from this map entirely,
|
||||
// which is why SJM -> NO looked injective and why Norway appeared in no downstream table at all --
|
||||
// consumers filter on `ISO3_2[iso3]`, so a missing code is not an error, it is a silent omission.
|
||||
// Added from ISO 3166-1 (NO/NOR), which is what CLDR's codeMappings encodes; the full CLDR vendor
|
||||
// still comes with BUILD-ISO3-TABLE-COVERS-ALL-242-COUNTRIES and will re-derive this alongside 66 others.
|
||||
const MUST = { NER: 'NE', NGA: 'NG', PSE: 'PS', ISR: 'IL', HKG: 'HK', MAC: 'MO', ABW: 'AW', SJM: 'SJ', NOR: 'NO' };
|
||||
// ABSENT MUST FAIL, NOT PASS. The first version read `map[k] && map[k] !== v`, so a
|
||||
// code MISSING ENTIRELY was falsey and slipped through — deleting NER outright passed
|
||||
// all three checks and this line still printed "NER … all correct" about a code that
|
||||
|
|
@ -77,7 +87,9 @@ if (!missing.length && !wrong.length) ok('previously-wrong codes (NER, PSE, HKG,
|
|||
|
||||
// The map must also stay COMPLETE. A silent shrink (175 -> 174) means a country
|
||||
// stopped being fetched at all, which no per-code check can see.
|
||||
const EXPECTED_MIN = 175;
|
||||
// 175 -> 176 with NOR. The floor RISES when a code is added, or the check stops being able to see the
|
||||
// shrink it exists for: left at 175 it would have called a regression back to the pre-NOR map clean.
|
||||
const EXPECTED_MIN = 176;
|
||||
if (Object.keys(map).length < EXPECTED_MIN) {
|
||||
fail(`map has ${Object.keys(map).length} codes, expected at least ${EXPECTED_MIN} — a country has DISAPPEARED`);
|
||||
} else ok(`${Object.keys(map).length} codes present (>= ${EXPECTED_MIN})`);
|
||||
|
|
|
|||
100
library/utils/countryName.js
Normal file
100
library/utils/countryName.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* countryName.js — a country's name in the reader's language.
|
||||
*
|
||||
* Our data speaks ISO 3166-1 ALPHA-3 ("DEU"). Intl.DisplayNames('region') accepts alpha-2 or UN M49
|
||||
* and NOT alpha-3, so every country name was stuck in English until the two were joined. The join is
|
||||
* library/data/iso3-to-alpha2.json, derived from vendored CLDR by generate-iso3-to-alpha2.mjs.
|
||||
*
|
||||
* ★ Intl.DisplayNames THROWS on a missing code. `dn.of(undefined)` is a RangeError, not a null return,
|
||||
* so every path here checks the alpha-2 before asking. That is the single defect this file exists to
|
||||
* not have: a country absent from the map would otherwise take down whatever rendered it.
|
||||
*
|
||||
* ★ AND IT THROWS ON A MALFORMED ONE TOO, which is why the regex guard is not redundant with the
|
||||
* lookup. dn.of('XYZ') is also a RangeError -- 'structurally invalid' is a different failure from
|
||||
* 'not found', and only the second returns the input unchanged.
|
||||
*
|
||||
* FALLBACK ORDER, each step a real answer rather than a placeholder:
|
||||
* 1. the localized name from Intl.DisplayNames
|
||||
* 2. the canonical ENGLISH name from library/data/countries.json — a real name in the wrong language
|
||||
* beats a bare code for a reader, and it is what our own tables already show
|
||||
* 3. the alpha-3 code itself, so something identifiable always renders
|
||||
*
|
||||
* No i18n framework is required: this reads a locale STRING the caller already has.
|
||||
*
|
||||
* ★★ WHERE NOT TO USE THIS, MEASURED. It has NO consumer yet on purpose. Intl's English names
|
||||
* differ from the canonical names in library/data/countries.json for 42 OF 242 COUNTRIES -- "Antigua &
|
||||
* Barbuda" vs "Antigua and Barbuda", "Cape Verde" vs "Cabo Verde", "Hong Kong SAR China" vs "Hong
|
||||
* Kong", "Myanmar (Burma)" vs "Myanmar", "Caribbean Netherlands" vs "Bonaire, Sint Eustatius, and
|
||||
* Saba". So this is NOT a drop-in replacement for a name we already display:
|
||||
*
|
||||
* ✗ do NOT swap it in where an API already returns a LOCALIZED label. region-dashboards.vue:370
|
||||
* prefers the API's label deliberately, and the regions surfaces are measured against live
|
||||
* doxa.life -- switching the source would change 42 English names and break clone parity, which
|
||||
* is a product decision about which name is canonical, not a refactor.
|
||||
* ✓ DO use it where the only thing in hand is an alpha-3 and there is no label at all, or where
|
||||
* a label arrives in English regardless of the reader's locale. That is the gap it was built for.
|
||||
*
|
||||
* Stated here rather than left to be discovered, because the failure mode is silent: the names all
|
||||
* still look like names.
|
||||
*/
|
||||
import table from '../data/iso3-to-alpha2.json'
|
||||
import countries from '../data/countries.json'
|
||||
|
||||
const ALPHA2 = table.map
|
||||
const ENGLISH = new Map(countries.map((c) => [c.value, c.label]))
|
||||
|
||||
/** The alpha-2 for an alpha-3 code, or undefined. Exported because callers building URLs and flag
|
||||
* assets need the same lookup, and a second copy of it would drift. */
|
||||
export function alpha2Of(iso3) {
|
||||
return iso3 ? ALPHA2[String(iso3).toUpperCase()] : undefined
|
||||
}
|
||||
|
||||
/** The canonical English name, or undefined. */
|
||||
export function englishName(iso3) {
|
||||
return iso3 ? ENGLISH.get(String(iso3).toUpperCase()) : undefined
|
||||
}
|
||||
|
||||
// One DisplayNames per locale. Constructing it is not cheap and a country list rebuilds it per row;
|
||||
// a Map keyed by locale is the whole optimisation. A locale that throws is cached as null so the
|
||||
// failure costs one construction, not one per call.
|
||||
const cache = new Map()
|
||||
function displayNames(locale) {
|
||||
const key = locale || 'en'
|
||||
if (cache.has(key)) return cache.get(key)
|
||||
let dn = null
|
||||
try {
|
||||
dn = new Intl.DisplayNames([key], { type: 'region' })
|
||||
} catch (_) {
|
||||
dn = null // an unsupported locale tag, or no Intl.DisplayNames at all
|
||||
}
|
||||
cache.set(key, dn)
|
||||
return dn
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} iso3 ISO 3166-1 alpha-3, e.g. 'DEU'. Case-insensitive.
|
||||
* @param {string} [locale] BCP-47 tag, e.g. 'ar'. Defaults to English.
|
||||
* @returns {string} the country's name in that locale, its English name, or the code.
|
||||
*/
|
||||
export function countryName(iso3, locale) {
|
||||
const code = iso3 ? String(iso3).toUpperCase() : ''
|
||||
const a2 = ALPHA2[code]
|
||||
const fallback = ENGLISH.get(code) || code
|
||||
|
||||
// Guard BEFORE asking: dn.of(undefined) and dn.of('XYZ') both throw.
|
||||
if (!a2 || !/^[A-Z]{2}$/.test(a2)) return fallback
|
||||
|
||||
const dn = displayNames(locale)
|
||||
if (!dn) return fallback
|
||||
try {
|
||||
const name = dn.of(a2)
|
||||
// An UNKNOWN-but-well-formed region comes back as the input string. That is not a name, so it
|
||||
// must not win over our English label -- this is the difference between "no translation exists"
|
||||
// and "the translation is literally 'DE'".
|
||||
return name && name !== a2 ? name : fallback
|
||||
} catch (_) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export default countryName
|
||||
41
library/vendor/cldr/LICENSE
vendored
Normal file
41
library/vendor/cldr/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
UNICODE LICENSE V3
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 2015-2024 Unicode, Inc.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of data files and any associated documentation (the "Data Files") or
|
||||
software and any associated documentation (the "Software") to deal in the
|
||||
Data Files or Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||
copies of the Data Files or Software, and to permit persons to whom the
|
||||
Data Files or Software are furnished to do so, provided that either (a)
|
||||
this copyright and permission notice appear with all copies of the Data
|
||||
Files or Software, or (b) this copyright and permission notice appear in
|
||||
associated Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
THIRD PARTY RIGHTS.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||
FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
|
||||
SPDX-License-Identifier: Unicode-3.0
|
||||
42
library/vendor/cldr/README.md
vendored
Normal file
42
library/vendor/cldr/README.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Vendored CLDR — `codeMappings.json`
|
||||
|
||||
The ISO 3166-1 alpha-2 / alpha-3 / numeric correspondences, used to turn the alpha-3 codes our data
|
||||
speaks into the alpha-2 codes `Intl.DisplayNames` requires to produce a LOCALIZED country name.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Package | `cldr-core` (unicode-org/cldr-json) |
|
||||
| Version | **48.2.2** |
|
||||
| Source | `https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/cldr-core/supplemental/codeMappings.json` |
|
||||
| Licence | **Unicode-3.0** — full text in `LICENSE` beside this file |
|
||||
| Bytes | 35,948 |
|
||||
| sha256 | `0d1ef50b92c1140e5847d22d96faf1a9c35543b0dedf8e9a2fcb87e4c51b9ed6` |
|
||||
| Vendored | 2026-09-27 |
|
||||
|
||||
## Why vendored rather than a dependency
|
||||
|
||||
No npm package is added for this. A dependency would pull CLDR's whole supplemental tree, and it would
|
||||
make the derivation of a shipped data file depend on an install step. The file is 35KB, it is
|
||||
licensed for redistribution, and vendoring it means `bun run gen:iso3-to-alpha2` reproduces the derived
|
||||
map OFFLINE — which is the actual point of vendoring, not merely having a copy.
|
||||
|
||||
## What is derived from it
|
||||
|
||||
`library/scripts/generate-iso3-to-alpha2.mjs` joins this file against the 242 alpha-3 codes in
|
||||
`library/data/countries.json` and writes `library/data/iso3-to-alpha2.json`. The generator asserts its
|
||||
own output (242 coverage, injectivity, the one documented exception) and exits non-zero if any of it
|
||||
fails, so a bad refresh cannot land quietly.
|
||||
|
||||
## DO NOT CONFUSE THIS WITH THE RESEARCH MAP
|
||||
|
||||
`library/scripts/research/out/iso3-to-iso2.json` is a DIFFERENT map with a different job: it is the
|
||||
176-country research spine that drives the data fetches, and expanding it changes what gets fetched —
|
||||
a data-scope decision that belongs to the Driver, not a side effect of adding country names. The map
|
||||
derived here is for DISPLAY NAMES ONLY and drives no fetch. CLDR agrees with all 176 values in the
|
||||
research map, which is a useful independent check on it, but the two files are not interchangeable.
|
||||
|
||||
## Refreshing
|
||||
|
||||
Re-download the URL above, update the version, bytes and sha256 in this table, then run the generator
|
||||
and let its assertions grade the result. If CLDR ever disagrees with a value in the research map, the
|
||||
generator says so rather than silently overwriting anything.
|
||||
1934
library/vendor/cldr/codeMappings.json
vendored
Normal file
1934
library/vendor/cldr/codeMappings.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -162,6 +162,7 @@
|
|||
"check:one-palette": "node internal/check-one-palette.mjs",
|
||||
"check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test",
|
||||
"check:optional-t-equivalence": "node internal/check-optional-t-equivalence.mjs",
|
||||
"check:package-json-unique-keys": "node internal/check-package-json-unique-keys.mjs",
|
||||
"check:packet-browser": "node internal/check-packet-browser.mjs",
|
||||
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
|
||||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||||
|
|
@ -195,7 +196,9 @@
|
|||
"check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test",
|
||||
"check:script-flags": "node internal/check-script-flags-reachable.mjs",
|
||||
"check:settled": "node internal/settled-tree-gate.mjs",
|
||||
"check:setup-order": "node internal/check-setup-order.mjs",
|
||||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||||
"check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs",
|
||||
"check:regions-uses-library": "node plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs",
|
||||
"check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs",
|
||||
"check:share-policy": "node internal/check-share-policy.mjs",
|
||||
|
|
@ -234,8 +237,9 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
"lists": "node library/scripts/people-groups/build-lists.mjs",
|
||||
|
|
@ -339,7 +343,7 @@
|
|||
"check:heartbeat:self-test": "THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||||
"check:hover-channel": "SPEC-DASHBOARD-KIT D3: the bus's third coordinated channel points at a record without committing to one. Hover looks like a weaker selection, and the cheap implementation is to put the key in selectionSet 'just while hovering' - every consequence of which is bad and none obvious at the call site: useSelectionCamera watches selectionVersion, so a hover would fly the camera at every mousemove (the resize-strobe class, on a photosensitive Driver); the CSV exporter reads selection > filter > all, so a hover would change what an export contains; the table's selection pill and the shell's live region would both narrate the pointer. Runs the real bus: hover holds its own ref, never enters the selection, never bumps the version, never touches the filters, a repeat emits once (hover fires on every mousemove across 2,107 rows), and the three channels clear independently. Does NOT check which panels light up - D3 says panels opt in, and the first opt-in belongs with a consumer that has a reason. No browser.",
|
||||
"check:host-locale-contract": "a HOST's profile-config.locale reaches every leaf: each leaf page is loaded with ONLY \"locale\":\"ar\" added to its profile-config (no ?lang=, the page keeps lang=\"en\") and must draw at least half the Arabic text the same page draws at ?lang=ar. regions-dashboard/regions-list is the positive control (NOT MEASURED if it does not enter Arabic). Guards ProfileLoader's inject('appI18n'): a bundle that never provides appI18n silently ignores the host. --dir <tree> for a before/after, --only a/b,c/d to narrow. Through browser-slot.sh.",
|
||||
"check:i18n-keys-exist": "every literal catalogue key named in code exists in library/i18n/locales/en/. A localized CALL SITE is not a localized STRING: the dashboard's five resize handles all read T('aria.resizeFiltersRail', ...) and look correct in review, while aria.json held eleven keys and not one was a resize key - so every handle announced itself in English in all eleven locales, and the fallback that makes a missing key degrade to readable English is exactly what kept it silent. No other gate asks this: i18n-parity is satisfied because the key is missing from ALL locales equally, and english-leaks and composite-locale-leaks both need the catalogue to HOLD the string before an appearance counts as a leak. Matches tStatic/$t/t and the house-pattern T( alias, all of which take a full dotted key; prefix helpers like L( and any concatenated or template-literal key are COUNTED as unresolved and never guessed at. Found 22 missing keys on its first run.",
|
||||
"check:i18n-keys-exist": "every literal catalogue key named in code exists in library/i18n/locales/en/. A localized CALL SITE is not a localized STRING: the dashboard's five resize handles all read T('aria.resizeFiltersRail', ...) and look correct in review, while aria.json held eleven keys and not one was a resize key - so every handle announced itself in English in all eleven locales, and the fallback that makes a missing key degrade to readable English is exactly what kept it silent. No other gate asks this: i18n-parity is satisfied because the key is missing from ALL locales equally, and english-leaks and composite-locale-leaks both need the catalogue to HOLD the string before an appearance counts as a leak. Matches tStatic/$t/t and the house-pattern T( alias, all of which take a full dotted key; prefix helpers like L( and any concatenated or template-literal key are COUNTED as unresolved and never guessed at. Found 22 missing keys on its first run. ROUND 2, after go-int planted T('poison_key') behind `const T = useGiT()` and this gate reported the tree CLEAN -- the one failure a check is not forgiven, because a red gate gets fixed and a green one that should be red gets believed. THREE independent blindnesses stacked, and any one alone hid the key: the file never writes the word tStatic so the precondition skipped it before anything ran; useGiT is not tStatic so no alias was found; and the alias is PREFIXED (useOptionalT carries a namespace) while its call sites pass BARE keys, which the full-key pattern cannot match because it requires a dot. Now a translator that arrives from a COMPOSABLE is resolved like any other, with the namespace READ from the factory's one-line body rather than listed here -- useOptionalT/useOptionalI18n directly, plus any `export function useXxxT() { return useOptionalT('ns') }`, which is how useNeighborT and usePplrT are written. Because qualify() prefixes a bare key and leaves an already-qualified one alone, one binding registers as BOTH shapes. MEASURED before and after with the same poison on disk: the old version PASSES it, this one fails it as 'neighbor.zzz_not_a_real_key (via nt())'; coverage went 683 -> 743 full-key and 49 -> 56 prefixed call sites, so 67 sites were being scored clean without being read. A self-test now asserts all three fixes separately and exits 3 if any goes blind, because this gate guards goal (d) and had already been trusted once while lying. Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away; measured at zero today, since all five such sites pass dotted keys.",
|
||||
"check:i18n-never-translated": "a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||||
"check:i18n-stopped-scope": "createAppI18n() must succeed while a STOPPED effect scope is current: the Driver's P0 (Plugin Studio gallery, every tab after the first threw 'SyntaxError: Unexpected error' from createI18n, because Vue 3.5 makes a new scope inactive under a stopped one). A bare createI18n() in the same scope must throw first, or the harness proves nothing. Also asserts a bundle's second provide('appI18n') raises no warning.",
|
||||
"check:i18n-treeshake": "each built bundle carries every translation area its code asks for: every area-prefixed key literal in <bundle>.js belongs to an area the build included (the build narrows library/i18n's catalogue glob per bundle, internal/i18n-areas.mjs). Headless.",
|
||||
|
|
@ -378,6 +382,7 @@
|
|||
"check:no-free-identifiers": "every name a .vue script uses is imported, declared, a parameter or a known global (scope-aware, across plugin-designs and library). A free identifier compiles and then throws at runtime in exactly the path nobody clicked.",
|
||||
"check:no-silent-engine": "A profile must NEVER fall back to a different map engine than its provider asked for. `engineGlobal(provider) || window.mapboxgl` looks defensive and is not: engineGlobal already returns the right global for the requested provider (useMapInstance.js:115 falls back to mapbox's NAME only for an UNKNOWN provider, never a missing engine), so the || fires in exactly one case — the page asked for maplibre and maplibre failed to load — and hands back the TOKEN-GATED engine to a page that asked for the KEYLESS one. Worse, accessToken is gated on !isMaplibre, so the substitute arrives WITHOUT a token: 401s on every tile instead of a legible failure. Found by regions' C3 read 2026-09-26; 2 offenders remained (cities-internet, gospel-resources) after my-upg-100-list was fixed. NOT a duplicate of check:leaf-engine-per-provider / check:provider-switch / check:auto-engine — all three assert the HAPPY path (what loads) and this bug only appears when something does NOT load, so all three were green while it existed. A SOURCE check on purpose: reproducing it live means serving a leaf with MapLibre deliberately blocked, spending a browser slot on what a grep decides. Exits 75 when no file calls engineGlobal — 'found nothing' is not 'found nothing wrong'. Run :self-test to prove the pattern still matches.",
|
||||
"check:optional-t-equivalence": "C4's shared guarded translator (library/composables/useOptionalT.js) must behave like the twenty hand-written try{useI18n()}catch blocks it replaces: all three host states, and the per-site register asserts each migrated file still passes ITS OWN fallback rather than inheriting the catalogue, because routing them through tStatic would change what seven bare-host bundles render. EVERY LATER C4 WAVE APPENDS A ROW. Extract-and-executes the module source, since useOptionalT imports vue-i18n and an i18n index that calls import.meta.glob.",
|
||||
"check:package-json-unique-keys": "no object in package.json may carry the same key twice: JSON.parse keeps the last silently, so a duplicate kills the first entry while every gate that reads the PARSED object passes (a //scripts note sat twice after a merge). A small scanner tracks keys per object; its self-test runs every time.",
|
||||
"check:packet-browser": "the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.",
|
||||
"check:packet-fixture": "the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).",
|
||||
"check:packet-render:expect-funnel": "check:packet-render with the page-2 FUNNEL declared REQUIRED. Exists because of a real near-miss: a merge resolved with `git checkout --ours` deleted the printPacket call site that supplies the funnel data (60 lines) and EVERY GATE STAYED GREEN — it had to, because the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. Always requiring it would be the wrong fix: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. So the CALLER, which knows whether this packet was built with funnel data, declares it — an absence is then either declared (no flag) or a failure (flag passed, page missing), never a silent nothing. Pass it for any packet from a profile that wires packetFunnel(). Browser gate.",
|
||||
|
|
@ -398,7 +403,9 @@
|
|||
"check:religion-label-not-value": "the library's two RELIGION_FAMILIES sites (useLegendData, useSearch) show religionFamilyLabel() to the reader, while the value a filter or search matches on stays the canonical English family name: a translated label must never become a wire value.",
|
||||
"check:scrim-viewport": "a full-screen SCRIM must cover the VIEWPORT, not just the content pane. A position:fixed;inset:0 scrim is PANE-relative wherever an ancestor sets contain/transform — VerticalTabsLayout sets contain:layout on .vtl-content — so a scrim dimmed 1024x793 @ (256,107) at a 1280x900 viewport and left the 256px rail CLICKABLE behind a modal. A source grep cannot see it: the declaration is correct, the ANCESTOR is the variable. Needs `bun run dev`; browser gate, not in build:unlocked.",
|
||||
"check:scrim-viewport:self-test": "reverts each scrim to plain inset:0 and requires EVERY case to FAIL — a gate that cannot fail is worse than no gate, and this one measures a property that looks fine in a screenshot either way.",
|
||||
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0s from the Driver's console came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
|
||||
"check:sfc-parse": "every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
|
||||
"check:shadow-css-parses": "the CSS we assemble as JS STRINGS must parse as CSS — brace balance and declarations stranded outside any rule. check:sfc-parse proves only that the JS parses, so it passed a stylesheet with 9 orphaned declarations and braces at 12 open / 21 close (REGLIB batch 1); nothing else in the tree validates CSS syntax. Reads every useShadowStyles() argument off the @babel/parser AST the build already uses, because three hand-rolled lexers each produced a confident FALSE RED: a /* CSS comment */ read as a declaration, an apostrophe in a .vue prose header opening a phantom string literal, and a regex literal containing a quote. 9 self-tests run inline on every invocation, including the exact batch-1 damage, so it cannot silently stop being able to fail.",
|
||||
"check:shared-reached": "a shared library fix is only as deployed as the last rebuild of EVERY bundle that consumes it. Each bundle ships its own COMPILED copy, so a one-line fix in library/ is live in whichever bundles happened to be rebuilt after it and silently absent from the rest — and every other gate measures ONE leaf, which renders the old behaviour perfectly. Asserts a distinctive marker string per tracked change; consumers are DERIVED from imports, never hardcoded.",
|
||||
"check:shot-distinctness": "two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",
|
||||
"check:shot-fidelity": "warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; the `check:shot-fidelity:strict` script to block once an emoji font is installed.",
|
||||
|
|
@ -422,6 +429,7 @@
|
|||
"check:zone-census": "every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
|
||||
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
|
||||
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
|
||||
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
|
||||
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",
|
||||
"library-index": "regenerate documentation/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
|
||||
"lists": "regenerate data/people-groups/lists/<id>.json, the DOXA slugs in each people-group list of library/lists/lists.json, from the IMB CSV and the DOXA snapshot. Re-run after either changes.",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
"dashboard-plus-grid/dashboard-plus-grid",
|
||||
"research-map-plus-grid/research-map-plus-grid",
|
||||
"regions-dashboard/regions-mfe",
|
||||
"gospel-internet/board",
|
||||
"my-upg-100-list/upg-100-mfe",
|
||||
"doxa-simple-map/doxa-simple-map",
|
||||
"cities-internet/connectivity-map",
|
||||
|
|
|
|||
|
|
@ -230,9 +230,6 @@ const NAV_MODEL = [
|
|||
// worth more than a complete feature nobody can open, and its absence was the one real gap
|
||||
// between the Driver's seven and the six that shipped.
|
||||
{ id: 'regions-dashboard', icon: 'map' },
|
||||
// 4. Gospel x Internet — the Driver asked for it INSIDE the gallery as one of its dashboards
|
||||
// (2026-09-27): gospel access by language x how much of each country is online.
|
||||
{ id: 'gospel-internet', icon: 'chart' },
|
||||
] },
|
||||
// Prayer Mobilization — now TWO rows only: the onboarding ramp's steps 2 and 3. Big Picture (former
|
||||
// step 1) moved to Dashboards above; Gospel Resources (former step 4) moved to Gospel Research below
|
||||
|
|
@ -416,10 +413,6 @@ const TAB_DEFS = [
|
|||
id: 'regions-dashboard', label: 'Regions', sub: 'regions and their countries', emoji: '🌐',
|
||||
bundle: 'regions-dashboard', profile: 'regions-mfe'
|
||||
},
|
||||
{
|
||||
id: 'gospel-internet', label: 'Gospel x Internet', sub: 'gospel access through the internet', emoji: '📶',
|
||||
bundle: 'gospel-internet', profile: 'board'
|
||||
},
|
||||
{
|
||||
id: 'upg-100', label: 'UPG-100 List', sub: 'my unreached list + posters', emoji: '📋',
|
||||
bundle: 'my-upg-100-list', profile: 'upg-100-mfe'
|
||||
|
|
@ -456,7 +449,7 @@ const HEAVY_ELEMENTS = new Set([
|
|||
'my-upg-100-list-map', 'doxa-map', 'doxa-maps-spa',
|
||||
// composites that NEST a map, so they own a context too (measured, see above)
|
||||
'dashboard-plus-grid', 'research-dashboard-map', 'demo-map', 'all-people-groups',
|
||||
'country-posters-map', 'key-cities-map', 'research-map-plus-grid', 'gospel-internet-board'
|
||||
'country-posters-map', 'key-cities-map', 'research-map-plus-grid'
|
||||
])
|
||||
|
||||
// SCROLL-STACK ELEMENTS — the rows this shell mounts WITHOUT the fixed-viewport inline style
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Cities with Internet', 'sub.cities-internet': 'connectivity of key cities',
|
||||
'tab.gospel-resources': 'Gospel Resources', 'sub.gospel-resources': 'resources by place',
|
||||
'tab.regions-dashboard': 'Regions', 'sub.regions-dashboard': 'regions and their countries',
|
||||
'tab.gospel-internet': "Gospel x Internet", 'sub.gospel-internet': "gospel access through the internet",
|
||||
'tab.upg-100': 'UPG-100 List', 'sub.upg-100': 'my unreached list + posters',
|
||||
navLabel: 'Research maps', meta: '{n} maps', loading: 'Loading {label}…', loadFailed: 'Could not load {label}.',
|
||||
manifestHint: 'This tab is composed from manifest.json — build the tree, or serve the built tree, to load it.',
|
||||
|
|
@ -45,7 +44,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Ciudades con Internet', 'sub.cities-internet': 'conectividad de ciudades clave',
|
||||
'tab.gospel-resources': 'Recursos del Evangelio', 'sub.gospel-resources': 'recursos por lugar',
|
||||
'tab.regions-dashboard': 'Regiones', 'sub.regions-dashboard': 'regiones y sus países',
|
||||
'tab.gospel-internet': "Evangelio e Internet", 'sub.gospel-internet': "acceso al evangelio por internet",
|
||||
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'mi lista de no alcanzados + carteles',
|
||||
navLabel: 'Mapas de investigación', meta: '{n} mapas', loading: 'Cargando {label}…', loadFailed: 'No se pudo cargar {label}.',
|
||||
manifestHint: 'Esta pestaña se compone a partir de manifest.json: compila el árbol o sirve el árbol compilado para cargarla.',
|
||||
|
|
@ -66,7 +64,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Villes avec Internet', 'sub.cities-internet': 'connectivité des villes clés',
|
||||
'tab.gospel-resources': "Ressources de l'Évangile", 'sub.gospel-resources': 'ressources par lieu',
|
||||
'tab.regions-dashboard': 'Régions', 'sub.regions-dashboard': 'régions et leurs pays',
|
||||
'tab.gospel-internet': "Évangile et Internet", 'sub.gospel-internet': "l’accès à l’Évangile par internet",
|
||||
'tab.upg-100': 'Liste UPG-100', 'sub.upg-100': 'ma liste des non atteints + affiches',
|
||||
navLabel: 'Cartes de recherche', meta: '{n} cartes', loading: 'Chargement de {label}…', loadFailed: 'Impossible de charger {label}.',
|
||||
manifestHint: "Cet onglet est composé à partir de manifest.json : construisez l'arborescence ou servez l'arborescence construite pour le charger.",
|
||||
|
|
@ -87,7 +84,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Städte mit Internet', 'sub.cities-internet': 'Konnektivität wichtiger Städte',
|
||||
'tab.gospel-resources': 'Evangeliumsressourcen', 'sub.gospel-resources': 'Ressourcen nach Ort',
|
||||
'tab.regions-dashboard': 'Regionen', 'sub.regions-dashboard': 'Regionen und ihre Länder',
|
||||
'tab.gospel-internet': "Evangelium × Internet", 'sub.gospel-internet': "Zugang zum Evangelium über das Internet",
|
||||
'tab.upg-100': 'UPG-100-Liste', 'sub.upg-100': 'meine Liste der Unerreichten + Poster',
|
||||
navLabel: 'Forschungskarten', meta: '{n} Karten', loading: '{label} wird geladen…', loadFailed: '{label} konnte nicht geladen werden.',
|
||||
manifestHint: 'Dieser Tab wird aus manifest.json zusammengesetzt – bauen Sie den Baum oder stellen Sie den gebauten Baum bereit, um ihn zu laden.',
|
||||
|
|
@ -108,7 +104,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Cidades com Internet', 'sub.cities-internet': 'conectividade das principais cidades',
|
||||
'tab.gospel-resources': 'Recursos do Evangelho', 'sub.gospel-resources': 'recursos por lugar',
|
||||
'tab.regions-dashboard': 'Regiões', 'sub.regions-dashboard': 'regiões e seus países',
|
||||
'tab.gospel-internet': "Evangelho e Internet", 'sub.gospel-internet': "acesso ao evangelho pela internet",
|
||||
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'minha lista de não alcançados + cartazes',
|
||||
navLabel: 'Mapas de pesquisa', meta: '{n} mapas', loading: 'Carregando {label}…', loadFailed: 'Não foi possível carregar {label}.',
|
||||
manifestHint: 'Esta aba é composta a partir do manifest.json — compile a árvore ou sirva a árvore compilada para carregá-la.',
|
||||
|
|
@ -131,7 +126,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Città con Internet', 'sub.cities-internet': 'connettività delle città chiave',
|
||||
'tab.gospel-resources': 'Risorse del Vangelo', 'sub.gospel-resources': 'risorse per luogo',
|
||||
'tab.regions-dashboard': 'Regioni', 'sub.regions-dashboard': 'regioni e i loro paesi',
|
||||
'tab.gospel-internet': "Vangelo e Internet", 'sub.gospel-internet': "l’accesso al vangelo tramite internet",
|
||||
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'la mia lista dei non raggiunti + poster',
|
||||
navLabel: 'Mappe di ricerca', meta: '{n} mappe', loading: 'Caricamento di {label}…', loadFailed: 'Impossibile caricare {label}.',
|
||||
manifestHint: "Questa scheda è composta da manifest.json: compila l'albero o pubblica l'albero compilato per caricarla.",
|
||||
|
|
@ -152,7 +146,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Orașe cu internet', 'sub.cities-internet': 'conectivitatea orașelor-cheie',
|
||||
'tab.gospel-resources': 'Resurse ale Evangheliei', 'sub.gospel-resources': 'resurse după loc',
|
||||
'tab.regions-dashboard': 'Regiuni', 'sub.regions-dashboard': 'regiunile și țările lor',
|
||||
'tab.gospel-internet': "Evanghelie și Internet", 'sub.gospel-internet': "accesul la Evanghelie prin internet",
|
||||
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'lista mea de neatinși + afișe',
|
||||
navLabel: 'Hărți de cercetare', meta: '{n} hărți', loading: 'Se încarcă {label}…', loadFailed: '{label} nu a putut fi încărcat.',
|
||||
manifestHint: 'Această filă este compusă din manifest.json — construiți arborele sau serviți arborele construit pentru a o încărca.',
|
||||
|
|
@ -173,7 +166,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'Города с интернетом', 'sub.cities-internet': 'подключённость ключевых городов',
|
||||
'tab.gospel-resources': 'Евангельские ресурсы', 'sub.gospel-resources': 'ресурсы по местам',
|
||||
'tab.regions-dashboard': 'Регионы', 'sub.regions-dashboard': 'регионы и их страны',
|
||||
'tab.gospel-internet': "Евангелие и интернет", 'sub.gospel-internet': "доступ к Евангелию через интернет",
|
||||
'tab.upg-100': 'Список UPG-100', 'sub.upg-100': 'мой список недостигнутых + плакаты',
|
||||
navLabel: 'Исследовательские карты', meta: 'Карт: {n}', loading: 'Загрузка: {label}…', loadFailed: 'Не удалось загрузить: {label}.',
|
||||
manifestHint: 'Эта вкладка собирается из manifest.json — соберите дерево или разместите собранное дерево, чтобы загрузить её.',
|
||||
|
|
@ -194,7 +186,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'مدن متصلة بالإنترنت', 'sub.cities-internet': 'اتصال المدن الرئيسية',
|
||||
'tab.gospel-resources': 'موارد الإنجيل', 'sub.gospel-resources': 'الموارد حسب المكان',
|
||||
'tab.regions-dashboard': 'المناطق', 'sub.regions-dashboard': 'المناطق ودولها',
|
||||
'tab.gospel-internet': "الإنجيل والإنترنت", 'sub.gospel-internet': "الوصول إلى الإنجيل عبر الإنترنت",
|
||||
'tab.upg-100': 'قائمة UPG-100', 'sub.upg-100': 'قائمتي لغير المُبشَّرين + ملصقات',
|
||||
navLabel: 'خرائط البحث', meta: 'عدد الخرائط: {n}', loading: 'جارٍ تحميل {label}…', loadFailed: 'تعذّر تحميل {label}.',
|
||||
manifestHint: 'تُبنى علامة التبويب هذه من manifest.json — ابنِ الشجرة أو قدّم الشجرة المبنية لتحميلها.',
|
||||
|
|
@ -215,7 +206,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': 'इंटरनेट वाले शहर', 'sub.cities-internet': 'प्रमुख शहरों की कनेक्टिविटी',
|
||||
'tab.gospel-resources': 'सुसमाचार संसाधन', 'sub.gospel-resources': 'स्थान के अनुसार संसाधन',
|
||||
'tab.regions-dashboard': 'क्षेत्र', 'sub.regions-dashboard': 'क्षेत्र और उनके देश',
|
||||
'tab.gospel-internet': "सुसमाचार और इंटरनेट", 'sub.gospel-internet': "इंटरनेट के माध्यम से सुसमाचार तक पहुँच",
|
||||
'tab.upg-100': 'UPG-100 सूची', 'sub.upg-100': 'मेरी अप्राप्त सूची + पोस्टर',
|
||||
navLabel: 'शोध मानचित्र', meta: '{n} मानचित्र', loading: '{label} लोड हो रहा है…', loadFailed: '{label} लोड नहीं हो सका।',
|
||||
manifestHint: 'यह टैब manifest.json से बनता है — इसे लोड करने के लिए ट्री बनाएँ या बना हुआ ट्री सर्व करें।',
|
||||
|
|
@ -236,7 +226,6 @@ export const STRINGS = {
|
|||
'tab.cities-internet': '有互联网的城市', 'sub.cities-internet': '主要城市的网络连接',
|
||||
'tab.gospel-resources': '福音资源', 'sub.gospel-resources': '按地点划分的资源',
|
||||
'tab.regions-dashboard': '区域', 'sub.regions-dashboard': '区域及其国家',
|
||||
'tab.gospel-internet': "福音与互联网", 'sub.gospel-internet': "通过互联网接触福音",
|
||||
'tab.upg-100': 'UPG-100 名单', 'sub.upg-100': '我的未得之民名单 + 海报',
|
||||
navLabel: '研究地图', meta: '{n} 张地图', loading: '正在加载{label}…', loadFailed: '无法加载{label}。',
|
||||
manifestHint: '此标签页由 manifest.json 组成——请构建目录树或提供已构建的目录树以加载它。',
|
||||
|
|
|
|||
|
|
@ -2012,6 +2012,22 @@ watch(() => mapStore.filters, () => {
|
|||
// would mean guessing a mapping, and a wrong link is worse than an honest gap.
|
||||
let applyingExternal = false
|
||||
|
||||
// ─── FIT THE MAP TO A SET OF PEOPLES: state, declared here on purpose ────────
|
||||
// These belong with applyFitSlugs() further down, and that is where they used to be. They cannot
|
||||
// live there: the watcher immediately below is `immediate: true`, so it runs DURING setup and
|
||||
// calls applyFitSlugs(raw.fitSlugs) — while a `let`/`const` further down the file is still in its
|
||||
// temporal dead zone. `function applyFitSlugs` hoists; its constants do not, and reading
|
||||
// _lastFitKey threw `Cannot access '_lastFitKey' before initialization`, which killed the whole
|
||||
// setup and blanked the map. Anything an immediate watcher can reach must be declared above it.
|
||||
const FIT_PADDING_PX = 70
|
||||
const FIT_MAX_ZOOM = 5.5 // several peoples: stop at regional framing, never street level
|
||||
const SINGLE_ROW_ZOOM = 6 // one people group: its coordinate is a country-scale centroid
|
||||
const DEGENERATE_SPAN_DEG = 0.01 // a cluster this tight is a point by another route
|
||||
const WIDE_SPAN_DEG = 110 // past this the set wraps the globe; frame the world instead
|
||||
const WORLD_ZOOM = 1.45
|
||||
const FLY_DURATION_MS = 900
|
||||
let _lastFitKey = ''
|
||||
|
||||
watch(() => profileConfig?.value?.externalFilters, (raw) => {
|
||||
if (!raw || typeof raw !== 'object' || !mapStore) return
|
||||
applyingExternal = true
|
||||
|
|
@ -2419,15 +2435,9 @@ watch(() => uiStore.religionFilter, (key) => {
|
|||
else applyDimFilter({ kind: null })
|
||||
})
|
||||
|
||||
// ─── FIT THE MAP TO A SET OF PEOPLES (see the fitSlugs note in the externalFilters watcher) ───
|
||||
const FIT_PADDING_PX = 70
|
||||
const FIT_MAX_ZOOM = 5.5 // several peoples: stop at regional framing, never street level
|
||||
const SINGLE_ROW_ZOOM = 6 // one people group: its coordinate is a country-scale centroid
|
||||
const DEGENERATE_SPAN_DEG = 0.01 // a cluster this tight is a point by another route
|
||||
const WIDE_SPAN_DEG = 110 // past this the set wraps the globe; frame the world instead
|
||||
const WORLD_ZOOM = 1.45
|
||||
const FLY_DURATION_MS = 900
|
||||
let _lastFitKey = ''
|
||||
// ─── FIT THE MAP TO A SET OF PEOPLES ─────────────────────────────────────────
|
||||
// Its constants and the _lastFitKey guard are declared ABOVE the externalFilters watcher, not here
|
||||
// beside applyFitSlugs() — see the note there.
|
||||
|
||||
function prefersReducedMotion() {
|
||||
try {
|
||||
|
|
@ -2620,6 +2630,11 @@ let _containerObserver = null
|
|||
// the scrollport settles (a lazy-mounted host has no final height at any fixed timeout; see the
|
||||
// comment on _syncScrollZoomToHost above). Torn down in onBeforeUnmount.
|
||||
let _scrollZoomObserver = null
|
||||
// Its window-'resize' twin, declared out here for the same reason the observer is: both are created
|
||||
// after onMounted's `await initializeMap()`, which is past the point where a lifecycle hook can
|
||||
// still attach. The observer was already torn down below; the listener was not, because its
|
||||
// onBeforeUnmount sat beside the code that added it and was silently never registered.
|
||||
let _resyncScrollZoom = null
|
||||
function applyContainerScale(width) {
|
||||
const el = rmRoot.value
|
||||
if (!el) return
|
||||
|
|
@ -2729,7 +2744,7 @@ onMounted(async () => {
|
|||
})
|
||||
// See _syncScrollZoomToHost above (BUG-CANNOT-SCROLL-DOWN-TO-THE-GRID, mobile pair).
|
||||
_syncScrollZoomToHost(map.value)
|
||||
const _resyncScrollZoom = () => _syncScrollZoomToHost(map.value)
|
||||
_resyncScrollZoom = () => _syncScrollZoomToHost(map.value)
|
||||
try {
|
||||
_scrollZoomObserver = new ResizeObserver(_resyncScrollZoom)
|
||||
const host = (map.value.getContainer && map.value.getContainer()) || null
|
||||
|
|
@ -2739,7 +2754,6 @@ onMounted(async () => {
|
|||
if (document.body) _scrollZoomObserver.observe(document.body)
|
||||
} catch (_) { /* no ResizeObserver: the initial call above still answers for a static page */ }
|
||||
window.addEventListener('resize', _resyncScrollZoom)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', _resyncScrollZoom))
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
@ -2747,6 +2761,7 @@ onBeforeUnmount(() => {
|
|||
window.removeEventListener('legend:highlight', onLegendHighlight)
|
||||
if (_containerObserver) { _containerObserver.disconnect(); _containerObserver = null }
|
||||
if (_scrollZoomObserver) { _scrollZoomObserver.disconnect(); _scrollZoomObserver = null }
|
||||
if (_resyncScrollZoom) { window.removeEventListener('resize', _resyncScrollZoom); _resyncScrollZoom = null }
|
||||
mapLayers.stopPrayerGlow()
|
||||
clustering.cleanup?.()
|
||||
selectedPin.cleanup?.()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@ import { PALETTE as REGION_PALETTE, WAGF_REGIONS, REGION_ORDER } from '@map/colo
|
|||
// component's shadow CSS so every var() below resolves inside the shadow root.
|
||||
import { DXLIFE_TOKENS } from './dxlife-tokens.js'
|
||||
import RegionCountryMap from './src/RegionCountryMap.vue'
|
||||
// REGLIB batch 2: the stat card, the three goal cards and their bars are library components now.
|
||||
// They carry their own shadow CSS (useShadowStyles) and the card surface they share, so the rules
|
||||
// for .rd-card / .rd-stat* / .rd-goal* / .rd-big* / .rd-bar* / .rd-legend are no longer below.
|
||||
import StatCard from '@map/components/regions/StatCard.vue'
|
||||
import GoalCard from '@map/components/regions/GoalCard.vue'
|
||||
import ProgressBar from '@map/components/charts/ProgressBar.vue'
|
||||
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
|
||||
import { summarizeRegions, emptyStats, toNumber, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js'
|
||||
|
||||
|
|
@ -255,12 +261,16 @@ const tiles = computed(() => {
|
|||
const partialPrayerCount = computed(() => Math.max(0, (subject.value.stats.withPrayer || 0) - (subject.value.stats.withFullPrayer || 0)))
|
||||
const noPrayerCount = computed(() => Math.max(0, (subject.value.stats.count || 0) - (subject.value.stats.withPrayer || 0)))
|
||||
|
||||
// RegionDetail.vue:28-30. Capped at 100 for the same reason theirs is: a bar wider than its
|
||||
// track is a rendering bug, not a data story.
|
||||
function percentOfSubject(count) {
|
||||
const total = subject.value.stats.count || 0
|
||||
return total ? Math.min(100, (count / total) * 100) : 0
|
||||
}
|
||||
// The three prayer-coverage bands as ONE list, feeding both the bar widths and the legend numbers.
|
||||
// ProgressBar divides each `value` by its `max`, so a width and the figure printed beside it come
|
||||
// from the same number and cannot drift apart -- before extraction they were computed separately.
|
||||
// The colours are the very tokens the deleted .rd-seg--full/--partial/--none rules set, so every
|
||||
// rendered colour is unchanged; only the mechanism moved from a class to a value.
|
||||
const prayerSegments = computed(() => [
|
||||
{ value: subject.value.stats.withFullPrayer || 0, color: 'var(--dx-accent)', label: t('100+ People Praying') },
|
||||
{ value: partialPrayerCount.value, color: 'var(--dx-partial)', label: t('1+ People Praying') },
|
||||
{ value: noPrayerCount.value, color: 'var(--dx-track)', label: t('No One Praying') },
|
||||
])
|
||||
|
||||
// Their four headline stats, RegionDetail.vue:52-69: People Groups / Countries / Population /
|
||||
// Unengaged. A COUNTRY subject has no countryCount, and their own country page does not show one
|
||||
|
|
@ -280,8 +290,6 @@ const totalCountryCount = computed(() => regions.value.reduce((n, r) => n + (r.c
|
|||
|
||||
// Their three goal cards, RegionDetail.vue:88-141. The goal SENTENCES are theirs verbatim --
|
||||
// they are the DOXA goals, not copy we get to reword.
|
||||
const adoptedPct = computed(() => percentOfSubject(subject.value.stats.adopted || 0))
|
||||
const engagedPct = computed(() => percentOfSubject(subject.value.stats.engaged || 0))
|
||||
|
||||
// Their per-country table, RegionDetail.vue:144-171 -- five columns, same order.
|
||||
// A country subject keeps its REGION's rows with itself highlighted, for the reason the chart
|
||||
|
|
@ -833,35 +841,17 @@ useShadowStyles(
|
|||
'.rd-summary{ color:var(--dx-muted); font-size:var(--font-size-md); }' +
|
||||
'.rd-prompt{ margin:0; color:var(--dx-muted); font-size:var(--font-size-sm); }' +
|
||||
|
||||
// ── .card: blocks/_cards.scss:9-21. radius --border-radius-xlg (35px);
|
||||
// padding clamp(--spacing-lg, 6vw, --spacing-2xl) clamp(--spacing-lg, 6vw, --spacing-3xl).
|
||||
'.rd-card{ position:relative; border-radius:var(--dx-radius);' +
|
||||
' background:var(--dx-elevated); color:var(--dx-text);' +
|
||||
' padding:clamp(var(--spacing-lg), 6cqw, var(--spacing-2xl)) clamp(var(--spacing-lg), 6cqw, var(--spacing-3xl)); }' +
|
||||
// .card[padding-small] -> --spacing-lg flat
|
||||
'.rd-card--small{ padding:var(--spacing-lg); }' +
|
||||
// .card[data-variant="secondary"] -> surface-brand-dark on text-on-brand-light
|
||||
'.rd-card--secondary{ background:var(--dx-dark); color:var(--dx-on-dark); }' +
|
||||
// ── .card (blocks/_cards.scss:9-21) lives in library/composables/useCardSurface.js now: StatCard
|
||||
// and GoalCard both need it, and both call useCardSurface(), which injects ONE copy per shadow
|
||||
// root under a stable label. This element renders no .rd-card markup of its own any more, so it
|
||||
// does not call it. If markup here ever needs the surface again, call useCardSurface() -- do not
|
||||
// re-declare the rules, or there will be two copies to keep in step.
|
||||
|
||||
// ── .region-stats + .region-stat: RegionDetail.vue's scoped block ──
|
||||
// ── .region-stats: RegionDetail.vue's scoped block. The GRID stays here and the CARD went to the
|
||||
// library, deliberately: how many stats there are and how they wrap is this page's decision,
|
||||
// while the look of one stat is the reusable part. See library/components/regions/StatCard.vue.
|
||||
'.rd-stats{ display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));' +
|
||||
' gap:var(--spacing-md); }' +
|
||||
'.rd-stat{ text-align:center; }' +
|
||||
// ★ THE data-len SHRINK IS GONE, AND ITS OWN COMMENT SHOWED WHY. Their .region-stat__value is
|
||||
// font-family-heading + font-size-3xl + line-height:1 and nothing else -- no such attribute exists
|
||||
// in their markup. Ours was a shrink-to-fit workaround whose stated premise was "our condensed
|
||||
// fallback is wider" than Bebas. That is STALE: Bebas is vendored now (assets/fonts/bebas-neue,
|
||||
// glyph oracle 279 = 279 SAME), so there is no wider fallback to compensate for.
|
||||
// Its other claim -- "every value DX-life itself renders keeps their size" -- was FALSIFIED by
|
||||
// measurement: rega found "43,045,275" (10 chars, so it hit the 'long' bucket) rendered at
|
||||
// 38.15px against their 47.68px on the same-width card, while the other three stats matched
|
||||
// exactly. A rule whose own invariant is violated in practice is not protecting anything.
|
||||
// Deleted WITH its mechanism -- the binding and lenBucket() too, not just the rules.
|
||||
'.rd-stat__value{ margin:0; font-size:var(--font-size-3xl); line-height:1;' +
|
||||
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' font-weight:400; }' +
|
||||
'.rd-stat__label{ margin:var(--spacing-2xs) 0 0; font-size:var(--font-size-sm); color:var(--dx-muted); }' +
|
||||
|
||||
'.rd-chart{ padding:var(--spacing-lg) clamp(var(--spacing-md), 4cqw, var(--spacing-xl)); }' +
|
||||
// RegionDetail wraps the note and the map in a .stack--sm (13.3px). The note is their
|
||||
// .text-center .color-primary .font-weight-bold -- --color-brand-primary at weight 700.
|
||||
|
|
@ -925,18 +915,9 @@ useShadowStyles(
|
|||
// substituted face can render in their card. Pinning it there keeps THEIR LAYOUT (three
|
||||
// cards across, heading on two lines) which is what the clone is actually for. The table
|
||||
// heading keeps the full clamp -- it has 1200px and no such constraint.
|
||||
// .stack--lg (var(--spacing-lg)) inside each card, centred; .region-goal pins to the bottom
|
||||
// (margin-top:auto) so three cards of different heights still align their goal lines.
|
||||
// ★ THEIR .stack IS margin-top, AND THAT IS LOAD-BEARING — not a stylistic detail.
|
||||
// composition/_stack.scss: `.stack > * + * { margin-top: var(--stack-spacing) }`, with
|
||||
// `.stack--lg` setting that to --spacing-lg. RegionDetail.vue's scoped
|
||||
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
|
||||
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
|
||||
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
|
||||
// so every goal card carried ~23.5px of space their cards do not have. The DRIVER saw it
|
||||
// ('spcing is nto ths ame', naming these three cards). Ported as their mechanism.
|
||||
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
|
||||
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
|
||||
// The three goal cards are library/components/regions/GoalCard.vue now, and the .stack--lg /
|
||||
// margin-top:auto layout that aligns their goal lines went with them. The .rd-h2 rule below is
|
||||
// still here because this element keeps ONE heading of its own -- the breakdown table's title.
|
||||
// h2: _globals.scss clamp(--font-size-2xl, 6vw, --font-size-4xl) = clamp(var(--font-size-2xl),_,var(--font-size-4xl))
|
||||
'.rd-h2{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl)); line-height:1;' +
|
||||
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
|
|
@ -955,78 +936,19 @@ useShadowStyles(
|
|||
'[lang="ru"] .rd-title, [lang="ru-RU"] .rd-title,' +
|
||||
'[lang="ru"] .rd-h2, [lang="ru-RU"] .rd-h2{ font-family:"Oswald","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' letter-spacing:-0.02em; }' +
|
||||
// .font-size-4xl .font-weight-medium / .font-size-lg
|
||||
// Their .font-size-4xl (var(--font-size-4xl)) verified live. Same substitution consequence as the h2:
|
||||
// in our wider stand-in face 'Adoption Progress' 21 / 2,107 breaks after the slash inside a
|
||||
// 372px card, which reads as two numbers. Stepped to --font-size-3xl (the next rung down on
|
||||
// THEIR scale) and pinned nowrap so a figure can never be split across lines. The stat-card
|
||||
// values keep the full var(--font-size-3xl) -- those cards are wider and were measured to fit.
|
||||
// ★ NO line-height HERE ANY MORE. Theirs is `<p class="font-size-4xl font-weight-medium">` and the
|
||||
// .font-size-4xl utility sets ONLY a font-size — so their number inherits body's 1.5. Ours forced
|
||||
// 1.1, which was compensating for the base line-height we never ported (now on :host in
|
||||
// dxlife-tokens.js). rega measured the cost: 24px of card height, their number ~12px lower and its
|
||||
// label ~24px lower. Inheriting is the faithful port.
|
||||
// ★ NOW A CLAMP, LIKE THEIRS, AND IT RESOLVES THE NOWRAP WORRY RATHER THAN FIGHTING IT.
|
||||
// utilities/_typography.scss gives .font-size-4xl
|
||||
// font-size: clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))
|
||||
// and ours was a flat var(--font-size-4xl). Identical at 1440 and above (both sit on the ceiling),
|
||||
// but below that ours never shrank -- MEASURED against their resolved values:
|
||||
// 390px viewport theirs 38.15 ours 59.60 ours 21.46 too large
|
||||
// 768px viewport theirs 46.08 ours 59.60 ours 13.52 too large
|
||||
// 1024px and up theirs 59.60 ours 59.60 identical
|
||||
// I had reported this as unchangeable because of the nowrap note above -- a figure must never
|
||||
// split across lines in a 372px card. That was backwards: the clamp makes the figure SMALLER
|
||||
// exactly where the wrap was observed. In a 372px container 6cqw is 22.3px, below the 2xl floor,
|
||||
// so it resolves to 38.15px against the 59.60px that was breaking. The clamp helps the very
|
||||
// problem the flat value was chosen for, and the nowrap stays as a belt.
|
||||
// cqw not vw, consistent with every other clamp in this element (the container-vs-viewport
|
||||
// substitution the Driver is still ruling on); at 768 that lands 42.4 against their 46.08, which
|
||||
// is 3.68 off instead of 13.52.
|
||||
'.rd-big{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl));' +
|
||||
' font-weight:500; }' +
|
||||
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
|
||||
'.rd-big-label{ margin:0; font-size:var(--font-size-lg); }' +
|
||||
// .stack .stack--xs, inside the Prayer Coverage card.
|
||||
'.rd-stack-xs{ display:flex; flex-direction:column; }' +
|
||||
'.rd-stack-xs > * + *{ margin-top:10px; }' +
|
||||
// .region-goal — margin-top:auto, spelled at higher specificity than the stack rule above
|
||||
// so the override is unambiguous rather than depending on source order.
|
||||
// ★ margin-top:auto, AND IT IS LOAD-BEARING. Theirs is RegionDetail.vue:24 `.region-goal {
|
||||
// margin-top: auto }` inside `<div class="stack stack--lg | card | text-center">`, and
|
||||
// composition/_stack.scss makes .stack a flex COLUMN with `> * + * { margin-top:
|
||||
// var(--stack-spacing) }`. So the auto OVERRIDES that child margin and pushes the goal line to the
|
||||
// BOTTOM of the card. Ours was margin:0, which left it directly under the text above it.
|
||||
// The visible consequence, and why it matters for a clone: the three goal cards hold different
|
||||
// amounts of copy, so theirs align their goal lines flush along the cards' bottom edge while ours
|
||||
// floated at three different heights.
|
||||
// FOUND BY READING THEIR MARKUP after rega's spacing probe measured this pair as "same" -- which
|
||||
// it is, at 1440, because with no free space in the card the auto resolves to 0. An equal computed
|
||||
// margin hid a real difference, so the probe was right and incomplete at the same time.
|
||||
'.rd-goal{ margin:auto 0 0; font-size:var(--font-size-sm); }' +
|
||||
// .rd-big / .rd-big-label / .rd-goal and the .stack--xs that separates a bar from its legend are
|
||||
// in library/components/regions/GoalCard.vue and library/components/charts/ProgressBar.vue now,
|
||||
// together with the measurement history that fixed each value. The `strong` rule below STAYS:
|
||||
// UupgsList renders a <strong> in this same shadow root and depends on it.
|
||||
// base/_globals.scss:200-202 — <strong> is --font-weight-medium (500) on their site, not the
|
||||
// UA's bold. The 'Goal:' label in all three cards is a <strong>, so ours was rendering 700.
|
||||
'.rd-root strong{ font-weight:500; }' +
|
||||
'.rd-goal-card > .rd-goal{ margin-top:auto; }' +
|
||||
|
||||
// ── .progress-bar: blocks/_progress-bar.scss. The HEIGHT comes from padding-top, not height
|
||||
// -- clamp(15px, 6%, 25px) at [data-size="md"] -- so the track scales with its own width.
|
||||
'.rd-bar{ position:relative; overflow:hidden; width:100%; border-radius:100px;' +
|
||||
' background:var(--dx-track); border:none; padding-top:clamp(15px, 6%, 25px); }' +
|
||||
'.rd-bar__slider{ position:absolute; top:0; height:100%; width:0; border-radius:100px;' +
|
||||
' background:var(--dx-accent); transition:width 1s ease-out; }' +
|
||||
// .prayer-coverage-segments overrides that padding with a real height (their own scoped rule)
|
||||
'.rd-bar--segments{ display:flex; height:clamp(15px, 6cqw, 25px); padding-top:0; }' +
|
||||
'.rd-seg{ height:100%; transition:width 1s ease-out; }' +
|
||||
'.rd-seg--full{ background:var(--dx-accent); }' +
|
||||
'.rd-seg--partial{ background:var(--dx-partial); }' +
|
||||
'.rd-seg--none{ background:var(--dx-track); }' +
|
||||
'.rd-legend{ display:grid; justify-content:center; gap:3.5px; font-size:var(--font-size-sm);' +
|
||||
' font-weight:600; text-align:center; }' +
|
||||
'.rd-legend span{ display:inline-flex; align-items:center; justify-content:center; gap:4px; }' +
|
||||
'.rd-key{ display:inline-block; flex:0 0 auto; width:7.5px; height:7.5px; border-radius:100px; }' +
|
||||
'.rd-key--full{ background:var(--dx-accent); }' +
|
||||
'.rd-key--partial{ background:var(--dx-partial); }' +
|
||||
'.rd-key--none{ background:var(--dx-track); }' +
|
||||
// ── .progress-bar (blocks/_progress-bar.scss) + their .prayer-coverage-segments override and
|
||||
// legend are library/components/charts/ProgressBar.vue now. The three --full/--partial/--none
|
||||
// colour rules are GONE rather than left behind: a segment's colour is a prop there, passed as
|
||||
// the same var(--dx-accent) / var(--dx-partial) / var(--dx-track) tokens these rules set, so a
|
||||
// shared bar needs no vocabulary about prayer. A rule no selector can reach is not a fallback.
|
||||
|
||||
// ── the breakdown table: RegionDetail.vue's scoped block. min-width 560px inside an
|
||||
// overflow-x:auto wrapper is how they keep five columns readable on a phone instead of
|
||||
|
|
@ -1152,10 +1074,7 @@ useShadowStyles(
|
|||
|
||||
<!-- RegionDetail.vue:53-70 — four stat cards. CountryDetail has none. -->
|
||||
<div v-if="subject.kind !== 'country'" class="rd-stats">
|
||||
<div v-for="st in stats" :key="st.label" class="rd-card rd-card--small rd-stat">
|
||||
<p class="rd-stat__value">{{ st.value }}</p>
|
||||
<p class="rd-stat__label">{{ st.label }}</p>
|
||||
</div>
|
||||
<StatCard v-for="st in stats" :key="st.label" :value="st.value" :label="st.label" />
|
||||
</div>
|
||||
|
||||
<!-- RegionDetail.vue:72-86 and CountryDetail.vue:47-61 — a .stack--sm of the note and the
|
||||
|
|
@ -1209,46 +1128,42 @@ useShadowStyles(
|
|||
|
||||
<!-- RegionDetail.vue:88-141 — the three DOXA goal cards in their .switcher -->
|
||||
<div v-if="subject.kind === 'region'" class="rd-switcher">
|
||||
<div class="rd-card rd-card--secondary rd-goal-card">
|
||||
<h2 class="rd-h2">{{ t('Prayer Coverage') }}</h2>
|
||||
<div>
|
||||
<p class="rd-big">{{ formatNumber(subject.stats.peopleCommitted) }}</p>
|
||||
<p class="rd-big-label">{{ t('People committed to praying') }}</p>
|
||||
</div>
|
||||
<div class="rd-stack-xs">
|
||||
<div class="rd-bar rd-bar--segments" :aria-label="t('Prayer coverage progress')">
|
||||
<div class="rd-seg rd-seg--full" :style="{ width: percentOfSubject(subject.stats.withFullPrayer) + '%' }" />
|
||||
<div class="rd-seg rd-seg--partial" :style="{ width: percentOfSubject(partialPrayerCount) + '%' }" />
|
||||
<div class="rd-seg rd-seg--none" :style="{ width: percentOfSubject(noPrayerCount) + '%' }" />
|
||||
</div>
|
||||
<div class="rd-legend">
|
||||
<span><i class="rd-key rd-key--full" />{{ t('100+ People Praying') }}: {{ formatNumber(subject.stats.withFullPrayer) }}</span>
|
||||
<span><i class="rd-key rd-key--partial" />{{ t('1+ People Praying') }}: {{ formatNumber(partialPrayerCount) }}</span>
|
||||
<span><i class="rd-key rd-key--none" />{{ t('No One Praying') }}: {{ formatNumber(noPrayerCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="rd-goal"><strong>{{ t('Goal') }}:</strong> {{ t('Mobilize 100+ people praying daily for each DOXA people group') }}</p>
|
||||
</div>
|
||||
<GoalCard
|
||||
variant="secondary"
|
||||
:title="t('Prayer Coverage')"
|
||||
:value="formatNumber(subject.stats.peopleCommitted)"
|
||||
:value-label="t('People committed to praying')"
|
||||
:goal-label="t('Goal')"
|
||||
:goal="t('Mobilize 100+ people praying daily for each DOXA people group')"
|
||||
>
|
||||
<ProgressBar
|
||||
:max="subject.stats.count"
|
||||
:segments="prayerSegments"
|
||||
legend
|
||||
:format="formatNumber"
|
||||
:label="t('Prayer coverage progress')"
|
||||
/>
|
||||
</GoalCard>
|
||||
|
||||
<div class="rd-card rd-goal-card">
|
||||
<h2 class="rd-h2">{{ t('Adoption Progress') }}</h2>
|
||||
<div>
|
||||
<p class="rd-big">{{ formatNumber(subject.stats.adopted) }} / {{ formatNumber(subject.stats.count) }}</p>
|
||||
<p class="rd-big-label">{{ t('people groups adopted') }}</p>
|
||||
</div>
|
||||
<div class="rd-bar"><div class="rd-bar__slider" :style="{ width: adoptedPct + '%' }" /></div>
|
||||
<p class="rd-goal"><strong>{{ t('Goal') }}:</strong> {{ t('Every unengaged people group adopted by a church committed to prayer, giving, and sending') }}</p>
|
||||
</div>
|
||||
<GoalCard
|
||||
:title="t('Adoption Progress')"
|
||||
:value="formatNumber(subject.stats.adopted) + ' / ' + formatNumber(subject.stats.count)"
|
||||
:value-label="t('people groups adopted')"
|
||||
:goal-label="t('Goal')"
|
||||
:goal="t('Every unengaged people group adopted by a church committed to prayer, giving, and sending')"
|
||||
>
|
||||
<ProgressBar :value="subject.stats.adopted" :max="subject.stats.count" />
|
||||
</GoalCard>
|
||||
|
||||
<div class="rd-card rd-goal-card">
|
||||
<h2 class="rd-h2">{{ t('Engagement Progress') }}</h2>
|
||||
<div>
|
||||
<p class="rd-big">{{ formatNumber(subject.stats.engaged) }} / {{ formatNumber(subject.stats.count) }}</p>
|
||||
<p class="rd-big-label">{{ t('people groups engaged') }}</p>
|
||||
</div>
|
||||
<div class="rd-bar"><div class="rd-bar__slider" :style="{ width: engagedPct + '%' }" /></div>
|
||||
<p class="rd-goal"><strong>{{ t('Goal') }}:</strong> {{ t('No unengaged people groups by 2033') }}</p>
|
||||
</div>
|
||||
<GoalCard
|
||||
:title="t('Engagement Progress')"
|
||||
:value="formatNumber(subject.stats.engaged) + ' / ' + formatNumber(subject.stats.count)"
|
||||
:value-label="t('people groups engaged')"
|
||||
:goal-label="t('Goal')"
|
||||
:goal="t('No unengaged people groups by 2033')"
|
||||
>
|
||||
<ProgressBar :value="subject.stats.engaged" :max="subject.stats.count" />
|
||||
</GoalCard>
|
||||
</div>
|
||||
|
||||
<!-- RegionDetail.vue:144-171 — the five-column breakdown. CountryDetail has no table. -->
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ let fails = 0
|
|||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
const read = (p) => readFileSync(resolve(ROOT, p), 'utf8')
|
||||
|
||||
// area, library file, the plugin file that must consume it, the classes the library file now owns
|
||||
// area, library file, the plugin file that must consume it, the classes the library file now owns.
|
||||
// `css` names the file(s) allowed to carry those rules -- normally the component itself, but a rule set
|
||||
// two components share (the card surface) legitimately lives in one module both of them call.
|
||||
const EXTRACTED = [
|
||||
{
|
||||
lib: 'library/components/regions/RegionSectionCard.vue',
|
||||
|
|
@ -28,13 +30,33 @@ const EXTRACTED = [
|
|||
tag: 'RegionSectionCard',
|
||||
owns: ['region-card', 'countries-grid', 'country-link'],
|
||||
},
|
||||
// ── REGLIB batch 2: the Driver's "banners/cards that tell you the prayer progress" ─────────────
|
||||
{
|
||||
lib: 'library/components/regions/StatCard.vue',
|
||||
consumer: 'plugin-designs/regions-dashboard/region-dashboards.vue',
|
||||
tag: 'StatCard',
|
||||
owns: ['rd-stat'],
|
||||
},
|
||||
{
|
||||
lib: 'library/components/regions/GoalCard.vue',
|
||||
consumer: 'plugin-designs/regions-dashboard/region-dashboards.vue',
|
||||
tag: 'GoalCard',
|
||||
owns: ['rd-goal-card', 'rd-big', 'rd-goal'],
|
||||
},
|
||||
{
|
||||
lib: 'library/components/charts/ProgressBar.vue',
|
||||
consumer: 'plugin-designs/regions-dashboard/region-dashboards.vue',
|
||||
tag: 'ProgressBar',
|
||||
owns: ['rd-bar', 'rd-seg', 'rd-legend', 'rd-key'],
|
||||
},
|
||||
]
|
||||
|
||||
for (const e of EXTRACTED) {
|
||||
const name = e.lib.split('/').pop()
|
||||
const dir = e.lib.split('/').slice(-2, -1)[0]
|
||||
ok(existsSync(resolve(ROOT, e.lib)), `${name} exists in library/`)
|
||||
const plugin = read(e.consumer)
|
||||
ok(plugin.includes(`@map/components/regions/${name}`), `the plugin IMPORTS it from @map/, not a local path`)
|
||||
ok(plugin.includes(`@map/components/${dir}/${name}`), `${name}: the plugin IMPORTS it from @map/, not a local path`)
|
||||
ok(new RegExp(`<${e.tag}[\\s>]`).test(plugin), `and actually RENDERS <${e.tag}>`)
|
||||
|
||||
// the half that makes it real: the plugin must no longer own what it delegated.
|
||||
|
|
@ -52,6 +74,47 @@ for (const e of EXTRACTED) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── the card surface: ONE rule set, two components, one injected copy ───────────────────────────
|
||||
// StatCard and GoalCard both need .rd-card. A copy in each is the shape that drifts, so it lives in a
|
||||
// composable they both call, dedup'd on a stable label. This asserts that arrangement rather than
|
||||
// trusting it: the module carries the rules, both cards call it, and the plugin has neither.
|
||||
{
|
||||
const SURFACE = 'library/composables/useCardSurface.js'
|
||||
ok(existsSync(resolve(ROOT, SURFACE)), 'useCardSurface.js exists')
|
||||
const surface = read(SURFACE)
|
||||
for (const cls of ['rd-card', 'rd-card--small', 'rd-card--secondary']) {
|
||||
ok(new RegExp(`'\\.${cls}[{ ]`).test(surface), `the shared surface carries .${cls}`)
|
||||
}
|
||||
ok(/useShadowStyles\(CARD_SURFACE_CSS, 'dxlife-card-surface'\)/.test(surface),
|
||||
'and injects it under a STABLE label, so N calling components still put exactly one <style> in the root')
|
||||
for (const f of ['library/components/regions/StatCard.vue', 'library/components/regions/GoalCard.vue']) {
|
||||
ok(/useCardSurface\(\)/.test(read(f)), `${f.split('/').pop()} calls useCardSurface()`)
|
||||
}
|
||||
const dash = read('plugin-designs/regions-dashboard/region-dashboards.vue')
|
||||
ok(!/'\.rd-card[{ -]/.test(dash), 'the plugin ships NO .rd-card CSS of its own any more')
|
||||
ok(!/class="[^"]*\brd-card\b/.test(dash.slice(dash.indexOf('<template>'))),
|
||||
'and renders no .rd-card markup of its own -- all four cards are library components')
|
||||
}
|
||||
|
||||
// ── the ONE substitution in the port, asserted rather than described ────────────────────────────
|
||||
// A segment's colour moved from three prayer-specific classes (.rd-seg--full/--partial/--none) to a
|
||||
// prop. That is only faithful if the VALUES are unchanged, so: the classes are gone tree-wide, and the
|
||||
// consumer passes the very tokens they used to set.
|
||||
{
|
||||
const bar = read('library/components/charts/ProgressBar.vue')
|
||||
const dash = read('plugin-designs/regions-dashboard/region-dashboards.vue')
|
||||
for (const dead of ['rd-seg--full', 'rd-seg--partial', 'rd-seg--none', 'rd-key--full', 'rd-key--partial', 'rd-key--none']) {
|
||||
ok(!new RegExp(`'\\.${dead}[{ ]`).test(bar) && !new RegExp(`'\\.${dead}[{ ]`).test(dash),
|
||||
`.${dead} is DELETED, not left behind unreachable`)
|
||||
}
|
||||
for (const token of ['var(--dx-accent)', 'var(--dx-partial)', 'var(--dx-track)']) {
|
||||
ok(dash.includes(`color: '${token}'`), `the consumer still passes ${token} -- the colour is unchanged, only its mechanism moved`)
|
||||
}
|
||||
ok(/segments: \{ type: Array/.test(bar), 'ProgressBar takes segments as a prop, so it needs no vocabulary about prayer')
|
||||
ok(/prefers-reduced-motion/.test(bar),
|
||||
'and carries its own reduced-motion guard -- the plugin\'s reset is scoped to .rd-root and would not follow it to another consumer')
|
||||
}
|
||||
|
||||
// NEG CONTROLS: each assertion must be able to fail.
|
||||
ok(/class="[^"]*\brl-root\b/.test(read('plugin-designs/regions-dashboard/regions-list.vue')),
|
||||
'NEG CONTROL: the plugin still uses its OWN classes (.rl-root), so the "no longer uses" checks above are not vacuously true')
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import { parse } from '@vue/compiler-sfc'
|
|||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const CHIPS = join(ROOT, 'plugin-designs/research-dashboard/src/components/dashboards/DashboardFilterChips.vue')
|
||||
const CHIPS = join(ROOT, 'library/components/dashboards/DashboardFilterChips.vue') // moved by kit landing 4c-2
|
||||
const SHELL = join(ROOT, 'plugin-designs/research-dashboard/research-dashboard.vue')
|
||||
// ★ THE CHIPS THE USER ACTUALLY TOUCHES ARE NOT IN DashboardFilterChips.vue.
|
||||
// That component is exported from the kit's index.js and mounted by NOBODY — DashboardTopBar's
|
||||
|
|
|
|||
|
|
@ -143,6 +143,157 @@ if (!fails.some((f) => f.startsWith('1'))) notes.push(`insets: ${SURFACES.length
|
|||
}
|
||||
}
|
||||
|
||||
// ── 3. THE INVERTED DEPENDENCY, AND THE NO-LEGEND FALLBACK (kit landing 4b) ───────────────────
|
||||
// The rail and the chips used to IMPORT useTreeLegendColors / useTreeLegendLabels — the DOXA
|
||||
// palettes and the People code tables, which are research-specific and stay in plugin-designs.
|
||||
// That import is what stopped the rail from being able to move into the kit at all: a library
|
||||
// component importing a plugin composable is what check:no-cross-plugin-imports refuses. The shell
|
||||
// now supplies both as dashboardConfig.legend, so the panels declare what they need and the research
|
||||
// plugin keeps owning its colours.
|
||||
//
|
||||
// ★ WHAT THIS ARM DOES AND DOES NOT DO, stated because the difference matters. It drives the
|
||||
// FALLBACK EXPRESSIONS lifted out of the SFC — so "no legend yields a neutral swatch and a raw
|
||||
// value" is measured, not assumed — and it asserts the imports are gone. It does NOT mount the
|
||||
// component: mounting an SFC here needs the Vite/no-op-renderer harness check:composites-mount
|
||||
// carries, and a no-op renderer cannot show a colour anyway, so a mount would prove only "it did
|
||||
// not throw" at the cost of a dev server in a gate that otherwise runs in milliseconds. The
|
||||
// rendered result is rega's, on the NEEDS-EYES row for this landing.
|
||||
{
|
||||
const RAIL = 'library/components/dashboards/DashboardFacetRail.vue' // moved by kit landing 4c-3
|
||||
const CHIP = 'library/components/dashboards/DashboardFilterChips.vue' // moved by kit landing 4c-2
|
||||
for (const [rel, what] of [[RAIL, 'the facet rail'], [CHIP, 'the chip bar']]) {
|
||||
const src = read(rel)
|
||||
if (!src) { fails.push(`3 ${rel} is missing.`); continue }
|
||||
const code = src.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n')
|
||||
if (/from '\.\.\/\.\.\/composables\/useTreeLegend(Colors|Labels)\.js'/.test(code)) {
|
||||
fails.push(`3 ${what} still imports a useTreeLegend* composable. Those are the research palettes; a kit component importing them is the library-to-plugin dependency that stops this file from moving at all.`)
|
||||
}
|
||||
if (!/legend/.test(code)) fails.push(`3 ${what} never reads cfg.legend, so the shell has nothing to hand it.`)
|
||||
}
|
||||
// Drive the rail's two fallbacks with NO legend at all.
|
||||
const railSrc = read(RAIL) || ''
|
||||
const neutral = /const KIT_NEUTRAL_SWATCH = '(#[0-9a-fA-F]{3,8})'/.exec(railSrc)
|
||||
const cFor = /const colorFor = (\(\.\.\.a\)[^\n]*)/.exec(railSrc)
|
||||
const lFor = /const legendLabelFor = (\([^\n]*)/.exec(railSrc)
|
||||
if (!neutral || !cFor || !lFor) {
|
||||
fails.push('3 could not lift the rail\'s no-legend fallbacks (KIT_NEUTRAL_SWATCH, colorFor, legendLabelFor) — a consumer that supplies no legend would get whatever this gate can no longer see.')
|
||||
} else {
|
||||
const legend = { value: null }
|
||||
const KIT_NEUTRAL_SWATCH = neutral[1]
|
||||
const colorFor = new Function('legend', 'KIT_NEUTRAL_SWATCH', `return ${cFor[1]}`)(legend, KIT_NEUTRAL_SWATCH)
|
||||
const legendLabelFor = new Function('legend', `return ${lFor[1]}`)(legend)
|
||||
if (colorFor('religion', 'religion', 'M', '') !== KIT_NEUTRAL_SWATCH) {
|
||||
fails.push('3 with no legend the rail does not fall back to its neutral swatch — an unconfigured dashboard should look PLAIN, not coloured by accident.')
|
||||
}
|
||||
if (legendLabelFor('affinity_bloc', 'A012') !== 'A012') {
|
||||
fails.push('3 with no legend the rail does not pass the raw value through as its label — a consumer with no code tables must see the code, not blank.')
|
||||
}
|
||||
// And the research palette's own NEUTRAL must NOT be imported to get that value.
|
||||
// ★ COMMENT-STRIPPED, and this is the FIFTH time I have written this hole. The comment beside
|
||||
// KIT_NEUTRAL_SWATCH explains the decision by NAMING the research NEUTRAL, so testing the raw
|
||||
// source reported a coupling that does not exist — my own documentation failing my own check.
|
||||
// I had already fixed this shape four times in other gates and still wrote the raw-source
|
||||
// version here, which says the fix belongs in a habit rather than in each gate: every text
|
||||
// search over source strips comments first, without thinking about it.
|
||||
const railCode = railSrc.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1')
|
||||
if (/NEUTRAL/.test(railCode.replace(/KIT_NEUTRAL_SWATCH/g, ''))) {
|
||||
fails.push('3 the rail references the research palette\'s NEUTRAL. Coupling the kit\'s neutral to the research one re-creates the dependency this landing removes; they happen to match today and must stay independent.')
|
||||
}
|
||||
if (!fails.some((f) => f.startsWith('3'))) notes.push(`no-legend fallback: neutral ${KIT_NEUTRAL_SWATCH} and raw values, with the research imports gone`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. THE ROLLUP BUCKET: SENTINEL KEY, TRANSLATED LABEL (kit landing 4c-1) ───────────────────
|
||||
// aggregate()'s topN tail becomes `{ key: 'Other', other: true }`, and that English word was
|
||||
// rendered directly in both rails, in every locale. check:i18n-keys-exist could not see it because
|
||||
// there was no T() call to inspect — the string was never a lookup, it was a KEY being used as a
|
||||
// LABEL. Found while assessing 4c, and fixed before the module moved, because unfixed it would have
|
||||
// been exported to every future dashboard the moment this became kit code.
|
||||
// Both halves are asserted, and they pull in opposite directions: the KEY must stay English (the
|
||||
// rail tests it to make the row inert, and the charts rail refuses to filter on it — a localized key
|
||||
// would make the bucket clickable in Arabic on a value no row carries), while the LABEL must not be.
|
||||
{
|
||||
const AGG = 'library/composables/useFacetAggregation.js'
|
||||
const src = read(AGG)
|
||||
if (!src) notes.push(`${AGG}: not present yet (kit landing 4c-1 not applied) — skipped`)
|
||||
else {
|
||||
const code = src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1')
|
||||
const k = /export const OTHER_KEY = '([^']*)'/.exec(code)
|
||||
if (!k) fails.push(`4 ${AGG}: no OTHER_KEY export — the sentinel and its label have no single owner, which is how they drift.`)
|
||||
else if (k[1] !== 'Other') fails.push(`4 OTHER_KEY is ${JSON.stringify(k[1])}. It is a SENTINEL: the rail tests it to make the bucket inert and the charts rail refuses to filter on it, so changing it silently makes the bucket clickable on a value no row carries.`)
|
||||
if (!/export const otherLabel = \(\) =>[\s\S]{0,120}tStatic\('dashboard\.other'\)/.test(code)) {
|
||||
fails.push(`4 ${AGG}: otherLabel() does not read dashboard.other, so the bucket renders English in every locale.`)
|
||||
}
|
||||
// the catalogue has to hold it, in every locale
|
||||
const LOCALES = ['ar', 'de', 'en', 'es', 'fr', 'hi', 'it', 'pt', 'ro', 'ru', 'zh']
|
||||
const missing = LOCALES.filter((l) => { const j = read(`library/i18n/locales/${l}/dashboard.json`); return !j || !JSON.parse(j).other })
|
||||
if (missing.length) fails.push(`4 dashboard.other is missing in ${missing.length} locale(s): ${missing.join(', ')}`)
|
||||
// and the render sites must project rather than print the key
|
||||
const rail = read('library/components/dashboards/DashboardFacetRail.vue') || '' // moved by kit landing 4c-3
|
||||
if (!/label: d\.other \? otherLabel\(\) : d\.key/.test(rail)) {
|
||||
fails.push('4 the facet rail does not project the bucket label — it renders the sentinel, so every locale reads "Other".')
|
||||
}
|
||||
if (!/facet, value: d\.key/.test(rail)) {
|
||||
fails.push('4 the facet rail no longer carries d.key as the row VALUE. The value is what inertness is tested against; projecting it would make the bucket filterable.')
|
||||
}
|
||||
if (!fails.some((f) => f.startsWith('4'))) notes.push(`rollup bucket: key stays ${JSON.stringify(k[1])}, label reads dashboard.other in all ${LOCALES.length} locales`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. THE MOVED CHIP BAR: THE FILE IS ACTUALLY GONE, AND ITS DEFAULT RESOLVER IS DRIVEN ──────
|
||||
// Kit landing 4c-2 moves DashboardFilterChips.vue into library/components/dashboards/. Two things
|
||||
// can go wrong in a "move" and only one of them is visible by reading the new file:
|
||||
// (a) the old copy survives, so the tree holds two chip bars that drift apart — the failure this
|
||||
// repo has already paid for with useNeighborT/usePplrT at Jaccard 0.82;
|
||||
// (b) the kit's UNCONFIGURED behaviour is wrong, which nobody notices because every dashboard in
|
||||
// this tree supplies a resolver. go-int is the consumer that will not, at first.
|
||||
// So the arm asserts the move is complete and then LIFTS the default out of the source and RUNS it,
|
||||
// the same way arm 3 drives the rail's fallbacks. Not a mount: a no-op renderer cannot show a chip,
|
||||
// and the rendered result is rega's.
|
||||
{
|
||||
const NEW = 'library/components/dashboards/DashboardFilterChips.vue'
|
||||
const OLD = 'plugin-designs/research-dashboard/src/components/dashboards/DashboardFilterChips.vue'
|
||||
const BARREL = 'plugin-designs/research-dashboard/src/components/dashboards/index.js'
|
||||
const src = read(NEW)
|
||||
if (!src) {
|
||||
fails.push(`5 ${NEW} is missing — the kit component the barrel now points at does not exist.`)
|
||||
} else {
|
||||
if (read(OLD)) fails.push(`5 ${OLD} still exists. A move that leaves the original behind is a COPY, and two chip bars over one filterSet drift apart the moment one is edited.`)
|
||||
const barrel = read(BARREL) || ''
|
||||
if (!/DashboardFilterChips.*@map\/components\/dashboards/.test(barrel)) {
|
||||
fails.push(`5 ${BARREL} does not re-export DashboardFilterChips from the library — an importer of the barrel would resolve the path that just moved.`)
|
||||
}
|
||||
// Comments first: this file's own notes name cfg.legend and useTreeLegendLabels.
|
||||
const code = src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1')
|
||||
if (/from '\.\.\//.test(code) || /useTreeLegend/.test(code)) {
|
||||
fails.push('5 the kit chip bar imports something relative or research-specific. A library component reaching back into plugin-designs is what check:no-cross-plugin-imports refuses, and it is the one thing 4b removed to make this move possible.')
|
||||
}
|
||||
for (const dead of ['dataStore', 'searchSourceId']) {
|
||||
if (new RegExp(`inject\\(\\s*'${dead}'`).test(code)) {
|
||||
fails.push(`5 the kit chip bar still injects '${dead}', which existed only to build useTreeLegendLabels. An unused inject is a claim about the host: a new dashboard would provide it before finding out it is never read.`)
|
||||
}
|
||||
}
|
||||
const lf = /const labelFor = (\([^\n]*)/.exec(src)
|
||||
if (!lf) {
|
||||
fails.push('5 could not lift `const labelFor` out of the kit chip bar, so its unconfigured behaviour is whatever this gate can no longer see. Keep it one expression.')
|
||||
} else {
|
||||
const make = (cfg) => new Function('cfg', `return ${lf[1]}`)(cfg)
|
||||
const bare = make({})
|
||||
if (bare('affinity_bloc', 'A012') !== 'A012') {
|
||||
fails.push('5 with NO resolver the kit chip bar does not pass a scalar value through — a dashboard that has not wired its code tables must see the code, not blank.')
|
||||
}
|
||||
if (bare('religion', ['Islam', 'Hinduism']) !== 'Islam, Hinduism') {
|
||||
fails.push(`5 with NO resolver a MULTI-VALUE filter (spec D1) renders as ${JSON.stringify(String(bare('religion', ['Islam', 'Hinduism'])))} instead of a joined list. Configured and unconfigured must differ in whether CODES RESOLVE, never in punctuation — a comma-jammed "A012,A013" reads as a rendering bug rather than as missing configuration.`)
|
||||
}
|
||||
const wired = make({ legend: { labelFor: () => 'SUPPLIED' } })
|
||||
if (wired('affinity_bloc', 'A012') !== 'SUPPLIED') {
|
||||
fails.push('5 a supplied cfg.legend.labelFor does not win over the default, so a dashboard that configures its labels would still show raw codes.')
|
||||
}
|
||||
if (!fails.some((f) => f.startsWith('5'))) notes.push('kit chip bar: moved with no copy left behind, no dead injects, and its no-resolver default passes scalars through and joins lists')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of notes) console.log(' · ' + n)
|
||||
if (fails.length) {
|
||||
console.error(`\n${TAG}: FAIL`)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { tStatic } from '@map/i18n/index.js'
|
|||
*/
|
||||
import { inject, computed, ref } from 'vue'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { aggregate, buildDonut, buildBars, lbl, buildCountryRegionColors } from '../../composables/useDashboardAggregations.js'
|
||||
import { aggregate, buildDonut, buildBars, lbl, buildCountryRegionColors, OTHER_KEY, otherLabel } from '../../composables/useDashboardAggregations.js'
|
||||
import { buildSearchAccessors, makeSearchMatcher, filterRowsBySearch } from '../../composables/useDashboardShell.js'
|
||||
|
||||
/**
|
||||
|
|
@ -95,7 +95,13 @@ const isActive = (facet, key) => bus.isFilterActive(facet, key)
|
|||
// derived in English rather than taken from the API, so it is the only one that needs projecting;
|
||||
// every other facet's values arrive already localized. Returns the key unchanged for anything else,
|
||||
// so a new facet is never silently rewritten.
|
||||
const legendLabel = (facet, key) => (facet === 'religion_family' ? religionFamilyLabel(key) : key)
|
||||
// Two projections, one function, and both follow the same rule: the key crosses, the label is read.
|
||||
// OTHER_KEY is the rollup sentinel aggregate() produces; it must stay English on the wire (this
|
||||
// component refuses to filter on it by testing `key === 'Other'`) and be localized for a reader.
|
||||
const legendLabel = (facet, key) => {
|
||||
if (key === OTHER_KEY) return otherLabel()
|
||||
return facet === 'religion_family' ? religionFamilyLabel(key) : key
|
||||
}
|
||||
// true when this chart's own facet has an active selection → drives the pop-out / dim-others state.
|
||||
const hasSelection = (facet) => filterSet[facet] != null // any selection, scalar or array
|
||||
|
||||
|
|
@ -339,7 +345,7 @@ useShadowStyles(`
|
|||
<svg class="rd-donut" :class="{ 'has-sel': hasSelection(chart.facet) }" viewBox="0 0 132 132" role="img" :aria-label="chart.title + ' donut'">
|
||||
<path v-for="s in chart.geometry" :key="s.key" :d="s.path" :fill="s.color" class="rd-slice"
|
||||
:class="{ on: isActive(chart.facet, s.key) }" :data-facet="chart.facet" :data-key="s.key">
|
||||
<title>{{ s.key }}: {{ s.value }} ({{ s.pct }}%)</title>
|
||||
<title>{{ legendLabel(chart.facet, s.key) }}: {{ s.value }} ({{ s.pct }}%)</title>
|
||||
</path>
|
||||
</svg>
|
||||
<div class="rd-legend">
|
||||
|
|
|
|||
|
|
@ -177,6 +177,13 @@ let added = false
|
|||
// container calls map.resize() on ANY container-size change, so the canvas always fills its cell.
|
||||
// General-correct (not table-specific): also covers window/rail/facet reflow. rAF-coalesced.
|
||||
let containerRO = null
|
||||
// The scroll-zoom resync observer and its window listener. Both are CREATED inside onMounted,
|
||||
// which is async — so they are declared out here and torn down by the one top-level
|
||||
// onBeforeUnmount below. A lifecycle hook registered after that callback's first `await` never
|
||||
// attaches to this instance (Vue has no active instance by then), so the teardown cannot live
|
||||
// next to the code that creates these; it has to live in a hook registered during setup.
|
||||
let scrollRO = null
|
||||
let scrollResync = null
|
||||
let offToggleTable = null
|
||||
let offLayoutResizing = null
|
||||
let _resizeRaf = 0
|
||||
|
|
@ -1219,21 +1226,16 @@ onMounted(async () => {
|
|||
// observer may again be useful if a future host reintroduces the unbounded-map-row precondition)
|
||||
// rather than torn out, but do not read `resync` as gating anything today.
|
||||
syncScrollZoomToHost(m)
|
||||
const resync = () => syncScrollZoomToHost(m)
|
||||
let ro = null
|
||||
scrollResync = () => syncScrollZoomToHost(m)
|
||||
try {
|
||||
ro = new ResizeObserver(resync)
|
||||
scrollRO = new ResizeObserver(scrollResync)
|
||||
const host = (m.getContainer && m.getContainer()) || null
|
||||
if (host) ro.observe(host)
|
||||
if (host) scrollRO.observe(host)
|
||||
const port = host ? findScrollportAncestor(host) : null
|
||||
if (port) ro.observe(port)
|
||||
if (document.body) ro.observe(document.body)
|
||||
if (port) scrollRO.observe(port)
|
||||
if (document.body) scrollRO.observe(document.body)
|
||||
} catch (_) { /* no ResizeObserver: the initial call above still answers for a static page */ }
|
||||
window.addEventListener('resize', resync)
|
||||
onBeforeUnmount(() => {
|
||||
try { ro && ro.disconnect() } catch (_) { /* already torn down */ }
|
||||
window.removeEventListener('resize', resync)
|
||||
})
|
||||
window.addEventListener('resize', scrollResync)
|
||||
// Keep the zoom buttons' enabled state in step with the live camera. 'zoom' fires only during
|
||||
// an actual zoom; 'zoomend' catches the settle. Both just set two booleans (no camera writes).
|
||||
m.on('zoom', syncZoomLimits)
|
||||
|
|
@ -1277,6 +1279,8 @@ onBeforeUnmount(() => {
|
|||
try { offToggleTable && offToggleTable() } catch (_) {} offToggleTable = null
|
||||
try { offLayoutResizing && offLayoutResizing() } catch (_) {} offLayoutResizing = null
|
||||
if (containerRO) { try { containerRO.disconnect() } catch (_) {} containerRO = null }
|
||||
if (scrollRO) { try { scrollRO.disconnect() } catch (_) {} scrollRO = null }
|
||||
if (scrollResync) { try { window.removeEventListener('resize', scrollResync) } catch (_) {} scrollResync = null }
|
||||
if (attribRO) { try { attribRO.disconnect() } catch (_) {} attribRO = null } attribEl = null
|
||||
if (_resizeRaf) { cancelAnimationFrame(_resizeRaf); _resizeRaf = 0 }
|
||||
// Detach the zoom-limit listeners before destroy() so they can never run on a dead map.
|
||||
|
|
|
|||
|
|
@ -179,7 +179,13 @@ const { labelFor } = useTreeLegendLabels(cfg, inject('dataStore', null), inject(
|
|||
const activeChips = computed(() => {
|
||||
const out = Object.keys(filterSet).map((k) => {
|
||||
const v = filterSet[k]
|
||||
return { name: k, label: `${FACETS.value[k]?.label || k}: ${k === PRAYER_KEY ? v : labelFor(k, v)}` }
|
||||
// An ARRAY is a multi-value filter (D1). labelFor() joins one for display at its single owner,
|
||||
// but the prayer arm skips labelFor by the note above, so it has to do its own join — otherwise
|
||||
// a two-value pick renders as "A,B" with no spacing. LATENT, not live: nothing in this plugin
|
||||
// calls toggleFilterValue, so no caller produces a prayer array today. Closed here because the
|
||||
// next caller that does would not think to look in a chip label for the reason.
|
||||
const shown = k === PRAYER_KEY ? (Array.isArray(v) ? v.join(', ') : v) : labelFor(k, v)
|
||||
return { name: k, label: `${FACETS.value[k]?.label || k}: ${shown}` }
|
||||
})
|
||||
// ★ THE TYPED SEARCH IS A FILTER AND MUST LOOK LIKE ONE (Driver 2026-09-23: "I searched Bhutan
|
||||
// and it did not switch the toggle to show the country was selected"). MEASURED: typing narrows
|
||||
|
|
@ -192,7 +198,14 @@ const activeChips = computed(() => {
|
|||
// (First attempt went into DashboardFilterChips.vue -- a component the topbar dedup pass had
|
||||
// already unmounted; THIS band is the documented single source of truth for chips.)
|
||||
const term = (bus?.searchTerm?.value || '').trim()
|
||||
if (term) out.push({ name: '__search__', label: `Search: ${term}` })
|
||||
// ★ THE SEARCH CHIP WAS THE ONE CHIP STILL IN ENGLISH, and the string it needed already existed.
|
||||
// dashboard.search_live is in all eleven locales, written for exactly this line — and it has to
|
||||
// be, because the punctuation is not assemblable here: fr is "Recherche : {term}" with a narrow
|
||||
// space before the colon and zh is "搜索:{term}" with a full-width one. Composing 'Search' + ':'
|
||||
// in code would get both wrong even with a translated word. It was simply never wired, so ten
|
||||
// locales rendered an English label beside localized facet chips. tStatic takes no params, so
|
||||
// {term} is filled here rather than by the catalogue.
|
||||
if (term) out.push({ name: '__search__', label: (tStatic('dashboard.search_live') || 'Search: {term}').replace('{term}', term) })
|
||||
return out
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,18 @@
|
|||
* useGeocoderSearch composable via a busAdapter sink (kind-switch stays in the composable).
|
||||
*/
|
||||
export { default as DashboardTopBar } from './DashboardTopBar.vue'
|
||||
export { default as DashboardFacetRail } from './DashboardFacetRail.vue'
|
||||
// MOVED to library/components/dashboards/ (kit landing 4c-3), re-exported for one cycle like the
|
||||
// chip bar before it, so research-dashboard.vue's import list does not change in the landing that
|
||||
// moves the file.
|
||||
export { default as DashboardFacetRail } from '@map/components/dashboards/DashboardFacetRail.vue'
|
||||
export { default as DashboardDataTable } from './DashboardDataTable.vue'
|
||||
export { default as DashboardChartsRail } from './DashboardChartsRail.vue'
|
||||
export { default as DashboardDetailDrawer } from './DashboardDetailDrawer.vue'
|
||||
export { default as DashboardFilterChips } from './DashboardFilterChips.vue'
|
||||
// MOVED to library/components/dashboards/ (kit landing 4c-2) and re-exported here for one cycle,
|
||||
// so no consumer's import line has to change in the same landing that moves the file. Anything
|
||||
// importing DashboardFilterChips from this barrel keeps working; the re-export goes when the rail
|
||||
// follows it in 4c-3 and this barrel becomes the library's own.
|
||||
export { default as DashboardFilterChips } from '@map/components/dashboards/DashboardFilterChips.vue'
|
||||
export { default as DashboardBreadcrumb } from './DashboardBreadcrumb.vue'
|
||||
export { default as DashboardMapPanel } from './DashboardMapPanel.vue'
|
||||
export { default as DashboardSearchHeader } from './DashboardSearchHeader.vue'
|
||||
|
|
|
|||
|
|
@ -49,15 +49,14 @@ import { PALETTE as RELIGION_PALETTE, RELIGION_FAMILIES, getReligionFamily } fro
|
|||
* than re-implemented, so chart and pin can never drift.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A deliberately NEUTRAL tone for a value this facet has no palette for — matching the tree
|
||||
* legend's NEUTRAL exactly. An uncoloured slice must read as "no colour strategy for this key",
|
||||
* never as a confident member of some other family. Critically it is STABLE: the same key gets
|
||||
* the same neutral every time, so nothing changes colour when a filter reorders the list.
|
||||
*/
|
||||
export const NEUTRAL = '#c4c8c0'
|
||||
// ★ THE GENERIC HALF MOVED OUT (kit landing 4c-1) AND IS RE-EXPORTED HERE FOR ONE CYCLE.
|
||||
// lbl() and aggregate() are pure counting and now live in @map/composables/useFacetAggregation.js.
|
||||
// Everything still in this file — colorFor and its hardcoded facet branches, the religion palettes,
|
||||
// the donut and bar builders — is research-specific and stays. DashboardChartsRail imports those and
|
||||
// is untouched by this commit.
|
||||
export { lbl, aggregate, OTHER_KEY, otherLabel } from '@map/composables/useFacetAggregation.js'
|
||||
import { lbl, aggregate } from '@map/composables/useFacetAggregation.js'
|
||||
|
||||
export const lbl = (v) => (v === null || v === undefined ? '' : (typeof v === 'object' ? (v.label ?? v.value ?? '') : v))
|
||||
|
||||
// Place: the facet emits the localized label but the authoritative palette is slug-keyed
|
||||
// (doxa-region.js documents that the label is localized per `lang` and the slug is stable).
|
||||
|
|
@ -134,19 +133,6 @@ function religionLetterFromLabel(key, families) {
|
|||
return 'U'
|
||||
}
|
||||
|
||||
/** Count rows by a key accessor; collapse the long tail into 'Other'. */
|
||||
export function aggregate(rows, keyFn, topN) {
|
||||
const m = new Map()
|
||||
for (const r of rows) { const k = keyFn(r); if (k === null || k === undefined || k === '') continue; m.set(k, (m.get(k) || 0) + 1) }
|
||||
let arr = [...m.entries()].map(([key, value]) => ({ key, value })).sort((a, b) => b.value - a.value)
|
||||
if (topN && arr.length > topN) {
|
||||
const top = arr.slice(0, topN)
|
||||
const other = arr.slice(topN).reduce((s, x) => s + x.value, 0)
|
||||
if (other > 0) top.push({ key: 'Other', value: other, other: true })
|
||||
arr = top
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* COUNTRY -> REGION HUE. The Driver's ask: "countries should match the colour of their regions."
|
||||
|
|
|
|||
|
|
@ -23,12 +23,13 @@
|
|||
* responsive switch, collapsible table dock, segmented mobile view, and the location.hash sync.
|
||||
* Only the SOURCE of the per-dashboard constants changed: from hard-coded → config.
|
||||
*/
|
||||
import { exactMatches, fuzzyMatches, resolveFuzzyValues } from '@map/components/search/fuzzy.js'
|
||||
import { provide, inject, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { createSelectionBus } from '@map/composables/useSelectionBus.js'
|
||||
import { buildSearchAccessors, makeSearchMatcher, filterRowsBySearch } from '@map/composables/useDashboardSearch.js'
|
||||
import { useDashboardData } from './useDashboardData.js'
|
||||
import { useCsvExport } from './useCsvExport.js'
|
||||
import { useCsvExport } from '@map/composables/useCsvExport.js'
|
||||
import { useTreeLegendLabels } from './useTreeLegendLabels.js'
|
||||
import { useTreeLegendColors } from './useTreeLegendColors.js'
|
||||
import { DataSourceManager } from '@map/api/DataSourceManager.js'
|
||||
import sourcesConfig from '@map/api/sources.json'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
|
@ -40,12 +41,80 @@ const mapValues = (obj, fn) =>
|
|||
// number formatter (same as the old inline `fmt`); exported for the profile's KPI strip.
|
||||
export const fmt = (n) => (n === null || n === undefined ? '—' : Number(n).toLocaleString('en-US'))
|
||||
|
||||
// ★ THE SEARCH MOVED OUT (kit landing 4a) AND IS RE-EXPORTED FROM HERE FOR ONE CYCLE.
|
||||
// Eight files import these three names from this module; re-exporting means the move and its
|
||||
// consumers do not have to land together, exactly as the composable shims did in landings 1-3.
|
||||
// Unlike those, this one is a barrel line rather than a file, so there is nothing to delete later —
|
||||
// only a line to remove once the eight importers point at the library directly.
|
||||
export { buildSearchAccessors, makeSearchMatcher, filterRowsBySearch } from '@map/composables/useDashboardSearch.js'
|
||||
/**
|
||||
* Build the free-text search accessors for a dashboard config (header-1 search bar).
|
||||
* config.searchKeys (facet keys) → those facet accessors; else the string columns' accessors.
|
||||
* ONE declaration reused by the shell (KPIs) AND every panel (table/map/charts/facets) so the
|
||||
* searchable text can never drift from what the panels show, and the substring match is not
|
||||
* copy-pasted into each panel (kills the very duplication DOXA-LIB-2 removed).
|
||||
*/
|
||||
export function buildSearchAccessors(config) {
|
||||
const FACET_ACC = mapValues(config.facets, (f) => f.get)
|
||||
const COL_ACC = Object.fromEntries((config.columns || []).map((c) => [c.key, c.get]))
|
||||
const keys = Array.isArray(config.searchKeys) ? config.searchKeys : null
|
||||
if (keys) return keys.map((k) => FACET_ACC[k] || COL_ACC[k]).filter((fn) => typeof fn === 'function')
|
||||
return (config.columns || []).filter((c) => c.type !== 'num' && typeof c.get === 'function').map((c) => c.get)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable predicate: does `row` match the bus's current free-text search term?
|
||||
* Returns true when there is no term (identical to pre-search behaviour). `accessors` is the
|
||||
* array from buildSearchAccessors(config). Case-insensitive substring over each accessor's value.
|
||||
*/
|
||||
export function makeSearchMatcher(bus, accessors) {
|
||||
// ★ FUZZY, NOT SUBSTRING (Driver 2026-09-23: "add an n to the end of a country name" must
|
||||
// still find it -- check what DX-life's search bar bakes in). DX-life runs real Fuse.js with
|
||||
// { threshold: 0.4, ignoreLocation: true, minMatchCharLength: 3 } (UupgsList.vue:288), so its
|
||||
// search tolerates typos. This matcher was a plain .includes() -- the one search surface that
|
||||
// silently lost that behaviour in the migration (measured: 'Bhutan' 13 rows, 'Bhutann' 0).
|
||||
// It now delegates to the shared fuzzyMatches() in @map fuzzy.js -- the same matcher the
|
||||
// grid's Fuse uses -- with the same DX-life options, so every search surface agrees.
|
||||
// Row-level predicates for the TIERED contract (see fuzzy.js exactMatches): the caller
|
||||
// (filterRowsBySearch below) runs the exact tier over the whole row set first and falls back
|
||||
// to the fuzzy tier only when exact yields nothing, so an exact term agrees with the facet.
|
||||
const test = (row, pred) => {
|
||||
const needle = (bus?.searchTerm?.value || '').trim()
|
||||
if (!needle) return true
|
||||
for (const acc of accessors) {
|
||||
const v = acc(row)
|
||||
if (v != null && pred(String(v), needle, { threshold: 0.4, minMatchCharLength: 3 })) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const matcher = (row) => test(row, fuzzyMatches)
|
||||
matcher.exact = (row) => test(row, exactMatches)
|
||||
matcher.active = () => !!(bus?.searchTerm?.value || '').trim()
|
||||
matcher.term = () => (bus?.searchTerm?.value || '').trim()
|
||||
matcher.accessors = accessors
|
||||
return matcher
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the TIERED search to a row set:
|
||||
* 1. exact substring -- an exact term returns exactly the rows the equivalent facet selects.
|
||||
* 2. facet-VALUE resolution (Driver: "Austrian or Canadian... a selection of that country --
|
||||
* the table under the map shows the result for that country"): the term is resolved by
|
||||
* whole-value edit distance against each accessor's DISTINCT values, and the rows returned
|
||||
* are those carrying a resolved value -- byte-identical to picking it as a filter.
|
||||
* 3. the generic windowed fuzzy rescue, only when nothing resolves.
|
||||
*/
|
||||
export function filterRowsBySearch(rows, matcher) {
|
||||
if (!matcher.active()) return rows
|
||||
const exact = rows.filter(matcher.exact)
|
||||
if (exact.length) return exact
|
||||
const term = matcher.term()
|
||||
// ONE pool across every accessor's distinct values, so the GLOBAL best match wins (see
|
||||
// resolveFuzzyValues: ties at the minimal distance stay, everything farther drops --
|
||||
// 'Canadian' resolves to Canada, not to 47 rows of <=3-edit neighbours across facets).
|
||||
const distinct = new Set()
|
||||
for (const acc of matcher.accessors) for (const r of rows) { const v = acc(r); if (v != null && v !== '') distinct.add(String(v)) }
|
||||
const winners = new Set(resolveFuzzyValues(term, [...distinct], { threshold: 0.4 }))
|
||||
if (winners.size) {
|
||||
const resolved = rows.filter((r) => matcher.accessors.some((acc) => { const v = acc(r); return v != null && winners.has(String(v)) }))
|
||||
if (resolved.length) return resolved
|
||||
}
|
||||
return rows.filter(matcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic KPI computation driven by config.kpis. Each kpi def declares a `kind`:
|
||||
|
|
@ -308,6 +377,22 @@ export function useDashboardShell(config) {
|
|||
useHashSync(bus, labelFor)
|
||||
const exporter = useCsvExport({ rowStore: data.rowStore, rows: data.rows, selectionSet: bus.selectionSet, filterSet: bus.filterSet, FACET_ACC, dataOpts: config.data, labelFor, rowMatches: bus.rowMatchesFilters })
|
||||
|
||||
// ★ THE RESEARCH LEGEND, HANDED TO THE PANELS AS DATA (kit landing 4b).
|
||||
// The facet rail and the chip bar used to IMPORT useTreeLegendColors and useTreeLegendLabels
|
||||
// directly. Those two files are the DOXA palettes and the People code tables — research-specific
|
||||
// by this kit's own reckoning, and they stay in plugin-designs. A library component importing
|
||||
// them would be a library-to-plugin dependency, which check:no-cross-plugin-imports rightly
|
||||
// refuses, so the rail could never have become kit code by moving alone.
|
||||
// So the dependency inverts: the panels declare what they need (two functions) and the SHELL
|
||||
// supplies it. The shell is the right supplier because it is the only thing that already has the
|
||||
// config, the dataStore and the source id these composables require, and because a composable
|
||||
// cannot be called at config-module scope.
|
||||
// It goes on dashboardConfig rather than through a new inject key, per section 5.1 of the spec: a
|
||||
// panel that needs a new capability gets a new CONFIG key, never a new channel. Merged rather than
|
||||
// assigned, so a profile that supplies its own `legend` wins and is not silently overwritten.
|
||||
const { colorFor } = useTreeLegendColors()
|
||||
if (!config.legend) config.legend = { colorFor, labelFor }
|
||||
|
||||
provide('selectionBus', bus)
|
||||
provide('dashboardData', data)
|
||||
provide('dashboardConfig', config)
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const { createSelectionBus } = await import(BUS)
|
|||
// re-export shim, found no predicate to object to, and PASSED VACUOUSLY — a gate going quiet
|
||||
// about the thing it guards, which is worse than one that fails.
|
||||
'library/composables/useCsvExport.js',
|
||||
'plugin-designs/research-dashboard/src/components/dashboards/DashboardFacetRail.vue',
|
||||
'library/components/dashboards/DashboardFacetRail.vue', // follows the file (kit landing 4c-3)
|
||||
'plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue',
|
||||
'plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -119,6 +119,34 @@ for (const p of catalogues) {
|
|||
// missing keys — a check that fails on its own documentation is a check nobody trusts.
|
||||
const files = listFiles((p) => /\.(vue|js|mjs)$/.test(p) && !p.includes('/scripts/') && !p.includes('/locales/'))
|
||||
|
||||
// ── ★ A TRANSLATOR CAN ARRIVE FROM A COMPOSABLE, NOT ONLY FROM A LOCAL WRAPPER ───────────────
|
||||
// go-int planted a poison key behind `const T = useGiT()` and this gate PASSED. Three failures of
|
||||
// the same shape stacked up: (1) the file never writes the word tStatic, so the precondition below
|
||||
// skipped it outright; (2) `useGiT` is not `tStatic`, so aliasesIn() found no alias at all; and
|
||||
// (3) the alias is PREFIXED — useOptionalT carries a namespace — while its call sites pass BARE
|
||||
// keys, which the full-key pattern cannot match because it requires a dot. Any one of the three
|
||||
// alone would have hidden the key. A census says this was never one bundle's problem: 26 alias
|
||||
// sites across three factories (useOptionalT, useNeighborT, usePplrT).
|
||||
//
|
||||
// THE NAMESPACE IS READ, NOT LISTED, for the same reason the wrapper names are. Every factory in
|
||||
// this tree is one line — `return useOptionalT('neighbor')` — so following it costs nothing, and
|
||||
// the next useXxxT() is covered the day it is written instead of the day someone remembers to come
|
||||
// back here. A factory whose namespace this gate cannot read is left alone rather than guessed at.
|
||||
const FACTORY_NS = new Map([['useOptionalT', null], ['useOptionalI18n', null]]) // null = read the call site's own first argument
|
||||
const factoriesIn = (src, into) => {
|
||||
for (const m of src.matchAll(/export\s+function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{\s*return\s+useOptional(?:T|I18n)\s*\(\s*'([^']*)'/g)) into.set(m[1], m[2])
|
||||
return into
|
||||
}
|
||||
for (const f of files) {
|
||||
let src = ''
|
||||
try { src = readFile(f) || '' } catch (_) { continue }
|
||||
if (src.includes('useOptional')) factoriesIn(src, FACTORY_NS)
|
||||
}
|
||||
let FACTORIES = [...FACTORY_NS.keys()].join('|')
|
||||
let fallbackOnly = 0
|
||||
const seesFile = (src) => !!src && (src.includes('tStatic') || src.includes('$t(') || src.includes(' t(')
|
||||
|| new RegExp(`(?:${FACTORIES})\\s*\\(`).test(src))
|
||||
|
||||
// ── ★ ALIASES ARE DISCOVERED PER FILE, NOT HARDCODED ─────────────────────────────────────────
|
||||
// The first version of this gate matched tStatic/$t/t and found four missing keys while sailing
|
||||
// past the ten it was written for, because the house pattern is a file-local wrapper. Adding `T` by
|
||||
|
|
@ -153,9 +181,78 @@ function aliasesIn(src) {
|
|||
else if (/=\s*tStatic\s*$/.test(body.trim())) full.add(name)
|
||||
}
|
||||
}
|
||||
// A NAME BOUND FROM A FACTORY: `const t = useOptionalT('connectivity')`, `const nt =
|
||||
// useNeighborT()`, `const { t: T, te } = useOptionalI18n('gospel')`.
|
||||
for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s+(?:([A-Za-z_$][\\w$]*)|\\{([^}]*)\\})\\s*=\\s*(${FACTORIES})\\s*\\(([^\\n]*)`, 'g'))) {
|
||||
const [, plain, destructured, factory, argsTail] = m
|
||||
const declared = FACTORY_NS.get(factory)
|
||||
const ns = declared === null || declared === undefined ? (/^\s*'([^']*)'/.exec(argsTail)?.[1] ?? null) : declared
|
||||
if (ns === null) continue
|
||||
const names = plain ? [plain]
|
||||
: [...String(destructured).matchAll(/\bt\s*:\s*([A-Za-z_$][\w$]*)|(?:^|,)\s*(t)\s*(?=,|$)/g)].map((x) => x[1] || x[2]).filter(Boolean)
|
||||
for (const n of names) {
|
||||
if (ns) {
|
||||
// qualify() prefixes a BARE key and leaves an already-qualified one alone, so ONE name is
|
||||
// both shapes: prefixed for t('map_title'), full-key for t('connectivity.map_title'). The
|
||||
// two patterns below are disjoint — one demands a dot, the other forbids it — so
|
||||
// registering the name twice double-counts nothing.
|
||||
prefixed.set(n, [...new Set([...(prefixed.get(n) || []), ns])])
|
||||
full.add(n)
|
||||
} else {
|
||||
// ★ AN EMPTY NAMESPACE PLUS A BARE KEY WOULD BE OUTSIDE THIS GATE'S MODEL, so it is counted
|
||||
// rather than assumed away. useOptionalT('') leaves the key alone, so t('approx') would ask
|
||||
// for a TOP-LEVEL key, while the catalogue is indexed area-first ("aria.foo") the way
|
||||
// tStatic walks it — unfindable here either way. MEASURED at zero: all five empty-namespace
|
||||
// sites (poster-wall, upg-100-mfe, resources-map, ApproxPinNote, FilterDropdown) pass FULLY
|
||||
// DOTTED keys, so every one of them is checked by the full-key pass above and nothing is
|
||||
// being waved through. The counter stays because the shape is writable, and the day someone
|
||||
// writes it this prints a stated limit instead of a clean result.
|
||||
full.add(n)
|
||||
fallbackOnly += (src.match(new RegExp(`(?:^|[^\\w$.])${n}\\(\\s*'[\\w-]+'`, 'g')) || []).length
|
||||
}
|
||||
}
|
||||
}
|
||||
return { full, prefixed }
|
||||
}
|
||||
|
||||
// ── ★ SELF-TEST, BECAUSE THIS GATE ALREADY PASSED A PLANTED POISON KEY ────────────────────────
|
||||
// go-int planted `T('poison_key')` behind `const T = useGiT()` and this gate reported the tree
|
||||
// clean. That is the failure a check cannot be forgiven for: a red gate gets fixed, a green one that
|
||||
// should be red gets BELIEVED, and this one guards goal (d). Measured before and after, with the
|
||||
// same poison on disk: the version on main PASSES it, this version FAILS it as
|
||||
// `neighbor.zzz_not_a_real_key (via nt())`. Coverage went 683 -> 743 full-key and 49 -> 56 prefixed
|
||||
// call sites, so 67 sites that were being scored as clean were simply not being read.
|
||||
//
|
||||
// The three things asserted are the three that were independently broken; any one of them alone
|
||||
// hid the key, so testing them together would not have told me which was fixed.
|
||||
{
|
||||
const FIXTURE_FACTORY = "export function useGiT() {\n return useOptionalT('gospelInternet')\n}"
|
||||
const FIXTURE_SITE = "const T = useGiT()\nconst a = T('poison_key')\nconst b = T('gospelInternet.qualified_key')"
|
||||
// Registered in the REAL map, because aliasesIn() resolves a factory's namespace through it — a
|
||||
// copy would have the self-test exercise a lookup the scan does not use.
|
||||
const probe = factoriesIn(FIXTURE_FACTORY, FACTORY_NS)
|
||||
const saved = FACTORIES
|
||||
FACTORIES = [...probe.keys()].join('|')
|
||||
const fails = []
|
||||
// 1. the factory's namespace is READ from its one-line body, not guessed.
|
||||
if (probe.get('useGiT') !== 'gospelInternet') fails.push('a useXxxT() factory\'s namespace is not read from its body')
|
||||
// 2. a file whose ONLY translator comes from a composable is not skipped before anything runs.
|
||||
if (!seesFile(FIXTURE_SITE)) fails.push('a file with no tStatic but a composable translator is still skipped')
|
||||
// 3. the binding becomes BOTH shapes, because qualify() prefixes a bare key and leaves a
|
||||
// qualified one alone — bare-only would miss T('poison_key'), full-only would miss nothing
|
||||
// visible and is exactly how this passed before.
|
||||
const al = aliasesIn(FIXTURE_SITE)
|
||||
if (!(al.prefixed.get('T') || []).includes('gospelInternet')) fails.push('a composable-bound alias is not registered as PREFIXED')
|
||||
if (!al.full.has('T')) fails.push('a composable-bound alias is not also registered as FULL-key')
|
||||
FACTORIES = saved
|
||||
FACTORY_NS.delete('useGiT') // the fixture's factory is not part of this tree
|
||||
if (fails.length) {
|
||||
console.error(`${TAG}: SELF-TEST FAILED — ${fails.join('; ')}.`)
|
||||
console.error(' This gate cannot be trusted while blind to a composable-bound translator: that is how a planted key passed.')
|
||||
process.exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
const missing = new Map() // key -> { sites: [], via: Set }
|
||||
let literalCount = 0, prefixedCount = 0, dynamicCount = 0
|
||||
const lineAt = (src, i) => src.slice(0, i).split('\n').length
|
||||
|
|
@ -173,7 +270,10 @@ const record = (key, site, via) => {
|
|||
const callSites = new Map() // bundle -> count
|
||||
for (const f of files) {
|
||||
const src = readFile(f)
|
||||
if (!src || !src.includes('tStatic') && !src.includes('$t(') && !src.includes(' t(')) continue
|
||||
// seesFile()'s last clause is not decoration: a file whose only translator comes from a composable
|
||||
// never writes the word tStatic, and skipping it here is how the poison key got past everything
|
||||
// below. The self-test above drives this exact predicate for that reason.
|
||||
if (!seesFile(src)) continue
|
||||
const { full, prefixed } = aliasesIn(src)
|
||||
const names = [...full, ...prefixed.keys()].map((n) => n.replace('$', '\\$')).join('|')
|
||||
if (!names) continue
|
||||
|
|
@ -222,6 +322,11 @@ console.log(` · ${KEYS.size} English keys across ${catalogues.length} catalogu
|
|||
if (dynamicCount) {
|
||||
console.log(` · ${dynamicCount} DYNAMIC key expression(s) not checked — this gate will not invent the key set a runtime expression might produce.`)
|
||||
}
|
||||
if (fallbackOnly) {
|
||||
console.log(` · ${fallbackOnly} bare-key call site(s) behind useOptionalT('') are OUTSIDE this model and not checked: an empty namespace leaves the key`)
|
||||
console.log(` unqualified, while the catalogue is indexed area-first the way tStatic walks it. Those sites pass their own literal fallback MAP`)
|
||||
console.log(` rather than the catalogue, by the decision recorded in library/composables/useOptionalT.js — a limit, not a clean result.`)
|
||||
}
|
||||
{
|
||||
const bundles = listFiles((p) => /^plugin-designs\/[^/]+\//.test(p)).map((p) => p.split('/')[1])
|
||||
const silent = [...new Set(bundles)].filter((b) => !callSites.get(b)).sort()
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-3rrykWo456uGE2YmJ/i7AeehqxCWQe0bVDjDOj5VEwaPZV5pFw4iegauoNaZMyDR" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-I45T4m8wem7bZa+vwYOeNA2bLE0UQwsQ44OPqWnGXO569NQ/822/a013lKWpUC0U" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-SftV2pzJtasZnTIxHPqlL4QTf6juY2qb+eoUEwSsjtU/ldg7qPmbjAjr+uNrD7Cf" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-4//gka76np8NjhhynRAoi+IwIomgQsIz8IG27CXFo9rBWS/isxI+Rka1nRLjj1fY" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-SftV2pzJtasZnTIxHPqlL4QTf6juY2qb+eoUEwSsjtU/ldg7qPmbjAjr+uNrD7Cf" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-4//gka76np8NjhhynRAoi+IwIomgQsIz8IG27CXFo9rBWS/isxI+Rka1nRLjj1fY" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../big-picture.js" integrity="sha384-SftV2pzJtasZnTIxHPqlL4QTf6juY2qb+eoUEwSsjtU/ldg7qPmbjAjr+uNrD7Cf" crossorigin="anonymous"></script>
|
||||
<script src="../big-picture.js" integrity="sha384-4//gka76np8NjhhynRAoi+IwIomgQsIz8IG27CXFo9rBWS/isxI+Rka1nRLjj1fY" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-PX/Ip+YNyNzpLKKUMO7ud3cQRKx39yp5Xom4+y/HgK5x4JhtEdJnaamoNo4ooEPI" crossorigin="anonymous"></script>
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-SwGjy6nKza+3ths+kDMc7srH9hg4Qgcx7SZF/UzGuw9oXEQuDYtXDY9nsCsQXN5s" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-PX/Ip+YNyNzpLKKUMO7ud3cQRKx39yp5Xom4+y/HgK5x4JhtEdJnaamoNo4ooEPI" crossorigin="anonymous"></script>
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-SwGjy6nKza+3ths+kDMc7srH9hg4Qgcx7SZF/UzGuw9oXEQuDYtXDY9nsCsQXN5s" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../demo-map.js" integrity="sha384-S96oNhtx2xtbFL2YLgOlHA0IkLgz80K08+NVoO8qpHXb2xsJSz3CqVHL89QxlSUX" crossorigin="anonymous"></script>
|
||||
<script src="../demo-map.js" integrity="sha384-U61Eq4Jc5slgktUQnAG2Hgy+131x34DxfqDzGwXVcKFSwpr/lCIxmuWn8LkWROXp" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../../demo-map.js" integrity="sha384-S96oNhtx2xtbFL2YLgOlHA0IkLgz80K08+NVoO8qpHXb2xsJSz3CqVHL89QxlSUX" crossorigin="anonymous"></script>
|
||||
<script src="../../demo-map.js" integrity="sha384-U61Eq4Jc5slgktUQnAG2Hgy+131x34DxfqDzGwXVcKFSwpr/lCIxmuWn8LkWROXp" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-ZaUaF0Dx4UBlHI8JhLyifYVUt9vkrjG64kS+DIwF/YcownDDi5uh8zJ5GRqllen5" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-4D7c47asfYu6h4aR9CXkO35ayNbjl72ZotuI8NcgVfZRE6OrgKXCMYaWEC+Y+uJz" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-ZaUaF0Dx4UBlHI8JhLyifYVUt9vkrjG64kS+DIwF/YcownDDi5uh8zJ5GRqllen5" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-4D7c47asfYu6h4aR9CXkO35ayNbjl72ZotuI8NcgVfZRE6OrgKXCMYaWEC+Y+uJz" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"bundle": "all-people-groups",
|
||||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"output": "dx-maps/all-people-groups/all-people-groups.js",
|
||||
"integrity": "sha384-3rrykWo456uGE2YmJ/i7AeehqxCWQe0bVDjDOj5VEwaPZV5pFw4iegauoNaZMyDR",
|
||||
"integrity": "sha384-I45T4m8wem7bZa+vwYOeNA2bLE0UQwsQ44OPqWnGXO569NQ/822/a013lKWpUC0U",
|
||||
"elements": [
|
||||
"all-people-groups"
|
||||
],
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-3rrykWo456uGE2YmJ/i7AeehqxCWQe0bVDjDOj5VEwaPZV5pFw4iegauoNaZMyDR\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-I45T4m8wem7bZa+vwYOeNA2bLE0UQwsQ44OPqWnGXO569NQ/822/a013lKWpUC0U\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
},
|
||||
"bundle": "all-people-groups"
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
"bundle": "big-picture",
|
||||
"scriptUrl": "./big-picture/big-picture.js",
|
||||
"output": "dx-maps/big-picture/big-picture.js",
|
||||
"integrity": "sha384-SftV2pzJtasZnTIxHPqlL4QTf6juY2qb+eoUEwSsjtU/ldg7qPmbjAjr+uNrD7Cf",
|
||||
"integrity": "sha384-4//gka76np8NjhhynRAoi+IwIomgQsIz8IG27CXFo9rBWS/isxI+Rka1nRLjj1fY",
|
||||
"elements": [
|
||||
"bp-big-picture",
|
||||
"bp-research-header",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"scriptUrl": "./big-picture/big-picture.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./big-picture/big-picture/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=\"./big-picture/big-picture.js\" integrity=\"sha384-SftV2pzJtasZnTIxHPqlL4QTf6juY2qb+eoUEwSsjtU/ldg7qPmbjAjr+uNrD7Cf\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-4//gka76np8NjhhynRAoi+IwIomgQsIz8IG27CXFo9rBWS/isxI+Rka1nRLjj1fY\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
"bundle": "cities-internet",
|
||||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"output": "dx-maps/cities-internet/cities-internet.js",
|
||||
"integrity": "sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO",
|
||||
"integrity": "sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00",
|
||||
"elements": [
|
||||
"cities-internet-map"
|
||||
],
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/connectivity-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/connectivity-map/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-xcQGL99SboRUkzjl0kv3CY5IK9ysI1ijOW1WBMTvRi5wLGxl8MgxOK5zni3OvW00\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo",
|
||||
"integrity": "sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-map/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=\"./country-posters/country-posters.js\" integrity=\"sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"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=\"./country-posters/country-posters.js\" integrity=\"sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -191,7 +191,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/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=\"./country-posters/country-posters.js\" integrity=\"sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -205,7 +205,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/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=\"./country-posters/country-posters.js\" integrity=\"sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"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=\"./country-posters/country-posters.js\" integrity=\"sha384-GeeqYm+/7MdvlVROUr5CkLqhLIhMBOAN1sYjoCuKQ/STWl5B4y6QViRjMFH7zQqr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
"bundle": "dashboard-plus-grid",
|
||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"integrity": "sha384-PX/Ip+YNyNzpLKKUMO7ud3cQRKx39yp5Xom4+y/HgK5x4JhtEdJnaamoNo4ooEPI",
|
||||
"integrity": "sha384-SwGjy6nKza+3ths+kDMc7srH9hg4Qgcx7SZF/UzGuw9oXEQuDYtXDY9nsCsQXN5s",
|
||||
"elements": [
|
||||
"dashboard-plus-grid"
|
||||
],
|
||||
|
|
@ -234,7 +234,7 @@
|
|||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-PX/Ip+YNyNzpLKKUMO7ud3cQRKx39yp5Xom4+y/HgK5x4JhtEdJnaamoNo4ooEPI\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-SwGjy6nKza+3ths+kDMc7srH9hg4Qgcx7SZF/UzGuw9oXEQuDYtXDY9nsCsQXN5s\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -258,7 +258,7 @@
|
|||
"bundle": "demo-map",
|
||||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"output": "dx-maps/demo-map/demo-map.js",
|
||||
"integrity": "sha384-S96oNhtx2xtbFL2YLgOlHA0IkLgz80K08+NVoO8qpHXb2xsJSz3CqVHL89QxlSUX",
|
||||
"integrity": "sha384-U61Eq4Jc5slgktUQnAG2Hgy+131x34DxfqDzGwXVcKFSwpr/lCIxmuWn8LkWROXp",
|
||||
"elements": [
|
||||
"demo-map"
|
||||
],
|
||||
|
|
@ -274,7 +274,7 @@
|
|||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./demo-map/demo-map/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=\"./demo-map/demo-map.js\" integrity=\"sha384-S96oNhtx2xtbFL2YLgOlHA0IkLgz80K08+NVoO8qpHXb2xsJSz3CqVHL89QxlSUX\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
"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=\"./demo-map/demo-map.js\" integrity=\"sha384-U61Eq4Jc5slgktUQnAG2Hgy+131x34DxfqDzGwXVcKFSwpr/lCIxmuWn8LkWROXp\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -301,7 +301,7 @@
|
|||
"bundle": "doxa-maps-spa",
|
||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||
"integrity": "sha384-ZaUaF0Dx4UBlHI8JhLyifYVUt9vkrjG64kS+DIwF/YcownDDi5uh8zJ5GRqllen5",
|
||||
"integrity": "sha384-4D7c47asfYu6h4aR9CXkO35ayNbjl72ZotuI8NcgVfZRE6OrgKXCMYaWEC+Y+uJz",
|
||||
"elements": [
|
||||
"doxa-maps-spa"
|
||||
],
|
||||
|
|
@ -309,7 +309,6 @@
|
|||
"dashboard-plus-grid/dashboard-plus-grid",
|
||||
"research-map-plus-grid/research-map-plus-grid",
|
||||
"regions-dashboard/regions-mfe",
|
||||
"gospel-internet/board",
|
||||
"my-upg-100-list/upg-100-mfe",
|
||||
"doxa-simple-map/doxa-simple-map",
|
||||
"cities-internet/connectivity-map",
|
||||
|
|
@ -327,7 +326,7 @@
|
|||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-ZaUaF0Dx4UBlHI8JhLyifYVUt9vkrjG64kS+DIwF/YcownDDi5uh8zJ5GRqllen5\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-4D7c47asfYu6h4aR9CXkO35ayNbjl72ZotuI8NcgVfZRE6OrgKXCMYaWEC+Y+uJz\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -354,7 +353,7 @@
|
|||
"bundle": "doxa-research-map",
|
||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
||||
"integrity": "sha384-SVlzGcpHBhm5LtBu6CndA/qSFs88jyD9MK3eI2d2B0Ursn2grdvGWlDkl0vxVm53",
|
||||
"integrity": "sha384-nW33n+BmoeR0K/iI9apdAnWxew5RbSHAAg8PHy+ypiwcQA6+BMyF4anc7JwXeD5U",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -370,7 +369,7 @@
|
|||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-research-map/research-map/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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-SVlzGcpHBhm5LtBu6CndA/qSFs88jyD9MK3eI2d2B0Ursn2grdvGWlDkl0vxVm53\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
"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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-nW33n+BmoeR0K/iI9apdAnWxew5RbSHAAg8PHy+ypiwcQA6+BMyF4anc7JwXeD5U\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -396,7 +395,7 @@
|
|||
"bundle": "doxa-simple-map",
|
||||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
|
||||
"integrity": "sha384-m+IYUL1Olw/De0exTI0vll3SMhItlrMysGUng9oLBC9RUkbg2bOba4iXEi/6Kpa4",
|
||||
"integrity": "sha384-xVSokdr9nSgDxbGS14E41FI3aiDhfTfHQXbKler2QDU4LwWX3/un29Z6hVl5uuz4",
|
||||
"elements": [
|
||||
"doxa-map",
|
||||
"doxa-simple-map"
|
||||
|
|
@ -413,7 +412,7 @@
|
|||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-m+IYUL1Olw/De0exTI0vll3SMhItlrMysGUng9oLBC9RUkbg2bOba4iXEi/6Kpa4\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
"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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-xVSokdr9nSgDxbGS14E41FI3aiDhfTfHQXbKler2QDU4LwWX3/un29Z6hVl5uuz4\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -579,7 +578,7 @@
|
|||
"bundle": "gospel-internet",
|
||||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"output": "dx-maps/gospel-internet/gospel-internet.js",
|
||||
"integrity": "sha384-U7bazICc5JfAz9tkdielIA3nCFEuNae3pLV4xZ67dchqj2+kYF8H3ULU1HfWQgmw",
|
||||
"integrity": "sha384-NVgxHdVCb5aUY7aSYhuffQ3upM9FZCTOVLq80etKnDXCr8ZnVlhgmeu/FNwo/0ld",
|
||||
"elements": [
|
||||
"gospel-internet-board"
|
||||
],
|
||||
|
|
@ -595,7 +594,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-U7bazICc5JfAz9tkdielIA3nCFEuNae3pLV4xZ67dchqj2+kYF8H3ULU1HfWQgmw\" 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-NVgxHdVCb5aUY7aSYhuffQ3upM9FZCTOVLq80etKnDXCr8ZnVlhgmeu/FNwo/0ld\" 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": [
|
||||
{
|
||||
|
|
@ -695,7 +694,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY",
|
||||
"integrity": "sha384-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -715,7 +714,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-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY\" 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-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -729,7 +728,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-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY\" 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-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -743,7 +742,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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-resources/gospel-resources.js\" integrity=\"sha384-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"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-resources/gospel-resources.js\" integrity=\"sha384-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -757,7 +756,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-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY\" 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-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -771,7 +770,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": "<!-- 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-resources/gospel-resources.js\" integrity=\"sha384-kXVNzR8RP0GizvIAyxlbFGQw4UZU24kFVvvMjJqGZuSLMxqVa/UFwJ3EYhyqKdQY\" 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": "<!-- 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-resources/gospel-resources.js\" integrity=\"sha384-C/5d5nGkmOyzO2EbPddlvYy5s1dTUkC+ttXWErExDmkJFEuqQ75vwd5kwGpsDlmg\" 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -780,7 +779,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-Gz3XNgcuG2TIpbSCY+VHxzh3HmE/CRsFTExJmybR0+ddZq5mwAU3aLkkmns+Mext",
|
||||
"integrity": "sha384-J3l3xsNrTnZg76LcoKSEaioXa9UcoDT9p9iI/ouWx8JuEK93iKU7quj/F1hIkWrH",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -800,7 +799,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Gz3XNgcuG2TIpbSCY+VHxzh3HmE/CRsFTExJmybR0+ddZq5mwAU3aLkkmns+Mext\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-J3l3xsNrTnZg76LcoKSEaioXa9UcoDT9p9iI/ouWx8JuEK93iKU7quj/F1hIkWrH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -814,7 +813,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Gz3XNgcuG2TIpbSCY+VHxzh3HmE/CRsFTExJmybR0+ddZq5mwAU3aLkkmns+Mext\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-J3l3xsNrTnZg76LcoKSEaioXa9UcoDT9p9iI/ouWx8JuEK93iKU7quj/F1hIkWrH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -828,7 +827,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Gz3XNgcuG2TIpbSCY+VHxzh3HmE/CRsFTExJmybR0+ddZq5mwAU3aLkkmns+Mext\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-J3l3xsNrTnZg76LcoKSEaioXa9UcoDT9p9iI/ouWx8JuEK93iKU7quj/F1hIkWrH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -837,7 +836,7 @@
|
|||
"bundle": "people-groups-grid",
|
||||
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
|
||||
"output": "dx-maps/people-groups-grid/people-groups-grid.js",
|
||||
"integrity": "sha384-Oz37OCp7FfJyAzSIaCYmse5VhkiwC2dai2wkwLMagEV6WGl5QMaUVFlsK4KqVsCC",
|
||||
"integrity": "sha384-tp61XV2xYud3vQ487gn6dAdZ7POrL8AevcdZP6aCzetjcvgkU416Z/fhTRD/kcH3",
|
||||
"elements": [
|
||||
"people-groups-grid"
|
||||
],
|
||||
|
|
@ -853,7 +852,7 @@
|
|||
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/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=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-Oz37OCp7FfJyAzSIaCYmse5VhkiwC2dai2wkwLMagEV6WGl5QMaUVFlsK4KqVsCC\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-tp61XV2xYud3vQ487gn6dAdZ7POrL8AevcdZP6aCzetjcvgkU416Z/fhTRD/kcH3\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -898,7 +897,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-Jc2znVVtfkv01BBMY6IpyHr/W2r1JtqflWXd/s8LWPfJ+Ge62BgIK2vUbmvtvZH+",
|
||||
"integrity": "sha384-IfQjP4zwT8qoAdLAv0bNgMvN0mwsqIZYEqMfwbn/MZtcb41kmOVwRkzdANs8kLGS",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -926,7 +925,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/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=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-Jc2znVVtfkv01BBMY6IpyHr/W2r1JtqflWXd/s8LWPfJ+Ge62BgIK2vUbmvtvZH+\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-IfQjP4zwT8qoAdLAv0bNgMvN0mwsqIZYEqMfwbn/MZtcb41kmOVwRkzdANs8kLGS\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -953,7 +952,7 @@
|
|||
"bundle": "pplr-data-maps",
|
||||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"output": "dx-maps/pplr-data-maps/pplr-data-maps.js",
|
||||
"integrity": "sha384-OFc3TlXMyY/aABsA3Hg0rJqeZ0h9iQmT+O4rgxoAzVXU+UJc/+DbDd2l6ippqDKr",
|
||||
"integrity": "sha384-fqyrXAqslnCC8sOScI19p4IGV89NbHCrE+w1B1Gnfk/EmHLLbdlkii52FBGBSqmB",
|
||||
"elements": [
|
||||
"pplr-data-maps"
|
||||
],
|
||||
|
|
@ -969,7 +968,7 @@
|
|||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-OFc3TlXMyY/aABsA3Hg0rJqeZ0h9iQmT+O4rgxoAzVXU+UJc/+DbDd2l6ippqDKr\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
"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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-fqyrXAqslnCC8sOScI19p4IGV89NbHCrE+w1B1Gnfk/EmHLLbdlkii52FBGBSqmB\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
},
|
||||
"bundle": "pplr-data-maps"
|
||||
}
|
||||
|
|
@ -1004,7 +1003,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-4vm0RpawduoUDKxpuvOuiUJlclNFb8rMhQhChKTg6yVXfIel4jqIfUXef7fVW2MF",
|
||||
"integrity": "sha384-wkk1KnfGYwr+dnT8pKvoLQUHLk1x2PsvxB2EdEDeu/Z5HB+MqB0EL29imRMHthCE",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -1024,7 +1023,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-4vm0RpawduoUDKxpuvOuiUJlclNFb8rMhQhChKTg6yVXfIel4jqIfUXef7fVW2MF\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-wkk1KnfGYwr+dnT8pKvoLQUHLk1x2PsvxB2EdEDeu/Z5HB+MqB0EL29imRMHthCE\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1038,7 +1037,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-4vm0RpawduoUDKxpuvOuiUJlclNFb8rMhQhChKTg6yVXfIel4jqIfUXef7fVW2MF\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-wkk1KnfGYwr+dnT8pKvoLQUHLk1x2PsvxB2EdEDeu/Z5HB+MqB0EL29imRMHthCE\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1052,7 +1051,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-4vm0RpawduoUDKxpuvOuiUJlclNFb8rMhQhChKTg6yVXfIel4jqIfUXef7fVW2MF\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-wkk1KnfGYwr+dnT8pKvoLQUHLk1x2PsvxB2EdEDeu/Z5HB+MqB0EL29imRMHthCE\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1061,7 +1060,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "dx-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-PQlB58I+fZ8b9Ukt03YyDHUDRgkjDZkw3M7WBdUCFvDy9jwqESSClOryUHTogRyp",
|
||||
"integrity": "sha384-ppPrvC/o+iuZaAtg5bgKFp+s29lcdncUa1uw4h/RaGVmJxjn48TXJi3dyzVxZS7U",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -1077,7 +1076,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-PQlB58I+fZ8b9Ukt03YyDHUDRgkjDZkw3M7WBdUCFvDy9jwqESSClOryUHTogRyp\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
"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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-ppPrvC/o+iuZaAtg5bgKFp+s29lcdncUa1uw4h/RaGVmJxjn48TXJi3dyzVxZS7U\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
@ -1175,7 +1174,7 @@
|
|||
"bundle": "upg-neighbor",
|
||||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
|
||||
"integrity": "sha384-lT4ZpxDXicGkM7cvBbADGEspxp1e980qMRDDb9f+7yGvfS5KIR7QXbC4EqrNlxe3",
|
||||
"integrity": "sha384-JmNCukhu5CosIRLQDKefoZZj/yPlD6fnJVpScTsjpfT8y/7OFIskmcS+PjfbOpGu",
|
||||
"elements": [
|
||||
"upg-neighbor"
|
||||
],
|
||||
|
|
@ -1191,7 +1190,7 @@
|
|||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-lT4ZpxDXicGkM7cvBbADGEspxp1e980qMRDDb9f+7yGvfS5KIR7QXbC4EqrNlxe3\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-JmNCukhu5CosIRLQDKefoZZj/yPlD6fnJVpScTsjpfT8y/7OFIskmcS+PjfbOpGu\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -1274,7 +1273,7 @@
|
|||
"bundle": "wall-of-people-groups",
|
||||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
|
||||
"integrity": "sha384-Nabfkk/WeIHWXHKIVpl7aF/A+DlvvqLNvR2qEZ0DeKlPfh4ntH4rqABRawDDultp",
|
||||
"integrity": "sha384-iCCdBzu4FrSv9Y4uE+jXvkc1maEDPguf+bXHV3IOJq8rv8oEpG9qzfC553PZpUGY",
|
||||
"elements": [
|
||||
"wall-of-people-groups"
|
||||
],
|
||||
|
|
@ -1290,7 +1289,7 @@
|
|||
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/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=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-Nabfkk/WeIHWXHKIVpl7aF/A+DlvvqLNvR2qEZ0DeKlPfh4ntH4rqABRawDDultp\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-iCCdBzu4FrSv9Y4uE+jXvkc1maEDPguf+bXHV3IOJq8rv8oEpG9qzfC553PZpUGY\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
|
||||
},
|
||||
"bundle": "wall-of-people-groups"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||
<script src="../doxa-research-map.js" integrity="sha384-SVlzGcpHBhm5LtBu6CndA/qSFs88jyD9MK3eI2d2B0Ursn2grdvGWlDkl0vxVm53" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-research-map.js" integrity="sha384-nW33n+BmoeR0K/iI9apdAnWxew5RbSHAAg8PHy+ypiwcQA6+BMyF4anc7JwXeD5U" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||
<script src="../../doxa-research-map.js" integrity="sha384-SVlzGcpHBhm5LtBu6CndA/qSFs88jyD9MK3eI2d2B0Ursn2grdvGWlDkl0vxVm53" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-research-map.js" integrity="sha384-nW33n+BmoeR0K/iI9apdAnWxew5RbSHAAg8PHy+ypiwcQA6+BMyF4anc7JwXeD5U" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-m+IYUL1Olw/De0exTI0vll3SMhItlrMysGUng9oLBC9RUkbg2bOba4iXEi/6Kpa4" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-xVSokdr9nSgDxbGS14E41FI3aiDhfTfHQXbKler2QDU4LwWX3/un29Z6hVl5uuz4" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-m+IYUL1Olw/De0exTI0vll3SMhItlrMysGUng9oLBC9RUkbg2bOba4iXEi/6Kpa4" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-xVSokdr9nSgDxbGS14E41FI3aiDhfTfHQXbKler2QDU4LwWX3/un29Z6hVl5uuz4" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue