From e9f5edf79dd0b2847303e6db37b98ddf5c7d2246 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 26 Sep 2026 11:47:31 +0000 Subject: [PATCH] A gate for English typed into a template, so the ground we have won stays won MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../scripts/check-ui-strings-have-keys.mjs | 165 +++++++ .../scripts/ui-strings-baseline.json | 438 ++++++++++++++++++ 2 files changed, 603 insertions(+) create mode 100644 plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs create mode 100644 plugin-designs/research-map-plus-grid/scripts/ui-strings-baseline.json diff --git a/plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs b/plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs new file mode 100644 index 000000000..78db3712b --- /dev/null +++ b/plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs @@ -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.`) diff --git a/plugin-designs/research-map-plus-grid/scripts/ui-strings-baseline.json b/plugin-designs/research-map-plus-grid/scripts/ui-strings-baseline.json new file mode 100644 index 000000000..8c036f12c --- /dev/null +++ b/plugin-designs/research-map-plus-grid/scripts/ui-strings-baseline.json @@ -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" + ] + } +}