Merge main (23976fb0) after landing 9
Batch 1's content is on main asde24e633, studio's re-application rather than mycf8cc929, so the commit is not an ancestor and only the CONTENT test says it landed: RegionSectionCard.vue on origin/main is byte-identical to mine. Three conflicts, none resolved by choosing a side: gates:post — both sides added tokens. Mine brought check:shadow-css-parses, main brought check:no-silent-engine and check:i18n-stopped-scope. Taking either side would have dropped the other's gates, so the tokens are unioned: 81, none lost. check-regions-uses-library.mjs — add/add. Mine is the superset; the only two lines unique to main's copy are ones batch 2 deliberately replaced, generalising the import assertion to @map/components/<dir>/<name> so ProgressBar in charts/ resolves. reference-library-index.md — derived, so regenerated rather than merged by hand. 113 items. Verified after merging by content and by authorship, not by reading a diff: all four batch-2 components, the CLDR vendor and the gate survive; the plugin still renders them and still ships no .rd-card CSS; SJM -> SJ and NOR -> NO hold. git log over the merge base shows only my own commit touched region-dashboards.vue, so nothing of another agent's was at risk there. 12 gates green.
This commit is contained in:
commit
d378b38653
150 changed files with 3460 additions and 1174 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",
|
||||
|
|
|
|||
64
internal/check-i18n-stopped-scope.mjs
Normal file
64
internal/check-i18n-stopped-scope.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-i18n-stopped-scope.mjs — createAppI18n() must succeed while a STOPPED effect scope is current.
|
||||
*
|
||||
* WHY (P0, the Driver's console, 2026-09-27): in Plugin Studio's gallery every tab after the first threw
|
||||
* "SyntaxError: Unexpected error" from createI18n inside createAppI18n. vue-i18n's createGlobal() runs in
|
||||
* scope.run() and throws UNEXPECTED_ERROR when that returns undefined; Vue 3.5 creates a new EffectScope
|
||||
* INACTIVE when the currently active one is stopped; and a custom element's connectedCallback ->
|
||||
* configureApp -> createAppI18n can run while the released tab's stopped scope is still current.
|
||||
*
|
||||
* The CONTROL comes first: a bare createI18n() inside the same stopped scope must THROW, or this harness
|
||||
* does not reproduce the failure and a green result would prove nothing. Then createAppI18n() inside it
|
||||
* must succeed, translate, and keep reacting to a locale change.
|
||||
*/
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
let bad = 0
|
||||
const check = (name, ok, got) => { if (!ok) bad++; console.log(` ${ok ? 'ok ' : 'FAIL'} ${name}${ok ? '' : ' — ' + got}`) }
|
||||
|
||||
const { createServer } = await import('vite')
|
||||
const server = await createServer({ root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
|
||||
server: { middlewareMode: true, hmr: false, ws: false, watch: null }, resolve: { alias: { '@map': join(ROOT, 'library') } } })
|
||||
try {
|
||||
globalThis.__INTLIFY_JIT_COMPILATION__ = true
|
||||
const { effectScope } = await import('vue')
|
||||
const { createI18n } = await import('vue-i18n')
|
||||
const mod = await server.ssrLoadModule('/library/i18n/index.js')
|
||||
// Set AFTER vue loads (runtime-dom probes `document` at import). These are the two page facts
|
||||
// createAppI18n reads, the URL and <html lang>, as a plain English page would give them.
|
||||
globalThis.window ??= { location: { search: '' } }
|
||||
globalThis.document ??= { documentElement: { lang: 'en', dir: '' } }
|
||||
|
||||
// A scope that ran, was stopped, and is still the current one — the released tab's.
|
||||
const stopped = effectScope()
|
||||
stopped.run(() => {})
|
||||
stopped.stop()
|
||||
const inStopped = (fn) => { stopped.on(); try { return fn() } finally { stopped.off() } }
|
||||
|
||||
let controlThrew = false
|
||||
try { inStopped(() => createI18n({ legacy: false, locale: 'en', messages: { en: { a: 'b' } } })) } catch { controlThrew = true }
|
||||
check('CONTROL: a bare createI18n() inside a stopped scope throws (the harness reproduces the bug)', controlThrew, 'it did not throw')
|
||||
|
||||
let i18n = null, err = null
|
||||
try { i18n = inStopped(() => mod.createAppI18n()) } catch (e) { err = e }
|
||||
check('createAppI18n() succeeds inside a stopped scope', !!i18n && !err, err && String(err))
|
||||
if (i18n) {
|
||||
check('and it translates', i18n.global.t('buttons.close') !== 'buttons.close', i18n.global.t('buttons.close'))
|
||||
i18n.global.locale.value = 'ar'
|
||||
await new Promise((r) => setTimeout(r, 0))
|
||||
check('and its locale watcher is live (the static reader follows a runtime switch)', mod.getRuntimeLocale() === 'ar', mod.getRuntimeLocale())
|
||||
// The bundle entries that still provide appI18n themselves after app.use(i18n) must not make Vue warn.
|
||||
const { createApp } = await import('vue')
|
||||
const warns = []; const warn = console.warn; console.warn = (...a) => warns.push(a.join(' '))
|
||||
try { const app = createApp({}); app.use(i18n); app.provide('appI18n', i18n) } finally { console.warn = warn }
|
||||
check('a bundle providing appI18n again after app.use(i18n) raises no Vue warning', !warns.some((w) => /appI18n/.test(w)), warns.join(' | '))
|
||||
}
|
||||
} finally {
|
||||
await server.close()
|
||||
}
|
||||
|
||||
if (bad) { console.error(`\nI18N-STOPPED-SCOPE: FAIL — ${bad} assertion(s)`); process.exit(1) }
|
||||
console.log('\nI18N-STOPPED-SCOPE: PASS — the app i18n is created live even while a stopped effect scope is current.')
|
||||
|
|
@ -63,12 +63,36 @@ const PATTERN = new RegExp(
|
|||
function scan(text) {
|
||||
const hits = []
|
||||
const lines = text.split('\n')
|
||||
// ⚠ BLOCK-COMMENT STATE, not a per-line prefix test. The first version asked only whether a line
|
||||
// STARTS with //, * or /*, and that is not what a comment is: it misses every CONTINUATION line of a
|
||||
// /* … */ block whose text happens to begin with something else.
|
||||
//
|
||||
// IT COST A FALSE ACCUSATION, 2026-09-27, and the accusation was aimed at the correct fix. gospel
|
||||
// fixed this defect in resources-map.vue — `const gl = engineGlobal(grProvider)`, no `||` — and
|
||||
// documented what they removed in a /* */ block whose second line begins with a BACKTICK:
|
||||
// `engineGlobal(grProvider) || window.mapboxgl`. engineGlobal already defaults to mapbox, so
|
||||
// The prefix test saw no comment marker, matched the quoted pattern, and reported gospel as the last
|
||||
// remaining offender at main 9df086e5. I relayed that to dx-shepherd as fact and it was wrong: the
|
||||
// line number had also moved three times (1490 -> 1505 -> 1509) purely because their file grew, which
|
||||
// is what made a stale card look like a live defect.
|
||||
// ★ A GATE THAT FAILS ON THE EXPLANATION OF A FIX IS WORSE THAN NO GATE. It tells the one person who
|
||||
// did the right thing that they did the wrong thing, and the cheapest way to make it green is to
|
||||
// DELETE the explanation — so the gate actively destroys the reasoning it should protect. This file's
|
||||
// own header quotes the pattern for the same reason, which is why the skip existed at all; it was
|
||||
// just too shallow to cover the form another agent actually used.
|
||||
let inBlock = false
|
||||
lines.forEach((line, i) => {
|
||||
// Skip comment lines: this file's own documentation quotes the bad pattern, and so does every fix
|
||||
// that explains what it removed. A gate that fails on its own explanation teaches people to delete
|
||||
// the explanation.
|
||||
const t = line.trim()
|
||||
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return
|
||||
const wasInBlock = inBlock
|
||||
// Track /* and */ on this line so the NEXT line knows where it stands. Deliberately simple: a
|
||||
// block marker inside a string literal would fool it, and the alternative is a JS parser for a
|
||||
// one-expression grep. The failure mode of over-skipping is a missed offender, which the
|
||||
// --self-test guards against by keeping real forms in the MUST-MATCH list.
|
||||
const opens = (line.match(/\/\*/g) || []).length
|
||||
const closes = (line.match(/\*\//g) || []).length
|
||||
if (opens > closes) inBlock = true
|
||||
else if (closes > opens) inBlock = false
|
||||
if (wasInBlock || t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return
|
||||
PATTERN.lastIndex = 0
|
||||
let m
|
||||
while ((m = PATTERN.exec(line)) !== null) hits.push({ line: i + 1, engine: m[1], text: t.slice(0, 110) })
|
||||
|
|
@ -90,6 +114,21 @@ if (SELF_TEST) {
|
|||
'const gl = engineGlobal(p) || null',
|
||||
'const token = mapboxToken?.value || profileConfig?.value?.tk || \'\'',
|
||||
]
|
||||
// ★ MULTI-LINE CASES, because the single-line list above cannot express the bug that actually
|
||||
// happened: a CONTINUATION line of a /* */ block. Each entry is scanned as one text so the
|
||||
// block-comment state machine is what is under test, not the prefix check.
|
||||
const shouldNotMultiline = [
|
||||
// gospel's real fix at resources-map.vue, verbatim in shape — the line that was falsely accused
|
||||
// begins with a BACKTICK, so no prefix test could ever have skipped it.
|
||||
[` /* ⚠ NEVER SWITCH ENGINES SILENTLY. This used to read
|
||||
\`engineGlobal(grProvider) || window.mapboxgl\`. engineGlobal already defaults to mapbox, so
|
||||
the fallback could only ever fire for provider 'maplibre'. */
|
||||
const gl = engineGlobal(grProvider)`, 'a /* */ block quoting the removed fallback, then the correct fix'],
|
||||
// and the inverse: a real offender AFTER a block comment closes must still be caught, or the fix
|
||||
// above would have traded a false positive for a false negative.
|
||||
[` /* explanation that mentions engineGlobal(p) || window.mapboxgl and then ends */
|
||||
const gl = engineGlobal(p) || window.mapboxgl`, null],
|
||||
]
|
||||
let bad = 0
|
||||
for (const s of should) {
|
||||
const n = scan(s).length
|
||||
|
|
@ -101,6 +140,17 @@ if (SELF_TEST) {
|
|||
console.log(` ${n ? 'FALSE+ ' : 'ignored '} ${s.trim().slice(0, 62)}`)
|
||||
if (n) bad++
|
||||
}
|
||||
// The multi-line pairs: a label means "must be IGNORED and here is why", null means "must MATCH".
|
||||
// Both directions are asserted together on purpose — fixing the false positive by skipping more is
|
||||
// only correct if a real offender after the block still fails.
|
||||
for (const [text, why] of shouldNotMultiline) {
|
||||
const n = scan(text).length
|
||||
const wantHit = why === null
|
||||
const ok = wantHit ? n > 0 : n === 0
|
||||
if (!ok) bad++
|
||||
console.log(` ${ok ? (wantHit ? 'MATCHED ' : 'ignored ') : (wantHit ? 'MISSED ' : 'FALSE+ ')} `
|
||||
+ `[multi-line] ${wantHit ? 'a real offender AFTER a closed block comment' : why}`)
|
||||
}
|
||||
if (bad) {
|
||||
console.error(`\nNO-SILENT-ENGINE-SUBSTITUTION --self-test: FAIL — ${bad} case(s) wrong.`)
|
||||
process.exit(1)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
* See documentation/feedback.md → Section A for the design rationale.
|
||||
*/
|
||||
|
||||
import { watch } from 'vue'
|
||||
import { watch, effectScope } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Eager glob — all locale subfolders inlined at build time
|
||||
|
|
@ -154,7 +154,14 @@ export function createAppI18n() {
|
|||
document.documentElement.dir = 'rtl'
|
||||
}
|
||||
} catch { /* no document (SSR/tests) — nothing to reflect */ }
|
||||
const i18n = createI18n({
|
||||
// ★ A DETACHED EFFECT SCOPE (P0, the Driver's console, 2026-09-27). vue-i18n's createGlobal() runs
|
||||
// inside scope.run() and throws UNEXPECTED_ERROR ("SyntaxError: Unexpected error") when that returns
|
||||
// undefined; Vue 3.5 creates a new EffectScope INACTIVE when the currently active scope is STOPPED.
|
||||
// A custom element's connectedCallback -> configureApp -> here can run while a released tab's stopped
|
||||
// scope is still current (Plugin Studio's gallery, every tab after the first). effectScope(true) never
|
||||
// inherits the ambient scope, so this instance and its locale watcher are always created live.
|
||||
const i18nScope = effectScope(true)
|
||||
const i18n = i18nScope.run(() => createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
locale,
|
||||
|
|
@ -162,7 +169,7 @@ export function createAppI18n() {
|
|||
messages,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
})
|
||||
}))
|
||||
|
||||
// ★ INSTALLING i18n ALSO PROVIDES IT, so a bundle CANNOT forget. This is the durable half of a
|
||||
// bug that is invisible by construction.
|
||||
|
|
@ -195,12 +202,17 @@ export function createAppI18n() {
|
|||
// Watching the instance's own locale ref is the single choke point: ProfileLoader, the SPA's
|
||||
// picker and the studio's picker all move THIS ref, so none of them has to remember to also tell
|
||||
// the static reader.
|
||||
watch(i18n.global.locale, (l) => setRuntimeLocale(l), { immediate: true })
|
||||
i18nScope.run(() => watch(i18n.global.locale, (l) => setRuntimeLocale(l), { immediate: true }))
|
||||
|
||||
const install = i18n.install.bind(i18n)
|
||||
i18n.install = (app, ...rest) => {
|
||||
const out = install(app, ...rest)
|
||||
app.provide('appI18n', i18n)
|
||||
// Several bundle entries still call app.provide('appI18n', i18n) themselves after app.use(i18n). The
|
||||
// value is the same instance, but Vue warns "App already provides property with key appI18n" on every
|
||||
// mount. A second provide of THIS instance under that key is made a no-op; anything else passes through.
|
||||
const provide = app.provide.bind(app)
|
||||
app.provide = (key, value) => (key === 'appI18n' && value === i18n ? app : provide(key, value))
|
||||
return out
|
||||
}
|
||||
return i18n
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "فجوة الصم",
|
||||
"t_resources": "الموارد حسب الشعب",
|
||||
"t_languages": "اللغات حسب البلد",
|
||||
"t_growth": "الوصول مقابل نمو السكان"
|
||||
"t_growth": "الوصول مقابل نمو السكان",
|
||||
"t_bridgereach": "لغة واحدة، كم شعبًا؟"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "موارد الإنجيل حسب اللغة",
|
||||
|
|
|
|||
|
|
@ -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": "اللوحة",
|
||||
|
|
|
|||
|
|
@ -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} لغة"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "बधिर कमी",
|
||||
"t_resources": "जनसमूह अनुसार संसाधन",
|
||||
"t_languages": "देश अनुसार भाषाएँ",
|
||||
"t_growth": "पहुँच बनाम जनसंख्या वृद्धि"
|
||||
"t_growth": "पहुँच बनाम जनसंख्या वृद्धि",
|
||||
"t_bridgereach": "एक भाषा, कितने जनसमूह"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "भाषा अनुसार सुसमाचार संसाधन",
|
||||
|
|
|
|||
|
|
@ -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": "बोर्ड",
|
||||
|
|
|
|||
|
|
@ -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} भाषाओं में नाम"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "Пробел у глухих",
|
||||
"t_resources": "Ресурсы по народам",
|
||||
"t_languages": "Языки по странам",
|
||||
"t_growth": "Доступ и рост населения"
|
||||
"t_growth": "Доступ и рост населения",
|
||||
"t_bridgereach": "Один язык — сколько народов"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "Ресурсы Евангелия по языкам",
|
||||
|
|
|
|||
|
|
@ -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": "Панель",
|
||||
|
|
|
|||
|
|
@ -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} языках"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"t_deaf": "聋人缺口",
|
||||
"t_resources": "按族群列出资源",
|
||||
"t_languages": "各国语言",
|
||||
"t_growth": "可及性与人口增长"
|
||||
"t_growth": "可及性与人口增长",
|
||||
"t_bridgereach": "一种语言,覆盖多少民族"
|
||||
},
|
||||
"pages": {
|
||||
"listTitle": "按语言列出的福音资源",
|
||||
|
|
|
|||
|
|
@ -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": "看板",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* _qa-cycle/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`)
|
||||
}
|
||||
|
|
@ -182,7 +182,9 @@
|
|||
"funnel_list_unreached": "Project 7000: unreached people groups",
|
||||
"funnel_list_frontier": "Project 4700: frontier people groups",
|
||||
"funnel_list_frontier-500k": "Project 700: frontier people groups of 500,000 or more",
|
||||
"funnel_list_unengaged": "Unengaged unreached people groups"
|
||||
"funnel_list_unengaged": "Unengaged unreached people groups",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} more"
|
||||
},
|
||||
"es": {
|
||||
"map_page_kicker": "GRUPOS ÉTNICOS NO EVANGELIZADOS",
|
||||
|
|
@ -357,7 +359,9 @@
|
|||
"funnel_list_unreached": "Proyecto 7000: grupos étnicos no alcanzados",
|
||||
"funnel_list_frontier": "Proyecto 4700: grupos étnicos de frontera",
|
||||
"funnel_list_frontier-500k": "Proyecto 700: grupos étnicos de frontera de 500.000 o más",
|
||||
"funnel_list_unengaged": "Grupos étnicos no alcanzados y sin obreros"
|
||||
"funnel_list_unengaged": "Grupos étnicos no alcanzados y sin obreros",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} más"
|
||||
},
|
||||
"fr": {
|
||||
"map_page_kicker": "GROUPES ETHNIQUES NON ATTEINTS",
|
||||
|
|
@ -532,7 +536,9 @@
|
|||
"funnel_list_unreached": "Projet 7000 : peuples non atteints",
|
||||
"funnel_list_frontier": "Projet 4700 : peuples frontières",
|
||||
"funnel_list_frontier-500k": "Projet 700 : peuples frontières de 500 000 personnes ou plus",
|
||||
"funnel_list_unengaged": "Peuples non atteints et sans ouvrier"
|
||||
"funnel_list_unengaged": "Peuples non atteints et sans ouvrier",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} autres"
|
||||
},
|
||||
"pt": {
|
||||
"map_page_kicker": "GRUPOS ÉTNICOS NÃO ALCANÇADOS",
|
||||
|
|
@ -707,7 +713,9 @@
|
|||
"funnel_list_unreached": "Projeto 7000: grupos étnicos não alcançados",
|
||||
"funnel_list_frontier": "Projeto 4700: grupos étnicos de fronteira",
|
||||
"funnel_list_frontier-500k": "Projeto 700: grupos étnicos de fronteira de 500.000 ou mais",
|
||||
"funnel_list_unengaged": "Grupos étnicos não alcançados e sem obreiros"
|
||||
"funnel_list_unengaged": "Grupos étnicos não alcançados e sem obreiros",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} mais"
|
||||
},
|
||||
"de": {
|
||||
"map_page_kicker": "UNERREICHTE VOLKSGRUPPEN",
|
||||
|
|
@ -882,7 +890,9 @@
|
|||
"funnel_list_unreached": "Projekt 7000: unerreichte Volksgruppen",
|
||||
"funnel_list_frontier": "Projekt 4700: Frontier-Volksgruppen",
|
||||
"funnel_list_frontier-500k": "Projekt 700: Frontier-Volksgruppen mit 500.000 oder mehr",
|
||||
"funnel_list_unengaged": "Unerreichte Volksgruppen ohne Mitarbeiter"
|
||||
"funnel_list_unengaged": "Unerreichte Volksgruppen ohne Mitarbeiter",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} weitere"
|
||||
},
|
||||
"it": {
|
||||
"map_page_kicker": "GRUPPI ETNICI NON RAGGIUNTI",
|
||||
|
|
@ -1057,7 +1067,9 @@
|
|||
"funnel_list_unreached": "Progetto 7000: gruppi etnici non raggiunti",
|
||||
"funnel_list_frontier": "Progetto 4700: gruppi etnici di frontiera",
|
||||
"funnel_list_frontier-500k": "Progetto 700: gruppi etnici di frontiera di 500.000 o più",
|
||||
"funnel_list_unengaged": "Gruppi etnici non raggiunti e senza operai"
|
||||
"funnel_list_unengaged": "Gruppi etnici non raggiunti e senza operai",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + altri {n}"
|
||||
},
|
||||
"ro": {
|
||||
"map_page_kicker": "GRUPURI ETNICE NEATINSE",
|
||||
|
|
@ -1232,7 +1244,9 @@
|
|||
"funnel_list_unreached": "Proiectul 7000: grupuri etnice neatinse",
|
||||
"funnel_list_frontier": "Proiectul 4700: grupuri etnice de frontieră",
|
||||
"funnel_list_frontier-500k": "Proiectul 700: grupuri etnice de frontieră de 500.000 sau mai multe",
|
||||
"funnel_list_unengaged": "Grupuri etnice neatinse și fără lucrători"
|
||||
"funnel_list_unengaged": "Grupuri etnice neatinse și fără lucrători",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + încă {n}"
|
||||
},
|
||||
"ru": {
|
||||
"map_page_kicker": "НЕОХВАЧЕННЫЕ НАРОДЫ",
|
||||
|
|
@ -1407,7 +1421,9 @@
|
|||
"funnel_list_unreached": "Проект 7000: неохваченные народы",
|
||||
"funnel_list_frontier": "Проект 4700: народы-фронтир",
|
||||
"funnel_list_frontier-500k": "Проект 700: народы-фронтир численностью 500 000 и более",
|
||||
"funnel_list_unengaged": "Неохваченные народы без работников"
|
||||
"funnel_list_unengaged": "Неохваченные народы без работников",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + ещё {n}"
|
||||
},
|
||||
"ar": {
|
||||
"map_page_kicker": "شعوب غير مبلَّغة",
|
||||
|
|
@ -1582,7 +1598,9 @@
|
|||
"funnel_list_unreached": "مشروع 7000: المجموعات الشعبية غير المبلَّغة",
|
||||
"funnel_list_frontier": "مشروع 4700: المجموعات الشعبية الحدودية",
|
||||
"funnel_list_frontier-500k": "مشروع 700: المجموعات الشعبية الحدودية التي يبلغ عددها 500,000 أو أكثر",
|
||||
"funnel_list_unengaged": "مجموعات شعبية غير مبلَّغة وبلا عاملين"
|
||||
"funnel_list_unengaged": "مجموعات شعبية غير مبلَّغة وبلا عاملين",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} أخرى"
|
||||
},
|
||||
"hi": {
|
||||
"map_page_kicker": "अनछुए जनसमूह",
|
||||
|
|
@ -1757,7 +1775,9 @@
|
|||
"funnel_list_unreached": "परियोजना 7000: अप्राप्त जनसमूह",
|
||||
"funnel_list_frontier": "परियोजना 4700: सीमांत जनसमूह",
|
||||
"funnel_list_frontier-500k": "परियोजना 700: 500,000 या अधिक के सीमांत जनसमूह",
|
||||
"funnel_list_unengaged": "अप्राप्त जनसमूह जिनमें कोई कार्यकर्ता नहीं"
|
||||
"funnel_list_unengaged": "अप्राप्त जनसमूह जिनमें कोई कार्यकर्ता नहीं",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + {n} और"
|
||||
},
|
||||
"zh": {
|
||||
"map_page_kicker": "未得之民",
|
||||
|
|
@ -1932,6 +1952,8 @@
|
|||
"funnel_list_unreached": "7000 计划:未得之民族群",
|
||||
"funnel_list_frontier": "4700 计划:前沿族群",
|
||||
"funnel_list_frontier-500k": "700 计划:50 万人以上的前沿族群",
|
||||
"funnel_list_unengaged": "无人服事的未得之民族群"
|
||||
"funnel_list_unengaged": "无人服事的未得之民族群",
|
||||
"zone_join": "{a} + {b}",
|
||||
"zone_join_more": "{a} + 另外 {n} 个"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
"check:i18n-keys-exist": "node plugin-designs/research-map-plus-grid/scripts/check-i18n-keys-exist.mjs",
|
||||
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs",
|
||||
"check:i18n-parity": "node internal/check-i18n-parity.mjs",
|
||||
"check:i18n-stopped-scope": "node internal/check-i18n-stopped-scope.mjs",
|
||||
"check:i18n-treeshake": "node internal/check-i18n-treeshake.mjs",
|
||||
"check:kit-options-reachable": "node plugin-designs/research-dashboard/scripts/check-kit-options-reachable.mjs",
|
||||
"check:kpi-unit": "node internal/check-kpi-unit-once.mjs",
|
||||
|
|
@ -233,7 +234,7 @@
|
|||
"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:regions-uses-library check:shadow-css-parses",
|
||||
"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",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
|
|
@ -340,6 +341,7 @@
|
|||
"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-never-translated": "a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||||
"check:i18n-stopped-scope": "createAppI18n() must succeed while a STOPPED effect scope is current: the Driver's P0 (Plugin Studio gallery, every tab after the first threw 'SyntaxError: Unexpected error' from createI18n, because Vue 3.5 makes a new scope inactive under a stopped one). A bare createI18n() in the same scope must throw first, or the harness proves nothing. Also asserts a bundle's second provide('appI18n') raises no warning.",
|
||||
"check:i18n-treeshake": "each built bundle carries every translation area its code asks for: every area-prefixed key literal in <bundle>.js belongs to an area the build included (the build narrows library/i18n's catalogue glob per bundle, internal/i18n-areas.mjs). Headless.",
|
||||
"check:kit-options-reachable": "every option a shared kit composable ACCEPTS must be honoured somewhere in its body. Written because I nearly shipped one: kit landing 3 gave useSelectionCamera a `coords` option so a dashboard whose rows spell their coordinate differently could say so, and it was accepted in the signature and never passed to selectionBounds - a caller could set it, type-check clean, build clean and watch it do nothing. That is the worst shape a missing feature takes, because it READS as supported and nobody files a bug against a knob they assume they misused; this tree already paid for it once with a defineProps on a component the custom element never binds. It matters more for the kit than for one dashboard: the whole promise of SPEC-DASHBOARD-KIT is that a config produces a working dashboard, and a parameter that is merely present turns that promise into a guess. Strips comments from BOTH the signature and the body before searching - the first version was satisfied by the comment beside the fixed call site, and the second read comment prose as option names. Not a type checker: it catches a completely unused option, which is the failure invisible from outside.",
|
||||
"check:language-code-link": "the map's language selection reaches the grid as a locale-stable CODE, or narrows nothing at all. rega measured Languages > Language > \"Arabic\" taking the grid 2,107 -> 0 in BOTH locales: the emitter looked for a row whose language LABEL equalled the selected name, but a language tier's label is derived by splitting the API label at the comma, and no row reads \"Arabic\" - they read \"Arabic, Sudanese\", \"Arabic, Levantine\", 22 codes in all. Lifts the emitter's match rule and the composite's gridLanguage() from the shipping source (never re-typed) and runs them against a committed fixture of the 864 distinct language label/value pairs: the emitter's rule must agree with the legend's own three-arm filter on every (label, tier) pair, every tier must select at least one row, a tier covering one code must emit it, a tier covering several must emit NONE and leave the axis unfiltered. No network, no browser; --refresh-fixture re-takes the fixture.",
|
||||
|
|
|
|||
|
|
@ -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 || []) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1114,7 +1114,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()),
|
||||
// 650KB and it is the point of the map, so it loads with the boundaries rather
|
||||
// than behind a tap. URLs are normalised into `codes` by ISO 639-3, so the 12,379
|
||||
// pins carry a code rather than 12,379 copies of the same jesusfilm.org link.
|
||||
|
|
@ -1270,7 +1270,7 @@ async function ensureProvinceTable() {
|
|||
return provinceRows.value
|
||||
}
|
||||
|
||||
const PROVINCE_FILE = { IND: 'gb-IND-ADM1-full.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
const PROVINCE_FILE = { IND: 'gb-IND-ADM1-full-web.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
|
||||
async function ensureProvinces(iso3) {
|
||||
const map = mapRef.value
|
||||
|
|
|
|||
164
plugin-designs/my-upg-100-list/data/adm0/world-adm0-web.geojson
vendored
Normal file
164
plugin-designs/my-upg-100-list/data/adm0/world-adm0-web.geojson
vendored
Normal file
File diff suppressed because one or more lines are too long
36
plugin-designs/my-upg-100-list/data/gb-IND-ADM1-web.geojson
vendored
Normal file
36
plugin-designs/my-upg-100-list/data/gb-IND-ADM1-web.geojson
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -85,6 +85,16 @@ function leafPopulation(row, records) {
|
|||
export function buildLegendNodes(clusters, opts = {}) {
|
||||
const deafCount = opts.deafCount ?? 123;
|
||||
const records = Array.isArray(opts.records) ? opts.records : null;
|
||||
// ★ OPTIONAL GENERATED TITLE (Driver, 2026-09-27). `opts.title(zone)` returns a localized title
|
||||
// composed from already-translated parts, or NULL for a zone whose title cannot be generated — see
|
||||
// data/zone-title.js. Passing nothing keeps every hand-written English label exactly as it was, which
|
||||
// is why this is an option rather than a rewrite: the legend, the poster and the packet adopt it
|
||||
// independently, and a caller that has no records yet (records:null) still builds a correct tree.
|
||||
// ⚠ NULL MEANS "KEEP YOURS", NOT "NO TITLE". 18 of 28 zones decline (their labels are sub-national or
|
||||
// taxonomic, and Intl has no subdivisions), so the fallback is the normal path, not the error path.
|
||||
const titleOf = typeof opts.title === 'function'
|
||||
? (zone, fallback) => opts.title(zone) || fallback
|
||||
: (_zone, fallback) => fallback;
|
||||
const lists = clusters.lists || [];
|
||||
const asia = clusters.asia_lists || [];
|
||||
// slice 09b-6 §1(a): wire the 7 uncovered "Frontier Asia" zones (Nepal,
|
||||
|
|
@ -137,7 +147,7 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
{
|
||||
id: l.id,
|
||||
zoneKey: l.id,
|
||||
label: l.label,
|
||||
label: titleOf(l, l.label),
|
||||
// Carry the province enumeration the region-name label replaced.
|
||||
...(l.provinceLabel ? { provinceLabel: l.provinceLabel } : {}),
|
||||
color: colorOf(l.id),
|
||||
|
|
@ -171,7 +181,7 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
{
|
||||
id: r.id,
|
||||
zoneKey: r.id,
|
||||
label: r.label,
|
||||
label: titleOf(r, r.label),
|
||||
// The province enumeration the region-name label replaced. Carried through
|
||||
// like adm0/provinces above — a node rebuilt field-by-field silently drops
|
||||
// anything not named, which is exactly how adm0 and population were lost.
|
||||
|
|
@ -228,7 +238,7 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
{
|
||||
id: u.id,
|
||||
zoneKey: u.id,
|
||||
label: u.label,
|
||||
label: titleOf(u, u.label),
|
||||
// Carry the province enumeration the region-name label replaced.
|
||||
...(u.provinceLabel ? { provinceLabel: u.provinceLabel } : {}),
|
||||
color: colorOf(u.id),
|
||||
|
|
|
|||
178
plugin-designs/my-upg-100-list/data/zone-title.js
vendored
Normal file
178
plugin-designs/my-upg-100-list/data/zone-title.js
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* zone-title — compose a zone's DISPLAY TITLE from parts that are ALREADY translated, so adding a
|
||||
* project list costs no new translation and adding a locale costs no code change.
|
||||
*
|
||||
* THE PROBLEM (Driver, 2026-09-27). Every zone label is a hand-written English string — "Middle East
|
||||
* Core", "Malabar Coast" — so the Arabic map reads English. Hand-translating them does not scale,
|
||||
* because UPG-100 is becoming a PATTERN: Project 7000 / 4700 / 3000 / 2000 / 700 each invent their own
|
||||
* ~100 clusters, and a per-title translation is a cost paid again for every list, forever. The Driver's
|
||||
* question was precisely "how do you generate a title without a new translation each time".
|
||||
*
|
||||
* ★ WHAT THE DATA ACTUALLY ALLOWS, measured before any of this was written (2026-09-27):
|
||||
*
|
||||
* SOURCE COVERAGE OF OUR DATA FAILURE MODE
|
||||
* country_code_label from the records API all 175 codes, every locale (absent field)
|
||||
* Intl.DisplayNames({type:'region'}) all 175, every locale THROWS on ISO-3
|
||||
* Intl.DisplayNames({type:'language'}) 213 of 2242 = 9.5% ECHOES THE CODE
|
||||
*
|
||||
* ⚠ LANGUAGE IS NOT A USABLE TITLE SOURCE, which kills the more attractive half of the original idea.
|
||||
* `new Intl.DisplayNames(['en'],{type:'language'}).of('kfa')` returns the STRING "kfa" — it does not
|
||||
* throw — so a miss prints a raw code onto a printed poster with nothing raising a hand. And the 90.5%
|
||||
* miss is STRUCTURAL rather than a gap someone can fill: unreached peoples speak small languages by
|
||||
* definition, and CLDR carries the big ones. So this module never names a language.
|
||||
*
|
||||
* ★ AND COUNTRY NAMES COME FROM THE RECORDS, NOT FROM Intl, which was the measurement that simplified
|
||||
* the whole design. The records API already returns `country_code_label` localized by its own `lang`
|
||||
* parameter — verified live: IND is الهند / भारत / 印度 / Индия for lang=ar/hi/zh/ru, matching Intl
|
||||
* exactly. Since the packet and the page ALREADY fetch these records at the reader's locale, the
|
||||
* localized name is free and needs no ISO-3166 alpha-3 → alpha-2 mapping at all. Intl is kept only as a
|
||||
* FALLBACK for a caller who has a code and no record.
|
||||
*
|
||||
* WHICH ZONES THIS CAN AND CANNOT TITLE — stated here because a generator that silently produces a bad
|
||||
* title is worse than one that declines:
|
||||
* TIER A selector country_code → one country name. GENERATED
|
||||
* TIER B selector country_codes → 2..n country names + a joiner. GENERATED
|
||||
* TIER C wagf_block / wagf_region / pg_slugs / union / affinity_bloc → null. DECLINED
|
||||
* Tier C is 18 of our 28 zones and it cannot be done from country names: the 12 India/China zones are
|
||||
* `pg_slugs` whose every record is ONE country (measured: asia-india-kerala is 78/78 IND), so countries
|
||||
* would render "India" twelve times and could not tell "Malabar Coast" from "Northwest India" — those
|
||||
* labels are SUB-NATIONAL. Naming them needs a province/subdivision source, which Intl has none of; the
|
||||
* curated-table-vs-keep-English choice is with the Driver (repo's digest item D). Until it is decided
|
||||
* this returns null for tier C and the caller keeps the existing label, so nothing regresses.
|
||||
*
|
||||
* THE ONLY TRANSLATED STRINGS ARE THE TWO JOINERS — `zone_join` ("{a} + {b}") and `zone_join_more`
|
||||
* ("{a} + {n} more") — in packet-strings.json, one pair per locale block. That is the entire
|
||||
* translation cost for every project list that ever uses this.
|
||||
*/
|
||||
|
||||
/** Tier A/B selector types. Anything else is tier C and declined. */
|
||||
const COUNTRY_SELECTORS = new Set(['country_code', 'country_codes'])
|
||||
|
||||
/**
|
||||
* The ISO-3 codes a selector names, in the order the selector lists them.
|
||||
* ⚠ ORDER IS THE AUTHOR'S, NOT ALPHABETICAL. clusters.json lists the countries of a zone with the
|
||||
* biggest/most representative first (middle-east-core starts IRN,YEM,IRQ), and that order is editorial
|
||||
* information: it is what makes "Iran + Yemen + 8 more" a better title than "Bahrain + Iran + 8 more".
|
||||
* Sorting here would discard a judgement someone already made.
|
||||
*/
|
||||
export function zoneCountryCodes(selector) {
|
||||
if (!selector || !COUNTRY_SELECTORS.has(selector.type)) return []
|
||||
if (selector.type === 'country_code') return selector.value ? [selector.value] : []
|
||||
return Array.isArray(selector.values) ? selector.values.filter(Boolean) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO-3 → a localized country name.
|
||||
*
|
||||
* @param {string} iso3
|
||||
* @param {object} opts
|
||||
* @param {Map<string,string>} [opts.byCode] ISO-3 → country_code_label, built from the records this
|
||||
* page/packet already loaded AT THE READER'S LOCALE. The preferred source: no alpha-2 mapping,
|
||||
* no second service, and it agrees with Intl where both have an answer.
|
||||
* @param {string} [opts.locale] used only by the Intl fallback.
|
||||
* @param {(iso3:string, locale:string)=>string} [opts.countryName] regions-plugin's coming library
|
||||
* helper (their card BUILD-LOCALIZED-COUNTRY-NAMES-ISO3-TO-ISO2). Injected rather than imported
|
||||
* so this module does not depend on an unlanded file; when it lands, pass it and it wins over
|
||||
* the local Intl fallback.
|
||||
* @returns {string|null} null rather than the raw code — see below.
|
||||
*/
|
||||
export function countryLabel(iso3, opts = {}) {
|
||||
const code = String(iso3 || '').toUpperCase()
|
||||
if (!code) return null
|
||||
// ★ ORDER: regions' helper (or Intl) FIRST, the records' label SECOND — and this order is the
|
||||
// opposite of the one I wrote first, corrected by measuring what the two sources actually say.
|
||||
// The records API returns the FORMAL country name, which is wrong for a poster header:
|
||||
// LAO "Lao People's Democratic Republic" vs Intl "Laos"
|
||||
// IRN "Islamic Republic of Iran" vs Intl "Iran"
|
||||
// CHN "People's Republic of China" vs Intl "China"
|
||||
// 28 of our 175 country labels are long formal forms like these. My first version preferred the
|
||||
// records because they were already localized and needed no code mapping — true, and it produced
|
||||
// "Lao People's Democratic Republic" as the title of the zone whose hand-written label was "Laos",
|
||||
// i.e. a generated title WORSE than the one it replaced. Intl gives the short common form in every
|
||||
// locale, which is what a header and a 380px legend row want.
|
||||
// The records stay as the fallback because they need no alpha-2 mapping and are always present.
|
||||
if (typeof opts.countryName === 'function') {
|
||||
const n = opts.countryName(code, opts.locale || 'en')
|
||||
if (n && n !== code) return n
|
||||
}
|
||||
// ⚠ Intl REGIONS TAKE ALPHA-2 AND THROW ON ALPHA-3 — `new Intl.DisplayNames(['en'],{type:'region'})
|
||||
// .of('IND')` is a RangeError, not a miss. So a 3-letter code needs a mapping, and `alpha2` is that
|
||||
// mapping, INJECTED rather than imported: regions-plugin owns the ISO-3→ISO-2 table (their card
|
||||
// BUILD-LOCALIZED-COUNTRY-NAMES-ISO3-TO-ISO2) and it has not landed, so this module must work without
|
||||
// it and improve when it arrives. With no mapper a 3-letter code simply falls through to the records
|
||||
// label below — the formal name, but a real localized name.
|
||||
// That THROW-vs-ECHO asymmetry with the language type is also why every lookup here ends in null
|
||||
// rather than in a code: one failure is loud, the other silent, and null normalises both.
|
||||
const two = code.length === 2 ? code
|
||||
: (typeof opts.alpha2 === 'function' ? String(opts.alpha2(code) || '').toUpperCase() : '')
|
||||
if (two.length === 2) {
|
||||
try {
|
||||
const n = new Intl.DisplayNames([opts.locale || 'en'], { type: 'region' }).of(two)
|
||||
if (n && n !== two) return n
|
||||
} catch (_) { /* not a region Intl knows */ }
|
||||
}
|
||||
// The records' own label: always localized, always present, but the FORMAL form. Second choice, not
|
||||
// no choice.
|
||||
const fromRecords = opts.byCode?.get(code)
|
||||
if (fromRecords) return fromRecords
|
||||
// ★ NEVER RETURN THE CODE. "SDN + 3 more" on a poster looks like a data bug to a reader and, worse,
|
||||
// reads as a successful translation to a gate that only checks the string is non-empty. A null makes
|
||||
// the caller fall back to the existing label, which is English but is at least a WORD.
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The generated title for a zone, or null if this zone is not the kind that can be generated.
|
||||
*
|
||||
* @param {object} zone a clusters.json zone (needs .selector)
|
||||
* @param {object} strings the resolved locale block from packet-strings.json (per-KEY fallback to en
|
||||
* already applied by the caller, as packet.mjs does)
|
||||
* @param {object} opts as countryLabel, plus:
|
||||
* @param {number} [opts.maxParts=2] how many countries to name before collapsing to "+ n more".
|
||||
* 2 by default because these titles print on a poster header and in a legend row: three names
|
||||
* already wrap at the legend's 380px panel width in ar and hi, where the words are longer.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function zoneTitle(zone, strings, opts = {}) {
|
||||
const codes = zoneCountryCodes(zone?.selector)
|
||||
if (!codes.length) return null // tier C — declined, caller keeps its label
|
||||
const maxParts = Math.max(1, opts.maxParts ?? 2)
|
||||
|
||||
const named = codes.map((c) => countryLabel(c, opts))
|
||||
// ★ ALL-OR-NOTHING ON THE NAMED HEAD. If any of the countries we would SHOW has no name, decline the
|
||||
// whole title rather than printing a partial one: a title that names two of three countries and drops
|
||||
// the third silently misinforms, and it cannot be distinguished from a correct title downstream.
|
||||
const head = named.slice(0, maxParts)
|
||||
if (head.some((n) => !n)) return null
|
||||
|
||||
if (codes.length === 1) return head[0]
|
||||
if (codes.length <= maxParts) {
|
||||
// Fold left so three names read "a + b + c" using the same two-part joiner, rather than needing a
|
||||
// separate three-part string per locale.
|
||||
return head.reduce((acc, n) => fill(strings?.zone_join, { a: acc, b: n }) ?? `${acc} + ${n}`)
|
||||
}
|
||||
const rest = codes.length - maxParts
|
||||
const joined = head.reduce((acc, n) => fill(strings?.zone_join, { a: acc, b: n }) ?? `${acc} + ${n}`)
|
||||
return fill(strings?.zone_join_more, { a: joined, n: rest }) ?? `${joined} + ${rest} more`
|
||||
}
|
||||
|
||||
/** `{a}` / `{b}` / `{n}` interpolation. Returns null on a missing pattern so the caller can default. */
|
||||
function fill(pattern, params) {
|
||||
if (!pattern) return null
|
||||
return String(pattern).replace(/\{(\w+)\}/g, (m, k) => (params[k] != null ? String(params[k]) : m))
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO-3 → localized country label, from records the caller already has.
|
||||
* Built once per render rather than scanned per zone: 2107 records x 28 zones is 59k scans for data
|
||||
* that does not change during a render.
|
||||
*/
|
||||
export function countryLabelIndex(records) {
|
||||
const m = new Map()
|
||||
for (const r of records || []) {
|
||||
const cc = r?.country_code
|
||||
const label = r?.country_code_label
|
||||
if (cc && label && !m.has(cc)) m.set(String(cc).toUpperCase(), label)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
|
@ -612,14 +612,32 @@ useShadowStyles(
|
|||
// The detail panel is the legend's mirror image (it sits top-RIGHT in LTR), so it has to swap too or
|
||||
// the two would trade places and collide on the other side.
|
||||
'[dir="rtl"] .upg-detail-panel{right:auto!important;left:8px!important}' +
|
||||
// ⚠ AND THE ROWS THEMSELVES. dir on the wrapper does NOT reach content that sets its own direction,
|
||||
// and the legend's rows render zone names + numbers that must read right-to-left as a unit. Without
|
||||
// this the panel mirrors but its contents stay LTR — which is what the picture shows.
|
||||
'[dir="rtl"] .stl-panel,[dir="rtl"] .stl-panel *{direction:rtl}' +
|
||||
// Numbers stay LTR inside an RTL line: a count or a percentage is a left-to-right run even in Arabic,
|
||||
// and without isolation the bidi algorithm reorders a run that STARTS with a digit. Same fix the
|
||||
// packet needed for scripture references.
|
||||
'[dir="rtl"] .stl-panel .stl-count,[dir="rtl"] .stl-panel .stl-pop{direction:ltr;unicode-bidi:isolate}' +
|
||||
// ★ THE TWO LEGEND-CONTENT RULES THAT USED TO SIT HERE ARE DELETED (2026-09-27), and they were NOT
|
||||
// equally provisional — one was redundant and the other was DEAD, which is worth separating because
|
||||
// only one of them was ever doing anything.
|
||||
//
|
||||
// '[dir="rtl"] .stl-panel,[dir="rtl"] .stl-panel *{direction:rtl}'
|
||||
// REDUNDANT. It matched real elements (.stl-panel appears 69 times in SemanticTreeLegend) but the
|
||||
// library gets there on its own: it uses the :dir(rtl) PSEUDO-CLASS (:645-646), which reads the
|
||||
// INHERITED direction rather than needing a declaration, and CSS `direction` inherits across the
|
||||
// shadow boundary. rega measured the consequence at main 4ca2dcf9 (row 56b): Simple Research and
|
||||
// Prayer Progress, which carry NO override at all, compute direction rtl on their legend rows under
|
||||
// ?lang=ar. A rule whose effect is indistinguishable from its absence on two other profiles is not
|
||||
// load-bearing here either.
|
||||
//
|
||||
// '[dir="rtl"] .stl-panel .stl-count,[dir="rtl"] .stl-panel .stl-pop{direction:ltr;unicode-bidi:isolate}'
|
||||
// ⚠ DEAD CSS, and it always was. The library's count and population cells are `.stl-num` (:1314-1315).
|
||||
// `.stl-count` and `.stl-pop` DO NOT EXIST — `git grep` finds those two class names nowhere in
|
||||
// library/ or any other plugin, only inside this override. So this rule matched zero elements from the
|
||||
// day it was written, and the digit order rega verified ("694", "1,413" keeping their order under ar)
|
||||
// was always the library's doing, never mine.
|
||||
// ★ THE LESSON IS ABOUT THE EVIDENCE, NOT THE CSS: this rule was written to fix a real bidi problem,
|
||||
// was believed to have fixed it, and was cited as the reason the numbers read correctly — while
|
||||
// matching nothing. A style rule fails SILENTLY when its selector is wrong; it does not warn, and the
|
||||
// page looks right if something else is already handling it. The only way to know a rule works is to
|
||||
// check that its selector exists, which is a different act from looking at the rendered page.
|
||||
// (dx-shepherd UPGDEL authorised deleting both as provisional; recording here that one of the two was
|
||||
// never provisional but inert, so nobody re-adds it believing it once worked.)
|
||||
// ⚠ AND THE REOPEN BUTTON, which I missed on my first pass and repo caught. SemanticTreeLegend has
|
||||
// TWO physical `left:8px` rules, not one: :619 for the panel and :921 for .stl-reopen, the button that
|
||||
// brings the panel back after it is collapsed. Mirroring only the panel would leave the button on the
|
||||
|
|
@ -2192,10 +2210,10 @@ onMounted(async () => {
|
|||
// and it overlapped nothing: a promise created after an await starts only once that await
|
||||
// resolves, so the code read as parallel and behaved strictly serially.
|
||||
const boundaryPromises = {
|
||||
adm0: getJSON('world-adm0.geojson'),
|
||||
adm0: getJSON('world-adm0-web.geojson'),
|
||||
adm1Chn: getJSON('gb-CHN-ADM1.geojson'),
|
||||
// 1,628 KB, and only the India pg_slugs zones need it.
|
||||
adm1Ind: getJSON('gb-IND-ADM1.geojson'),
|
||||
adm1Ind: getJSON('gb-IND-ADM1-web.geojson'),
|
||||
}
|
||||
|
||||
// ── live records WITH rop1 + population (full record; LESSON L3 / slice 04u) ──
|
||||
|
|
|
|||
|
|
@ -193,14 +193,66 @@ function paneElement(pane) {
|
|||
// `lang` is the alias the page may carry; children read `locale`.
|
||||
if (inherited.locale === undefined && inherited.lang !== undefined) inherited.locale = inherited.lang
|
||||
const pcfg = { ...inherited, ...spec.value.config, ...pane.config }
|
||||
// ★ NO "TAP A COLOURED DOT" COACH-MARK ON THIS PROFILE, AT ALL (Driver, 2026-09-27).
|
||||
// ProfileLoader mounts PinHint by default and self-gates it on a map <canvas> existing, which is a
|
||||
// sound gate for the question it asks ("is there a map here?") and the WRONG question for this
|
||||
// profile. UPG-100's map is a POSTER PREVIEW: the reader is looking at what will be printed, and there
|
||||
// is no per-dot detail to open. So the pill invites a gesture that does nothing, and on the phone the
|
||||
// Driver's screenshots show it over a page with no coloured dots on it at all.
|
||||
// ⚠ THIS SUPERSEDES THE inert WORK FOR THIS PROFILE, and that is worth saying plainly rather than
|
||||
// quietly leaving both in: PILLRISK was "the pill shows over the posters", and I fixed WHERE it shows.
|
||||
// The Driver's answer is that it should not show HERE, full stop — a narrower and more correct claim
|
||||
// than mine. The inert signal stays because it is still right for its own reasons (a stacked pane
|
||||
// should not be in tab order or the a11y tree, and studio's PinHint change serves profiles that DO
|
||||
// want the pill), but it is no longer what keeps this pill off this app.
|
||||
// Opt-out via the contract that already existed — ProfileLoader.vue:150 reads
|
||||
// `config.pinHint !== false` — so this is a config value from my side, not a library change.
|
||||
pcfg.pinHint = false
|
||||
if (tk.value) pcfg.tk = tk.value
|
||||
el.setAttribute('profile-config', JSON.stringify(pcfg))
|
||||
return el
|
||||
}
|
||||
|
||||
// ★ TELL THE INVISIBLE PANE THAT IT IS INVISIBLE. `inert` on every pane except the active one.
|
||||
//
|
||||
// THE DEFECT, reported by the Driver and traced by repo (PILLRISK, 2026-09-27): PinHint's "Tap a
|
||||
// coloured dot" pill appeared OVER the Prayer Resources posters on a phone. Its IntersectionObserver
|
||||
// was working correctly — that is the point. The panes are `position:absolute; inset:0` in one
|
||||
// scroll-snap column, so the map pane is STACKED BEHIND the active view at full size: 100% of its
|
||||
// geometry is inside the viewport while none of it is visible. An IntersectionObserver measures
|
||||
// INTERSECTION, not OCCLUSION, and no amount of tuning it can distinguish those two states. The
|
||||
// missing information was never geometric — only the composite knows which pane the reader chose.
|
||||
//
|
||||
// ⚠ inert, NOT display:none OR v-show, and this is the constraint that decides the whole approach:
|
||||
// hiding the pane would tear down the live map. A MapLibre/Mapbox canvas in a zero-size container
|
||||
// loses its size and re-initialises on the way back, which is exactly the cold-start path rega had to
|
||||
// verify in row 52. inert leaves the pane mounted, painted and warm; it only declares it non-interactive
|
||||
// — and, unlike aria-hidden, also removes it from tab order and the accessibility tree, so a keyboard
|
||||
// user cannot tab into a map they cannot see. (studio's contract, END-STU-PILL: PinHint walks up through
|
||||
// shadow roots for inert/hidden/aria-hidden and watches that chain with a MutationObserver, so no event
|
||||
// is needed and switching views brings the pill straight back.)
|
||||
//
|
||||
// ★ CORRECT AT EVERY VIEWPORT, not just the phone. pane-wrapper renders ONE column with
|
||||
// `scroll-snap-type: y mandatory` and `.pane { height: 100% }`, so exactly one pane fills the stage on
|
||||
// any width — there is no side-by-side mode in v1 whose second pane would be wrongly marked. Verified
|
||||
// in pane-wrapper.js:56-59 + :101 rather than assumed from the phone report.
|
||||
function markActivePane(activeId) {
|
||||
if (!column) return
|
||||
for (const pane of column.panes) {
|
||||
const el = column.mountEl(pane.id)
|
||||
if (!el) continue
|
||||
if (pane.id === activeId) el.removeAttribute('inert')
|
||||
else el.setAttribute('inert', '')
|
||||
}
|
||||
}
|
||||
|
||||
function go(id) {
|
||||
view.value = id
|
||||
column?.scrollTo(id)
|
||||
// Immediately, not on the scroll callback: scrollTo is SMOOTH, so onScroll's 120ms debounce fires
|
||||
// only after the animation settles. Waiting for it would leave the pill visible over the posters for
|
||||
// the length of the scroll — a shorter version of the same bug.
|
||||
markActivePane(id)
|
||||
}
|
||||
|
||||
// Cross-pane action (shell seam): the poster wall dispatches a scoped window event
|
||||
|
|
@ -253,10 +305,21 @@ onMounted(() => {
|
|||
paneEls[pane.id] = el
|
||||
column.mountEl(pane.id)?.appendChild(el)
|
||||
}
|
||||
// ★ THE FRESH-LOAD CASE, which is the one the Driver actually hit. Without this the panes carry no
|
||||
// inert attribute at all until the reader touches the switcher, so the pill is over the posters on
|
||||
// ARRIVAL — before any view change has happened. A signal that only fires on transitions never
|
||||
// describes the initial state.
|
||||
markActivePane(column.current())
|
||||
|
||||
// Tabs are a readout as well as a control — follow the user's scroll.
|
||||
offScroll = column.onScroll(() => {
|
||||
const cur = column.current()
|
||||
if (cur && cur !== view.value) view.value = cur
|
||||
// Unconditionally, NOT inside the `cur !== view.value` guard above: go() has already set view.value
|
||||
// before this fires, so a switcher-driven scroll takes the early exit and would skip the marking. A
|
||||
// scroll-driven one needs it and a switcher-driven one is idempotent, so doing it always is both
|
||||
// cheaper to reason about and the only version that is correct on both paths.
|
||||
if (cur) markActivePane(cur)
|
||||
})
|
||||
window.addEventListener('doxa-upg-100-list:show-on-map', onShowOnMap)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ useShadowStyles(
|
|||
<div class="pdm-root">
|
||||
<div class="pdm-bar">
|
||||
<span class="pdm-brand">PPLR data maps</span>
|
||||
<div class="pdm-seg" role="tablist" aria-label="Choose a view">
|
||||
<div class="pdm-seg" role="tablist" :aria-label="t('pplr.ui.chooseView')">
|
||||
<button v-for="v in VIEWS" :key="v.key" role="tab" :aria-selected="view === v.key"
|
||||
:class="{ on: view === v.key }" @click="setView(v.key)">{{ t(v.tKey) }}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
* component into its toolbar.
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
|
||||
defineProps({
|
||||
/** Array of { label, url, fields[] } */
|
||||
|
|
@ -16,19 +17,20 @@ defineProps({
|
|||
})
|
||||
|
||||
const open = ref(false)
|
||||
const t = usePplrT()
|
||||
function toggle() { open.value = !open.value }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dsd-wrap">
|
||||
<button class="toolbar-btn round" type="button" title="Data sources" @click="toggle">
|
||||
<button class="toolbar-btn round" type="button" :title="t('pplr.ui.dataSources')" @click="toggle">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="open" class="dsd-panel">
|
||||
<div class="dsd-header">Data sources</div>
|
||||
<div class="dsd-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in sources" :key="src.url" class="dsd-src">
|
||||
<div class="dsd-src-label">{{ src.label }}</div>
|
||||
<div class="dsd-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
import { ref, computed } from 'vue'
|
||||
import { useInstance } from '../composables/usePplrInstance.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array, default: () => [] },
|
||||
|
|
@ -30,6 +31,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['select'])
|
||||
|
||||
const instance = useInstance()
|
||||
const t = usePplrT()
|
||||
const collapsed = ref(false)
|
||||
|
||||
const theme = computed(() => instance?.theme.value || 'dark')
|
||||
|
|
@ -77,7 +79,7 @@ function clearSel() {
|
|||
</svg>
|
||||
</button>
|
||||
<div class="fl-title">{{ title }}</div>
|
||||
<button v-if="selectedKey" class="fl-clear" title="Clear selection (Esc)" @click="clearSel">×</button>
|
||||
<button v-if="selectedKey" class="fl-clear" :title="t('pplr.ui.clearSelectionEsc')" @click="clearSel">×</button>
|
||||
</div>
|
||||
<div class="fl-rows" :class="{ 'has-sel': !!selectedKey }">
|
||||
<button
|
||||
|
|
@ -99,12 +101,12 @@ function clearSel() {
|
|||
<button v-if="collapsed"
|
||||
class="fl-reopen"
|
||||
:data-theme="theme"
|
||||
title="Open legend"
|
||||
:title="t('pplr.ui.openLegend')"
|
||||
@click="collapsed = false">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
|
||||
<polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Legend</span>
|
||||
<span>{{ t('pplr.ui.legend') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@
|
|||
* <LoadingGlobe v-if="!isReady" :label="loadingStage" @retry="retry" />
|
||||
*/
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: 'Loading map' },
|
||||
label: { type: String, default: '' }, // empty = the translated "Loading map"
|
||||
stuckAfterMs:{ type: Number, default: 30000 },
|
||||
})
|
||||
const emit = defineEmits(['retry'])
|
||||
const t = usePplrT()
|
||||
|
||||
const stuck = ref(false)
|
||||
let stuckTimer = null
|
||||
|
|
@ -56,13 +58,13 @@ function onRetry() {
|
|||
<line x1="6" y1="32" x2="58" y2="32" stroke="#73A17F" stroke-width="1.2" opacity="0.45"/>
|
||||
<line x1="32" y1="6" x2="32" y2="58" stroke="#73A17F" stroke-width="1.2" opacity="0.45"/>
|
||||
</svg>
|
||||
<span class="lg-text">{{ label }}</span>
|
||||
<span class="lg-text">{{ label || t('pplr.ui.loadingMap') }}</span>
|
||||
<button v-if="stuck" class="lg-retry" type="button" @click="onRetry">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-3-6.7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<polyline points="21 4 21 11 14 11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Retry
|
||||
{{ t('pplr.ui.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|||
import { useInstance } from '../composables/usePplrInstance.js'
|
||||
import { LOCALES, LOCALE_NAMES } from '../utils/pplrMap.js'
|
||||
import { loadNames } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** Optional override — usually read from instance.pinDetail */
|
||||
|
|
@ -30,6 +31,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['close', 'locate'])
|
||||
|
||||
const instance = useInstance()
|
||||
const tr = usePplrT() // not `t`: the names table below is already called t
|
||||
const pinData = computed(() => props.pin || instance?.pinDetail.value || null)
|
||||
|
||||
// All 17 locale names for this PG (n_en, n_hi, n_bn, …). Per user 2026-05-02:
|
||||
|
|
@ -59,12 +61,12 @@ const fields = computed(() => {
|
|||
const dash = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : v
|
||||
// Curated field order — most identifying first, registry IDs last
|
||||
return [
|
||||
{ label: 'Country', value: dash(p.country), mono: false },
|
||||
{ label: 'Religion', value: dash(p.religion), mono: false },
|
||||
{ label: 'Language', value: dash(p.language), mono: false },
|
||||
{ label: 'ISO-3 Country', value: dash(p.iso3), mono: true, hidden: !p.iso3 },
|
||||
{ label: 'Affinity Bloc', value: dash(p.rop1), mono: true, hidden: !p.rop1 },
|
||||
{ label: 'People Cluster', value: dash(p.rop2), mono: true, hidden: !p.rop2 },
|
||||
{ label: tr('pplr.popup.country'), value: dash(p.country), mono: false },
|
||||
{ label: tr('pplr.popup.religion'), value: dash(p.religion), mono: false },
|
||||
{ label: tr('pplr.popup.language'), value: dash(p.language), mono: false },
|
||||
{ label: tr('pplr.popup.iso3Country'), value: dash(p.iso3), mono: true, hidden: !p.iso3 },
|
||||
{ label: tr('pplr.popup.affinityBloc'), value: dash(p.rop1), mono: true, hidden: !p.rop1 },
|
||||
{ label: tr('pplr.popup.peopleCluster'), value: dash(p.rop2), mono: true, hidden: !p.rop2 },
|
||||
{ label: 'ROP25', value: dash(p.rop3), mono: true, hidden: !p.rop3 },
|
||||
{ label: 'PGIC GeoID', value: dash(p.rop25), mono: true, hidden: !p.rop25 },
|
||||
{ label: 'PEID', value: dash(p.peid), mono: true, hidden: !p.peid },
|
||||
|
|
@ -98,8 +100,8 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
|||
|
||||
<!-- Title + close -->
|
||||
<div class="pdc-header">
|
||||
<span class="pdc-eyebrow">People Group</span>
|
||||
<button class="pdc-close" title="Close (Esc)" @click="close">
|
||||
<span class="pdc-eyebrow">{{ tr('pplr.popup.peopleGroup') }}</span>
|
||||
<button class="pdc-close" :title="tr('pplr.ui.closeEsc')" @click="close">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 2L10 10M10 2L2 10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
|
@ -117,7 +119,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
|||
</div>
|
||||
|
||||
<div class="pdc-section-divider">
|
||||
<span>Name in 17 locales</span>
|
||||
<span>{{ tr('pplr.card.nameInLocales', { n: locales.length }) }}</span>
|
||||
</div>
|
||||
<div v-for="loc in locales" :key="loc.code" class="pdc-field pdc-locale">
|
||||
<span class="pdc-field-label">{{ loc.name }} <code>{{ loc.code }}</code></span>
|
||||
|
|
@ -134,9 +136,9 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
|||
<path d="M12 21s7-7 7-12a7 7 0 1 0-14 0c0 5 7 12 7 12z" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="12" cy="9" r="2.5" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
Fly to pin
|
||||
{{ tr('pplr.ui.flyToPin') }}
|
||||
</button>
|
||||
<button class="pdc-btn" @click="close">Close</button>
|
||||
<button class="pdc-btn" @click="close">{{ tr('pplr.ui.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const A11Y_EN = {
|
|||
'legend.loadingRows': 'Loading {label}…', 'legend.loadingData': 'Loading data…',
|
||||
'legend.noDataAtThisLevel': 'No data at this level', 'legend.showingTop': 'Showing top {n} of {m}',
|
||||
'legend.useGeocoderHint': 'use the geocoder above to find the rest',
|
||||
'legend.sortByPop': 'Sort by Population',
|
||||
}
|
||||
const fillA11y = (str, p) => String(str).replace(/\{(\w+)\}/g, (m, k) => (p && k in p ? p[k] : m))
|
||||
let ta = (key, p) => fillA11y(A11Y_EN[key] ?? key, p)
|
||||
|
|
@ -441,7 +442,7 @@ function rowClasses(row) {
|
|||
</button>
|
||||
<button v-if="hasPop" class="stl-hdr-num"
|
||||
:class="{ active: sortBy === 'pop' }"
|
||||
title="Sort by Population"
|
||||
:title="ta('legend.sortByPop')"
|
||||
@click="clickSortHeader('pop')">
|
||||
Pop
|
||||
<span v-if="sortBy === 'pop'" class="stl-sort-arrow">{{ sortDir === 'desc' ? '▼' : '▲' }}</span>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const LOCALE_NAMES = {
|
|||
|
||||
// ── PPLR FeatureServer — country polygons (live, not bundled) ────────────────
|
||||
import { dataUrl, glFor } from './pplrData.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
export const COUNTRY_FS_URL = 'https://services2.arcgis.com/S4ydGgujXcif36k3/arcgis/rest/services/aCountry/FeatureServer/0/query'
|
||||
export const COUNTRY_FS_FIELDS = ['GENCalpha3', 'ISOalpha2', 'CtryNm', 'ctryNmDisp']
|
||||
|
|
@ -332,17 +333,20 @@ export const escHtml = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&':
|
|||
|
||||
export function popupForPG(p, extraRows = '') {
|
||||
const dash = v => (v == null || v === 'None' || v === '' ? '<span class="m">—</span>' : escHtml(v))
|
||||
// Labels in the page's language (tStatic falls back to English). Registry names (ROP3, ROP25,
|
||||
// JoshuaProject, IMB People, WCD) are identifiers and stay as they are.
|
||||
const L = (k) => escHtml(tStatic('pplr.popup.' + k))
|
||||
return `<div class="pplr-popup">
|
||||
<h3>${p.name ? escHtml(p.name) : '(no name)'}</h3>
|
||||
<h3>${p.name ? escHtml(p.name) : L('noName')}</h3>
|
||||
<table>
|
||||
<tr><td>Country</td><td>${dash(p.country)} <code>${dash(p.iso3)}</code></td></tr>
|
||||
<tr><td>${L('country')}</td><td>${dash(p.country)} <code>${dash(p.iso3)}</code></td></tr>
|
||||
<tr><td>ROP3</td><td><code>${dash(p.rop3)}</code></td></tr>
|
||||
<tr><td>ROP25 (Ethne)</td><td><code>${dash(p.rop25)}</code></td></tr>
|
||||
<tr><td>JoshuaProject</td><td><code>${dash(p.jp)}</code></td></tr>
|
||||
<tr><td>IMB People</td><td><code>${dash(p.imb)}</code></td></tr>
|
||||
<tr><td>WCD</td><td><code>${dash(p.wcd)}</code></td></tr>
|
||||
<tr><td>Religion</td><td>${dash(p.religion)}</td></tr>
|
||||
<tr><td>Language</td><td>${dash(p.language)}</td></tr>
|
||||
<tr><td>${L('religion')}</td><td>${dash(p.religion)}</td></tr>
|
||||
<tr><td>${L('language')}</td><td>${dash(p.language)}</td></tr>
|
||||
${extraRows}
|
||||
</table>
|
||||
</div>`
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ onMounted(async () => {
|
|||
const html = `<div class="pplr-popup">
|
||||
<h3>${escHtml(p.CtryNm || p.ctryNmDisp || '—')}<small>${escHtml(p.GENCalpha3 || p.ISOalpha2 || '?')}</small></h3>
|
||||
<table>
|
||||
<tr><td>PG count</td><td><b>${escHtml(p.pgCount || 0)}</b></td></tr>
|
||||
<tr><td>Avg locales filled</td><td>
|
||||
<tr><td>${escHtml(t('pplr.popup.pgCount'))}</td><td><b>${escHtml(p.pgCount || 0)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.avgLocalesFilled'))}</td><td>
|
||||
<b>${p.avgFilled != null ? p.avgFilled.toFixed(1) : '—'}</b> / 17</td></tr>
|
||||
</table>
|
||||
</div>`
|
||||
|
|
@ -148,9 +148,9 @@ const COV_POPUP_CSS = POPUP_CSS + `
|
|||
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">Translation coverage · choropleth = avg locales filled per country · click PG pin or country</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.coverage') }}</div>
|
||||
<div class="legend">
|
||||
<div class="title">Avg locales / 17</div>
|
||||
<div class="title">{{ t('pplr.legendTitle.avgLocales', { n: 17 }) }}</div>
|
||||
<div class="bar">
|
||||
<div v-for="s in SCALE" :key="s.stop" class="seg" :style="{ background: s.color }">
|
||||
<span v-if="s.label">{{ s.label }}</span>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ onMounted(async () => {
|
|||
|
||||
<template>
|
||||
<div class="profile">
|
||||
<LoadingGlobe v-if="!tree" label="Loading ROP hierarchy" @retry="reload" />
|
||||
<LoadingGlobe v-if="!tree" :label="t('pplr.status.loadingRop')" @retry="reload" />
|
||||
<template v-else>
|
||||
<SemanticSunburst
|
||||
:tree="tree"
|
||||
|
|
@ -74,18 +74,18 @@ onMounted(async () => {
|
|||
:depth-labels="['','Affinity Bloc','People Cluster','PG (not rendered)']"
|
||||
:max-depth="2"
|
||||
/>
|
||||
<div class="header-pill">ROP Hierarchy · Affinity Bloc → People Cluster · scroll to zoom</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.sunburst') }}</div>
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="map-toolbar">
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<div class="settings-header">Data sources</div>
|
||||
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
|
||||
<div class="settings-src-label">{{ src.label }}</div>
|
||||
<div class="settings-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -415,10 +415,10 @@ function popupHtml(p) {
|
|||
return `<div class="pplr-popup">
|
||||
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
|
||||
<table>
|
||||
<tr><td>Language</td><td>${d(p.language)}</td></tr>
|
||||
<tr><td>Family</td><td><b style="color:${color}">${d(p.fam)}</b></td></tr>
|
||||
<tr><td>Affinity Bloc</td><td>${d(p.rop1)}</td></tr>
|
||||
<tr><td>Religion</td><td>${d(p.religion)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.family'))}</td><td><b style="color:${color}">${d(p.fam)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.affinityBloc'))}</td><td>${d(p.rop1)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td>${d(p.religion)}</td></tr>
|
||||
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
|
||||
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
|
||||
</table>
|
||||
|
|
@ -522,7 +522,7 @@ const LF_CSS = POPUP_CSS + `
|
|||
:local-index="langLocalIndex"
|
||||
:kind-labels="LANG_KIND_LABELS"
|
||||
:mapbox-token="mapboxToken"
|
||||
placeholder="Search by people, place, language, religion…"
|
||||
:placeholder="t('pplr.ui.search')"
|
||||
@place="onGeocoderPlace"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -539,13 +539,13 @@ const LF_CSS = POPUP_CSS + `
|
|||
v-else
|
||||
:nodes="langTree"
|
||||
:tabs="LANG_TABS"
|
||||
title="Language Families"
|
||||
:title="t('pplr.tabs.languageFamilies')"
|
||||
@select="onLegendSelect"
|
||||
/>
|
||||
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">Ethnolinguistic families (PPLR) · click pin for detail</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.languageFamilies') }}</div>
|
||||
|
||||
<!-- Toolbar — rounded pill controls -->
|
||||
<div class="map-toolbar">
|
||||
|
|
@ -564,28 +564,28 @@ const LF_CSS = POPUP_CSS + `
|
|||
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
|
||||
</button>
|
||||
<div class="basemap-wrap">
|
||||
<button class="toolbar-btn round" title="Base map" @click="showBasemap = !showBasemap">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')" @click="showBasemap = !showBasemap">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M3 12l9 4 9-4M3 17l9 4 9-4" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showBasemap" class="basemap-panel">
|
||||
<div class="basemap-title">Base map</div>
|
||||
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
|
||||
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt"
|
||||
:class="{ active: currentBasemap === b.key }"
|
||||
@click="setBasemap(b.key)">{{ b.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<div class="settings-header">Data sources</div>
|
||||
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
|
||||
<div class="settings-src-label">{{ src.label }}</div>
|
||||
<div class="settings-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -68,18 +68,18 @@ onBeforeUnmount(() => destroy())
|
|||
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">All PG-in-country pins · click for cross-system IDs</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.pins') }}</div>
|
||||
|
||||
<div class="map-toolbar">
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<div class="settings-header">Data sources</div>
|
||||
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
|
||||
<div class="settings-src-label">{{ src.label }}</div>
|
||||
<div class="settings-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -236,8 +236,8 @@ function popupHtml(p) {
|
|||
return `<div class="pplr-popup">
|
||||
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
|
||||
<table>
|
||||
<tr><td>Religion</td><td><b style="color:${grp?.color || '#ccc'}">${d(p.religion)}</b></td></tr>
|
||||
<tr><td>Language</td><td>${d(p.language)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td><b style="color:${grp?.color || '#ccc'}">${d(p.religion)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
|
||||
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
|
||||
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
|
||||
</table>
|
||||
|
|
@ -294,7 +294,7 @@ const REL_CSS = POPUP_CSS + `
|
|||
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">Religion distribution · click pin for detail</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.religion') }}</div>
|
||||
|
||||
<!-- Decoupled geocoder — searches religions + countries + places -->
|
||||
<div class="geocoder-slot">
|
||||
|
|
@ -302,7 +302,7 @@ const REL_CSS = POPUP_CSS + `
|
|||
:local-index="religionLocalIndex"
|
||||
:kind-labels="REL_KIND_LABELS"
|
||||
:mapbox-token="mapboxToken"
|
||||
placeholder="Search by people, place, language, religion…"
|
||||
:placeholder="t('pplr.ui.search')"
|
||||
@place="onGeocoderPlace"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -318,7 +318,7 @@ const REL_CSS = POPUP_CSS + `
|
|||
<FlatLegend
|
||||
v-else
|
||||
:items="religionLegendItems"
|
||||
title="Primary Religion"
|
||||
:title="t('pplr.ui.primaryReligion')"
|
||||
@select="onLegendSelect"
|
||||
/>
|
||||
|
||||
|
|
@ -339,27 +339,27 @@ const REL_CSS = POPUP_CSS + `
|
|||
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
|
||||
</button>
|
||||
<div class="basemap-wrap">
|
||||
<button class="toolbar-btn round" title="Base map" @click="showBasemap = !showBasemap">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')" @click="showBasemap = !showBasemap">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M3 12l9 4 9-4M3 17l9 4 9-4" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showBasemap" class="basemap-panel">
|
||||
<div class="basemap-title">Base map</div>
|
||||
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
|
||||
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt" :class="{ active: currentBasemap === b.key }" @click="setBasemap(b.key)">{{ b.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data sources — moved into the toolbar to match ROP-tree (per user 2026-05-02) -->
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showSettings" class="settings-panel toolbar-attached">
|
||||
<div class="settings-header">Data sources</div>
|
||||
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
|
||||
<div class="settings-src-label">{{ src.label }}</div>
|
||||
<div class="settings-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -380,14 +380,14 @@ function popupHtml(p) {
|
|||
return `<div class="pplr-popup">
|
||||
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
|
||||
<table>
|
||||
<tr><td>Affinity Bloc</td><td><b>${d(p.rop1)}</b></td></tr>
|
||||
<tr><td>Cluster</td><td><b>${d(p.rop2)}</b></td></tr>
|
||||
<tr><td>People Group</td><td><b>${d(p.rop3)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.affinityBloc'))}</td><td><b>${d(p.rop1)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.cluster'))}</td><td><b>${d(p.rop2)}</b></td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.peopleGroup'))}</td><td><b>${d(p.rop3)}</b></td></tr>
|
||||
<tr><td>PEID</td><td><code>${d(p.peid)}</code></td></tr>
|
||||
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
|
||||
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
|
||||
<tr><td>Religion</td><td>${d(p.religion)}</td></tr>
|
||||
<tr><td>Language</td><td>${d(p.language)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td>${d(p.religion)}</td></tr>
|
||||
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
|
||||
</table>
|
||||
</div>`
|
||||
}
|
||||
|
|
@ -491,7 +491,7 @@ const TREE_POPUP_CSS = POPUP_CSS + `
|
|||
:local-index="localIndex"
|
||||
:kind-labels="KIND_LABELS"
|
||||
:mapbox-token="mapboxToken"
|
||||
placeholder="Search by people, place, language, religion…"
|
||||
:placeholder="t('pplr.ui.search')"
|
||||
@place="onGeocoderPlace"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -508,13 +508,13 @@ const TREE_POPUP_CSS = POPUP_CSS + `
|
|||
v-else
|
||||
:nodes="ropTree"
|
||||
:tabs="ROP_TABS"
|
||||
title="ROP Hierarchy"
|
||||
:title="t('pplr.ui.ropHierarchy')"
|
||||
@select="onRopLegendSelect"
|
||||
/>
|
||||
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">ROP Hierarchy · Affinity Bloc → Cluster → People Group → PGIC</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.ropTree') }}</div>
|
||||
|
||||
<!-- Toolbar — rounded pill controls -->
|
||||
<div class="map-toolbar">
|
||||
|
|
@ -533,7 +533,7 @@ const TREE_POPUP_CSS = POPUP_CSS + `
|
|||
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
|
||||
</button>
|
||||
<div class="basemap-wrap">
|
||||
<button class="toolbar-btn round" title="Base map"
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')"
|
||||
@click="showBasemap = !showBasemap">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
|
|
@ -541,21 +541,21 @@ const TREE_POPUP_CSS = POPUP_CSS + `
|
|||
</svg>
|
||||
</button>
|
||||
<div v-if="showBasemap" class="basemap-panel">
|
||||
<div class="basemap-title">Base map</div>
|
||||
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
|
||||
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt"
|
||||
:class="{ active: currentBasemap === b.key }"
|
||||
@click="setBasemap(b.key)">{{ b.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
||||
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<div class="settings-header">Data sources</div>
|
||||
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
|
||||
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
|
||||
<div class="settings-src-label">{{ src.label }}</div>
|
||||
<div class="settings-src-url">{{ src.url }}</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
|||
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
|
||||
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS, escHtml } from '../utils/pplrMap.js'
|
||||
import LoadingGlobe from '../components/LoadingGlobe.vue'
|
||||
import DataSourcesDropdown from '../components/DataSourcesDropdown.vue'
|
||||
|
||||
|
|
@ -123,11 +123,11 @@ onMounted(async () => {
|
|||
].filter(Boolean)
|
||||
|
||||
const rosettaRow = sample
|
||||
? `<tr><td>Rosetta ★</td><td><b style="color:#d97706">★ sample PG</b></td></tr>`
|
||||
? `<tr><td>Rosetta ★</td><td><b style="color:#d97706">${escHtml(t('pplr.rosetta.samplePg'))}</b></td></tr>`
|
||||
: ''
|
||||
const missingNote = missingIds.length
|
||||
? `<tr><td colspan="2" style="padding-top:6px;font-size:10px;color:#6b7280;font-style:italic">
|
||||
${missingIds.join(', ')} ID${missingIds.length > 1 ? 's' : ''} not yet cross-mapped in PPLR — empty rows are a data gap, not an error.
|
||||
${escHtml(t('pplr.rosetta.missingIds', { ids: missingIds.join(', ') }))}
|
||||
</td></tr>`
|
||||
: ''
|
||||
new (glFor(map.value)).Popup({ maxWidth: '360px' })
|
||||
|
|
@ -154,11 +154,11 @@ onMounted(async () => {
|
|||
onBeforeUnmount(() => destroy())
|
||||
|
||||
const LEGEND = [
|
||||
{ color: '#fbbf24', label: 'JP + IMB + WCD all' },
|
||||
{ color: '#fbbf24', label: t('pplr.rosetta.legend.all') },
|
||||
{ color: '#3b82f6', label: 'JP + IMB' },
|
||||
{ color: '#a78bfa', label: 'IMB only' },
|
||||
{ color: '#ec4899', label: 'JP only' },
|
||||
{ color: '#6b7280', label: 'neither' },
|
||||
{ color: '#a78bfa', label: t('pplr.rosetta.legend.imbOnly') },
|
||||
{ color: '#ec4899', label: t('pplr.rosetta.legend.jpOnly') },
|
||||
{ color: '#6b7280', label: t('pplr.rosetta.legend.neither') },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
|
@ -167,9 +167,9 @@ const LEGEND = [
|
|||
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
|
||||
<div ref="mapContainer" class="map" />
|
||||
<div class="status">{{ status }}</div>
|
||||
<div class="header-pill">Cross-system ID Rosetta · color = which IDs exist · ★ = sample row</div>
|
||||
<div class="header-pill">{{ t('pplr.pill.rosetta') }}</div>
|
||||
<div class="legend">
|
||||
<div class="title">ID coverage</div>
|
||||
<div class="title">{{ t('pplr.legendTitle.idCoverage') }}</div>
|
||||
<div v-for="l in LEGEND" :key="l.label" class="row">
|
||||
<span class="dot" :style="{ background: l.color }" />{{ l.label }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ function addLayers() {
|
|||
// Add EMPTY first, then setData after the fetch: the addLayer calls below must run
|
||||
// SYNCHRONOUSLY to keep these fills beneath the pins (layer order is insertion order).
|
||||
m.addSource(SRC_COUNTRIES, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
fetch(`${resolveAssetsBase(profileConfig?.value)}/boundaries/world-adm0.geojson`)
|
||||
fetch(`${resolveAssetsBase(profileConfig?.value)}/boundaries/world-adm0-web.geojson`)
|
||||
.then((r) => r.json())
|
||||
.then((gj) => {
|
||||
for (const f of gj.features || []) {
|
||||
|
|
|
|||
|
|
@ -834,7 +834,7 @@ function addLayers() {
|
|||
// keep this layer underneath the pins (layer order is insertion order). An awaited fetch
|
||||
// here would put the choropleth on TOP of every bubble.
|
||||
m.addSource('rd-countries', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
fetch(`${resolveAssetsBase(profileConfig.value)}/boundaries/world-adm0.geojson`)
|
||||
fetch(`${resolveAssetsBase(profileConfig.value)}/boundaries/world-adm0-web.geojson`)
|
||||
.then((r) => r.json())
|
||||
.then((gj) => {
|
||||
for (const f of gj.features || []) {
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ useShadowStyles(`
|
|||
<input class="cpt-search" type="search" :value="store.search.value" @input="onSearch"
|
||||
:placeholder="$t('neighbor.searchCockpit')" :aria-label="$t('neighbor.search')" />
|
||||
<span class="cpt-counter">
|
||||
<b>{{ store.counts.value.senders.toLocaleString() }}</b> senders →
|
||||
<b>{{ store.counts.value.clusters.toLocaleString() }}</b> clusters · {{ store.counts.value.hidden }} hidden
|
||||
<b>{{ store.counts.value.senders.toLocaleString() }}</b> {{ $t('neighbor.cockpit.senders') }}
|
||||
<b>{{ store.counts.value.clusters.toLocaleString() }}</b> {{ $t('neighbor.cockpit.clustersHidden', { n: store.counts.value.hidden }) }}
|
||||
</span>
|
||||
<span class="cpt-prov" :class="{ err: store.provenance.value.apiError }"
|
||||
:title="store.provenance.value.reachedFilter">
|
||||
|
|
@ -163,8 +163,8 @@ useShadowStyles(`
|
|||
<div class="cpt-cell map"><MapCell /></div>
|
||||
<div class="cpt-cell flower"><FlowerCell /></div>
|
||||
<div class="cpt-seg" role="tablist" :aria-label="$t('neighbor.cockpit.viewSwitch')">
|
||||
<button :class="{ on: store.mode.value === 'map' }" @click="setMode('map')">MAP</button>
|
||||
<button :class="{ on: store.mode.value === 'flower' }" @click="setMode('flower')">FLOWER</button>
|
||||
<button :class="{ on: store.mode.value === 'map' }" @click="setMode('map')">{{ $t('neighbor.cockpit.map') }}</button>
|
||||
<button :class="{ on: store.mode.value === 'flower' }" @click="setMode('flower')">{{ $t('neighbor.cockpit.flower') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,13 +177,13 @@ useShadowStyles(`
|
|||
<!-- LEGEND RIBBON (one shared key for all four cells) -->
|
||||
<div class="cpt-legend">
|
||||
<span class="cpt-lg"><span class="cpt-sw" :style="{ background: COLORS.RED }"></span> {{ $t('neighbor.cockpit.unreachedTarget') }}</span>
|
||||
<span class="cpt-lg">reached %
|
||||
<span class="cpt-lg">{{ $t('neighbor.strip.pctReached') }}
|
||||
<span class="cpt-ramp">
|
||||
<i v-for="(g, i) in COLORS.GREEN_BINS" :key="i" :style="{ background: g }"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="cpt-lg"><span class="cpt-line" :style="{ borderColor: COLORS.NAVY }"></span> SEND (solid=same-lang · dashed=cluster · dotted=in-country)</span>
|
||||
<span class="cpt-lg"><span class="cpt-sw" :style="{ background: COLORS.GOLD }"></span> selection · pop = log radius · Luke 10:2</span>
|
||||
<span class="cpt-lg"><span class="cpt-line" :style="{ borderColor: COLORS.NAVY }"></span> {{ $t('neighbor.cockpit.legendSend') }}</span>
|
||||
<span class="cpt-lg"><span class="cpt-sw" :style="{ background: COLORS.GOLD }"></span> {{ $t('neighbor.cockpit.legendSelection') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- loading / error overlay -->
|
||||
|
|
|
|||
|
|
@ -395,8 +395,8 @@ useShadowStyles(
|
|||
|
||||
<!-- ring legend -->
|
||||
<div v-if="placed" class="cf-rings">
|
||||
<b>inner</b> same-country · <b>petals</b> cross-border<br>
|
||||
navy = SEND · <span :style="{ color: COLORS.GOLD }">gold</span> = same-language mesh
|
||||
{{ $t('neighbor.fm.whorl') }}<br>
|
||||
{{ $t('neighbor.cockpit.navySend') }} · <span :style="{ color: COLORS.GOLD }">{{ $t('neighbor.cockpit.goldMesh') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -328,9 +328,9 @@ useShadowStyles(`
|
|||
`, 'flower-clusters')
|
||||
|
||||
const MODES = [
|
||||
{ key: 'family', label: 'Family', sz: 'fewer · bigger' },
|
||||
{ key: 'language', label: 'Language', sz: 'more' },
|
||||
{ key: 'dialect', label: 'Dialect', sz: 'most' }
|
||||
{ key: 'family', tKey: 'neighbor.fc.mode.family', szKey: 'neighbor.fc.size.fewerBigger' },
|
||||
{ key: 'language', tKey: 'neighbor.fc.mode.language', szKey: 'neighbor.fc.size.more' },
|
||||
{ key: 'dialect', tKey: 'neighbor.fc.mode.dialect', szKey: 'neighbor.fc.size.most' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
|
@ -343,9 +343,9 @@ const MODES = [
|
|||
<div class="fc-modes" role="tablist" :aria-label="$t('neighbor.fc.clusterBy')">
|
||||
<button v-for="md in MODES" :key="md.key" class="fc-mode" :class="{ on: mode === md.key }"
|
||||
role="tab" :aria-selected="mode === md.key" @click="setMode(md.key)">
|
||||
<span class="fc-circle">{{ md.label.slice(0,3).toUpperCase() }}</span>
|
||||
<span class="fc-mode-lbl">{{ md.label }}</span>
|
||||
<span class="fc-mode-sz">{{ md.sz }}</span>
|
||||
<span class="fc-circle">{{ md.key.slice(0,3).toUpperCase() }}</span>
|
||||
<span class="fc-mode-lbl">{{ $t(md.tKey) }}</span>
|
||||
<span class="fc-mode-sz">{{ $t(md.szKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,12 +63,12 @@ const hiddenCount = computed(() => counts.value.hidden || 0)
|
|||
|
||||
// ── sort options ──────────────────────────────────────────────────────────────
|
||||
const SORTS = [
|
||||
{ key: 'mobilization', label: 'Mobilization' },
|
||||
{ key: 'pct', label: '%Christian' },
|
||||
{ key: 'pop', label: 'Population' },
|
||||
{ key: 'bridge', label: 'Bridge' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'frontier', label: 'Frontier first' }
|
||||
{ key: 'mobilization', tKey: 'neighbor.list.sortMobilization' },
|
||||
{ key: 'pct', tKey: 'neighbor.list.colPctChristian' },
|
||||
{ key: 'pop', tKey: 'neighbor.list.colPopulation' },
|
||||
{ key: 'bridge', tKey: 'neighbor.strip.bridgeType' },
|
||||
{ key: 'name', tKey: 'neighbor.list.colName' },
|
||||
{ key: 'frontier', tKey: 'neighbor.list.sortFrontier' }
|
||||
]
|
||||
function pickSort(key) { store.setSort(key) }
|
||||
|
||||
|
|
@ -303,7 +303,7 @@ useShadowStyles(`
|
|||
:class="{ active: sortKey === s.key, frontier: s.key === 'frontier' }"
|
||||
type="button"
|
||||
@click="pickSort(s.key)"
|
||||
>{{ s.label }}</button>
|
||||
>{{ $t(s.tKey) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ useShadowStyles(`
|
|||
class="cpt-l-info"
|
||||
:class="{ on: pinnedInfo === r.id }"
|
||||
type="button"
|
||||
title="Details"
|
||||
:title="$t('neighbor.cockpit.details')"
|
||||
@click.stop="toggleInfo(r.id)"
|
||||
>i</button>
|
||||
</div>
|
||||
|
|
@ -374,12 +374,12 @@ useShadowStyles(`
|
|||
<dt>{{ $t('neighbor.list.colPeopleGroup') }}</dt><dd>{{ r.name }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colCountry') }}</dt><dd>{{ r.country }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colLanguage') }}</dt><dd>{{ r.lang }}<span v-if="r.rol3"> · {{ r.rol3 }}</span></dd>
|
||||
<dt>%Christian</dt><dd>{{ pctLabel(r.pct) }} · {{ binLabel(r.pct) }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colPctChristian') }}</dt><dd>{{ pctLabel(r.pct) }} · {{ binLabel(r.pct) }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colPopulation') }}</dt><dd>{{ fmtPop(r.pop) }}</dd>
|
||||
<dt>Bridge</dt><dd>{{ r.bridge }}</dd>
|
||||
<dt>Adjacency</dt><dd>{{ r.adjacency }}</dd>
|
||||
<dt>{{ $t('neighbor.strip.bridgeType') }}</dt><dd>{{ r.bridge }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colAdjacency') }}</dt><dd>{{ r.adjacency }}</dd>
|
||||
<dt>{{ $t('neighbor.list.colReligion') }}</dt><dd>{{ r.religion_bg || r.religionFamily }}</dd>
|
||||
<dt v-if="r.code">Code</dt><dd v-if="r.code">{{ r.code }}</dd>
|
||||
<dt v-if="r.code">{{ $t('neighbor.list.colCode') }}</dt><dd v-if="r.code">{{ r.code }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-JoGdbQlwjgq9+dOQy3eDJ1DjLwyZ6VbclEci++67/f8faF0HOa1ox10mkNn1fj73" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-3rrykWo456uGE2YmJ/i7AeehqxCWQe0bVDjDOj5VEwaPZV5pFw4iegauoNaZMyDR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
36
public/dx-maps/assets/boundaries/gb-IND-ADM1-web.geojson
Normal file
36
public/dx-maps/assets/boundaries/gb-IND-ADM1-web.geojson
Normal file
File diff suppressed because one or more lines are too long
164
public/dx-maps/assets/boundaries/world-adm0-web.geojson
Normal file
164
public/dx-maps/assets/boundaries/world-adm0-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",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-Yyfio5ozzXMi/atFe+yX9p275iNd8nHRt72eiac+wqcE7pDa4ZJdOWPdb69snAkV" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-Yyfio5ozzXMi/atFe+yX9p275iNd8nHRt72eiac+wqcE7pDa4ZJdOWPdb69snAkV" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-Yyfio5ozzXMi/atFe+yX9p275iNd8nHRt72eiac+wqcE7pDa4ZJdOWPdb69snAkV" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-07lCkgnIAJa8y6BMWEnw2cKXpHnC2gnfNaM8v1dZvvVXS+rF/dqV6ZH4ULXQrVsO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-xwzODwX387yfNONPEKpKzvyroMMl53Q2sNDSad+GSend5mDN4FG4BWR0OIQNjdty" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-eIqFr2k0+NvcvJiIuMymyN88gxKXYH/DQwOeQhcHwjnh4Fp1j3xV8AmO52OC/CZo" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue