Update from source f4871401
This commit is contained in:
parent
b25d403a90
commit
de284359dd
212 changed files with 9316 additions and 1833 deletions
38
data/boundaries/gb-IND-ADM1-full-web.geojson
Normal file
38
data/boundaries/gb-IND-ADM1-full-web.geojson
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Messaging & App Channels",
|
||||
"grain": "one row per country x communication app",
|
||||
"why": "Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp is the top messaging app in 54 countries, Telegram in 38 — neither appears in referral share at all.",
|
||||
"why": "Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp (Messenger or Business) is the top messaging app in 73 countries, Telegram in 38 — neither appears in referral share at all.",
|
||||
"ios_caveat": "iOS ONLY. Most of these countries are Android-majority and Google publishes no keyless equivalent, so this is one eye open. An app absent here is NOT absent in-country. 50 of 175 countries have no Apple storefront.",
|
||||
"ranking_caveat": "A top-free RANK is most-DOWNLOADED recently, not most-used.",
|
||||
"classification_caveat": "kind is a name-keyword HEURISTIC over a known list, not a verified taxonomy.",
|
||||
|
|
@ -2830,19 +2830,6 @@
|
|||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Thailand",
|
||||
"iso3": "THA",
|
||||
"region": "Asia",
|
||||
"app": "LINE MAN: Food Delivery & more",
|
||||
"kind": "messaging",
|
||||
"rank": 13,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 8,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Burkina Faso",
|
||||
"iso3": "BFA",
|
||||
|
|
@ -3006,7 +2993,7 @@
|
|||
"app": "WhatsApp Messenger",
|
||||
"kind": "messaging",
|
||||
"rank": 37,
|
||||
"is_top_messaging": false,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 8,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
|
|
@ -7809,19 +7796,6 @@
|
|||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Maldives",
|
||||
"iso3": "MDV",
|
||||
"region": "Asia",
|
||||
"app": "Malé Taxi Line",
|
||||
"kind": "messaging",
|
||||
"rank": 1,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 1,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Northern Mariana Islands",
|
||||
"iso3": "MNP",
|
||||
|
|
@ -13120,7 +13094,7 @@
|
|||
"app": "WhatsApp Messenger",
|
||||
"kind": "messaging",
|
||||
"rank": 33,
|
||||
"is_top_messaging": false,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 1,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
|
|
|
|||
|
|
@ -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,6 +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).
|
||||
|
|
@ -115,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
|
||||
|
|
@ -127,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.
|
||||
|
|
@ -192,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
|
||||
|
|
@ -233,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 agent'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)
|
||||
|
|
@ -1519,6 +1519,12 @@ function main() {
|
|||
// for maps that colour every state. The curated file above is UPG-100's 34-unit partition.
|
||||
[resolve(__dirname, '..', 'data', 'boundaries', 'gb-IND-ADM1-full.geojson'), 'gb-IND-ADM1-full.geojson'],
|
||||
[resolve(__dirname, '..', 'data', 'boundaries', 'gb-IND-ADM1-full.meta.json'), 'gb-IND-ADM1-full.meta.json'],
|
||||
// WEB copies (library/scripts/boundaries/build-web-boundaries.mjs): every feature and island kept, about
|
||||
// a fifth of the bytes. The interactive maps fetch these; posters, print and build-time point-in-polygon
|
||||
// keep the full files above (REPORT-WEB-BOUNDARIES.md).
|
||||
[join(dataDir, 'adm0', 'world-adm0-web.geojson'), 'world-adm0-web.geojson'],
|
||||
[join(dataDir, 'gb-IND-ADM1-web.geojson'), 'gb-IND-ADM1-web.geojson'],
|
||||
[resolve(__dirname, '..', 'data', 'boundaries', 'gb-IND-ADM1-full-web.geojson'), 'gb-IND-ADM1-full-web.geojson'],
|
||||
]) if (existsSync(from)) cpSync(from, join(bDir, to))
|
||||
console.log('[generate-tree] gathered boundary geojson')
|
||||
|
||||
|
|
@ -1579,6 +1585,9 @@ function main() {
|
|||
'boundaries/gb-CHN-ADM1.geojson',
|
||||
'boundaries/gb-IND-ADM1-full.geojson', // all 36 states (Cities with Internet's NFHS layer)
|
||||
'boundaries/gb-IND-ADM1-full.meta.json', // its source and licence, served beside it
|
||||
'boundaries/world-adm0-web.geojson', // the web copies the interactive maps fetch
|
||||
'boundaries/gb-IND-ADM1-web.geojson',
|
||||
'boundaries/gb-IND-ADM1-full-web.geojson',
|
||||
// people-groups — all-people-groups.vue (the slim JSON only; NOT the 16 MB CSV,
|
||||
// which no element fetches and which dropin.exclude already withholds).
|
||||
'people-groups/people-groups-slim.json',
|
||||
|
|
|
|||
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)),
|
||||
|
|
@ -99,6 +99,41 @@ const visible = ref(false)
|
|||
// which only the reader's × turns off: scrolling away hides the pill, scrolling back shows it again.
|
||||
const onScreen = ref(true)
|
||||
let mapIO = null
|
||||
// Whether the map's pane is the ACTIVE view. A composite that stacks views (upg-100-mfe on a phone puts the
|
||||
// map pane BEHIND Prayer Resources, both position:absolute; inset:0) passes the geometry test above while
|
||||
// the reader sees only the posters, because an IntersectionObserver measures geometry, not occlusion. The
|
||||
// contract: a composite marks an inactive view `inert` (or hidden / aria-hidden="true"), which is right for
|
||||
// assistive technology anyway, and the pill hides while any ancestor of its map carries one of those.
|
||||
const paneActive = ref(true)
|
||||
let paneMOs = []
|
||||
const INACTIVE_ATTRS = ['inert', 'hidden', 'aria-hidden']
|
||||
function ancestorsOf(el) {
|
||||
const out = []
|
||||
let n = el
|
||||
while (n) {
|
||||
if (n.nodeType === 1) out.push(n)
|
||||
n = n.parentNode || n.host || null // a ShadowRoot has no parentNode; continue at its host
|
||||
}
|
||||
return out
|
||||
}
|
||||
function isInactive(el) {
|
||||
return ancestorsOf(el).some((a) => a.hasAttribute('inert') || a.hasAttribute('hidden') || a.getAttribute('aria-hidden') === 'true')
|
||||
}
|
||||
function watchPaneActive(box) {
|
||||
const check = () => {
|
||||
const now = !isInactive(box)
|
||||
if (now !== paneActive.value) { paneActive.value = now; if (now) nextTick(clampIntoView) }
|
||||
}
|
||||
check()
|
||||
if (typeof MutationObserver === 'undefined') return
|
||||
for (const a of ancestorsOf(box)) {
|
||||
try {
|
||||
const mo = new MutationObserver(check)
|
||||
mo.observe(a, { attributes: true, attributeFilter: INACTIVE_ATTRS })
|
||||
paneMOs.push(mo)
|
||||
} catch (_) { /* detached or foreign node: skip it */ }
|
||||
}
|
||||
}
|
||||
const hintEl = ref(null)
|
||||
let watchTimer = null
|
||||
let movedTo = null // the element we re-parented into, so we can put ourselves back
|
||||
|
|
@ -210,7 +245,7 @@ function sheetFloor(pill) {
|
|||
|
||||
function clampIntoView() {
|
||||
const el = hintEl.value
|
||||
if (!el || !onScreen.value) return
|
||||
if (!el || !onScreen.value || !paneActive.value) return
|
||||
try {
|
||||
// ★ ADJUST `bottom`, NEVER `transform`. My first attempt set el.style.transform and cleared it
|
||||
// between measurements — both wrong here, and the diagnosis took two failed builds:
|
||||
|
|
@ -303,6 +338,7 @@ onMounted(() => {
|
|||
// SHOW ONLY WHILE THE MAP IS IN VIEW. The observer's root is the viewport, and it clips by every
|
||||
// scrolling ancestor, so a map pane scrolled out of a composite's column reads as not intersecting.
|
||||
// Less than a fifth of the map on screen counts as away: the pill needs map under it.
|
||||
if (box) watchPaneActive(box)
|
||||
if (box && typeof IntersectionObserver !== 'undefined') {
|
||||
try {
|
||||
mapIO = new IntersectionObserver((entries) => {
|
||||
|
|
@ -340,6 +376,8 @@ onMounted(() => {
|
|||
onBeforeUnmount(() => {
|
||||
if (watchTimer) clearInterval(watchTimer)
|
||||
if (mapIO) { try { mapIO.disconnect() } catch (_) {} mapIO = null }
|
||||
for (const mo of paneMOs) { try { mo.disconnect() } catch (_) {} }
|
||||
paneMOs = []
|
||||
if (attribRO) { try { attribRO.disconnect() } catch (_) {} attribRO = null } attribEl = null
|
||||
if (hostRO) { try { hostRO.disconnect() } catch (_) {} hostRO = null }
|
||||
if (sheetRO) { try { sheetRO.disconnect() } catch (_) {} sheetRO = null } sheetWatched = null
|
||||
|
|
@ -400,7 +438,7 @@ useShadowStyles(`
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" v-show="onScreen" ref="hintEl" class="pin-hint" :class="{ dark: isDark }" role="status">
|
||||
<div v-if="visible" v-show="onScreen && paneActive" ref="hintEl" class="pin-hint" :class="{ dark: isDark }" role="status">
|
||||
<span class="pin-hint-pill">
|
||||
<span class="pin-hint-dot" aria-hidden="true"></span>
|
||||
{{ label }}
|
||||
|
|
|
|||
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 agent 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 agent 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>
|
||||
275
library/components/regions/RegionSectionCard.vue
Normal file
275
library/components/regions/RegionSectionCard.vue
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
<script setup>
|
||||
/**
|
||||
* RegionSectionCard.vue — one region as a card: a heading, a summary line, a grid of its countries,
|
||||
* and a footer link. The DX-life /regions card, made reusable.
|
||||
*
|
||||
* WHY THIS IS IN library/. The agent, verbatim: "let's say we want to reuse that on future plugins.
|
||||
* We make it a reusable component, parameterized — you can set the region titles wherever you want
|
||||
* them to be, fill in the countries to whatever you want them to be." The design is the point as much
|
||||
* as the markup: the 156 lines of CSS below are lifted UNCHANGED from the regions plugin, so a caller
|
||||
* gets the look, not a lookalike.
|
||||
*
|
||||
* ★ IT TAKES DATA, NOT AN API. The plugin version fetched its own regions and owned the layout, which
|
||||
* is what made it unusable anywhere else. This takes `title`, `summary` and `countries` and emits what
|
||||
* was clicked. Nothing here knows where regions come from.
|
||||
*
|
||||
* ★ THE CHEVRON IS INLINE ON PURPOSE, and this is the one thing a mover has to get right. The plugin
|
||||
* declared <symbol id="chevron-up"> ONCE in its own template and every card referenced it with
|
||||
* <use href="#chevron-up">. A shadow-root id is scoped to that root, so the same card mounted anywhere
|
||||
* else would reference a symbol that does not exist — and an unresolved <use> renders NOTHING, with no
|
||||
* error. The chevron would simply be missing. The plugin's own comment warns about exactly this class
|
||||
* for external hrefs ("would 404 SILENTLY and the chevron would simply vanish"); a cross-root id
|
||||
* reference fails the same way. So the path travels with the component.
|
||||
*
|
||||
* ★ RTL NEEDS AN ANCESTOR CARRYING dir, and that is a REQUIREMENT ON THE CONSUMER. The rules below
|
||||
* include [dir="rtl"] .region-card__icon, ported because a chevron meaning "forward" must mirror in
|
||||
* Arabic while a disclosure caret meaning "open/closed" must not — and the rtl --open rule is
|
||||
* load-bearing, since [dir="rtl"] .region-card__icon is specificity 0-2-0 and would otherwise beat the
|
||||
* 0-1-0 --open modifier and freeze the caret. The regions plugin satisfies this because .rl-root
|
||||
* carries :dir itself. A new consumer must set dir on an ancestor inside its own shadow root, or the
|
||||
* mirroring silently will not happen.
|
||||
*
|
||||
* ★ EXTRA CONTENT IS A SLOT, NOT A PROP. The regions plugin shows a country-poster wall under each
|
||||
* card, configured with fourteen PosterWall props. Baking that in would have dragged one plugin's
|
||||
* feature into every consumer's API. The `extra` slot keeps it where it belongs.
|
||||
*/
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** The region's display name. Already localized by the caller. */
|
||||
title: { type: String, default: '' },
|
||||
/** The line under the title, e.g. "12 countries · 480 groups". Caller-formatted. */
|
||||
summary: { type: String, default: '' },
|
||||
/** [{ name, count, slug }] — `count` is optional and rendered in parentheses when present. */
|
||||
countries: { type: Array, default: () => [] },
|
||||
/** The footer link's text, e.g. "View the region dashboard". Omit it to hide the footer. */
|
||||
footerLabel: { type: String, default: '' },
|
||||
/**
|
||||
* OPTIONAL DISCLOSURE ROW, and the card owns it deliberately. The regions plugin needed a
|
||||
* "View country posters" toggle under each card. The first cut put that row in the consumer's slot
|
||||
* — which immediately meant copying this component's footer-link look and its rotating caret back
|
||||
* into the plugin, i.e. re-creating the duplication this extraction exists to remove. A disclosure
|
||||
* inside a card is a CARD feature; only the revealed CONTENT belongs to the consumer.
|
||||
* Omit the label to hide the row entirely.
|
||||
*/
|
||||
disclosureLabel: { type: String, default: '' },
|
||||
disclosureOpen: { type: Boolean, default: false },
|
||||
/** id for aria-controls, so the button and the revealed region are really associated. */
|
||||
disclosureId: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select', 'select-country', 'toggle-disclosure'])
|
||||
|
||||
useShadowStyles(
|
||||
// .region-card: DX-life's --border-radius-xlg (35px), overflow hidden, white body, no border --
|
||||
// the dark header band supplies its own edge, unlike our old grey-outlined card.
|
||||
'.region-card{ flex:0 0 auto; border-radius:var(--dx-radius); overflow:hidden; background:var(--dx-elevated); }' +
|
||||
// .region-card__header: padding = --spacing-md / clamp(--spacing-md,4vw,--spacing-2xl) -- the clamp
|
||||
// is KEPT as a clamp (var(--spacing-md) / var(--spacing-2xl) are the resolved ends), not frozen at its desktop maximum.
|
||||
// gap = --spacing-2xs (7.5px), on --color-surface-brand.
|
||||
'.region-card__header{ display:flex; flex-direction:column; gap:var(--spacing-2xs);' +
|
||||
' padding:var(--spacing-md) clamp(var(--spacing-md), 4cqw, var(--spacing-2xl));' +
|
||||
' background:var(--dx-header-bg); color:var(--dx-header-text); }' +
|
||||
// .region-card__title: font-size clamp(--font-size-xl,4vw,--font-size-3xl) = clamp(var(--font-size-xl),_,var(--font-size-3xl)).
|
||||
// A phone gets the var(--font-size-xl) FLOOR, as theirs does; it only reaches var(--font-size-3xl) past ~1192px of width.
|
||||
// DX-life's heading font (Bebas Neue -- a bold condensed display face), regular weight (the
|
||||
// face itself carries the boldness), all-caps to match the source screenshot exactly.
|
||||
// Bebas Neue itself is DX-life's own self-hosted font file (doxa.life/assets/fonts/BebasNeue/),
|
||||
// not served with CORS -- cross-origin @font-face silently fails without it, so it cannot be
|
||||
// loaded here without shipping a font asset this repo doesn't own. Approximated with the
|
||||
// standard condensed-display-face fallback stack (bold weight + font-stretch:condensed + tight
|
||||
// tracking), since Bebas Neue's glyphs read as heavy/compressed even at its nominal "400" weight.
|
||||
// getComputedStyle() on the live card title reads weight 400 / line-height 1.0 (47.68px on a
|
||||
// 47.69px font-size) / letter-spacing normal / text-transform none -- Bebas Neue's own glyphs
|
||||
// are what read as bold+condensed+caps-like, DX-life applies NO transform on top of the font.
|
||||
// Our fallback stack has no true Bebas-Neue substitute, so font-stretch:condensed stands in for
|
||||
// the face's natural narrowness, but weight/leading/tracking/case now match the measured values
|
||||
// exactly rather than compounding extra "boldness" the source doesn't have.
|
||||
'.region-card__title{ margin:0; font-size:clamp(var(--font-size-xl), 4cqw, var(--font-size-3xl)); line-height:1;' +
|
||||
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' font-weight:400; letter-spacing:normal; }' +
|
||||
// Their link is an <a class="region-card__link | with-icon">, so it carries their `a` rule's
|
||||
// font-weight: var(--font-weight-medium) = 500 (_globals.scss:132). Ours is a <button>
|
||||
// because this element has no router, and `font:inherit` gave it 400 — MEASURED against
|
||||
// their live /regions. Same class of miss as the dashboard back-link.
|
||||
// The icon spacing is theirs too: .with-icon is inline-flex with align-items:baseline and NO
|
||||
// gap (composition/_icon.scss:12), and the 1rem separation comes from
|
||||
// `.with-icon .icon.right { margin-inline-start: 1rem }` — 10px at their 10px root. We had
|
||||
// the same 10px via `gap`, which looks identical but means their rule would not apply, so
|
||||
// the mechanism is now theirs: margin on the icon, gap left alone.
|
||||
// ★ text-align:START, not left. Theirs is a NuxtLink, an inline <a> that shrink-wraps to its
|
||||
// text; ours must be a <button> because this element changes the embed's subject in place instead
|
||||
// of navigating a route. A button's UA default is text-align:center, so the port overrode it -- but
|
||||
// it overrode it with the PHYSICAL `left`, which is identical to `start` in English and its
|
||||
// opposite in Arabic. MEASURED on live /ar/regions vs ours at dir=rtl: same 246.9px row and the
|
||||
// same 102.7px of text, their label flush to the row's RIGHT edge (gap 0px, the RTL reading start)
|
||||
// and ours flush LEFT -- every label stranded 144.2px on the wrong side. Invisible in English,
|
||||
// which is exactly why it survived a 37-property card diff and an 11-property bullet diff.
|
||||
'.region-card__link{ appearance:none; background:none; border:0; padding:0; margin:0; text-align:start;' +
|
||||
' cursor:pointer; color:inherit; font:inherit; font-weight:500;' +
|
||||
' display:inline-flex; align-items:baseline; }' +
|
||||
'.region-card__link:hover{ color:#92b195; }' +
|
||||
'.region-card__link:focus-visible{ outline:2px solid #fff; outline-offset:2px; }' +
|
||||
// .region-card__icon: DX-life's own <svg class="icon | rotate-90 right"><use href="arrow-
|
||||
// chevron.svg#chevron-up"></use></svg> -- a REAL chevron path (viewBox 0 0 489.67 289.877),
|
||||
// not a text glyph. Ported verbatim (fill:currentColor tracks the surrounding text colour,
|
||||
// same as their `.icon{ fill:currentColor }` utility) and rotated 90deg to point right, same
|
||||
// as their `rotate-90` class. getComputedStyle measured it at 33.38px square on the live page.
|
||||
// ★ 1rem WAS THE ONE UNIT IN THIS PORT THAT INHERITS THE HOST PAGE'S ROOT, and it was wrong on
|
||||
// our own leaf, not only in an embed. `rem` resolves against the DOCUMENT root wherever the
|
||||
// element lives — which is the whole reason dxlife-tokens.js resolves their scale to px — and
|
||||
// their root is 10px (html{font-size: var(--font-size-base)}, $font-size-base: 10px). Ours
|
||||
// inherits whatever the host sets: 16px by default, so this gap rendered 16px against their
|
||||
// 10px, and on a host with the common html{font-size:62.5%} reset it would render 10px by
|
||||
// accident. Pinned to the value their root produces, like every other token here.
|
||||
// No diff caught it: the list diff's `link` spec reads colour, decoration, weight, display and
|
||||
// alignItems — no margin — so the icon's own spacing was never a measured property. It is now.
|
||||
// (Credit where due: the class of bug came to me as a flock relay from another agent's sweep.)
|
||||
'.region-card__icon{ width:0.7em; height:0.7em; flex:none; fill:currentColor; transform:rotate(90deg);' +
|
||||
' margin-inline-start:10px; }' +
|
||||
// .region-card__summary: --font-size-md (var(--font-size-md)), 0.85 opacity, on the header band.
|
||||
'.region-card__summary{ font-size:var(--font-size-md); opacity:0.85; margin:0; }' +
|
||||
// ★ 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. We ship a `ru` locale and had neither the rule nor the font.
|
||||
// ONLY the headings, exactly as theirs — their .region-stat__value is a <p> and keeps Bebas even in
|
||||
// Russian, which is their behaviour and so is ours.
|
||||
// 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"] .rl-title, [lang="ru-RU"] .rl-title,' +
|
||||
'[lang="ru"] .region-card__title, [lang="ru-RU"] .region-card__title{ font-family:"Oswald","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
||||
' letter-spacing:-0.02em; }' +
|
||||
// .region-card__body: padding --spacing-lg / clamp(...) / --spacing-xl -> var(--spacing-lg) / clamp / var(--spacing-xl)
|
||||
'.region-card__body{ padding:var(--spacing-lg) clamp(var(--spacing-md), 4cqw, var(--spacing-2xl)) var(--spacing-xl); }' +
|
||||
// .countries-grid: DX-life's track is minmax(220px,1fr). Gap --spacing-xs / --spacing-xl
|
||||
// (10px / var(--spacing-xl)). getComputedStyle on the live grid shows padding-left:35.16px -- that's the
|
||||
// page's global `ul` reset (bullets kept, not list-style:none), so the whole grid is indented
|
||||
// one list-item's worth even though DX-life's countries-grid a rule doesn't set it directly.
|
||||
// Reproduced with list-style:none + the same 35.16px indent kept explicitly so a real list
|
||||
// bullet has somewhere to sit without stealing width from the text column.
|
||||
// padding-INLINE-start, as theirs is (1.8em). The physical `padding-left:35.2px` it replaces
|
||||
// put the indent on the wrong side in Arabic, where the bullets hang off the right edge.
|
||||
'.countries-grid{ list-style:none; margin:0; padding-inline-start:1.8em; display:grid;' +
|
||||
' grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:10px var(--spacing-xl); }' +
|
||||
// Plain inline text (not a flex row) so multi-line country names wrap the same way DX-life's
|
||||
// <NuxtLink> text does -- a flex row with align-items:baseline pinned the bullet to the first
|
||||
// line only and stranded the (count) span beside the second line on wrap. font-weight:500
|
||||
// matches getComputedStyle() on the live country links (theirs is medium, not regular).
|
||||
// Their country entries are bare <NuxtLink>s inside .countries-grid li — no class of their
|
||||
// own — so they take their `a` rule: color var(--color-brand-light) = $color-green-400 =
|
||||
// #4e594f, font-weight medium. MEASURED on their live /regions: rgb(78, 89, 79). Ours used
|
||||
// var(--dx-text), which resolves to rgb(42, 51, 42) — the body ink, not their link ink.
|
||||
'.country-link{ appearance:none; background:none; border:0; padding:2px 0; margin:0;' +
|
||||
' text-align:start; cursor:pointer; color:#4e594f; font:inherit; font-size:var(--font-size-md);' +
|
||||
' font-weight:500; width:100%; position:relative; }' +
|
||||
// inset-inline-start, NOT left: the bullet hangs off the text's starting edge, which is the
|
||||
// RIGHT edge in Arabic. With `left:-16px` it stayed on the physical left in RTL — measured
|
||||
// at locale=ar, computed right was 256.938px instead of the -16px near-edge overhang.
|
||||
// ★ THEIR BULLET, COPIED — not a dot of my own. base/_globals.scss:117-131 paints the list
|
||||
// marker with a 16x16 data-URI SVG circle in #92b195 sized to 1.5em, hung off the text's start
|
||||
// edge by margin-inline-start:-1.3em (-2em in RTL), top .3em, line-height .9. Their
|
||||
// .countries-grid comment is explicit that keeping it is deliberate ("Keeps the site's list
|
||||
// bullets (global ul padding + ::before dot)").
|
||||
// I had re-created it as a 6px border-radius:50% square, and MEASURED against live it was wrong
|
||||
// in 9 of 11 properties: 6px vs 29.2969px box, solid fill vs a transparent box carrying the
|
||||
// image, top 11.72px vs 5.86px, and positioned with inset-inline-start:-16px instead of their
|
||||
// negative margin. Not a near-miss — a different bullet.
|
||||
// The rule is a double-quoted JS string because the data URI contains single quotes.
|
||||
".country-link::before{ content:''; position:absolute; top:0.3em; width:1.5em; height:1.5em;" +
|
||||
" line-height:0.9; margin-inline-start:-1.3em; background-repeat:no-repeat;" +
|
||||
" background-image:url(\"data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='%2392b195' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='7' stroke='%2392b195' stroke-width='1.5'/%3E%3C/svg%3E\"); }" +
|
||||
// Their RTL variant. It is a separate rule here rather than a nested [dir="rtl"] & because this
|
||||
// CSS lives in a shadow root, where a [dir] selector on an outer ancestor does not reach in.
|
||||
'[dir="rtl"] .country-link::before{ margin-inline-start:-2em; }' +
|
||||
'.country-link:hover{ text-decoration:underline; text-decoration-color:#92b195; }' +
|
||||
'.country-link:focus-visible{ outline:2px solid #92b195; outline-offset:2px; border-radius:4px; }' +
|
||||
'.country-link__count{ color:var(--dx-muted); }' +
|
||||
// .region-card__footer: DX-life's exact rule -- flex, flex-end, margin-top --spacing-lg (var(--spacing-lg)),
|
||||
// NO margin-bottom. This element is a <p>; its user-agent default margin-block-end (~var(--font-size-md) at
|
||||
// this font-size) was leaking through because only margin-top was ever set -- pin margin:0
|
||||
// first, then apply margin-top, so the bottom UA default can't survive the cascade.
|
||||
// The toggle row and the wall it opens. The caret rotates rather than swapping glyphs,
|
||||
// which is what makes open/closed legible at a glance.
|
||||
'.region-card__posters-toggle{ margin-top:0; }' +
|
||||
'.region-card__icon--open{ transform:rotate(180deg); }' +
|
||||
// RTL MIRRORING, ported from their utilities/_shapes.scss:
|
||||
// .rotate-90 { transform:rotate(90deg); [dir="rtl"] & { transform:rotate(-90deg); } }
|
||||
// Their .rotate-180 has NO rtl variant, and that distinction is the whole rule: a chevron that
|
||||
// means "forward" must mirror, a disclosure caret that means "open/closed" must not. MEASURED
|
||||
// on live doxa.life: .region-card__link svg is matrix(0,1,-1,0,0,0) at /regions and
|
||||
// matrix(0,-1,1,0,0,0) at /ar/regions — ours stayed at +90deg in Arabic, so the chevron sat on
|
||||
// the correct side pointing the WRONG WAY. Found by looking at the Arabic screenshot.
|
||||
// The selector works inside our shadow root because .rl-root carries :dir itself.
|
||||
'[dir="rtl"] .region-card__icon{ transform:rotate(-90deg); }' +
|
||||
// ...and this one is load-bearing: [dir="rtl"] .region-card__icon is specificity 0-2-0, which
|
||||
// would otherwise beat the 0-1-0 --open modifier and freeze the poster caret in Arabic.
|
||||
'[dir="rtl"] .region-card__icon--open{ transform:rotate(180deg); }' +
|
||||
'.region-card__posters{ margin-top:13.3px; }' +
|
||||
'.region-card__footer{ display:flex; justify-content:flex-end; margin:0; margin-top:var(--spacing-lg);' +
|
||||
' padding:0 clamp(var(--spacing-md), 4cqw, var(--spacing-2xl)) var(--spacing-lg); }' +
|
||||
'.region-card__footer-link{ appearance:none; background:none; border:0; padding:0; margin:0;' +
|
||||
' cursor:pointer; font:inherit; font-size:var(--font-size-md); font-weight:500; color:var(--dx-link);' +
|
||||
' display:inline-flex; align-items:baseline; gap:10px; text-decoration:underline; text-decoration-thickness:2px; }' +
|
||||
'.region-card__footer-link:hover{ color:var(--dx-header-bg); }' +
|
||||
'.region-card__footer-link:focus-visible{ outline:2px solid var(--dx-link); outline-offset:2px; border-radius:4px; }',
|
||||
'regions-list'
|
||||
, 'region-section-card')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="region-card">
|
||||
<div class="region-card__header">
|
||||
<h2 class="region-card__title">
|
||||
<button type="button" class="region-card__link" @click="emit('select')">
|
||||
{{ title }}
|
||||
<svg class="region-card__icon" viewBox="0 0 489.67 289.877" aria-hidden="true">
|
||||
<path d="M439.017,211.678L263.258,35.919c-3.9-3.9-8.635-6.454-13.63-7.665-9.539-2.376-20.051.161-27.509,7.619L46.361,211.632c-10.323,10.323-10.323,27.056,0,37.379,10.323,10.323,27.056,10.323,37.379,0l161.068-161.068,161.068,161.068c10.323,10.323,27.056,10.323,37.379,0,10.277-10.324,10.277-27.011-.238-37.333Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</h2>
|
||||
<p v-if="summary" class="region-card__summary">{{ summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="region-card__body">
|
||||
<ul class="countries-grid">
|
||||
<li v-for="c in countries" :key="c.slug || c.name">
|
||||
<button type="button" class="country-link" @click="emit('select-country', c)">
|
||||
{{ c.name }}<span v-if="c.count != null" class="country-link__count"> ({{ c.count }})</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p v-if="footerLabel" class="region-card__footer">
|
||||
<button type="button" class="region-card__footer-link" @click="emit('select')">
|
||||
{{ footerLabel }}
|
||||
<svg class="region-card__icon" viewBox="0 0 489.67 289.877" aria-hidden="true">
|
||||
<path d="M439.017,211.678L263.258,35.919c-3.9-3.9-8.635-6.454-13.63-7.665-9.539-2.376-20.051.161-27.509,7.619L46.361,211.632c-10.323,10.323-10.323,27.056,0,37.379,10.323,10.323,27.056,10.323,37.379,0l161.068-161.068,161.068,161.068c10.323,10.323,27.056,10.323,37.379,0,10.277-10.324,10.277-27.011-.238-37.333Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p v-if="disclosureLabel" class="region-card__footer region-card__posters-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="region-card__footer-link"
|
||||
:aria-expanded="disclosureOpen ? 'true' : 'false'"
|
||||
:aria-controls="disclosureId || undefined"
|
||||
@click="emit('toggle-disclosure')"
|
||||
>
|
||||
{{ disclosureLabel }}
|
||||
<svg class="region-card__icon" :class="{ 'region-card__icon--open': disclosureOpen }" viewBox="0 0 489.67 289.877" aria-hidden="true">
|
||||
<path d="M439.017,211.678L263.258,35.919c-3.9-3.9-8.635-6.454-13.63-7.665-9.539-2.376-20.051.161-27.509,7.619L46.361,211.632c-10.323,10.323-10.323,27.056,0,37.379,10.323,10.323,27.056,10.323,37.379,0l161.068-161.068,161.068,161.068c10.323,10.323,27.056,10.323,37.379,0,10.277-10.324,10.277-27.011-.238-37.333Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<!-- Only the revealed CONTENT is the consumer's. The regions plugin puts its PosterWall here. -->
|
||||
<div v-if="disclosureOpen" :id="disclosureId || undefined" class="region-card__posters">
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</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')
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ export function useCountryOutline(getMap, opts = {}) {
|
|||
// Add EMPTY first, then setData after the fetch, so addLayer below stays SYNCHRONOUS and
|
||||
// the outline keeps its place in the layer order.
|
||||
m.addSource(SOURCE_ID, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
fetch(`${resolveAssetsBase(null)}/boundaries/world-adm0.geojson`)
|
||||
fetch(`${resolveAssetsBase(null)}/boundaries/world-adm0-web.geojson`)
|
||||
.then((r) => r.json())
|
||||
.then((gj) => {
|
||||
// The tileset names the ISO field iso_3166_1_alpha_3; the geojson names it iso3.
|
||||
|
|
|
|||
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
|
||||
|
|
@ -434,7 +434,7 @@ export function useMapLayers(options = {}) {
|
|||
// EMPTY first so the addLayer below stays synchronous and keeps its z-order slot
|
||||
// (regions → shadow → pins → hitbox); setData once the fetch lands.
|
||||
map.addSource('regions', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||
fetch(`${resolveAssetsBase(null)}/boundaries/world-adm0.geojson`)
|
||||
fetch(`${resolveAssetsBase(null)}/boundaries/world-adm0-web.geojson`)
|
||||
.then((r) => r.json())
|
||||
.then((gj) => {
|
||||
// Tileset field is iso_3166_1_alpha_3; the geojson's is iso3. Normalise ONCE
|
||||
|
|
|
|||
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": "أخرى"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "فجوة الصم",
|
||||
"t_resources": "الموارد حسب الشعب",
|
||||
"t_languages": "اللغات حسب البلد",
|
||||
"t_growth": "الوصول مقابل نمو السكان"
|
||||
"t_growth": "الوصول مقابل نمو السكان",
|
||||
"t_bridgereach": "لغة واحدة، كم شعبًا؟"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "موارد الإنجيل حسب اللغة",
|
||||
|
|
@ -159,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": "الكتاب المقدس (أي نوع)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59% متصلون",
|
||||
"yes": "60% أو أكثر متصلون"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "عرض",
|
||||
"map": "الخريطة",
|
||||
"tree": "شجرة اللغات"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "الوصول بلغتهم، حسب استخدام الإنترنت في البلد",
|
||||
"aria": "رسوم بيانية",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "المجموعات",
|
||||
"map": "الخريطة"
|
||||
},
|
||||
"res": {
|
||||
"aria": "موارد الإنجيل للغات ضمن النطاق",
|
||||
"count": "{n} موردًا في {langs} من {all} لغة ضمن النطاق",
|
||||
"empty": "لم يُعثر على شيء للغات ضمن النطاق.",
|
||||
"groups": "المجموعات هنا",
|
||||
"note": "اللغة التي ليس لها صف هنا لم يُعثر لها على شيء في المصادر التي فُحصت.",
|
||||
"resource": "المورد",
|
||||
"scope": "النطاق",
|
||||
"source": "المصدر",
|
||||
"type": "النوع"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "متوسطة",
|
||||
"strong": "قوية"
|
||||
},
|
||||
"table": {
|
||||
"aria": "الجدول",
|
||||
"groups": "المجموعات العرقية",
|
||||
"resources": "موارد الإنجيل"
|
||||
},
|
||||
"title": "الوصول إلى الإنجيل عبر الإنترنت",
|
||||
"tree": {
|
||||
"aria": "شجرة اللغات: كل عائلة لغوية لهذه المجموعات العرقية، ملوّنة بحسب ما وُجد في كل لغة",
|
||||
"branch": "فرع",
|
||||
"dialect": "لهجة",
|
||||
"family": "عائلة",
|
||||
"groupsN": "{n} مجموعة عرقية",
|
||||
"inherited": "لون لغتها: لا يفهرس أي مصدر الموارد بحسب اللهجة.",
|
||||
"language": "لغة",
|
||||
"languagesHere": "لغات هنا",
|
||||
"legendTitle": "بلغتهم",
|
||||
"levels": "عائلة › فرع › لغة › لهجة. تُظهر العائلة أو الفرع أسوأ لون بداخله. وتُظهر اللهجة لون لغتها بدرجة أفتح: تُفهرس الموارد لكل لغة لا لكل لهجة. انقر دائرة لحصر اللوحة فيها.",
|
||||
"sources": "الشجرة: Glottolog (CC BY 4.0). الموارد: eBible وFaith Comes By Hearing وScripture Earth وPNG.Bible وفيلم يسوع وGlobal Recordings وغيرها، حسب ISO 639-3.",
|
||||
"st": {
|
||||
"g": "وُجد العهد الجديد أو الكتاب المقدس كاملًا",
|
||||
"r": "لم يُعثر على شيء",
|
||||
"y": "وُجد شيء لكن بلا عهد جديد"
|
||||
},
|
||||
"via": "أيضًا على هذا المسار:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "العرض",
|
||||
"board": "اللوحة",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
|
||||
"upg100WallTag": "تعبئة للصلاة",
|
||||
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>.",
|
||||
"upg100WallIntroA": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم ",
|
||||
"upg100WallIntroEmph": "باسم شعبهم",
|
||||
"upg100WallIntroB": ".",
|
||||
"loadingNamed": "جارٍ تحميل {name}…",
|
||||
"couldNotLoadNamed": "تعذّر تحميل {name}.",
|
||||
"engineMissing": "لم يتم تحميل محرك الخرائط {engine}.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "الدولة",
|
||||
"colLanguage": "اللغة",
|
||||
"colPopulation": "عدد السكان",
|
||||
"colReligion": "الدين"
|
||||
"colReligion": "الدين",
|
||||
"colPctChristian": "% مسيحيون",
|
||||
"colAdjacency": "التجاور",
|
||||
"colCode": "الرمز",
|
||||
"sortMobilization": "التعبئة",
|
||||
"sortFrontier": "المناطق الحدودية أولًا",
|
||||
"colName": "الاسم"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "التجميع حسب",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "جار مُبشَّر (تفصيلي، إحداثيات حقيقية)",
|
||||
"legendHelp": "انقر دائرة عليا لإعادة التجميع · انقر نواة لتتفتح بتلاتها",
|
||||
"clustering": "جارٍ تجميع الحصاد حسب اللغة…",
|
||||
"signLanguage": "لغة الإشارة"
|
||||
"signLanguage": "لغة الإشارة",
|
||||
"mode": {
|
||||
"family": "العائلة",
|
||||
"language": "اللغة",
|
||||
"dialect": "اللهجة"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "أقل · أكبر",
|
||||
"more": "أكثر",
|
||||
"most": "الأكثر"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "ترميز اللون الأخضر",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "عرض الخريطة أو الزهرة",
|
||||
"loading": "جارٍ تحميل حقل الحصاد…",
|
||||
"unreachedTarget": "هدف غير مُبشَّر"
|
||||
"unreachedTarget": "هدف غير مُبشَّر",
|
||||
"map": "الخريطة",
|
||||
"flower": "الزهرة",
|
||||
"details": "التفاصيل",
|
||||
"senders": "مُرسِلون ←",
|
||||
"clustersHidden": "مجموعات · {n} مخفية",
|
||||
"legendSend": "SEND (متصل = اللغة نفسها · متقطع = مجموعة · منقط = داخل الدولة)",
|
||||
"legendSelection": "التحديد · نصف القطر = لوغاريتم السكان · لوقا 10:2",
|
||||
"navySend": "الكحلي = SEND",
|
||||
"goldMesh": "الذهبي = شبكة اللغة الواحدة"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "اختر هدفًا أحمر على الخريطة —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "جاهز · دبابيس الشعوب في الدول: {pins} · انقر أي دبوس",
|
||||
"readyLanguage": "جاهز · الدبابيس: {pins} · تغطية العائلات {coverage}% · أعلى 25 = {top}%",
|
||||
"readyReligion": "الدبابيس: {pins} · الأكثر شيوعًا: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "اختر عرضًا",
|
||||
"search": "ابحث بالشعب أو المكان أو اللغة أو الدين…",
|
||||
"baseMap": "الخريطة الأساسية",
|
||||
"dataSources": "مصادر البيانات",
|
||||
"legend": "مفتاح الخريطة",
|
||||
"openLegend": "افتح مفتاح الخريطة",
|
||||
"clearSelectionEsc": "إلغاء التحديد (Esc)",
|
||||
"closeEsc": "إغلاق (Esc)",
|
||||
"close": "إغلاق",
|
||||
"retry": "أعد المحاولة",
|
||||
"loadingMap": "جارٍ تحميل الخريطة",
|
||||
"flyToPin": "انتقل إلى الدبوس",
|
||||
"primaryReligion": "الدين الرئيسي",
|
||||
"ropHierarchy": "تسلسل ROP الهرمي"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "توزيع الأديان · انقر دبوسًا للتفاصيل",
|
||||
"ropTree": "تسلسل ROP الهرمي · الكتلة ← المجموعة ← الشعب ← PGIC",
|
||||
"languageFamilies": "العائلات العرقية اللغوية (PPLR) · انقر دبوسًا للتفاصيل",
|
||||
"sunburst": "تسلسل ROP الهرمي · الكتلة ← مجموعة الشعوب · مرّر للتكبير",
|
||||
"pins": "كل دبابيس الشعوب في الدول · انقر لمعرّفات الأنظمة",
|
||||
"coverage": "تغطية الترجمة · التظليل = متوسط اللغات المعبّأة لكل دولة · انقر دبوسًا أو دولة",
|
||||
"rosetta": "جدول معرّفات الأنظمة · اللون = المعرّفات الموجودة · ★ = صف نموذجي"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "متوسط اللغات / {n}",
|
||||
"idCoverage": "تغطية المعرّفات"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD، الثلاثة",
|
||||
"imbOnly": "IMB فقط",
|
||||
"jpOnly": "JP فقط",
|
||||
"neither": "لا هذا ولا ذاك"
|
||||
},
|
||||
"samplePg": "★ شعب نموذجي",
|
||||
"missingIds": "لم تُربط بعد في PPLR: {ids}. الصف الفارغ نقص في البيانات وليس خطأ."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(بلا اسم)",
|
||||
"country": "الدولة",
|
||||
"religion": "الدين",
|
||||
"language": "اللغة",
|
||||
"family": "العائلة",
|
||||
"affinityBloc": "الكتلة",
|
||||
"cluster": "المجموعة",
|
||||
"peopleCluster": "مجموعة الشعوب",
|
||||
"peopleGroup": "الشعب",
|
||||
"pgCount": "الشعوب",
|
||||
"avgLocalesFilled": "متوسط اللغات المعبّأة",
|
||||
"iso3Country": "رمز الدولة ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "الاسم في {n} لغة"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "Die Gehörlosen-Lücke",
|
||||
"t_resources": "Ressourcen nach Volksgruppe",
|
||||
"t_languages": "Sprachen nach Land",
|
||||
"t_growth": "Zugang vs. Bevölkerungswachstum"
|
||||
"t_growth": "Zugang vs. Bevölkerungswachstum",
|
||||
"t_bridgereach": "Eine Sprache, wie viele Völker"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Evangeliums-Ressourcen nach Sprache",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % online",
|
||||
"yes": "60 % oder mehr online"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Anzeigen",
|
||||
"map": "Karte",
|
||||
"tree": "Sprachbaum"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Zugang in eigener Sprache, nach Internetnutzung im Land",
|
||||
"aria": "Diagramme",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Gruppen",
|
||||
"map": "Karte"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Evangeliums-Ressourcen für die Sprachen im Auswahlbereich",
|
||||
"count": "{n} Ressourcen in {langs} von {all} Sprachen im Auswahlbereich",
|
||||
"empty": "Für die Sprachen im Auswahlbereich wurde nichts gefunden.",
|
||||
"groups": "Gruppen hier",
|
||||
"note": "Eine Sprache ohne Zeile hier hat in den geprüften Quellen nichts.",
|
||||
"resource": "Ressource",
|
||||
"scope": "Umfang",
|
||||
"source": "Quelle",
|
||||
"type": "Art"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "mittel",
|
||||
"strong": "stark"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tabelle",
|
||||
"groups": "Volksgruppen",
|
||||
"resources": "Evangeliums-Ressourcen"
|
||||
},
|
||||
"title": "Zugang zum Evangelium über das Internet",
|
||||
"tree": {
|
||||
"aria": "Sprachbaum: jede Sprachfamilie dieser Volksgruppen, gefärbt danach, was in jeder Sprache gefunden wurde",
|
||||
"branch": "Zweig",
|
||||
"dialect": "Dialekt",
|
||||
"family": "Familie",
|
||||
"groupsN": "{n} Volksgruppen",
|
||||
"inherited": "Farbe ihrer Sprache: Keine Quelle erfasst Ressourcen nach Dialekt.",
|
||||
"language": "Sprache",
|
||||
"languagesHere": "Sprachen hier",
|
||||
"legendTitle": "In ihrer eigenen Sprache",
|
||||
"levels": "Familie › Zweig › Sprache › Dialekt. Eine Familie oder ein Zweig zeigt die schlechteste Farbe darin. Ein Dialekt zeigt die Farbe seiner Sprache, heller: Ressourcen werden pro Sprache erfasst, nicht pro Dialekt. Klicken Sie auf einen Kreis, um die Übersicht darauf einzugrenzen.",
|
||||
"sources": "Baum: Glottolog (CC BY 4.0). Ressourcen: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, JESUS-Film, Global Recordings und weitere, nach ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Neues Testament oder ganze Bibel gefunden",
|
||||
"r": "Nichts gefunden",
|
||||
"y": "Etwas gefunden, aber kein Neues Testament"
|
||||
},
|
||||
"via": "Auch auf diesem Pfad:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Ansicht",
|
||||
"board": "Übersicht",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Land",
|
||||
"colLanguage": "Sprache",
|
||||
"colPopulation": "Bevölkerung",
|
||||
"colReligion": "Religion"
|
||||
"colReligion": "Religion",
|
||||
"colPctChristian": "% Christen",
|
||||
"colAdjacency": "Nachbarschaft",
|
||||
"colCode": "Kennung",
|
||||
"sortMobilization": "Mobilisierung",
|
||||
"sortFrontier": "Grenzgebiete zuerst",
|
||||
"colName": "Name"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Gruppieren nach",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Erreichter Nachbar (fein, echte Koordinaten)",
|
||||
"legendHelp": "oberen Kreis anklicken zum Neugruppieren · Kern anklicken, um die Blütenblätter zu öffnen",
|
||||
"clustering": "Die Ernte wird nach Sprache gruppiert…",
|
||||
"signLanguage": "Gebärdensprache"
|
||||
"signLanguage": "Gebärdensprache",
|
||||
"mode": {
|
||||
"family": "Familie",
|
||||
"language": "Sprache",
|
||||
"dialect": "Dialekt"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "weniger · größer",
|
||||
"more": "mehr",
|
||||
"most": "am meisten"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Kodierung der Grünfarbe",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Karten- oder Blumenansicht",
|
||||
"loading": "Erntefeld wird geladen…",
|
||||
"unreachedTarget": "unerreichtes Ziel"
|
||||
"unreachedTarget": "unerreichtes Ziel",
|
||||
"map": "Karte",
|
||||
"flower": "Blume",
|
||||
"details": "Details",
|
||||
"senders": "Sender →",
|
||||
"clustersHidden": "Cluster · {n} ausgeblendet",
|
||||
"legendSend": "SEND (durchgezogen = gleiche Sprache · gestrichelt = Cluster · gepunktet = im Land)",
|
||||
"legendSelection": "Auswahl · Radius = log. Bevölkerung · Lukas 10,2",
|
||||
"navySend": "Marineblau = SEND",
|
||||
"goldMesh": "Gold = Netz gleicher Sprache"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Ein rotes Ziel auf der Karte wählen —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Bereit · Pins: {pins} · einen Pin anklicken",
|
||||
"readyLanguage": "Bereit · Pins: {pins} · Familienabdeckung {coverage} % · Top 25 = {top} %",
|
||||
"readyReligion": "Pins: {pins} · am häufigsten: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Ansicht wählen",
|
||||
"search": "Nach Volk, Ort, Sprache, Religion suchen…",
|
||||
"baseMap": "Grundkarte",
|
||||
"dataSources": "Datenquellen",
|
||||
"legend": "Legende",
|
||||
"openLegend": "Legende öffnen",
|
||||
"clearSelectionEsc": "Auswahl aufheben (Esc)",
|
||||
"closeEsc": "Schließen (Esc)",
|
||||
"close": "Schließen",
|
||||
"retry": "Erneut versuchen",
|
||||
"loadingMap": "Karte wird geladen",
|
||||
"flyToPin": "Zum Pin fliegen",
|
||||
"primaryReligion": "Hauptreligion",
|
||||
"ropHierarchy": "ROP-Hierarchie"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Verteilung der Religionen · Pin anklicken für Details",
|
||||
"ropTree": "ROP-Hierarchie · Affinitätsblock → Cluster → Volk → PGIC",
|
||||
"languageFamilies": "Ethnolinguistische Familien (PPLR) · Pin anklicken für Details",
|
||||
"sunburst": "ROP-Hierarchie · Affinitätsblock → Volkscluster · scrollen zum Zoomen",
|
||||
"pins": "Alle Volksgruppen-Pins je Land · anklicken für systemübergreifende IDs",
|
||||
"coverage": "Übersetzungsabdeckung · Färbung = durchschnittlich gefüllte Sprachen je Land · Pin oder Land anklicken",
|
||||
"rosetta": "ID-Rosetta · Farbe = vorhandene IDs · ★ = Beispielzeile"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Durchschnitt Sprachen / {n}",
|
||||
"idCoverage": "ID-Abdeckung"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, alle drei",
|
||||
"imbOnly": "Nur IMB",
|
||||
"jpOnly": "Nur JP",
|
||||
"neither": "keins von beiden"
|
||||
},
|
||||
"samplePg": "★ Beispiel-Volk",
|
||||
"missingIds": "In PPLR noch nicht zugeordnet: {ids}. Eine leere Zeile ist eine Datenlücke, kein Fehler."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(ohne Namen)",
|
||||
"country": "Land",
|
||||
"religion": "Religion",
|
||||
"language": "Sprache",
|
||||
"family": "Familie",
|
||||
"affinityBloc": "Affinitätsblock",
|
||||
"cluster": "Cluster",
|
||||
"peopleCluster": "Volkscluster",
|
||||
"peopleGroup": "Volk",
|
||||
"pgCount": "Völker",
|
||||
"avgLocalesFilled": "Durchschnittlich gefüllte Sprachen",
|
||||
"iso3Country": "ISO-3-Land"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Name in {n} Sprachen"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "The Deaf Gap",
|
||||
"t_resources": "Resources by People Group",
|
||||
"t_languages": "Languages by Country",
|
||||
"t_growth": "Access vs Population Growth"
|
||||
"t_growth": "Access vs Population Growth",
|
||||
"t_bridgereach": "One Language, How Many Peoples"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Gospel Resources by Language",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25-59% online",
|
||||
"yes": "60% or more online"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Show",
|
||||
"map": "Map",
|
||||
"tree": "Language tree"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Own-language access, by country internet use",
|
||||
"aria": "Charts",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Groups",
|
||||
"map": "Map"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Gospel resources for the languages in scope",
|
||||
"count": "{n} resources in {langs} of {all} languages in scope",
|
||||
"empty": "Nothing found for the languages in scope.",
|
||||
"groups": "Groups here",
|
||||
"note": "A language with no row here has nothing found in the sources checked.",
|
||||
"resource": "Resource",
|
||||
"scope": "Scope",
|
||||
"source": "Source",
|
||||
"type": "Type"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moderate",
|
||||
"strong": "strong"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Table",
|
||||
"groups": "People groups",
|
||||
"resources": "Gospel resources"
|
||||
},
|
||||
"title": "Gospel access through the internet",
|
||||
"tree": {
|
||||
"aria": "Language tree: every language family of these people groups, coloured by what is found in each language",
|
||||
"branch": "Branch",
|
||||
"dialect": "Dialect",
|
||||
"family": "Family",
|
||||
"groupsN": "{n} people groups",
|
||||
"inherited": "Colour of its language: no source catalogues resources by dialect.",
|
||||
"language": "Language",
|
||||
"languagesHere": "languages here",
|
||||
"legendTitle": "In their own language",
|
||||
"levels": "Family › branch › language › dialect. A family or branch shows the worst colour inside it. A dialect shows its language's colour, lighter: resources are catalogued per language, not per dialect. Click a circle to narrow the board to it.",
|
||||
"sources": "Tree: Glottolog (CC BY 4.0). Resources: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, JESUS Film, Global Recordings and others, keyed by ISO 639-3.",
|
||||
"st": {
|
||||
"g": "New Testament or full Bible found",
|
||||
"r": "Nothing found",
|
||||
"y": "Something found, but no New Testament"
|
||||
},
|
||||
"via": "Also on this path:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "View",
|
||||
"board": "Board",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Country",
|
||||
"colLanguage": "Language",
|
||||
"colPopulation": "Population",
|
||||
"colReligion": "Religion"
|
||||
"colReligion": "Religion",
|
||||
"colPctChristian": "% Christian",
|
||||
"colAdjacency": "Adjacency",
|
||||
"colCode": "Code",
|
||||
"sortMobilization": "Mobilization",
|
||||
"sortFrontier": "Frontier first",
|
||||
"colName": "Name"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Cluster by",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Reached neighbor (granular, real coordinates)",
|
||||
"legendHelp": "click a top circle to re-cluster · click a core to bloom its petals",
|
||||
"clustering": "Clustering the harvest by language…",
|
||||
"signLanguage": "Sign Language"
|
||||
"signLanguage": "Sign Language",
|
||||
"mode": {
|
||||
"family": "Family",
|
||||
"language": "Language",
|
||||
"dialect": "Dialect"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "fewer · bigger",
|
||||
"more": "more",
|
||||
"most": "most"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Green color encoding",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Map or flower view",
|
||||
"loading": "Loading the harvest field…",
|
||||
"unreachedTarget": "unreached target"
|
||||
"unreachedTarget": "unreached target",
|
||||
"map": "Map",
|
||||
"flower": "Flower",
|
||||
"details": "Details",
|
||||
"senders": "senders →",
|
||||
"clustersHidden": "clusters · {n} hidden",
|
||||
"legendSend": "SEND (solid = same language · dashed = cluster · dotted = in-country)",
|
||||
"legendSelection": "selection · radius = log of population · Luke 10:2",
|
||||
"navySend": "navy = SEND",
|
||||
"goldMesh": "gold = same-language mesh"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Pick a red target on the map —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Ready · {pins} PG-in-country pins · click any pin",
|
||||
"readyLanguage": "Ready · {pins} pins · {coverage}% family coverage · top-25 = {top}%",
|
||||
"readyReligion": "{pins} pins · {religion} most common ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Choose a view",
|
||||
"search": "Search by people, place, language, religion…",
|
||||
"baseMap": "Base map",
|
||||
"dataSources": "Data sources",
|
||||
"legend": "Legend",
|
||||
"openLegend": "Open legend",
|
||||
"clearSelectionEsc": "Clear selection (Esc)",
|
||||
"closeEsc": "Close (Esc)",
|
||||
"close": "Close",
|
||||
"retry": "Retry",
|
||||
"loadingMap": "Loading map",
|
||||
"flyToPin": "Fly to pin",
|
||||
"primaryReligion": "Primary religion",
|
||||
"ropHierarchy": "ROP hierarchy"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Religion distribution · click a pin for detail",
|
||||
"ropTree": "ROP hierarchy · Affinity Bloc → Cluster → People Group → PGIC",
|
||||
"languageFamilies": "Ethnolinguistic families (PPLR) · click a pin for detail",
|
||||
"sunburst": "ROP hierarchy · Affinity Bloc → People Cluster · scroll to zoom",
|
||||
"pins": "All people-group-in-country pins · click for cross-system IDs",
|
||||
"coverage": "Translation coverage · shading = average locales filled per country · click a pin or a country",
|
||||
"rosetta": "Cross-system ID Rosetta · colour = which IDs exist · ★ = sample row"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Average locales / {n}",
|
||||
"idCoverage": "ID coverage"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, all three",
|
||||
"imbOnly": "IMB only",
|
||||
"jpOnly": "JP only",
|
||||
"neither": "neither"
|
||||
},
|
||||
"samplePg": "★ sample people group",
|
||||
"missingIds": "Not yet cross-mapped in PPLR: {ids}. An empty row is a data gap, not an error."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(no name)",
|
||||
"country": "Country",
|
||||
"religion": "Religion",
|
||||
"language": "Language",
|
||||
"family": "Family",
|
||||
"affinityBloc": "Affinity Bloc",
|
||||
"cluster": "Cluster",
|
||||
"peopleCluster": "People cluster",
|
||||
"peopleGroup": "People group",
|
||||
"pgCount": "People groups",
|
||||
"avgLocalesFilled": "Average locales filled",
|
||||
"iso3Country": "ISO-3 country"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Name in {n} locales"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "La brecha sorda",
|
||||
"t_resources": "Recursos por pueblo",
|
||||
"t_languages": "Idiomas por país",
|
||||
"t_growth": "Acceso vs crecimiento"
|
||||
"t_growth": "Acceso vs crecimiento",
|
||||
"t_bridgereach": "Una lengua, cuántos pueblos"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Recursos del evangelio por idioma",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % en línea",
|
||||
"yes": "60 % o más en línea"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Mostrar",
|
||||
"map": "Mapa",
|
||||
"tree": "Árbol de idiomas"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Acceso en su idioma, según el uso de internet del país",
|
||||
"aria": "Gráficos",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Grupos",
|
||||
"map": "Mapa"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Recursos del evangelio para los idiomas en la selección",
|
||||
"count": "{n} recursos en {langs} de {all} idiomas en la selección",
|
||||
"empty": "No se encontró nada para los idiomas en la selección.",
|
||||
"groups": "Grupos aquí",
|
||||
"note": "Un idioma sin fila aquí no tiene nada en las fuentes consultadas.",
|
||||
"resource": "Recurso",
|
||||
"scope": "Alcance",
|
||||
"source": "Fuente",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moderada",
|
||||
"strong": "fuerte"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tabla",
|
||||
"groups": "Grupos étnicos",
|
||||
"resources": "Recursos del evangelio"
|
||||
},
|
||||
"title": "Acceso al evangelio a través de internet",
|
||||
"tree": {
|
||||
"aria": "Árbol de idiomas: cada familia lingüística de estos grupos étnicos, coloreada según lo que existe en cada idioma",
|
||||
"branch": "Rama",
|
||||
"dialect": "Dialecto",
|
||||
"family": "Familia",
|
||||
"groupsN": "{n} grupos étnicos",
|
||||
"inherited": "Color de su idioma: ninguna fuente cataloga recursos por dialecto.",
|
||||
"language": "Idioma",
|
||||
"languagesHere": "idiomas aquí",
|
||||
"legendTitle": "En su propio idioma",
|
||||
"levels": "Familia › rama › idioma › dialecto. Una familia o rama muestra el peor color que contiene. Un dialecto muestra el color de su idioma, más claro: los recursos se catalogan por idioma, no por dialecto. Haga clic en un círculo para limitar el tablero a él.",
|
||||
"sources": "Árbol: Glottolog (CC BY 4.0). Recursos: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, película JESÚS, Global Recordings y otros, por ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Nuevo Testamento o Biblia completa encontrados",
|
||||
"r": "Nada encontrado",
|
||||
"y": "Algo encontrado, pero sin Nuevo Testamento"
|
||||
},
|
||||
"via": "También en esta ruta:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Vista",
|
||||
"board": "Tablero",
|
||||
|
|
|
|||
|
|
@ -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ó.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "País",
|
||||
"colLanguage": "Idioma",
|
||||
"colPopulation": "Población",
|
||||
"colReligion": "Religión"
|
||||
"colReligion": "Religión",
|
||||
"colPctChristian": "% cristianos",
|
||||
"colAdjacency": "Adyacencia",
|
||||
"colCode": "Código",
|
||||
"sortMobilization": "Movilización",
|
||||
"sortFrontier": "Frontera primero",
|
||||
"colName": "Nombre"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Agrupar por",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Vecino alcanzado (detallado, coordenadas reales)",
|
||||
"legendHelp": "haz clic en un círculo superior para reagrupar · haz clic en un núcleo para abrir sus pétalos",
|
||||
"clustering": "Agrupando la cosecha por idioma…",
|
||||
"signLanguage": "Lengua de señas"
|
||||
"signLanguage": "Lengua de señas",
|
||||
"mode": {
|
||||
"family": "Familia",
|
||||
"language": "Idioma",
|
||||
"dialect": "Dialecto"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "menos · más grandes",
|
||||
"more": "más",
|
||||
"most": "la mayoría"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Codificación del color verde",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Vista de mapa o de flor",
|
||||
"loading": "Cargando el campo de la cosecha…",
|
||||
"unreachedTarget": "objetivo no alcanzado"
|
||||
"unreachedTarget": "objetivo no alcanzado",
|
||||
"map": "Mapa",
|
||||
"flower": "Flor",
|
||||
"details": "Detalles",
|
||||
"senders": "emisores →",
|
||||
"clustersHidden": "clústeres · {n} ocultos",
|
||||
"legendSend": "SEND (continua = mismo idioma · discontinua = clúster · punteada = en el país)",
|
||||
"legendSelection": "selección · radio = log de la población · Lucas 10:2",
|
||||
"navySend": "azul marino = SEND",
|
||||
"goldMesh": "dorado = red de un mismo idioma"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Elige un objetivo rojo en el mapa —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Listo · pines: {pins} · haz clic en un pin",
|
||||
"readyLanguage": "Listo · pines: {pins} · cobertura de familias {coverage} % · top 25 = {top} %",
|
||||
"readyReligion": "Pines: {pins} · más común: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Elige una vista",
|
||||
"search": "Buscar por pueblo, lugar, idioma, religión…",
|
||||
"baseMap": "Mapa base",
|
||||
"dataSources": "Fuentes de datos",
|
||||
"legend": "Leyenda",
|
||||
"openLegend": "Abrir la leyenda",
|
||||
"clearSelectionEsc": "Borrar la selección (Esc)",
|
||||
"closeEsc": "Cerrar (Esc)",
|
||||
"close": "Cerrar",
|
||||
"retry": "Reintentar",
|
||||
"loadingMap": "Cargando el mapa",
|
||||
"flyToPin": "Ir al pin",
|
||||
"primaryReligion": "Religión principal",
|
||||
"ropHierarchy": "Jerarquía ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Distribución de religiones · haz clic en un pin para ver detalles",
|
||||
"ropTree": "Jerarquía ROP · bloque de afinidad → clúster → pueblo → PGIC",
|
||||
"languageFamilies": "Familias etnolingüísticas (PPLR) · haz clic en un pin para ver detalles",
|
||||
"sunburst": "Jerarquía ROP · bloque de afinidad → clúster de pueblos · desplázate para ampliar",
|
||||
"pins": "Todos los pines de pueblos por país · haz clic para ver los identificadores de cada sistema",
|
||||
"coverage": "Cobertura de traducción · sombreado = promedio de idiomas completados por país · haz clic en un pin o un país",
|
||||
"rosetta": "Rosetta de identificadores · color = identificadores existentes · ★ = fila de ejemplo"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Promedio de idiomas / {n}",
|
||||
"idCoverage": "Cobertura de identificadores"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, los tres",
|
||||
"imbOnly": "Solo IMB",
|
||||
"jpOnly": "Solo JP",
|
||||
"neither": "ninguno"
|
||||
},
|
||||
"samplePg": "★ pueblo de ejemplo",
|
||||
"missingIds": "Aún sin vincular en PPLR: {ids}. Una fila vacía es un hueco en los datos, no un error."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(sin nombre)",
|
||||
"country": "País",
|
||||
"religion": "Religión",
|
||||
"language": "Idioma",
|
||||
"family": "Familia",
|
||||
"affinityBloc": "Bloque de afinidad",
|
||||
"cluster": "Clúster",
|
||||
"peopleCluster": "Clúster de pueblos",
|
||||
"peopleGroup": "Pueblo",
|
||||
"pgCount": "Pueblos",
|
||||
"avgLocalesFilled": "Promedio de idiomas completados",
|
||||
"iso3Country": "País ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Nombre en {n} idiomas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "L'écart sourd",
|
||||
"t_resources": "Ressources par peuple",
|
||||
"t_languages": "Langues par pays",
|
||||
"t_growth": "Accès vs croissance"
|
||||
"t_growth": "Accès vs croissance",
|
||||
"t_bridgereach": "Une langue, combien de peuples"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Ressources de l'Évangile par langue",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % en ligne",
|
||||
"yes": "60 % ou plus en ligne"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Afficher",
|
||||
"map": "Carte",
|
||||
"tree": "Arbre des langues"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Accès dans leur langue, selon l’usage d’internet du pays",
|
||||
"aria": "Graphiques",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Groupes",
|
||||
"map": "Carte"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Ressources de l’Évangile pour les langues sélectionnées",
|
||||
"count": "{n} ressources dans {langs} des {all} langues sélectionnées",
|
||||
"empty": "Rien trouvé pour les langues sélectionnées.",
|
||||
"groups": "Groupes ici",
|
||||
"note": "Une langue sans ligne ici n’a rien dans les sources consultées.",
|
||||
"resource": "Ressource",
|
||||
"scope": "Portée",
|
||||
"source": "Source",
|
||||
"type": "Type"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moyenne",
|
||||
"strong": "forte"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tableau",
|
||||
"groups": "Groupes ethniques",
|
||||
"resources": "Ressources de l’Évangile"
|
||||
},
|
||||
"title": "L’accès à l’Évangile par internet",
|
||||
"tree": {
|
||||
"aria": "Arbre des langues : chaque famille de langues de ces groupes ethniques, colorée selon ce qui existe dans chaque langue",
|
||||
"branch": "Branche",
|
||||
"dialect": "Dialecte",
|
||||
"family": "Famille",
|
||||
"groupsN": "{n} groupes ethniques",
|
||||
"inherited": "Couleur de sa langue : aucune source ne recense les ressources par dialecte.",
|
||||
"language": "Langue",
|
||||
"languagesHere": "langues ici",
|
||||
"legendTitle": "Dans leur propre langue",
|
||||
"levels": "Famille › branche › langue › dialecte. Une famille ou une branche prend la pire couleur qu’elle contient. Un dialecte prend la couleur de sa langue, plus claire : les ressources sont recensées par langue, pas par dialecte. Cliquez sur un cercle pour y restreindre le tableau.",
|
||||
"sources": "Arbre : Glottolog (CC BY 4.0). Ressources : eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, film JÉSUS, Global Recordings et d’autres, par code ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Nouveau Testament ou Bible complète trouvé",
|
||||
"r": "Rien trouvé",
|
||||
"y": "Quelque chose trouvé, mais pas de Nouveau Testament"
|
||||
},
|
||||
"via": "Aussi sur ce chemin :"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Vue",
|
||||
"board": "Tableau",
|
||||
|
|
|
|||
|
|
@ -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é.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Pays",
|
||||
"colLanguage": "Langue",
|
||||
"colPopulation": "Population",
|
||||
"colReligion": "Religion"
|
||||
"colReligion": "Religion",
|
||||
"colPctChristian": "% chrétiens",
|
||||
"colAdjacency": "Proximité",
|
||||
"colCode": "Code",
|
||||
"sortMobilization": "Mobilisation",
|
||||
"sortFrontier": "Frontières d’abord",
|
||||
"colName": "Nom"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Regrouper par",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Voisin atteint (détaillé, coordonnées réelles)",
|
||||
"legendHelp": "cliquez sur un cercle du haut pour regrouper · cliquez sur un cœur pour ouvrir ses pétales",
|
||||
"clustering": "Regroupement de la moisson par langue…",
|
||||
"signLanguage": "Langue des signes"
|
||||
"signLanguage": "Langue des signes",
|
||||
"mode": {
|
||||
"family": "Famille",
|
||||
"language": "Langue",
|
||||
"dialect": "Dialecte"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "moins · plus grands",
|
||||
"more": "plus",
|
||||
"most": "le plus"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Codage de la couleur verte",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Vue carte ou fleur",
|
||||
"loading": "Chargement du champ de la moisson…",
|
||||
"unreachedTarget": "cible non atteinte"
|
||||
"unreachedTarget": "cible non atteinte",
|
||||
"map": "Carte",
|
||||
"flower": "Fleur",
|
||||
"details": "Détails",
|
||||
"senders": "envoyeurs →",
|
||||
"clustersHidden": "grappes · {n} masqués",
|
||||
"legendSend": "SEND (plein = même langue · tirets = grappe · pointillé = dans le pays)",
|
||||
"legendSelection": "sélection · rayon = log de la population · Luc 10.2",
|
||||
"navySend": "bleu marine = SEND",
|
||||
"goldMesh": "or = réseau de même langue"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Choisissez une cible rouge sur la carte —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Prêt · épingles : {pins} · cliquez sur une épingle",
|
||||
"readyLanguage": "Prêt · épingles : {pins} · couverture des familles {coverage} % · top 25 = {top} %",
|
||||
"readyReligion": "Épingles : {pins} · la plus fréquente : {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Choisir une vue",
|
||||
"search": "Rechercher par peuple, lieu, langue, religion…",
|
||||
"baseMap": "Fond de carte",
|
||||
"dataSources": "Sources des données",
|
||||
"legend": "Légende",
|
||||
"openLegend": "Ouvrir la légende",
|
||||
"clearSelectionEsc": "Effacer la sélection (Échap)",
|
||||
"closeEsc": "Fermer (Échap)",
|
||||
"close": "Fermer",
|
||||
"retry": "Réessayer",
|
||||
"loadingMap": "Chargement de la carte",
|
||||
"flyToPin": "Aller à l’épingle",
|
||||
"primaryReligion": "Religion principale",
|
||||
"ropHierarchy": "Hiérarchie ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Répartition des religions · cliquez sur une épingle pour le détail",
|
||||
"ropTree": "Hiérarchie ROP · bloc d’affinité → grappe → peuple → PGIC",
|
||||
"languageFamilies": "Familles ethnolinguistiques (PPLR) · cliquez sur une épingle pour le détail",
|
||||
"sunburst": "Hiérarchie ROP · bloc d’affinité → grappe de peuples · faites défiler pour zoomer",
|
||||
"pins": "Toutes les épingles peuple-pays · cliquez pour les identifiants des systèmes",
|
||||
"coverage": "Couverture de traduction · teinte = moyenne des langues renseignées par pays · cliquez sur une épingle ou un pays",
|
||||
"rosetta": "Rosetta des identifiants · couleur = identifiants présents · ★ = ligne d’exemple"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Moyenne des langues / {n}",
|
||||
"idCoverage": "Couverture des identifiants"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, les trois",
|
||||
"imbOnly": "IMB seulement",
|
||||
"jpOnly": "JP seulement",
|
||||
"neither": "aucun des deux"
|
||||
},
|
||||
"samplePg": "★ peuple d’exemple",
|
||||
"missingIds": "Pas encore mis en correspondance dans PPLR : {ids}. Une ligne vide est une lacune des données, pas une erreur."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(sans nom)",
|
||||
"country": "Pays",
|
||||
"religion": "Religion",
|
||||
"language": "Langue",
|
||||
"family": "Famille",
|
||||
"affinityBloc": "Bloc d’affinité",
|
||||
"cluster": "Grappe",
|
||||
"peopleCluster": "Grappe de peuples",
|
||||
"peopleGroup": "Peuple",
|
||||
"pgCount": "Peuples",
|
||||
"avgLocalesFilled": "Moyenne des langues renseignées",
|
||||
"iso3Country": "Pays ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Nom dans {n} langues"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "प्रार्थना करने वाले लोग",
|
||||
"selected_first": "चयनित पहले",
|
||||
"selected_first_hint": "चयनित जनसमूहों को ऊपर लाएँ, आपका कॉलम क्रम बनाए रखते हुए",
|
||||
"n_shown": "{n} दिखाए गए"
|
||||
"n_shown": "{n} दिखाए गए",
|
||||
"other": "अन्य"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "बधिर कमी",
|
||||
"t_resources": "जनसमूह अनुसार संसाधन",
|
||||
"t_languages": "देश अनुसार भाषाएँ",
|
||||
"t_growth": "पहुँच बनाम जनसंख्या वृद्धि"
|
||||
"t_growth": "पहुँच बनाम जनसंख्या वृद्धि",
|
||||
"t_bridgereach": "एक भाषा, कितने जनसमूह"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "भाषा अनुसार सुसमाचार संसाधन",
|
||||
|
|
@ -159,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": "पवित्र शास्त्र (कोई भी)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59% ऑनलाइन",
|
||||
"yes": "60% या अधिक ऑनलाइन"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "दिखाएँ",
|
||||
"map": "मानचित्र",
|
||||
"tree": "भाषा वृक्ष"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "अपनी भाषा में पहुँच, देश के इंटरनेट उपयोग के अनुसार",
|
||||
"aria": "चार्ट",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "समूह",
|
||||
"map": "मानचित्र"
|
||||
},
|
||||
"res": {
|
||||
"aria": "दायरे की भाषाओं के लिए सुसमाचार संसाधन",
|
||||
"count": "दायरे की {all} भाषाओं में से {langs} में {n} संसाधन",
|
||||
"empty": "दायरे की भाषाओं के लिए कुछ नहीं मिला।",
|
||||
"groups": "यहाँ समूह",
|
||||
"note": "जिस भाषा की यहाँ कोई पंक्ति नहीं है, उसके लिए जाँचे गए स्रोतों में कुछ नहीं मिला।",
|
||||
"resource": "संसाधन",
|
||||
"scope": "दायरा",
|
||||
"source": "स्रोत",
|
||||
"type": "प्रकार"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "मध्यम",
|
||||
"strong": "मज़बूत"
|
||||
},
|
||||
"table": {
|
||||
"aria": "तालिका",
|
||||
"groups": "जन-समूह",
|
||||
"resources": "सुसमाचार संसाधन"
|
||||
},
|
||||
"title": "इंटरनेट के माध्यम से सुसमाचार तक पहुँच",
|
||||
"tree": {
|
||||
"aria": "भाषा वृक्ष: इन जन-समूहों का हर भाषा परिवार, हर भाषा में जो मिला उसके अनुसार रंगा हुआ",
|
||||
"branch": "शाखा",
|
||||
"dialect": "बोली",
|
||||
"family": "परिवार",
|
||||
"groupsN": "{n} जन-समूह",
|
||||
"inherited": "इसकी भाषा का रंग: कोई भी स्रोत बोली के अनुसार संसाधन सूचीबद्ध नहीं करता।",
|
||||
"language": "भाषा",
|
||||
"languagesHere": "भाषाएँ यहाँ",
|
||||
"legendTitle": "उनकी अपनी भाषा में",
|
||||
"levels": "परिवार › शाखा › भाषा › बोली। परिवार या शाखा अपने भीतर का सबसे खराब रंग दिखाती है। बोली अपनी भाषा का रंग हल्का दिखाती है: संसाधन भाषा के अनुसार सूचीबद्ध होते हैं, बोली के अनुसार नहीं। बोर्ड को उस तक सीमित करने के लिए किसी वृत्त पर क्लिक करें।",
|
||||
"sources": "वृक्ष: Glottolog (CC BY 4.0)। संसाधन: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, यीशु फ़िल्म, Global Recordings और अन्य, ISO 639-3 के अनुसार।",
|
||||
"st": {
|
||||
"g": "नया नियम या पूरी बाइबल मिली",
|
||||
"r": "कुछ नहीं मिला",
|
||||
"y": "कुछ मिला, पर नया नियम नहीं"
|
||||
},
|
||||
"via": "इस पथ पर भी:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "दृश्य",
|
||||
"board": "बोर्ड",
|
||||
|
|
|
|||
|
|
@ -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} नक्शा इंजन लोड नहीं हुआ।",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "देश",
|
||||
"colLanguage": "भाषा",
|
||||
"colPopulation": "जनसंख्या",
|
||||
"colReligion": "धर्म"
|
||||
"colReligion": "धर्म",
|
||||
"colPctChristian": "% ईसाई",
|
||||
"colAdjacency": "निकटता",
|
||||
"colCode": "कोड",
|
||||
"sortMobilization": "संग्रहण",
|
||||
"sortFrontier": "सीमांत पहले",
|
||||
"colName": "नाम"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "समूह बनाएँ",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "पहुँचा पड़ोसी (बारीक, असली निर्देशांक)",
|
||||
"legendHelp": "फिर से समूह बनाने के लिए ऊपर के घेरे पर क्लिक करें · पंखुड़ियाँ खोलने के लिए केंद्र पर क्लिक करें",
|
||||
"clustering": "फ़सल को भाषा के अनुसार समूहित किया जा रहा है…",
|
||||
"signLanguage": "सांकेतिक भाषा"
|
||||
"signLanguage": "सांकेतिक भाषा",
|
||||
"mode": {
|
||||
"family": "परिवार",
|
||||
"language": "भाषा",
|
||||
"dialect": "बोली"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "कम · बड़े",
|
||||
"more": "अधिक",
|
||||
"most": "सर्वाधिक"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "हरे रंग की कोडिंग",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "नक्शा या फूल दृश्य",
|
||||
"loading": "फ़सल का खेत लोड हो रहा है…",
|
||||
"unreachedTarget": "अपहुँचा लक्ष्य"
|
||||
"unreachedTarget": "अपहुँचा लक्ष्य",
|
||||
"map": "मानचित्र",
|
||||
"flower": "फूल",
|
||||
"details": "विवरण",
|
||||
"senders": "भेजने वाले →",
|
||||
"clustersHidden": "क्लस्टर · {n} छिपे",
|
||||
"legendSend": "SEND (ठोस = वही भाषा · डैश = क्लस्टर · बिंदु = देश के भीतर)",
|
||||
"legendSelection": "चयन · त्रिज्या = जनसंख्या का लघुगणक · लूका 10:2",
|
||||
"navySend": "गहरा नीला = SEND",
|
||||
"goldMesh": "सुनहरा = एक-भाषा जाल"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "नक्शे पर एक लाल लक्ष्य चुनें —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "तैयार · पिन: {pins} · किसी पिन पर क्लिक करें",
|
||||
"readyLanguage": "तैयार · पिन: {pins} · परिवार कवरेज {coverage}% · शीर्ष 25 = {top}%",
|
||||
"readyReligion": "पिन: {pins} · सबसे आम: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "दृश्य चुनें",
|
||||
"search": "जनसमूह, स्थान, भाषा, धर्म से खोजें…",
|
||||
"baseMap": "आधार मानचित्र",
|
||||
"dataSources": "डेटा स्रोत",
|
||||
"legend": "सूचकांक",
|
||||
"openLegend": "सूचकांक खोलें",
|
||||
"clearSelectionEsc": "चयन हटाएँ (Esc)",
|
||||
"closeEsc": "बंद करें (Esc)",
|
||||
"close": "बंद करें",
|
||||
"retry": "फिर से कोशिश करें",
|
||||
"loadingMap": "मानचित्र लोड हो रहा है",
|
||||
"flyToPin": "पिन पर जाएँ",
|
||||
"primaryReligion": "प्रमुख धर्म",
|
||||
"ropHierarchy": "ROP पदानुक्रम"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "धर्मों का वितरण · विवरण के लिए किसी पिन पर क्लिक करें",
|
||||
"ropTree": "ROP पदानुक्रम · सम्बद्धता खंड → क्लस्टर → जनसमूह → PGIC",
|
||||
"languageFamilies": "नृजातीय-भाषाई परिवार (PPLR) · विवरण के लिए किसी पिन पर क्लिक करें",
|
||||
"sunburst": "ROP पदानुक्रम · सम्बद्धता खंड → जनसमूह क्लस्टर · ज़ूम के लिए स्क्रॉल करें",
|
||||
"pins": "देश-वार सभी जनसमूह पिन · प्रणालियों की पहचान-संख्याओं के लिए क्लिक करें",
|
||||
"coverage": "अनुवाद कवरेज · रंग = प्रति देश भरी गई भाषाओं का औसत · किसी पिन या देश पर क्लिक करें",
|
||||
"rosetta": "पहचान-संख्या रोसेटा · रंग = कौन-सी पहचान-संख्याएँ हैं · ★ = नमूना पंक्ति"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "भाषाओं का औसत / {n}",
|
||||
"idCoverage": "पहचान-संख्या कवरेज"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, तीनों",
|
||||
"imbOnly": "केवल IMB",
|
||||
"jpOnly": "केवल JP",
|
||||
"neither": "दोनों में से कोई नहीं"
|
||||
},
|
||||
"samplePg": "★ नमूना जनसमूह",
|
||||
"missingIds": "PPLR में अभी मिलान नहीं हुआ: {ids}। खाली पंक्ति डेटा की कमी है, त्रुटि नहीं।"
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(कोई नाम नहीं)",
|
||||
"country": "देश",
|
||||
"religion": "धर्म",
|
||||
"language": "भाषा",
|
||||
"family": "परिवार",
|
||||
"affinityBloc": "सम्बद्धता खंड",
|
||||
"cluster": "क्लस्टर",
|
||||
"peopleCluster": "जनसमूह क्लस्टर",
|
||||
"peopleGroup": "जनसमूह",
|
||||
"pgCount": "जनसमूह",
|
||||
"avgLocalesFilled": "भरी गई भाषाओं का औसत",
|
||||
"iso3Country": "ISO-3 देश"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "{n} भाषाओं में नाम"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "Il divario sordo",
|
||||
"t_resources": "Risorse per popolo",
|
||||
"t_languages": "Lingue per paese",
|
||||
"t_growth": "Accesso vs crescita"
|
||||
"t_growth": "Accesso vs crescita",
|
||||
"t_bridgereach": "Una lingua, quanti popoli"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Risorse del Vangelo per lingua",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % online",
|
||||
"yes": "60 % o più online"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Mostra",
|
||||
"map": "Mappa",
|
||||
"tree": "Albero delle lingue"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Accesso nella propria lingua, per uso di internet del paese",
|
||||
"aria": "Grafici",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Gruppi",
|
||||
"map": "Mappa"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Risorse del vangelo per le lingue selezionate",
|
||||
"count": "{n} risorse in {langs} delle {all} lingue selezionate",
|
||||
"empty": "Niente trovato per le lingue selezionate.",
|
||||
"groups": "Gruppi qui",
|
||||
"note": "Una lingua senza riga qui non ha niente nelle fonti consultate.",
|
||||
"resource": "Risorsa",
|
||||
"scope": "Ambito",
|
||||
"source": "Fonte",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moderata",
|
||||
"strong": "forte"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tabella",
|
||||
"groups": "Gruppi etnici",
|
||||
"resources": "Risorse del vangelo"
|
||||
},
|
||||
"title": "L’accesso al vangelo tramite internet",
|
||||
"tree": {
|
||||
"aria": "Albero delle lingue: ogni famiglia linguistica di questi gruppi etnici, colorata secondo ciò che esiste in ogni lingua",
|
||||
"branch": "Ramo",
|
||||
"dialect": "Dialetto",
|
||||
"family": "Famiglia",
|
||||
"groupsN": "{n} gruppi etnici",
|
||||
"inherited": "Colore della sua lingua: nessuna fonte cataloga le risorse per dialetto.",
|
||||
"language": "Lingua",
|
||||
"languagesHere": "lingue qui",
|
||||
"legendTitle": "Nella propria lingua",
|
||||
"levels": "Famiglia › ramo › lingua › dialetto. Una famiglia o un ramo mostra il colore peggiore al suo interno. Un dialetto mostra il colore della sua lingua, più chiaro: le risorse sono catalogate per lingua, non per dialetto. Clicca un cerchio per restringere il pannello.",
|
||||
"sources": "Albero: Glottolog (CC BY 4.0). Risorse: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, film JESUS, Global Recordings e altre, per ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Nuovo Testamento o Bibbia completa trovati",
|
||||
"r": "Niente trovato",
|
||||
"y": "Qualcosa trovato, ma nessun Nuovo Testamento"
|
||||
},
|
||||
"via": "Anche su questo percorso:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Vista",
|
||||
"board": "Pannello",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Paese",
|
||||
"colLanguage": "Lingua",
|
||||
"colPopulation": "Popolazione",
|
||||
"colReligion": "Religione"
|
||||
"colReligion": "Religione",
|
||||
"colPctChristian": "% cristiani",
|
||||
"colAdjacency": "Adiacenza",
|
||||
"colCode": "Codice",
|
||||
"sortMobilization": "Mobilitazione",
|
||||
"sortFrontier": "Prima le frontiere",
|
||||
"colName": "Nome"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Raggruppa per",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Vicino raggiunto (dettagliato, coordinate reali)",
|
||||
"legendHelp": "clicca un cerchio in alto per raggruppare · clicca un nucleo per aprire i petali",
|
||||
"clustering": "Raggruppamento del raccolto per lingua…",
|
||||
"signLanguage": "Lingua dei segni"
|
||||
"signLanguage": "Lingua dei segni",
|
||||
"mode": {
|
||||
"family": "Famiglia",
|
||||
"language": "Lingua",
|
||||
"dialect": "Dialetto"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "meno · più grandi",
|
||||
"more": "di più",
|
||||
"most": "il massimo"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Codifica del colore verde",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Vista mappa o fiore",
|
||||
"loading": "Caricamento del campo della mietitura…",
|
||||
"unreachedTarget": "obiettivo non raggiunto"
|
||||
"unreachedTarget": "obiettivo non raggiunto",
|
||||
"map": "Mappa",
|
||||
"flower": "Fiore",
|
||||
"details": "Dettagli",
|
||||
"senders": "mittenti →",
|
||||
"clustersHidden": "cluster · {n} nascosti",
|
||||
"legendSend": "SEND (continua = stessa lingua · tratteggiata = cluster · punteggiata = nel paese)",
|
||||
"legendSelection": "selezione · raggio = log della popolazione · Luca 10,2",
|
||||
"navySend": "blu scuro = SEND",
|
||||
"goldMesh": "oro = rete della stessa lingua"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Scegli un obiettivo rosso sulla mappa —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Pronto · segnaposto: {pins} · clicca un segnaposto",
|
||||
"readyLanguage": "Pronto · segnaposto: {pins} · copertura famiglie {coverage}% · top 25 = {top}%",
|
||||
"readyReligion": "Segnaposto: {pins} · più comune: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Scegli una vista",
|
||||
"search": "Cerca per popolo, luogo, lingua, religione…",
|
||||
"baseMap": "Mappa di base",
|
||||
"dataSources": "Fonti dei dati",
|
||||
"legend": "Legenda",
|
||||
"openLegend": "Apri la legenda",
|
||||
"clearSelectionEsc": "Cancella la selezione (Esc)",
|
||||
"closeEsc": "Chiudi (Esc)",
|
||||
"close": "Chiudi",
|
||||
"retry": "Riprova",
|
||||
"loadingMap": "Caricamento della mappa",
|
||||
"flyToPin": "Vai al segnaposto",
|
||||
"primaryReligion": "Religione principale",
|
||||
"ropHierarchy": "Gerarchia ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Distribuzione delle religioni · clicca un segnaposto per i dettagli",
|
||||
"ropTree": "Gerarchia ROP · blocco di affinità → cluster → popolo → PGIC",
|
||||
"languageFamilies": "Famiglie etnolinguistiche (PPLR) · clicca un segnaposto per i dettagli",
|
||||
"sunburst": "Gerarchia ROP · blocco di affinità → cluster di popoli · scorri per ingrandire",
|
||||
"pins": "Tutti i segnaposto popolo-paese · clicca per gli identificativi dei sistemi",
|
||||
"coverage": "Copertura delle traduzioni · colore = media delle lingue compilate per paese · clicca un segnaposto o un paese",
|
||||
"rosetta": "Rosetta degli identificativi · colore = identificativi presenti · ★ = riga di esempio"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Media delle lingue / {n}",
|
||||
"idCoverage": "Copertura degli identificativi"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, tutti e tre",
|
||||
"imbOnly": "Solo IMB",
|
||||
"jpOnly": "Solo JP",
|
||||
"neither": "nessuno dei due"
|
||||
},
|
||||
"samplePg": "★ popolo di esempio",
|
||||
"missingIds": "Non ancora collegati in PPLR: {ids}. Una riga vuota è una lacuna nei dati, non un errore."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(senza nome)",
|
||||
"country": "Paese",
|
||||
"religion": "Religione",
|
||||
"language": "Lingua",
|
||||
"family": "Famiglia",
|
||||
"affinityBloc": "Blocco di affinità",
|
||||
"cluster": "Cluster",
|
||||
"peopleCluster": "Cluster di popoli",
|
||||
"peopleGroup": "Popolo",
|
||||
"pgCount": "Popoli",
|
||||
"avgLocalesFilled": "Media delle lingue compilate",
|
||||
"iso3Country": "Paese ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Nome in {n} lingue"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "A lacuna surda",
|
||||
"t_resources": "Recursos por povo",
|
||||
"t_languages": "Idiomas por país",
|
||||
"t_growth": "Acesso vs crescimento"
|
||||
"t_growth": "Acesso vs crescimento",
|
||||
"t_bridgereach": "Uma língua, quantos povos"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Recursos do evangelho por idioma",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % online",
|
||||
"yes": "60 % ou mais online"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Mostrar",
|
||||
"map": "Mapa",
|
||||
"tree": "Árvore de línguas"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Acesso na própria língua, por uso de internet do país",
|
||||
"aria": "Gráficos",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Grupos",
|
||||
"map": "Mapa"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Recursos do evangelho para as línguas no recorte",
|
||||
"count": "{n} recursos em {langs} de {all} línguas no recorte",
|
||||
"empty": "Nada encontrado para as línguas no recorte.",
|
||||
"groups": "Grupos aqui",
|
||||
"note": "Uma língua sem linha aqui não tem nada nas fontes verificadas.",
|
||||
"resource": "Recurso",
|
||||
"scope": "Abrangência",
|
||||
"source": "Fonte",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moderada",
|
||||
"strong": "forte"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tabela",
|
||||
"groups": "Povos",
|
||||
"resources": "Recursos do evangelho"
|
||||
},
|
||||
"title": "Acesso ao evangelho pela internet",
|
||||
"tree": {
|
||||
"aria": "Árvore de línguas: cada família linguística destes povos, colorida pelo que existe em cada língua",
|
||||
"branch": "Ramo",
|
||||
"dialect": "Dialeto",
|
||||
"family": "Família",
|
||||
"groupsN": "{n} povos",
|
||||
"inherited": "Cor da sua língua: nenhuma fonte cataloga recursos por dialeto.",
|
||||
"language": "Língua",
|
||||
"languagesHere": "línguas aqui",
|
||||
"legendTitle": "Na própria língua",
|
||||
"levels": "Família › ramo › língua › dialeto. Uma família ou ramo mostra a pior cor que contém. Um dialeto mostra a cor da sua língua, mais clara: os recursos são catalogados por língua, não por dialeto. Clique num círculo para restringir o painel a ele.",
|
||||
"sources": "Árvore: Glottolog (CC BY 4.0). Recursos: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, filme JESUS, Global Recordings e outros, por ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Novo Testamento ou Bíblia completa encontrados",
|
||||
"r": "Nada encontrado",
|
||||
"y": "Algo encontrado, mas sem Novo Testamento"
|
||||
},
|
||||
"via": "Também neste caminho:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Visão",
|
||||
"board": "Painel",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "País",
|
||||
"colLanguage": "Idioma",
|
||||
"colPopulation": "População",
|
||||
"colReligion": "Religião"
|
||||
"colReligion": "Religião",
|
||||
"colPctChristian": "% cristãos",
|
||||
"colAdjacency": "Adjacência",
|
||||
"colCode": "Código",
|
||||
"sortMobilization": "Mobilização",
|
||||
"sortFrontier": "Fronteira primeiro",
|
||||
"colName": "Nome"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Agrupar por",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Vizinho alcançado (detalhado, coordenadas reais)",
|
||||
"legendHelp": "clique num círculo superior para reagrupar · clique num núcleo para abrir as pétalas",
|
||||
"clustering": "A agrupar a colheita por idioma…",
|
||||
"signLanguage": "Língua de sinais"
|
||||
"signLanguage": "Língua de sinais",
|
||||
"mode": {
|
||||
"family": "Família",
|
||||
"language": "Idioma",
|
||||
"dialect": "Dialeto"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "menos · maiores",
|
||||
"more": "mais",
|
||||
"most": "o máximo"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Codificação da cor verde",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Visão de mapa ou de flor",
|
||||
"loading": "Carregando o campo da colheita…",
|
||||
"unreachedTarget": "alvo não alcançado"
|
||||
"unreachedTarget": "alvo não alcançado",
|
||||
"map": "Mapa",
|
||||
"flower": "Flor",
|
||||
"details": "Detalhes",
|
||||
"senders": "enviadores →",
|
||||
"clustersHidden": "agrupamentos · {n} ocultos",
|
||||
"legendSend": "SEND (contínua = mesmo idioma · tracejada = agrupamento · pontilhada = no país)",
|
||||
"legendSelection": "seleção · raio = log da população · Lucas 10:2",
|
||||
"navySend": "azul-marinho = SEND",
|
||||
"goldMesh": "dourado = rede do mesmo idioma"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Escolha um alvo vermelho no mapa —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Pronto · marcadores: {pins} · clique num marcador",
|
||||
"readyLanguage": "Pronto · marcadores: {pins} · cobertura de famílias {coverage}% · top 25 = {top}%",
|
||||
"readyReligion": "Marcadores: {pins} · mais comum: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Escolha uma vista",
|
||||
"search": "Pesquisar por povo, lugar, idioma, religião…",
|
||||
"baseMap": "Mapa base",
|
||||
"dataSources": "Fontes de dados",
|
||||
"legend": "Legenda",
|
||||
"openLegend": "Abrir a legenda",
|
||||
"clearSelectionEsc": "Limpar a seleção (Esc)",
|
||||
"closeEsc": "Fechar (Esc)",
|
||||
"close": "Fechar",
|
||||
"retry": "Tentar novamente",
|
||||
"loadingMap": "A carregar o mapa",
|
||||
"flyToPin": "Ir para o marcador",
|
||||
"primaryReligion": "Religião principal",
|
||||
"ropHierarchy": "Hierarquia ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Distribuição das religiões · clique num marcador para ver detalhes",
|
||||
"ropTree": "Hierarquia ROP · bloco de afinidade → agrupamento → povo → PGIC",
|
||||
"languageFamilies": "Famílias etnolinguísticas (PPLR) · clique num marcador para ver detalhes",
|
||||
"sunburst": "Hierarquia ROP · bloco de afinidade → agrupamento de povos · role para ampliar",
|
||||
"pins": "Todos os marcadores povo-país · clique para ver os identificadores dos sistemas",
|
||||
"coverage": "Cobertura de tradução · cor = média de idiomas preenchidos por país · clique num marcador ou num país",
|
||||
"rosetta": "Rosetta de identificadores · cor = identificadores existentes · ★ = linha de exemplo"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Média de idiomas / {n}",
|
||||
"idCoverage": "Cobertura de identificadores"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, os três",
|
||||
"imbOnly": "Apenas IMB",
|
||||
"jpOnly": "Apenas JP",
|
||||
"neither": "nenhum"
|
||||
},
|
||||
"samplePg": "★ povo de exemplo",
|
||||
"missingIds": "Ainda não correspondidos no PPLR: {ids}. Uma linha vazia é uma lacuna nos dados, não um erro."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(sem nome)",
|
||||
"country": "País",
|
||||
"religion": "Religião",
|
||||
"language": "Idioma",
|
||||
"family": "Família",
|
||||
"affinityBloc": "Bloco de afinidade",
|
||||
"cluster": "Agrupamento",
|
||||
"peopleCluster": "Agrupamento de povos",
|
||||
"peopleGroup": "Povo",
|
||||
"pgCount": "Povos",
|
||||
"avgLocalesFilled": "Média de idiomas preenchidos",
|
||||
"iso3Country": "País ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Nome em {n} idiomas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "Golul surd",
|
||||
"t_resources": "Resurse pe popoare",
|
||||
"t_languages": "Limbi pe țări",
|
||||
"t_growth": "Acces vs creștere"
|
||||
"t_growth": "Acces vs creștere",
|
||||
"t_bridgereach": "O limbă, câte popoare"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Resurse ale Evangheliei pe limbi",
|
||||
|
|
@ -159,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)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % online",
|
||||
"yes": "60 % sau mai mult online"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Afișează",
|
||||
"map": "Hartă",
|
||||
"tree": "Arborele limbilor"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Acces în propria limbă, după folosirea internetului în țară",
|
||||
"aria": "Grafice",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Grupuri",
|
||||
"map": "Hartă"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Resurse ale Evangheliei pentru limbile selectate",
|
||||
"count": "{n} resurse în {langs} din {all} limbi selectate",
|
||||
"empty": "Nu s-a găsit nimic pentru limbile selectate.",
|
||||
"groups": "Grupuri aici",
|
||||
"note": "O limbă fără rând aici nu are nimic în sursele verificate.",
|
||||
"resource": "Resursă",
|
||||
"scope": "Domeniu",
|
||||
"source": "Sursă",
|
||||
"type": "Tip"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "moderată",
|
||||
"strong": "puternică"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Tabel",
|
||||
"groups": "Grupuri etnice",
|
||||
"resources": "Resurse ale Evangheliei"
|
||||
},
|
||||
"title": "Accesul la Evanghelie prin internet",
|
||||
"tree": {
|
||||
"aria": "Arborele limbilor: fiecare familie de limbi a acestor grupuri etnice, colorată după ce există în fiecare limbă",
|
||||
"branch": "Ramură",
|
||||
"dialect": "Dialect",
|
||||
"family": "Familie",
|
||||
"groupsN": "{n} grupuri etnice",
|
||||
"inherited": "Culoarea limbii sale: nicio sursă nu catalogează resursele pe dialecte.",
|
||||
"language": "Limbă",
|
||||
"languagesHere": "limbi aici",
|
||||
"legendTitle": "În propria lor limbă",
|
||||
"levels": "Familie › ramură › limbă › dialect. O familie sau o ramură arată cea mai rea culoare din interior. Un dialect arată culoarea limbii sale, mai deschisă: resursele sunt catalogate pe limbă, nu pe dialect. Faceți clic pe un cerc pentru a restrânge panoul la el.",
|
||||
"sources": "Arbore: Glottolog (CC BY 4.0). Resurse: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, filmul JESUS, Global Recordings și altele, după ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Noul Testament sau Biblia întreagă găsite",
|
||||
"r": "Nimic găsit",
|
||||
"y": "Ceva găsit, dar fără Noul Testament"
|
||||
},
|
||||
"via": "Tot pe această cale:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Vedere",
|
||||
"board": "Panou",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Țară",
|
||||
"colLanguage": "Limbă",
|
||||
"colPopulation": "Populație",
|
||||
"colReligion": "Religie"
|
||||
"colReligion": "Religie",
|
||||
"colPctChristian": "% creștini",
|
||||
"colAdjacency": "Vecinătate",
|
||||
"colCode": "Cod",
|
||||
"sortMobilization": "Mobilizare",
|
||||
"sortFrontier": "Frontiera întâi",
|
||||
"colName": "Nume"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Grupați după",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Vecin atins (detaliat, coordonate reale)",
|
||||
"legendHelp": "clic pe un cerc de sus pentru regrupare · clic pe un nucleu pentru a-i deschide petalele",
|
||||
"clustering": "Se grupează recolta după limbă…",
|
||||
"signLanguage": "Limbajul semnelor"
|
||||
"signLanguage": "Limbajul semnelor",
|
||||
"mode": {
|
||||
"family": "Familie",
|
||||
"language": "Limbă",
|
||||
"dialect": "Dialect"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "mai puține · mai mari",
|
||||
"more": "mai multe",
|
||||
"most": "cele mai multe"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Codificarea culorii verzi",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Vedere hartă sau floare",
|
||||
"loading": "Se încarcă câmpul secerișului…",
|
||||
"unreachedTarget": "țintă neatinsă"
|
||||
"unreachedTarget": "țintă neatinsă",
|
||||
"map": "Hartă",
|
||||
"flower": "Floare",
|
||||
"details": "Detalii",
|
||||
"senders": "trimițători →",
|
||||
"clustersHidden": "grupuri · {n} ascunse",
|
||||
"legendSend": "SEND (continuă = aceeași limbă · întreruptă = grup · punctată = în țară)",
|
||||
"legendSelection": "selecție · rază = log al populației · Luca 10:2",
|
||||
"navySend": "bleumarin = SEND",
|
||||
"goldMesh": "auriu = rețea de aceeași limbă"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Alege o țintă roșie pe hartă —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Gata · repere: {pins} · dați clic pe un reper",
|
||||
"readyLanguage": "Gata · repere: {pins} · acoperirea familiilor {coverage}% · top 25 = {top}%",
|
||||
"readyReligion": "Repere: {pins} · cea mai frecventă: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Alegeți o vedere",
|
||||
"search": "Căutați după popor, loc, limbă, religie…",
|
||||
"baseMap": "Hartă de bază",
|
||||
"dataSources": "Surse de date",
|
||||
"legend": "Legendă",
|
||||
"openLegend": "Deschide legenda",
|
||||
"clearSelectionEsc": "Șterge selecția (Esc)",
|
||||
"closeEsc": "Închide (Esc)",
|
||||
"close": "Închide",
|
||||
"retry": "Reîncercați",
|
||||
"loadingMap": "Se încarcă harta",
|
||||
"flyToPin": "Mergi la reper",
|
||||
"primaryReligion": "Religia principală",
|
||||
"ropHierarchy": "Ierarhia ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Distribuția religiilor · dați clic pe un reper pentru detalii",
|
||||
"ropTree": "Ierarhia ROP · bloc de afinitate → grup → popor → PGIC",
|
||||
"languageFamilies": "Familii etnolingvistice (PPLR) · dați clic pe un reper pentru detalii",
|
||||
"sunburst": "Ierarhia ROP · bloc de afinitate → grup de popoare · derulați pentru zoom",
|
||||
"pins": "Toate reperele popor-țară · dați clic pentru identificatorii sistemelor",
|
||||
"coverage": "Acoperirea traducerilor · nuanța = media limbilor completate pe țară · dați clic pe un reper sau pe o țară",
|
||||
"rosetta": "Rosetta identificatorilor · culoare = identificatorii existenți · ★ = rând de exemplu"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Media limbilor / {n}",
|
||||
"idCoverage": "Acoperirea identificatorilor"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, toate trei",
|
||||
"imbOnly": "Doar IMB",
|
||||
"jpOnly": "Doar JP",
|
||||
"neither": "niciunul"
|
||||
},
|
||||
"samplePg": "★ popor de exemplu",
|
||||
"missingIds": "Încă neasociate în PPLR: {ids}. Un rând gol este o lipsă de date, nu o eroare."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(fără nume)",
|
||||
"country": "Țară",
|
||||
"religion": "Religie",
|
||||
"language": "Limbă",
|
||||
"family": "Familie",
|
||||
"affinityBloc": "Bloc de afinitate",
|
||||
"cluster": "Grup",
|
||||
"peopleCluster": "Grup de popoare",
|
||||
"peopleGroup": "Popor",
|
||||
"pgCount": "Popoare",
|
||||
"avgLocalesFilled": "Media limbilor completate",
|
||||
"iso3Country": "Țară ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Numele în {n} limbi"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "Молящиеся",
|
||||
"selected_first": "Выбранные сначала",
|
||||
"selected_first_hint": "Поднять выбранные народы наверх, сохранив сортировку столбцов",
|
||||
"n_shown": "{n} показано"
|
||||
"n_shown": "{n} показано",
|
||||
"other": "Другое"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "Пробел у глухих",
|
||||
"t_resources": "Ресурсы по народам",
|
||||
"t_languages": "Языки по странам",
|
||||
"t_growth": "Доступ и рост населения"
|
||||
"t_growth": "Доступ и рост населения",
|
||||
"t_bridgereach": "Один язык — сколько народов"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Ресурсы Евангелия по языкам",
|
||||
|
|
@ -159,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": "Писание (любое)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "25–59 % в сети",
|
||||
"yes": "60 % и более в сети"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "Показать",
|
||||
"map": "Карта",
|
||||
"tree": "Дерево языков"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "Доступ на родном языке по уровню интернета в стране",
|
||||
"aria": "Диаграммы",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "Группы",
|
||||
"map": "Карта"
|
||||
},
|
||||
"res": {
|
||||
"aria": "Ресурсы Евангелия для языков в выборке",
|
||||
"count": "Ресурсов: {n} на {langs} из {all} языков в выборке",
|
||||
"empty": "Для языков в выборке ничего не найдено.",
|
||||
"groups": "Групп здесь",
|
||||
"note": "Язык без строки здесь — в проверенных источниках ничего нет.",
|
||||
"resource": "Ресурс",
|
||||
"scope": "Объём",
|
||||
"source": "Источник",
|
||||
"type": "Тип"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "средний",
|
||||
"strong": "сильный"
|
||||
},
|
||||
"table": {
|
||||
"aria": "Таблица",
|
||||
"groups": "Народы",
|
||||
"resources": "Ресурсы Евангелия"
|
||||
},
|
||||
"title": "Доступ к Евангелию через интернет",
|
||||
"tree": {
|
||||
"aria": "Дерево языков: каждая языковая семья этих народов, окрашенная по тому, что есть на каждом языке",
|
||||
"branch": "Ветвь",
|
||||
"dialect": "Диалект",
|
||||
"family": "Семья",
|
||||
"groupsN": "Народов: {n}",
|
||||
"inherited": "Цвет его языка: ни один источник не учитывает ресурсы по диалектам.",
|
||||
"language": "Язык",
|
||||
"languagesHere": "языков здесь",
|
||||
"legendTitle": "На родном языке",
|
||||
"levels": "Семья › ветвь › язык › диалект. Семья или ветвь показывает худший цвет внутри. Диалект показывает цвет своего языка, светлее: ресурсы учитываются по языкам, а не по диалектам. Нажмите на круг, чтобы сузить панель до него.",
|
||||
"sources": "Дерево: Glottolog (CC BY 4.0). Ресурсы: eBible, Faith Comes By Hearing, Scripture Earth, PNG.Bible, фильм «Иисус», Global Recordings и другие, по ISO 639-3.",
|
||||
"st": {
|
||||
"g": "Найден Новый Завет или вся Библия",
|
||||
"r": "Ничего не найдено",
|
||||
"y": "Что-то найдено, но Нового Завета нет"
|
||||
},
|
||||
"via": "Также на этом пути:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "Вид",
|
||||
"board": "Панель",
|
||||
|
|
|
|||
|
|
@ -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} не загрузился.",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "Страна",
|
||||
"colLanguage": "Язык",
|
||||
"colPopulation": "Численность",
|
||||
"colReligion": "Религия"
|
||||
"colReligion": "Религия",
|
||||
"colPctChristian": "% христиан",
|
||||
"colAdjacency": "Соседство",
|
||||
"colCode": "Код",
|
||||
"sortMobilization": "Мобилизация",
|
||||
"sortFrontier": "Сначала рубежи",
|
||||
"colName": "Название"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "Группировать по",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "Охваченный сосед (детально, реальные координаты)",
|
||||
"legendHelp": "нажмите верхний круг, чтобы перегруппировать · нажмите ядро, чтобы раскрыть лепестки",
|
||||
"clustering": "Группировка жатвы по языку…",
|
||||
"signLanguage": "Жестовый язык"
|
||||
"signLanguage": "Жестовый язык",
|
||||
"mode": {
|
||||
"family": "Семья",
|
||||
"language": "Язык",
|
||||
"dialect": "Диалект"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "меньше · крупнее",
|
||||
"more": "больше",
|
||||
"most": "больше всего"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "Кодирование зелёного цвета",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "Вид карты или цветка",
|
||||
"loading": "Загрузка поля жатвы…",
|
||||
"unreachedTarget": "недостигнутая цель"
|
||||
"unreachedTarget": "недостигнутая цель",
|
||||
"map": "Карта",
|
||||
"flower": "Цветок",
|
||||
"details": "Подробности",
|
||||
"senders": "отправителей →",
|
||||
"clustersHidden": "кластеров · скрыто: {n}",
|
||||
"legendSend": "SEND (сплошная = тот же язык · пунктир = кластер · точки = внутри страны)",
|
||||
"legendSelection": "выбор · радиус = логарифм населения · Луки 10:2",
|
||||
"navySend": "тёмно-синий = SEND",
|
||||
"goldMesh": "золотой = сеть одного языка"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "Выберите красную цель на карте —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "Готово · меток: {pins} · нажмите на метку",
|
||||
"readyLanguage": "Готово · меток: {pins} · покрытие семей {coverage}% · топ-25 = {top}%",
|
||||
"readyReligion": "Меток: {pins} · чаще всего: {religion} ({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "Выберите вид",
|
||||
"search": "Поиск по народу, месту, языку, религии…",
|
||||
"baseMap": "Базовая карта",
|
||||
"dataSources": "Источники данных",
|
||||
"legend": "Легенда",
|
||||
"openLegend": "Открыть легенду",
|
||||
"clearSelectionEsc": "Снять выделение (Esc)",
|
||||
"closeEsc": "Закрыть (Esc)",
|
||||
"close": "Закрыть",
|
||||
"retry": "Повторить",
|
||||
"loadingMap": "Загрузка карты",
|
||||
"flyToPin": "Перейти к метке",
|
||||
"primaryReligion": "Основная религия",
|
||||
"ropHierarchy": "Иерархия ROP"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "Распределение религий · нажмите на метку, чтобы узнать подробности",
|
||||
"ropTree": "Иерархия ROP · блок родства → кластер → народ → PGIC",
|
||||
"languageFamilies": "Этнолингвистические семьи (PPLR) · нажмите на метку, чтобы узнать подробности",
|
||||
"sunburst": "Иерархия ROP · блок родства → кластер народов · прокрутите для масштаба",
|
||||
"pins": "Все метки «народ в стране» · нажмите, чтобы увидеть идентификаторы систем",
|
||||
"coverage": "Покрытие переводов · заливка = среднее число заполненных языков по стране · нажмите на метку или страну",
|
||||
"rosetta": "Розетта идентификаторов · цвет = какие идентификаторы есть · ★ = пример"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "Среднее число языков / {n}",
|
||||
"idCoverage": "Покрытие идентификаторов"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD, все три",
|
||||
"imbOnly": "Только IMB",
|
||||
"jpOnly": "Только JP",
|
||||
"neither": "ни того, ни другого"
|
||||
},
|
||||
"samplePg": "★ пример народа",
|
||||
"missingIds": "Ещё не сопоставлено в PPLR: {ids}. Пустая строка — пробел в данных, а не ошибка."
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(без названия)",
|
||||
"country": "Страна",
|
||||
"religion": "Религия",
|
||||
"language": "Язык",
|
||||
"family": "Семья",
|
||||
"affinityBloc": "Блок родства",
|
||||
"cluster": "Кластер",
|
||||
"peopleCluster": "Кластер народов",
|
||||
"peopleGroup": "Народ",
|
||||
"pgCount": "Народы",
|
||||
"avgLocalesFilled": "Среднее число заполненных языков",
|
||||
"iso3Country": "Страна ISO-3"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "Название на {n} языках"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
"people_praying": "祷告的人数",
|
||||
"selected_first": "所选优先",
|
||||
"selected_first_hint": "将所选民族置顶,同时保留您的列排序",
|
||||
"n_shown": "显示 {n} 个"
|
||||
"n_shown": "显示 {n} 个",
|
||||
"other": "其他"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "聋人缺口",
|
||||
"t_resources": "按族群列出资源",
|
||||
"t_languages": "各国语言",
|
||||
"t_growth": "可及性与人口增长"
|
||||
"t_growth": "可及性与人口增长",
|
||||
"t_bridgereach": "一种语言,覆盖多少民族"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "按语言列出的福音资源",
|
||||
|
|
@ -159,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": "圣经(任意)",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"partial": "上网率 25–59%",
|
||||
"yes": "上网率 60% 或以上"
|
||||
},
|
||||
"centre": {
|
||||
"aria": "显示",
|
||||
"map": "地图",
|
||||
"tree": "语言树"
|
||||
},
|
||||
"charts": {
|
||||
"accessByBand": "母语资源情况,按国家互联网使用率",
|
||||
"aria": "图表",
|
||||
|
|
@ -149,11 +154,46 @@
|
|||
"grid": "族群",
|
||||
"map": "地图"
|
||||
},
|
||||
"res": {
|
||||
"aria": "当前范围内各语言的福音资源",
|
||||
"count": "当前范围 {all} 种语言中,有 {langs} 种共 {n} 项资源",
|
||||
"empty": "当前范围内的语言未找到任何资源。",
|
||||
"groups": "此处族群",
|
||||
"note": "此处没有行的语言,在已查阅的来源中一无所有。",
|
||||
"resource": "资源",
|
||||
"scope": "范围",
|
||||
"source": "来源",
|
||||
"type": "类型"
|
||||
},
|
||||
"strength": {
|
||||
"moderate": "中等",
|
||||
"strong": "强"
|
||||
},
|
||||
"table": {
|
||||
"aria": "表格",
|
||||
"groups": "族群",
|
||||
"resources": "福音资源"
|
||||
},
|
||||
"title": "通过互联网接触福音",
|
||||
"tree": {
|
||||
"aria": "语言树:这些族群的每个语系,按各语言中已找到的资源着色",
|
||||
"branch": "分支",
|
||||
"dialect": "方言",
|
||||
"family": "语系",
|
||||
"groupsN": "{n} 个族群",
|
||||
"inherited": "其语言的颜色:没有任何来源按方言编目资源。",
|
||||
"language": "语言",
|
||||
"languagesHere": "种语言",
|
||||
"legendTitle": "以他们的母语",
|
||||
"levels": "语系 › 分支 › 语言 › 方言。语系或分支显示其内部最差的颜色。方言显示其语言的颜色(较浅):资源按语言编目,而不是按方言。点击圆圈即可将看板缩小到该范围。",
|
||||
"sources": "语言树:Glottolog(CC BY 4.0)。资源:eBible、Faith Comes By Hearing、Scripture Earth、PNG.Bible、耶稣传影片、Global Recordings 等,按 ISO 639-3 编目。",
|
||||
"st": {
|
||||
"g": "已找到新约或整本圣经",
|
||||
"r": "未找到",
|
||||
"y": "找到一些资源,但没有新约"
|
||||
},
|
||||
"via": "此路径上还有:"
|
||||
},
|
||||
"view": {
|
||||
"aria": "视图",
|
||||
"board": "看板",
|
||||
|
|
|
|||
|
|
@ -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} 地图引擎未能加载。",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@
|
|||
"colCountry": "国家",
|
||||
"colLanguage": "语言",
|
||||
"colPopulation": "人口",
|
||||
"colReligion": "宗教"
|
||||
"colReligion": "宗教",
|
||||
"colPctChristian": "基督徒 %",
|
||||
"colAdjacency": "邻近关系",
|
||||
"colCode": "代码",
|
||||
"sortMobilization": "动员",
|
||||
"sortFrontier": "前沿优先",
|
||||
"colName": "名称"
|
||||
},
|
||||
"fc": {
|
||||
"clusterBy": "分组依据",
|
||||
|
|
@ -124,7 +130,17 @@
|
|||
"legendPetal": "已得邻居(细分,真实坐标)",
|
||||
"legendHelp": "点击上方圆圈重新分组 · 点击核心展开花瓣",
|
||||
"clustering": "正在按语言整理庄稼…",
|
||||
"signLanguage": "手语"
|
||||
"signLanguage": "手语",
|
||||
"mode": {
|
||||
"family": "语系",
|
||||
"language": "语言",
|
||||
"dialect": "方言"
|
||||
},
|
||||
"size": {
|
||||
"fewerBigger": "更少 · 更大",
|
||||
"more": "更多",
|
||||
"most": "最多"
|
||||
}
|
||||
},
|
||||
"mc": {
|
||||
"greenEncoding": "绿色编码",
|
||||
|
|
@ -138,7 +154,16 @@
|
|||
"cockpit": {
|
||||
"viewSwitch": "地图或花朵视图",
|
||||
"loading": "正在加载庄稼之田…",
|
||||
"unreachedTarget": "未得目标"
|
||||
"unreachedTarget": "未得目标",
|
||||
"map": "地图",
|
||||
"flower": "花朵",
|
||||
"details": "详情",
|
||||
"senders": "派遣方 →",
|
||||
"clustersHidden": "个民族群 · 隐藏 {n} 个",
|
||||
"legendSend": "SEND(实线 = 同语言 · 虚线 = 民族群 · 点线 = 国内)",
|
||||
"legendSelection": "选中 · 半径 = 人口对数 · 路加福音 10:2",
|
||||
"navySend": "深蓝 = SEND",
|
||||
"goldMesh": "金色 = 同语言网络"
|
||||
},
|
||||
"flower": {
|
||||
"pickTarget": "在地图上选择一个红色目标 —",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,61 @@
|
|||
"readyPins": "就绪 · 族群标记:{pins} · 点击任一标记",
|
||||
"readyLanguage": "就绪 · 标记:{pins} · 语系覆盖率 {coverage}% · 前 25 = {top}%",
|
||||
"readyReligion": "标记:{pins} · 最常见:{religion}({count})"
|
||||
},
|
||||
"ui": {
|
||||
"chooseView": "选择视图",
|
||||
"search": "按族群、地点、语言、宗教搜索…",
|
||||
"baseMap": "底图",
|
||||
"dataSources": "数据来源",
|
||||
"legend": "图例",
|
||||
"openLegend": "打开图例",
|
||||
"clearSelectionEsc": "清除选择 (Esc)",
|
||||
"closeEsc": "关闭 (Esc)",
|
||||
"close": "关闭",
|
||||
"retry": "重试",
|
||||
"loadingMap": "正在加载地图",
|
||||
"flyToPin": "飞到标记",
|
||||
"primaryReligion": "主要宗教",
|
||||
"ropHierarchy": "ROP 层级"
|
||||
},
|
||||
"pill": {
|
||||
"religion": "宗教分布 · 点击标记查看详情",
|
||||
"ropTree": "ROP 层级 · 亲缘区块 → 民族群 → 族群 → PGIC",
|
||||
"languageFamilies": "民族语言语系 (PPLR) · 点击标记查看详情",
|
||||
"sunburst": "ROP 层级 · 亲缘区块 → 民族群 · 滚动缩放",
|
||||
"pins": "全部国家内族群标记 · 点击查看跨系统 ID",
|
||||
"coverage": "翻译覆盖率 · 着色 = 各国平均已填语种数 · 点击标记或国家",
|
||||
"rosetta": "跨系统 ID 对照 · 颜色 = 已有哪些 ID · ★ = 示例行"
|
||||
},
|
||||
"legendTitle": {
|
||||
"avgLocales": "平均语种数 / {n}",
|
||||
"idCoverage": "ID 覆盖"
|
||||
},
|
||||
"rosetta": {
|
||||
"legend": {
|
||||
"all": "JP + IMB + WCD 三者俱全",
|
||||
"imbOnly": "仅 IMB",
|
||||
"jpOnly": "仅 JP",
|
||||
"neither": "两者皆无"
|
||||
},
|
||||
"samplePg": "★ 示例族群",
|
||||
"missingIds": "PPLR 尚未对应:{ids}。空行是数据缺口,不是错误。"
|
||||
},
|
||||
"popup": {
|
||||
"noName": "(无名称)",
|
||||
"country": "国家",
|
||||
"religion": "宗教",
|
||||
"language": "语言",
|
||||
"family": "语系",
|
||||
"affinityBloc": "亲缘区块",
|
||||
"cluster": "民族群",
|
||||
"peopleCluster": "民族群",
|
||||
"peopleGroup": "族群",
|
||||
"pgCount": "族群数",
|
||||
"avgLocalesFilled": "平均已填语种数",
|
||||
"iso3Country": "ISO-3 国家"
|
||||
},
|
||||
"card": {
|
||||
"nameInLocales": "{n} 种语言中的名称"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
library/scripts/boundaries/build-web-boundaries.mjs
Normal file
64
library/scripts/boundaries/build-web-boundaries.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-web-boundaries.mjs — the INTERACTIVE-map copies of the boundary files: small enough to parse
|
||||
* fast, with every feature, every island and every property kept.
|
||||
*
|
||||
* node library/scripts/boundaries/build-web-boundaries.mjs
|
||||
*
|
||||
* WHY. rega measured ~3.6 MB of boundary GeoJSON (world-adm0 1.9 MB, gb-IND-ADM1 1.6 MB) costing
|
||||
* 5–6 s of MapLibre worker parsing before the first territory draws, on every page of every
|
||||
* partner site. A web map drawn at zoom 1–7 needs a fraction of those vertices. The FULL files stay
|
||||
* as they are: posters and print use them, and so does every build-time point-in-polygon.
|
||||
*
|
||||
* HOW (mapshaper, pinned below; fetched with npx, it is not a repo dependency):
|
||||
* -explode split each multi-part feature into its parts, so every island is its own shape
|
||||
* -simplify visvalingam weighted percentage=P keep-shapes
|
||||
* topology-preserving (shared borders stay shared); keep-shapes stops any shape
|
||||
* from vanishing, which, after the explode, means no island is ever dropped
|
||||
* (without the explode, keep-shapes guards only whole features: 572 parts -> 240)
|
||||
* -dissolve KEY copy-fields=... put each feature back together, same properties
|
||||
* precision=0.0001 ~11 m, far below a pixel at the zooms these files are drawn at
|
||||
*
|
||||
* VALIDATED, the run FAILS otherwise: the same feature count, the same keys, the same polygon-part
|
||||
* count (no dropped island), and the same property names and values on every feature.
|
||||
* The measured report (size, parts, area error, point-in-polygon drift) is in
|
||||
* internal-tracker/03-deployment-cycle/REPORT-WEB-BOUNDARIES.md.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync, statSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const MAPSHAPER = 'mapshaper@0.6.121'
|
||||
const UPG = 'plugin-designs/my-upg-100-list/data'
|
||||
const JOBS = [
|
||||
// src, out, dissolve key, the other properties, simplification percentage
|
||||
{ src: `${UPG}/adm0/world-adm0.geojson`, out: `${UPG}/adm0/world-adm0-web.geojson`, key: 'iso3', copy: ['shapeGroup', 'shapeName'], pct: '8%' },
|
||||
{ src: 'data/boundaries/gb-IND-ADM1-full.geojson', out: 'data/boundaries/gb-IND-ADM1-full-web.geojson', key: 'shapeName', copy: ['iso3', 'shapeGroup', 'shapeISO'], pct: '10%' },
|
||||
// UPG-100's curated partition: `listId` is load-bearing (zone-geometry.js), so it must survive.
|
||||
{ src: `${UPG}/gb-IND-ADM1.geojson`, out: `${UPG}/gb-IND-ADM1-web.geojson`, key: 'shapeName', copy: ['iso3', 'shapeGroup', 'listId'], pct: '10%' },
|
||||
// gb-CHN-ADM1.geojson is 56 KB (12 provinces): already web-sized, no copy made.
|
||||
]
|
||||
|
||||
const fail = (m) => { console.error('build-web-boundaries: FAIL — ' + m); process.exit(1) }
|
||||
const parts = (fc) => fc.features.reduce((t, f) => t + (f.geometry.type === 'Polygon' ? 1 : f.geometry.coordinates.length), 0)
|
||||
|
||||
for (const j of JOBS) {
|
||||
const src = join(ROOT, j.src), out = join(ROOT, j.out)
|
||||
execFileSync('npx', ['--yes', MAPSHAPER, src, '-explode',
|
||||
'-simplify', 'visvalingam', 'weighted', `percentage=${j.pct}`, 'keep-shapes',
|
||||
'-dissolve', j.key, `copy-fields=${j.copy.join(',')}`,
|
||||
'-o', out, 'format=geojson', 'precision=0.0001'], { stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
const a = JSON.parse(readFileSync(src, 'utf8')), b = JSON.parse(readFileSync(out, 'utf8'))
|
||||
if (a.features.length !== b.features.length) fail(`${j.out}: ${b.features.length} features, source has ${a.features.length}`)
|
||||
const byKey = new Map(b.features.map((f) => [f.properties[j.key], f]))
|
||||
for (const f of a.features) {
|
||||
const g = byKey.get(f.properties[j.key])
|
||||
if (!g) fail(`${j.out}: lost ${j.key}=${f.properties[j.key]}`)
|
||||
for (const [k, v] of Object.entries(f.properties)) if (g.properties[k] !== v) fail(`${j.out}: ${f.properties[j.key]}.${k} is ${JSON.stringify(g.properties[k])}, source ${JSON.stringify(v)}`)
|
||||
}
|
||||
if (parts(a) !== parts(b)) fail(`${j.out}: ${parts(b)} polygon parts, source has ${parts(a)} (an island was dropped)`)
|
||||
const kb = (p) => Math.round(statSync(p).size / 1024)
|
||||
console.log(`${j.out}: ${kb(src)} KB -> ${kb(out)} KB · ${b.features.length} features · ${parts(b)} parts · properties identical`)
|
||||
}
|
||||
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 agent (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)`)
|
||||
26
library/scripts/research/app-kind.mjs
Normal file
26
library/scripts/research/app-kind.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// app-kind.mjs — what KIND of communication channel an App Store app is, from its name.
|
||||
//
|
||||
// A NAME-AND-KEYWORD HEURISTIC over a known list, recorded as such in every table that uses it.
|
||||
// It lives in its own file so the fetcher (fetch-top-apps.mjs) and the table builder
|
||||
// (build-tabulator-tables.mjs) classify with the SAME rule: the builder re-derives the kind from
|
||||
// the stored name, so a cached fetch made with an older rule cannot carry an old answer forward.
|
||||
//
|
||||
// ★ A BRAND THAT IS ALSO AN ENGLISH WORD MUST BE ANCHORED. `line` matched as a bare word made
|
||||
// "LINE MAN: Food Delivery & more" (Thailand) and "Malé Taxi Line" (Maldives) each country's TOP
|
||||
// messaging app, ahead of WhatsApp. LINE's own app is "LINE" or "LINE: Calls & Messages", so it
|
||||
// matches only as the whole name or the name before a colon.
|
||||
export const KIND = [
|
||||
[/\b(whatsapp|telegram|signal|viber|imo|zalo|wechat|weixin|kakaotalk|botim|imessage|messenger|skype|hike|jio ?chat|soul|beetalk)\b|^line(?:$|\s*:)/i, 'messaging'],
|
||||
[/\b(tiktok|douyin|kwai|likee|snack ?video|moj|josh|triller|reels)\b/i, 'short-video'],
|
||||
[/\b(facebook|instagram|snapchat|x|twitter|threads|vk|odnoklassniki|weibo|qzone|xiaohongshu|little red|sharechat|koo|mastodon|bluesky)\b/i, 'social'],
|
||||
[/\b(youtube|bigo|nimo|twitch|rumble|dailymotion|vimeo)\b/i, 'video'],
|
||||
[/\b(opera|uc ?browser|chrome|firefox|brave|puffin|phoenix ?browser)\b/i, 'browser'],
|
||||
];
|
||||
|
||||
export function classify(name) {
|
||||
for (const [re, k] of KIND) if (re.test(String(name || '').trim())) return k;
|
||||
return 'other';
|
||||
}
|
||||
|
||||
// The kinds a reachability table keeps (everything else is not a communication channel).
|
||||
export const isComms = (kind) => kind !== 'other' && kind !== 'browser';
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { classify, isComms } from './app-kind.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = join(__dirname, 'out');
|
||||
|
|
@ -553,6 +554,15 @@ if (has('social-platforms-by-country.json')) {
|
|||
}
|
||||
}
|
||||
|
||||
// The headline counts are COUNTED from the rows, never typed: a typed "54" stayed true only until
|
||||
// the classifier changed. WhatsApp is its two apps (Messenger + Business) together.
|
||||
function messagingWhy(rows) {
|
||||
const top = new Map();
|
||||
for (const r of rows) if (r.is_top_messaging) top.set(r.iso3, r.app);
|
||||
const count = (re) => [...top.values()].filter((a) => re.test(a)).length;
|
||||
return `Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp (Messenger or Business) is the top messaging app in ${count(/whatsapp/i)} countries, Telegram in ${count(/telegram/i)} — neither appears in referral share at all.`;
|
||||
}
|
||||
|
||||
// ═══ TABLE 7 — MESSAGING & APP CHANNELS (what referral stats cannot see) ═══
|
||||
// StatCounter's ENTIRE tracked universe is Western referral platforms — measured:
|
||||
// Facebook/Instagram/Twitter/YouTube/Pinterest/reddit/LinkedIn/Tumblr/HN/VK/Fark.
|
||||
|
|
@ -572,7 +582,11 @@ if (has('top-apps-by-country.json')) {
|
|||
for (const c of Object.values(apps.countries)) {
|
||||
const meta = g[c.iso3] || {};
|
||||
// One row per COUNTRY x APP so a list view can sort "where does Telegram rank".
|
||||
for (const a of (c.comms_apps || [])) {
|
||||
// The kind is RE-DERIVED from the name with the shared classifier (app-kind.mjs), and the top
|
||||
// messaging app with it: the cached fetch may carry a kind from an older rule.
|
||||
const comms = (c.comms_apps || []).map((a) => ({ ...a, kind: classify(a.name) })).filter((a) => isComms(a.kind));
|
||||
const topMessaging = comms.slice().sort((x, y) => x.rank - y.rank).find((a) => a.kind === 'messaging')?.name || null;
|
||||
for (const a of comms) {
|
||||
rows.push({
|
||||
country: meta.country || c.iso3,
|
||||
iso3: c.iso3,
|
||||
|
|
@ -580,7 +594,7 @@ if (has('top-apps-by-country.json')) {
|
|||
app: a.name,
|
||||
kind: a.kind,
|
||||
rank: a.rank,
|
||||
is_top_messaging: a.name === c.top_messaging,
|
||||
is_top_messaging: a.name === topMessaging,
|
||||
people_groups: meta.groups || 0,
|
||||
platform_bias: c.platform_bias,
|
||||
classification: c.classification_method,
|
||||
|
|
@ -605,7 +619,7 @@ if (has('top-apps-by-country.json')) {
|
|||
], {
|
||||
title: 'Messaging & App Channels',
|
||||
grain: 'one row per country x communication app',
|
||||
why: 'Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp is the top messaging app in 54 countries, Telegram in 38 — neither appears in referral share at all.',
|
||||
why: messagingWhy(rows),
|
||||
ios_caveat: 'iOS ONLY. Most of these countries are Android-majority and Google publishes no keyless equivalent, so this is one eye open. An app absent here is NOT absent in-country. 50 of 175 countries have no Apple storefront.',
|
||||
ranking_caveat: 'A top-free RANK is most-DOWNLOADED recently, not most-used.',
|
||||
classification_caveat: 'kind is a name-keyword HEURISTIC over a known list, not a verified taxonomy.',
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { classify, isComms } from './app-kind.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = join(__dirname, 'out');
|
||||
|
|
@ -55,19 +56,7 @@ const SOURCE = {
|
|||
why: 'StatCounter tracks only Western referral platforms and cannot see WhatsApp/WeChat/Telegram/TikTok/LINE/Viber/imo/Zalo — the channels that dominate most of this corpus',
|
||||
};
|
||||
|
||||
// Known non-Western + messaging platforms worth surfacing explicitly. Matching is
|
||||
// a HEURISTIC on the app name; recorded as such.
|
||||
const KIND = [
|
||||
[/\b(whatsapp|telegram|signal|viber|imo|line|zalo|wechat|weixin|kakaotalk|botim|imessage|messenger|skype|hike|jio ?chat|soul|beetalk)\b/i, 'messaging'],
|
||||
[/\b(tiktok|douyin|kwai|likee|snack ?video|moj|josh|triller|reels)\b/i, 'short-video'],
|
||||
[/\b(facebook|instagram|snapchat|x|twitter|threads|vk|odnoklassniki|weibo|qzone|xiaohongshu|little red|sharechat|koo|mastodon|bluesky)\b/i, 'social'],
|
||||
[/\b(youtube|bigo|nimo|twitch|rumble|dailymotion|vimeo)\b/i, 'video'],
|
||||
[/\b(opera|uc ?browser|chrome|firefox|brave|puffin|phoenix ?browser)\b/i, 'browser'],
|
||||
];
|
||||
const classify = (name) => {
|
||||
for (const [re, k] of KIND) if (re.test(name)) return k;
|
||||
return 'other';
|
||||
};
|
||||
// The classifier (KIND / classify) is shared with build-tabulator-tables.mjs: see app-kind.mjs.
|
||||
|
||||
const ISO2 = existsSync(join(OUT, 'iso3-to-iso2.json'))
|
||||
? JSON.parse(readFileSync(join(OUT, 'iso3-to-iso2.json'), 'utf8'))
|
||||
|
|
@ -113,7 +102,7 @@ for (let n = 0; n < todo.length; n++) {
|
|||
}
|
||||
} catch (e) { err = String(e.message || e).slice(0, 60); }
|
||||
|
||||
const comms = apps.filter((a) => a.kind !== 'other' && a.kind !== 'browser');
|
||||
const comms = apps.filter((a) => isComms(a.kind));
|
||||
out[iso3] = {
|
||||
iso3, iso2: ISO2[iso3],
|
||||
apps: apps.slice(0, 25),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Messaging & App Channels",
|
||||
"grain": "one row per country x communication app",
|
||||
"why": "Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp is the top messaging app in 54 countries, Telegram in 38 — neither appears in referral share at all.",
|
||||
"why": "Referral stats (StatCounter) structurally cannot see messaging apps. Measured here: WhatsApp (Messenger or Business) is the top messaging app in 73 countries, Telegram in 38 — neither appears in referral share at all.",
|
||||
"ios_caveat": "iOS ONLY. Most of these countries are Android-majority and Google publishes no keyless equivalent, so this is one eye open. An app absent here is NOT absent in-country. 50 of 175 countries have no Apple storefront.",
|
||||
"ranking_caveat": "A top-free RANK is most-DOWNLOADED recently, not most-used.",
|
||||
"classification_caveat": "kind is a name-keyword HEURISTIC over a known list, not a verified taxonomy.",
|
||||
|
|
@ -2830,19 +2830,6 @@
|
|||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Thailand",
|
||||
"iso3": "THA",
|
||||
"region": "Asia",
|
||||
"app": "LINE MAN: Food Delivery & more",
|
||||
"kind": "messaging",
|
||||
"rank": 13,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 8,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Burkina Faso",
|
||||
"iso3": "BFA",
|
||||
|
|
@ -3006,7 +2993,7 @@
|
|||
"app": "WhatsApp Messenger",
|
||||
"kind": "messaging",
|
||||
"rank": 37,
|
||||
"is_top_messaging": false,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 8,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
|
|
@ -7809,19 +7796,6 @@
|
|||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Maldives",
|
||||
"iso3": "MDV",
|
||||
"region": "Asia",
|
||||
"app": "Malé Taxi Line",
|
||||
"kind": "messaging",
|
||||
"rank": 1,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 1,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
"source": "Apple App Store RSS — top free apps by country"
|
||||
},
|
||||
{
|
||||
"country": "Northern Mariana Islands",
|
||||
"iso3": "MNP",
|
||||
|
|
@ -13120,7 +13094,7 @@
|
|||
"app": "WhatsApp Messenger",
|
||||
"kind": "messaging",
|
||||
"rank": 33,
|
||||
"is_top_messaging": false,
|
||||
"is_top_messaging": true,
|
||||
"people_groups": 1,
|
||||
"platform_bias": "iOS only — most of these countries are Android-majority; absence here is not absence in-country",
|
||||
"classification": "name-keyword HEURISTIC, not a verified taxonomy",
|
||||
|
|
|
|||
|
|
@ -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 agent, 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
15
package.json
15
package.json
|
|
@ -161,6 +161,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",
|
||||
|
|
@ -194,7 +195,10 @@
|
|||
"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",
|
||||
"check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs",
|
||||
|
|
@ -232,8 +236,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",
|
||||
"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",
|
||||
|
|
@ -336,7 +341,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 the agent); 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 agent'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.",
|
||||
|
|
@ -375,6 +380,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.",
|
||||
|
|
@ -395,7 +401,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 agent'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.",
|
||||
|
|
@ -419,6 +427,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.",
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ async function load() {
|
|||
// review caught it as "India seems to have no internet coverage". The fix was a
|
||||
// second file, world-adm0-plus.geojson; that file is gone and India is fetched
|
||||
// at ADM0 like every other country (data/source-boundaries.mjs).
|
||||
fetch(`${assetsBase.value}/boundaries/world-adm0.geojson`).then((r) => r.json()),
|
||||
fetch(`${assetsBase.value}/boundaries/world-adm0-web.geojson`).then((r) => r.json()),
|
||||
])
|
||||
rows.value = data.countries || []
|
||||
return adm0
|
||||
|
|
@ -304,7 +304,7 @@ async function loadAdm1() {
|
|||
fetch(`${assetsBase.value}/tables/internet-by-adm1.json`).then((r) => { if (!r.ok) throw new Error('internet-by-adm1 ' + r.status); return r.json() }),
|
||||
// All 36 states (data/boundaries/gb-IND-ADM1-full.geojson). NOT gb-IND-ADM1.geojson: that one is
|
||||
// UPG-100's partition, curated to 34 units, and it left Mizoram and Nagaland with no geometry.
|
||||
fetch(`${assetsBase.value}/boundaries/gb-IND-ADM1-full.geojson`).then((r) => { if (!r.ok) throw new Error('gb-IND-ADM1-full ' + r.status); return r.json() }),
|
||||
fetch(`${assetsBase.value}/boundaries/gb-IND-ADM1-full-web.geojson`).then((r) => { if (!r.ok) throw new Error('gb-IND-ADM1-full ' + r.status); return r.json() }),
|
||||
])
|
||||
const byArea = {}
|
||||
for (const r of table.rows || []) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue