A gate for English typed into a template, so the ground we have won stays won
check:composite-locale-leaks asks a good question — "does the catalogue
translate this string, and is the page showing English anyway?" — but its
criterion is that the catalogue already HOLDS the string, so a string nobody
ever added is invisible to it by construction. And it runs in a browser, so it
only ever sees what happens to be on one screen.
That blind spot was measured, not guessed. After both composites passed the leak
gate with zero findings, a source audit of the same two trees found 31 more
user-visible strings. Most were aria-labels, titles and tooltips: text only a
screen-reader user or a hovering mouse ever meets. Our localization verification
had been "what can I see", and the blind spot was exactly the readers who most
need the words.
TWO CLASSES, because they are different defects with different fixes:
NO KEY wording must be written and translated — sometimes the Driver's call.
HAS A KEY the translation already ships in eleven locales and the reader is
shown English anyway. The cheapest defect in the catalogue: the
work was done and wasted. Nobody was looking for this one; it only
appeared because the report was split in two.
★ WARN-ONLY AGAINST A BASELINE, DELIBERATELY. Failing on the whole backlog — 285
without a key, 29 hardcoded-with-a-key across 126 components — would make it
unlandable and it would be switched off within a day. It fails on what is NEW, so
the count can only fall; --update refreshes the baseline when a batch lands.
QaCorner is excluded BY NAME, with the reason in the file: an opt-in developer
widget that studio is making host-neutral or removing. Eleven locales for a
corner only we open is not worth the translations, and a judgement like that is
better written down than left as a silent gap.
NEGATIVE-TESTED ON BOTH CLASSES, because a gate never seen to fail is not
evidence: a new text node and a new title attribute are both caught and named;
an aria-label hardcoded to "Reset all" — which already ships in every locale — is
caught as the second class with the fix stated. Restored, it passes.
This commit is contained in:
parent
1ac32fe3d0
commit
e9f5edf79d
2 changed files with 603 additions and 0 deletions
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-ui-strings-have-keys.mjs — user-visible text must come from the catalogue, and the ground
|
||||
* we have won must not be given back.
|
||||
*
|
||||
* ★ WHY THIS EXISTS, AND WHY NO EXISTING CHECK COULD SEE IT.
|
||||
* check:composite-locale-leaks asks "does the catalogue translate this string, and is the page
|
||||
* showing English anyway?" — a LOOKUP, and a good one. But its criterion is that the catalogue
|
||||
* already HOLDS the string, so a string nobody ever added is invisible to it by construction. And
|
||||
* it runs in a browser, so it only ever sees what happens to be rendered on one screen.
|
||||
*
|
||||
* That blind spot was measured, not guessed: after both composites passed the leak gate with 0
|
||||
* findings, a source audit of the same two trees found 31 more user-visible strings — 24 with no
|
||||
* catalogue entry at all and 7 that HAD a translation in eleven languages and were still typed into
|
||||
* the template. Most were aria-labels, titles and tooltips: text only a screen-reader user or a
|
||||
* hovering mouse ever meets. Our localization verification had been "what can I see", and the blind
|
||||
* spot was exactly the readers who most need the words.
|
||||
*
|
||||
* TWO CLASSES, because they are different defects with different fixes:
|
||||
* NO KEY wording must be written and translated — a decision, sometimes the Driver's.
|
||||
* HAS A KEY the translation already ships in eleven locales and the reader is shown English
|
||||
* anyway. The cheapest defect in the catalogue: the work was done and wasted.
|
||||
*
|
||||
* ★ WARN-ONLY AGAINST A BASELINE, ON PURPOSE. Failing on the whole backlog would make this
|
||||
* unlandable and it would be switched off within a day. It fails on what is NEW — a string added
|
||||
* today with no key, or a newly hardcoded one — so the count can only fall. When a batch lands,
|
||||
* refresh the baseline with --update and the gate holds the new line.
|
||||
*
|
||||
* QaCorner is excluded by name: it is an opt-in DEVELOPER widget, not reader-facing, and studio is
|
||||
* making it host-neutral or removing it. Eleven locales for a corner only we open is not worth the
|
||||
* translations, and that is a judgement worth writing down rather than leaving as a silent gap.
|
||||
*
|
||||
* USAGE node .../check-ui-strings-have-keys.mjs check against the baseline
|
||||
* node .../check-ui-strings-have-keys.mjs --update rewrite the baseline from today's tree
|
||||
* Exit: 0 clean or only improvements · 1 a NEW uncatalogued or newly hardcoded string.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parse as parseSFC } from '@vue/compiler-sfc'
|
||||
|
||||
const TAG = 'UI-STRINGS-HAVE-KEYS'
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
const BASELINE = join(ROOT, 'plugin-designs', 'research-map-plus-grid', 'scripts', 'ui-strings-baseline.json')
|
||||
const ROOTS = ['plugin-designs', 'library']
|
||||
// Not reader-facing: an opt-in developer widget, being made host-neutral or removed.
|
||||
const EXCLUDE = [/\/qa\/QaCorner\.vue$/]
|
||||
|
||||
// ── the catalogue: English value -> the key(s) that hold it ─────────────────────────────────────
|
||||
const flat = (o, p, out) => {
|
||||
for (const [k, v] of Object.entries(o || {})) {
|
||||
const K = p ? `${p}.${k}` : k
|
||||
if (v && typeof v === 'object') flat(v, K, out); else out[K] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
const en = {}
|
||||
const enDir = join(ROOT, 'library/i18n/locales/en')
|
||||
for (const f of readdirSync(enDir).filter((f) => f.endsWith('.json'))) {
|
||||
flat(JSON.parse(readFileSync(join(enDir, f), 'utf8')), f.replace(/\.json$/, ''), en)
|
||||
}
|
||||
const known = new Map()
|
||||
for (const [k, v] of Object.entries(en)) {
|
||||
const t = String(v).trim()
|
||||
if (!t) continue
|
||||
if (!known.has(t)) known.set(t, [])
|
||||
known.get(t).push(k)
|
||||
}
|
||||
|
||||
const vues = (d, out = []) => {
|
||||
let es; try { es = readdirSync(d) } catch { return out }
|
||||
for (const e of es) {
|
||||
if (e === 'node_modules' || e.startsWith('.')) continue
|
||||
const p = join(d, e)
|
||||
if (statSync(p).isDirectory()) vues(p, out); else if (e.endsWith('.vue')) out.push(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const VISIBLE_ATTRS = new Set(['placeholder', 'title', 'aria-label', 'alt', 'label', 'aria-placeholder', 'aria-description'])
|
||||
/** Worth translating? Real words, not a glyph, a number, a class name or a CSS value. */
|
||||
const worthIt = (t) => {
|
||||
const s = String(t || '').trim()
|
||||
if (s.length < 3 || s.length > 120) return false
|
||||
if (!/[A-Za-z]{2}/.test(s)) return false
|
||||
if (!/[A-Za-z]/.test(s[0])) return false
|
||||
if (/^[a-z][a-z0-9-]*$/.test(s)) return false
|
||||
if (/[{}<>;]|:\s*\d|px$|rem$|%$/.test(s)) return false
|
||||
if (/^(true|false|null|undefined)$/i.test(s)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function walk(node, hit) {
|
||||
if (!node) return
|
||||
if (node.type === 2 && node.content) hit(node.content)
|
||||
if (node.type === 1) {
|
||||
for (const p of node.props || []) {
|
||||
if (p.type === 6 && VISIBLE_ATTRS.has(p.name) && p.value && p.value.content) hit(p.value.content)
|
||||
}
|
||||
}
|
||||
for (const c of node.children || []) walk(c, hit)
|
||||
if (node.branches) for (const b of node.branches) walk(b, hit)
|
||||
}
|
||||
|
||||
const findings = { noKey: {}, hasKey: {} }
|
||||
let scanned = 0
|
||||
for (const r of ROOTS) {
|
||||
for (const file of vues(join(ROOT, r))) {
|
||||
const rel = relative(ROOT, file)
|
||||
if (EXCLUDE.some((re) => re.test(rel))) continue
|
||||
let d
|
||||
try { ({ descriptor: d } = parseSFC(readFileSync(file, 'utf8'), { filename: file })) } catch { continue }
|
||||
if (!d.template || !d.template.ast) continue
|
||||
scanned++
|
||||
const seen = new Set()
|
||||
walk(d.template.ast, (text) => {
|
||||
const t = String(text).replace(/\s+/g, ' ').trim()
|
||||
if (!worthIt(t) || seen.has(t)) return
|
||||
seen.add(t)
|
||||
const bucket = known.has(t) ? findings.hasKey : findings.noKey
|
||||
;(bucket[rel] ||= []).push(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const b of [findings.noKey, findings.hasKey]) for (const k of Object.keys(b)) b[k].sort()
|
||||
|
||||
if (process.argv.includes('--update')) {
|
||||
writeFileSync(BASELINE, JSON.stringify(findings, null, 2) + '\n')
|
||||
const n = (b) => Object.values(b).reduce((a, l) => a + l.length, 0)
|
||||
console.log(`${TAG}: baseline rewritten — ${n(findings.noKey)} without a key, ${n(findings.hasKey)} hardcoded-with-a-key, across ${scanned} components.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!existsSync(BASELINE)) {
|
||||
console.log(`${TAG}: FAIL — no baseline at ${relative(ROOT, BASELINE)}. Create it with --update, and read it before you do.`)
|
||||
process.exit(1)
|
||||
}
|
||||
const base = JSON.parse(readFileSync(BASELINE, 'utf8'))
|
||||
const inBase = (cls, file, text) => (base[cls]?.[file] || []).includes(text)
|
||||
|
||||
const fresh = { noKey: [], hasKey: [] }
|
||||
const fixed = { noKey: [], hasKey: [] }
|
||||
for (const cls of ['noKey', 'hasKey']) {
|
||||
for (const [file, list] of Object.entries(findings[cls])) {
|
||||
for (const t of list) if (!inBase(cls, file, t)) fresh[cls].push([file, t])
|
||||
}
|
||||
for (const [file, list] of Object.entries(base[cls] || {})) {
|
||||
for (const t of list) if (!(findings[cls][file] || []).includes(t)) fixed[cls].push([file, t])
|
||||
}
|
||||
}
|
||||
|
||||
const count = (b) => Object.values(b).reduce((a, l) => a + l.length, 0)
|
||||
console.log(`${TAG}: ${scanned} component(s); ${count(findings.noKey)} string(s) with no catalogue entry, ${count(findings.hasKey)} with a key but hardcoded.`)
|
||||
if (fixed.noKey.length || fixed.hasKey.length) {
|
||||
console.log(` ✓ ${fixed.noKey.length + fixed.hasKey.length} baseline string(s) fixed since it was set — refresh with --update to hold the new line.`)
|
||||
}
|
||||
if (fresh.noKey.length || fresh.hasKey.length) {
|
||||
console.log(`\n${TAG}: FAIL — ${fresh.noKey.length + fresh.hasKey.length} NEW string(s) since the baseline:`)
|
||||
for (const [f, t] of fresh.noKey) console.log(` ✗ no key ${f} ${JSON.stringify(t)}`)
|
||||
for (const [f, t] of fresh.hasKey) console.log(` ✗ has a key ${f} ${JSON.stringify(t)} — the translation already ships; read it instead of typing it`)
|
||||
console.log('\n A string with NO key needs wording and eleven translations, which is sometimes the')
|
||||
console.log(' Driver\'s call. A string that HAS a key needs one edit: the work is already done.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${TAG}: PASS — no new English has appeared in a template since the baseline.`)
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
{
|
||||
"noKey": {
|
||||
"plugin-designs/country-posters/countries-map.vue": [
|
||||
"Research map loading…"
|
||||
],
|
||||
"plugin-designs/doxa-simple-map/doxa-simple-map.vue": [
|
||||
"pray.doxa.life"
|
||||
],
|
||||
"plugin-designs/golden-path-check/golden-path-check.vue": [
|
||||
"Could not read",
|
||||
"Golden path — live self-test",
|
||||
"Reading the catalog manifest…",
|
||||
"This element was authored in",
|
||||
"beside this page.",
|
||||
"bun run build",
|
||||
"manifest.json",
|
||||
"plugin-designs/"
|
||||
],
|
||||
"plugin-designs/gospel-resources/charts.vue": [
|
||||
"Counted BY LANGUAGE, so",
|
||||
"Scripture found:",
|
||||
"languages have",
|
||||
"languages present here have nothing found in the pools checked.",
|
||||
"no scripture and no film found in the pools checked",
|
||||
"people groups collapse to",
|
||||
"people in",
|
||||
"people — cannot appear here. The \"Where Nothing Was Found\" table is keyed per people group and does include them.",
|
||||
"people.",
|
||||
"with no ISO 639-3 code in the source —"
|
||||
],
|
||||
"plugin-designs/gospel-resources/data-tables.vue": [
|
||||
"Loading",
|
||||
"deaths per day.",
|
||||
"people groups are",
|
||||
"people, an estimated",
|
||||
"with no scripture and no film found in the pools checked —"
|
||||
],
|
||||
"plugin-designs/gospel-resources/resources-list.vue": [
|
||||
"The largest are",
|
||||
"This page lists",
|
||||
"What “no text” means.",
|
||||
"Which unengaged people groups have scripture",
|
||||
"eBible.org",
|
||||
"in their own language",
|
||||
"languages — no text Scripture found in the pools checked",
|
||||
"people groups across",
|
||||
"published resource availability only",
|
||||
"translations across"
|
||||
],
|
||||
"plugin-designs/gospel-resources/resources-map.vue": [
|
||||
"A bridge, not a translation",
|
||||
"A second language is a way in, not a translation — the gap in their own language is unchanged.",
|
||||
"A “found” colour is equally bounded: it counts what these pools list, not everything that exists.",
|
||||
"About SUN ↗",
|
||||
"Bible video ·",
|
||||
"Bridge strength:",
|
||||
"DOOR International",
|
||||
"Deaf Bible Society",
|
||||
"Deaths/day applies a national crude death rate to a sub-population — order of magnitude, not a count.",
|
||||
"Every one of those",
|
||||
"Every resource used in",
|
||||
"Gospel presentation ·",
|
||||
"Gospel resources found (",
|
||||
"How these numbers are made",
|
||||
"It sits on the same point as",
|
||||
"Largest with nothing:",
|
||||
"Link states:",
|
||||
"Matched on ISO 639-3",
|
||||
"No resource found (",
|
||||
"Not ranked and not recommended",
|
||||
"Not their language — but spoken around them.",
|
||||
"Nothing found for any group here in the pools checked.",
|
||||
"Nothing in",
|
||||
"Nothing was found in",
|
||||
"Possibly a way in — worth checking locally.",
|
||||
"SUN",
|
||||
"SUN is already being taught in",
|
||||
"Scripture and the JESUS Film",
|
||||
"Scripture was translated by",
|
||||
"Scripture,",
|
||||
"See the 40 largest →",
|
||||
"Show all",
|
||||
"Showing 12 of",
|
||||
"Showing 8 of",
|
||||
"Showing only",
|
||||
"So is",
|
||||
"That is a statement about how the standard groups these codes,",
|
||||
"That is a statement about the pools searched,",
|
||||
"The",
|
||||
"The dot marks where this people is found, not a town or a boundary.",
|
||||
"The groups with nothing found in any pool checked AND nobody praying —",
|
||||
"The largest language among the people groups mapped here that does have Scripture and the JESUS Film is",
|
||||
"The open catalogues are",
|
||||
"There is one signed language with Scripture in it.",
|
||||
"This is a sign language.",
|
||||
"Training",
|
||||
"Translated by",
|
||||
"Way in:",
|
||||
"What that number is made of.",
|
||||
"Zume Training publishes its discipleship course in this language — free, no account needed to read it.",
|
||||
"Zume Training publishes its discipleship course in this language.",
|
||||
"a different set of groups",
|
||||
"already praying",
|
||||
"and is an official language",
|
||||
"and official",
|
||||
"both a film and a text Scripture.",
|
||||
"different situations share it, and only the first is a translation problem:",
|
||||
"filmed in this sign language",
|
||||
"finished for every group in the gap",
|
||||
"from the curated country analysis, over",
|
||||
"groups (",
|
||||
"groups doxa.life publishes a page for can appear here at all.",
|
||||
"groups have nothing ·",
|
||||
"groups share this coordinate, so the position tells you the locality and not which group is where.",
|
||||
"groups sharing this language.",
|
||||
"groups with nothing ·",
|
||||
"groups ·",
|
||||
"has a possible way in",
|
||||
"has resources",
|
||||
"have a possible way in",
|
||||
"is never evidence that a link works or fails.",
|
||||
"is spoken by",
|
||||
"matches — keep typing to narrow.",
|
||||
"no text Scripture found in the pools checked",
|
||||
"nothing found, and nobody praying — open their prayer page ↗",
|
||||
"of its",
|
||||
"of them also carry a code no pool can index, so they are doubly stuck.",
|
||||
"of them. That is a count of THIS MAP's groups, not of the country — no published figure for the country was available.",
|
||||
"of these",
|
||||
"of those",
|
||||
"people groups and does have",
|
||||
"people groups can already hear the gospel — a recording, the JESUS Film, a Bible video or Words of Life — but have",
|
||||
"people groups have no Scripture, audio, film, Bible video or Door43 resource in the pools checked —",
|
||||
"people ·",
|
||||
"people) — every pool this build can reach was checked and found nothing. This is the real gap.",
|
||||
"people,",
|
||||
"people.",
|
||||
"related:",
|
||||
"shown, largest first. Tapping one opens their prayer page on doxa.life.",
|
||||
"sign languages on this map, covering",
|
||||
"the JESUS Film",
|
||||
"together with",
|
||||
"way in:"
|
||||
],
|
||||
"plugin-designs/key-cities/cities-list.vue": [
|
||||
"A cluster of people-group centroids within ~25km —",
|
||||
"Internet figures are NATIONAL",
|
||||
"Nominatim/OpenStreetMap",
|
||||
"This page carries people-group locations and connectivity only — no church, believer or meeting-point data, by design.",
|
||||
"What a row is.",
|
||||
"multi-group centres were resolved by"
|
||||
],
|
||||
"plugin-designs/my-upg-100-list/upg-100-list.vue": [
|
||||
"Batch Select",
|
||||
"Map unavailable",
|
||||
"Tap territories on the map or legend rows to add · tap again to remove",
|
||||
"UUPG"
|
||||
],
|
||||
"plugin-designs/plugin-studio/plugin-studio.vue": [
|
||||
"https://doxa.life/api/maps/token",
|
||||
"pk.eyJ…"
|
||||
],
|
||||
"plugin-designs/plugin-studio/src/StudioTree.vue": [
|
||||
"CAP"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/pplr-data-maps.vue": [
|
||||
"PPLR data maps"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/DataSourcesDropdown.vue": [
|
||||
"Data sources"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/FlatLegend.vue": [
|
||||
"Clear selection (Esc)",
|
||||
"Open legend"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/LoadingGlobe.vue": [
|
||||
"Retry"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/PinDetailCard.vue": [
|
||||
"Close (Esc)",
|
||||
"Fly to pin",
|
||||
"Name in 17 locales"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue": [
|
||||
"Loading",
|
||||
"Loading data…",
|
||||
"Pop",
|
||||
"Showing top",
|
||||
"Sort by UPGs",
|
||||
"UPGs",
|
||||
"use the geocoder above to find the rest"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-coverage.vue": [
|
||||
"Avg locales / 17",
|
||||
"Translation coverage · choropleth = avg locales filled per country · click PG pin or country"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-d3-tree.vue": [
|
||||
"Data sources",
|
||||
"Loading ROP hierarchy",
|
||||
"ROP Hierarchy · Affinity Bloc → People Cluster · scroll to zoom"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-index.vue": [
|
||||
"Click a tile, or use the topbar tabs above to switch displays. Click",
|
||||
"Many small displays — each answers one validation question about the PPLR people-group data. Source:",
|
||||
"PPLR ArcGIS FeatureServer",
|
||||
"PPLR data validation",
|
||||
"top-right to see every slot at once."
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue": [
|
||||
"Base map",
|
||||
"Data sources",
|
||||
"Ethnolinguistic families (PPLR) · click pin for detail",
|
||||
"Search by people, place, language, religion…"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-pins.vue": [
|
||||
"All PG-in-country pins · click for cross-system IDs",
|
||||
"Data sources"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-religion.vue": [
|
||||
"Base map",
|
||||
"Data sources",
|
||||
"Primary Religion",
|
||||
"Religion distribution · click pin for detail",
|
||||
"Search by people, place, language, religion…"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-rop-tree.vue": [
|
||||
"Base map",
|
||||
"Data sources",
|
||||
"ROP Hierarchy",
|
||||
"ROP Hierarchy · Affinity Bloc → Cluster → People Group → PGIC",
|
||||
"Search by people, place, language, religion…"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-rosetta-cards.vue": [
|
||||
"Cross-system ID Rosetta · color = which IDs exist · ★ = sample row",
|
||||
"ID coverage"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-semantic-trees.vue": [
|
||||
"API field names per external system",
|
||||
"Engagement status tiers?",
|
||||
"Migration corridors?",
|
||||
"Religious sub-streams?",
|
||||
"Semantic tree reference",
|
||||
"Translation lineage?",
|
||||
"What other semantic trees",
|
||||
"are hiding in the data?",
|
||||
"denomination → tradition → school",
|
||||
"origin → diaspora pathways",
|
||||
"source language → script → orthography",
|
||||
"unreached → unengaged → unevangelized"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-translations.vue": [
|
||||
"Data sources",
|
||||
"PG name · 17 locales · click any pin"
|
||||
],
|
||||
"plugin-designs/stack-demo/stack-demo.vue": [
|
||||
"Stacked view",
|
||||
"scroll ↓"
|
||||
],
|
||||
"plugin-designs/template-bundle/template-map-b.vue": [
|
||||
"Profile B",
|
||||
"Served from",
|
||||
"bundle.",
|
||||
"in the",
|
||||
"note:",
|
||||
"template-map-b.vue"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/bloom-cockpit.vue": [
|
||||
"FLOWER",
|
||||
"LIVE",
|
||||
"Loading the harvest field…",
|
||||
"MAP",
|
||||
"Map or flower view",
|
||||
"SEND (solid=same-lang · dashed=cluster · dotted=in-country)",
|
||||
"clusters ·",
|
||||
"senders →",
|
||||
"unreached target",
|
||||
"unreached · baked senders"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/cockpit/FlowerCell.vue": [
|
||||
"Pick a red target on the map —",
|
||||
"its reached neighbors bloom here as a flower.",
|
||||
"navy = SEND ·",
|
||||
"same-country ·",
|
||||
"unreached ·"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/cockpit/FlowerClusters.vue": [
|
||||
"Flower-clusters",
|
||||
"reached petals · by",
|
||||
"red cores (clustered) → granular green reached petals (same language)",
|
||||
"taxonomy = research-map semantic tree (family→language→dialect)",
|
||||
"unreached = LIVE pray.doxa.life · reached = baked PPLR"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/cockpit/ListCell.vue": [
|
||||
"Adjacency",
|
||||
"Bridge",
|
||||
"Build send list",
|
||||
"Code",
|
||||
"Details",
|
||||
"No senders match the current filters",
|
||||
"Pick a red target",
|
||||
"Tap a red bubble on the map to build its send list.",
|
||||
"This target has no reached neighbors in the baked set yet",
|
||||
"reached senders nearby",
|
||||
"senders available",
|
||||
"senders hidden — clear a facet to see them",
|
||||
"unreached clusters ·"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/flower-mesh.vue": [
|
||||
"Flower-Mesh",
|
||||
"center = doxa-2000 (pray.doxa.life)",
|
||||
"km)",
|
||||
"neighbors = baked",
|
||||
"province N/A in source — finest grain = country (see RE)",
|
||||
"reached neighbor → unreached center"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/lang-cluster.vue": [
|
||||
"Bounded sets",
|
||||
"IN/CN province = real ADM1 (my-upg-100-list point-in-polygon)",
|
||||
"Language clusters",
|
||||
"reached = baked reached-neighbors",
|
||||
"red unreached · green reached-by-language",
|
||||
"unreached = LIVE pray.doxa.life ("
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/send-map.vue": [
|
||||
"Adjacency",
|
||||
"Language pair",
|
||||
"Lower persecution",
|
||||
"No qualifying reached senders with a known country centroid for this target.",
|
||||
"SEND",
|
||||
"UPGs ·",
|
||||
"chain: UPG-100 → FPG base → My UPG Neighbor",
|
||||
"km)",
|
||||
"reached neighbor → unreached target",
|
||||
"reached senders",
|
||||
"senders = baked",
|
||||
"target = LIVE (JP UnreachedPeoplesByCountry)"
|
||||
],
|
||||
"plugin-designs/wall-of-people-groups/src/components/PeopleGroupCard.vue": [
|
||||
"View prayers and comments",
|
||||
"prayed that they would receive God's love and the love of a missionary"
|
||||
],
|
||||
"plugin-designs/wall-of-people-groups/src/components/SourceSelector.vue": [
|
||||
"CSV includes ~1,439 groups with images from Joshua Project",
|
||||
"Current:",
|
||||
"Live prayer data — people praying, committed workers, adoption, images",
|
||||
"Source URL:",
|
||||
"WordPress API — descriptions & photos, no prayer data"
|
||||
],
|
||||
"plugin-designs/wall-of-people-groups/wall-of-people-groups.vue": [
|
||||
"The wall could not load its drawing library:"
|
||||
],
|
||||
"library/ProfileLoader.vue": [
|
||||
"Example:",
|
||||
"not found.",
|
||||
"prop."
|
||||
],
|
||||
"library/components/legends/LegendRows.vue": [
|
||||
"Toggle children"
|
||||
],
|
||||
"library/components/legends/SemanticTreeLegend.vue": [
|
||||
"Loading",
|
||||
"Loading data…",
|
||||
"Showing top",
|
||||
"use the geocoder above to find the rest"
|
||||
],
|
||||
"library/components/poster/PosterPreview.vue": [
|
||||
"Loading map snapshot…",
|
||||
"map preview"
|
||||
],
|
||||
"library/components/poster/PosterWall.vue": [
|
||||
"people groups ·",
|
||||
"preview unavailable"
|
||||
],
|
||||
"library/layouts/VerticalTabsLayout.vue": [
|
||||
"Could not load",
|
||||
"Loading"
|
||||
]
|
||||
},
|
||||
"hasKey": {
|
||||
"plugin-designs/country-posters/countries-map.vue": [
|
||||
"The map could not be loaded."
|
||||
],
|
||||
"plugin-designs/country-posters/country-posters-mfe.vue": [
|
||||
"Views"
|
||||
],
|
||||
"plugin-designs/demo-map/demo-map.vue": [
|
||||
"Loading map…",
|
||||
"The map could not be loaded."
|
||||
],
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue": [
|
||||
"Change language"
|
||||
],
|
||||
"plugin-designs/gospel-resources/charts.vue": [
|
||||
"people group"
|
||||
],
|
||||
"plugin-designs/gospel-resources/resources-map.vue": [
|
||||
"Audio",
|
||||
"Bible video",
|
||||
"Find a people group, language or ISO code",
|
||||
"Loading resources…",
|
||||
"Scripture",
|
||||
"Show all groups",
|
||||
"Showing",
|
||||
"link checked"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/pplr-data-maps.vue": [
|
||||
"Choose a view"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/FlatLegend.vue": [
|
||||
"Legend"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/PinDetailCard.vue": [
|
||||
"Close",
|
||||
"People Group"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue": [
|
||||
"No data at this level",
|
||||
"Sort by Population"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-index.vue": [
|
||||
"Overview"
|
||||
],
|
||||
"plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue": [
|
||||
"Language Families"
|
||||
],
|
||||
"plugin-designs/upg-neighbor/src/cockpit/ListCell.vue": [
|
||||
"Country",
|
||||
"Export",
|
||||
"Language",
|
||||
"People group",
|
||||
"Population",
|
||||
"Religion"
|
||||
],
|
||||
"library/components/poster/PosterWall.vue": [
|
||||
"people groups"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue